* test(#3664): failing-first rows for the config-dir foreign-agent warning * fix(#3664): warn when config-dir targets a foreign-agent destination * test(#3664): fold the foreign-agent warning rows into the install-regressions suite * fix(#3664): close review findings — kimi-agents kind, gsd.md ownership, e2e gate * test(#3664): sync boolean call sites and the path-vocab registries * chore(#3664): backfill changeset pr number * test(#3663): skip the posix case-pin on win32 where folding is the fix --------- Co-authored-by: sim <sim@local>
This commit is contained in:
@@ -576,6 +576,14 @@ describe('W027 — #3663 isActiveWorktreePath casing/separator normalization', (
|
||||
});
|
||||
|
||||
test('W027 still fires for differently-cased paths on posix (case-sensitive pin)', (t) => {
|
||||
// POSIX-only by definition: on win32 the #3663 fix correctly FOLDS the
|
||||
// casing, so the differently-cased path IS the active worktree and must
|
||||
// NOT be flagged — that win32 behavior is pinned by the predicate rows
|
||||
// above (isActiveWorktreePath … 'win32').
|
||||
if (process.platform === 'win32') {
|
||||
t.skip('win32 folds casing by design (#3663); the posix case-sensitivity pin has no meaning there');
|
||||
return;
|
||||
}
|
||||
const cwd = createTempDir('gsd-3663-w027-posix-case-');
|
||||
t.after(() => cleanup(cwd));
|
||||
fs.mkdirSync(planningDirOf(cwd), { recursive: true });
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
* regressions file for the installer module cluster.
|
||||
*
|
||||
* Defects covered:
|
||||
* #3664 — --config-dir foreign-agent destination warning (warn-and-proceed)
|
||||
* #3664 Defect #1 — stale skills/gsd/gsd-<stem>/ dirs on Hermes upgrade
|
||||
* #3664 Defect #2 — --hermes --profile=core falls through to wrong path
|
||||
* #2973 M1–M3 — dev-preferences migration at profile=core for hermes/qwen/claude
|
||||
@@ -20,6 +21,7 @@ const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const { runNode } = require('./helpers/process-seam.cjs');
|
||||
const { throwIfFailed } = require('./helpers/git-fixture.cjs');
|
||||
const { INSTALL_TIMEOUT_MS } = require('./helpers/timeouts.cjs');
|
||||
|
||||
const { createTempDir, cleanup } = require('./helpers.cjs');
|
||||
const {
|
||||
@@ -53,7 +55,6 @@ const {
|
||||
|
||||
const INSTALL_SCRIPT = path.join(__dirname, '..', 'bin', 'install.js');
|
||||
// #3145: class-norm timeout, not a per-suite value — see helpers/timeouts.cjs.
|
||||
const { INSTALL_TIMEOUT_MS } = require('./helpers/timeouts.cjs');
|
||||
const HOOKS_SRC = path.join(__dirname, '..', 'hooks');
|
||||
const REAL_COMMANDS_DIR = path.join(__dirname, '..', 'commands', 'gsd');
|
||||
const MANIFEST = loadSkillsManifest(REAL_COMMANDS_DIR);
|
||||
@@ -1685,3 +1686,165 @@ describe('#3329 regression: stale managed .sh hook commands are reconciled on in
|
||||
assert.strictEqual(settings.hooks.SessionStart[0].hooks[0].command, original);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── #3664 — config-dir foreign-agent destination warning ───────────────────
|
||||
//
|
||||
// --config-dir aimed at a directory that is not a supported runtime's config
|
||||
// home must never be a SILENT success: the installer emits the selected
|
||||
// runtime's artifacts verbatim (Claude-only Skill tool IDs, mcp__server__tool
|
||||
// grants), which are inert or invalid in a foreign harness and surface only
|
||||
// at dispatch time. Warn-and-proceed when the destination already holds
|
||||
// foreign (non-GSD) agent files; fresh custom dirs, gsd-only dirs (updates,
|
||||
// the --all shared dir), the no-flag default-home path, and test temp dirs
|
||||
// stay silent. Folded into this suite per the lint-test-file-count cap
|
||||
// (primary + one integration per production module).
|
||||
|
||||
const installerMod = require('../bin/install.js');
|
||||
|
||||
function capturedLogs(t, fn) {
|
||||
const lines = [];
|
||||
const mock = t.mock.method(console, 'log', (...args) => {
|
||||
lines.push(args.join(' '));
|
||||
});
|
||||
const result = fn();
|
||||
mock.mock.restore();
|
||||
return { lines, result };
|
||||
}
|
||||
|
||||
function makeForeignDest(t, name, agentFiles) {
|
||||
const dest = createTempDir(`gsd-3664-${name}-`);
|
||||
const agentsDir = path.join(dest, 'agents');
|
||||
fs.mkdirSync(agentsDir, { recursive: true });
|
||||
for (const file of agentFiles) {
|
||||
fs.writeFileSync(path.join(agentsDir, file), '---\ntools: Read\n---\nbody\n');
|
||||
}
|
||||
t.after(() => cleanup(dest));
|
||||
return { dest, agentsDir };
|
||||
}
|
||||
|
||||
describe('#3664 — config-dir foreign-agent destination warning', () => {
|
||||
test('warns when the destination holds foreign agent files', (t) => {
|
||||
const { dest } = makeForeignDest(t, 'foreign', ['junie-guide.md']);
|
||||
const { lines } = capturedLogs(t, () =>
|
||||
installerMod.warnIfForeignAgentDest('claude', dest, 'global', true),
|
||||
);
|
||||
const warning = lines.find((l) => l.includes('(#3664)'));
|
||||
assert.ok(warning, `expected a #3664 warning, got: ${lines.join(' | ') || '(none)'}`);
|
||||
assert.ok(warning.includes('claude'), `warning must name the selected runtime: ${warning}`);
|
||||
assert.ok(
|
||||
/tool IDs and MCP grants may (be inert|not apply)/.test(warning),
|
||||
`warning must name the tool/MCP risk: ${warning}`,
|
||||
);
|
||||
});
|
||||
|
||||
test('installer warns and proceeds on a foreign-agent destination', (t) => {
|
||||
const home = createTempDir('gsd-3664-e2e-home-');
|
||||
t.after(() => cleanup(home));
|
||||
const dest = createTempDir('gsd-3664-e2e-dest-');
|
||||
t.after(() => cleanup(dest));
|
||||
fs.mkdirSync(path.join(dest, 'agents'), { recursive: true });
|
||||
fs.writeFileSync(path.join(dest, 'agents', 'junie-guide.md'), '---\ntools: Read\n---\nbody\n');
|
||||
|
||||
const result = runNode(
|
||||
[path.join(__dirname, '..', 'bin', 'install.js'), '--claude', '--global', '--config-dir', dest],
|
||||
{
|
||||
env: {
|
||||
...process.env,
|
||||
HOME: home,
|
||||
USERPROFILE: home,
|
||||
GSD_HOME: home,
|
||||
CI: '1',
|
||||
},
|
||||
timeoutMs: INSTALL_TIMEOUT_MS,
|
||||
},
|
||||
);
|
||||
assert.equal(result.exitCode, 0, `warn-and-proceed must exit 0; stderr: ${result.stderr.slice(0, 500)}`);
|
||||
assert.ok(
|
||||
result.stdout.includes('(#3664)'),
|
||||
`stdout must carry the #3664 warning: ${result.stdout.slice(-800)}`,
|
||||
);
|
||||
const emitted = fs.existsSync(path.join(dest, 'agents'))
|
||||
? fs.readdirSync(path.join(dest, 'agents')).filter((f) => /^gsd-.*\.md$/.test(f))
|
||||
: [];
|
||||
assert.ok(emitted.length > 0, 'install must still emit the gsd-* agents');
|
||||
assert.ok(fs.existsSync(path.join(dest, 'agents', 'junie-guide.md')));
|
||||
});
|
||||
|
||||
test('silent on a fresh custom dir', (t) => {
|
||||
const { dest } = makeForeignDest(t, 'fresh', []);
|
||||
const { lines } = capturedLogs(t, () =>
|
||||
installerMod.warnIfForeignAgentDest('claude', dest, 'global', true),
|
||||
);
|
||||
assert.ok(!lines.some((l) => l.includes('(#3664)')), `fresh dir must be silent: ${lines.join(' | ')}`);
|
||||
});
|
||||
|
||||
test('silent on a gsd-only agents dir', (t) => {
|
||||
const { dest } = makeForeignDest(t, 'gsdonly', ['gsd-executor.md', 'gsd-verifier.md']);
|
||||
const { lines } = capturedLogs(t, () =>
|
||||
installerMod.warnIfForeignAgentDest('claude', dest, 'global', true),
|
||||
);
|
||||
assert.ok(!lines.some((l) => l.includes('(#3664)')), `gsd-only dir must be silent: ${lines.join(' | ')}`);
|
||||
});
|
||||
|
||||
test('silent when the config-dir flag was not passed', (t) => {
|
||||
const { dest } = makeForeignDest(t, 'noflag', ['personal-agent.md']);
|
||||
const { lines } = capturedLogs(t, () =>
|
||||
installerMod.warnIfForeignAgentDest('claude', dest, 'global', false),
|
||||
);
|
||||
assert.ok(!lines.some((l) => l.includes('(#3664)')), `no-flag path must be silent: ${lines.join(' | ')}`);
|
||||
});
|
||||
|
||||
test('warns on mixed gsd and personal agents', (t) => {
|
||||
const { dest } = makeForeignDest(t, 'mixed', ['gsd-executor.md', 'my-own-agent.md']);
|
||||
const { lines } = capturedLogs(t, () =>
|
||||
installerMod.warnIfForeignAgentDest('claude', dest, 'global', true),
|
||||
);
|
||||
const warning = lines.find((l) => l.includes('(#3664)'));
|
||||
assert.ok(warning, 'mixed dir with a foreign agent must warn');
|
||||
assert.ok(warning.includes('contains 1 non-GSD agent file'), `warning reports the foreign count: ${warning}`);
|
||||
});
|
||||
|
||||
test('ignores non-markdown files', (t) => {
|
||||
const { dest, agentsDir } = makeForeignDest(t, 'nonmd', []);
|
||||
fs.writeFileSync(path.join(agentsDir, 'notes.txt'), 'not an agent');
|
||||
const { lines } = capturedLogs(t, () =>
|
||||
installerMod.warnIfForeignAgentDest('claude', dest, 'global', true),
|
||||
);
|
||||
assert.ok(!lines.some((l) => l.includes('(#3664)')), `non-agent files are not harness evidence: ${lines.join(' | ')}`);
|
||||
});
|
||||
|
||||
test('warns for the kimi runtime (kimi-agents kind)', (t) => {
|
||||
const { dest } = makeForeignDest(t, 'kimi-foreign', ['junie-guide.md']);
|
||||
const { lines } = capturedLogs(t, () =>
|
||||
installerMod.warnIfForeignAgentDest('kimi', dest, 'global', true),
|
||||
);
|
||||
assert.ok(
|
||||
lines.some((l) => l.includes('(#3664)') && l.includes('kimi')),
|
||||
`kimi's kimi-agents kind must take the gate: ${lines.join(' | ')}`,
|
||||
);
|
||||
});
|
||||
|
||||
test('silent on kimi\'s own gsd.md in a shared multi-runtime dir', (t) => {
|
||||
// kimi's root agent is agents/gsd.md — a bare stem, no gsd- prefix. A
|
||||
// shared --all dir accumulates it alongside gsd-*.md; none of it is
|
||||
// foreign, so every runtime's install into that dir must stay silent.
|
||||
const { dest } = makeForeignDest(t, 'shared-kimi', ['gsd-executor.md', 'gsd.md']);
|
||||
const { lines } = capturedLogs(t, () => {
|
||||
installerMod.warnIfForeignAgentDest('kimi', dest, 'global', true);
|
||||
installerMod.warnIfForeignAgentDest('claude', dest, 'global', true);
|
||||
});
|
||||
assert.ok(
|
||||
!lines.some((l) => l.includes('(#3664)')),
|
||||
`GSD-owned gsd.md must not read as a foreign agent: ${lines.join(' | ')}`,
|
||||
);
|
||||
});
|
||||
|
||||
test('degrades silently when the layout cannot resolve', (t) => {
|
||||
const dest = createTempDir('gsd-3664-unknown-');
|
||||
t.after(() => cleanup(dest));
|
||||
const { lines } = capturedLogs(t, () =>
|
||||
installerMod.warnIfForeignAgentDest('not-a-registered-runtime', dest, 'global', true),
|
||||
);
|
||||
assert.ok(!lines.some((l) => l.includes('(#3664)')), `unresolvable layout must degrade silently: ${lines.join(' | ')}`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -50,6 +50,8 @@ const INSTALL_JS_PATH_HELPERS = [
|
||||
// #2088 (ADR-1239 upgrade 3): resolves the skills-install dir honoring a
|
||||
// skills-kind `home` override (e.g. Codex → $HOME/.agents/skills).
|
||||
'_resolveSkillsRootDir',
|
||||
// #3664: shared kind-destination resolver (skills/agents/kimi-agents kinds).
|
||||
'_kindDestDir',
|
||||
];
|
||||
|
||||
describe('portability-vocab drift guard', () => {
|
||||
|
||||
Reference in New Issue
Block a user