Adds an opt-in --next (alias --rc) flag to /gsd-update targeting the @next RC dist-tag (ADR #660), with a {latest,next} allowlist enforced at three layers, channel-aware version check + banner, and byte-for-byte unchanged default @latest behavior. Closes #815
This commit is contained in:
@@ -93,3 +93,62 @@ describe('Bug #2992: error paths', () => {
|
||||
assert.deepEqual(r, { ok: true, version: '1.40.0-rc.1', reason: CHECK_REASON.OK });
|
||||
});
|
||||
});
|
||||
|
||||
describe('Issue #815: --next dist-tag support', () => {
|
||||
const { buildViewArgs, resolveTag, ALLOWED_TAGS } = require(
|
||||
path.join(ROOT, 'gsd-core', 'bin', 'check-latest-version.cjs'),
|
||||
);
|
||||
|
||||
test('ALLOWED_TAGS is the sanctioned channel allowlist (latest, next)', () => {
|
||||
assert.deepEqual([...ALLOWED_TAGS].sort(), ['latest', 'next']);
|
||||
});
|
||||
|
||||
test('buildViewArgs() defaults to the bare latest spec (byte-for-byte unchanged)', () => {
|
||||
assert.deepEqual(buildViewArgs(), ['view', '@opengsd/gsd-core', 'version']);
|
||||
assert.deepEqual(buildViewArgs('latest'), ['view', '@opengsd/gsd-core', 'version']);
|
||||
});
|
||||
|
||||
test('buildViewArgs("next") targets the @next dist-tag', () => {
|
||||
assert.deepEqual(buildViewArgs('next'), ['view', '@opengsd/gsd-core@next', 'version']);
|
||||
});
|
||||
|
||||
test('resolveTag defaults to latest when no --tag flag', () => {
|
||||
assert.equal(resolveTag(['--json']), 'latest');
|
||||
});
|
||||
|
||||
test('resolveTag reads --tag next', () => {
|
||||
assert.equal(resolveTag(['--json', '--tag', 'next']), 'next');
|
||||
});
|
||||
|
||||
test('resolveTag rejects an unknown tag (typo guard)', () => {
|
||||
assert.throws(() => resolveTag(['--tag', 'nightly']), /invalid --tag 'nightly'/);
|
||||
});
|
||||
|
||||
test('resolveTag rejects --tag with no value', () => {
|
||||
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: '' }) });
|
||||
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', () => {
|
||||
assert.throws(
|
||||
() => checkLatestVersion({ tag: 'nightly', spawn: () => ({ status: 0, stdout: '9.9.9\n', stderr: '' }) }),
|
||||
/invalid dist-tag 'nightly'/,
|
||||
);
|
||||
});
|
||||
|
||||
test('resolveTag handles the --tag=next equals form', () => {
|
||||
assert.equal(resolveTag(['--json', '--tag=next']), 'next');
|
||||
});
|
||||
|
||||
test('resolveTag rejects an unknown --tag=value equals form (no silent fallback)', () => {
|
||||
assert.throws(() => resolveTag(['--tag=nightly']), /invalid --tag 'nightly'/);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,18 +4,20 @@
|
||||
// Regression guard for bug #3130.
|
||||
//
|
||||
// Two failure modes were observed with the pre-fix npx invocation form:
|
||||
// 1. Cache-stale: bare `npx -y @opengsd/gsd-core@latest` hits npx's local
|
||||
// cache and may pull an older version instead of @latest.
|
||||
// 1. Cache-stale: bare `npx -y @opengsd/gsd-core@<tag>` hits npx's local
|
||||
// cache and may pull an older version instead of the target tag.
|
||||
// 2. Token-routing: Bash-tool wrappers misroute the `@` token in
|
||||
// `@opengsd/gsd-core@latest`, causing npm to error with
|
||||
// "Unknown command: @opengsd/gsd-core@latest".
|
||||
// `@opengsd/gsd-core@<tag>`, causing npm to error with
|
||||
// "Unknown command: @opengsd/gsd-core@<tag>".
|
||||
//
|
||||
// The robust form is:
|
||||
// npx -y --package=@opengsd/gsd-core@latest -- gsd-core $ARGS
|
||||
// npx -y --package=@opengsd/gsd-core@"$TAG" -- gsd-core $ARGS
|
||||
//
|
||||
// `--package=` forces a fresh registry fetch, bypassing the npx cache.
|
||||
// `--` clearly delineates npx flags from the run-command, preventing
|
||||
// Bash-tool @-token misrouting.
|
||||
// `$TAG` is a shell variable (latest by default, next under --next/--rc),
|
||||
// set by the parse_update_channel step (#815).
|
||||
|
||||
const { test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
@@ -28,9 +30,9 @@ const UPDATE_WF = path.join(ROOT, 'gsd-core', 'workflows', 'update.md');
|
||||
const src = fs.readFileSync(UPDATE_WF, 'utf8');
|
||||
|
||||
test('bug #3130: update.md contains no bare npx invocations (cache-stale form)', () => {
|
||||
// Any occurrence of `npx -y @opengsd/gsd-core@latest` without `--package=`
|
||||
// Any occurrence of `npx -y @opengsd/gsd-core@<something>` without `--package=`
|
||||
// is the stale form that triggers the two failure modes.
|
||||
const stale = (src.match(/npx -y @opengsd\/gsd-core@latest[^\n]*/g) || []);
|
||||
const stale = (src.match(/npx -y @opengsd\/gsd-core@\S+[^\n]*/g) || []);
|
||||
assert.deepEqual(
|
||||
stale,
|
||||
[],
|
||||
@@ -40,7 +42,8 @@ test('bug #3130: update.md contains no bare npx invocations (cache-stale form)',
|
||||
|
||||
test('bug #3130: update.md has >=3 robust npx invocations (--package= + -- separator)', () => {
|
||||
// Three sibling invocations: local, global, and unknown/fallback.
|
||||
const robust = (src.match(/npx -y --package=@opengsd\/gsd-core@latest -- gsd-core/g) || []);
|
||||
// The tag is now a $TAG variable (latest by default, next under --next/--rc).
|
||||
const robust = (src.match(/npx -y --package=@opengsd\/gsd-core@\S+ -- gsd-core/g) || []);
|
||||
assert.ok(
|
||||
robust.length >= 3,
|
||||
`Expected >=3 robust npx invocations in update.md, found ${robust.length}`,
|
||||
|
||||
44
tests/issue-815-update-next-channel.test.cjs
Normal file
44
tests/issue-815-update-next-channel.test.cjs
Normal file
@@ -0,0 +1,44 @@
|
||||
'use strict';
|
||||
// allow-test-rule: reads product workflow/command markdown to verify the --next RC channel contract — not a source-grep test
|
||||
|
||||
// Issue #815: `/gsd-update --next` (alias `--rc`) must thread the @next dist-tag
|
||||
// through the whole update flow (version check + install) while leaving the
|
||||
// default @latest path unchanged.
|
||||
|
||||
const { test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const ROOT = path.join(__dirname, '..');
|
||||
const WF = fs.readFileSync(path.join(ROOT, 'gsd-core', 'workflows', 'update.md'), 'utf8');
|
||||
const CMD = fs.readFileSync(path.join(ROOT, 'commands', 'gsd', 'update.md'), 'utf8');
|
||||
|
||||
test('issue #815: workflow parses --next/--rc into a TAG channel', () => {
|
||||
assert.match(WF, /--next/);
|
||||
assert.match(WF, /--rc/);
|
||||
assert.match(WF, /TAG="next"/);
|
||||
assert.match(WF, /TAG="latest"/);
|
||||
});
|
||||
|
||||
test('issue #815: version check threads the tag through check-latest-version.cjs', () => {
|
||||
// The script path is double-quoted in the shell command, so the line is:
|
||||
// node "$GSD_DIR/gsd-core/bin/check-latest-version.cjs" --json --tag "$TAG"
|
||||
// The closing " on the script path sits between .cjs and --json.
|
||||
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=@opengsd\/gsd-core@"\$TAG" -- gsd-core/g) || [];
|
||||
assert.ok(robust.length >= 3, `expected >=3 tag-parameterized npx invocations, found ${robust.length}`);
|
||||
assert.doesNotMatch(WF, /--package=@opengsd\/gsd-core@latest -- gsd-core/,
|
||||
'install lines must not hardcode @latest once --next exists');
|
||||
assert.doesNotMatch(WF, /--package=@opengsd\/gsd-core@(?:latest|next|beta|canary|rc) -- gsd-core/,
|
||||
'install lines must use the $TAG variable, never a hardcoded dist-tag literal');
|
||||
});
|
||||
|
||||
test('issue #815: command documents --next/--rc and routes it to the update workflow', () => {
|
||||
assert.match(CMD, /--next/);
|
||||
assert.match(CMD, /--rc/);
|
||||
assert.match(CMD, /argument-hint:.*--next/);
|
||||
});
|
||||
Reference in New Issue
Block a user