feat: install and update msd from git release tags, not npm
MSD is not published to npm, so /msd-update and the SessionStart update check could never find a release. Releases are now vX.Y.Z git tags plus a stable branch: - check-latest-version reads release tags via git ls-remote against the baked repository URL (latest = highest stable tag, next = incl. -rc) - /msd-update reruns the bootstrap installer pinned to the checked tag and reads the changelog from that tag - bootstrap.sh defaults to the stable branch and supports --local/--global - package identity: changelog URL on stable, manual install via msd.golem15.com - README quickstart and update how-to describe the one-line installer - track the repo-root CLAUDE.md Emitted-Drift-Ack-Growth: update.md — the update flow now explains the git-tag release lookup and pins the bootstrap installer to the checked tag, replacing the npm/npx wording
This commit is contained in:
@@ -30,13 +30,13 @@ const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { runHook: runHookSeam } = require('./helpers/process-seam.cjs');
|
||||
const { createTempDir, cleanup } = require('./helpers.cjs');
|
||||
const { NPM_VIEW_TIMEOUT_MS } = require('../msd-core/bin/check-latest-version.cjs');
|
||||
const { LOOKUP_TIMEOUT_MS } = require('../msd-core/bin/check-latest-version.cjs');
|
||||
|
||||
const WORKER_PATH = path.join(__dirname, '..', 'hooks', 'msd-check-update-worker.js');
|
||||
|
||||
// #4091/2026-09-06 Windows CI incident: the worker's real `npm view` call
|
||||
// (checkLatestVersion, msd-core/bin/check-latest-version.cjs) is bounded at
|
||||
// NPM_VIEW_TIMEOUT_MS. This outer test harness timeout used to be hardcoded
|
||||
// LOOKUP_TIMEOUT_MS. This outer test harness timeout used to be hardcoded
|
||||
// to the SAME 15000ms, so a slow registry response raced two SIGKILLs at the
|
||||
// exact same wall-clock instant: the worker's own inner npm-view timeout
|
||||
// fires and it needs real time to catch that failure, build a degraded
|
||||
@@ -124,7 +124,7 @@ describe('msd-check-update-worker.js: atomic cache publish (#4091)', () => {
|
||||
MSD_PROJECT_VERSION_FILE: path.join(cacheDir, 'no-such-project', 'VERSION'),
|
||||
MSD_GLOBAL_VERSION_FILE: path.join(cacheDir, 'no-such-global', 'VERSION'),
|
||||
};
|
||||
const r = runHookSeam(WORKER_PATH, [], { env, timeoutMs: NPM_VIEW_TIMEOUT_MS + WORKER_TEARDOWN_MARGIN_MS });
|
||||
const r = runHookSeam(WORKER_PATH, [], { env, timeoutMs: LOOKUP_TIMEOUT_MS + WORKER_TEARDOWN_MARGIN_MS });
|
||||
assert.equal(r.exitCode, 0, `worker must exit 0; stderr: ${r.stderr}`);
|
||||
const cache = JSON.parse(fs.readFileSync(cacheFile, 'utf8'));
|
||||
assert.equal(cache.installed, '0.0.0', 'worker record replaced the pre-existing one');
|
||||
@@ -132,16 +132,16 @@ describe('msd-check-update-worker.js: atomic cache publish (#4091)', () => {
|
||||
assert.deepEqual(residue, [], 'no temp stage files may remain after a successful publish');
|
||||
});
|
||||
|
||||
test('outer worker-run timeout keeps real margin beyond the inner npm-view timeout (#4091 exact-tie race)', () => {
|
||||
test('outer worker-run timeout keeps real margin beyond the inner lookup timeout (#4091 exact-tie race)', () => {
|
||||
assert.ok(
|
||||
WORKER_TEARDOWN_MARGIN_MS >= 5000,
|
||||
'the outer test timeout must give the worker real margin beyond the inner npm-view ' +
|
||||
'timeout (NPM_VIEW_TIMEOUT_MS) it wraps, or a slow registry response races the two ' +
|
||||
'the outer test timeout must give the worker real margin beyond the inner lookup ' +
|
||||
'timeout (LOOKUP_TIMEOUT_MS) it wraps, or a slow remote response races the two ' +
|
||||
'SIGKILLs (see #4091/2026-09-06 Windows CI incident: exact-tie timeout killed the worker ' +
|
||||
'before it could degrade gracefully)',
|
||||
);
|
||||
assert.ok(
|
||||
NPM_VIEW_TIMEOUT_MS + WORKER_TEARDOWN_MARGIN_MS > NPM_VIEW_TIMEOUT_MS,
|
||||
LOOKUP_TIMEOUT_MS + WORKER_TEARDOWN_MARGIN_MS > LOOKUP_TIMEOUT_MS,
|
||||
'the combined outer timeout must strictly exceed the inner npm-view timeout it wraps',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -29,17 +29,17 @@ const assert = require('node:assert/strict');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { scanFencedBlocks } = require('../msd-core/bin/lib/markdown-sectionizer.cjs');
|
||||
const { NPM_VIEW_TIMEOUT_MS } = require('../msd-core/bin/check-latest-version.cjs');
|
||||
const { LOOKUP_TIMEOUT_MS } = require('../msd-core/bin/check-latest-version.cjs');
|
||||
|
||||
/**
|
||||
* NOT a margin above NPM_VIEW_TIMEOUT_MS (imported above for disclosure,
|
||||
* NOT a margin above LOOKUP_TIMEOUT_MS (imported above for disclosure,
|
||||
* per this batch's epic issue -- this file and check-latest-version.cjs
|
||||
* once independently guessed the same 15000ms value, causing a Windows
|
||||
* double-SIGKILL collision fixed in PR #4428). This specific scenario
|
||||
* (missing compiled runtime library) degrades BEFORE reaching the wrapped
|
||||
* `npm view` call at all -- see this test's own #3582 comment
|
||||
* release-tag lookup at all -- see this test's own #3582 comment
|
||||
* ("checkLatestVersion -> not ok" on the degrade, never invoked) -- so its
|
||||
* bound has no margin relationship to NPM_VIEW_TIMEOUT_MS to preserve.
|
||||
* bound has no margin relationship to LOOKUP_TIMEOUT_MS to preserve.
|
||||
* Kept as its own independent, pre-existing value.
|
||||
*/
|
||||
const WORKER_DEGRADED_PATH_TIMEOUT_MS = 8000;
|
||||
@@ -48,7 +48,7 @@ const WORKER_DEGRADED_PATH_TIMEOUT_MS = 8000;
|
||||
* msd-update-banner.js only reads a small cache file from disk and prints
|
||||
* JSON -- no subprocess/network work -- so this leaves generous headroom
|
||||
* over its sub-second worst case even on a contended CI runner. NOT
|
||||
* related to NPM_VIEW_TIMEOUT_MS or WORKER_DEGRADED_PATH_TIMEOUT_MS above
|
||||
* related to LOOKUP_TIMEOUT_MS or WORKER_DEGRADED_PATH_TIMEOUT_MS above
|
||||
* (a different hook script entirely).
|
||||
*/
|
||||
const UPDATE_BANNER_HOOK_TIMEOUT_MS = 10_000;
|
||||
@@ -57,7 +57,7 @@ const UPDATE_BANNER_HOOK_TIMEOUT_MS = 10_000;
|
||||
// doc comment above): confirms this file references the collision-prone
|
||||
// constant from check-latest-version.cjs without deriving any of this
|
||||
// file's own timeout values from it.
|
||||
void NPM_VIEW_TIMEOUT_MS;
|
||||
void LOOKUP_TIMEOUT_MS;
|
||||
|
||||
const WORKER_PATH = path.join(__dirname, '..', 'hooks', 'msd-check-update-worker.js');
|
||||
const PROJECTION_PATH = path.join(
|
||||
@@ -319,96 +319,124 @@ const assert = require('node:assert/strict');
|
||||
const path = require('node:path');
|
||||
|
||||
const ROOT = path.join(__dirname, '..');
|
||||
const { checkLatestVersion, CHECK_REASON, PACKAGE_NAME } = require(
|
||||
const { checkLatestVersion, CHECK_REASON, PACKAGE_NAME, REPO_URL } = require(
|
||||
path.join(ROOT, 'msd-core', 'bin', 'check-latest-version.cjs'),
|
||||
);
|
||||
|
||||
// checkLatestVersion is a pure-ish function: it spawns one fixed npm
|
||||
// command, validates the output, and returns { ok, version | reason }.
|
||||
// The package name is HARDCODED — not a free choice for the caller.
|
||||
// Tests use a pluggable spawn so no real npm process is invoked.
|
||||
// checkLatestVersion is a pure-ish function: it runs one fixed
|
||||
// `git ls-remote --tags` against the baked repository URL, picks the highest
|
||||
// release tag, and returns { ok, version | reason }. The repository is
|
||||
// HARDCODED — not a free choice for the caller. Tests use a pluggable spawn
|
||||
// so no real git process (or network) is involved.
|
||||
|
||||
// `git ls-remote --tags --refs` output: one `<sha>\t<ref>` line per tag.
|
||||
const lsRemote = (...tags) => tags.map((t, i) => `${String(i).padStart(40, '0')}\trefs/tags/${t}`).join('\n') + '\n';
|
||||
const okSpawn = (...tags) => () => ({ status: 0, stdout: lsRemote(...tags), stderr: '' });
|
||||
|
||||
describe('Bug #2992: deterministic latest-version check', () => {
|
||||
test('PACKAGE_NAME is the constant @golem15/msd-core (no callers can override)', () => {
|
||||
assert.equal(PACKAGE_NAME, '@golem15/msd-core');
|
||||
});
|
||||
|
||||
test('REPO_URL is the baked repository the release tags are read from', () => {
|
||||
assert.equal(REPO_URL, 'https://git.golem15.com/golem15/msd-core');
|
||||
});
|
||||
|
||||
test('CHECK_REASON enum exposes the documented codes', () => {
|
||||
assert.deepEqual(
|
||||
Object.keys(CHECK_REASON).sort(),
|
||||
['FAIL_INVALID_OUTPUT', 'FAIL_NPM_FAILED', 'OK'].sort(),
|
||||
['FAIL_LOOKUP_FAILED', 'FAIL_NO_RELEASES', 'OK'].sort(),
|
||||
);
|
||||
});
|
||||
|
||||
test('returns { ok: true, version } when npm prints a valid semver', () => {
|
||||
const fakeSpawn = () => ({ status: 0, stdout: '1.39.1\n', stderr: '' });
|
||||
const r = checkLatestVersion({ spawn: fakeSpawn });
|
||||
assert.deepEqual(r, { ok: true, version: '1.39.1', reason: CHECK_REASON.OK });
|
||||
test('returns { ok: true, version } for the highest vX.Y.Z tag', () => {
|
||||
const r = checkLatestVersion({ spawn: okSpawn('v1.39.1', 'v1.40.0', 'v1.9.9') });
|
||||
assert.deepEqual(r, { ok: true, version: '1.40.0', reason: CHECK_REASON.OK });
|
||||
});
|
||||
});
|
||||
|
||||
describe('Bug #2992: error paths', () => {
|
||||
const { checkLatestVersion, CHECK_REASON } = require(require('node:path').join(__dirname, '..', 'msd-core', 'bin', 'check-latest-version.cjs'));
|
||||
|
||||
test('FAIL_NPM_FAILED when npm exits non-zero (e.g. offline, 404)', () => {
|
||||
test('FAIL_LOOKUP_FAILED when git exits non-zero (e.g. offline, repo moved)', () => {
|
||||
const r = checkLatestVersion({
|
||||
spawn: () => ({ status: 1, stdout: '', stderr: 'npm ERR! 404\n' }),
|
||||
spawn: () => ({ status: 128, stdout: '', stderr: 'fatal: unable to access\n' }),
|
||||
});
|
||||
assert.equal(r.ok, false);
|
||||
assert.equal(r.reason, CHECK_REASON.FAIL_NPM_FAILED);
|
||||
assert.equal(r.detail, 'npm ERR! 404',
|
||||
'detail should be the trimmed stderr when npm reports a real error');
|
||||
assert.equal(r.reason, CHECK_REASON.FAIL_LOOKUP_FAILED);
|
||||
assert.equal(r.detail, 'fatal: unable to access',
|
||||
'detail should be the trimmed stderr when git reports a real error');
|
||||
});
|
||||
|
||||
// #2993 CR: distinguish timeout from genuine npm failure in `detail`.
|
||||
// #2993 CR: distinguish timeout from genuine failure in `detail`.
|
||||
// spawnSync sets status=null and signal='SIGTERM' on timeout; stderr is
|
||||
// typically empty. Without the signal-first branch, both shape as
|
||||
// 'npm exited non-zero' and the operator cannot tell timeout from failure.
|
||||
test('FAIL_NPM_FAILED detail names the signal when spawn times out', () => {
|
||||
// 'git exited non-zero' and the operator cannot tell timeout from failure.
|
||||
test('FAIL_LOOKUP_FAILED detail names the signal when spawn times out', () => {
|
||||
const r = checkLatestVersion({
|
||||
spawn: () => ({ status: null, signal: 'SIGTERM', stdout: '', stderr: '' }),
|
||||
});
|
||||
assert.equal(r.ok, false);
|
||||
assert.equal(r.reason, CHECK_REASON.FAIL_NPM_FAILED);
|
||||
assert.equal(r.detail, 'npm timed out (signal: SIGTERM)',
|
||||
assert.equal(r.reason, CHECK_REASON.FAIL_LOOKUP_FAILED);
|
||||
assert.equal(r.detail, 'git timed out (signal: SIGTERM)',
|
||||
'detail should explicitly name the signal when status is null and signal is set');
|
||||
});
|
||||
|
||||
test('FAIL_NPM_FAILED detail falls back to generic when neither stderr nor signal is present', () => {
|
||||
test('FAIL_LOOKUP_FAILED detail falls back to generic when neither stderr nor signal is present', () => {
|
||||
const r = checkLatestVersion({
|
||||
spawn: () => ({ status: 1, stdout: '', stderr: '' }),
|
||||
});
|
||||
assert.equal(r.detail, 'npm exited non-zero');
|
||||
assert.equal(r.detail, 'git exited non-zero');
|
||||
});
|
||||
|
||||
test('FAIL_INVALID_OUTPUT when npm prints something that is not a semver', () => {
|
||||
// E.g. if a future npm version changes the output format, or if the
|
||||
// network returns an HTML error page captured as stdout.
|
||||
const r = checkLatestVersion({
|
||||
spawn: () => ({ status: 0, stdout: '<html>not a version</html>\n', stderr: '' }),
|
||||
});
|
||||
test('FAIL_NO_RELEASES when the remote has no release tags', () => {
|
||||
const r = checkLatestVersion({ spawn: () => ({ status: 0, stdout: '', stderr: '' }) });
|
||||
assert.equal(r.ok, false);
|
||||
assert.equal(r.reason, CHECK_REASON.FAIL_INVALID_OUTPUT);
|
||||
assert.equal(r.reason, CHECK_REASON.FAIL_NO_RELEASES);
|
||||
});
|
||||
|
||||
test('FAIL_INVALID_OUTPUT when stdout is empty', () => {
|
||||
const r = checkLatestVersion({
|
||||
spawn: () => ({ status: 0, stdout: '', stderr: '' }),
|
||||
});
|
||||
test('FAIL_NO_RELEASES when only non-release refs exist (junk tags are data, not versions)', () => {
|
||||
const r = checkLatestVersion({ spawn: okSpawn('nightly', 'v1.2', '1.2.3', 'v1.2.3<script>') });
|
||||
assert.equal(r.ok, false);
|
||||
assert.equal(r.reason, CHECK_REASON.FAIL_INVALID_OUTPUT);
|
||||
assert.equal(r.reason, CHECK_REASON.FAIL_NO_RELEASES);
|
||||
});
|
||||
|
||||
test('accepts pre-release semver (e.g. 1.40.0-rc.1)', () => {
|
||||
const r = checkLatestVersion({
|
||||
spawn: () => ({ status: 0, stdout: '1.40.0-rc.1\n', stderr: '' }),
|
||||
});
|
||||
assert.deepEqual(r, { ok: true, version: '1.40.0-rc.1', reason: CHECK_REASON.OK });
|
||||
test('the latest channel skips prerelease tags', () => {
|
||||
const r = checkLatestVersion({ spawn: okSpawn('v1.39.0', 'v1.40.0-rc.1') });
|
||||
assert.deepEqual(r, { ok: true, version: '1.39.0', reason: CHECK_REASON.OK });
|
||||
});
|
||||
});
|
||||
|
||||
describe('Issue #815: --next dist-tag support', () => {
|
||||
const { buildViewArgs, resolveTag, ALLOWED_TAGS } = require(
|
||||
describe('pickLatestVersion / compareVersions: release tag ordering', () => {
|
||||
const { pickLatestVersion, compareVersions } = require(
|
||||
path.join(ROOT, 'msd-core', 'bin', 'check-latest-version.cjs'),
|
||||
);
|
||||
|
||||
test('numeric, not lexical: 1.10.0 beats 1.9.0', () => {
|
||||
assert.equal(pickLatestVersion(lsRemote('v1.9.0', 'v1.10.0')), '1.10.0');
|
||||
});
|
||||
|
||||
test('only refs/tags/v* refs count (branches named like versions are ignored)', () => {
|
||||
const out = `${'a'.repeat(40)}\trefs/heads/v9.9.9\n${'b'.repeat(40)}\trefs/tags/v1.0.0\n`;
|
||||
assert.equal(pickLatestVersion(out), '1.0.0');
|
||||
});
|
||||
|
||||
test('next channel: rc.10 beats rc.2 and the final release beats its prereleases', () => {
|
||||
assert.equal(pickLatestVersion(lsRemote('v2.0.0-rc.2', 'v2.0.0-rc.10'), 'next'), '2.0.0-rc.10');
|
||||
assert.equal(pickLatestVersion(lsRemote('v2.0.0-rc.10', 'v2.0.0'), 'next'), '2.0.0');
|
||||
assert.equal(pickLatestVersion(lsRemote('v1.9.0', 'v2.0.0-rc.1'), 'next'), '2.0.0-rc.1');
|
||||
});
|
||||
|
||||
test('compareVersions orders prereleases below their release', () => {
|
||||
assert.equal(compareVersions('2.0.0', '2.0.0-rc.1'), 1);
|
||||
assert.equal(compareVersions('2.0.0-rc.1', '2.0.0'), -1);
|
||||
assert.equal(compareVersions('2.0.0-rc.1', '2.0.0-rc.1'), 0);
|
||||
assert.equal(compareVersions('2.0.0-alpha', '2.0.0-beta'), -1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Issue #815: --next channel support', () => {
|
||||
const { buildLsRemoteArgs, resolveTag, ALLOWED_TAGS } = require(
|
||||
path.join(ROOT, 'msd-core', 'bin', 'check-latest-version.cjs'),
|
||||
);
|
||||
|
||||
@@ -416,13 +444,8 @@ describe('Issue #815: --next dist-tag support', () => {
|
||||
assert.deepEqual([...ALLOWED_TAGS].sort(), ['latest', 'next']);
|
||||
});
|
||||
|
||||
test('buildViewArgs() defaults to the bare latest spec (byte-for-byte unchanged)', () => {
|
||||
assert.deepEqual(buildViewArgs(), ['view', '@golem15/msd-core', 'version']);
|
||||
assert.deepEqual(buildViewArgs('latest'), ['view', '@golem15/msd-core', 'version']);
|
||||
});
|
||||
|
||||
test('buildViewArgs("next") targets the @next dist-tag', () => {
|
||||
assert.deepEqual(buildViewArgs('next'), ['view', '@golem15/msd-core@next', 'version']);
|
||||
test('buildLsRemoteArgs lists only tag refs of the baked repository', () => {
|
||||
assert.deepEqual(buildLsRemoteArgs(), ['ls-remote', '--tags', '--refs', 'https://git.golem15.com/golem15/msd-core.git']);
|
||||
});
|
||||
|
||||
test('resolveTag defaults to latest when no --tag flag', () => {
|
||||
@@ -441,19 +464,15 @@ describe('Issue #815: --next dist-tag support', () => {
|
||||
assert.throws(() => resolveTag(['--tag']), /invalid --tag ''/);
|
||||
});
|
||||
|
||||
test('checkLatestVersion accepts an RC under the next tag', () => {
|
||||
const r = checkLatestVersion({ tag: 'next', spawn: () => ({ status: 0, stdout: '1.4.0-rc.1\n', stderr: '' }) });
|
||||
test('checkLatestVersion accepts an RC under the next channel', () => {
|
||||
const r = checkLatestVersion({ tag: 'next', spawn: okSpawn('v1.3.1', 'v1.4.0-rc.1') });
|
||||
assert.deepEqual(r, { ok: true, version: '1.4.0-rc.1', reason: CHECK_REASON.OK });
|
||||
});
|
||||
|
||||
test('buildViewArgs rejects a tag outside the allowlist (exported-API guard)', () => {
|
||||
assert.throws(() => buildViewArgs('nightly'), /invalid dist-tag 'nightly'/);
|
||||
});
|
||||
|
||||
test('checkLatestVersion rejects an out-of-allowlist tag even with an injected spawn', () => {
|
||||
test('checkLatestVersion rejects an out-of-allowlist channel even with an injected spawn', () => {
|
||||
assert.throws(
|
||||
() => checkLatestVersion({ tag: 'nightly', spawn: () => ({ status: 0, stdout: '9.9.9\n', stderr: '' }) }),
|
||||
/invalid dist-tag 'nightly'/,
|
||||
() => checkLatestVersion({ tag: 'nightly', spawn: okSpawn('v9.9.9') }),
|
||||
/invalid channel 'nightly'/,
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -173,11 +173,11 @@ describe('Issue #498: deriveIdentity (pure, package.json -> coordinates)', () =>
|
||||
assert.equal(deriveIdentity(FAKE_PKG).repoUrl, 'https://github.com/acme/example-pkg');
|
||||
});
|
||||
|
||||
test('a git.golem15.com repository yields in-place raw URLs on the next branch', () => {
|
||||
test('a git.golem15.com repository yields in-place raw URLs on the stable branch', () => {
|
||||
const id = deriveIdentity({ ...FAKE_PKG, repository: 'git+https://git.golem15.com/golem15/msd-core.git' });
|
||||
assert.equal(id.repoSlug, 'golem15/msd-core');
|
||||
assert.equal(id.repoUrl, 'https://git.golem15.com/golem15/msd-core');
|
||||
assert.equal(id.changelogRawUrl, 'https://git.golem15.com/golem15/msd-core/raw/branch/next/CHANGELOG.md');
|
||||
assert.equal(id.changelogRawUrl, 'https://git.golem15.com/golem15/msd-core/raw/branch/stable/CHANGELOG.md');
|
||||
});
|
||||
|
||||
test('changelogRawUrl points at raw.githubusercontent main CHANGELOG', () => {
|
||||
@@ -218,18 +218,18 @@ describe('Issue #498: slugifyPackageName (pure helper for cache filename)', () =
|
||||
});
|
||||
});
|
||||
|
||||
describe('Issue #498: formatManualInstall (the npx fallback command)', () => {
|
||||
test('global scope, no runtime -> npx with --global only', () => {
|
||||
describe('Issue #498: formatManualInstall (the bootstrap-installer fallback command)', () => {
|
||||
test('global scope, no runtime -> bootstrap with --global only', () => {
|
||||
assert.equal(
|
||||
formatManualInstall({ packageName: '@scope/example-pkg', binName: 'example-pkg', scope: 'global' }),
|
||||
'npx -y --package=@scope/example-pkg@latest -- example-pkg --global',
|
||||
'curl -fsSL https://msd.golem15.com | bash -s -- --global',
|
||||
);
|
||||
});
|
||||
|
||||
test('local scope with runtime -> --<runtime> before --<scope>', () => {
|
||||
assert.equal(
|
||||
formatManualInstall({ packageName: '@scope/example-pkg', binName: 'example-pkg', scope: 'local', runtime: 'claude' }),
|
||||
'npx -y --package=@scope/example-pkg@latest -- example-pkg --claude --local',
|
||||
'curl -fsSL https://msd.golem15.com | bash -s -- --claude --local',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -237,7 +237,7 @@ describe('Issue #498: formatManualInstall (the npx fallback command)', () => {
|
||||
const id = deriveIdentity(require(path.join(ROOT, 'package.json')));
|
||||
assert.equal(
|
||||
formatManualInstall({ packageName: id.packageName, binName: id.binName, scope: 'global', runtime: 'claude' }),
|
||||
'npx -y --package=@golem15/msd-core@latest -- msd-core --claude --global',
|
||||
'curl -fsSL https://msd.golem15.com | bash -s -- --claude --global',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -269,7 +269,7 @@ describe('Issue #498: generated runtime module (baked)', () => {
|
||||
const id = require(GENERATED);
|
||||
assert.equal(
|
||||
id.manualInstallCommand({ scope: 'global', runtime: 'claude' }),
|
||||
'npx -y --package=@golem15/msd-core@latest -- msd-core --claude --global',
|
||||
'curl -fsSL https://msd.golem15.com | bash -s -- --claude --global',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -67,7 +67,7 @@ describe('#4153 regression: unresolved update targets stop before later workflow
|
||||
assert.match(step, /INSTALL_SCOPE` is `UNKNOWN`, `TARGET_RUNTIME` is empty, or `MSD_DIR` is empty/);
|
||||
assert.match(step, /rerun from a valid installed runtime/i);
|
||||
assert.match(step, /Rerun from a valid installed runtime: `\/msd:update`\./);
|
||||
assert.match(step, /npx -y --package=@golem15\/msd-core@latest -- msd-core --global/);
|
||||
assert.match(step, /curl -fsSL https:\/\/msd\.golem15\.com \| bash -s -- --global/);
|
||||
assert.match(step, /target runtime \(`claude`, `opencode`, `codex`, `antigravity`\)/);
|
||||
assert.ok(exit > unresolved, 'unresolved target must exit before the next step');
|
||||
|
||||
@@ -88,7 +88,7 @@ describe('#4153 regression: unresolved update targets stop before later workflow
|
||||
const mutationSpies = [
|
||||
{ name: 'version check', text: src, needle: 'check-latest-version.cjs', after: end },
|
||||
{ name: 'custom-file detection', text: src, needle: 'detect-custom-files --config-dir', after: end },
|
||||
{ name: 'resolved installer', text: src, needle: 'npx -y --package=@golem15/msd-core@"$TAG" -- msd-core "$RUNTIME_FLAG"', after: end },
|
||||
{ name: 'resolved installer', text: src, needle: 'bash -s -- --ref "v$LATEST_VERSION" "$RUNTIME_FLAG"', after: end },
|
||||
{ name: 'update-cache removal', text: src, needle: 'rm -f "$HOME/.cache/msd/msd-update-check"', after: end },
|
||||
{ name: 'restore apply', text: src, needle: 'restore-custom-files --config-dir "$MSD_DIR" --apply', after: end },
|
||||
{ name: 'patch check', text: src, needle: 'check_local_patches', after: end },
|
||||
@@ -194,17 +194,16 @@ test('issue #815: version check threads the tag through check-latest-version.cjs
|
||||
assert.match(WF, /check-latest-version\.cjs"? --json --tag "\$TAG"/);
|
||||
});
|
||||
|
||||
test('issue #815: install uses the selected tag, not a hardcoded @latest', () => {
|
||||
const robust = WF.match(/npx -y --package=@golem15\/msd-core@"\$TAG" -- msd-core/g) || [];
|
||||
test('issue #815: install pins the release the channel resolved, not a moving branch', () => {
|
||||
const pinned = WF.match(/bash -s -- --ref "v\$LATEST_VERSION"/g) || [];
|
||||
const runUpdateStart = WF.indexOf('<step name="run_update">');
|
||||
const runUpdateEnd = WF.indexOf('</step>', runUpdateStart);
|
||||
assert.ok(runUpdateStart >= 0 && runUpdateEnd > runUpdateStart, 'run_update step must exist');
|
||||
const runUpdate = WF.slice(runUpdateStart, runUpdateEnd);
|
||||
assert.ok(robust.length >= 2, `expected >=2 tag-parameterized npx invocations, found ${robust.length}`);
|
||||
assert.doesNotMatch(runUpdate, /--package=@golem15\/msd-core@latest -- msd-core/,
|
||||
'install lines must not hardcode @latest once --next exists');
|
||||
assert.doesNotMatch(runUpdate, /--package=@golem15\/msd-core@(?:latest|next|beta|canary|rc) -- msd-core/,
|
||||
'install lines must use the $TAG variable, never a hardcoded dist-tag literal');
|
||||
assert.ok(pinned.length >= 2, `expected >=2 version-pinned installer invocations, found ${pinned.length}`);
|
||||
assert.doesNotMatch(runUpdate, /--ref (?:stable|next|main)\b/,
|
||||
'install lines must pin the checked $LATEST_VERSION tag, never a moving branch');
|
||||
assert.doesNotMatch(runUpdate, /\bnpx\b/, 'MSD is not published to npm; install must not use npx');
|
||||
});
|
||||
|
||||
test('issue #815: command documents --next/--rc and routes it to the update workflow', () => {
|
||||
@@ -315,15 +314,15 @@ __t3130('bug #3130: update.md contains no bare npx invocations (cache-stale form
|
||||
);
|
||||
});
|
||||
|
||||
__t3130('bug #3130: update.md has exactly two robust resolved-install invocations', () => {
|
||||
__t3130('bug #3130: update.md has exactly two resolved-install invocations (local + global)', () => {
|
||||
const start = src3130.indexOf('<step name="run_update">');
|
||||
const end = src3130.indexOf('</step>', start);
|
||||
assert3130.ok(start >= 0 && end > start, 'run_update step must exist');
|
||||
const robust = (src3130.slice(start, end).match(/npx -y --package=@golem15\/msd-core@\S+ -- msd-core/g) || []);
|
||||
const robust = (src3130.slice(start, end).match(/curl -fsSL https:\/\/msd\.golem15\.com \| bash -s -- --ref "v\$LATEST_VERSION" "\$RUNTIME_FLAG" --(?:local|global)/g) || []);
|
||||
assert3130.strictEqual(
|
||||
robust.length,
|
||||
2,
|
||||
`Expected two resolved-install npx invocations in update.md, found ${robust.length}`,
|
||||
`Expected two resolved-install bootstrap invocations (local + global) in update.md, found ${robust.length}`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user