Merge branch 'next' into fix/2137-derive-progress-header-driven
This commit is contained in:
@@ -3952,12 +3952,14 @@ describe('FIX 3: tightened runtime validator — probeExists optional string', (
|
||||
// ── 24e. Closed-vocab set exports are correct ─────────────────────────────────
|
||||
|
||||
describe('ADR-1016 phase 5a: closed-vocab set exports', () => {
|
||||
test('VALID_CONFIG_HOME_KINDS has exactly the 4 expected values', () => {
|
||||
test('VALID_CONFIG_HOME_KINDS has exactly the 5 expected values', () => {
|
||||
assert.ok(VALID_CONFIG_HOME_KINDS instanceof Set);
|
||||
for (const v of ['dot-home', 'dot-home-nested', 'xdg', 'generic-agents-root']) {
|
||||
// 'none' added #2103 — a runtime with NO file-projected config directory
|
||||
// at all (e.g. vscode — Marketplace/VSIX-distributed).
|
||||
for (const v of ['dot-home', 'dot-home-nested', 'xdg', 'generic-agents-root', 'none']) {
|
||||
assert.ok(VALID_CONFIG_HOME_KINDS.has(v), 'VALID_CONFIG_HOME_KINDS must contain "' + v + '"');
|
||||
}
|
||||
assert.strictEqual(VALID_CONFIG_HOME_KINDS.size, 4, 'VALID_CONFIG_HOME_KINDS must have exactly 4 members');
|
||||
assert.strictEqual(VALID_CONFIG_HOME_KINDS.size, 5, 'VALID_CONFIG_HOME_KINDS must have exactly 5 members');
|
||||
});
|
||||
|
||||
test('VALID_COMMAND_STYLES has exactly 2 values', () => {
|
||||
@@ -3967,12 +3969,12 @@ describe('ADR-1016 phase 5a: closed-vocab set exports', () => {
|
||||
assert.strictEqual(VALID_COMMAND_STYLES.size, 2);
|
||||
});
|
||||
|
||||
test('VALID_HOOKS_SURFACES has exactly 7 values', () => {
|
||||
test('VALID_HOOKS_SURFACES has exactly 8 values', () => {
|
||||
assert.ok(VALID_HOOKS_SURFACES instanceof Set);
|
||||
for (const v of ['settings-json', 'codex-hooks-json', 'cursor-hooks-json', 'copilot-inline', 'cline-rules', 'kimi-hooks-toml', 'none']) {
|
||||
for (const v of ['settings-json', 'codex-hooks-json', 'cursor-hooks-json', 'copilot-inline', 'cline-rules', 'kimi-hooks-toml', 'windsurf-hooks-json', 'none']) {
|
||||
assert.ok(VALID_HOOKS_SURFACES.has(v), 'VALID_HOOKS_SURFACES must contain "' + v + '"');
|
||||
}
|
||||
assert.strictEqual(VALID_HOOKS_SURFACES.size, 7);
|
||||
assert.strictEqual(VALID_HOOKS_SURFACES.size, 8);
|
||||
});
|
||||
|
||||
test('VALID_HOOK_EVENTS has exactly 2 managed-hook dialects (claude/gemini)', () => {
|
||||
@@ -4261,15 +4263,18 @@ describe('ADR-857 phase 5e: configFormat ↔ installSurface parity gate', () =>
|
||||
});
|
||||
|
||||
// INSTALL_SURFACE_TO_CONFIG_FORMAT export check
|
||||
test('INSTALL_SURFACE_TO_CONFIG_FORMAT covers all 6 installSurface values with correct mappings', () => {
|
||||
test('INSTALL_SURFACE_TO_CONFIG_FORMAT covers all 7 installSurface values with correct mappings', () => {
|
||||
assert.ok(INSTALL_SURFACE_TO_CONFIG_FORMAT instanceof Map, 'Must be a Map');
|
||||
assert.strictEqual(INSTALL_SURFACE_TO_CONFIG_FORMAT.size, 6, 'Must cover 6 installSurface values');
|
||||
assert.strictEqual(INSTALL_SURFACE_TO_CONFIG_FORMAT.size, 7, 'Must cover 7 installSurface values');
|
||||
assert.strictEqual(INSTALL_SURFACE_TO_CONFIG_FORMAT.get('settings-json'), 'settings-json');
|
||||
assert.strictEqual(INSTALL_SURFACE_TO_CONFIG_FORMAT.get('codex-toml'), 'toml');
|
||||
assert.strictEqual(INSTALL_SURFACE_TO_CONFIG_FORMAT.get('copilot-instructions'), 'markdown');
|
||||
assert.strictEqual(INSTALL_SURFACE_TO_CONFIG_FORMAT.get('cline-rules'), 'markdown-dir');
|
||||
assert.strictEqual(INSTALL_SURFACE_TO_CONFIG_FORMAT.get('cursor-hooks-json'), 'none');
|
||||
assert.strictEqual(INSTALL_SURFACE_TO_CONFIG_FORMAT.get('profile-marker-only'), 'none');
|
||||
// 'none' added #2103 — a runtime with no CLI install surface at all (e.g.
|
||||
// vscode) has no config-file format to write either.
|
||||
assert.strictEqual(INSTALL_SURFACE_TO_CONFIG_FORMAT.get('none'), 'none');
|
||||
});
|
||||
|
||||
// Feature capabilities (role:feature) are silently ignored by the gate
|
||||
@@ -4432,10 +4437,71 @@ describe('ADR-857 phase 5f: cross-field consistency gate rejection tests (DEFECT
|
||||
);
|
||||
});
|
||||
|
||||
// #2103: configHome.kind==='none' carve-out (VS Code — Marketplace/VSIX, no
|
||||
// file-projected config directory). Adversarial review flagged AC4's
|
||||
// "accept vscode AND reject a faked configHome" requirement as untested —
|
||||
// the real vscode descriptor validating clean (tests/capability-registry.test.cjs's
|
||||
// "ADR-1016 phase 5a" suite, capability-validator-parity tests, etc.) only
|
||||
// proves the ACCEPT half. These three tests are the REJECT half: they prove
|
||||
// the two new branches in validateRuntimeBody/validateConfigHome actually
|
||||
// fire, quoting their EXACT error strings so a future edit that silently
|
||||
// weakens either branch fails loudly here.
|
||||
describe('#2103 configHome.kind==="none" carve-out (vscode) — accept/reject pair', () => {
|
||||
test('ACCEPT: configHome.kind:"none" + localConfigDir:null + installSurface:"none" + hooksSurface:"none" validates with ZERO errors', () => {
|
||||
const cap = makeValidRuntimeCap({
|
||||
runtime: {
|
||||
configHome: { kind: 'none', name: 'test-none-rt', env: [] },
|
||||
localConfigDir: null,
|
||||
configFormat: 'none',
|
||||
installSurface: 'none',
|
||||
hooksSurface: 'none',
|
||||
},
|
||||
});
|
||||
const errors = validateRuntimeBody(cap);
|
||||
assert.deepEqual(errors, [], 'a genuinely kind:"none" descriptor (vscode-shaped) must validate clean, got: ' + JSON.stringify(errors));
|
||||
});
|
||||
|
||||
test('REJECT #1: localConfigDir non-null while configHome.kind==="none" → exact validateRuntimeBody error', () => {
|
||||
const cap = makeValidRuntimeCap({
|
||||
runtime: {
|
||||
configHome: { kind: 'none', name: 'test-none-rt', env: [] },
|
||||
localConfigDir: '.vscode', // faked — a kind:'none' descriptor must not also claim a local dir
|
||||
configFormat: 'none',
|
||||
installSurface: 'none',
|
||||
hooksSurface: 'none',
|
||||
},
|
||||
});
|
||||
const errors = validateRuntimeBody(cap);
|
||||
assert.deepEqual(
|
||||
errors,
|
||||
['runtime.localConfigDir must be null or absent when configHome.kind is "none"; got: ".vscode"'],
|
||||
'expected the exact localConfigDir-vs-kind:"none" rejection, got: ' + JSON.stringify(errors),
|
||||
);
|
||||
});
|
||||
|
||||
test('REJECT #2: configHome.name present-but-empty while configHome.kind==="none" → exact validateConfigHome error', () => {
|
||||
const cap = makeValidRuntimeCap({
|
||||
runtime: {
|
||||
configHome: { kind: 'none', name: '', env: [] }, // faked — present name must still be non-empty
|
||||
localConfigDir: null,
|
||||
configFormat: 'none',
|
||||
installSurface: 'none',
|
||||
hooksSurface: 'none',
|
||||
},
|
||||
});
|
||||
const errors = validateRuntimeBody(cap);
|
||||
assert.deepEqual(
|
||||
errors,
|
||||
['capability "test-runtime" runtime.configHome.name must be a non-empty string if present when kind is "none"'],
|
||||
'expected the exact configHome.name-vs-kind:"none" rejection, got: ' + JSON.stringify(errors),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// Verify the new constants are well-formed
|
||||
test('INSTALL_SURFACE_TO_ALLOWED_HOOKS_SURFACES covers all 6 installSurface values', () => {
|
||||
test('INSTALL_SURFACE_TO_ALLOWED_HOOKS_SURFACES covers all 7 installSurface values', () => {
|
||||
assert.ok(INSTALL_SURFACE_TO_ALLOWED_HOOKS_SURFACES instanceof Map, 'Must be a Map');
|
||||
assert.strictEqual(INSTALL_SURFACE_TO_ALLOWED_HOOKS_SURFACES.size, 6, 'Must cover 6 installSurface values');
|
||||
assert.strictEqual(INSTALL_SURFACE_TO_ALLOWED_HOOKS_SURFACES.size, 7, 'Must cover 7 installSurface values');
|
||||
for (const installSurface of VALID_INSTALL_SURFACES) {
|
||||
assert.ok(
|
||||
INSTALL_SURFACE_TO_ALLOWED_HOOKS_SURFACES.has(installSurface),
|
||||
@@ -6509,7 +6575,10 @@ describe('enh-1055 descriptor-drive: ALLOWED_CONFIG_RUNTIMES completeness', () =
|
||||
test('equals the registry runtimes that declare an installSurface', () => {
|
||||
const descriptorAllowed = new Set(
|
||||
Object.entries(enh1055Registry.runtimes)
|
||||
.filter(([, cap]) => cap && cap.runtime && typeof cap.runtime.installSurface === 'string')
|
||||
// installSurface:'none' (#2103 vscode — extension-distributed, no config
|
||||
// directory) is NOT a config-adapter runtime: production ALLOWED_CONFIG_RUNTIMES
|
||||
// excludes it so `allRuntimes === ALLOWED_CONFIG_RUNTIMES` (issue-57) stays true.
|
||||
.filter(([, cap]) => cap && cap.runtime && typeof cap.runtime.installSurface === 'string' && cap.runtime.installSurface !== 'none')
|
||||
.map(([id]) => id),
|
||||
);
|
||||
assert.deepStrictEqual(new Set(ALLOWED_CONFIG_RUNTIMES), descriptorAllowed);
|
||||
|
||||
149
tests/copilot-upgrades.test.cjs
Normal file
149
tests/copilot-upgrades.test.cjs
Normal file
@@ -0,0 +1,149 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Copilot capability UPGRADES — ADR-1239 / #2099 (EoS/copilot).
|
||||
*
|
||||
* Drives the user-reachable surface (spawned `bin/install.js` via
|
||||
* `runMinimalInstall`) plus a direct negotiation-contract check to prove the
|
||||
* two real upgrades Copilot contributes as part of the EoS migration:
|
||||
*
|
||||
* UPGRADE 1 — multi-event hook bus: buildCopilotHookConfig() previously
|
||||
* emitted only `sessionStart`. This PR wires four additional events —
|
||||
* `preToolUse`, `postToolUse`, `userPromptSubmitted`, `sessionEnd` — each a
|
||||
* static, deterministic advisory command (no node-runner invocation), so a
|
||||
* live install's hooks/gsd-session.json registers all five events.
|
||||
*
|
||||
* UPGRADE 2 — dispatch.background: Copilot's capability.json already
|
||||
* declares `dispatch.background: true`, which legitimately EXCEEDS the
|
||||
* `declarative-cli` profile baseline (`false`, per
|
||||
* `PROFILE_BASELINES['declarative-cli']` in src/host-integration.cts).
|
||||
* NO agent-file/frontmatter change is involved — Copilot's .agent.md
|
||||
* frontmatter has no background-dispatch field (fields are
|
||||
* description/infer/mcp-servers/model/name/tools). This is surfaced
|
||||
* purely through the negotiated contract: a documented `true` value must
|
||||
* survive negotiation without a downgrade warning.
|
||||
*/
|
||||
|
||||
const { test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const { runMinimalInstall } = require('./helpers/install-shared.cjs');
|
||||
const { cleanup } = require('./helpers.cjs');
|
||||
const {
|
||||
profileOf,
|
||||
negotiateHostCapabilities,
|
||||
PROFILE_BASELINES,
|
||||
} = require('../gsd-core/bin/lib/host-integration.cjs');
|
||||
|
||||
const COPILOT_CAP = JSON.parse(
|
||||
fs.readFileSync(path.join(__dirname, '..', 'capabilities', 'copilot', 'capability.json'), 'utf8'),
|
||||
);
|
||||
const COPILOT_AXES = COPILOT_CAP.runtime.hostIntegration;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// UPGRADE 1: multi-event hook bus — all 4 newly-wired events' live-install
|
||||
// coverage, on top of the pre-existing sessionStart.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const NEWLY_WIRED_EVENTS = ['preToolUse', 'postToolUse', 'userPromptSubmitted', 'sessionEnd'];
|
||||
const ALL_EXPECTED_EVENTS = ['sessionStart', ...NEWLY_WIRED_EVENTS];
|
||||
|
||||
test('copilot --global: hooks/gsd-session.json wires sessionStart plus all 4 newly-added events (UPGRADE 1)', (t) => {
|
||||
const { configDir, root } = runMinimalInstall({ runtime: 'copilot', scope: 'global' });
|
||||
t.after(() => cleanup(root));
|
||||
|
||||
const hookPath = path.join(configDir, 'hooks', 'gsd-session.json');
|
||||
assert.ok(fs.existsSync(hookPath), `${hookPath} must exist`);
|
||||
const parsed = JSON.parse(fs.readFileSync(hookPath, 'utf8'));
|
||||
|
||||
assert.strictEqual(parsed.version, 1, 'hook config version must be 1');
|
||||
assert.ok(parsed.hooks && typeof parsed.hooks === 'object', 'has hooks object');
|
||||
|
||||
for (const eventName of ALL_EXPECTED_EVENTS) {
|
||||
const eventHooks = parsed.hooks[eventName];
|
||||
assert.ok(Array.isArray(eventHooks) && eventHooks.length > 0,
|
||||
`hooks.${eventName} must exist and be non-empty`);
|
||||
const entry = eventHooks[0];
|
||||
assert.strictEqual(entry.type, 'command', `${eventName} entry type must be 'command'`);
|
||||
assert.ok(typeof entry.bash === 'string' && entry.bash.length > 0,
|
||||
`${eventName} entry must have a non-empty inline bash body`);
|
||||
assert.ok(typeof entry.powershell === 'string' && entry.powershell.length > 0,
|
||||
`${eventName} entry must have a non-empty inline powershell body`);
|
||||
assert.strictEqual(entry.timeoutSec, 10, `${eventName} entry must use timeoutSec 10`);
|
||||
}
|
||||
});
|
||||
|
||||
test('each newly-wired event emits a distinct static advisory (no shared boilerplate, no node-runner invocation)', (t) => {
|
||||
const { configDir, root } = runMinimalInstall({ runtime: 'copilot', scope: 'global' });
|
||||
t.after(() => cleanup(root));
|
||||
|
||||
const hookPath = path.join(configDir, 'hooks', 'gsd-session.json');
|
||||
const parsed = JSON.parse(fs.readFileSync(hookPath, 'utf8'));
|
||||
|
||||
const bashBodies = new Set();
|
||||
for (const eventName of ALL_EXPECTED_EVENTS) {
|
||||
const entry = parsed.hooks[eventName][0];
|
||||
assert.ok(entry.bash.includes('"additionalContext"'),
|
||||
`${eventName} bash body must emit the additionalContext JSON envelope`);
|
||||
assert.ok(!/hooks\/gsd-[\w-]+\.(js|cjs|sh)/.test(entry.bash),
|
||||
`${eventName} bash body must not reference an external hook script (cannot dangle)`);
|
||||
bashBodies.add(entry.bash);
|
||||
}
|
||||
assert.strictEqual(bashBodies.size, ALL_EXPECTED_EVENTS.length,
|
||||
'each event must emit its own distinct advisory command, not a copy-pasted duplicate');
|
||||
});
|
||||
|
||||
test('a runtime whose hook config omits the new events does NOT get them (descriptor-gated, not a global default)', (t) => {
|
||||
const { configDir, root } = runMinimalInstall({ runtime: 'claude', scope: 'global' });
|
||||
t.after(() => cleanup(root));
|
||||
|
||||
// Claude uses settings.json, not hooks/gsd-session.json — the Copilot
|
||||
// multi-event bus additions are self-contained to buildCopilotHookConfig
|
||||
// and must not leak into another runtime's hook surface.
|
||||
const copilotHookPath = path.join(configDir, 'hooks', 'gsd-session.json');
|
||||
assert.ok(!fs.existsSync(copilotHookPath),
|
||||
'claude must not have a Copilot-style hooks/gsd-session.json file');
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// UPGRADE 2: dispatch.background negotiation — documented true survives,
|
||||
// exceeding the declarative-cli profile baseline. No agent-file/frontmatter
|
||||
// change; negotiated-contract only (Copilot .agent.md has no background field).
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test("copilot classifies as the 'declarative-cli' profile, whose baseline dispatch.background is false", () => {
|
||||
assert.equal(profileOf(COPILOT_AXES), 'declarative-cli');
|
||||
assert.equal(PROFILE_BASELINES['declarative-cli'].dispatch.background, false,
|
||||
'sanity: the declarative-cli baseline is false — copilot legitimately exceeds it');
|
||||
});
|
||||
|
||||
test('negotiateHostCapabilities surfaces copilot\'s documented dispatch.background:true with no downgrade warning (UPGRADE 2)', () => {
|
||||
assert.equal(COPILOT_AXES.dispatch.background, true,
|
||||
'sanity: the descriptor declares dispatch.background: true (documented, not undocumented)');
|
||||
|
||||
const { effective, warnings } = negotiateHostCapabilities(COPILOT_AXES);
|
||||
|
||||
assert.equal(effective.dispatch.background, true,
|
||||
'a documented true value must survive negotiation, exceeding the declarative-cli baseline of false');
|
||||
assert.ok(
|
||||
!warnings.some((w) => w.includes('dispatch.background')),
|
||||
`no warning may be raised for the documented dispatch.background axis, got: ${JSON.stringify(warnings)}`,
|
||||
);
|
||||
});
|
||||
|
||||
test('copilot .agent.md frontmatter has no background-dispatch field (negotiated contract only, sanity)', (t) => {
|
||||
const { configDir, root } = runMinimalInstall({ runtime: 'copilot', scope: 'global' });
|
||||
t.after(() => cleanup(root));
|
||||
|
||||
const agentsDir = path.join(configDir, 'agents');
|
||||
const agentFiles = fs.readdirSync(agentsDir).filter((f) => f.endsWith('.agent.md'));
|
||||
assert.ok(agentFiles.length > 0, 'at least one .agent.md must be installed');
|
||||
|
||||
const sample = fs.readFileSync(path.join(agentsDir, agentFiles[0]), 'utf8');
|
||||
const frontmatterMatch = sample.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
||||
assert.ok(frontmatterMatch, 'agent file must have a frontmatter block');
|
||||
assert.ok(!/^background:/m.test(frontmatterMatch[1]),
|
||||
'UPGRADE 2 must not add a background: frontmatter field — Copilot .agent.md has no such field');
|
||||
});
|
||||
164
tests/declarative-reference-copilot.test.cjs
Normal file
164
tests/declarative-reference-copilot.test.cjs
Normal file
@@ -0,0 +1,164 @@
|
||||
// allow-test-rule: structural-regression-guard — AC2: assert no `runtime === 'copilot'` string-equality branch, no live `isCopilot` read remains in bin/install.js, src/install-engine.cts, src/surface.cts, or src/runtime-artifact-conversion.cts — a source-text property, so source-grep is the faithful check (#2099, expanded #2103)
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Declarative reference host — GitHub Copilot (#2099 / ADR-1239 EoS).
|
||||
*
|
||||
* Copilot already installs through the descriptor-driven artifactLayout
|
||||
* (skills/agents, each with a named `converter`), and its capability.json
|
||||
* already declares `hostIntegration` + `dispatch` axes. Issue #2099's
|
||||
* literal premises about a "legacy exclusion list" / writesSharedSettings
|
||||
* mismatch and a hardcoded RUNTIME_CONTENT_DISPATCH branch were WRONG (see
|
||||
* the deep-research writeup on the issue) — this migration instead folds
|
||||
* the real residual `isCopilot` branches:
|
||||
* 1. src/install-engine.cts's `.agent.md` destination-suffix rename —
|
||||
* folded onto `hostBehaviors.agentFileExtension`.
|
||||
* 2. bin/install.js uninstall's two Copilot side-effect branches
|
||||
* (repo-root AGENTS.md cleanup + copilot-instructions.md/hook
|
||||
* cleanup) — folded onto `resolveInstallPlan(runtime).installSurface
|
||||
* === 'copilot-instructions'` (unique to copilot, so byte-parity).
|
||||
* 3. bin/install.js's two `skipSharedHooksInstall` checks — folded onto
|
||||
* `hostBehaviors.skipSharedHooksInstall:true` (copilot's golden has
|
||||
* only hooks/gsd-session.json, no shared gsd-*.js scripts).
|
||||
* 4. bin/install.js's dead legacy agent-converter dispatch arm in the
|
||||
* inline agent-copy loop — unreachable because copilot is a member of
|
||||
* `_DESCRIPTOR_AGENTS_RUNTIMES` (installRuntimeArtifacts already wrote
|
||||
* the agents before that loop runs) — deleted outright.
|
||||
*
|
||||
* This test is the reference-host dogfood mirroring
|
||||
* tests/declarative-reference-codebuddy.test.cjs: it (1) classifies
|
||||
* Copilot's profile via profileOf, (2) confirms the public declarative
|
||||
* adapter classifies it as declarative, (3) round-trips a real install
|
||||
* proving a gsd agent/skill surface is emitted, (4) proves negotiation
|
||||
* fails CLOSED on a corrupted descriptor, (5) proves the validator accepts
|
||||
* the descriptor, and (6) source-greps the folded modules for the retired
|
||||
* `isCopilot` branches (AC2).
|
||||
*
|
||||
* UPGRADE 1 (multi-event hook bus) + UPGRADE 2 (dispatch.background
|
||||
* negotiation) live-install coverage is in tests/copilot-upgrades.test.cjs
|
||||
* — not duplicated here.
|
||||
*/
|
||||
|
||||
const { test, before } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const { execFileSync } = require('node:child_process');
|
||||
|
||||
const {
|
||||
profileOf,
|
||||
negotiateHostCapabilities,
|
||||
PROFILE_BASELINES,
|
||||
UNDOCUMENTED,
|
||||
} = require('../gsd-core/bin/lib/host-integration.cjs');
|
||||
const { validateCapability } = require('../gsd-core/bin/lib/capability-validator.cjs');
|
||||
const { createDeclarativeAdapter } = require('../gsd-core/bin/lib/adapter-declarative.cjs');
|
||||
const { cleanup } = require('./helpers.cjs');
|
||||
const { walk, runMinimalInstall, BUILD_SCRIPT } = require('./helpers/install-shared.cjs');
|
||||
|
||||
const DESC = path.join(__dirname, '..', 'capabilities', 'copilot', 'capability.json');
|
||||
const COPILOT_CAP = JSON.parse(fs.readFileSync(DESC, 'utf8'));
|
||||
const COPILOT_AXES = COPILOT_CAP.runtime.hostIntegration;
|
||||
|
||||
// hooks/dist is gitignored and built (mirrors golden-install-parity harness).
|
||||
before(() => {
|
||||
execFileSync(process.execPath, [BUILD_SCRIPT], { encoding: 'utf-8', stdio: 'pipe' });
|
||||
});
|
||||
|
||||
test('Copilot classifies as the declarative-cli reference profile (profileOf)', () => {
|
||||
const desc = JSON.parse(fs.readFileSync(DESC, 'utf8'));
|
||||
const axes = desc.runtime.hostIntegration;
|
||||
assert.ok(axes && axes.embeddingMode, 'copilot descriptor declares hostIntegration axes');
|
||||
assert.equal(profileOf(axes), 'declarative-cli',
|
||||
'Copilot is a Declarative-CLI host');
|
||||
});
|
||||
|
||||
test('the public declarative adapter classifies Copilot as a declarative host', () => {
|
||||
const adapter = createDeclarativeAdapter({ runtime: 'copilot' });
|
||||
assert.equal(adapter.kind, 'declarative');
|
||||
assert.equal(adapter.runtime, 'copilot');
|
||||
assert.equal(typeof adapter.install, 'function');
|
||||
assert.equal(typeof adapter.uninstall, 'function');
|
||||
});
|
||||
|
||||
test('a real Copilot install emits a gsd agent/skill surface (invocable)', () => {
|
||||
const { configDir, root } = runMinimalInstall({ runtime: 'copilot', scope: 'global' });
|
||||
try {
|
||||
const files = walk(configDir);
|
||||
assert.ok(files.length > 0, 'install must emit artifacts');
|
||||
const gsdSurface = files.filter((f) => /gsd/i.test(path.relative(configDir, f)));
|
||||
assert.ok(gsdSurface.length > 0,
|
||||
'install must emit a gsd agent/skill surface (declarative reference)');
|
||||
const agentsDir = path.join(configDir, 'agents');
|
||||
assert.ok(fs.existsSync(agentsDir), 'agents/ directory must exist');
|
||||
const agentFiles = fs.readdirSync(agentsDir)
|
||||
.filter((f) => f.startsWith('gsd-') && f.endsWith('.agent.md'));
|
||||
assert.ok(agentFiles.length > 0, 'agents/ must contain gsd-*.agent.md files');
|
||||
const skillsDir = path.join(configDir, 'skills');
|
||||
assert.ok(fs.existsSync(skillsDir), 'skills/ directory must exist');
|
||||
} finally {
|
||||
cleanup(root);
|
||||
}
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// #2099 EoS/copilot — fail-closed negotiation + validator acceptance +
|
||||
// the folded descriptor (mirrors codebuddy/antigravity/qwen reference tests).
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test('negotiateHostCapabilities never throws for copilot, even fully corrupted', () => {
|
||||
assert.doesNotThrow(() => negotiateHostCapabilities({}));
|
||||
assert.doesNotThrow(() => negotiateHostCapabilities({ ...COPILOT_AXES, embeddingMode: UNDOCUMENTED }));
|
||||
assert.doesNotThrow(() => negotiateHostCapabilities({ ...COPILOT_AXES, embeddingMode: 'future-unknown' }));
|
||||
assert.doesNotThrow(() => negotiateHostCapabilities({ ...COPILOT_AXES, dispatch: 'corrupted-not-an-object' }));
|
||||
assert.doesNotThrow(() => negotiateHostCapabilities({ ...COPILOT_AXES, dispatch: { ...COPILOT_AXES.dispatch, maxDepth: 'not-a-number' } }));
|
||||
});
|
||||
|
||||
test('a partial/empty copilot descriptor degrades to the safe floor, not the declarative-cli baseline', () => {
|
||||
const result = negotiateHostCapabilities({});
|
||||
assert.equal(result.effective.embeddingMode, 'declarative', 'omitted embeddingMode degrades closed');
|
||||
assert.equal(result.effective.hookBus, 'none');
|
||||
assert.notDeepEqual(result.effective, PROFILE_BASELINES['declarative-cli']);
|
||||
assert.ok(result.warnings.length > 0);
|
||||
});
|
||||
|
||||
test('capabilities/copilot/capability.json validates — no errors', () => {
|
||||
const errors = validateCapability(COPILOT_CAP, 'copilot');
|
||||
assert.deepEqual(errors, [], `validateCapability must return no errors, got: ${JSON.stringify(errors)}`);
|
||||
});
|
||||
|
||||
// -- AC2: the hardcoded branches are retired across all folded modules ------
|
||||
|
||||
test('no `runtime === "copilot"` string-equality branch (nor live `isCopilot` read) remains in bin/install.js, src/install-engine.cts, src/surface.cts, or src/runtime-artifact-conversion.cts (AC2)', () => {
|
||||
const strip = (src) => src
|
||||
.replace(/\/\*[\s\S]*?\*\//g, '')
|
||||
.replace(/\/\/[^\r\n]*/g, '')
|
||||
.replace(/`[^`]*`/g, '');
|
||||
const repoRoot = path.join(__dirname, '..');
|
||||
const files = [
|
||||
path.join(repoRoot, 'bin', 'install.js'),
|
||||
path.join(repoRoot, 'src', 'install-engine.cts'),
|
||||
path.join(repoRoot, 'src', 'surface.cts'),
|
||||
path.join(repoRoot, 'src', 'runtime-artifact-conversion.cts'),
|
||||
];
|
||||
for (const file of files) {
|
||||
const src = fs.readFileSync(file, 'utf8');
|
||||
const stripped = strip(src);
|
||||
|
||||
// NIT-1: catch both quote styles (single- and double-quoted 'copilot').
|
||||
const eqOffenders = stripped.match(/runtime\s*[!=]==\s*["']copilot["']/g) || [];
|
||||
assert.deepEqual(eqOffenders, [],
|
||||
`AC2: no hardcoded runtime==='copilot' branch may remain in ${path.relative(repoRoot, file)}; found: ${eqOffenders.join(', ')}`);
|
||||
|
||||
// Excludes legit enumeration sites: --copilot CLI flag parsing, the
|
||||
// '7':'copilot' numbered-menu map, allRuntimes/_DESCRIPTOR_AGENTS_RUNTIMES
|
||||
// set literals, config-dir lists, the copilot conversion FUNCTION names
|
||||
// (convertClaudeCommandToCopilotSkill / convertClaudeAgentToCopilotAgent /
|
||||
// convertCopilotToolName), and installSurface==='copilot-instructions'
|
||||
// (a descriptor-field string, not a runtime literal) — none of those
|
||||
// contain the token `isCopilot`, so a literal-word match is precise here.
|
||||
const isCopilotHits = stripped.match(/\bisCopilot\b/g) || [];
|
||||
assert.deepEqual(isCopilotHits, [],
|
||||
`AC2: no live isCopilot read may remain in ${path.relative(repoRoot, file)}; found ${isCopilotHits.length} occurrence(s)`);
|
||||
}
|
||||
});
|
||||
180
tests/declarative-reference-windsurf.test.cjs
Normal file
180
tests/declarative-reference-windsurf.test.cjs
Normal file
@@ -0,0 +1,180 @@
|
||||
// allow-test-rule: structural-regression-guard — AC2: assert no `runtime === 'windsurf'` string-equality branch, no live `isWindsurf` read remains in bin/install.js, src/install-engine.cts, src/surface.cts, or src/runtime-artifact-conversion.cts — a source-text property, so source-grep is the faithful check (#2100)
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Declarative reference host — Windsurf (#2100 / ADR-1239 EoS).
|
||||
*
|
||||
* STAGE 1 (folds): Windsurf already installs through the descriptor-driven
|
||||
* artifactLayout (agents/commands, each with a named `converter`), and its
|
||||
* capability.json already declares `hostIntegration` + `hostBehaviors` axes
|
||||
* (skipSharedHooksInstall, legacyDevinSkillsCleanup,
|
||||
* installsCommandBodiesForWorkflowDelegation, verificationStyle). Every
|
||||
* former `isWindsurf` branch in bin/install.js was folded onto those
|
||||
* descriptor axes (see the `#2100: isWindsurf dropped` comments left at the
|
||||
* fold sites).
|
||||
*
|
||||
* STAGE 2 (this file's focus, HOOK-BRIDGE): Windsurf's `hooksSurface` moved
|
||||
* from `"none"` to `"windsurf-hooks-json"` — GSD's write/command safety
|
||||
* guards are now wired into Cascade's native `.windsurf/hooks.json` /
|
||||
* `~/.codeium/windsurf/hooks.json` hook bus via `writeWindsurfHooksJson`
|
||||
* (mirrors `writeCursorHooksJson`'s infra shape, but Cascade blocks via EXIT
|
||||
* CODE 2 + stderr, not Cursor's stdout-JSON `{block,reason}` form). Only 2 of
|
||||
* Cascade's documented hook events (pre_write_code, pre_run_command) have a
|
||||
* blocking counterpart wired — Cascade has no context-injection channel, so
|
||||
* the 4 advisory events GSD registers on Cursor have no Windsurf analog.
|
||||
* Hook-bus mechanics (writer/reconcile/remove + the 2 guard scripts spawned
|
||||
* directly) are covered in tests/windsurf-hooks-bridge.test.cjs — not
|
||||
* duplicated here.
|
||||
*
|
||||
* This test is the reference-host dogfood mirroring
|
||||
* tests/declarative-reference-copilot.test.cjs: it (1) classifies Windsurf's
|
||||
* profile via profileOf, (2) confirms the public declarative adapter
|
||||
* classifies it as declarative, (3) round-trips a real install proving a
|
||||
* gsd agent surface is emitted, (4) proves negotiation fails CLOSED on a
|
||||
* corrupted descriptor, (5) proves the validator accepts the descriptor,
|
||||
* (6) asserts the new hooksSurface value + GATE A membership, and (7)
|
||||
* source-greps the folded modules for the retired `isWindsurf` branches (AC2).
|
||||
*/
|
||||
|
||||
const { test, before } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const { execFileSync } = require('node:child_process');
|
||||
|
||||
const {
|
||||
profileOf,
|
||||
negotiateHostCapabilities,
|
||||
PROFILE_BASELINES,
|
||||
UNDOCUMENTED,
|
||||
} = require('../gsd-core/bin/lib/host-integration.cjs');
|
||||
const { validateCapability } = require('../gsd-core/bin/lib/capability-validator.cjs');
|
||||
const { createDeclarativeAdapter } = require('../gsd-core/bin/lib/adapter-declarative.cjs');
|
||||
const {
|
||||
resolveRuntimeConfigIntent,
|
||||
resolveInstallPlan,
|
||||
} = require('../gsd-core/bin/lib/runtime-config-adapter-registry.cjs');
|
||||
const { cleanup } = require('./helpers.cjs');
|
||||
const { walk, runMinimalInstall, BUILD_SCRIPT } = require('./helpers/install-shared.cjs');
|
||||
|
||||
const DESC = path.join(__dirname, '..', 'capabilities', 'windsurf', 'capability.json');
|
||||
const WINDSURF_CAP = JSON.parse(fs.readFileSync(DESC, 'utf8'));
|
||||
const WINDSURF_AXES = WINDSURF_CAP.runtime.hostIntegration;
|
||||
|
||||
// hooks/dist is gitignored and built (mirrors golden-install-parity harness).
|
||||
before(() => {
|
||||
execFileSync(process.execPath, [BUILD_SCRIPT], { encoding: 'utf-8', stdio: 'pipe' });
|
||||
});
|
||||
|
||||
test('Windsurf classifies as the declarative-cli reference profile (profileOf)', () => {
|
||||
const desc = JSON.parse(fs.readFileSync(DESC, 'utf8'));
|
||||
const axes = desc.runtime.hostIntegration;
|
||||
assert.ok(axes && axes.embeddingMode, 'windsurf descriptor declares hostIntegration axes');
|
||||
assert.equal(profileOf(axes), 'declarative-cli', 'Windsurf is a Declarative-CLI host');
|
||||
});
|
||||
|
||||
test('the public declarative adapter classifies Windsurf as a declarative host', () => {
|
||||
const adapter = createDeclarativeAdapter({ runtime: 'windsurf' });
|
||||
assert.equal(adapter.kind, 'declarative');
|
||||
assert.equal(adapter.runtime, 'windsurf');
|
||||
assert.equal(typeof adapter.install, 'function');
|
||||
assert.equal(typeof adapter.uninstall, 'function');
|
||||
});
|
||||
|
||||
test('a real Windsurf install emits a gsd agent surface (invocable)', () => {
|
||||
const { configDir, root } = runMinimalInstall({ runtime: 'windsurf', scope: 'global' });
|
||||
try {
|
||||
const files = walk(configDir);
|
||||
assert.ok(files.length > 0, 'install must emit artifacts');
|
||||
const gsdSurface = files.filter((f) => /gsd/i.test(path.relative(configDir, f)));
|
||||
assert.ok(gsdSurface.length > 0, 'install must emit a gsd agent surface (declarative reference)');
|
||||
const agentsDir = path.join(configDir, 'agents');
|
||||
assert.ok(fs.existsSync(agentsDir), 'agents/ directory must exist');
|
||||
const agentFiles = fs.readdirSync(agentsDir).filter((f) => f.startsWith('gsd-') && f.endsWith('.md'));
|
||||
assert.ok(agentFiles.length > 0, 'agents/ must contain gsd-*.md files');
|
||||
} finally {
|
||||
cleanup(root);
|
||||
}
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// #2100 EoS/windsurf — fail-closed negotiation + validator acceptance +
|
||||
// the folded descriptor (mirrors codebuddy/antigravity/qwen/copilot reference tests).
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test('negotiateHostCapabilities never throws for windsurf, even fully corrupted', () => {
|
||||
assert.doesNotThrow(() => negotiateHostCapabilities({}));
|
||||
assert.doesNotThrow(() => negotiateHostCapabilities({ ...WINDSURF_AXES, embeddingMode: UNDOCUMENTED }));
|
||||
assert.doesNotThrow(() => negotiateHostCapabilities({ ...WINDSURF_AXES, embeddingMode: 'future-unknown' }));
|
||||
assert.doesNotThrow(() => negotiateHostCapabilities({ ...WINDSURF_AXES, dispatch: 'corrupted-not-an-object' }));
|
||||
assert.doesNotThrow(() => negotiateHostCapabilities({ ...WINDSURF_AXES, dispatch: { ...WINDSURF_AXES.dispatch, maxDepth: 'not-a-number' } }));
|
||||
});
|
||||
|
||||
test('a partial/empty windsurf descriptor degrades to the safe floor, not the declarative-cli baseline', () => {
|
||||
const result = negotiateHostCapabilities({});
|
||||
assert.equal(result.effective.embeddingMode, 'declarative', 'omitted embeddingMode degrades closed');
|
||||
assert.equal(result.effective.hookBus, 'none');
|
||||
assert.notDeepEqual(result.effective, PROFILE_BASELINES['declarative-cli']);
|
||||
assert.ok(result.warnings.length > 0);
|
||||
});
|
||||
|
||||
test('capabilities/windsurf/capability.json validates — no errors', () => {
|
||||
const errors = validateCapability(WINDSURF_CAP, 'windsurf');
|
||||
assert.deepEqual(errors, [], `validateCapability must return no errors, got: ${JSON.stringify(errors)}`);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// #2100 Stage 2 — hooksSurface moved from 'none' to 'windsurf-hooks-json'
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test('windsurf descriptor declares hooksSurface: windsurf-hooks-json', () => {
|
||||
assert.equal(WINDSURF_CAP.runtime.hooksSurface, 'windsurf-hooks-json');
|
||||
});
|
||||
|
||||
test('windsurf installSurface stays profile-marker-only (unchanged by Stage 2)', () => {
|
||||
const intent = resolveRuntimeConfigIntent('windsurf');
|
||||
assert.equal(intent.installSurface, 'profile-marker-only');
|
||||
assert.equal(intent.writesSharedSettings, false);
|
||||
assert.equal(intent.finishPermissionWriter, null);
|
||||
});
|
||||
|
||||
test('resolveInstallPlan(windsurf).hooksSurface is windsurf-hooks-json', () => {
|
||||
const plan = resolveInstallPlan('windsurf');
|
||||
assert.equal(plan.hooksSurface, 'windsurf-hooks-json');
|
||||
assert.equal(plan.installSurface, 'profile-marker-only');
|
||||
});
|
||||
|
||||
// -- AC2: the hardcoded branches are retired across all folded modules ------
|
||||
|
||||
test('no `runtime === "windsurf"` string-equality branch (nor live `isWindsurf` read) remains in bin/install.js, src/install-engine.cts, src/surface.cts, or src/runtime-artifact-conversion.cts (AC2)', () => {
|
||||
const strip = (src) => src
|
||||
.replace(/\/\*[\s\S]*?\*\//g, '')
|
||||
.replace(/\/\/[^\r\n]*/g, '')
|
||||
.replace(/`[^`]*`/g, '');
|
||||
const repoRoot = path.join(__dirname, '..');
|
||||
const files = [
|
||||
path.join(repoRoot, 'bin', 'install.js'),
|
||||
path.join(repoRoot, 'src', 'install-engine.cts'),
|
||||
path.join(repoRoot, 'src', 'surface.cts'),
|
||||
path.join(repoRoot, 'src', 'runtime-artifact-conversion.cts'),
|
||||
];
|
||||
for (const file of files) {
|
||||
const src = fs.readFileSync(file, 'utf8');
|
||||
const stripped = strip(src);
|
||||
|
||||
const eqOffenders = stripped.match(/runtime\s*[!=]==\s*["']windsurf["']/g) || [];
|
||||
assert.deepEqual(eqOffenders, [],
|
||||
`AC2: no hardcoded runtime==='windsurf' branch may remain in ${path.relative(repoRoot, file)}; found: ${eqOffenders.join(', ')}`);
|
||||
|
||||
// Excludes legit enumeration sites: --windsurf CLI flag parsing, numbered-menu
|
||||
// maps, allRuntimes/_DESCRIPTOR_AGENTS_RUNTIMES set literals, config-dir
|
||||
// lists, the windsurf conversion FUNCTION names (convertClaudeAgentToWindsurfAgent
|
||||
// / convertClaudeCommandToWindsurfWorkflow), and hooksSurface==='windsurf-hooks-json'
|
||||
// / installSurface==='profile-marker-only' (descriptor-field strings, not
|
||||
// runtime literals) — none of those contain the token `isWindsurf`, so a
|
||||
// literal-word match is precise here.
|
||||
const isWindsurfHits = stripped.match(/\bisWindsurf\b/g) || [];
|
||||
assert.deepEqual(isWindsurfHits, [],
|
||||
`AC2: no live isWindsurf read may remain in ${path.relative(repoRoot, file)}; found ${isWindsurfHits.length} occurrence(s)`);
|
||||
}
|
||||
});
|
||||
211
tests/declarative-reference-zcode.test.cjs
Normal file
211
tests/declarative-reference-zcode.test.cjs
Normal file
@@ -0,0 +1,211 @@
|
||||
// allow-test-rule: structural-regression-guard — AC2: assert no `runtime === 'zcode'` string-equality branch, no live `isZcode` read remains in bin/install.js, src/install-engine.cts, src/surface.cts, or src/runtime-artifact-conversion.cts — a source-text property, so source-grep is the faithful check (#2101)
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Declarative reference host — ZCode (#2101 / ADR-1239 EoS).
|
||||
*
|
||||
* ZCode already installs through the descriptor-driven artifactLayout
|
||||
* (nested skills/, flat commands/, flat agents/, each with a named
|
||||
* `converter`), and its capability.json already declared `hostIntegration`
|
||||
* axes. Issue #2101 found ZCode was already descriptor-driven except for one
|
||||
* residual `isZcode` branch in bin/install.js: the shared-hooks-install
|
||||
* exclusion (`&& !isZcode`), kept hardcoded because zcode's `hostBehaviors`
|
||||
* block was previously empty. ZCode's golden install tree has ZERO hook
|
||||
* files (verified), so folding this onto `hostBehaviors.skipSharedHooksInstall`
|
||||
* is byte-parity — the same fold already done for
|
||||
* windsurf/copilot/cursor/cline/kilo/trae (#2089/#2090/#2093/#2094/#2099/#2100).
|
||||
*
|
||||
* This test is the reference-host dogfood mirroring
|
||||
* tests/declarative-reference-windsurf.test.cjs: it (1) classifies ZCode's
|
||||
* profile via profileOf, (2) confirms the public declarative adapter
|
||||
* classifies it as declarative, (3) round-trips a real install proving a
|
||||
* gsd surface is emitted, (4) proves negotiation fails CLOSED on a corrupted
|
||||
* descriptor, (5) proves the validator accepts the descriptor, and (6)
|
||||
* source-greps the folded modules for the retired `isZcode` branch (AC2).
|
||||
*
|
||||
* Both capability upgrades anticipated by the issue (hook automation via
|
||||
* ZCode's plugin Hook component; native MCP registration) remain BLOCKED —
|
||||
* ZCode's docs do not publish the on-disk config format/location/schema for
|
||||
* either surface (see docs/reference/host-integration-capability-matrix.md
|
||||
* ## zcode for the cited doc URLs and rationale). No upgrade code is added
|
||||
* here; implementing a guessed format would risk a false-green descriptor.
|
||||
*/
|
||||
|
||||
const { test, before } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const { execFileSync } = require('node:child_process');
|
||||
|
||||
const {
|
||||
profileOf,
|
||||
negotiateHostCapabilities,
|
||||
PROFILE_BASELINES,
|
||||
UNDOCUMENTED,
|
||||
} = require('../gsd-core/bin/lib/host-integration.cjs');
|
||||
const { validateCapability } = require('../gsd-core/bin/lib/capability-validator.cjs');
|
||||
const { createDeclarativeAdapter } = require('../gsd-core/bin/lib/adapter-declarative.cjs');
|
||||
const { cleanup } = require('./helpers.cjs');
|
||||
const { walk, runMinimalInstall, BUILD_SCRIPT } = require('./helpers/install-shared.cjs');
|
||||
|
||||
const DESC = path.join(__dirname, '..', 'capabilities', 'zcode', 'capability.json');
|
||||
const ZCODE_CAP = JSON.parse(fs.readFileSync(DESC, 'utf8'));
|
||||
const ZCODE_AXES = ZCODE_CAP.runtime.hostIntegration;
|
||||
|
||||
// hooks/dist is gitignored and built (mirrors golden-install-parity harness).
|
||||
before(() => {
|
||||
execFileSync(process.execPath, [BUILD_SCRIPT], { encoding: 'utf-8', stdio: 'pipe' });
|
||||
});
|
||||
|
||||
test('ZCode classifies as the declarative-cli reference profile (profileOf)', () => {
|
||||
const desc = JSON.parse(fs.readFileSync(DESC, 'utf8'));
|
||||
const axes = desc.runtime.hostIntegration;
|
||||
assert.ok(axes && axes.embeddingMode, 'zcode descriptor declares hostIntegration axes');
|
||||
assert.equal(profileOf(axes), 'declarative-cli', 'ZCode is a Declarative-CLI host');
|
||||
});
|
||||
|
||||
test('the public declarative adapter classifies ZCode as a declarative host', () => {
|
||||
const adapter = createDeclarativeAdapter({ runtime: 'zcode' });
|
||||
assert.equal(adapter.kind, 'declarative');
|
||||
assert.equal(adapter.runtime, 'zcode');
|
||||
assert.equal(typeof adapter.install, 'function');
|
||||
assert.equal(typeof adapter.uninstall, 'function');
|
||||
});
|
||||
|
||||
test('a real ZCode install emits an invocable gsd skill/command/agent surface', () => {
|
||||
const { configDir, root } = runMinimalInstall({ runtime: 'zcode', scope: 'global' });
|
||||
try {
|
||||
const files = walk(configDir);
|
||||
assert.ok(files.length > 0, 'install must emit artifacts');
|
||||
const gsdSurface = files.filter((f) => /gsd/i.test(path.relative(configDir, f)));
|
||||
assert.ok(gsdSurface.length > 0, 'install must emit a gsd surface (declarative reference)');
|
||||
|
||||
// ZCode's artifactLayout (capabilities/zcode/capability.json) declares
|
||||
// nested skills/, flat commands/, and flat agents/ — verify all three.
|
||||
const skillsDir = path.join(configDir, 'skills');
|
||||
assert.ok(fs.existsSync(skillsDir), 'skills/ directory must exist');
|
||||
const skillDirs = fs.readdirSync(skillsDir, { withFileTypes: true })
|
||||
.filter((e) => e.isDirectory() && e.name.startsWith('gsd-'));
|
||||
assert.ok(skillDirs.length > 0, 'skills/ must contain nested gsd-* skill directories');
|
||||
const firstSkillFiles = fs.readdirSync(path.join(skillsDir, skillDirs[0].name));
|
||||
assert.ok(firstSkillFiles.includes('SKILL.md'), 'each nested skill dir must contain SKILL.md');
|
||||
|
||||
const commandsDir = path.join(configDir, 'commands');
|
||||
assert.ok(fs.existsSync(commandsDir), 'commands/ directory must exist');
|
||||
const cmdFiles = fs.readdirSync(commandsDir).filter((f) => f.startsWith('gsd-') && f.endsWith('.md'));
|
||||
assert.ok(cmdFiles.length > 0, 'commands/ must contain flat gsd-*.md slash commands');
|
||||
|
||||
const agentsDir = path.join(configDir, 'agents');
|
||||
assert.ok(fs.existsSync(agentsDir), 'agents/ directory must exist');
|
||||
const agentFiles = fs.readdirSync(agentsDir).filter((f) => f.startsWith('gsd-') && f.endsWith('.md'));
|
||||
assert.ok(agentFiles.length > 0, 'agents/ must contain flat gsd-*.md agent files');
|
||||
|
||||
// #2101: zcode's shared-hooks exclusion is now descriptor-driven
|
||||
// (hostBehaviors.skipSharedHooksInstall:true) — golden has zero hook
|
||||
// files, so no hooks/ directory should be installed.
|
||||
assert.ok(!fs.existsSync(path.join(configDir, 'hooks')), 'zcode install must not emit a hooks/ directory');
|
||||
} finally {
|
||||
cleanup(root);
|
||||
}
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// #2101 EoS/zcode — fail-closed negotiation + validator acceptance +
|
||||
// the folded descriptor (mirrors codebuddy/windsurf/augment reference tests).
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test('negotiateHostCapabilities never throws for zcode, even fully corrupted', () => {
|
||||
assert.doesNotThrow(() => negotiateHostCapabilities({}));
|
||||
assert.doesNotThrow(() => negotiateHostCapabilities({ ...ZCODE_AXES, embeddingMode: UNDOCUMENTED }));
|
||||
assert.doesNotThrow(() => negotiateHostCapabilities({ ...ZCODE_AXES, embeddingMode: 'future-unknown' }));
|
||||
assert.doesNotThrow(() => negotiateHostCapabilities({ ...ZCODE_AXES, dispatch: 'corrupted-not-an-object' }));
|
||||
assert.doesNotThrow(() => negotiateHostCapabilities({ ...ZCODE_AXES, dispatch: { ...ZCODE_AXES.dispatch, maxDepth: 'not-a-number' } }));
|
||||
});
|
||||
|
||||
test('a partial/empty zcode descriptor degrades to the safe floor, not the declarative-cli baseline', () => {
|
||||
const result = negotiateHostCapabilities({});
|
||||
assert.equal(result.effective.embeddingMode, 'declarative', 'omitted embeddingMode degrades closed');
|
||||
assert.equal(result.effective.hookBus, 'none');
|
||||
assert.notDeepEqual(result.effective, PROFILE_BASELINES['declarative-cli']);
|
||||
assert.ok(result.warnings.length > 0);
|
||||
});
|
||||
|
||||
// AC-style proof: the 2 still-`undocumented` dispatch sub-axes (nested/
|
||||
// maxDepth) must degrade to the most-restrictive KNOWN value, never their
|
||||
// optimistic value. Unlike augment (3 undocumented sub-axes) or antigravity
|
||||
// (4), zcode documents namedDispatch/background/subagentToolkit/
|
||||
// backgroundDispatch, leaving only nested + maxDepth undocumented. Real
|
||||
// values confirmed via:
|
||||
// node -e "const {negotiateHostCapabilities}=require('./gsd-core/bin/lib/host-integration.cjs');
|
||||
// const cap=require('./capabilities/zcode/capability.json');
|
||||
// console.log(negotiateHostCapabilities(cap.runtime.hostIntegration).effective.dispatch)"
|
||||
// -> { namedDispatch:true, nested:false, maxDepth:0, background:false, subagentToolkit:'full', backgroundDispatch:false }
|
||||
test("zcode's 2 still-undocumented dispatch sub-axes (nested/maxDepth) degrade to the most-restrictive known value, not their optimistic value", () => {
|
||||
// Sanity: the descriptor itself still declares these 2 as the undocumented
|
||||
// sentinel, while namedDispatch/background/subagentToolkit/backgroundDispatch
|
||||
// are documented.
|
||||
assert.equal(ZCODE_AXES.dispatch.nested, 'undocumented');
|
||||
assert.equal(ZCODE_AXES.dispatch.maxDepth, 'undocumented');
|
||||
assert.equal(ZCODE_AXES.dispatch.namedDispatch, true, 'sanity: namedDispatch is documented, not part of the undocumented set');
|
||||
assert.equal(ZCODE_AXES.dispatch.background, false, 'sanity: background is documented, not part of the undocumented set');
|
||||
assert.equal(ZCODE_AXES.dispatch.subagentToolkit, 'full', 'sanity: subagentToolkit is documented, not part of the undocumented set');
|
||||
assert.equal(ZCODE_AXES.dispatch.backgroundDispatch, false, 'sanity: backgroundDispatch is documented, not part of the undocumented set');
|
||||
|
||||
const { effective, warnings } = negotiateHostCapabilities(ZCODE_AXES);
|
||||
|
||||
assert.equal(effective.dispatch.nested, false, 'undocumented nested must degrade to false, never true');
|
||||
assert.equal(effective.dispatch.maxDepth, 0, 'undocumented maxDepth must degrade to 0, never -1/unbounded');
|
||||
|
||||
// namedDispatch/background/subagentToolkit/backgroundDispatch are documented
|
||||
// — they are trusted and survive negotiation unchanged.
|
||||
assert.equal(effective.dispatch.namedDispatch, true, "documented 'true' namedDispatch is trusted, unlike the undocumented sub-axes");
|
||||
assert.equal(effective.dispatch.background, false, "documented 'false' background is trusted");
|
||||
assert.equal(effective.dispatch.subagentToolkit, 'full', "documented 'full' subagentToolkit is trusted, unlike the undocumented sub-axes");
|
||||
assert.equal(effective.dispatch.backgroundDispatch, false, "documented 'false' backgroundDispatch is trusted");
|
||||
|
||||
assert.ok(
|
||||
warnings.some((w) => w.includes('dispatch.nested') && w.includes('undocumented')),
|
||||
'a warning must be raised for the undocumented dispatch.nested axis',
|
||||
);
|
||||
assert.ok(
|
||||
warnings.some((w) => w.includes('dispatch.maxDepth')),
|
||||
'a warning must be raised for the undocumented dispatch.maxDepth axis (reported as missing/non-number)',
|
||||
);
|
||||
});
|
||||
|
||||
test('capabilities/zcode/capability.json validates — no errors', () => {
|
||||
const errors = validateCapability(ZCODE_CAP, 'zcode');
|
||||
assert.deepEqual(errors, [], `validateCapability must return no errors, got: ${JSON.stringify(errors)}`);
|
||||
});
|
||||
|
||||
// -- AC2: the hardcoded branch is retired across all folded modules ---------
|
||||
|
||||
test('no `runtime === "zcode"` string-equality branch (nor live `isZcode` read) remains in bin/install.js, src/install-engine.cts, src/surface.cts, or src/runtime-artifact-conversion.cts (AC2)', () => {
|
||||
const strip = (src) => src
|
||||
.replace(/\/\*[\s\S]*?\*\//g, '')
|
||||
.replace(/\/\/[^\r\n]*/g, '')
|
||||
.replace(/`[^`]*`/g, '');
|
||||
const repoRoot = path.join(__dirname, '..');
|
||||
const files = [
|
||||
path.join(repoRoot, 'bin', 'install.js'),
|
||||
path.join(repoRoot, 'src', 'install-engine.cts'),
|
||||
path.join(repoRoot, 'src', 'surface.cts'),
|
||||
path.join(repoRoot, 'src', 'runtime-artifact-conversion.cts'),
|
||||
];
|
||||
for (const file of files) {
|
||||
const src = fs.readFileSync(file, 'utf8');
|
||||
const stripped = strip(src);
|
||||
|
||||
const eqOffenders = stripped.match(/runtime\s*[!=]==\s*["']zcode["']/g) || [];
|
||||
assert.deepEqual(eqOffenders, [],
|
||||
`AC2: no hardcoded runtime==='zcode' branch may remain in ${path.relative(repoRoot, file)}; found: ${eqOffenders.join(', ')}`);
|
||||
|
||||
// Excludes legit enumeration sites: --zcode CLI flag parsing, the numbered
|
||||
// menu map ('16': 'zcode'), the allRuntimes set literal, help/usage text,
|
||||
// and the `// #2101: isZcode dropped` comment (stripped above) — none of
|
||||
// those contain the token `isZcode`, so a literal-word match is precise.
|
||||
const isZcodeHits = stripped.match(/\bisZcode\b/g) || [];
|
||||
assert.deepEqual(isZcodeHits, [],
|
||||
`AC2: no live isZcode read may remain in ${path.relative(repoRoot, file)}; found ${isZcodeHits.length} occurrence(s)`);
|
||||
}
|
||||
});
|
||||
178
tests/fix-2133-fast-md-log-to-state-schema.test.cjs
Normal file
178
tests/fix-2133-fast-md-log-to-state-schema.test.cjs
Normal file
@@ -0,0 +1,178 @@
|
||||
/**
|
||||
* #2133 — fast.md log_to_state schema gate is unreachable.
|
||||
*
|
||||
* PR #85 added a column-count guard to fast.md's log_to_state step that used
|
||||
* `awk -F'|' '{print NF-1}'`. A markdown table header has both a leading and a
|
||||
* trailing pipe, so NF counts (real columns + 2) and NF-1 is always one too
|
||||
* high. The `-eq 5` test could therefore never hold for the 5-column header
|
||||
* quick.md writes, so /gsd-fast has never appended a Quick Task row since #85.
|
||||
*
|
||||
* This test does what the removed prose-regex test (bug-3805-*) did not: it
|
||||
* EXTRACTS the actual bash block deployed in fast.md and EXECUTES it against
|
||||
* real STATE.md fixtures, asserting on the filesystem result (row appended,
|
||||
* cell count aligned with header). It fails on the NF-1 bug and passes once
|
||||
* the count uses NF-2 and both 5/6-column schemas are accepted.
|
||||
*/
|
||||
|
||||
const { test, describe } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const { execFileSync } = require('node:child_process');
|
||||
const { createTempProject, cleanup } = require('./helpers.cjs');
|
||||
|
||||
const FAST_MD = path.join(__dirname, '..', 'gsd-core', 'workflows', 'fast.md');
|
||||
|
||||
const HEADER_5COL = '| # | Description | Date | Commit | Directory |';
|
||||
const SEP_5COL = '|---|-------------|------|--------|-----------|';
|
||||
const HEADER_6COL = '| # | Description | Date | Commit | Status | Directory |';
|
||||
const SEP_6COL = '|---|-------------|------|--------|--------|-----------|';
|
||||
|
||||
/**
|
||||
* Extract the ```bash block embedded in fast.md's <step name="log_to_state">.
|
||||
* This is the exact program the workflow runs — executing it is a behavioral
|
||||
* test of the deployed product, not a source-grep over its prose.
|
||||
*/
|
||||
function extractLogToStateBash() {
|
||||
const content = fs.readFileSync(FAST_MD, 'utf8');
|
||||
const stepTag = '<step name="log_to_state">';
|
||||
const stepStart = content.indexOf(stepTag);
|
||||
assert.notEqual(stepStart, -1, 'fast.md must contain a log_to_state step');
|
||||
const stepEnd = content.indexOf('</step>', stepStart);
|
||||
assert.notEqual(stepEnd, -1, 'log_to_state step must close');
|
||||
const step = content.slice(stepStart, stepEnd);
|
||||
const fenceStart = step.indexOf('```bash');
|
||||
assert.notEqual(fenceStart, -1, 'log_to_state step must contain a bash block');
|
||||
const codeStart = step.indexOf('\n', fenceStart) + 1;
|
||||
const fenceEnd = step.indexOf('\n```', codeStart);
|
||||
assert.notEqual(fenceEnd, -1, 'log_to_state bash block must close');
|
||||
return step.slice(codeStart, fenceEnd);
|
||||
}
|
||||
|
||||
function makeStateMd(headerLine, separatorLine, existingRows) {
|
||||
return [
|
||||
'# Project State',
|
||||
'',
|
||||
'### Blockers/Concerns',
|
||||
'',
|
||||
'None.',
|
||||
'',
|
||||
'### Quick Tasks Completed',
|
||||
'',
|
||||
headerLine,
|
||||
separatorLine,
|
||||
...existingRows,
|
||||
'',
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
/** Count `|` chars on a line — the invariant cell-count signal. */
|
||||
function pipeCount(line) {
|
||||
return (line.match(/\|/g) || []).length;
|
||||
}
|
||||
|
||||
/** Data rows = lines starting with `|` that are not the separator or header. */
|
||||
function dataRows(content) {
|
||||
return content.split(/\r?\n/).filter((l) => {
|
||||
if (!l.startsWith('|')) return false;
|
||||
if (/^[|][-: |]*[|]$/.test(l)) return false; // separator
|
||||
if (/Description/.test(l)) return false; // header
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
describe('#2133 fast.md log_to_state schema gate', () => {
|
||||
const bashBlock = extractLogToStateBash();
|
||||
const TASK_DESC = 'sample inline fix';
|
||||
|
||||
/**
|
||||
* Run the extracted log_to_state bash against a temp project's STATE.md and
|
||||
* return the post-run file content + captured stdout.
|
||||
*/
|
||||
function runAgainst(headerLine, separatorLine, existingRows) {
|
||||
const tmpDir = createTempProject('fix-2133-');
|
||||
const statePath = path.join(tmpDir, '.planning', 'STATE.md');
|
||||
fs.writeFileSync(statePath, makeStateMd(headerLine, separatorLine, existingRows));
|
||||
// Cleanup is bound by each caller via t.after(tmpDir).
|
||||
const stdout = execFileSync('bash', ['-c', bashBlock], {
|
||||
cwd: tmpDir,
|
||||
env: { ...process.env, TASK: TASK_DESC },
|
||||
encoding: 'utf8',
|
||||
});
|
||||
const after = fs.readFileSync(statePath, 'utf8');
|
||||
return { tmpDir, stdout, after };
|
||||
}
|
||||
|
||||
test('appends a 5-cell row to the 5-column (non-validate) schema (#27 stays fixed)', (t) => {
|
||||
const before = makeStateMd(HEADER_5COL, SEP_5COL, ['| 1 | earlier task | 2026-07-01 | deadbee | — |']);
|
||||
const { tmpDir, after } = runAgainst(HEADER_5COL, SEP_5COL, ['| 1 | earlier task | 2026-07-01 | deadbee | — |']);
|
||||
t.after(() => cleanup(tmpDir));
|
||||
|
||||
const rowsBefore = dataRows(before).length;
|
||||
const rowsAfter = dataRows(after).length;
|
||||
assert.equal(rowsAfter - rowsBefore, 1, 'exactly one row must be appended');
|
||||
|
||||
const appended = dataRows(after).slice(-1)[0];
|
||||
assert.equal(pipeCount(appended), pipeCount(HEADER_5COL),
|
||||
'appended row pipe-count must match the 5-column header (no malformed row)');
|
||||
assert.ok(appended.includes(TASK_DESC), 'appended row must carry the task description');
|
||||
});
|
||||
|
||||
test('appends a 6-cell row to the 6-column (validate, with Status) schema', (t) => {
|
||||
const before = makeStateMd(HEADER_6COL, SEP_6COL, []);
|
||||
const { tmpDir, after } = runAgainst(HEADER_6COL, SEP_6COL, []);
|
||||
t.after(() => cleanup(tmpDir));
|
||||
|
||||
const rowsBefore = dataRows(before).length;
|
||||
const rowsAfter = dataRows(after).length;
|
||||
assert.equal(rowsAfter - rowsBefore, 1, 'exactly one row must be appended to the 6-column table');
|
||||
|
||||
const appended = dataRows(after).slice(-1)[0];
|
||||
assert.equal(pipeCount(appended), pipeCount(HEADER_6COL),
|
||||
'appended row pipe-count must match the 6-column header (cell count aligned with header)');
|
||||
assert.ok(appended.includes(TASK_DESC), 'appended row must carry the task description');
|
||||
});
|
||||
|
||||
test('column count awk uses NF-2 (real columns — the off-by-one root cause)', () => {
|
||||
// The deployed bash must compute the real column count. A 5-column header
|
||||
// split on '|' yields NF=7; the correct real-column formula is NF-2=5.
|
||||
// (NF-1 was the off-by-one bug: it returned 6, making `-eq 5` unsatisfiable.)
|
||||
// Match the executable COL_COUNT assignment specifically — the explanatory
|
||||
// comment may still reference "NF-1" to document the history.
|
||||
assert.match(
|
||||
bashBlock,
|
||||
/COL_COUNT=\$\(.+awk -F'\|' '\{print NF-2\}'\)/,
|
||||
'COL_COUNT must be derived via awk NF-2 (NF-1 was the off-by-one bug)'
|
||||
);
|
||||
});
|
||||
|
||||
test('unrecognized schema still skips with a warning (safety guard intact)', (t) => {
|
||||
// A 3-column table quick.md never writes must NOT receive a row.
|
||||
const weirdHeader = '| Alpha | Beta | Gamma |';
|
||||
const weirdSep = '|-------|------|-------|';
|
||||
const before = makeStateMd(weirdHeader, weirdSep, []);
|
||||
const { tmpDir, stdout, after } = runAgainst(weirdHeader, weirdSep, []);
|
||||
t.after(() => cleanup(tmpDir));
|
||||
|
||||
assert.equal(dataRows(after).length, dataRows(before).length,
|
||||
'no row may be appended for an unrecognized schema');
|
||||
assert.ok(/unrecognized schema/i.test(stdout),
|
||||
'the unrecognized-schema warning must be emitted');
|
||||
});
|
||||
|
||||
test('no Quick Tasks table → silent no-op', (t) => {
|
||||
const tmpDir = createTempProject('fix-2133-noop-');
|
||||
t.after(() => cleanup(tmpDir));
|
||||
const statePath = path.join(tmpDir, '.planning', 'STATE.md');
|
||||
const before = '# Project State\n\n### Blockers/Concerns\n\nNone.\n';
|
||||
fs.writeFileSync(statePath, before);
|
||||
const stdout = execFileSync('bash', ['-c', bashBlock], {
|
||||
cwd: tmpDir,
|
||||
env: { ...process.env, TASK: TASK_DESC },
|
||||
encoding: 'utf8',
|
||||
});
|
||||
const after = fs.readFileSync(statePath, 'utf8');
|
||||
assert.equal(after, before, 'STATE.md must be untouched when no Quick Tasks table exists');
|
||||
assert.equal(stdout, '', 'no output when there is no table to update');
|
||||
});
|
||||
});
|
||||
120
tests/fix-2136-clock-local-today.test.cjs
Normal file
120
tests/fix-2136-clock-local-today.test.cjs
Normal file
@@ -0,0 +1,120 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* #2136 — operator-facing date-only fields must use the HOST-LOCAL calendar day,
|
||||
* not the UTC calendar day.
|
||||
*
|
||||
* `clock.today()` derived the day by slicing a UTC ISO instant
|
||||
* (`nowIso().split('T')[0]`), so in any negative-UTC-offset zone during UTC's
|
||||
* early hours `last_activity` named a day the operator had not reached yet — a
|
||||
* day AHEAD of `last_updated`'s local date, written by the same call.
|
||||
*
|
||||
* The fix adds `clock.localToday()` (host-local calendar day, honoring the same
|
||||
* GSD_NOW_MS pin) and routes operator-facing date-only fields through it. These
|
||||
* tests prove:
|
||||
* (1) realClock.localToday() returns the local day under a pinned instant + TZ;
|
||||
* (2) realClock.today() is unchanged (UTC) — internal/cosmetic stamps stay UTC;
|
||||
* (3) the fake clock mirrors localToday();
|
||||
* (4) the `last_activity` field is stamped from localToday, not today
|
||||
* (a split clock where the two differ proves the wiring).
|
||||
*/
|
||||
|
||||
const { describe, test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const { execFileSync } = require('node:child_process');
|
||||
const path = require('node:path');
|
||||
|
||||
const CLOCK_CJS = path.join(__dirname, '..', 'gsd-core', 'bin', 'lib', 'clock.cjs');
|
||||
const STATE_TRANSITION_CJS = path.join(__dirname, '..', 'gsd-core', 'bin', 'lib', 'state-transition.cjs');
|
||||
const { stateExtractField } = require('../gsd-core/bin/lib/state-document.cjs');
|
||||
const { makeFakeClock } = require('./helpers/clock.cjs');
|
||||
|
||||
// 2020-06-15T02:00:00.000Z. Under America/Chicago (UTC−5 in June) this instant
|
||||
// is 2020-06-14 21:00 local — the local calendar day is 2020-06-14 while the UTC
|
||||
// calendar day is 2020-06-15. This is the issue's exact repro instant.
|
||||
const PINNED_MS = '1592186400000';
|
||||
|
||||
/**
|
||||
* Run a one-liner in a FRESH subprocess so TZ is set at process start (Node
|
||||
* caches timezone; runtime mutation of process.env.TZ is unreliable across
|
||||
* versions). GSD_TEST_MODE + GSD_NOW_MS pin realClock via the documented seam.
|
||||
*/
|
||||
function clockInSubprocess(expr, env) {
|
||||
return execFileSync(process.execPath, ['-e', expr], {
|
||||
env,
|
||||
encoding: 'utf8',
|
||||
}).trim();
|
||||
}
|
||||
|
||||
describe('#2136 realClock.localToday() — host-local calendar day', () => {
|
||||
test('returns the LOCAL calendar day under a negative-UTC-offset zone', () => {
|
||||
const out = clockInSubprocess(
|
||||
`const {realClock}=require(${JSON.stringify(CLOCK_CJS)});process.stdout.write(realClock.localToday());`,
|
||||
{ PATH: process.env.PATH, GSD_TEST_MODE: '1', GSD_NOW_MS: PINNED_MS, TZ: 'America/Chicago' },
|
||||
);
|
||||
assert.strictEqual(out, '2020-06-14',
|
||||
'localToday() must be the local calendar day (2020-06-14 in Chicago), not the UTC day (2020-06-15)');
|
||||
});
|
||||
|
||||
test('realClock.today() is unchanged (UTC calendar day) — internal stamps stay UTC', () => {
|
||||
const out = clockInSubprocess(
|
||||
`const {realClock}=require(${JSON.stringify(CLOCK_CJS)});process.stdout.write(realClock.today());`,
|
||||
{ PATH: process.env.PATH, GSD_TEST_MODE: '1', GSD_NOW_MS: PINNED_MS, TZ: 'America/Chicago' },
|
||||
);
|
||||
assert.strictEqual(out, '2020-06-15',
|
||||
'today() must remain the UTC calendar day; only operator-facing fields moved to localToday()');
|
||||
});
|
||||
|
||||
test('localToday === today on a UTC host (no spurious divergence)', () => {
|
||||
const out = clockInSubprocess(
|
||||
`const {realClock}=require(${JSON.stringify(CLOCK_CJS)});process.stdout.write(realClock.localToday()+'|'+realClock.today());`,
|
||||
{ PATH: process.env.PATH, GSD_TEST_MODE: '1', GSD_NOW_MS: PINNED_MS, TZ: 'UTC' },
|
||||
);
|
||||
const [local, utc] = out.split('|');
|
||||
assert.strictEqual(local, '2020-06-15');
|
||||
assert.strictEqual(utc, '2020-06-15');
|
||||
});
|
||||
});
|
||||
|
||||
describe('#2136 makeFakeClock mirrors localToday()', () => {
|
||||
test('fake clock localToday() derives the local day from the pinned epoch', () => {
|
||||
const clock = makeFakeClock(Number(PINNED_MS));
|
||||
// The fake honors the host TZ of the test process; assert it returns a
|
||||
// YYYY-MM-DD string derived from local Date methods (structure + determinism
|
||||
// under the pinned epoch), mirroring realClock.localToday().
|
||||
assert.ok(/^\d{4}-\d{2}-\d{2}$/.test(clock.localToday()),
|
||||
'fake localToday() must return a YYYY-MM-DD string');
|
||||
assert.strictEqual(typeof clock.localToday, 'function',
|
||||
'makeFakeClock must expose localToday so it stays a drop-in Clock substitute');
|
||||
});
|
||||
});
|
||||
|
||||
describe('#2136 last_activity is stamped from localToday, not today', () => {
|
||||
// A clock where the UTC day and the local day DIFFER. If the transition core
|
||||
// still consulted today(), Last Activity would read the UTC value; the fix
|
||||
// routes it through localToday().
|
||||
const splitClock = Object.freeze({
|
||||
today: () => '2020-06-15',
|
||||
localToday: () => '2020-06-14',
|
||||
nowIso: () => '2020-06-15T02:00:00.000Z',
|
||||
});
|
||||
|
||||
test('sync transition writes the LOCAL day into Last Activity', () => {
|
||||
const { transitionCore } = require(STATE_TRANSITION_CJS);
|
||||
const input = [
|
||||
'# Project State',
|
||||
'',
|
||||
'**Total Plans in Phase:** 2',
|
||||
'**Last Activity:** 2020-06-10',
|
||||
'**Progress:** [████░░░░░░] 40%',
|
||||
'',
|
||||
].join('\n');
|
||||
const result = transitionCore(
|
||||
input,
|
||||
{ kind: 'sync', totalPlansInPhase: 5, percent: 60 },
|
||||
{ clock: splitClock, progressProvider: () => null },
|
||||
);
|
||||
assert.strictEqual(stateExtractField(result.content, 'Last Activity'), '2020-06-14',
|
||||
'Last Activity must use localToday() (2020-06-14), not today() (2020-06-15)');
|
||||
});
|
||||
});
|
||||
63
tests/fix-2138-ship-note-lost-on-merge.test.cjs
Normal file
63
tests/fix-2138-ship-note-lost-on-merge.test.cjs
Normal file
@@ -0,0 +1,63 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* #2138 — track_shipping ship-note must be pushed onto the PR branch.
|
||||
*
|
||||
* ship.md's track_shipping step committed the STATE ship-note (Phase N shipped —
|
||||
* PR #N) AFTER create_pr and never pushed it. The commit stayed local-only, so
|
||||
* when the GitHub PR merged (especially fast/auto-merge) the ship-note was not in
|
||||
* the source branch and never reached the default branch — STATE's ship-status
|
||||
* was silently lost.
|
||||
*
|
||||
* The fix pushes the ship-note commit onto the PR branch with a `[ci skip]`
|
||||
* trailer (honored by GitHub) so it lands on merge without triggering a redundant
|
||||
* pipeline. This test is a source-text regression guard: ship.md IS the product
|
||||
* the runtime loads, so asserting its track_shipping step pushes (with [ci skip])
|
||||
* guards the deployed contract.
|
||||
*/
|
||||
|
||||
const { describe, test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const SHIP_MD = path.join(__dirname, '..', 'gsd-core', 'workflows', 'ship.md');
|
||||
|
||||
function extractStep(name) {
|
||||
const content = fs.readFileSync(SHIP_MD, 'utf8');
|
||||
const open = `<step name="${name}">`;
|
||||
const start = content.indexOf(open);
|
||||
assert.notEqual(start, -1, `ship.md must contain a ${name} step`);
|
||||
const end = content.indexOf('</step>', start);
|
||||
assert.notEqual(end, -1, `${name} step must close`);
|
||||
return content.slice(start, end);
|
||||
}
|
||||
|
||||
describe('#2138 ship.md track_shipping pushes the ship-note onto the PR branch', () => {
|
||||
const step = extractStep('track_shipping');
|
||||
|
||||
test('track_shipping pushes the committed ship-note (not local-only)', () => {
|
||||
// The bug was that the ship-note commit was never pushed. The fix adds a
|
||||
// `git push origin ${CURRENT_BRANCH}` inside track_shipping.
|
||||
assert.ok(
|
||||
/git push origin \$\{CURRENT_BRANCH\}/.test(step),
|
||||
'track_shipping must push the ship-note commit onto the PR branch so it survives merge (#2138)',
|
||||
);
|
||||
});
|
||||
|
||||
test('the ship-note commit carries a [ci skip] trailer to avoid a redundant pipeline', () => {
|
||||
// GitHub honors `[ci skip]` / `[skip ci]`; the trailer suppresses the second
|
||||
// pipeline the post-create_pr push would otherwise trigger.
|
||||
assert.ok(
|
||||
/\[ci skip\]|\[skip ci\]/.test(step),
|
||||
'track_shipping ship-note commit must include a [ci skip] trailer',
|
||||
);
|
||||
});
|
||||
|
||||
test('the ship-note commit still records the phase + PR number in STATE', () => {
|
||||
assert.ok(
|
||||
/ship phase \$\{PHASE_NUMBER\}.*PR #\$\{PR_NUMBER\}/.test(step),
|
||||
'track_shipping must still commit the phase + PR-number ship-note into STATE',
|
||||
);
|
||||
});
|
||||
});
|
||||
44
tests/fix-2194-review-timeout-guidance.test.cjs
Normal file
44
tests/fix-2194-review-timeout-guidance.test.cjs
Normal file
@@ -0,0 +1,44 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* #2194 — review.md prompt-fed reviewers (Gemini/Claude/Codex) need explicit
|
||||
* Bash timeout guidance.
|
||||
*
|
||||
* Without it each lane inherits the host default (~2 min on Claude Code), so a
|
||||
* source-grounded review (~570s Codex xhigh, ~525s headless Claude) is killed
|
||||
* mid-review, its output is empty, and the cross-AI review silently proceeds
|
||||
* with fewer lanes. CodeRabbit and OpenCode already documented a timeout; the
|
||||
* four main lanes did not. review.md IS the product the runtime loads, so this
|
||||
* asserts the deployed text carries the guidance.
|
||||
*/
|
||||
|
||||
const { describe, test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const REVIEW_MD = path.join(__dirname, '..', 'gsd-core', 'workflows', 'review.md');
|
||||
|
||||
describe('#2194 review.md prompt-fed reviewer timeout guidance', () => {
|
||||
const content = fs.readFileSync(REVIEW_MD, 'utf-8');
|
||||
const sectionStart = content.indexOf('invoke in sequence');
|
||||
const section = sectionStart !== -1
|
||||
? content.slice(sectionStart, sectionStart + 3000)
|
||||
: '';
|
||||
|
||||
test('review.md has the reviewer-invocation section', () => {
|
||||
assert.notEqual(sectionStart, -1, 'review.md must contain the "invoke in sequence" section');
|
||||
});
|
||||
|
||||
test('the section carries Bash timeout guidance for the prompt-fed lanes', () => {
|
||||
assert.ok(/timeout/i.test(section),
|
||||
'the Gemini/Claude/Codex reviewer blocks must carry Bash timeout guidance');
|
||||
assert.ok(/900000|1200000/.test(section),
|
||||
'timeout guidance must recommend a high ms value (>= 900000) so a slow lane is not killed');
|
||||
});
|
||||
|
||||
test('a slow-lane empty output is framed as a timeout, not a crash', () => {
|
||||
assert.ok(/timeout.+not.+crash|not a crash/i.test(section),
|
||||
'the guidance must distinguish a timeout kill from a crash so operators do not misdiagnose (e.g. the Codex 0xc0000142 misattribution)');
|
||||
});
|
||||
});
|
||||
@@ -42,7 +42,7 @@
|
||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "6bba2b9c9fa47cb8",
|
||||
"gsd-core/bin/shared/model-catalog.json": "d0b59409a46a456a",
|
||||
"gsd-core/bin/shared/model-catalog.json": "b55176ca044728d3",
|
||||
"gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a",
|
||||
"gsd-core/bin/verify-reapply-patches.cjs": "8bc541aabc2e143c",
|
||||
"gsd-core/contexts/dev.md": "dcb0de9dce33cf41",
|
||||
@@ -155,7 +155,7 @@
|
||||
"gsd-core/templates/SECURITY.md": "b628f7f1c6d2328f",
|
||||
"gsd-core/templates/UAT.md": "68d32d1fea14e184",
|
||||
"gsd-core/templates/UI-SPEC.md": "7dd5c7cdc7ece0ec",
|
||||
"gsd-core/templates/VALIDATION.md": "6144951011cdca57",
|
||||
"gsd-core/templates/VALIDATION.md": "e4d0f1c48727fce3",
|
||||
"gsd-core/templates/claude-md.md": "8797c0c7da927c8e",
|
||||
"gsd-core/templates/codebase/architecture.md": "282db635ba093b1a",
|
||||
"gsd-core/templates/codebase/concerns.md": "e66c584daa636fc5",
|
||||
@@ -203,7 +203,7 @@
|
||||
"gsd-core/workflows/ai-integration-phase.md": "22f5466085c47c00",
|
||||
"gsd-core/workflows/analyze-dependencies.md": "77aff48f97fa6f1c",
|
||||
"gsd-core/workflows/audit-fix.md": "3ecffffe78021e0a",
|
||||
"gsd-core/workflows/audit-milestone.md": "77089ccd7b45c0f0",
|
||||
"gsd-core/workflows/audit-milestone.md": "280cd85908dd45cb",
|
||||
"gsd-core/workflows/audit-uat.md": "ea8ddd910ed16ba4",
|
||||
"gsd-core/workflows/autonomous.md": "603ce2019835f00e",
|
||||
"gsd-core/workflows/check-todos.md": "5a62092df800ad7c",
|
||||
@@ -242,7 +242,7 @@
|
||||
"gsd-core/workflows/execute-plan.md": "907af77eafc3d97b",
|
||||
"gsd-core/workflows/explore.md": "934c00f9f216dbdb",
|
||||
"gsd-core/workflows/extract-learnings.md": "167ea7f0e23bf496",
|
||||
"gsd-core/workflows/fast.md": "54fe93778b45a7eb",
|
||||
"gsd-core/workflows/fast.md": "0242082ca646819a",
|
||||
"gsd-core/workflows/forensics.md": "b64f0309b8c3fde1",
|
||||
"gsd-core/workflows/graduation.md": "a9d8f15ba81a993f",
|
||||
"gsd-core/workflows/health.md": "1ac4d567ee95acfb",
|
||||
@@ -285,14 +285,14 @@
|
||||
"gsd-core/workflows/remove-phase.md": "23b9eb0858a2535e",
|
||||
"gsd-core/workflows/remove-workspace.md": "d0bd7e0601138798",
|
||||
"gsd-core/workflows/resume-project.md": "98e2cf8908e73a52",
|
||||
"gsd-core/workflows/review.md": "9ff117dd12ce68ba",
|
||||
"gsd-core/workflows/review.md": "43c052bba1cbd4ac",
|
||||
"gsd-core/workflows/scan.md": "a7fecd67e5cd655f",
|
||||
"gsd-core/workflows/secure-phase.md": "96b199dfac00e60f",
|
||||
"gsd-core/workflows/session-report.md": "2e5b1205324ddefa",
|
||||
"gsd-core/workflows/settings-advanced.md": "289b3d653d18e284",
|
||||
"gsd-core/workflows/settings-advanced.md": "6d25100a9de15b31",
|
||||
"gsd-core/workflows/settings-integrations.md": "4d3001ad2b5dad8f",
|
||||
"gsd-core/workflows/settings.md": "8258f7bd3700d608",
|
||||
"gsd-core/workflows/ship.md": "984bd7660e7791fd",
|
||||
"gsd-core/workflows/ship.md": "82a63ca54f4b322f",
|
||||
"gsd-core/workflows/sketch-wrap-up.md": "0f842a609851a401",
|
||||
"gsd-core/workflows/sketch.md": "314b7d323c6b57eb",
|
||||
"gsd-core/workflows/smart-entry.md": "3ce5b6228238fdb6",
|
||||
@@ -308,7 +308,7 @@
|
||||
"gsd-core/workflows/ultraplan-phase.md": "d8e92b0b7214eba6",
|
||||
"gsd-core/workflows/undo.md": "6ab639d1fc7e0721",
|
||||
"gsd-core/workflows/update.md": "2c58df5e21c41c31",
|
||||
"gsd-core/workflows/validate-phase.md": "6c0ab739d15709fa",
|
||||
"gsd-core/workflows/validate-phase.md": "ae3f0180a2316fcd",
|
||||
"gsd-core/workflows/verify-phase.md": "0eefbb6bb1b0bed6",
|
||||
"gsd-core/workflows/verify-work.md": "59276d94999ee022",
|
||||
"hooks/gsd-check-update-worker.js": "fa301e6366270d5f",
|
||||
@@ -331,11 +331,13 @@
|
||||
"hooks/gsd-statusline.js": "8ae31be7a006204b",
|
||||
"hooks/gsd-update-banner.js": "55143a25f978f301",
|
||||
"hooks/gsd-validate-commit.sh": "bf5dd61d33cb3a38",
|
||||
"hooks/gsd-windsurf-pre-command.js": "948be1c6d14c79cd",
|
||||
"hooks/gsd-windsurf-pre-write.js": "92d4dbfbc36ab0cf",
|
||||
"hooks/gsd-workflow-guard.js": "91ae24a15d2bca6f",
|
||||
"hooks/gsd-worktree-path-guard.js": "838498aa91619740",
|
||||
"hooks/lib/git-cmd.js": "268ba15992ca0b23",
|
||||
"hooks/lib/gsd-graphify-rebuild.sh": "66af89601074d2a9",
|
||||
"hooks/managed-hooks-registry.cjs": "721d696556b7509f",
|
||||
"hooks/managed-hooks-registry.cjs": "82a4121cbcb82756",
|
||||
"mcp_config.json": "9956d6a6e88a49e1",
|
||||
"package.json": "dbf8353f77358bc1",
|
||||
"scripts/changeset/README.md": "86ff89331dfd94b2",
|
||||
@@ -411,7 +413,7 @@
|
||||
"skills/gsd-spec-phase/SKILL.md": "6b35d59f65e2e607",
|
||||
"skills/gsd-spike/SKILL.md": "63b5093cbc7978ca",
|
||||
"skills/gsd-stats/SKILL.md": "1cef40c0a0e7b19b",
|
||||
"skills/gsd-surface/SKILL.md": "963aa5a09d8e3695",
|
||||
"skills/gsd-surface/SKILL.md": "19e018ebe4c89857",
|
||||
"skills/gsd-thread/SKILL.md": "4d91aab9f5ed4ca7",
|
||||
"skills/gsd-ui-phase/SKILL.md": "b2412418ebf0dfb8",
|
||||
"skills/gsd-ui-review/SKILL.md": "e49734488684fd9a",
|
||||
|
||||
@@ -95,7 +95,7 @@
|
||||
"commands/gsd-spec-phase.md": "8bb332566911dfd9",
|
||||
"commands/gsd-spike.md": "a99190d9496c6ac0",
|
||||
"commands/gsd-stats.md": "58b4d86a5390e243",
|
||||
"commands/gsd-surface.md": "acab871ec856e353",
|
||||
"commands/gsd-surface.md": "12078dca898a6c16",
|
||||
"commands/gsd-thread.md": "51be0cdeb925ab28",
|
||||
"commands/gsd-ui-phase.md": "75d4be44797ccef7",
|
||||
"commands/gsd-ui-review.md": "9191082973068dbf",
|
||||
@@ -113,7 +113,7 @@
|
||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "6bba2b9c9fa47cb8",
|
||||
"gsd-core/bin/shared/model-catalog.json": "d0b59409a46a456a",
|
||||
"gsd-core/bin/shared/model-catalog.json": "b55176ca044728d3",
|
||||
"gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a",
|
||||
"gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904",
|
||||
"gsd-core/contexts/dev.md": "dcb0de9dce33cf41",
|
||||
@@ -226,7 +226,7 @@
|
||||
"gsd-core/templates/SECURITY.md": "b628f7f1c6d2328f",
|
||||
"gsd-core/templates/UAT.md": "9e296471b97ebcec",
|
||||
"gsd-core/templates/UI-SPEC.md": "7dd5c7cdc7ece0ec",
|
||||
"gsd-core/templates/VALIDATION.md": "f53e0ca061d3528e",
|
||||
"gsd-core/templates/VALIDATION.md": "6f0fe4214dff6dd1",
|
||||
"gsd-core/templates/claude-md.md": "d1d333e4b963c0d2",
|
||||
"gsd-core/templates/codebase/architecture.md": "6be88214162fdd89",
|
||||
"gsd-core/templates/codebase/concerns.md": "efa26d1fb5132f25",
|
||||
@@ -274,7 +274,7 @@
|
||||
"gsd-core/workflows/ai-integration-phase.md": "40c217869a06981f",
|
||||
"gsd-core/workflows/analyze-dependencies.md": "52942af10f140717",
|
||||
"gsd-core/workflows/audit-fix.md": "eedb2da4bffb7575",
|
||||
"gsd-core/workflows/audit-milestone.md": "6866ce260980c21e",
|
||||
"gsd-core/workflows/audit-milestone.md": "3835762c3147a927",
|
||||
"gsd-core/workflows/audit-uat.md": "fcfbec501620b564",
|
||||
"gsd-core/workflows/autonomous.md": "7e5683728ef33707",
|
||||
"gsd-core/workflows/check-todos.md": "bdeaf43f9c61e0cc",
|
||||
@@ -313,7 +313,7 @@
|
||||
"gsd-core/workflows/execute-plan.md": "2c412310dce31a0b",
|
||||
"gsd-core/workflows/explore.md": "6c04f2e658d93261",
|
||||
"gsd-core/workflows/extract-learnings.md": "b6f01ca3d8f58de4",
|
||||
"gsd-core/workflows/fast.md": "94136fb570d20a9d",
|
||||
"gsd-core/workflows/fast.md": "8878ec034b401f36",
|
||||
"gsd-core/workflows/forensics.md": "0d500a3f5ab26913",
|
||||
"gsd-core/workflows/graduation.md": "47f1594c88c08501",
|
||||
"gsd-core/workflows/health.md": "107e3c72e76d9535",
|
||||
@@ -356,14 +356,14 @@
|
||||
"gsd-core/workflows/remove-phase.md": "df9a45f0b1880999",
|
||||
"gsd-core/workflows/remove-workspace.md": "a7ca66db6b7c132c",
|
||||
"gsd-core/workflows/resume-project.md": "f28da1200e4545f4",
|
||||
"gsd-core/workflows/review.md": "93fe46bd0b5dd3d1",
|
||||
"gsd-core/workflows/review.md": "ad7c0f372ed986ae",
|
||||
"gsd-core/workflows/scan.md": "003883d71c37da7d",
|
||||
"gsd-core/workflows/secure-phase.md": "29fc6b62c5c5dc62",
|
||||
"gsd-core/workflows/session-report.md": "2e5b1205324ddefa",
|
||||
"gsd-core/workflows/settings-advanced.md": "b6fa466a953dd3a2",
|
||||
"gsd-core/workflows/settings-advanced.md": "414db4dbea97ba44",
|
||||
"gsd-core/workflows/settings-integrations.md": "70515c5838fb9826",
|
||||
"gsd-core/workflows/settings.md": "d96ddf01fb85e61e",
|
||||
"gsd-core/workflows/ship.md": "7b8fe9f89143e648",
|
||||
"gsd-core/workflows/ship.md": "b377dab2bc92a6e5",
|
||||
"gsd-core/workflows/sketch-wrap-up.md": "5f5ebb6a80d610c6",
|
||||
"gsd-core/workflows/sketch.md": "8319cedf3f93fc35",
|
||||
"gsd-core/workflows/smart-entry.md": "449238eb94abe187",
|
||||
@@ -379,7 +379,7 @@
|
||||
"gsd-core/workflows/ultraplan-phase.md": "0fb8291153e3937d",
|
||||
"gsd-core/workflows/undo.md": "96d2775f008b3a85",
|
||||
"gsd-core/workflows/update.md": "fd160e13f8b7e83c",
|
||||
"gsd-core/workflows/validate-phase.md": "2c6d7671fcaabcaa",
|
||||
"gsd-core/workflows/validate-phase.md": "ce9890f21c6e5607",
|
||||
"gsd-core/workflows/verify-phase.md": "22f18492581f1da5",
|
||||
"gsd-core/workflows/verify-work.md": "63b3f680d8f0a6f3",
|
||||
"hooks/gsd-check-update-worker.js": "cc1ef5f840f9dfc9",
|
||||
@@ -402,11 +402,13 @@
|
||||
"hooks/gsd-statusline.js": "3be32d2012c77fc1",
|
||||
"hooks/gsd-update-banner.js": "55143a25f978f301",
|
||||
"hooks/gsd-validate-commit.sh": "bf5dd61d33cb3a38",
|
||||
"hooks/gsd-windsurf-pre-command.js": "948be1c6d14c79cd",
|
||||
"hooks/gsd-windsurf-pre-write.js": "92d4dbfbc36ab0cf",
|
||||
"hooks/gsd-workflow-guard.js": "91ae24a15d2bca6f",
|
||||
"hooks/gsd-worktree-path-guard.js": "65b934c3a1709e89",
|
||||
"hooks/lib/git-cmd.js": "268ba15992ca0b23",
|
||||
"hooks/lib/gsd-graphify-rebuild.sh": "66af89601074d2a9",
|
||||
"hooks/managed-hooks-registry.cjs": "e61da0f7a3037c35",
|
||||
"hooks/managed-hooks-registry.cjs": "29a8d4fa81378d7d",
|
||||
"package.json": "dbf8353f77358bc1",
|
||||
"scripts/changeset/README.md": "86ff89331dfd94b2",
|
||||
"scripts/changeset/cli.cjs": "68f92a344b199271",
|
||||
@@ -447,7 +449,7 @@
|
||||
"skills/gsd-ns-manage/skills/settings/SKILL.md": "8751c29421208a00",
|
||||
"skills/gsd-ns-manage/skills/ship/SKILL.md": "7498424e965545e1",
|
||||
"skills/gsd-ns-manage/skills/stats/SKILL.md": "0bce39e8f3b64f5e",
|
||||
"skills/gsd-ns-manage/skills/surface/SKILL.md": "0644bd7dbeeb0a73",
|
||||
"skills/gsd-ns-manage/skills/surface/SKILL.md": "ca10834ce41967c9",
|
||||
"skills/gsd-ns-manage/skills/thread/SKILL.md": "fbe591e8162f8b1c",
|
||||
"skills/gsd-ns-manage/skills/undo/SKILL.md": "5ffc86a2c67aa9df",
|
||||
"skills/gsd-ns-manage/skills/update/SKILL.md": "c8165b8085ba106a",
|
||||
|
||||
@@ -94,7 +94,7 @@
|
||||
"commands/gsd-spec-phase.md": "383b4928c4df4bf1",
|
||||
"commands/gsd-spike.md": "98d789db7ca53873",
|
||||
"commands/gsd-stats.md": "a9c0e3f338bd61a5",
|
||||
"commands/gsd-surface.md": "998bf327c3f4a001",
|
||||
"commands/gsd-surface.md": "20bbba15eac52024",
|
||||
"commands/gsd-thread.md": "1b78c7b75b53ad4e",
|
||||
"commands/gsd-ui-phase.md": "67a759b3973f961f",
|
||||
"commands/gsd-ui-review.md": "9b4ecf2d40bdc476",
|
||||
@@ -112,7 +112,7 @@
|
||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "6bba2b9c9fa47cb8",
|
||||
"gsd-core/bin/shared/model-catalog.json": "d0b59409a46a456a",
|
||||
"gsd-core/bin/shared/model-catalog.json": "b55176ca044728d3",
|
||||
"gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a",
|
||||
"gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904",
|
||||
"gsd-core/contexts/dev.md": "dcb0de9dce33cf41",
|
||||
@@ -225,7 +225,7 @@
|
||||
"gsd-core/templates/SECURITY.md": "b628f7f1c6d2328f",
|
||||
"gsd-core/templates/UAT.md": "68d32d1fea14e184",
|
||||
"gsd-core/templates/UI-SPEC.md": "7dd5c7cdc7ece0ec",
|
||||
"gsd-core/templates/VALIDATION.md": "6144951011cdca57",
|
||||
"gsd-core/templates/VALIDATION.md": "e4d0f1c48727fce3",
|
||||
"gsd-core/templates/claude-md.md": "d8f0fe8dba3bb28a",
|
||||
"gsd-core/templates/codebase/architecture.md": "6be88214162fdd89",
|
||||
"gsd-core/templates/codebase/concerns.md": "efa26d1fb5132f25",
|
||||
@@ -273,7 +273,7 @@
|
||||
"gsd-core/workflows/ai-integration-phase.md": "3503f52a7356caf0",
|
||||
"gsd-core/workflows/analyze-dependencies.md": "77aff48f97fa6f1c",
|
||||
"gsd-core/workflows/audit-fix.md": "816c71b0ef2d8c1d",
|
||||
"gsd-core/workflows/audit-milestone.md": "a35795246b955bdb",
|
||||
"gsd-core/workflows/audit-milestone.md": "ae8605b4ecc37c0b",
|
||||
"gsd-core/workflows/audit-uat.md": "1c4a02a8c1ab930f",
|
||||
"gsd-core/workflows/autonomous.md": "6adf957e64518d15",
|
||||
"gsd-core/workflows/check-todos.md": "8f2c6b27f18cc5e2",
|
||||
@@ -312,7 +312,7 @@
|
||||
"gsd-core/workflows/execute-plan.md": "f17623fd47e795dd",
|
||||
"gsd-core/workflows/explore.md": "95e463d4bdd6dadd",
|
||||
"gsd-core/workflows/extract-learnings.md": "fd75072c339b58bd",
|
||||
"gsd-core/workflows/fast.md": "54fe93778b45a7eb",
|
||||
"gsd-core/workflows/fast.md": "0242082ca646819a",
|
||||
"gsd-core/workflows/forensics.md": "857d7b064f4cca21",
|
||||
"gsd-core/workflows/graduation.md": "ecf8da93e094fd2e",
|
||||
"gsd-core/workflows/health.md": "551e63aa6f3df711",
|
||||
@@ -355,14 +355,14 @@
|
||||
"gsd-core/workflows/remove-phase.md": "8effc8742d58a11a",
|
||||
"gsd-core/workflows/remove-workspace.md": "10882656198d9075",
|
||||
"gsd-core/workflows/resume-project.md": "af9761bcec0f6fe9",
|
||||
"gsd-core/workflows/review.md": "f9b63577ff23c2a9",
|
||||
"gsd-core/workflows/review.md": "eec3a15bebb7fcf0",
|
||||
"gsd-core/workflows/scan.md": "75c670d08cee8680",
|
||||
"gsd-core/workflows/secure-phase.md": "64ec4d06ca85720a",
|
||||
"gsd-core/workflows/session-report.md": "2e5b1205324ddefa",
|
||||
"gsd-core/workflows/settings-advanced.md": "a44896a18f1f2edc",
|
||||
"gsd-core/workflows/settings-advanced.md": "94d61da368e9f85b",
|
||||
"gsd-core/workflows/settings-integrations.md": "dfe3672c4fabf139",
|
||||
"gsd-core/workflows/settings.md": "acdd79110699a608",
|
||||
"gsd-core/workflows/ship.md": "44af1c72d86e153b",
|
||||
"gsd-core/workflows/ship.md": "2e50ecfb12d48282",
|
||||
"gsd-core/workflows/sketch-wrap-up.md": "d52a5462bafda830",
|
||||
"gsd-core/workflows/sketch.md": "dbe6acc4d976060c",
|
||||
"gsd-core/workflows/smart-entry.md": "1850447c045f36d8",
|
||||
@@ -378,7 +378,7 @@
|
||||
"gsd-core/workflows/ultraplan-phase.md": "b926ba7e4de0c76d",
|
||||
"gsd-core/workflows/undo.md": "d759702f84e308fa",
|
||||
"gsd-core/workflows/update.md": "2a59b4edf4a3c8c7",
|
||||
"gsd-core/workflows/validate-phase.md": "83eeefeeca31c2b5",
|
||||
"gsd-core/workflows/validate-phase.md": "3150904744b4a1bd",
|
||||
"gsd-core/workflows/verify-phase.md": "6ae6f159be75dcdd",
|
||||
"gsd-core/workflows/verify-work.md": "de14acdc8e925338",
|
||||
"hooks/gsd-check-update-worker.js": "a530efdb5fdc0da3",
|
||||
@@ -401,11 +401,13 @@
|
||||
"hooks/gsd-statusline.js": "7c315416ffc99a9a",
|
||||
"hooks/gsd-update-banner.js": "b457746cb76c1957",
|
||||
"hooks/gsd-validate-commit.sh": "bf5dd61d33cb3a38",
|
||||
"hooks/gsd-windsurf-pre-command.js": "948be1c6d14c79cd",
|
||||
"hooks/gsd-windsurf-pre-write.js": "92d4dbfbc36ab0cf",
|
||||
"hooks/gsd-workflow-guard.js": "59b46a74d19d58d3",
|
||||
"hooks/gsd-worktree-path-guard.js": "02be1bb504b22eb5",
|
||||
"hooks/lib/git-cmd.js": "268ba15992ca0b23",
|
||||
"hooks/lib/gsd-graphify-rebuild.sh": "66af89601074d2a9",
|
||||
"hooks/managed-hooks-registry.cjs": "f2e325aa9ba31647",
|
||||
"hooks/managed-hooks-registry.cjs": "1d955ec5d64e8a5f",
|
||||
"package.json": "dbf8353f77358bc1",
|
||||
"scripts/changeset/README.md": "86ff89331dfd94b2",
|
||||
"scripts/changeset/cli.cjs": "68f92a344b199271",
|
||||
|
||||
22
tests/fixtures/golden-install-parity/claude.json
vendored
22
tests/fixtures/golden-install-parity/claude.json
vendored
@@ -41,7 +41,7 @@
|
||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "6bba2b9c9fa47cb8",
|
||||
"gsd-core/bin/shared/model-catalog.json": "d0b59409a46a456a",
|
||||
"gsd-core/bin/shared/model-catalog.json": "b55176ca044728d3",
|
||||
"gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a",
|
||||
"gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904",
|
||||
"gsd-core/contexts/dev.md": "dcb0de9dce33cf41",
|
||||
@@ -154,7 +154,7 @@
|
||||
"gsd-core/templates/SECURITY.md": "b628f7f1c6d2328f",
|
||||
"gsd-core/templates/UAT.md": "68d32d1fea14e184",
|
||||
"gsd-core/templates/UI-SPEC.md": "7dd5c7cdc7ece0ec",
|
||||
"gsd-core/templates/VALIDATION.md": "6144951011cdca57",
|
||||
"gsd-core/templates/VALIDATION.md": "e4d0f1c48727fce3",
|
||||
"gsd-core/templates/claude-md.md": "d8f0fe8dba3bb28a",
|
||||
"gsd-core/templates/codebase/architecture.md": "6be88214162fdd89",
|
||||
"gsd-core/templates/codebase/concerns.md": "efa26d1fb5132f25",
|
||||
@@ -202,7 +202,7 @@
|
||||
"gsd-core/workflows/ai-integration-phase.md": "a898d99b8d844215",
|
||||
"gsd-core/workflows/analyze-dependencies.md": "77aff48f97fa6f1c",
|
||||
"gsd-core/workflows/audit-fix.md": "eedb2da4bffb7575",
|
||||
"gsd-core/workflows/audit-milestone.md": "23c1e35f449933b8",
|
||||
"gsd-core/workflows/audit-milestone.md": "9f8ec364e58d5710",
|
||||
"gsd-core/workflows/audit-uat.md": "e11db0d74c2a7405",
|
||||
"gsd-core/workflows/autonomous.md": "722397c04272dfaa",
|
||||
"gsd-core/workflows/check-todos.md": "6c2a43d1d3e86589",
|
||||
@@ -241,7 +241,7 @@
|
||||
"gsd-core/workflows/execute-plan.md": "cce1a33fe9a0a32d",
|
||||
"gsd-core/workflows/explore.md": "b9eea1bac358c9ce",
|
||||
"gsd-core/workflows/extract-learnings.md": "d8177b0c13b7e5ee",
|
||||
"gsd-core/workflows/fast.md": "54fe93778b45a7eb",
|
||||
"gsd-core/workflows/fast.md": "0242082ca646819a",
|
||||
"gsd-core/workflows/forensics.md": "c01da0178fb97b21",
|
||||
"gsd-core/workflows/graduation.md": "47f1594c88c08501",
|
||||
"gsd-core/workflows/health.md": "f934b1f1e9f3ae72",
|
||||
@@ -284,14 +284,14 @@
|
||||
"gsd-core/workflows/remove-phase.md": "ada8a0546c686483",
|
||||
"gsd-core/workflows/remove-workspace.md": "f3ab3a88a7e9e1ed",
|
||||
"gsd-core/workflows/resume-project.md": "7f8dc986f0f35d96",
|
||||
"gsd-core/workflows/review.md": "2dbfb1118613be25",
|
||||
"gsd-core/workflows/review.md": "b0baf1dafebe3821",
|
||||
"gsd-core/workflows/scan.md": "47371c2073d6c0be",
|
||||
"gsd-core/workflows/secure-phase.md": "59d3c50aba8c9a6c",
|
||||
"gsd-core/workflows/session-report.md": "2e5b1205324ddefa",
|
||||
"gsd-core/workflows/settings-advanced.md": "fa4a285b6bcbc384",
|
||||
"gsd-core/workflows/settings-advanced.md": "339def28c34b0797",
|
||||
"gsd-core/workflows/settings-integrations.md": "53649313d20694ae",
|
||||
"gsd-core/workflows/settings.md": "7e7458cdb2b68ec5",
|
||||
"gsd-core/workflows/ship.md": "12e8e58c077a891a",
|
||||
"gsd-core/workflows/ship.md": "63e65d06a6038e8e",
|
||||
"gsd-core/workflows/sketch-wrap-up.md": "121ed4b8127abf04",
|
||||
"gsd-core/workflows/sketch.md": "737c0492686fea2d",
|
||||
"gsd-core/workflows/smart-entry.md": "ad20cf74ae2e8291",
|
||||
@@ -307,7 +307,7 @@
|
||||
"gsd-core/workflows/ultraplan-phase.md": "328664400a001fd5",
|
||||
"gsd-core/workflows/undo.md": "791e0bf96d9a057f",
|
||||
"gsd-core/workflows/update.md": "f9e7d8a760d0d3c8",
|
||||
"gsd-core/workflows/validate-phase.md": "2ac231dc541441c2",
|
||||
"gsd-core/workflows/validate-phase.md": "7a3db6ab8ce9f380",
|
||||
"gsd-core/workflows/verify-phase.md": "e0957e153788a222",
|
||||
"gsd-core/workflows/verify-work.md": "c8ffee621e7319de",
|
||||
"hooks/gsd-check-update-worker.js": "a530efdb5fdc0da3",
|
||||
@@ -330,11 +330,13 @@
|
||||
"hooks/gsd-statusline.js": "7c315416ffc99a9a",
|
||||
"hooks/gsd-update-banner.js": "b457746cb76c1957",
|
||||
"hooks/gsd-validate-commit.sh": "bf5dd61d33cb3a38",
|
||||
"hooks/gsd-windsurf-pre-command.js": "948be1c6d14c79cd",
|
||||
"hooks/gsd-windsurf-pre-write.js": "92d4dbfbc36ab0cf",
|
||||
"hooks/gsd-workflow-guard.js": "59b46a74d19d58d3",
|
||||
"hooks/gsd-worktree-path-guard.js": "02be1bb504b22eb5",
|
||||
"hooks/lib/git-cmd.js": "268ba15992ca0b23",
|
||||
"hooks/lib/gsd-graphify-rebuild.sh": "66af89601074d2a9",
|
||||
"hooks/managed-hooks-registry.cjs": "f2e325aa9ba31647",
|
||||
"hooks/managed-hooks-registry.cjs": "1d955ec5d64e8a5f",
|
||||
"package.json": "dbf8353f77358bc1",
|
||||
"scripts/changeset/README.md": "86ff89331dfd94b2",
|
||||
"scripts/changeset/cli.cjs": "68f92a344b199271",
|
||||
@@ -409,7 +411,7 @@
|
||||
"skills/gsd-spec-phase/SKILL.md": "7e4dfa2070b7d9d1",
|
||||
"skills/gsd-spike/SKILL.md": "04d5f50d8d4a1c1a",
|
||||
"skills/gsd-stats/SKILL.md": "5403a46852241fa8",
|
||||
"skills/gsd-surface/SKILL.md": "970f30acc073a7b9",
|
||||
"skills/gsd-surface/SKILL.md": "4b9c977a74a975ed",
|
||||
"skills/gsd-thread/SKILL.md": "a76c70c368f82dee",
|
||||
"skills/gsd-ui-phase/SKILL.md": "7c9404102a9b9d74",
|
||||
"skills/gsd-ui-review/SKILL.md": "ef8f643abff1486b",
|
||||
|
||||
18
tests/fixtures/golden-install-parity/cline.json
vendored
18
tests/fixtures/golden-install-parity/cline.json
vendored
@@ -45,7 +45,7 @@
|
||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "6bba2b9c9fa47cb8",
|
||||
"gsd-core/bin/shared/model-catalog.json": "d0b59409a46a456a",
|
||||
"gsd-core/bin/shared/model-catalog.json": "b55176ca044728d3",
|
||||
"gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a",
|
||||
"gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904",
|
||||
"gsd-core/contexts/dev.md": "dcb0de9dce33cf41",
|
||||
@@ -158,7 +158,7 @@
|
||||
"gsd-core/templates/SECURITY.md": "b628f7f1c6d2328f",
|
||||
"gsd-core/templates/UAT.md": "9e296471b97ebcec",
|
||||
"gsd-core/templates/UI-SPEC.md": "7dd5c7cdc7ece0ec",
|
||||
"gsd-core/templates/VALIDATION.md": "f53e0ca061d3528e",
|
||||
"gsd-core/templates/VALIDATION.md": "6f0fe4214dff6dd1",
|
||||
"gsd-core/templates/claude-md.md": "c9fea2d8afa17d80",
|
||||
"gsd-core/templates/codebase/architecture.md": "6be88214162fdd89",
|
||||
"gsd-core/templates/codebase/concerns.md": "efa26d1fb5132f25",
|
||||
@@ -206,7 +206,7 @@
|
||||
"gsd-core/workflows/ai-integration-phase.md": "5159c6bdf102f74b",
|
||||
"gsd-core/workflows/analyze-dependencies.md": "52942af10f140717",
|
||||
"gsd-core/workflows/audit-fix.md": "9bb427cd3b4075d5",
|
||||
"gsd-core/workflows/audit-milestone.md": "2266710d0ae48932",
|
||||
"gsd-core/workflows/audit-milestone.md": "202b726748604c93",
|
||||
"gsd-core/workflows/audit-uat.md": "1fb7b2ab9d587c8f",
|
||||
"gsd-core/workflows/autonomous.md": "cc5217b1b2238a4c",
|
||||
"gsd-core/workflows/check-todos.md": "32fdf33f5dc8bdde",
|
||||
@@ -245,7 +245,7 @@
|
||||
"gsd-core/workflows/execute-plan.md": "0c5551f99ee0a017",
|
||||
"gsd-core/workflows/explore.md": "e83af8ceae314cf9",
|
||||
"gsd-core/workflows/extract-learnings.md": "6f39375b7dc775f9",
|
||||
"gsd-core/workflows/fast.md": "94136fb570d20a9d",
|
||||
"gsd-core/workflows/fast.md": "8878ec034b401f36",
|
||||
"gsd-core/workflows/forensics.md": "9fc65a8eed5d8bfc",
|
||||
"gsd-core/workflows/graduation.md": "16fedecda36769eb",
|
||||
"gsd-core/workflows/health.md": "788fad84fa42eb9f",
|
||||
@@ -288,14 +288,14 @@
|
||||
"gsd-core/workflows/remove-phase.md": "e336350f8113a328",
|
||||
"gsd-core/workflows/remove-workspace.md": "e685dfbd736dfd90",
|
||||
"gsd-core/workflows/resume-project.md": "e23981178fa37b3d",
|
||||
"gsd-core/workflows/review.md": "86d03cb58f48f45b",
|
||||
"gsd-core/workflows/review.md": "6c689f4ff8146d28",
|
||||
"gsd-core/workflows/scan.md": "dfd92717caea0ce7",
|
||||
"gsd-core/workflows/secure-phase.md": "cf78183f06a02582",
|
||||
"gsd-core/workflows/session-report.md": "2e5b1205324ddefa",
|
||||
"gsd-core/workflows/settings-advanced.md": "7147ba291d13e007",
|
||||
"gsd-core/workflows/settings-advanced.md": "69f3a19bf2c61160",
|
||||
"gsd-core/workflows/settings-integrations.md": "76eee76d6eb57657",
|
||||
"gsd-core/workflows/settings.md": "3701faed09d55247",
|
||||
"gsd-core/workflows/ship.md": "5f931a25ea9102a4",
|
||||
"gsd-core/workflows/ship.md": "baf53afdff3601d4",
|
||||
"gsd-core/workflows/sketch-wrap-up.md": "1f44789553180d84",
|
||||
"gsd-core/workflows/sketch.md": "2226779b6003a71c",
|
||||
"gsd-core/workflows/smart-entry.md": "9a64f43927641ca4",
|
||||
@@ -311,7 +311,7 @@
|
||||
"gsd-core/workflows/ultraplan-phase.md": "ceff456b1e9d94d8",
|
||||
"gsd-core/workflows/undo.md": "96d2775f008b3a85",
|
||||
"gsd-core/workflows/update.md": "165beec33490bd28",
|
||||
"gsd-core/workflows/validate-phase.md": "5b4ae14c87859bd2",
|
||||
"gsd-core/workflows/validate-phase.md": "706627c190ae11aa",
|
||||
"gsd-core/workflows/verify-phase.md": "a4f918c92c927268",
|
||||
"gsd-core/workflows/verify-work.md": "3fb282f2bce34a79",
|
||||
"scripts/changeset/README.md": "86ff89331dfd94b2",
|
||||
@@ -353,7 +353,7 @@
|
||||
"skills/gsd-ns-manage/skills/settings/SKILL.md": "67eff2d16e17403c",
|
||||
"skills/gsd-ns-manage/skills/ship/SKILL.md": "4ad9695934e069ee",
|
||||
"skills/gsd-ns-manage/skills/stats/SKILL.md": "25898070fb2a4b20",
|
||||
"skills/gsd-ns-manage/skills/surface/SKILL.md": "6d6ed15c90424f3f",
|
||||
"skills/gsd-ns-manage/skills/surface/SKILL.md": "a89b3bde1d2b620a",
|
||||
"skills/gsd-ns-manage/skills/thread/SKILL.md": "67b5a0451b58c50c",
|
||||
"skills/gsd-ns-manage/skills/undo/SKILL.md": "88407c6379617c7b",
|
||||
"skills/gsd-ns-manage/skills/update/SKILL.md": "2666ab7786b69017",
|
||||
|
||||
@@ -95,7 +95,7 @@
|
||||
"commands/gsd-spec-phase.md": "145f2ab750344022",
|
||||
"commands/gsd-spike.md": "54c094f40b601688",
|
||||
"commands/gsd-stats.md": "60f7077e7949d2ad",
|
||||
"commands/gsd-surface.md": "29688da5df785b58",
|
||||
"commands/gsd-surface.md": "84d89b840d00b7f0",
|
||||
"commands/gsd-thread.md": "07da4f657b3efcc1",
|
||||
"commands/gsd-ui-phase.md": "3b90f3d1c8fa531d",
|
||||
"commands/gsd-ui-review.md": "4e5fb1e77def9b64",
|
||||
@@ -113,7 +113,7 @@
|
||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "6bba2b9c9fa47cb8",
|
||||
"gsd-core/bin/shared/model-catalog.json": "d0b59409a46a456a",
|
||||
"gsd-core/bin/shared/model-catalog.json": "b55176ca044728d3",
|
||||
"gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a",
|
||||
"gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904",
|
||||
"gsd-core/contexts/dev.md": "dcb0de9dce33cf41",
|
||||
@@ -226,7 +226,7 @@
|
||||
"gsd-core/templates/SECURITY.md": "b628f7f1c6d2328f",
|
||||
"gsd-core/templates/UAT.md": "9e296471b97ebcec",
|
||||
"gsd-core/templates/UI-SPEC.md": "7dd5c7cdc7ece0ec",
|
||||
"gsd-core/templates/VALIDATION.md": "f53e0ca061d3528e",
|
||||
"gsd-core/templates/VALIDATION.md": "6f0fe4214dff6dd1",
|
||||
"gsd-core/templates/claude-md.md": "d1d333e4b963c0d2",
|
||||
"gsd-core/templates/codebase/architecture.md": "6be88214162fdd89",
|
||||
"gsd-core/templates/codebase/concerns.md": "efa26d1fb5132f25",
|
||||
@@ -274,7 +274,7 @@
|
||||
"gsd-core/workflows/ai-integration-phase.md": "40c217869a06981f",
|
||||
"gsd-core/workflows/analyze-dependencies.md": "52942af10f140717",
|
||||
"gsd-core/workflows/audit-fix.md": "eedb2da4bffb7575",
|
||||
"gsd-core/workflows/audit-milestone.md": "6866ce260980c21e",
|
||||
"gsd-core/workflows/audit-milestone.md": "3835762c3147a927",
|
||||
"gsd-core/workflows/audit-uat.md": "fcfbec501620b564",
|
||||
"gsd-core/workflows/autonomous.md": "7e5683728ef33707",
|
||||
"gsd-core/workflows/check-todos.md": "bdeaf43f9c61e0cc",
|
||||
@@ -313,7 +313,7 @@
|
||||
"gsd-core/workflows/execute-plan.md": "933d10547116794a",
|
||||
"gsd-core/workflows/explore.md": "6c04f2e658d93261",
|
||||
"gsd-core/workflows/extract-learnings.md": "b6f01ca3d8f58de4",
|
||||
"gsd-core/workflows/fast.md": "94136fb570d20a9d",
|
||||
"gsd-core/workflows/fast.md": "8878ec034b401f36",
|
||||
"gsd-core/workflows/forensics.md": "0d500a3f5ab26913",
|
||||
"gsd-core/workflows/graduation.md": "47f1594c88c08501",
|
||||
"gsd-core/workflows/health.md": "107e3c72e76d9535",
|
||||
@@ -356,14 +356,14 @@
|
||||
"gsd-core/workflows/remove-phase.md": "df9a45f0b1880999",
|
||||
"gsd-core/workflows/remove-workspace.md": "a7ca66db6b7c132c",
|
||||
"gsd-core/workflows/resume-project.md": "f28da1200e4545f4",
|
||||
"gsd-core/workflows/review.md": "93fe46bd0b5dd3d1",
|
||||
"gsd-core/workflows/review.md": "ad7c0f372ed986ae",
|
||||
"gsd-core/workflows/scan.md": "003883d71c37da7d",
|
||||
"gsd-core/workflows/secure-phase.md": "29fc6b62c5c5dc62",
|
||||
"gsd-core/workflows/session-report.md": "2e5b1205324ddefa",
|
||||
"gsd-core/workflows/settings-advanced.md": "b6fa466a953dd3a2",
|
||||
"gsd-core/workflows/settings-advanced.md": "414db4dbea97ba44",
|
||||
"gsd-core/workflows/settings-integrations.md": "70515c5838fb9826",
|
||||
"gsd-core/workflows/settings.md": "d96ddf01fb85e61e",
|
||||
"gsd-core/workflows/ship.md": "7b8fe9f89143e648",
|
||||
"gsd-core/workflows/ship.md": "b377dab2bc92a6e5",
|
||||
"gsd-core/workflows/sketch-wrap-up.md": "2aba89ecd8f41a0d",
|
||||
"gsd-core/workflows/sketch.md": "5eedd93f9a5b49d5",
|
||||
"gsd-core/workflows/smart-entry.md": "449238eb94abe187",
|
||||
@@ -379,7 +379,7 @@
|
||||
"gsd-core/workflows/ultraplan-phase.md": "0fb8291153e3937d",
|
||||
"gsd-core/workflows/undo.md": "96d2775f008b3a85",
|
||||
"gsd-core/workflows/update.md": "5ff1f77222977648",
|
||||
"gsd-core/workflows/validate-phase.md": "2c6d7671fcaabcaa",
|
||||
"gsd-core/workflows/validate-phase.md": "ce9890f21c6e5607",
|
||||
"gsd-core/workflows/verify-phase.md": "22f18492581f1da5",
|
||||
"gsd-core/workflows/verify-work.md": "63b3f680d8f0a6f3",
|
||||
"hooks/gsd-check-update-worker.js": "bdc9324a2f080ddd",
|
||||
@@ -402,11 +402,13 @@
|
||||
"hooks/gsd-statusline.js": "ef8dcb6d64fd4493",
|
||||
"hooks/gsd-update-banner.js": "55143a25f978f301",
|
||||
"hooks/gsd-validate-commit.sh": "bf5dd61d33cb3a38",
|
||||
"hooks/gsd-windsurf-pre-command.js": "948be1c6d14c79cd",
|
||||
"hooks/gsd-windsurf-pre-write.js": "92d4dbfbc36ab0cf",
|
||||
"hooks/gsd-workflow-guard.js": "91ae24a15d2bca6f",
|
||||
"hooks/gsd-worktree-path-guard.js": "548fc57131a04fa7",
|
||||
"hooks/lib/git-cmd.js": "268ba15992ca0b23",
|
||||
"hooks/lib/gsd-graphify-rebuild.sh": "66af89601074d2a9",
|
||||
"hooks/managed-hooks-registry.cjs": "0e7a61bde8688e11",
|
||||
"hooks/managed-hooks-registry.cjs": "9c0d837594c7b772",
|
||||
"package.json": "dbf8353f77358bc1",
|
||||
"scripts/changeset/README.md": "86ff89331dfd94b2",
|
||||
"scripts/changeset/cli.cjs": "68f92a344b199271",
|
||||
@@ -481,7 +483,7 @@
|
||||
"skills/gsd-spec-phase/SKILL.md": "96a095cd634129d8",
|
||||
"skills/gsd-spike/SKILL.md": "d4af42b801139876",
|
||||
"skills/gsd-stats/SKILL.md": "7d1fbadcae5f0b67",
|
||||
"skills/gsd-surface/SKILL.md": "5008f49701e7deea",
|
||||
"skills/gsd-surface/SKILL.md": "1b840c8ddd76b9c7",
|
||||
"skills/gsd-thread/SKILL.md": "18ddd2aa60997949",
|
||||
"skills/gsd-ui-phase/SKILL.md": "132ccf2ddc9e24e0",
|
||||
"skills/gsd-ui-review/SKILL.md": "7e8cf0bcee9859cd",
|
||||
|
||||
18
tests/fixtures/golden-install-parity/codex.json
vendored
18
tests/fixtures/golden-install-parity/codex.json
vendored
@@ -59,7 +59,7 @@
|
||||
".agents/skills/gsd-spec-phase/SKILL.md": "56a5cbd606db9cba",
|
||||
".agents/skills/gsd-spike/SKILL.md": "77209fef7a04c11a",
|
||||
".agents/skills/gsd-stats/SKILL.md": "566024444ef71f44",
|
||||
".agents/skills/gsd-surface/SKILL.md": "492cda98187a969b",
|
||||
".agents/skills/gsd-surface/SKILL.md": "e8b9d3c291a5c18c",
|
||||
".agents/skills/gsd-thread/SKILL.md": "85326c97c83a02d1",
|
||||
".agents/skills/gsd-ui-phase/SKILL.md": "a0c8fbcbe5e3c2b9",
|
||||
".agents/skills/gsd-ui-review/SKILL.md": "081a3292a2d357f5",
|
||||
@@ -148,7 +148,7 @@
|
||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "6bba2b9c9fa47cb8",
|
||||
"gsd-core/bin/shared/model-catalog.json": "d0b59409a46a456a",
|
||||
"gsd-core/bin/shared/model-catalog.json": "b55176ca044728d3",
|
||||
"gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a",
|
||||
"gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904",
|
||||
"gsd-core/contexts/dev.md": "dcb0de9dce33cf41",
|
||||
@@ -261,7 +261,7 @@
|
||||
"gsd-core/templates/SECURITY.md": "b628f7f1c6d2328f",
|
||||
"gsd-core/templates/UAT.md": "59a3d4f6c4afbfc9",
|
||||
"gsd-core/templates/UI-SPEC.md": "7dd5c7cdc7ece0ec",
|
||||
"gsd-core/templates/VALIDATION.md": "e3ca8ebb8d7e2cd0",
|
||||
"gsd-core/templates/VALIDATION.md": "dae6246879d09d13",
|
||||
"gsd-core/templates/claude-md.md": "dd1a9011684f9753",
|
||||
"gsd-core/templates/codebase/architecture.md": "282db635ba093b1a",
|
||||
"gsd-core/templates/codebase/concerns.md": "e66c584daa636fc5",
|
||||
@@ -309,7 +309,7 @@
|
||||
"gsd-core/workflows/ai-integration-phase.md": "1dfa15d8f28c022d",
|
||||
"gsd-core/workflows/analyze-dependencies.md": "f799abc00907377f",
|
||||
"gsd-core/workflows/audit-fix.md": "eedb2da4bffb7575",
|
||||
"gsd-core/workflows/audit-milestone.md": "46edd152d8b63bfc",
|
||||
"gsd-core/workflows/audit-milestone.md": "5f362cead97ceaca",
|
||||
"gsd-core/workflows/audit-uat.md": "2564078edb15b5e9",
|
||||
"gsd-core/workflows/autonomous.md": "92f08626d4aea668",
|
||||
"gsd-core/workflows/check-todos.md": "b2b103e8638e760a",
|
||||
@@ -348,7 +348,7 @@
|
||||
"gsd-core/workflows/execute-plan.md": "ac1f1d9ada00a91e",
|
||||
"gsd-core/workflows/explore.md": "2ef10d17c8864a04",
|
||||
"gsd-core/workflows/extract-learnings.md": "f716aa03fcb5f8da",
|
||||
"gsd-core/workflows/fast.md": "13252d545947354d",
|
||||
"gsd-core/workflows/fast.md": "bbf1f8e219ec9031",
|
||||
"gsd-core/workflows/forensics.md": "2e8a01b5b44e65f3",
|
||||
"gsd-core/workflows/graduation.md": "47f1594c88c08501",
|
||||
"gsd-core/workflows/health.md": "5b835fc606fd3e9b",
|
||||
@@ -391,14 +391,14 @@
|
||||
"gsd-core/workflows/remove-phase.md": "9ee0fddd11a0d9d4",
|
||||
"gsd-core/workflows/remove-workspace.md": "19d7465aaa50cb62",
|
||||
"gsd-core/workflows/resume-project.md": "9965f87eb278f7f8",
|
||||
"gsd-core/workflows/review.md": "63c61eddf20d77f1",
|
||||
"gsd-core/workflows/review.md": "5faef3f4feb45c99",
|
||||
"gsd-core/workflows/scan.md": "1a3caa5d724d39e9",
|
||||
"gsd-core/workflows/secure-phase.md": "db91810d16964b1e",
|
||||
"gsd-core/workflows/session-report.md": "dd8fa011c9394075",
|
||||
"gsd-core/workflows/settings-advanced.md": "38566aab0c529f2c",
|
||||
"gsd-core/workflows/settings-advanced.md": "2431433811616f76",
|
||||
"gsd-core/workflows/settings-integrations.md": "77730321d3d6d317",
|
||||
"gsd-core/workflows/settings.md": "054c8c31b3905ced",
|
||||
"gsd-core/workflows/ship.md": "d86233c9a365effd",
|
||||
"gsd-core/workflows/ship.md": "b5ac8caee76f3727",
|
||||
"gsd-core/workflows/sketch-wrap-up.md": "07724a390fbb43f6",
|
||||
"gsd-core/workflows/sketch.md": "576f300dfdde1b7d",
|
||||
"gsd-core/workflows/smart-entry.md": "6f686195ae531b03",
|
||||
@@ -414,7 +414,7 @@
|
||||
"gsd-core/workflows/ultraplan-phase.md": "0bafc2af27be4591",
|
||||
"gsd-core/workflows/undo.md": "5ff7d63b0a2f46d5",
|
||||
"gsd-core/workflows/update.md": "5c35c0ec0f462ea6",
|
||||
"gsd-core/workflows/validate-phase.md": "020201a41049679f",
|
||||
"gsd-core/workflows/validate-phase.md": "6d6d10fe53f6b1d0",
|
||||
"gsd-core/workflows/verify-phase.md": "2e12c3cb97a9122a",
|
||||
"gsd-core/workflows/verify-work.md": "5b348e73fc0d0829",
|
||||
"hooks/gsd-check-update.js": "ef48957eb6ac6a10",
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "6bba2b9c9fa47cb8",
|
||||
"gsd-core/bin/shared/model-catalog.json": "d0b59409a46a456a",
|
||||
"gsd-core/bin/shared/model-catalog.json": "b55176ca044728d3",
|
||||
"gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a",
|
||||
"gsd-core/bin/verify-reapply-patches.cjs": "10226e9512dd44bf",
|
||||
"gsd-core/contexts/dev.md": "dcb0de9dce33cf41",
|
||||
@@ -156,7 +156,7 @@
|
||||
"gsd-core/templates/SECURITY.md": "b628f7f1c6d2328f",
|
||||
"gsd-core/templates/UAT.md": "68d32d1fea14e184",
|
||||
"gsd-core/templates/UI-SPEC.md": "7dd5c7cdc7ece0ec",
|
||||
"gsd-core/templates/VALIDATION.md": "6144951011cdca57",
|
||||
"gsd-core/templates/VALIDATION.md": "e4d0f1c48727fce3",
|
||||
"gsd-core/templates/claude-md.md": "f436ae75a9c8518a",
|
||||
"gsd-core/templates/codebase/architecture.md": "282db635ba093b1a",
|
||||
"gsd-core/templates/codebase/concerns.md": "e66c584daa636fc5",
|
||||
@@ -204,7 +204,7 @@
|
||||
"gsd-core/workflows/ai-integration-phase.md": "ef0c2474cee76b00",
|
||||
"gsd-core/workflows/analyze-dependencies.md": "77aff48f97fa6f1c",
|
||||
"gsd-core/workflows/audit-fix.md": "9fdfe8a7fbcd1417",
|
||||
"gsd-core/workflows/audit-milestone.md": "19e03e6d34d96f38",
|
||||
"gsd-core/workflows/audit-milestone.md": "258e4dfd259ab13f",
|
||||
"gsd-core/workflows/audit-uat.md": "5ea0e30548ed0933",
|
||||
"gsd-core/workflows/autonomous.md": "dd972ddde9663295",
|
||||
"gsd-core/workflows/check-todos.md": "be9b50b5f28d7504",
|
||||
@@ -243,7 +243,7 @@
|
||||
"gsd-core/workflows/execute-plan.md": "c5e9dae726db15cc",
|
||||
"gsd-core/workflows/explore.md": "5fd91a8510e1114b",
|
||||
"gsd-core/workflows/extract-learnings.md": "f34d0b1927545b18",
|
||||
"gsd-core/workflows/fast.md": "54fe93778b45a7eb",
|
||||
"gsd-core/workflows/fast.md": "0242082ca646819a",
|
||||
"gsd-core/workflows/forensics.md": "459644dce26ee2ef",
|
||||
"gsd-core/workflows/graduation.md": "f013efc29096faf0",
|
||||
"gsd-core/workflows/health.md": "f343fd32a0e398e4",
|
||||
@@ -286,14 +286,14 @@
|
||||
"gsd-core/workflows/remove-phase.md": "e262654e319d1bc4",
|
||||
"gsd-core/workflows/remove-workspace.md": "ceddfeef5f2d6754",
|
||||
"gsd-core/workflows/resume-project.md": "40db7f350f5866d8",
|
||||
"gsd-core/workflows/review.md": "5f146ed397bcfe5a",
|
||||
"gsd-core/workflows/review.md": "4b649f31865a1785",
|
||||
"gsd-core/workflows/scan.md": "dcc2f76d0850e2fb",
|
||||
"gsd-core/workflows/secure-phase.md": "d87bd706f85bcad6",
|
||||
"gsd-core/workflows/session-report.md": "2e5b1205324ddefa",
|
||||
"gsd-core/workflows/settings-advanced.md": "8c88bbcb8fa0fa39",
|
||||
"gsd-core/workflows/settings-advanced.md": "230a658de9c017a6",
|
||||
"gsd-core/workflows/settings-integrations.md": "a1d146d6bfd14db5",
|
||||
"gsd-core/workflows/settings.md": "f611f14f2f447f1e",
|
||||
"gsd-core/workflows/ship.md": "b9dc0aaee0ff68e7",
|
||||
"gsd-core/workflows/ship.md": "dc4ac0166a559f98",
|
||||
"gsd-core/workflows/sketch-wrap-up.md": "f2590cb6ddbfad94",
|
||||
"gsd-core/workflows/sketch.md": "e2063966439af8c1",
|
||||
"gsd-core/workflows/smart-entry.md": "6c707b959a41900b",
|
||||
@@ -309,10 +309,10 @@
|
||||
"gsd-core/workflows/ultraplan-phase.md": "7217c34dc9eaf7bb",
|
||||
"gsd-core/workflows/undo.md": "ba1ef7aa80bef6bd",
|
||||
"gsd-core/workflows/update.md": "f444a7cfcd246cfb",
|
||||
"gsd-core/workflows/validate-phase.md": "2f705775a4b76d42",
|
||||
"gsd-core/workflows/validate-phase.md": "e7aa423ebdd6a230",
|
||||
"gsd-core/workflows/verify-phase.md": "5c72780e34214e27",
|
||||
"gsd-core/workflows/verify-work.md": "c966971a3cbd1d71",
|
||||
"hooks/gsd-session.json": "0a462834f2a28fee",
|
||||
"hooks/gsd-session.json": "3382597f61e3a559",
|
||||
"scripts/changeset/README.md": "86ff89331dfd94b2",
|
||||
"scripts/changeset/cli.cjs": "68f92a344b199271",
|
||||
"scripts/changeset/github-release-notes.cjs": "795677f0c009b132",
|
||||
@@ -386,7 +386,7 @@
|
||||
"skills/gsd-spec-phase/SKILL.md": "e402110c96d1f44f",
|
||||
"skills/gsd-spike/SKILL.md": "de84271bdba0ef67",
|
||||
"skills/gsd-stats/SKILL.md": "e0beaf89d27c8058",
|
||||
"skills/gsd-surface/SKILL.md": "2f2647c7bd664605",
|
||||
"skills/gsd-surface/SKILL.md": "c6ae441eb5e2e84a",
|
||||
"skills/gsd-thread/SKILL.md": "8db4d6ebcf0a8898",
|
||||
"skills/gsd-ui-phase/SKILL.md": "317081b3be7c536f",
|
||||
"skills/gsd-ui-review/SKILL.md": "dc07bbe9094b6dbe",
|
||||
|
||||
20
tests/fixtures/golden-install-parity/cursor.json
vendored
20
tests/fixtures/golden-install-parity/cursor.json
vendored
@@ -95,7 +95,7 @@
|
||||
"commands/gsd-spec-phase.md": "7c95987dcd7aa3fb",
|
||||
"commands/gsd-spike.md": "8102cc426fa1f1b3",
|
||||
"commands/gsd-stats.md": "eb39e4c4ec8eccea",
|
||||
"commands/gsd-surface.md": "1fb3de1651eb182a",
|
||||
"commands/gsd-surface.md": "96a66a3607b253b4",
|
||||
"commands/gsd-thread.md": "fc5975fdb73eb045",
|
||||
"commands/gsd-ui-phase.md": "b91b691e1eb007c2",
|
||||
"commands/gsd-ui-review.md": "2f9842e07e7df4df",
|
||||
@@ -113,7 +113,7 @@
|
||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "6bba2b9c9fa47cb8",
|
||||
"gsd-core/bin/shared/model-catalog.json": "d0b59409a46a456a",
|
||||
"gsd-core/bin/shared/model-catalog.json": "b55176ca044728d3",
|
||||
"gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a",
|
||||
"gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904",
|
||||
"gsd-core/contexts/dev.md": "dcb0de9dce33cf41",
|
||||
@@ -226,7 +226,7 @@
|
||||
"gsd-core/templates/SECURITY.md": "b628f7f1c6d2328f",
|
||||
"gsd-core/templates/UAT.md": "68d32d1fea14e184",
|
||||
"gsd-core/templates/UI-SPEC.md": "7dd5c7cdc7ece0ec",
|
||||
"gsd-core/templates/VALIDATION.md": "6144951011cdca57",
|
||||
"gsd-core/templates/VALIDATION.md": "e4d0f1c48727fce3",
|
||||
"gsd-core/templates/claude-md.md": "d6d7da8b7817a04c",
|
||||
"gsd-core/templates/codebase/architecture.md": "6be88214162fdd89",
|
||||
"gsd-core/templates/codebase/concerns.md": "efa26d1fb5132f25",
|
||||
@@ -274,7 +274,7 @@
|
||||
"gsd-core/workflows/ai-integration-phase.md": "b79f320d59a68773",
|
||||
"gsd-core/workflows/analyze-dependencies.md": "77aff48f97fa6f1c",
|
||||
"gsd-core/workflows/audit-fix.md": "eedb2da4bffb7575",
|
||||
"gsd-core/workflows/audit-milestone.md": "23c1e35f449933b8",
|
||||
"gsd-core/workflows/audit-milestone.md": "9f8ec364e58d5710",
|
||||
"gsd-core/workflows/audit-uat.md": "e11db0d74c2a7405",
|
||||
"gsd-core/workflows/autonomous.md": "4b20eda9a0b587ce",
|
||||
"gsd-core/workflows/check-todos.md": "1a67337d1630848f",
|
||||
@@ -313,7 +313,7 @@
|
||||
"gsd-core/workflows/execute-plan.md": "83dc1bf7f73735c0",
|
||||
"gsd-core/workflows/explore.md": "b9eea1bac358c9ce",
|
||||
"gsd-core/workflows/extract-learnings.md": "d8177b0c13b7e5ee",
|
||||
"gsd-core/workflows/fast.md": "0162075e44072447",
|
||||
"gsd-core/workflows/fast.md": "91f126c4ef24217e",
|
||||
"gsd-core/workflows/forensics.md": "a65f817d4a515291",
|
||||
"gsd-core/workflows/graduation.md": "53a4a6fa3b4e6613",
|
||||
"gsd-core/workflows/health.md": "e4d770484b5e7496",
|
||||
@@ -356,14 +356,14 @@
|
||||
"gsd-core/workflows/remove-phase.md": "ada8a0546c686483",
|
||||
"gsd-core/workflows/remove-workspace.md": "433affcd1a200826",
|
||||
"gsd-core/workflows/resume-project.md": "7f8dc986f0f35d96",
|
||||
"gsd-core/workflows/review.md": "ed6b9f54f74204ff",
|
||||
"gsd-core/workflows/review.md": "3ba8bb85c8ede5b8",
|
||||
"gsd-core/workflows/scan.md": "47371c2073d6c0be",
|
||||
"gsd-core/workflows/secure-phase.md": "c55975672c4e1895",
|
||||
"gsd-core/workflows/session-report.md": "2e5b1205324ddefa",
|
||||
"gsd-core/workflows/settings-advanced.md": "93a20fc634b05964",
|
||||
"gsd-core/workflows/settings-advanced.md": "bf8ea69c8f7ae019",
|
||||
"gsd-core/workflows/settings-integrations.md": "166ca51b1f33c2a3",
|
||||
"gsd-core/workflows/settings.md": "0623c673eaf04799",
|
||||
"gsd-core/workflows/ship.md": "38830806d244fe9c",
|
||||
"gsd-core/workflows/ship.md": "8a2504f02df7529d",
|
||||
"gsd-core/workflows/sketch-wrap-up.md": "5be73b7bdf96b539",
|
||||
"gsd-core/workflows/sketch.md": "373bc0d83368a411",
|
||||
"gsd-core/workflows/smart-entry.md": "c8fc316358cdcd7b",
|
||||
@@ -379,7 +379,7 @@
|
||||
"gsd-core/workflows/ultraplan-phase.md": "466e01d65c350ef6",
|
||||
"gsd-core/workflows/undo.md": "18dec684fb1076f9",
|
||||
"gsd-core/workflows/update.md": "23e294ba707c3580",
|
||||
"gsd-core/workflows/validate-phase.md": "2df0c6e298a5f249",
|
||||
"gsd-core/workflows/validate-phase.md": "d03a94b7b807be12",
|
||||
"gsd-core/workflows/verify-phase.md": "e0957e153788a222",
|
||||
"gsd-core/workflows/verify-work.md": "e7e7e900c4874490",
|
||||
"hooks/gsd-cursor-post-tool.js": "019d503aee8b4a3f",
|
||||
@@ -461,7 +461,7 @@
|
||||
"skills/gsd-spec-phase/SKILL.md": "5c48117fbb7aa595",
|
||||
"skills/gsd-spike/SKILL.md": "3850674027d81c2a",
|
||||
"skills/gsd-stats/SKILL.md": "ebbd0d39b5cee9a9",
|
||||
"skills/gsd-surface/SKILL.md": "805deb95c48af938",
|
||||
"skills/gsd-surface/SKILL.md": "9b869434321867c3",
|
||||
"skills/gsd-thread/SKILL.md": "621cde6272262eef",
|
||||
"skills/gsd-ui-phase/SKILL.md": "505ad61ef61c9e9c",
|
||||
"skills/gsd-ui-review/SKILL.md": "dc79fe0ad42f4948",
|
||||
|
||||
22
tests/fixtures/golden-install-parity/hermes.json
vendored
22
tests/fixtures/golden-install-parity/hermes.json
vendored
@@ -42,7 +42,7 @@
|
||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "6bba2b9c9fa47cb8",
|
||||
"gsd-core/bin/shared/model-catalog.json": "d0b59409a46a456a",
|
||||
"gsd-core/bin/shared/model-catalog.json": "b55176ca044728d3",
|
||||
"gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a",
|
||||
"gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904",
|
||||
"gsd-core/contexts/dev.md": "dcb0de9dce33cf41",
|
||||
@@ -155,7 +155,7 @@
|
||||
"gsd-core/templates/SECURITY.md": "b628f7f1c6d2328f",
|
||||
"gsd-core/templates/UAT.md": "68d32d1fea14e184",
|
||||
"gsd-core/templates/UI-SPEC.md": "7dd5c7cdc7ece0ec",
|
||||
"gsd-core/templates/VALIDATION.md": "6144951011cdca57",
|
||||
"gsd-core/templates/VALIDATION.md": "e4d0f1c48727fce3",
|
||||
"gsd-core/templates/claude-md.md": "7c778398f79e25a3",
|
||||
"gsd-core/templates/codebase/architecture.md": "6be88214162fdd89",
|
||||
"gsd-core/templates/codebase/concerns.md": "efa26d1fb5132f25",
|
||||
@@ -203,7 +203,7 @@
|
||||
"gsd-core/workflows/ai-integration-phase.md": "ddab2912d025db65",
|
||||
"gsd-core/workflows/analyze-dependencies.md": "77aff48f97fa6f1c",
|
||||
"gsd-core/workflows/audit-fix.md": "2eee4a0be82ef951",
|
||||
"gsd-core/workflows/audit-milestone.md": "806d346ee5bfe9f8",
|
||||
"gsd-core/workflows/audit-milestone.md": "690289689e9eda7a",
|
||||
"gsd-core/workflows/audit-uat.md": "86d9131fccabf2ad",
|
||||
"gsd-core/workflows/autonomous.md": "dd572ca89862e5ec",
|
||||
"gsd-core/workflows/check-todos.md": "ea9a303c48a5d752",
|
||||
@@ -242,7 +242,7 @@
|
||||
"gsd-core/workflows/execute-plan.md": "4dbe9b6f0c976245",
|
||||
"gsd-core/workflows/explore.md": "48770d68e8b9c132",
|
||||
"gsd-core/workflows/extract-learnings.md": "e9e167c718949c0b",
|
||||
"gsd-core/workflows/fast.md": "54fe93778b45a7eb",
|
||||
"gsd-core/workflows/fast.md": "0242082ca646819a",
|
||||
"gsd-core/workflows/forensics.md": "91961b811917c5c4",
|
||||
"gsd-core/workflows/graduation.md": "d1fd52bbe41dcf34",
|
||||
"gsd-core/workflows/health.md": "c622a5ad0d347d30",
|
||||
@@ -285,14 +285,14 @@
|
||||
"gsd-core/workflows/remove-phase.md": "fce799aae3ab2715",
|
||||
"gsd-core/workflows/remove-workspace.md": "8facde381657dd71",
|
||||
"gsd-core/workflows/resume-project.md": "a0443839f1f83c2d",
|
||||
"gsd-core/workflows/review.md": "c9de3987db14b94f",
|
||||
"gsd-core/workflows/review.md": "761dd1ae5be40613",
|
||||
"gsd-core/workflows/scan.md": "b28f65d88c522767",
|
||||
"gsd-core/workflows/secure-phase.md": "f2957d4b88fb3746",
|
||||
"gsd-core/workflows/session-report.md": "2e5b1205324ddefa",
|
||||
"gsd-core/workflows/settings-advanced.md": "7374ef968d5280f0",
|
||||
"gsd-core/workflows/settings-advanced.md": "49be159144d7f426",
|
||||
"gsd-core/workflows/settings-integrations.md": "1dce76db0aca08a5",
|
||||
"gsd-core/workflows/settings.md": "0845d073009a4619",
|
||||
"gsd-core/workflows/ship.md": "dbf8bf636cb196c0",
|
||||
"gsd-core/workflows/ship.md": "f2c98eac4edcd333",
|
||||
"gsd-core/workflows/sketch-wrap-up.md": "f1ece50ac65ea281",
|
||||
"gsd-core/workflows/sketch.md": "d5887983e62b574a",
|
||||
"gsd-core/workflows/smart-entry.md": "47f5c5e8608e5f7a",
|
||||
@@ -308,7 +308,7 @@
|
||||
"gsd-core/workflows/ultraplan-phase.md": "0d103bf2622436f7",
|
||||
"gsd-core/workflows/undo.md": "791e0bf96d9a057f",
|
||||
"gsd-core/workflows/update.md": "7499bb4cb2a3ce6f",
|
||||
"gsd-core/workflows/validate-phase.md": "75e8971d3981d06b",
|
||||
"gsd-core/workflows/validate-phase.md": "88edc724568337d3",
|
||||
"gsd-core/workflows/verify-phase.md": "5c8d1305b47fbef4",
|
||||
"gsd-core/workflows/verify-work.md": "7a9c9541d2d73fdc",
|
||||
"hooks/gsd-check-update-worker.js": "7989cc2bedd1138d",
|
||||
@@ -331,11 +331,13 @@
|
||||
"hooks/gsd-statusline.js": "861808560e60b233",
|
||||
"hooks/gsd-update-banner.js": "b457746cb76c1957",
|
||||
"hooks/gsd-validate-commit.sh": "bf5dd61d33cb3a38",
|
||||
"hooks/gsd-windsurf-pre-command.js": "948be1c6d14c79cd",
|
||||
"hooks/gsd-windsurf-pre-write.js": "92d4dbfbc36ab0cf",
|
||||
"hooks/gsd-workflow-guard.js": "59b46a74d19d58d3",
|
||||
"hooks/gsd-worktree-path-guard.js": "108ab88ccbafc5d8",
|
||||
"hooks/lib/git-cmd.js": "268ba15992ca0b23",
|
||||
"hooks/lib/gsd-graphify-rebuild.sh": "66af89601074d2a9",
|
||||
"hooks/managed-hooks-registry.cjs": "a494d1a70ed87690",
|
||||
"hooks/managed-hooks-registry.cjs": "bc58c3a7609d7eae",
|
||||
"package.json": "dbf8353f77358bc1",
|
||||
"scripts/changeset/README.md": "86ff89331dfd94b2",
|
||||
"scripts/changeset/cli.cjs": "68f92a344b199271",
|
||||
@@ -377,7 +379,7 @@
|
||||
"skills/gsd/gsd-ns-manage/skills/settings/SKILL.md": "db053a82acb85969",
|
||||
"skills/gsd/gsd-ns-manage/skills/ship/SKILL.md": "3f7e7023802a185d",
|
||||
"skills/gsd/gsd-ns-manage/skills/stats/SKILL.md": "847f1a79382e05a3",
|
||||
"skills/gsd/gsd-ns-manage/skills/surface/SKILL.md": "f63005360828e726",
|
||||
"skills/gsd/gsd-ns-manage/skills/surface/SKILL.md": "232dc849db8ec365",
|
||||
"skills/gsd/gsd-ns-manage/skills/thread/SKILL.md": "12e14ca63fe5a982",
|
||||
"skills/gsd/gsd-ns-manage/skills/undo/SKILL.md": "cb410bf1813a2d1e",
|
||||
"skills/gsd/gsd-ns-manage/skills/update/SKILL.md": "433a236998c305b0",
|
||||
|
||||
20
tests/fixtures/golden-install-parity/kilo.json
vendored
20
tests/fixtures/golden-install-parity/kilo.json
vendored
@@ -95,7 +95,7 @@
|
||||
"command/gsd-spec-phase.md": "8f8e3e109ebb7011",
|
||||
"command/gsd-spike.md": "fdcb1ae289790818",
|
||||
"command/gsd-stats.md": "b54c0d533cff9710",
|
||||
"command/gsd-surface.md": "3290127fe8241ced",
|
||||
"command/gsd-surface.md": "e2e7898a020a954b",
|
||||
"command/gsd-thread.md": "9c6a1900b3c57be6",
|
||||
"command/gsd-ui-phase.md": "efeba2d2381173e3",
|
||||
"command/gsd-ui-review.md": "35a12e93cddf266c",
|
||||
@@ -113,7 +113,7 @@
|
||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "6bba2b9c9fa47cb8",
|
||||
"gsd-core/bin/shared/model-catalog.json": "d0b59409a46a456a",
|
||||
"gsd-core/bin/shared/model-catalog.json": "b55176ca044728d3",
|
||||
"gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a",
|
||||
"gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904",
|
||||
"gsd-core/contexts/dev.md": "dcb0de9dce33cf41",
|
||||
@@ -226,7 +226,7 @@
|
||||
"gsd-core/templates/SECURITY.md": "b628f7f1c6d2328f",
|
||||
"gsd-core/templates/UAT.md": "68d32d1fea14e184",
|
||||
"gsd-core/templates/UI-SPEC.md": "7dd5c7cdc7ece0ec",
|
||||
"gsd-core/templates/VALIDATION.md": "6144951011cdca57",
|
||||
"gsd-core/templates/VALIDATION.md": "e4d0f1c48727fce3",
|
||||
"gsd-core/templates/claude-md.md": "f35856254768bf7d",
|
||||
"gsd-core/templates/codebase/architecture.md": "282db635ba093b1a",
|
||||
"gsd-core/templates/codebase/concerns.md": "e66c584daa636fc5",
|
||||
@@ -274,7 +274,7 @@
|
||||
"gsd-core/workflows/ai-integration-phase.md": "c85c6afdc64f0da6",
|
||||
"gsd-core/workflows/analyze-dependencies.md": "77aff48f97fa6f1c",
|
||||
"gsd-core/workflows/audit-fix.md": "eedb2da4bffb7575",
|
||||
"gsd-core/workflows/audit-milestone.md": "23c1e35f449933b8",
|
||||
"gsd-core/workflows/audit-milestone.md": "9f8ec364e58d5710",
|
||||
"gsd-core/workflows/audit-uat.md": "e11db0d74c2a7405",
|
||||
"gsd-core/workflows/autonomous.md": "73f429f7472c9949",
|
||||
"gsd-core/workflows/check-todos.md": "ed4b4eb12be222d7",
|
||||
@@ -313,7 +313,7 @@
|
||||
"gsd-core/workflows/execute-plan.md": "8dc89b35582407f7",
|
||||
"gsd-core/workflows/explore.md": "14242d36d4822df6",
|
||||
"gsd-core/workflows/extract-learnings.md": "d8177b0c13b7e5ee",
|
||||
"gsd-core/workflows/fast.md": "54fe93778b45a7eb",
|
||||
"gsd-core/workflows/fast.md": "0242082ca646819a",
|
||||
"gsd-core/workflows/forensics.md": "c01da0178fb97b21",
|
||||
"gsd-core/workflows/graduation.md": "5cc8638dabcbfd40",
|
||||
"gsd-core/workflows/health.md": "064b3668d5d9569a",
|
||||
@@ -356,14 +356,14 @@
|
||||
"gsd-core/workflows/remove-phase.md": "ada8a0546c686483",
|
||||
"gsd-core/workflows/remove-workspace.md": "fc83f362a2d0a1b7",
|
||||
"gsd-core/workflows/resume-project.md": "7f8dc986f0f35d96",
|
||||
"gsd-core/workflows/review.md": "dcea2ffd4f54c845",
|
||||
"gsd-core/workflows/review.md": "f8109b9ec1f56962",
|
||||
"gsd-core/workflows/scan.md": "47371c2073d6c0be",
|
||||
"gsd-core/workflows/secure-phase.md": "e8855104c1e0417c",
|
||||
"gsd-core/workflows/session-report.md": "2e5b1205324ddefa",
|
||||
"gsd-core/workflows/settings-advanced.md": "e00cd1a5d3fbba9c",
|
||||
"gsd-core/workflows/settings-advanced.md": "edd858cd6cfddaf1",
|
||||
"gsd-core/workflows/settings-integrations.md": "3ea8095d5fad891b",
|
||||
"gsd-core/workflows/settings.md": "1925ecc2225c2216",
|
||||
"gsd-core/workflows/ship.md": "afd77be2093535f8",
|
||||
"gsd-core/workflows/ship.md": "8c68f5a43d9fd578",
|
||||
"gsd-core/workflows/sketch-wrap-up.md": "888c0548e63197b3",
|
||||
"gsd-core/workflows/sketch.md": "34a0c10fa56af7ea",
|
||||
"gsd-core/workflows/smart-entry.md": "7ffe4fdb93935400",
|
||||
@@ -379,7 +379,7 @@
|
||||
"gsd-core/workflows/ultraplan-phase.md": "29245758b8497fa0",
|
||||
"gsd-core/workflows/undo.md": "7cd2153f8b15e30d",
|
||||
"gsd-core/workflows/update.md": "07dc2fba78ad1865",
|
||||
"gsd-core/workflows/validate-phase.md": "557e3251e3b9349a",
|
||||
"gsd-core/workflows/validate-phase.md": "52b9fa8a9533b444",
|
||||
"gsd-core/workflows/verify-phase.md": "e0957e153788a222",
|
||||
"gsd-core/workflows/verify-work.md": "9fc717845828c6e3",
|
||||
"kilo.json": "13151e97ff23c1aa",
|
||||
@@ -457,7 +457,7 @@
|
||||
"skills/gsd-spec-phase/SKILL.md": "749dee6f739b9b44",
|
||||
"skills/gsd-spike/SKILL.md": "0938ac1386ca4a58",
|
||||
"skills/gsd-stats/SKILL.md": "25898070fb2a4b20",
|
||||
"skills/gsd-surface/SKILL.md": "1bfb5b380f42a02b",
|
||||
"skills/gsd-surface/SKILL.md": "0672169f8292ed7d",
|
||||
"skills/gsd-thread/SKILL.md": "d5917840279459f1",
|
||||
"skills/gsd-ui-phase/SKILL.md": "c35f175ccc747d56",
|
||||
"skills/gsd-ui-review/SKILL.md": "48267dc071481a89",
|
||||
|
||||
22
tests/fixtures/golden-install-parity/kimi.json
vendored
22
tests/fixtures/golden-install-parity/kimi.json
vendored
@@ -21,11 +21,13 @@
|
||||
".kimi/hooks/gsd-statusline.js": "2736b0885aa97bbf",
|
||||
".kimi/hooks/gsd-update-banner.js": "55143a25f978f301",
|
||||
".kimi/hooks/gsd-validate-commit.sh": "bf5dd61d33cb3a38",
|
||||
".kimi/hooks/gsd-windsurf-pre-command.js": "948be1c6d14c79cd",
|
||||
".kimi/hooks/gsd-windsurf-pre-write.js": "92d4dbfbc36ab0cf",
|
||||
".kimi/hooks/gsd-workflow-guard.js": "91ae24a15d2bca6f",
|
||||
".kimi/hooks/gsd-worktree-path-guard.js": "cfde29a547677422",
|
||||
".kimi/hooks/lib/git-cmd.js": "268ba15992ca0b23",
|
||||
".kimi/hooks/lib/gsd-graphify-rebuild.sh": "66af89601074d2a9",
|
||||
".kimi/hooks/managed-hooks-registry.cjs": "07c6a5ecd724edb3",
|
||||
".kimi/hooks/managed-hooks-registry.cjs": "08ec2585a3f8f132",
|
||||
".kimi/package.json": "dbf8353f77358bc1",
|
||||
"agents/gsd.md": "60fee7782ae4f2c6",
|
||||
"agents/gsd.yaml": "253a23ddda06c6c2",
|
||||
@@ -104,7 +106,7 @@
|
||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "6bba2b9c9fa47cb8",
|
||||
"gsd-core/bin/shared/model-catalog.json": "d0b59409a46a456a",
|
||||
"gsd-core/bin/shared/model-catalog.json": "b55176ca044728d3",
|
||||
"gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a",
|
||||
"gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904",
|
||||
"gsd-core/contexts/dev.md": "dcb0de9dce33cf41",
|
||||
@@ -217,7 +219,7 @@
|
||||
"gsd-core/templates/SECURITY.md": "b628f7f1c6d2328f",
|
||||
"gsd-core/templates/UAT.md": "9e296471b97ebcec",
|
||||
"gsd-core/templates/UI-SPEC.md": "7dd5c7cdc7ece0ec",
|
||||
"gsd-core/templates/VALIDATION.md": "f53e0ca061d3528e",
|
||||
"gsd-core/templates/VALIDATION.md": "6f0fe4214dff6dd1",
|
||||
"gsd-core/templates/claude-md.md": "d1d333e4b963c0d2",
|
||||
"gsd-core/templates/codebase/architecture.md": "6be88214162fdd89",
|
||||
"gsd-core/templates/codebase/concerns.md": "efa26d1fb5132f25",
|
||||
@@ -265,7 +267,7 @@
|
||||
"gsd-core/workflows/ai-integration-phase.md": "40c217869a06981f",
|
||||
"gsd-core/workflows/analyze-dependencies.md": "52942af10f140717",
|
||||
"gsd-core/workflows/audit-fix.md": "eedb2da4bffb7575",
|
||||
"gsd-core/workflows/audit-milestone.md": "6866ce260980c21e",
|
||||
"gsd-core/workflows/audit-milestone.md": "3835762c3147a927",
|
||||
"gsd-core/workflows/audit-uat.md": "fcfbec501620b564",
|
||||
"gsd-core/workflows/autonomous.md": "7e5683728ef33707",
|
||||
"gsd-core/workflows/check-todos.md": "bdeaf43f9c61e0cc",
|
||||
@@ -304,7 +306,7 @@
|
||||
"gsd-core/workflows/execute-plan.md": "c8502b7475d797a7",
|
||||
"gsd-core/workflows/explore.md": "6c04f2e658d93261",
|
||||
"gsd-core/workflows/extract-learnings.md": "b6f01ca3d8f58de4",
|
||||
"gsd-core/workflows/fast.md": "94136fb570d20a9d",
|
||||
"gsd-core/workflows/fast.md": "8878ec034b401f36",
|
||||
"gsd-core/workflows/forensics.md": "0d500a3f5ab26913",
|
||||
"gsd-core/workflows/graduation.md": "47f1594c88c08501",
|
||||
"gsd-core/workflows/health.md": "107e3c72e76d9535",
|
||||
@@ -347,14 +349,14 @@
|
||||
"gsd-core/workflows/remove-phase.md": "df9a45f0b1880999",
|
||||
"gsd-core/workflows/remove-workspace.md": "a7ca66db6b7c132c",
|
||||
"gsd-core/workflows/resume-project.md": "f28da1200e4545f4",
|
||||
"gsd-core/workflows/review.md": "93fe46bd0b5dd3d1",
|
||||
"gsd-core/workflows/review.md": "ad7c0f372ed986ae",
|
||||
"gsd-core/workflows/scan.md": "003883d71c37da7d",
|
||||
"gsd-core/workflows/secure-phase.md": "29fc6b62c5c5dc62",
|
||||
"gsd-core/workflows/session-report.md": "2e5b1205324ddefa",
|
||||
"gsd-core/workflows/settings-advanced.md": "b6fa466a953dd3a2",
|
||||
"gsd-core/workflows/settings-advanced.md": "414db4dbea97ba44",
|
||||
"gsd-core/workflows/settings-integrations.md": "70515c5838fb9826",
|
||||
"gsd-core/workflows/settings.md": "d96ddf01fb85e61e",
|
||||
"gsd-core/workflows/ship.md": "7b8fe9f89143e648",
|
||||
"gsd-core/workflows/ship.md": "b377dab2bc92a6e5",
|
||||
"gsd-core/workflows/sketch-wrap-up.md": "b767a1d3db129a8a",
|
||||
"gsd-core/workflows/sketch.md": "88cfdf4edcf222ab",
|
||||
"gsd-core/workflows/smart-entry.md": "449238eb94abe187",
|
||||
@@ -370,7 +372,7 @@
|
||||
"gsd-core/workflows/ultraplan-phase.md": "0fb8291153e3937d",
|
||||
"gsd-core/workflows/undo.md": "96d2775f008b3a85",
|
||||
"gsd-core/workflows/update.md": "6718e0632bba26ca",
|
||||
"gsd-core/workflows/validate-phase.md": "2c6d7671fcaabcaa",
|
||||
"gsd-core/workflows/validate-phase.md": "ce9890f21c6e5607",
|
||||
"gsd-core/workflows/verify-phase.md": "22f18492581f1da5",
|
||||
"gsd-core/workflows/verify-work.md": "63b3f680d8f0a6f3",
|
||||
"scripts/changeset/README.md": "86ff89331dfd94b2",
|
||||
@@ -446,7 +448,7 @@
|
||||
"skills/gsd-spec-phase/SKILL.md": "23ff63ddf425c1c2",
|
||||
"skills/gsd-spike/SKILL.md": "aa740909d20a82f2",
|
||||
"skills/gsd-stats/SKILL.md": "a7e478f76e0b6db1",
|
||||
"skills/gsd-surface/SKILL.md": "2f72da87138503aa",
|
||||
"skills/gsd-surface/SKILL.md": "f0356c0fef0b1d4c",
|
||||
"skills/gsd-thread/SKILL.md": "fb52a782c6e83e03",
|
||||
"skills/gsd-ui-phase/SKILL.md": "1d7ed00d7970ed51",
|
||||
"skills/gsd-ui-review/SKILL.md": "39b36951bafe1495",
|
||||
|
||||
@@ -95,7 +95,7 @@
|
||||
"command/gsd-spec-phase.md": "b92bca076ca2c5c6",
|
||||
"command/gsd-spike.md": "1b2fa4b468e831dc",
|
||||
"command/gsd-stats.md": "51af934859f87623",
|
||||
"command/gsd-surface.md": "26a5f0aab82c1c4a",
|
||||
"command/gsd-surface.md": "e559ca6733ce288f",
|
||||
"command/gsd-thread.md": "ae3b000bee0ee1e0",
|
||||
"command/gsd-ui-phase.md": "21c9c043d813dc0a",
|
||||
"command/gsd-ui-review.md": "ef36603b519e8fe8",
|
||||
@@ -113,7 +113,7 @@
|
||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "6bba2b9c9fa47cb8",
|
||||
"gsd-core/bin/shared/model-catalog.json": "d0b59409a46a456a",
|
||||
"gsd-core/bin/shared/model-catalog.json": "b55176ca044728d3",
|
||||
"gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a",
|
||||
"gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904",
|
||||
"gsd-core/contexts/dev.md": "dcb0de9dce33cf41",
|
||||
@@ -226,7 +226,7 @@
|
||||
"gsd-core/templates/SECURITY.md": "b628f7f1c6d2328f",
|
||||
"gsd-core/templates/UAT.md": "68d32d1fea14e184",
|
||||
"gsd-core/templates/UI-SPEC.md": "7dd5c7cdc7ece0ec",
|
||||
"gsd-core/templates/VALIDATION.md": "6144951011cdca57",
|
||||
"gsd-core/templates/VALIDATION.md": "e4d0f1c48727fce3",
|
||||
"gsd-core/templates/claude-md.md": "a4f5e38984001194",
|
||||
"gsd-core/templates/codebase/architecture.md": "282db635ba093b1a",
|
||||
"gsd-core/templates/codebase/concerns.md": "e66c584daa636fc5",
|
||||
@@ -274,7 +274,7 @@
|
||||
"gsd-core/workflows/ai-integration-phase.md": "90e5f97018715b18",
|
||||
"gsd-core/workflows/analyze-dependencies.md": "77aff48f97fa6f1c",
|
||||
"gsd-core/workflows/audit-fix.md": "2871c5d0a4b671fa",
|
||||
"gsd-core/workflows/audit-milestone.md": "7aa0db364a4b67e2",
|
||||
"gsd-core/workflows/audit-milestone.md": "8bd0b730de08d1a8",
|
||||
"gsd-core/workflows/audit-uat.md": "c800099fab1e1c1b",
|
||||
"gsd-core/workflows/autonomous.md": "6cb0c014f5f56965",
|
||||
"gsd-core/workflows/check-todos.md": "6526b64ee88c7d2e",
|
||||
@@ -313,7 +313,7 @@
|
||||
"gsd-core/workflows/execute-plan.md": "e8de8ea661c1fe81",
|
||||
"gsd-core/workflows/explore.md": "7f5f9231cfd3089b",
|
||||
"gsd-core/workflows/extract-learnings.md": "92b3c0979604b7d0",
|
||||
"gsd-core/workflows/fast.md": "54fe93778b45a7eb",
|
||||
"gsd-core/workflows/fast.md": "0242082ca646819a",
|
||||
"gsd-core/workflows/forensics.md": "9354cb830152fd28",
|
||||
"gsd-core/workflows/graduation.md": "13080b5c24eec27d",
|
||||
"gsd-core/workflows/health.md": "a91e0a8e5d20b2e9",
|
||||
@@ -356,14 +356,14 @@
|
||||
"gsd-core/workflows/remove-phase.md": "dea4661e8f89596f",
|
||||
"gsd-core/workflows/remove-workspace.md": "446847e71aa52504",
|
||||
"gsd-core/workflows/resume-project.md": "ad9f06a10bab8cc0",
|
||||
"gsd-core/workflows/review.md": "a039f632e0b89003",
|
||||
"gsd-core/workflows/review.md": "2d28a6683ff587de",
|
||||
"gsd-core/workflows/scan.md": "ad8ebcad4626d4a8",
|
||||
"gsd-core/workflows/secure-phase.md": "e9a488cec3b4efdc",
|
||||
"gsd-core/workflows/session-report.md": "2e5b1205324ddefa",
|
||||
"gsd-core/workflows/settings-advanced.md": "92524bd2a53b43f5",
|
||||
"gsd-core/workflows/settings-advanced.md": "252b0d3edc315339",
|
||||
"gsd-core/workflows/settings-integrations.md": "d1711a95f44fdbde",
|
||||
"gsd-core/workflows/settings.md": "2e42ee34c791378a",
|
||||
"gsd-core/workflows/ship.md": "924c79e3cfd1e42b",
|
||||
"gsd-core/workflows/ship.md": "3a80ea841f0a81b9",
|
||||
"gsd-core/workflows/sketch-wrap-up.md": "681800323681c5c6",
|
||||
"gsd-core/workflows/sketch.md": "fcb7af914159ef7b",
|
||||
"gsd-core/workflows/smart-entry.md": "2a253fe437496eea",
|
||||
@@ -379,7 +379,7 @@
|
||||
"gsd-core/workflows/ultraplan-phase.md": "f4bad8de3fdb49fa",
|
||||
"gsd-core/workflows/undo.md": "0bba5e7f6196c894",
|
||||
"gsd-core/workflows/update.md": "71b6cd852f38b4bc",
|
||||
"gsd-core/workflows/validate-phase.md": "abcdbc1b56780565",
|
||||
"gsd-core/workflows/validate-phase.md": "434d9927c65f2bba",
|
||||
"gsd-core/workflows/verify-phase.md": "126be1d026900102",
|
||||
"gsd-core/workflows/verify-work.md": "ae07b3fa8b6f864e",
|
||||
"hooks/gsd-check-update-worker.js": "385fb7c67810baf6",
|
||||
@@ -402,11 +402,13 @@
|
||||
"hooks/gsd-statusline.js": "9c132b5985800462",
|
||||
"hooks/gsd-update-banner.js": "55143a25f978f301",
|
||||
"hooks/gsd-validate-commit.sh": "bf5dd61d33cb3a38",
|
||||
"hooks/gsd-windsurf-pre-command.js": "948be1c6d14c79cd",
|
||||
"hooks/gsd-windsurf-pre-write.js": "92d4dbfbc36ab0cf",
|
||||
"hooks/gsd-workflow-guard.js": "91ae24a15d2bca6f",
|
||||
"hooks/gsd-worktree-path-guard.js": "726fb9afefda5d42",
|
||||
"hooks/lib/git-cmd.js": "268ba15992ca0b23",
|
||||
"hooks/lib/gsd-graphify-rebuild.sh": "66af89601074d2a9",
|
||||
"hooks/managed-hooks-registry.cjs": "9163e096b74ec4b3",
|
||||
"hooks/managed-hooks-registry.cjs": "bd57cc72f482a14f",
|
||||
"opencode.json": "2c12c446a88f2f36",
|
||||
"package.json": "dbf8353f77358bc1",
|
||||
"plugins/gsd-core.js": "931ca839dc9eb7f1",
|
||||
@@ -483,7 +485,7 @@
|
||||
"skills/gsd-spec-phase/SKILL.md": "332028ed34b85b1f",
|
||||
"skills/gsd-spike/SKILL.md": "9303cd74a8bf0741",
|
||||
"skills/gsd-stats/SKILL.md": "2af976632e239069",
|
||||
"skills/gsd-surface/SKILL.md": "ea512089d0e1d057",
|
||||
"skills/gsd-surface/SKILL.md": "a0cec79a0bbf9c97",
|
||||
"skills/gsd-thread/SKILL.md": "9ff9979a8213dbf8",
|
||||
"skills/gsd-ui-phase/SKILL.md": "5eee1bdd13640252",
|
||||
"skills/gsd-ui-review/SKILL.md": "c99d6725326bfbc4",
|
||||
|
||||
322
tests/fixtures/golden-install-parity/pi.json
vendored
Normal file
322
tests/fixtures/golden-install-parity/pi.json
vendored
Normal file
@@ -0,0 +1,322 @@
|
||||
{
|
||||
".gsd-profile": "0e716a5fef4e6dc1",
|
||||
".gsd/defaults.json": "560664b045e645cb",
|
||||
"extensions/gsd.cjs": "619cec0af9cfdadf",
|
||||
"gsd-core/VERSION": "ef0deccd81a6723c",
|
||||
"gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74",
|
||||
"gsd-core/bin/ensure-runtime-build.cjs": "51bc64467ab30f62",
|
||||
"gsd-core/bin/gsd-tools.cjs": "6a7616125440c8b1",
|
||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "6bba2b9c9fa47cb8",
|
||||
"gsd-core/bin/shared/model-catalog.json": "b55176ca044728d3",
|
||||
"gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a",
|
||||
"gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904",
|
||||
"gsd-core/contexts/dev.md": "dcb0de9dce33cf41",
|
||||
"gsd-core/contexts/research.md": "b3285d8e7209cc3b",
|
||||
"gsd-core/contexts/review.md": "dc578fdd74bbea11",
|
||||
"gsd-core/references/agent-contracts.md": "ff65e633c656c0d2",
|
||||
"gsd-core/references/agent-skills-bootstrap.md": "5ab875054b1adda9",
|
||||
"gsd-core/references/ai-evals.md": "b5afa786b938671e",
|
||||
"gsd-core/references/ai-frameworks.md": "f827de93dde124eb",
|
||||
"gsd-core/references/api-coverage.md": "66264d41dfd9154a",
|
||||
"gsd-core/references/artifact-types.md": "a6d2e1f9453ffbf5",
|
||||
"gsd-core/references/autonomous-smart-discuss.md": "2fc710cde0ec7785",
|
||||
"gsd-core/references/checkpoints.md": "6aa620c6ca38bdf0",
|
||||
"gsd-core/references/common-bug-patterns.md": "780145be56352626",
|
||||
"gsd-core/references/context-budget.md": "f1ce57bf418824af",
|
||||
"gsd-core/references/continuation-format.md": "ce73e57d053ab8a7",
|
||||
"gsd-core/references/debugger-philosophy.md": "0466f95a3d6bfcd3",
|
||||
"gsd-core/references/decimal-phase-calculation.md": "46b5ba045852c474",
|
||||
"gsd-core/references/doc-conflict-engine.md": "67d019d23e17f934",
|
||||
"gsd-core/references/domain-probes.md": "62d23ed1992c48a9",
|
||||
"gsd-core/references/edge-probe-fixtures/01-round-half-even/expected-coverage.json": "72d1e29cedc854ec",
|
||||
"gsd-core/references/edge-probe-fixtures/01-round-half-even/requirements.json": "fbc1b355d8625eeb",
|
||||
"gsd-core/references/edge-probe-fixtures/02-merge-intervals/expected-coverage.json": "fad67dcc8294f6da",
|
||||
"gsd-core/references/edge-probe-fixtures/02-merge-intervals/requirements.json": "30a78ee9ce3473ea",
|
||||
"gsd-core/references/edge-probe-fixtures/03-truncate-graphemes/expected-coverage.json": "66dd60957fee45f0",
|
||||
"gsd-core/references/edge-probe-fixtures/03-truncate-graphemes/requirements.json": "47fca61f076835fa",
|
||||
"gsd-core/references/edge-probe-fixtures/04-money-rounding/expected-coverage.json": "72d1e29cedc854ec",
|
||||
"gsd-core/references/edge-probe-fixtures/04-money-rounding/requirements.json": "80f04f5c04fb24cf",
|
||||
"gsd-core/references/edge-probe-fixtures/05-list-dedupe/expected-coverage.json": "fad67dcc8294f6da",
|
||||
"gsd-core/references/edge-probe-fixtures/05-list-dedupe/requirements.json": "d38147adb0e5b342",
|
||||
"gsd-core/references/edge-probe-fixtures/06-resolved-mixed/expected-coverage.json": "bc552c01939bf4f8",
|
||||
"gsd-core/references/edge-probe-fixtures/06-resolved-mixed/requirements.json": "30a78ee9ce3473ea",
|
||||
"gsd-core/references/edge-probe-fixtures/06-resolved-mixed/resolutions.json": "688ec62c13e08afe",
|
||||
"gsd-core/references/edge-probe.md": "5687eba25a078561",
|
||||
"gsd-core/references/execute-mvp-tdd.md": "a98a270a7ab126bc",
|
||||
"gsd-core/references/execute-phase-between-wave-reset.md": "3ad96ca0f7fee37e",
|
||||
"gsd-core/references/execute-phase-context-guard.md": "982006c1f3364242",
|
||||
"gsd-core/references/execute-phase-wave-guard.md": "de9ac22cead4cfd8",
|
||||
"gsd-core/references/executor-examples.md": "ba59243ed45c8ab1",
|
||||
"gsd-core/references/few-shot-examples/plan-checker.md": "2574808188ac9de4",
|
||||
"gsd-core/references/few-shot-examples/verifier.md": "5badee4560b14ae8",
|
||||
"gsd-core/references/gate-prompts.md": "e69f5993ab944d80",
|
||||
"gsd-core/references/gates.md": "bd79c7f90c8cb8d7",
|
||||
"gsd-core/references/git-integration.md": "77bf9dff38b2c9d4",
|
||||
"gsd-core/references/git-planning-commit.md": "f897a15ebfc3f5a7",
|
||||
"gsd-core/references/gsd-run-resolver.md": "e71eb728b84641b1",
|
||||
"gsd-core/references/honest-verifier.md": "8815c9fc18c35719",
|
||||
"gsd-core/references/ios-scaffold.md": "5ef0cb7e0fac891f",
|
||||
"gsd-core/references/loop-hook-dispatch.md": "32e5dfb4dba76987",
|
||||
"gsd-core/references/mandatory-initial-read.md": "fe59abce693717cf",
|
||||
"gsd-core/references/model-profile-resolution.md": "f32bb05102839767",
|
||||
"gsd-core/references/model-profiles.md": "e067ad3df6770db1",
|
||||
"gsd-core/references/mvp-concepts.md": "72f7e8b1f8ae7118",
|
||||
"gsd-core/references/phase-argument-parsing.md": "e5bbb985f3bc3e34",
|
||||
"gsd-core/references/planner-antipatterns.md": "7ed54ec1e2cc54ac",
|
||||
"gsd-core/references/planner-chunked.md": "79fe674221e738e6",
|
||||
"gsd-core/references/planner-gap-closure.md": "76bee257911413e7",
|
||||
"gsd-core/references/planner-graphify-auto-update.md": "6aeffd9097e25e22",
|
||||
"gsd-core/references/planner-guidance.md": "96486cac2f7885e6",
|
||||
"gsd-core/references/planner-human-verify-mode.md": "56d05e841630b3f4",
|
||||
"gsd-core/references/planner-interface-context.md": "b28fa3da6ae739a8",
|
||||
"gsd-core/references/planner-load-graph-context.md": "ca7a7af3f35ae61b",
|
||||
"gsd-core/references/planner-mvp-mode.md": "ec33050db81101a8",
|
||||
"gsd-core/references/planner-reviews.md": "dda0193a0fbd4947",
|
||||
"gsd-core/references/planner-revision.md": "86ba8a511f081f05",
|
||||
"gsd-core/references/planner-source-audit.md": "7de5bdb07232ce0b",
|
||||
"gsd-core/references/planning-config.md": "ac409835e8260a3e",
|
||||
"gsd-core/references/prohibition-probe-fixtures/01-streak-reminder/expected.json": "f10df472f2846cc6",
|
||||
"gsd-core/references/prohibition-probe-fixtures/02-clean-utility/expected.json": "31e8a781eeffe020",
|
||||
"gsd-core/references/prohibition-probe-fixtures/03-multi-prohibition/expected.json": "70a532a7cc1b6ae8",
|
||||
"gsd-core/references/prohibition-probe.md": "605dc3f5a118ff3b",
|
||||
"gsd-core/references/project-skills-discovery.md": "c155e03dce8dc3c2",
|
||||
"gsd-core/references/questioning.md": "a8c988cab05f4651",
|
||||
"gsd-core/references/research-documentation-lookup.md": "c070007d1d72ab71",
|
||||
"gsd-core/references/research-philosophy.md": "62930e66cc979c1a",
|
||||
"gsd-core/references/research-verification-protocol.md": "9c38c9d9a687e679",
|
||||
"gsd-core/references/reviewer-instances.md": "385501e4f9bbb31d",
|
||||
"gsd-core/references/revision-loop.md": "e55ff32dd98c63df",
|
||||
"gsd-core/references/scout-codebase.md": "ba266ecc18fbf172",
|
||||
"gsd-core/references/security-asvs-levels.md": "4774fac3b94b6ca8",
|
||||
"gsd-core/references/skeleton-template.md": "528691d1f0efa878",
|
||||
"gsd-core/references/sketch-interactivity.md": "7d982fe877e1e1cc",
|
||||
"gsd-core/references/sketch-theme-system.md": "33e2e96e450456f8",
|
||||
"gsd-core/references/sketch-tooling.md": "df6c4f24c1c27611",
|
||||
"gsd-core/references/sketch-variant-patterns.md": "66c197aa4fb52810",
|
||||
"gsd-core/references/specless-probe-fallback.md": "5e400dc05a15e972",
|
||||
"gsd-core/references/spidr-splitting.md": "074ac154c0e4f906",
|
||||
"gsd-core/references/tdd.md": "e4708ede157478b6",
|
||||
"gsd-core/references/thinking-models-debug.md": "2da61022b16c4e7c",
|
||||
"gsd-core/references/thinking-models-execution.md": "dcc650a8b5f3e049",
|
||||
"gsd-core/references/thinking-models-planning.md": "7e19462313fa028f",
|
||||
"gsd-core/references/thinking-models-research.md": "5f6bf3f3b889c6e4",
|
||||
"gsd-core/references/thinking-models-verification.md": "a71a933d51ca3d8d",
|
||||
"gsd-core/references/thinking-partner.md": "41069529ef776e39",
|
||||
"gsd-core/references/ui-brand.md": "48717bcfcd63bd27",
|
||||
"gsd-core/references/ui-consideration-probe.md": "7e019dfaae47f4c4",
|
||||
"gsd-core/references/universal-anti-patterns.md": "6a1245050b21df01",
|
||||
"gsd-core/references/untrusted-input-boundary.md": "d33b80d4d348599a",
|
||||
"gsd-core/references/user-profiling.md": "b50416fe57c1b321",
|
||||
"gsd-core/references/user-story-template.md": "0cc50e06a144ff8a",
|
||||
"gsd-core/references/verification-overrides.md": "8213de9bd62283b6",
|
||||
"gsd-core/references/verification-patterns.md": "72f4d4814fdae9a8",
|
||||
"gsd-core/references/verify-mvp-mode.md": "534bdc7f2432903a",
|
||||
"gsd-core/references/workstream-flag.md": "ca99ca79e716f0f5",
|
||||
"gsd-core/references/worktree-branch-check.md": "21d9c31bf6542b93",
|
||||
"gsd-core/references/worktree-path-safety.md": "3c8d74756f9b16a8",
|
||||
"gsd-core/templates/AI-SPEC.md": "efa1f8354bd3a24b",
|
||||
"gsd-core/templates/DEBUG.md": "a13470b82b1935e7",
|
||||
"gsd-core/templates/README.md": "93d3426fc64e2c12",
|
||||
"gsd-core/templates/SECURITY.md": "b628f7f1c6d2328f",
|
||||
"gsd-core/templates/UAT.md": "9e296471b97ebcec",
|
||||
"gsd-core/templates/UI-SPEC.md": "7dd5c7cdc7ece0ec",
|
||||
"gsd-core/templates/VALIDATION.md": "6f0fe4214dff6dd1",
|
||||
"gsd-core/templates/claude-md.md": "d1d333e4b963c0d2",
|
||||
"gsd-core/templates/codebase/architecture.md": "6be88214162fdd89",
|
||||
"gsd-core/templates/codebase/concerns.md": "efa26d1fb5132f25",
|
||||
"gsd-core/templates/codebase/conventions.md": "c2e07698dad6b364",
|
||||
"gsd-core/templates/codebase/integrations.md": "39bd23c71eedd564",
|
||||
"gsd-core/templates/codebase/stack.md": "116e7e67dd87ddec",
|
||||
"gsd-core/templates/codebase/structure.md": "222997133232a6f5",
|
||||
"gsd-core/templates/codebase/testing.md": "76abff7f2050c9ea",
|
||||
"gsd-core/templates/config.json": "a4b783ef759a0f37",
|
||||
"gsd-core/templates/context.md": "69b01e7909ea3f66",
|
||||
"gsd-core/templates/continue-here.md": "f522a51b6895fba8",
|
||||
"gsd-core/templates/copilot-instructions.md": "aea34bc52ff548ea",
|
||||
"gsd-core/templates/debug-subagent-prompt.md": "920656683dedb869",
|
||||
"gsd-core/templates/dev-preferences.md": "88d0a65ec0993a3a",
|
||||
"gsd-core/templates/discovery.md": "9a0e0935cc825dbc",
|
||||
"gsd-core/templates/discussion-log.md": "cac1b48ec0f4dcb8",
|
||||
"gsd-core/templates/milestone-archive.md": "591b6decdc0c0e51",
|
||||
"gsd-core/templates/milestone.md": "74d2f750ae9f4a9c",
|
||||
"gsd-core/templates/phase-prompt.md": "b811bf951092df2f",
|
||||
"gsd-core/templates/planner-subagent-prompt.md": "ebf29dbb27042370",
|
||||
"gsd-core/templates/project.md": "4f311fb1b05b823b",
|
||||
"gsd-core/templates/requirements.md": "a44de4c2f146e473",
|
||||
"gsd-core/templates/research-project/ARCHITECTURE.md": "746b9ef791d758b0",
|
||||
"gsd-core/templates/research-project/FEATURES.md": "f2b800de5df91b0f",
|
||||
"gsd-core/templates/research-project/PITFALLS.md": "3ef75fa768422eec",
|
||||
"gsd-core/templates/research-project/STACK.md": "82c85799ac4dd344",
|
||||
"gsd-core/templates/research-project/SUMMARY.md": "dceb2f346388839d",
|
||||
"gsd-core/templates/research.md": "88ce0920417091d0",
|
||||
"gsd-core/templates/retrospective.md": "03981e30dd760103",
|
||||
"gsd-core/templates/roadmap.md": "e4e35a9eb5dd4d4f",
|
||||
"gsd-core/templates/spec.md": "26d55bce940f0288",
|
||||
"gsd-core/templates/state.md": "4d123aa6cea167fe",
|
||||
"gsd-core/templates/summary-complex.md": "a5e40574fd8894dc",
|
||||
"gsd-core/templates/summary-minimal.md": "7d09b5e709e2e67c",
|
||||
"gsd-core/templates/summary-standard.md": "e8d9cf4a8377cdff",
|
||||
"gsd-core/templates/summary.md": "23c40f6503b3ea98",
|
||||
"gsd-core/templates/user-profile.md": "20749f23e4c413fc",
|
||||
"gsd-core/templates/user-setup.md": "78b7d718b6e8d67c",
|
||||
"gsd-core/templates/verification-report.md": "dd5faa6254183731",
|
||||
"gsd-core/workflows/_runtime-launcher.snippet.sh": "bf2dd5d1debd5335",
|
||||
"gsd-core/workflows/add-backlog.md": "1bc7377b105194fc",
|
||||
"gsd-core/workflows/add-phase.md": "46e0551ffdd8ce1a",
|
||||
"gsd-core/workflows/add-tests.md": "2c1da65d41dc12d2",
|
||||
"gsd-core/workflows/add-todo.md": "cc0efe270004c8fb",
|
||||
"gsd-core/workflows/ai-integration-phase.md": "40c217869a06981f",
|
||||
"gsd-core/workflows/analyze-dependencies.md": "52942af10f140717",
|
||||
"gsd-core/workflows/audit-fix.md": "eedb2da4bffb7575",
|
||||
"gsd-core/workflows/audit-milestone.md": "3835762c3147a927",
|
||||
"gsd-core/workflows/audit-uat.md": "fcfbec501620b564",
|
||||
"gsd-core/workflows/autonomous.md": "7e5683728ef33707",
|
||||
"gsd-core/workflows/check-todos.md": "bdeaf43f9c61e0cc",
|
||||
"gsd-core/workflows/cleanup.md": "0daa2f2720c111f1",
|
||||
"gsd-core/workflows/code-review-fix.md": "2e113d1f4350a075",
|
||||
"gsd-core/workflows/code-review.md": "334c90c401f291f8",
|
||||
"gsd-core/workflows/complete-milestone.md": "c1f91b77f4ace7f2",
|
||||
"gsd-core/workflows/debug.md": "849d7e5b9c12dcae",
|
||||
"gsd-core/workflows/diagnose-issues.md": "d6d978fddfd5da8d",
|
||||
"gsd-core/workflows/discovery-phase.md": "3ba7cfb89fb1e761",
|
||||
"gsd-core/workflows/discuss-phase-assumptions.md": "f5b765d33eba4f88",
|
||||
"gsd-core/workflows/discuss-phase-power.md": "0841f7dc6e9a054a",
|
||||
"gsd-core/workflows/discuss-phase.md": "acfe2baa4c8bebbe",
|
||||
"gsd-core/workflows/discuss-phase/modes/advisor.md": "db488d74d080c653",
|
||||
"gsd-core/workflows/discuss-phase/modes/all.md": "fa70d79066562e54",
|
||||
"gsd-core/workflows/discuss-phase/modes/analyze.md": "da0788f3be7f8105",
|
||||
"gsd-core/workflows/discuss-phase/modes/auto.md": "d06f0bea2ba0b240",
|
||||
"gsd-core/workflows/discuss-phase/modes/batch.md": "6946597770e2d448",
|
||||
"gsd-core/workflows/discuss-phase/modes/chain.md": "5502a67de7776853",
|
||||
"gsd-core/workflows/discuss-phase/modes/default.md": "67d1b67f61f03966",
|
||||
"gsd-core/workflows/discuss-phase/modes/power.md": "fdc7a728eaaa2261",
|
||||
"gsd-core/workflows/discuss-phase/modes/text.md": "da6d45207da4a988",
|
||||
"gsd-core/workflows/discuss-phase/templates/checkpoint.json": "e3bc3dca49db59eb",
|
||||
"gsd-core/workflows/discuss-phase/templates/context.md": "6cd929e989fe2b0f",
|
||||
"gsd-core/workflows/discuss-phase/templates/discussion-log.md": "1bbd7703f11128e1",
|
||||
"gsd-core/workflows/do.md": "7512dd1892f118a1",
|
||||
"gsd-core/workflows/docs-update.md": "f35922d15b7061c9",
|
||||
"gsd-core/workflows/edit-phase.md": "966a3eadd1bebc04",
|
||||
"gsd-core/workflows/eval-review.md": "f898936e2cfe4130",
|
||||
"gsd-core/workflows/execute-phase.md": "8f6c2443655aceb1",
|
||||
"gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "4e265392b3f2ba0e",
|
||||
"gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "7ebb7d1af6082028",
|
||||
"gsd-core/workflows/execute-phase/steps/post-merge-gate.md": "811b6d8489571581",
|
||||
"gsd-core/workflows/execute-phase/steps/regression-gate.md": "8ccc16a6c7cf6000",
|
||||
"gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md": "be84efbd71e1513e",
|
||||
"gsd-core/workflows/execute-plan.md": "ff172c3540b52e9d",
|
||||
"gsd-core/workflows/explore.md": "6c04f2e658d93261",
|
||||
"gsd-core/workflows/extract-learnings.md": "b6f01ca3d8f58de4",
|
||||
"gsd-core/workflows/fast.md": "8878ec034b401f36",
|
||||
"gsd-core/workflows/forensics.md": "0d500a3f5ab26913",
|
||||
"gsd-core/workflows/graduation.md": "47f1594c88c08501",
|
||||
"gsd-core/workflows/health.md": "107e3c72e76d9535",
|
||||
"gsd-core/workflows/help.md": "5d040504b9ab35e3",
|
||||
"gsd-core/workflows/help/modes/brief.md": "924860e1f07defb0",
|
||||
"gsd-core/workflows/help/modes/default.md": "08a02976c0c5cc50",
|
||||
"gsd-core/workflows/help/modes/full.md": "87951776f730390d",
|
||||
"gsd-core/workflows/help/modes/topic.md": "5c160093f3cbf35d",
|
||||
"gsd-core/workflows/import.md": "3d3fa603ceb8bc9f",
|
||||
"gsd-core/workflows/inbox.md": "437f981ef9ae7b26",
|
||||
"gsd-core/workflows/ingest-docs.md": "c859921c811ac11b",
|
||||
"gsd-core/workflows/insert-phase.md": "08dca838d831bb5b",
|
||||
"gsd-core/workflows/list-phase-assumptions.md": "53dd4b69536c1bc3",
|
||||
"gsd-core/workflows/list-seeds.md": "b687f2a843032d65",
|
||||
"gsd-core/workflows/list-workspaces.md": "7f94e18b5c549453",
|
||||
"gsd-core/workflows/manager.md": "3ba7a3e8213c4b23",
|
||||
"gsd-core/workflows/map-codebase.md": "83d750aade709983",
|
||||
"gsd-core/workflows/milestone-summary.md": "e0cdfddbd39a9043",
|
||||
"gsd-core/workflows/mvp-phase.md": "344dd300e0cc1e74",
|
||||
"gsd-core/workflows/new-milestone.md": "a616efb0f82d1a11",
|
||||
"gsd-core/workflows/new-project.md": "4c32c69910d55ec0",
|
||||
"gsd-core/workflows/new-workspace.md": "017688423110ed66",
|
||||
"gsd-core/workflows/next.md": "13fb800f2472d970",
|
||||
"gsd-core/workflows/node-repair.md": "07a1628e5a1ff96b",
|
||||
"gsd-core/workflows/note.md": "5a99eb396c744619",
|
||||
"gsd-core/workflows/onboard.md": "6f9e6c0b484271a9",
|
||||
"gsd-core/workflows/pause-work.md": "f2b33bba5593d422",
|
||||
"gsd-core/workflows/plan-milestone-gaps.md": "852f6d7c0c4299dc",
|
||||
"gsd-core/workflows/plan-phase.md": "5a841e26f1d6f9ae",
|
||||
"gsd-core/workflows/plan-phase/steps/closed-phase-gate.md": "b36f77ac7344a072",
|
||||
"gsd-core/workflows/plan-phase/steps/prd-express-path.md": "197c0590326371b2",
|
||||
"gsd-core/workflows/plan-phase/steps/windows-troubleshooting.md": "49f58c3f75be3eb5",
|
||||
"gsd-core/workflows/plan-review-convergence.md": "c635ecb8590a724e",
|
||||
"gsd-core/workflows/plant-seed.md": "10b92ae08a6fdede",
|
||||
"gsd-core/workflows/pr-branch.md": "c87db7ac8c28be1b",
|
||||
"gsd-core/workflows/profile-user.md": "1bac7f69142801ef",
|
||||
"gsd-core/workflows/progress.md": "893aa3c36983f74b",
|
||||
"gsd-core/workflows/quick.md": "4471707540842a41",
|
||||
"gsd-core/workflows/reapply-patches.md": "39050f72601aec89",
|
||||
"gsd-core/workflows/remove-phase.md": "df9a45f0b1880999",
|
||||
"gsd-core/workflows/remove-workspace.md": "a7ca66db6b7c132c",
|
||||
"gsd-core/workflows/resume-project.md": "f28da1200e4545f4",
|
||||
"gsd-core/workflows/review.md": "ad7c0f372ed986ae",
|
||||
"gsd-core/workflows/scan.md": "003883d71c37da7d",
|
||||
"gsd-core/workflows/secure-phase.md": "29fc6b62c5c5dc62",
|
||||
"gsd-core/workflows/session-report.md": "2e5b1205324ddefa",
|
||||
"gsd-core/workflows/settings-advanced.md": "414db4dbea97ba44",
|
||||
"gsd-core/workflows/settings-integrations.md": "70515c5838fb9826",
|
||||
"gsd-core/workflows/settings.md": "d96ddf01fb85e61e",
|
||||
"gsd-core/workflows/ship.md": "b377dab2bc92a6e5",
|
||||
"gsd-core/workflows/sketch-wrap-up.md": "838c701bd072ae73",
|
||||
"gsd-core/workflows/sketch.md": "c7725562b3efd311",
|
||||
"gsd-core/workflows/smart-entry.md": "449238eb94abe187",
|
||||
"gsd-core/workflows/spec-phase.md": "8c480bd91f3cef6f",
|
||||
"gsd-core/workflows/spike-wrap-up.md": "0b24057c340a8a17",
|
||||
"gsd-core/workflows/spike.md": "9134cdc3b75282c9",
|
||||
"gsd-core/workflows/stats.md": "76a42cbeaf6007c2",
|
||||
"gsd-core/workflows/sync-skills.md": "b505e6f8331c0918",
|
||||
"gsd-core/workflows/thread.md": "927e7eeefd2fcf5c",
|
||||
"gsd-core/workflows/transition.md": "cb8ec5affb7ebba1",
|
||||
"gsd-core/workflows/ui-phase.md": "d77fa19403a4684b",
|
||||
"gsd-core/workflows/ui-review.md": "7acfc485526d064b",
|
||||
"gsd-core/workflows/ultraplan-phase.md": "0fb8291153e3937d",
|
||||
"gsd-core/workflows/undo.md": "96d2775f008b3a85",
|
||||
"gsd-core/workflows/update.md": "7e69d278375a2493",
|
||||
"gsd-core/workflows/validate-phase.md": "ce9890f21c6e5607",
|
||||
"gsd-core/workflows/verify-phase.md": "22f18492581f1da5",
|
||||
"gsd-core/workflows/verify-work.md": "63b3f680d8f0a6f3",
|
||||
"hooks/gsd-check-update-worker.js": "55376b5b9335a580",
|
||||
"hooks/gsd-check-update.js": "a89562537a41f83d",
|
||||
"hooks/gsd-config-reload.js": "96546e0e8bb47904",
|
||||
"hooks/gsd-context-monitor.js": "1c48eb0f38a24318",
|
||||
"hooks/gsd-cursor-post-tool.js": "9168e0a09de1972a",
|
||||
"hooks/gsd-cursor-pre-tool.js": "873998b25e308c29",
|
||||
"hooks/gsd-cursor-session-start.js": "9b2e6f4f0c405375",
|
||||
"hooks/gsd-cursor-stop.js": "bfaaf60f419e3238",
|
||||
"hooks/gsd-cursor-subagent-start.js": "06d77fde5c1372b6",
|
||||
"hooks/gsd-cursor-subagent-stop.js": "4bbf22917da4d389",
|
||||
"hooks/gsd-ensure-canonical-path.js": "62d0819a51b55fc4",
|
||||
"hooks/gsd-graphify-update.sh": "e4c6e14fe6ad64ff",
|
||||
"hooks/gsd-phase-boundary.sh": "32739d5fbe0d0a1c",
|
||||
"hooks/gsd-prompt-guard.js": "a749b8cb2c5248de",
|
||||
"hooks/gsd-read-guard.js": "9e423cd03e2d1b16",
|
||||
"hooks/gsd-read-injection-scanner.js": "f454242c010804cf",
|
||||
"hooks/gsd-session-state.sh": "e54379ba86bf1b6d",
|
||||
"hooks/gsd-statusline.js": "5539e1ae859b987e",
|
||||
"hooks/gsd-update-banner.js": "55143a25f978f301",
|
||||
"hooks/gsd-validate-commit.sh": "bf5dd61d33cb3a38",
|
||||
"hooks/gsd-windsurf-pre-command.js": "948be1c6d14c79cd",
|
||||
"hooks/gsd-windsurf-pre-write.js": "92d4dbfbc36ab0cf",
|
||||
"hooks/gsd-workflow-guard.js": "91ae24a15d2bca6f",
|
||||
"hooks/gsd-worktree-path-guard.js": "2a2a7515c01ef998",
|
||||
"hooks/lib/git-cmd.js": "268ba15992ca0b23",
|
||||
"hooks/lib/gsd-graphify-rebuild.sh": "66af89601074d2a9",
|
||||
"hooks/managed-hooks-registry.cjs": "ac720a2b548ba200",
|
||||
"package.json": "dbf8353f77358bc1",
|
||||
"scripts/changeset/README.md": "86ff89331dfd94b2",
|
||||
"scripts/changeset/cli.cjs": "68f92a344b199271",
|
||||
"scripts/changeset/github-release-notes.cjs": "795677f0c009b132",
|
||||
"scripts/changeset/lint.cjs": "0066faed159154f0",
|
||||
"scripts/changeset/new.cjs": "4991e21fd17f5541",
|
||||
"scripts/changeset/parse.cjs": "f9a949cbcab56445",
|
||||
"scripts/changeset/render.cjs": "e47bc3e1587c3cae",
|
||||
"scripts/changeset/serialize.cjs": "ac0b8fe6f87cdb0e",
|
||||
"scripts/fix-slash-commands.cjs": "0519742531ff3529",
|
||||
"scripts/gen-capability-registry.cjs": "c52201ff4d1c2cd7",
|
||||
"scripts/gen-loop-host-contract.cjs": "c7f15237234811a0",
|
||||
"scripts/lib/allowlist-ratchet.cjs": "ffaceaac3efc2660",
|
||||
"scripts/lib/cli-exit.cjs": "612d0c372c75b7e7"
|
||||
}
|
||||
22
tests/fixtures/golden-install-parity/qwen.json
vendored
22
tests/fixtures/golden-install-parity/qwen.json
vendored
@@ -42,7 +42,7 @@
|
||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "6bba2b9c9fa47cb8",
|
||||
"gsd-core/bin/shared/model-catalog.json": "d0b59409a46a456a",
|
||||
"gsd-core/bin/shared/model-catalog.json": "b55176ca044728d3",
|
||||
"gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a",
|
||||
"gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904",
|
||||
"gsd-core/contexts/dev.md": "dcb0de9dce33cf41",
|
||||
@@ -155,7 +155,7 @@
|
||||
"gsd-core/templates/SECURITY.md": "b628f7f1c6d2328f",
|
||||
"gsd-core/templates/UAT.md": "68d32d1fea14e184",
|
||||
"gsd-core/templates/UI-SPEC.md": "7dd5c7cdc7ece0ec",
|
||||
"gsd-core/templates/VALIDATION.md": "6144951011cdca57",
|
||||
"gsd-core/templates/VALIDATION.md": "e4d0f1c48727fce3",
|
||||
"gsd-core/templates/claude-md.md": "ec5972ab31c0f5d0",
|
||||
"gsd-core/templates/codebase/architecture.md": "6be88214162fdd89",
|
||||
"gsd-core/templates/codebase/concerns.md": "efa26d1fb5132f25",
|
||||
@@ -203,7 +203,7 @@
|
||||
"gsd-core/workflows/ai-integration-phase.md": "afdc7c15f03a95fc",
|
||||
"gsd-core/workflows/analyze-dependencies.md": "77aff48f97fa6f1c",
|
||||
"gsd-core/workflows/audit-fix.md": "aaa1b74e001322b6",
|
||||
"gsd-core/workflows/audit-milestone.md": "b7617cf590d92a56",
|
||||
"gsd-core/workflows/audit-milestone.md": "78dbc1e8c4499d7d",
|
||||
"gsd-core/workflows/audit-uat.md": "97d441d07e7972b3",
|
||||
"gsd-core/workflows/autonomous.md": "3014ff90115f0daf",
|
||||
"gsd-core/workflows/check-todos.md": "ec8c22920f6b2df1",
|
||||
@@ -242,7 +242,7 @@
|
||||
"gsd-core/workflows/execute-plan.md": "503b0ced0731dc38",
|
||||
"gsd-core/workflows/explore.md": "e1a83a8982532e5b",
|
||||
"gsd-core/workflows/extract-learnings.md": "dd4fdb88605de49a",
|
||||
"gsd-core/workflows/fast.md": "54fe93778b45a7eb",
|
||||
"gsd-core/workflows/fast.md": "0242082ca646819a",
|
||||
"gsd-core/workflows/forensics.md": "82800a3138ac1da9",
|
||||
"gsd-core/workflows/graduation.md": "40401655435beee3",
|
||||
"gsd-core/workflows/health.md": "043de14edb6a9723",
|
||||
@@ -285,14 +285,14 @@
|
||||
"gsd-core/workflows/remove-phase.md": "e8ae4fbbfac700f0",
|
||||
"gsd-core/workflows/remove-workspace.md": "4ac64de862dc650e",
|
||||
"gsd-core/workflows/resume-project.md": "7f20769f302e5427",
|
||||
"gsd-core/workflows/review.md": "aa4674a4754438f2",
|
||||
"gsd-core/workflows/review.md": "bcbc20cb8df021cc",
|
||||
"gsd-core/workflows/scan.md": "949692db4834dd27",
|
||||
"gsd-core/workflows/secure-phase.md": "6758f1acf4113e9e",
|
||||
"gsd-core/workflows/session-report.md": "2e5b1205324ddefa",
|
||||
"gsd-core/workflows/settings-advanced.md": "d80d7acc4058cb40",
|
||||
"gsd-core/workflows/settings-advanced.md": "5e05212fb5cba531",
|
||||
"gsd-core/workflows/settings-integrations.md": "29c5de27fbbb18e9",
|
||||
"gsd-core/workflows/settings.md": "26b9b7979d3a5747",
|
||||
"gsd-core/workflows/ship.md": "9f94d0b155eb041e",
|
||||
"gsd-core/workflows/ship.md": "cee31be37b2b6c6a",
|
||||
"gsd-core/workflows/sketch-wrap-up.md": "89e0eab2af946b04",
|
||||
"gsd-core/workflows/sketch.md": "483387542d6fc3af",
|
||||
"gsd-core/workflows/smart-entry.md": "d309710bcabd4675",
|
||||
@@ -308,7 +308,7 @@
|
||||
"gsd-core/workflows/ultraplan-phase.md": "49921383982474e0",
|
||||
"gsd-core/workflows/undo.md": "791e0bf96d9a057f",
|
||||
"gsd-core/workflows/update.md": "b34cb866152d4de3",
|
||||
"gsd-core/workflows/validate-phase.md": "e989cbaa4228564c",
|
||||
"gsd-core/workflows/validate-phase.md": "dfb9bd178ee1b67b",
|
||||
"gsd-core/workflows/verify-phase.md": "0dc52b9e629a5f5a",
|
||||
"gsd-core/workflows/verify-work.md": "3355ddc14f052fff",
|
||||
"hooks/gsd-check-update-worker.js": "4bb354044e0dff91",
|
||||
@@ -331,11 +331,13 @@
|
||||
"hooks/gsd-statusline.js": "739140996a3c0d49",
|
||||
"hooks/gsd-update-banner.js": "b457746cb76c1957",
|
||||
"hooks/gsd-validate-commit.sh": "bf5dd61d33cb3a38",
|
||||
"hooks/gsd-windsurf-pre-command.js": "948be1c6d14c79cd",
|
||||
"hooks/gsd-windsurf-pre-write.js": "92d4dbfbc36ab0cf",
|
||||
"hooks/gsd-workflow-guard.js": "59b46a74d19d58d3",
|
||||
"hooks/gsd-worktree-path-guard.js": "8389e4c9175b2613",
|
||||
"hooks/lib/git-cmd.js": "268ba15992ca0b23",
|
||||
"hooks/lib/gsd-graphify-rebuild.sh": "66af89601074d2a9",
|
||||
"hooks/managed-hooks-registry.cjs": "a5a93d50c4ea7a0c",
|
||||
"hooks/managed-hooks-registry.cjs": "08741ed76f1d8970",
|
||||
"package.json": "dbf8353f77358bc1",
|
||||
"scripts/changeset/README.md": "86ff89331dfd94b2",
|
||||
"scripts/changeset/cli.cjs": "68f92a344b199271",
|
||||
@@ -376,7 +378,7 @@
|
||||
"skills/gsd-ns-manage/skills/settings/SKILL.md": "6d9fbddb0b00fd46",
|
||||
"skills/gsd-ns-manage/skills/ship/SKILL.md": "bd6cb3b46d57123f",
|
||||
"skills/gsd-ns-manage/skills/stats/SKILL.md": "5403a46852241fa8",
|
||||
"skills/gsd-ns-manage/skills/surface/SKILL.md": "0b3fe299b6544de7",
|
||||
"skills/gsd-ns-manage/skills/surface/SKILL.md": "ef08f90f89ca373c",
|
||||
"skills/gsd-ns-manage/skills/thread/SKILL.md": "a76c70c368f82dee",
|
||||
"skills/gsd-ns-manage/skills/undo/SKILL.md": "0ad1218f55c94e4b",
|
||||
"skills/gsd-ns-manage/skills/update/SKILL.md": "530b4206d729b56d",
|
||||
|
||||
18
tests/fixtures/golden-install-parity/trae.json
vendored
18
tests/fixtures/golden-install-parity/trae.json
vendored
@@ -42,7 +42,7 @@
|
||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "6bba2b9c9fa47cb8",
|
||||
"gsd-core/bin/shared/model-catalog.json": "d0b59409a46a456a",
|
||||
"gsd-core/bin/shared/model-catalog.json": "b55176ca044728d3",
|
||||
"gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a",
|
||||
"gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904",
|
||||
"gsd-core/contexts/dev.md": "dcb0de9dce33cf41",
|
||||
@@ -155,7 +155,7 @@
|
||||
"gsd-core/templates/SECURITY.md": "b628f7f1c6d2328f",
|
||||
"gsd-core/templates/UAT.md": "68d32d1fea14e184",
|
||||
"gsd-core/templates/UI-SPEC.md": "7dd5c7cdc7ece0ec",
|
||||
"gsd-core/templates/VALIDATION.md": "6144951011cdca57",
|
||||
"gsd-core/templates/VALIDATION.md": "e4d0f1c48727fce3",
|
||||
"gsd-core/templates/claude-md.md": "17217a07ab8a6485",
|
||||
"gsd-core/templates/codebase/architecture.md": "6be88214162fdd89",
|
||||
"gsd-core/templates/codebase/concerns.md": "efa26d1fb5132f25",
|
||||
@@ -203,7 +203,7 @@
|
||||
"gsd-core/workflows/ai-integration-phase.md": "d469eb120e52de0f",
|
||||
"gsd-core/workflows/analyze-dependencies.md": "77aff48f97fa6f1c",
|
||||
"gsd-core/workflows/audit-fix.md": "76607add3257cb37",
|
||||
"gsd-core/workflows/audit-milestone.md": "a379eda51d821bce",
|
||||
"gsd-core/workflows/audit-milestone.md": "ebec95af8f68f2ed",
|
||||
"gsd-core/workflows/audit-uat.md": "6e768b1c208a787a",
|
||||
"gsd-core/workflows/autonomous.md": "c482645c5d1ead46",
|
||||
"gsd-core/workflows/check-todos.md": "0dda8236355e8c9c",
|
||||
@@ -242,7 +242,7 @@
|
||||
"gsd-core/workflows/execute-plan.md": "4910f75bab2040ab",
|
||||
"gsd-core/workflows/explore.md": "8a5437aa0c239c38",
|
||||
"gsd-core/workflows/extract-learnings.md": "3fcc858b20d0d0e6",
|
||||
"gsd-core/workflows/fast.md": "0162075e44072447",
|
||||
"gsd-core/workflows/fast.md": "91f126c4ef24217e",
|
||||
"gsd-core/workflows/forensics.md": "665546666547875d",
|
||||
"gsd-core/workflows/graduation.md": "1ca877cda258a5de",
|
||||
"gsd-core/workflows/health.md": "1f9fd2deea45896f",
|
||||
@@ -285,14 +285,14 @@
|
||||
"gsd-core/workflows/remove-phase.md": "a46c2fe853bf4e86",
|
||||
"gsd-core/workflows/remove-workspace.md": "ae0e1c6d4438d663",
|
||||
"gsd-core/workflows/resume-project.md": "f242e4c8aba18ea2",
|
||||
"gsd-core/workflows/review.md": "51eb79b72a09d47a",
|
||||
"gsd-core/workflows/review.md": "835cf8c9594f17c1",
|
||||
"gsd-core/workflows/scan.md": "63631467651d9ca8",
|
||||
"gsd-core/workflows/secure-phase.md": "6cc236e53c2e7d56",
|
||||
"gsd-core/workflows/session-report.md": "2e5b1205324ddefa",
|
||||
"gsd-core/workflows/settings-advanced.md": "90d913043cd11502",
|
||||
"gsd-core/workflows/settings-advanced.md": "39e66386f6c48025",
|
||||
"gsd-core/workflows/settings-integrations.md": "f8f756709ec02363",
|
||||
"gsd-core/workflows/settings.md": "44b10c59215633b8",
|
||||
"gsd-core/workflows/ship.md": "c08f0fe3025d2c86",
|
||||
"gsd-core/workflows/ship.md": "da81bf2515257822",
|
||||
"gsd-core/workflows/sketch-wrap-up.md": "dbec602d104cb951",
|
||||
"gsd-core/workflows/sketch.md": "44ff275150b6d045",
|
||||
"gsd-core/workflows/smart-entry.md": "d8018578571f08d5",
|
||||
@@ -308,7 +308,7 @@
|
||||
"gsd-core/workflows/ultraplan-phase.md": "77b58ef8af5209bd",
|
||||
"gsd-core/workflows/undo.md": "59b8baa54efc4110",
|
||||
"gsd-core/workflows/update.md": "1f935251fca1f276",
|
||||
"gsd-core/workflows/validate-phase.md": "1c0ebe56d96a14d1",
|
||||
"gsd-core/workflows/validate-phase.md": "58fd1a0a679d7115",
|
||||
"gsd-core/workflows/verify-phase.md": "a151ed36eb51813b",
|
||||
"gsd-core/workflows/verify-work.md": "dde2a42a56c27c4e",
|
||||
"scripts/changeset/README.md": "86ff89331dfd94b2",
|
||||
@@ -350,7 +350,7 @@
|
||||
"skills/gsd-ns-manage/skills/settings/SKILL.md": "e3590bb5e3d0fdf2",
|
||||
"skills/gsd-ns-manage/skills/ship/SKILL.md": "456609cf127b36dc",
|
||||
"skills/gsd-ns-manage/skills/stats/SKILL.md": "f5f5f1fbfe65cee8",
|
||||
"skills/gsd-ns-manage/skills/surface/SKILL.md": "025a48d0b1129e47",
|
||||
"skills/gsd-ns-manage/skills/surface/SKILL.md": "2c92a300547a5a97",
|
||||
"skills/gsd-ns-manage/skills/thread/SKILL.md": "48894eaf985265b3",
|
||||
"skills/gsd-ns-manage/skills/undo/SKILL.md": "0deb95bc44edf37c",
|
||||
"skills/gsd-ns-manage/skills/update/SKILL.md": "ffcd813ad256769b",
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "6bba2b9c9fa47cb8",
|
||||
"gsd-core/bin/shared/model-catalog.json": "d0b59409a46a456a",
|
||||
"gsd-core/bin/shared/model-catalog.json": "b55176ca044728d3",
|
||||
"gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a",
|
||||
"gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904",
|
||||
"gsd-core/contexts/dev.md": "dcb0de9dce33cf41",
|
||||
@@ -155,7 +155,7 @@
|
||||
"gsd-core/templates/SECURITY.md": "b628f7f1c6d2328f",
|
||||
"gsd-core/templates/UAT.md": "68d32d1fea14e184",
|
||||
"gsd-core/templates/UI-SPEC.md": "7dd5c7cdc7ece0ec",
|
||||
"gsd-core/templates/VALIDATION.md": "6144951011cdca57",
|
||||
"gsd-core/templates/VALIDATION.md": "e4d0f1c48727fce3",
|
||||
"gsd-core/templates/claude-md.md": "20be2a0201ab92cd",
|
||||
"gsd-core/templates/codebase/architecture.md": "6be88214162fdd89",
|
||||
"gsd-core/templates/codebase/concerns.md": "efa26d1fb5132f25",
|
||||
@@ -203,7 +203,7 @@
|
||||
"gsd-core/workflows/ai-integration-phase.md": "8948988717506320",
|
||||
"gsd-core/workflows/analyze-dependencies.md": "77aff48f97fa6f1c",
|
||||
"gsd-core/workflows/audit-fix.md": "aa052e15623d759d",
|
||||
"gsd-core/workflows/audit-milestone.md": "ac2238ea6c11ae9e",
|
||||
"gsd-core/workflows/audit-milestone.md": "868db5a8f5d55040",
|
||||
"gsd-core/workflows/audit-uat.md": "d496e39402e160de",
|
||||
"gsd-core/workflows/autonomous.md": "fe7bb7c978f305a2",
|
||||
"gsd-core/workflows/check-todos.md": "afc840fceb07bf99",
|
||||
@@ -242,7 +242,7 @@
|
||||
"gsd-core/workflows/execute-plan.md": "c8567fb4438b4404",
|
||||
"gsd-core/workflows/explore.md": "04e461ff8159a24e",
|
||||
"gsd-core/workflows/extract-learnings.md": "af793bdf4ffd1c8a",
|
||||
"gsd-core/workflows/fast.md": "0162075e44072447",
|
||||
"gsd-core/workflows/fast.md": "91f126c4ef24217e",
|
||||
"gsd-core/workflows/forensics.md": "3d1ce16b5f605592",
|
||||
"gsd-core/workflows/graduation.md": "a766039ff6ca653f",
|
||||
"gsd-core/workflows/health.md": "7b19d6e2c0357c3e",
|
||||
@@ -285,14 +285,14 @@
|
||||
"gsd-core/workflows/remove-phase.md": "e7a6af429b36e77b",
|
||||
"gsd-core/workflows/remove-workspace.md": "b5e60fbb33b3e33a",
|
||||
"gsd-core/workflows/resume-project.md": "82cfe1b8cb17c085",
|
||||
"gsd-core/workflows/review.md": "31b1f08d08b8ccdc",
|
||||
"gsd-core/workflows/review.md": "3e72508a5dccd45a",
|
||||
"gsd-core/workflows/scan.md": "12c11b2edc165df9",
|
||||
"gsd-core/workflows/secure-phase.md": "7bf923689bf58288",
|
||||
"gsd-core/workflows/session-report.md": "2e5b1205324ddefa",
|
||||
"gsd-core/workflows/settings-advanced.md": "18eff9c17e0b1948",
|
||||
"gsd-core/workflows/settings-advanced.md": "2f86ec7b998f9485",
|
||||
"gsd-core/workflows/settings-integrations.md": "b082fc518b484c07",
|
||||
"gsd-core/workflows/settings.md": "002eb0ce3c10c741",
|
||||
"gsd-core/workflows/ship.md": "c035bb8b3bb7efbb",
|
||||
"gsd-core/workflows/ship.md": "bce39010f9518cf3",
|
||||
"gsd-core/workflows/sketch-wrap-up.md": "10063f56c2c7f141",
|
||||
"gsd-core/workflows/sketch.md": "25c1f8f7acfb1da9",
|
||||
"gsd-core/workflows/smart-entry.md": "bd81482cb6a7ac53",
|
||||
@@ -308,9 +308,11 @@
|
||||
"gsd-core/workflows/ultraplan-phase.md": "53b77a8edf60acd0",
|
||||
"gsd-core/workflows/undo.md": "18dec684fb1076f9",
|
||||
"gsd-core/workflows/update.md": "79aaf4b8f1f83045",
|
||||
"gsd-core/workflows/validate-phase.md": "2db47bf5547d7b9d",
|
||||
"gsd-core/workflows/validate-phase.md": "bf16fd7ff71286b2",
|
||||
"gsd-core/workflows/verify-phase.md": "f961cdb3ef03ff05",
|
||||
"gsd-core/workflows/verify-work.md": "cce8ae44b30957f1",
|
||||
"hooks/gsd-windsurf-pre-command.js": "7467a8a63e354aad",
|
||||
"hooks/gsd-windsurf-pre-write.js": "1c8a776d7ae2dcce",
|
||||
"scripts/changeset/README.md": "86ff89331dfd94b2",
|
||||
"scripts/changeset/cli.cjs": "68f92a344b199271",
|
||||
"scripts/changeset/github-release-notes.cjs": "795677f0c009b132",
|
||||
|
||||
20
tests/fixtures/golden-install-parity/zcode.json
vendored
20
tests/fixtures/golden-install-parity/zcode.json
vendored
@@ -95,7 +95,7 @@
|
||||
"commands/gsd-spec-phase.md": "c0e592ef19cc6721",
|
||||
"commands/gsd-spike.md": "5c85bade6e6b804f",
|
||||
"commands/gsd-stats.md": "bea8a4f0e6f2ddf3",
|
||||
"commands/gsd-surface.md": "b1ab5c2127dbf856",
|
||||
"commands/gsd-surface.md": "0317dc3b56ffcce6",
|
||||
"commands/gsd-thread.md": "56036040a95fc60b",
|
||||
"commands/gsd-ui-phase.md": "b48b780198ec7a2e",
|
||||
"commands/gsd-ui-review.md": "2ed61b5729f6e492",
|
||||
@@ -113,7 +113,7 @@
|
||||
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
|
||||
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
|
||||
"gsd-core/bin/shared/config-schema.manifest.json": "6bba2b9c9fa47cb8",
|
||||
"gsd-core/bin/shared/model-catalog.json": "d0b59409a46a456a",
|
||||
"gsd-core/bin/shared/model-catalog.json": "b55176ca044728d3",
|
||||
"gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a",
|
||||
"gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904",
|
||||
"gsd-core/contexts/dev.md": "dcb0de9dce33cf41",
|
||||
@@ -226,7 +226,7 @@
|
||||
"gsd-core/templates/SECURITY.md": "b628f7f1c6d2328f",
|
||||
"gsd-core/templates/UAT.md": "9e296471b97ebcec",
|
||||
"gsd-core/templates/UI-SPEC.md": "7dd5c7cdc7ece0ec",
|
||||
"gsd-core/templates/VALIDATION.md": "f53e0ca061d3528e",
|
||||
"gsd-core/templates/VALIDATION.md": "6f0fe4214dff6dd1",
|
||||
"gsd-core/templates/claude-md.md": "d1d333e4b963c0d2",
|
||||
"gsd-core/templates/codebase/architecture.md": "6be88214162fdd89",
|
||||
"gsd-core/templates/codebase/concerns.md": "efa26d1fb5132f25",
|
||||
@@ -274,7 +274,7 @@
|
||||
"gsd-core/workflows/ai-integration-phase.md": "40c217869a06981f",
|
||||
"gsd-core/workflows/analyze-dependencies.md": "52942af10f140717",
|
||||
"gsd-core/workflows/audit-fix.md": "eedb2da4bffb7575",
|
||||
"gsd-core/workflows/audit-milestone.md": "6866ce260980c21e",
|
||||
"gsd-core/workflows/audit-milestone.md": "3835762c3147a927",
|
||||
"gsd-core/workflows/audit-uat.md": "fcfbec501620b564",
|
||||
"gsd-core/workflows/autonomous.md": "7e5683728ef33707",
|
||||
"gsd-core/workflows/check-todos.md": "bdeaf43f9c61e0cc",
|
||||
@@ -313,7 +313,7 @@
|
||||
"gsd-core/workflows/execute-plan.md": "ed874410972d32b7",
|
||||
"gsd-core/workflows/explore.md": "6c04f2e658d93261",
|
||||
"gsd-core/workflows/extract-learnings.md": "b6f01ca3d8f58de4",
|
||||
"gsd-core/workflows/fast.md": "94136fb570d20a9d",
|
||||
"gsd-core/workflows/fast.md": "8878ec034b401f36",
|
||||
"gsd-core/workflows/forensics.md": "0d500a3f5ab26913",
|
||||
"gsd-core/workflows/graduation.md": "47f1594c88c08501",
|
||||
"gsd-core/workflows/health.md": "107e3c72e76d9535",
|
||||
@@ -356,14 +356,14 @@
|
||||
"gsd-core/workflows/remove-phase.md": "df9a45f0b1880999",
|
||||
"gsd-core/workflows/remove-workspace.md": "a7ca66db6b7c132c",
|
||||
"gsd-core/workflows/resume-project.md": "f28da1200e4545f4",
|
||||
"gsd-core/workflows/review.md": "93fe46bd0b5dd3d1",
|
||||
"gsd-core/workflows/review.md": "ad7c0f372ed986ae",
|
||||
"gsd-core/workflows/scan.md": "003883d71c37da7d",
|
||||
"gsd-core/workflows/secure-phase.md": "29fc6b62c5c5dc62",
|
||||
"gsd-core/workflows/session-report.md": "2e5b1205324ddefa",
|
||||
"gsd-core/workflows/settings-advanced.md": "b6fa466a953dd3a2",
|
||||
"gsd-core/workflows/settings-advanced.md": "414db4dbea97ba44",
|
||||
"gsd-core/workflows/settings-integrations.md": "70515c5838fb9826",
|
||||
"gsd-core/workflows/settings.md": "d96ddf01fb85e61e",
|
||||
"gsd-core/workflows/ship.md": "7b8fe9f89143e648",
|
||||
"gsd-core/workflows/ship.md": "b377dab2bc92a6e5",
|
||||
"gsd-core/workflows/sketch-wrap-up.md": "86db87b16548117e",
|
||||
"gsd-core/workflows/sketch.md": "e96f1866d3e60cab",
|
||||
"gsd-core/workflows/smart-entry.md": "449238eb94abe187",
|
||||
@@ -379,7 +379,7 @@
|
||||
"gsd-core/workflows/ultraplan-phase.md": "0fb8291153e3937d",
|
||||
"gsd-core/workflows/undo.md": "96d2775f008b3a85",
|
||||
"gsd-core/workflows/update.md": "dc580dee13f881a6",
|
||||
"gsd-core/workflows/validate-phase.md": "2c6d7671fcaabcaa",
|
||||
"gsd-core/workflows/validate-phase.md": "ce9890f21c6e5607",
|
||||
"gsd-core/workflows/verify-phase.md": "22f18492581f1da5",
|
||||
"gsd-core/workflows/verify-work.md": "63b3f680d8f0a6f3",
|
||||
"scripts/changeset/README.md": "86ff89331dfd94b2",
|
||||
@@ -421,7 +421,7 @@
|
||||
"skills/gsd-ns-manage/skills/settings/SKILL.md": "8fcd8b04bb607ca7",
|
||||
"skills/gsd-ns-manage/skills/ship/SKILL.md": "eb23c16b4893b035",
|
||||
"skills/gsd-ns-manage/skills/stats/SKILL.md": "af60a0fed275a90b",
|
||||
"skills/gsd-ns-manage/skills/surface/SKILL.md": "9bc5790f601615cf",
|
||||
"skills/gsd-ns-manage/skills/surface/SKILL.md": "cf3c08a3fe963be7",
|
||||
"skills/gsd-ns-manage/skills/thread/SKILL.md": "59e0ea339e45a105",
|
||||
"skills/gsd-ns-manage/skills/undo/SKILL.md": "22d4462c987b7b66",
|
||||
"skills/gsd-ns-manage/skills/update/SKILL.md": "01ca0efbdb8f6efa",
|
||||
|
||||
28
tests/fixtures/pi-host-plugin.cjs
vendored
28
tests/fixtures/pi-host-plugin.cjs
vendored
@@ -14,10 +14,18 @@
|
||||
* behaviorally testable without a live pi runtime. The default export matches
|
||||
* pi's extension entry shape: `export default function (pi: ExtensionAPI) { … }`.
|
||||
*
|
||||
* #2102 Stage 2: registerCommand takes `handler(args, ctx)` (args is the raw
|
||||
* string after the command) — NOT `execute(ctx)`, which the original #1682
|
||||
* cut used. gsd_invoke's `execute` takes pi's real 5-arg tool-execute
|
||||
* signature `(toolCallId, params, signal, onUpdate, ctx)`. See pi/gsd.cjs for
|
||||
* the full production binding this reference fixture mirrors.
|
||||
*
|
||||
* NOTE: this is the reference binding that proves the ExtensionAPI imperative
|
||||
* adapter (#1682 AC). Full `--pi` installable-runtime integration (descriptor +
|
||||
* installer wiring + golden parity 16→17) is a larger follow-up tracked
|
||||
* separately — it is intentionally NOT added to the runtime registry here.
|
||||
* adapter (#1682 AC), kept as a mock-friendly fixture independent of the real
|
||||
* `pi/gsd.cjs` extension. Full `--pi` installable-runtime integration
|
||||
* (descriptor + installer wiring + golden parity 16→17) shipped in #2102
|
||||
* Stage 1 — see capabilities/pi/capability.json and
|
||||
* tests/fixtures/golden-install-parity/pi.json.
|
||||
*
|
||||
* @param {object} pi pi ExtensionAPI (registerTool/registerCommand/on/…)
|
||||
*/
|
||||
@@ -30,9 +38,12 @@ module.exports = function gsdPiPlugin(pi) {
|
||||
// imperative adapter (createImperativeAdapter({runtime:'pi'}) + dispatch).
|
||||
pi.registerCommand('gsd', {
|
||||
description: 'Invoke a GSD command via the embedded engine (imperative adapter).',
|
||||
execute: async function /* ctx */ () {
|
||||
handler: async function (args, ctx) {
|
||||
void args;
|
||||
void ctx;
|
||||
// Engine dispatch is wired by the host at load (createImperativeAdapter).
|
||||
// Kept declarative in the reference; the real binding dispatches the hub.
|
||||
// Kept declarative in the reference; the real binding (pi/gsd.cjs)
|
||||
// dispatches through gsd-tools.cjs via dispatchGsdCommand.
|
||||
},
|
||||
});
|
||||
|
||||
@@ -41,7 +52,12 @@ module.exports = function gsdPiPlugin(pi) {
|
||||
pi.registerTool({
|
||||
name: 'gsd_invoke',
|
||||
description: 'Invoke a GSD command family/subcommand through the engine.',
|
||||
execute: async function () {
|
||||
execute: async function (toolCallId, params, signal, onUpdate, ctx) {
|
||||
void toolCallId;
|
||||
void params;
|
||||
void signal;
|
||||
void onUpdate;
|
||||
void ctx;
|
||||
return 'ok';
|
||||
},
|
||||
});
|
||||
|
||||
71
tests/fixtures/vscode-host-binding.cjs
vendored
71
tests/fixtures/vscode-host-binding.cjs
vendored
@@ -1,69 +1,12 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Reference VS Code IDE host binding for GSD (ADR-1239 Phase D / #1933).
|
||||
* Thin re-export of the SHIPPED VS Code host binding (#2103).
|
||||
*
|
||||
* VS Code is the IDE-profile reference host. It composes the Phase-3 engine
|
||||
* seams for the negotiated `ide` profile (host-integration.cts PROFILE_BASELINES):
|
||||
*
|
||||
* - modelMode: 'active' → createModelAdapter({modelMode:'active'}, {sendRequest})
|
||||
* backed by `vscode.lm` (LanguageModelChat). VS Code rejects
|
||||
* system-role messages, so the request mapper uses User role only.
|
||||
* - hookBus: 'engine' → createHookBus({bus:'engine'}) — VS Code has NO host event bus,
|
||||
* so GSD owns the bus in-process (full subscribe + emit).
|
||||
* - stateIO: 'sandboxed-storage' → createStateIO({io:'sandboxed-storage'}, {backend}) bound to a
|
||||
* host-supplied storage (no arbitrary FS — web/no-child_process safe).
|
||||
* - embeddingMode: 'imperative' → createImperativeAdapter({runtime:'vscode'}) — engine-as-library.
|
||||
*
|
||||
* Distribution: VS Code is shipped as an EXTENSION (Marketplace), NOT file-projected onto a
|
||||
* config dir, so it intentionally has NO runtime descriptor / `--vscode` installer entry — the
|
||||
* extension IS the host. This module is the binding the extension's activate() runs.
|
||||
*
|
||||
* Mock-friendly: takes `vscode` (with `vscode.lm`) + `hostStorage` ({read,write}) so it is
|
||||
* behaviorally testable without a live VS Code host.
|
||||
*
|
||||
* @param {{ lm: { sendRequest: (req: unknown) => unknown } }} vscode VS Code namespace (vscode.lm)
|
||||
* @param {{ read: (path: string) => string, write: (path: string, content: string) => void }} hostStorage
|
||||
* sandboxed-storage backend (e.g. globalState/workspaceState/secrets).
|
||||
* @returns {object} the composed IDE host surface: { runtime, model, hookBus, stateIO, adapter, commands }
|
||||
* `bindGsdToVscode` moved to `vscode/host-binding.js` (the module the real
|
||||
* extension's `activate()` requires) so it ships with the extension instead of
|
||||
* living only under tests/. This fixture re-exports it so every existing test
|
||||
* that requires `./fixtures/vscode-host-binding.cjs` keeps resolving without
|
||||
* edits (tests/vscode-ide-reference.test.cjs and any other consumer).
|
||||
*/
|
||||
module.exports = function bindGsdToVscode(vscode, hostStorage) {
|
||||
if (!vscode || !vscode.lm || typeof vscode.lm.sendRequest !== 'function') {
|
||||
throw new TypeError('bindGsdToVscode: vscode.lm.sendRequest is required (active model provider)');
|
||||
}
|
||||
if (!hostStorage || typeof hostStorage.read !== 'function' || typeof hostStorage.write !== 'function') {
|
||||
throw new TypeError('bindGsdToVscode: hostStorage {read,write} is required (sandboxed-storage backend)');
|
||||
}
|
||||
|
||||
const { createImperativeAdapter } = require('../../gsd-core/bin/lib/adapter-imperative.cjs');
|
||||
const { createModelAdapter } = require('../../gsd-core/bin/lib/model-adapter.cjs');
|
||||
const { createHookBus } = require('../../gsd-core/bin/lib/hook-bus.cjs');
|
||||
const { createStateIO } = require('../../gsd-core/bin/lib/state-io.cjs');
|
||||
|
||||
// Active model: GSD model calls route through vscode.lm. (No system-role
|
||||
// messages — VS Code rejects them; a full extension builds LanguageModelChatMessages
|
||||
// with User role only and selects a model via vscode.lm.selectChatModels.)
|
||||
const model = createModelAdapter({ modelMode: 'active' }, {
|
||||
sendRequest(req) {
|
||||
return vscode.lm.sendRequest(req);
|
||||
},
|
||||
});
|
||||
|
||||
// Engine-owned hook bus: VS Code has no host bus, so GSD owns it in-process.
|
||||
const hookBus = createHookBus({ bus: 'engine' });
|
||||
|
||||
// Sandboxed-storage stateIO bound to the host storage backend (no fs / no child_process).
|
||||
const stateIO = createStateIO({ io: 'sandboxed-storage' }, { backend: hostStorage });
|
||||
|
||||
// Imperative adapter: the engine-as-library for the VS Code runtime.
|
||||
const adapter = createImperativeAdapter({ runtime: 'vscode' });
|
||||
|
||||
// Command surface: Command Palette + Chat participant entries bound to the
|
||||
// GSD command-routing hub via the imperative adapter (interface point 1).
|
||||
const commands = Object.freeze({
|
||||
'gsd.invoke': Object.freeze({ description: 'Invoke a GSD command via the embedded engine (palette/chat).' }),
|
||||
'gsd.help': Object.freeze({ description: 'List GSD commands available in the IDE host.' }),
|
||||
});
|
||||
|
||||
return Object.freeze({ runtime: 'vscode', model, hookBus, stateIO, adapter, commands });
|
||||
};
|
||||
module.exports = require('../../vscode/host-binding.js');
|
||||
|
||||
@@ -164,15 +164,21 @@ describe('#1928 gemini removed from every runtime-name-policy surface', () => {
|
||||
assert.strictEqual(getRuntimeNewProjectCommand('gemini'), '/gsd-new-project', 'new-project override removed → default');
|
||||
});
|
||||
|
||||
test('runtimeFlags has no isGemini and covers exactly the non-claude registry runtimes (count-agnostic)', () => {
|
||||
test('runtimeFlags has no isGemini and covers exactly the non-claude, CLI-installable registry runtimes (count-agnostic)', () => {
|
||||
const flags = runtimeFlags('claude');
|
||||
assert.ok(!('isGemini' in flags), 'isGemini flag must be gone');
|
||||
// The flag set tracks the non-claude registry runtimes (one is<Runtime> per
|
||||
// id), so adding a runtime updates the count automatically — no hand-pinned
|
||||
// number that would break on the next runtime addition.
|
||||
const expectedNonClaudeCount = Object.keys(registry.runtimes).filter((id) => id !== 'claude').length;
|
||||
// #2103: registry runtimes with installSurface === 'none' (e.g. vscode —
|
||||
// Marketplace/VSIX-distributed, never CLI-installed) have no --<rt> flag
|
||||
// by design (see tests/runtime-flags.test.cjs's NON_INSTALLABLE_RUNTIMES)
|
||||
// and are excluded from this count too.
|
||||
const expectedNonClaudeCount = Object.keys(registry.runtimes)
|
||||
.filter((id) => id !== 'claude' && registry.runtimes[id].runtime.installSurface !== 'none')
|
||||
.length;
|
||||
assert.strictEqual(Object.keys(flags).length, expectedNonClaudeCount,
|
||||
'flag count must equal the non-claude registry runtime count');
|
||||
'flag count must equal the non-claude, CLI-installable registry runtime count');
|
||||
});
|
||||
|
||||
test('gemini no longer maps to GEMINI.md (defaults to AGENTS.md)', () => {
|
||||
|
||||
144
tests/gen-registry.test.cjs
Normal file
144
tests/gen-registry.test.cjs
Normal file
@@ -0,0 +1,144 @@
|
||||
'use strict';
|
||||
process.env.GSD_TEST_MODE = '1';
|
||||
|
||||
const { test, describe } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
const { spawnSync } = require('node:child_process');
|
||||
const { cleanup } = require('./helpers.cjs');
|
||||
|
||||
const SCRIPT_PATH = path.join(__dirname, '..', 'scripts', 'gen-registry.cjs');
|
||||
const { renderMarkdown } = require(path.join(__dirname, '..', 'scripts', 'registry-schema.cjs'));
|
||||
|
||||
// gen-registry.cjs resolves docs/registries/ from process.cwd() (mirrors
|
||||
// validate-registry.cjs), so tests drive it as a subprocess with `cwd`
|
||||
// pointed at an isolated temp fixture directory.
|
||||
|
||||
function validCapabilityEntry() {
|
||||
return {
|
||||
id: 'my-capability',
|
||||
name: 'My Capability',
|
||||
type: 'capability',
|
||||
repo: 'octocat/my-capability',
|
||||
description: 'Does a useful thing for GSD users.',
|
||||
author: 'Octocat',
|
||||
license: 'MIT',
|
||||
enginesGsd: '>=1.6.0 <3.0.0',
|
||||
install: 'gsd capability install https://github.com/octocat/my-capability.git#v1.0.0',
|
||||
uninstall: 'gsd capability remove my-capability',
|
||||
interactions: {
|
||||
loopExtensionPoints: ['execute:pre'],
|
||||
hookKinds: ['step'],
|
||||
configKeys: [],
|
||||
requires: [],
|
||||
runtimeCompat: ['all'],
|
||||
produces: [],
|
||||
consumes: [],
|
||||
},
|
||||
discussion: 'https://github.com/octocat/my-capability/discussions/1',
|
||||
};
|
||||
}
|
||||
|
||||
function withFixture(entries, fn) {
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-gen-registry-'));
|
||||
try {
|
||||
const registriesDir = path.join(tmp, 'docs', 'registries');
|
||||
fs.mkdirSync(registriesDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(registriesDir, 'capabilities.json'), JSON.stringify(entries, null, 2) + '\n');
|
||||
fn(tmp, registriesDir);
|
||||
} finally {
|
||||
cleanup(tmp);
|
||||
}
|
||||
}
|
||||
|
||||
function runGen(cwd, args = []) {
|
||||
return spawnSync(process.execPath, [SCRIPT_PATH, ...args], { cwd, encoding: 'utf8' });
|
||||
}
|
||||
|
||||
describe('gen-registry CLI (subprocess)', () => {
|
||||
test('--write then --check is clean (no drift) for a populated registry', () => {
|
||||
withFixture([validCapabilityEntry()], (tmp, registriesDir) => {
|
||||
const write = runGen(tmp, ['--write']);
|
||||
assert.equal(write.status, 0, `stderr: ${write.stderr}`);
|
||||
assert.ok(fs.existsSync(path.join(registriesDir, 'capability-registry.md')));
|
||||
|
||||
const check = runGen(tmp, ['--check']);
|
||||
assert.equal(check.status, 0, `stderr: ${check.stderr}`);
|
||||
});
|
||||
});
|
||||
|
||||
test('hand-mutating the generated md then --check fails (drift detected)', () => {
|
||||
withFixture([validCapabilityEntry()], (tmp, registriesDir) => {
|
||||
const write = runGen(tmp, ['--write']);
|
||||
assert.equal(write.status, 0, `stderr: ${write.stderr}`);
|
||||
|
||||
const mdPath = path.join(registriesDir, 'capability-registry.md');
|
||||
fs.appendFileSync(mdPath, '\nhand-edited drift line\n');
|
||||
|
||||
const check = runGen(tmp, ['--check']);
|
||||
assert.notEqual(check.status, 0);
|
||||
});
|
||||
});
|
||||
|
||||
test('--write on an empty registry ([]) produces md containing the empty-state text', () => {
|
||||
withFixture([], (tmp, registriesDir) => {
|
||||
const write = runGen(tmp, ['--write']);
|
||||
assert.equal(write.status, 0, `stderr: ${write.stderr}`);
|
||||
|
||||
const mdPath = path.join(registriesDir, 'capability-registry.md');
|
||||
const content = fs.readFileSync(mdPath, 'utf8');
|
||||
assert.match(content, /No entries yet/);
|
||||
});
|
||||
});
|
||||
|
||||
test('default (no flag) prints rendered markdown to stdout', () => {
|
||||
withFixture([validCapabilityEntry()], (tmp) => {
|
||||
const result = runGen(tmp, []);
|
||||
assert.equal(result.status, 0, `stderr: ${result.stderr}`);
|
||||
assert.ok(result.stdout.length > 0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('gen-registry CLI (subprocess): F3 — missing capabilities.json is an error, not a silent pass', () => {
|
||||
test('--check exits non-zero when docs/registries/ exists but capabilities.json is absent', () => {
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-gen-registry-nocaps-'));
|
||||
try {
|
||||
fs.mkdirSync(path.join(tmp, 'docs', 'registries'), { recursive: true });
|
||||
// Deliberately do NOT write capabilities.json — only eos.json is optional.
|
||||
const check = runGen(tmp, ['--check']);
|
||||
assert.notEqual(check.status, 0, `expected non-zero exit, got 0. stdout: ${check.stdout}`);
|
||||
assert.match(check.stderr, /capabilities\.json/);
|
||||
} finally {
|
||||
cleanup(tmp);
|
||||
}
|
||||
});
|
||||
|
||||
test('default mode (no flag) also hard-errors when capabilities.json is absent', () => {
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-gen-registry-nocaps-'));
|
||||
try {
|
||||
fs.mkdirSync(path.join(tmp, 'docs', 'registries'), { recursive: true });
|
||||
const result = runGen(tmp, []);
|
||||
assert.notEqual(result.status, 0, `expected non-zero exit, got 0. stdout: ${result.stdout}`);
|
||||
assert.match(result.stderr, /capabilities\.json/);
|
||||
} finally {
|
||||
cleanup(tmp);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('gen-registry: renderMarkdown (direct, via registry-schema)', () => {
|
||||
test('renders empty-state text for an empty capability registry', () => {
|
||||
const rendered = renderMarkdown([], { type: 'capability', sourceFile: 'capabilities.json' });
|
||||
assert.match(rendered, /No entries yet/);
|
||||
});
|
||||
|
||||
test('renders the shields.io badge + discussion link for a populated capability registry', () => {
|
||||
const entry = validCapabilityEntry();
|
||||
const rendered = renderMarkdown([entry], { type: 'capability', sourceFile: 'capabilities.json' });
|
||||
assert.match(rendered, /img\.shields\.io\/github\/v\/release/);
|
||||
assert.ok(rendered.includes(entry.discussion));
|
||||
});
|
||||
});
|
||||
@@ -14,6 +14,13 @@
|
||||
* - a structural cross-check that every descriptor's localConfigDir is a
|
||||
* non-empty dot-dir string.
|
||||
*
|
||||
* #2103: vscode's `configHome.kind === 'none'` (no file-projected config
|
||||
* directory at all) is the first descriptor with `localConfigDir: null` — it
|
||||
* is carved out of the "non-empty dot-dir string" invariant below and
|
||||
* asserted against getDirName's documented NO_LOCAL_CONFIG_DIR_SENTINEL
|
||||
* instead (mirrors the carve-out in
|
||||
* tests/non-claude-runtimes-registry-derivation.test.cjs).
|
||||
*
|
||||
* ADR-1239 Phase B (#1679). Behavioral tests only: assert on returned values.
|
||||
*/
|
||||
|
||||
@@ -22,14 +29,36 @@ const assert = require('node:assert/strict');
|
||||
const runtimeNamePolicy = require('../gsd-core/bin/lib/runtime-name-policy.cjs');
|
||||
const registry = require('../gsd-core/bin/lib/capability-registry.cjs');
|
||||
|
||||
const { getDirName } = runtimeNamePolicy;
|
||||
const { getDirName, NO_LOCAL_CONFIG_DIR_SENTINEL } = runtimeNamePolicy;
|
||||
|
||||
const RUNTIME_IDS = Object.keys(registry.runtimes);
|
||||
|
||||
// Runtimes whose configHome.kind === 'none' have NO file-projected config
|
||||
// directory at all (localConfigDir is legitimately null) — carved out of the
|
||||
// "non-empty dot-dir string" invariant, the same way dot-home-nested's
|
||||
// .parent is a conditional carve-out in the validator.
|
||||
const NO_LOCAL_CONFIG_DIR_RUNTIMES = new Set(
|
||||
RUNTIME_IDS.filter((id) => {
|
||||
const desc = registry.runtimes[id] && registry.runtimes[id].runtime;
|
||||
return !!(desc && desc.configHome && desc.configHome.kind === 'none');
|
||||
}),
|
||||
);
|
||||
|
||||
test('getDirName(id) projects each descriptor runtime.localConfigDir (derivation contract, count-agnostic)', () => {
|
||||
assert.ok(RUNTIME_IDS.length > 0, 'registry must contain at least one runtime');
|
||||
for (const id of RUNTIME_IDS) {
|
||||
const desc = registry.runtimes[id] && registry.runtimes[id].runtime;
|
||||
if (NO_LOCAL_CONFIG_DIR_RUNTIMES.has(id)) {
|
||||
// #2103: no file-projected config dir — getDirName must return the
|
||||
// documented sentinel, not the '.claude' default and not null.
|
||||
assert.strictEqual(desc.localConfigDir, null,
|
||||
`${id}: configHome.kind === 'none' runtimes must declare localConfigDir: null`);
|
||||
assert.strictEqual(
|
||||
getDirName(id),
|
||||
NO_LOCAL_CONFIG_DIR_SENTINEL,
|
||||
`getDirName('${id}') must equal the documented no-local-config-dir sentinel`);
|
||||
continue;
|
||||
}
|
||||
const expected = desc && desc.localConfigDir;
|
||||
assert.ok(typeof expected === 'string' && expected.length > 0,
|
||||
`registry.runtimes['${id}'].runtime.localConfigDir must be a non-empty string`);
|
||||
@@ -46,11 +75,16 @@ test('getDirName fallback: unknown / empty runtime returns ".claude" (fail-close
|
||||
assert.strictEqual(getDirName('__nonexistent_runtime__'), '.claude');
|
||||
});
|
||||
|
||||
test('registry cross-check: every runtimes[id].runtime.localConfigDir is a non-empty dot-dir string', () => {
|
||||
test('registry cross-check: every runtimes[id].runtime.localConfigDir is a non-empty dot-dir string, except configHome.kind==="none" runtimes (localConfigDir: null)', () => {
|
||||
for (const [id, entry] of Object.entries(registry.runtimes)) {
|
||||
if (!entry || typeof entry !== 'object') continue;
|
||||
const runtimeBlock = entry.runtime;
|
||||
if (!runtimeBlock || typeof runtimeBlock !== 'object') continue;
|
||||
if (NO_LOCAL_CONFIG_DIR_RUNTIMES.has(id)) {
|
||||
assert.strictEqual(runtimeBlock.localConfigDir, null,
|
||||
`registry.runtimes['${id}'].runtime.localConfigDir must be null (configHome.kind === 'none')`);
|
||||
continue;
|
||||
}
|
||||
const dir = runtimeBlock.localConfigDir;
|
||||
assert.strictEqual(typeof dir, 'string',
|
||||
`registry.runtimes['${id}'].runtime.localConfigDir must be a string (got: ${typeof dir})`);
|
||||
|
||||
@@ -47,11 +47,14 @@ const GOLDEN_FRAGMENT_MAP = {
|
||||
cline: "'.cline'",
|
||||
kimi: "'.config', 'agents'",
|
||||
zcode: "'.zcode'",
|
||||
pi: "'.pi', 'agent'",
|
||||
};
|
||||
|
||||
// Runtimes intentionally NOT in the table: claude is the default; antigravity is
|
||||
// resolved dynamically by the caller (resolveAntigravityGlobalDir + path.relative).
|
||||
const SPECIAL_CASED = new Set(['claude', 'antigravity']);
|
||||
// resolved dynamically by the caller (resolveAntigravityGlobalDir + path.relative);
|
||||
// vscode (#2103) is extension-distributed with no file-projected config home at
|
||||
// all — getGlobalConfigHomeFragment is never invoked for it (no install surface).
|
||||
const SPECIAL_CASED = new Set(['claude', 'antigravity', 'vscode']);
|
||||
|
||||
test('getGlobalConfigHomeFragment: golden map matches for all 13 table runtimes', () => {
|
||||
for (const [id, expected] of Object.entries(GOLDEN_FRAGMENT_MAP)) {
|
||||
|
||||
@@ -54,6 +54,36 @@ test('tools/call gsd_invoke_command: dispatches to the command hub (point 1); un
|
||||
assert.strictEqual(res.jsonrpc, '2.0');
|
||||
const payload = JSON.parse(res.result.content[0].text);
|
||||
assert.strictEqual(payload.ok, false, 'an unknown command dispatches to the hub and returns ok:false');
|
||||
assert.strictEqual(res.result.isError, true, 'unknown family surfaces as isError:true (#2102)');
|
||||
});
|
||||
|
||||
// REGRESSION #2102: gsd_invoke_command previously called
|
||||
// `commandRoutingHub.createHub()` with NO arguments, which always hits
|
||||
// `if (!_cjsRegistry) return makeUnknownCommand()` — every dispatch, valid
|
||||
// family or not, returned UnknownCommand. The test above (family:
|
||||
// 'no-such-family') could not catch this: an UnknownCommand result is
|
||||
// EXACTLY what an unknown family is supposed to return, whether or not
|
||||
// dispatch actually worked — it is vacuous by construction. This test proves
|
||||
// the fix: a VALID read-only family/subcommand must reach gsd-tools.cjs for
|
||||
// real and come back with actual data (not a crash, not UnknownCommand).
|
||||
test('tools/call gsd_invoke_command: REGRESSION #2102 — a valid read-only family dispatches for real (not the createHub()-with-no-args UnknownCommand bug)', () => {
|
||||
const dir = createTempDir();
|
||||
try {
|
||||
const res = handleMessage(
|
||||
{ jsonrpc: '2.0', id: 9, method: 'tools/call', params: { name: 'gsd_invoke_command', arguments: { family: 'progress', subcommand: 'json' } } },
|
||||
{ cwd: dir },
|
||||
);
|
||||
assert.strictEqual(res.jsonrpc, '2.0');
|
||||
assert.notStrictEqual(res.result.isError, true, 'a valid family must not surface as an error');
|
||||
const text = res.result.content[0].text;
|
||||
const parsed = JSON.parse(text);
|
||||
// Fail-first proof: under the bug, this would be
|
||||
// { ok: false, kind: 'UnknownCommand', command: 'progress json' } instead
|
||||
// of the real progress payload — `percent` would not exist.
|
||||
assert.strictEqual(typeof parsed.percent, 'number', 'the real "progress json" command ran (the engine was reached)');
|
||||
} finally {
|
||||
cleanup(dir);
|
||||
}
|
||||
});
|
||||
|
||||
test('tools/call: unknown tool name surfaces a tool error (isError), not a JSON-RPC protocol error', () => {
|
||||
|
||||
@@ -90,6 +90,20 @@ function makeFakeClock(startMs) {
|
||||
return new Date(_now).toISOString().split('T')[0];
|
||||
},
|
||||
|
||||
/**
|
||||
* Return the virtual date as a YYYY-MM-DD string in the HOST-LOCAL calendar
|
||||
* day. Mirrors realClock.localToday() so fake clocks are drop-in substitutes.
|
||||
* (#2136: operator-facing date-only fields use the local calendar day.)
|
||||
*
|
||||
* @returns {string} e.g. "2020-06-14" (local), may differ from today() near UTC midnight
|
||||
*/
|
||||
localToday() {
|
||||
const d = new Date(_now);
|
||||
const mm = String(d.getMonth() + 1).padStart(2, '0');
|
||||
const dd = String(d.getDate()).padStart(2, '0');
|
||||
return d.getFullYear() + '-' + mm + '-' + dd;
|
||||
},
|
||||
|
||||
/** Array of ms values passed to sleep() in call order. */
|
||||
get sleepCalls() {
|
||||
return _sleepCalls;
|
||||
|
||||
@@ -57,6 +57,7 @@ const RUNTIME_META = {
|
||||
kimi: { localDir: '.kimi-code', globalSuffix: path.join('.config', 'agents') },
|
||||
kilo: { localDir: '.kilo', globalSuffix: path.join('.config', 'kilo') },
|
||||
opencode: { localDir: '.opencode', globalSuffix: path.join('.config', 'opencode') },
|
||||
pi: { localDir: '.pi', globalSuffix: path.join('.pi', 'agent') },
|
||||
qwen: { localDir: '.qwen', globalSuffix: '.qwen' },
|
||||
trae: { localDir: '.trae', globalSuffix: '.trae' },
|
||||
windsurf: { localDir: '.windsurf', globalSuffix: path.join('.codeium', 'windsurf') },
|
||||
|
||||
@@ -38,9 +38,9 @@ const DISPATCH_KEYS = ['namedDispatch', 'nested', 'maxDepth', 'background', 'sub
|
||||
const RUNTIME_IDS = Object.keys(registry.runtimes);
|
||||
|
||||
// Contract-pinned profile split (derived from .host-cli-final.json):
|
||||
// programmatic-cli: claude, cline, cursor, hermes, kilo, kimi, opencode, qwen, trae (9)
|
||||
// programmatic-cli: claude, cline, cursor, hermes, kilo, kimi, opencode, pi, qwen, trae (10)
|
||||
// declarative-cli: antigravity, augment, codebuddy, codex, copilot, windsurf, zcode (7)
|
||||
// ide: 0
|
||||
// ide: vscode (1) — #2103, the first installed ide-profile host.
|
||||
const EXPECTED_PROFILES = {
|
||||
claude: 'programmatic-cli',
|
||||
cline: 'programmatic-cli',
|
||||
@@ -49,6 +49,7 @@ const EXPECTED_PROFILES = {
|
||||
kilo: 'programmatic-cli',
|
||||
kimi: 'programmatic-cli',
|
||||
opencode: 'programmatic-cli',
|
||||
pi: 'programmatic-cli',
|
||||
qwen: 'programmatic-cli',
|
||||
trae: 'programmatic-cli',
|
||||
antigravity: 'declarative-cli',
|
||||
@@ -58,6 +59,7 @@ const EXPECTED_PROFILES = {
|
||||
copilot: 'declarative-cli',
|
||||
windsurf: 'declarative-cli',
|
||||
zcode: 'declarative-cli',
|
||||
vscode: 'ide',
|
||||
};
|
||||
|
||||
describe('ADR-1239 Phase A: hostIntegration descriptors', () => {
|
||||
@@ -214,7 +216,9 @@ describe('ADR-1239 Phase A: hostIntegration descriptors', () => {
|
||||
test('contract-pin: profile split is internally consistent with EXPECTED_PROFILES (count-agnostic)', () => {
|
||||
// The counts are DERIVED from the curated EXPECTED_PROFILES map rather than
|
||||
// hand-pinned, so adding a runtime + its profile entry updates the counts
|
||||
// automatically. ide must remain 0 (no installed ide-profile host yet).
|
||||
// automatically. #2103: vscode is now the first installed ide-profile host,
|
||||
// so 'ide' is no longer pinned at a hardcoded 0 — it is derived below like
|
||||
// the other two profiles.
|
||||
const counts = { 'programmatic-cli': 0, 'declarative-cli': 0, 'ide': 0 };
|
||||
for (const id of RUNTIME_IDS) {
|
||||
const cap = registry.runtimes[id];
|
||||
@@ -235,7 +239,8 @@ describe('ADR-1239 Phase A: hostIntegration descriptors', () => {
|
||||
}
|
||||
assert.strictEqual(counts['programmatic-cli'], expectedCounts['programmatic-cli']);
|
||||
assert.strictEqual(counts['declarative-cli'], expectedCounts['declarative-cli']);
|
||||
assert.strictEqual(counts['ide'], 0, 'no installed host may carry the ide profile yet');
|
||||
assert.strictEqual(counts['ide'], expectedCounts['ide'],
|
||||
'ide-profile count must match EXPECTED_PROFILES (#2103: vscode is the first ide-profile host)');
|
||||
});
|
||||
|
||||
// ─── backgroundDispatch presence ─────────────────────────────────────────────
|
||||
@@ -283,10 +288,17 @@ describe('ADR-1239 Phase A: hostIntegration descriptors', () => {
|
||||
// #2087: OpenCode background subagents (v1.15 param, v1.17 default-on) →
|
||||
// dispatch.background/backgroundDispatch true → NOT force-flattened.
|
||||
opencode: false,
|
||||
// #2102: pi's dispatch.background/backgroundDispatch are both false
|
||||
// (undocumented background-subagent primitive) → force-flattened.
|
||||
pi: true,
|
||||
qwen: true,
|
||||
trae: true,
|
||||
windsurf: true,
|
||||
zcode: true,
|
||||
// #2103: vscode's dispatch.backgroundDispatch is 'undocumented' (no
|
||||
// documented background-subagent primitive) → fails closed to false →
|
||||
// force-flattened, mirroring the pi (#2102) precedent above.
|
||||
vscode: true,
|
||||
};
|
||||
|
||||
for (const id of RUNTIME_IDS) {
|
||||
|
||||
@@ -674,12 +674,14 @@ describe('#1755: .sh hooks are copied and executable after install', () => {
|
||||
// ─── #1821: Kilo/ZCode (hooksSurface:none, no plugin) receive no dead hooks ────
|
||||
//
|
||||
// #1821 reported dead hook scripts staged for runtimes with hooksSurface:'none'.
|
||||
// OpenCode ALSO declares hooksSurface:'none', but its #1914 native plugin adapter
|
||||
// (plugins/gsd-core.js) spawns the staged hooks/*.js via OpenCode's event bus —
|
||||
// so for OpenCode the hooks are LIVE and must keep being copied. Kilo and ZCode
|
||||
// have no plugin surface, so their staged hooks are genuinely dead: this is the
|
||||
// case the fix removes. These tests assert the split: Kilo/ZCode get no hooks;
|
||||
// OpenCode (and Claude) still do.
|
||||
// OpenCode and pi ALSO declare hooksSurface:'none', but each has a native plugin
|
||||
// adapter that spawns the staged hooks/*.js scripts as subprocesses (OpenCode's
|
||||
// #1914 plugins/gsd-core.js via OpenCode's event bus; pi's #2102 Stage 2
|
||||
// pi/gsd.cjs → extensions/gsd.cjs via pi.on(...) bridges) — so for both, the
|
||||
// hooks are LIVE and must keep being copied. Kilo and ZCode have no plugin
|
||||
// surface at all, so their staged hooks are genuinely dead: this is the case
|
||||
// the fix removes. These tests assert the split: Kilo/ZCode get no hooks;
|
||||
// OpenCode/pi (and Claude) still do.
|
||||
|
||||
describe('#1821: Kilo/ZCode receive no dead hook files; OpenCode/Claude keep their hooks', () => {
|
||||
function gsdHookFilesUnder(configDir) {
|
||||
@@ -691,7 +693,7 @@ describe('#1821: Kilo/ZCode receive no dead hook files; OpenCode/Claude keep the
|
||||
});
|
||||
}
|
||||
|
||||
function installAndCollect(runtime) {
|
||||
function installAndCollect(runtime, opts = {}) {
|
||||
const targetDir = fs.mkdtempSync(path.join(os.tmpdir(), `gsd-1821-${runtime}-`));
|
||||
try {
|
||||
const result = spawnSync(
|
||||
@@ -702,10 +704,12 @@ describe('#1821: Kilo/ZCode receive no dead hook files; OpenCode/Claude keep the
|
||||
assert.strictEqual(result.status, 0,
|
||||
`installer exited with status ${result.status} for --${runtime} --global\nstdout: ${result.stdout}\nstderr: ${result.stderr}`);
|
||||
// Collect results while targetDir still exists — cleanup() below removes it.
|
||||
const pluginRelPath = opts.pluginRelPath || path.join('plugins', 'gsd-core.js');
|
||||
return {
|
||||
hookFiles: gsdHookFilesUnder(targetDir),
|
||||
hooksLibExists: fs.existsSync(path.join(targetDir, 'hooks', 'lib')),
|
||||
pluginExists: fs.existsSync(path.join(targetDir, 'plugins', 'gsd-core.js')),
|
||||
gitCmdExists: fs.existsSync(path.join(targetDir, 'hooks', 'lib', 'git-cmd.js')),
|
||||
pluginExists: fs.existsSync(path.join(targetDir, pluginRelPath)),
|
||||
};
|
||||
} finally {
|
||||
cleanup(targetDir);
|
||||
@@ -713,7 +717,8 @@ describe('#1821: Kilo/ZCode receive no dead hook files; OpenCode/Claude keep the
|
||||
}
|
||||
|
||||
// Kilo and ZCode both declare hooksSurface:'none' with no plugin surface, so
|
||||
// their staged hooks are dead weight (#1821).
|
||||
// their staged hooks are genuinely dead weight (#1821) — this is the case
|
||||
// the fix removes.
|
||||
for (const runtime of ['kilo', 'zcode']) {
|
||||
test(`${runtime} --global install creates no gsd-*.js/.sh hook files or hooks/lib`, () => {
|
||||
const { hookFiles, hooksLibExists } = installAndCollect(runtime);
|
||||
@@ -735,6 +740,31 @@ describe('#1821: Kilo/ZCode receive no dead hook files; OpenCode/Claude keep the
|
||||
assert.ok(pluginExists, 'opencode install must install plugins/gsd-core.js (#1914 hook bridge)');
|
||||
});
|
||||
|
||||
// pi ALSO declares hooksSurface:'none', but — like OpenCode — it is NOT a
|
||||
// dead-weight case: pi's native extension (pi/gsd.cjs → extensions/gsd.cjs)
|
||||
// spawns the staged hooks/*.js scripts as bounded subprocesses (session_start
|
||||
// → gsd-ensure-canonical-path.js, before_agent_start → gsd-workflow-guard.js,
|
||||
// session_before_compact → gsd-context-monitor.js — #2102 Stage 2), and its
|
||||
// /gsd command handler tokenizes raw args via the shared hooks/lib/git-cmd.js
|
||||
// tokenizer. hostBehaviors.skipSharedHooksInstall is therefore NOT set for
|
||||
// pi (unlike Kilo/ZCode/Cursor/Cline/Trae/Copilot/Windsurf/Kimi) — pi is in
|
||||
// the OpenCode group, not the Kilo/ZCode group.
|
||||
test('pi --global install still copies hooks (spawned by the native extension) + hooks/lib/git-cmd.js + the extension itself', () => {
|
||||
const { hookFiles, hooksLibExists, gitCmdExists, pluginExists } = installAndCollect('pi', {
|
||||
pluginRelPath: path.join('extensions', 'gsd.cjs'),
|
||||
});
|
||||
const basenames = hookFiles.map((f) => path.basename(f));
|
||||
for (const expected of ['gsd-ensure-canonical-path.js', 'gsd-workflow-guard.js', 'gsd-context-monitor.js']) {
|
||||
assert.ok(
|
||||
basenames.includes(expected),
|
||||
`pi install must copy ${expected} (spawned by pi/gsd.cjs's event bridges), found: ${basenames.join(', ')}`,
|
||||
);
|
||||
}
|
||||
assert.ok(hooksLibExists, 'pi install must create hooks/lib/');
|
||||
assert.ok(gitCmdExists, 'pi install must copy hooks/lib/git-cmd.js (the /gsd command tokenizer)');
|
||||
assert.ok(pluginExists, 'pi install must install extensions/gsd.cjs (the native-extension hook bridge)');
|
||||
});
|
||||
|
||||
// Positive control: guards against over-exclusion breaking runtimes that
|
||||
// legitimately need hooks (hooksSurface !== 'none').
|
||||
test('claude --global install still copies gsd-*.js hooks', () => {
|
||||
|
||||
@@ -2084,6 +2084,30 @@ describe('Bug #3181: normalizeNodePath — Apple Silicon Homebrew Cellar paths
|
||||
});
|
||||
});
|
||||
|
||||
// #2185: Linuxbrew + any custom HOMEBREW_PREFIX — the Cellar prefix is derived
|
||||
// from the path itself, so one branch covers every Homebrew layout.
|
||||
describe('Bug #2185: normalizeNodePath — Linuxbrew + custom-prefix Cellar paths → <prefix>/bin/node', () => {
|
||||
test('Linuxbrew Cellar path maps to the stable linuxbrew symlink', () => {
|
||||
const result = normalizeNodePath('/home/linuxbrew/.linuxbrew/Cellar/node/26.0.0/bin/node');
|
||||
assert.equal(result, '/home/linuxbrew/.linuxbrew/bin/node');
|
||||
});
|
||||
|
||||
test('Linuxbrew Cellar path after a version bump (26.5.0) maps to stable symlink', () => {
|
||||
const result = normalizeNodePath('/home/linuxbrew/.linuxbrew/Cellar/node/26.5.0/bin/node');
|
||||
assert.equal(result, '/home/linuxbrew/.linuxbrew/bin/node');
|
||||
});
|
||||
|
||||
test('Linuxbrew versioned formula Cellar path (node@22) maps to stable symlink', () => {
|
||||
const result = normalizeNodePath('/home/linuxbrew/.linuxbrew/Cellar/node@22/22.11.0/bin/node');
|
||||
assert.equal(result, '/home/linuxbrew/.linuxbrew/bin/node');
|
||||
});
|
||||
|
||||
test('custom HOMEBREW_PREFIX Cellar path maps to its stable symlink', () => {
|
||||
const result = normalizeNodePath('/custom/brew/Cellar/node/25.8.1/bin/node');
|
||||
assert.equal(result, '/custom/brew/bin/node');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Bug #3181: normalizeNodePath — non-Homebrew paths are returned unchanged', () => {
|
||||
test('NVM path is unchanged', () => {
|
||||
const nvm = '/Users/dev/.nvm/versions/node/v20.11.0/bin/node';
|
||||
@@ -6075,6 +6099,30 @@ describe('Bug #3181: normalizeNodePath — Apple Silicon Homebrew Cellar paths
|
||||
});
|
||||
});
|
||||
|
||||
// #2185: Linuxbrew + any custom HOMEBREW_PREFIX — the Cellar prefix is derived
|
||||
// from the path itself, so one branch covers every Homebrew layout.
|
||||
describe('Bug #2185: normalizeNodePath — Linuxbrew + custom-prefix Cellar paths → <prefix>/bin/node', () => {
|
||||
test('Linuxbrew Cellar path maps to the stable linuxbrew symlink', () => {
|
||||
const result = normalizeNodePath('/home/linuxbrew/.linuxbrew/Cellar/node/26.0.0/bin/node');
|
||||
assert.equal(result, '/home/linuxbrew/.linuxbrew/bin/node');
|
||||
});
|
||||
|
||||
test('Linuxbrew Cellar path after a version bump (26.5.0) maps to stable symlink', () => {
|
||||
const result = normalizeNodePath('/home/linuxbrew/.linuxbrew/Cellar/node/26.5.0/bin/node');
|
||||
assert.equal(result, '/home/linuxbrew/.linuxbrew/bin/node');
|
||||
});
|
||||
|
||||
test('Linuxbrew versioned formula Cellar path (node@22) maps to stable symlink', () => {
|
||||
const result = normalizeNodePath('/home/linuxbrew/.linuxbrew/Cellar/node@22/22.11.0/bin/node');
|
||||
assert.equal(result, '/home/linuxbrew/.linuxbrew/bin/node');
|
||||
});
|
||||
|
||||
test('custom HOMEBREW_PREFIX Cellar path maps to its stable symlink', () => {
|
||||
const result = normalizeNodePath('/custom/brew/Cellar/node/25.8.1/bin/node');
|
||||
assert.equal(result, '/custom/brew/bin/node');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Bug #3181: normalizeNodePath — non-Homebrew paths are returned unchanged', () => {
|
||||
test('NVM path is unchanged', () => {
|
||||
const nvm = '/Users/dev/.nvm/versions/node/v20.11.0/bin/node';
|
||||
|
||||
@@ -39,6 +39,20 @@ const RUNTIME_INSTALL_CONTRACTS = {
|
||||
// dead hook scripts or the CommonJS package.json marker.
|
||||
kilo: { surface: 'flat-command', settings: false, packageJson: false },
|
||||
opencode: { surface: 'flat-command', settings: true, packageJson: true },
|
||||
// #2102 Stage 1/2: pi is a PLUGIN-ONLY install (hostBehaviors.pluginOnlyInstall)
|
||||
// for commands/agents/skills — NO commands/, agents/, or skills/ dir. pi's
|
||||
// /gsd command is registered programmatically by the native extension
|
||||
// (extensions/gsd.cjs) and dispatches via a bounded subprocess to
|
||||
// gsd-tools.cjs; it has no host-read markdown surface. Stage 2 (adversarial-
|
||||
// review fix): pi's native extension DOES spawn the shared hooks/*.js bundle
|
||||
// as bounded subprocesses (session_start/before_agent_start/session_before_
|
||||
// compact bridges) and its /gsd tokenizer requires hooks/lib/git-cmd.js, so
|
||||
// `hostBehaviors.skipSharedHooksInstall` was removed — pi now receives
|
||||
// hooks/ + hooks/lib/ + the {"type":"commonjs"} package.json marker, exactly
|
||||
// like OpenCode (architecturally identical: hooksSurface:'none' + a native
|
||||
// plugin that spawns the staged hooks), NOT like Kilo/ZCode (no plugin
|
||||
// surface, where the same hooks are genuinely dead weight).
|
||||
pi: { surface: 'plugin-only', settings: false, packageJson: true },
|
||||
qwen: { surface: 'flat-skills', settings: true, packageJson: true },
|
||||
trae: { surface: 'flat-skills', settings: false, packageJson: false },
|
||||
windsurf: { surface: 'global-artifacts-noop', settings: false, packageJson: false },
|
||||
@@ -262,6 +276,43 @@ function assertFreshInstallContract(runtime, targetDir) {
|
||||
listDirNames(targetDir, 'command').some((name) => name.startsWith('gsd-') && name.endsWith('.md')),
|
||||
`${runtime} should install flattened command markdown files`
|
||||
);
|
||||
} else if (contract.surface === 'plugin-only') {
|
||||
// #2102 Stage 1/2: pi — PLUGIN-ONLY install for commands/agents/skills
|
||||
// (hostBehaviors.pluginOnlyInstall). pi's /gsd command is registered
|
||||
// programmatically by the native extension and dispatches via a bounded
|
||||
// subprocess to gsd-tools.cjs — pi has no host-read markdown surface, so
|
||||
// NO commands/, agents/, or skills/ dir is written. The extension DOES
|
||||
// spawn the shared hooks/*.js bundle as bounded subprocesses (Stage 2
|
||||
// adversarial-review fix — hooksSurface:'none' no longer implies
|
||||
// skipSharedHooksInstall for pi, mirroring OpenCode), so hooks/ + the
|
||||
// git-cmd.js tokenizer helper ARE part of the artifact surface now.
|
||||
assert.ok(
|
||||
fs.existsSync(path.join(targetDir, 'extensions', 'gsd.cjs')),
|
||||
`${runtime} should install the native extension file at extensions/gsd.cjs`
|
||||
);
|
||||
assert.ok(
|
||||
fs.existsSync(path.join(targetDir, 'hooks', 'gsd-ensure-canonical-path.js')),
|
||||
`${runtime} should install the shared hooks/ bundle (spawned by the native extension's event bridges)`
|
||||
);
|
||||
assert.ok(
|
||||
fs.existsSync(path.join(targetDir, 'hooks', 'lib', 'git-cmd.js')),
|
||||
`${runtime} should install hooks/lib/git-cmd.js (the /gsd command tokenizer)`
|
||||
);
|
||||
assert.equal(
|
||||
fs.existsSync(path.join(targetDir, 'commands')),
|
||||
false,
|
||||
`${runtime} should NOT install a commands/ dir (plugin-only, no host-read markdown surface)`
|
||||
);
|
||||
assert.equal(
|
||||
fs.existsSync(path.join(targetDir, 'agents')),
|
||||
false,
|
||||
`${runtime} should NOT install an agents/ dir (plugin-only, no named-dispatch toolkit)`
|
||||
);
|
||||
assert.equal(
|
||||
fs.existsSync(path.join(targetDir, 'skills')),
|
||||
false,
|
||||
`${runtime} should NOT install a skills/ dir (plugin-only)`
|
||||
);
|
||||
} else if (contract.surface === 'commands-gsd') {
|
||||
assert.ok(
|
||||
listDirNames(targetDir, path.join('commands', 'gsd')).length > 0,
|
||||
@@ -301,7 +352,7 @@ function assertFreshInstallContract(runtime, targetDir) {
|
||||
);
|
||||
}
|
||||
|
||||
if (contract.surface !== 'kimi-skills-agents' && contract.surface !== 'global-artifacts-noop') {
|
||||
if (contract.surface !== 'kimi-skills-agents' && contract.surface !== 'global-artifacts-noop' && contract.surface !== 'plugin-only') {
|
||||
assert.ok(
|
||||
listDirNames(targetDir, 'agents').some((name) => name.startsWith('gsd-')),
|
||||
`${runtime} full install should install agents`
|
||||
|
||||
@@ -184,6 +184,36 @@ describe('issue-57 AC2 — config-mutation dispatch is closed over the explicit
|
||||
);
|
||||
});
|
||||
|
||||
// allow-test-rule: structural guard over bin/install.js source (#2103). VS Code
|
||||
// (capabilities/vscode/capability.json) is a registry runtime (role:runtime, for
|
||||
// validator/host-integration coverage) but is NEVER CLI-installed — it is a
|
||||
// Marketplace/VSIX extension with no --vscode flag and no allRuntimes membership
|
||||
// (see NON_INSTALLABLE_RUNTIMES in tests/runtime-flags.test.cjs). It must stay
|
||||
// fully descriptor-driven: bin/install.js must never special-case it by name.
|
||||
// This is a stricter, clearer-failure-message sibling of the generic
|
||||
// "every inline runtime === ..." guard above (which would also catch this, but
|
||||
// with a misleading "register it in the adapter registry" suggestion — vscode
|
||||
// must never be registered there at all, see the ALLOWED_CONFIG_RUNTIMES filter
|
||||
// in src/runtime-config-adapter-registry.cts).
|
||||
test('#2103: bin/install.js has ZERO runtime === "vscode" / isVscode branches (vscode stays fully descriptor-driven)', () => {
|
||||
const src = fs.readFileSync(path.join(ROOT, 'bin', 'install.js'), 'utf8');
|
||||
const runtimeComparisons = [...src.matchAll(/runtime === (?:'vscode'|"vscode")/g)];
|
||||
assert.deepStrictEqual(
|
||||
runtimeComparisons.map((m) => m[0]),
|
||||
[],
|
||||
'bin/install.js must not special-case vscode via `runtime === "vscode"` — vscode has no '
|
||||
+ 'install surface at all (installSurface: "none") and is never CLI-installed; any '
|
||||
+ 'vscode-specific behavior belongs in capabilities/vscode/capability.json, not an inline branch.',
|
||||
);
|
||||
const isVscodeRefs = [...src.matchAll(/\bisVscode\b/g)];
|
||||
assert.deepStrictEqual(
|
||||
isVscodeRefs.map((m) => m[0]),
|
||||
[],
|
||||
'bin/install.js must not introduce an isVscode flag — vscode is intentionally excluded '
|
||||
+ 'from runtimeFlags (Marketplace-distributed, never CLI-installed).',
|
||||
);
|
||||
});
|
||||
|
||||
// allow-test-rule: delegation-presence guard. Catches wholesale removal of the registry
|
||||
// dispatch (a regression to scattered per-runtime config branching). Presence-style, not
|
||||
// absence-grep, so it does not bite on incidental non-config `runtime === '...'` checks.
|
||||
|
||||
@@ -488,6 +488,71 @@ describe('requirements mark-complete command', () => {
|
||||
| INFRA-01 | Phase 6 | Pending |
|
||||
`;
|
||||
|
||||
// #2140: a traceability table EXISTS but has no row for the ID being completed.
|
||||
// Only the checkbox can reconcile; the table surface is unsynced. The CLI must
|
||||
// not report this as a payload indistinguishable from a full reconcile.
|
||||
const TABLE_WITHOUT_FOO = `# Requirements
|
||||
|
||||
## Coverage
|
||||
- [ ] **FOO-01**: feature one
|
||||
- [ ] **BAR-01**: feature two
|
||||
|
||||
## Traceability
|
||||
|
||||
| Requirement | Phase | Status |
|
||||
|-------------|-------|--------|
|
||||
| BAR-01 | Phase 1 | Pending |
|
||||
`;
|
||||
|
||||
test('#2140 checkbox-only reconcile surfaces table_unmatched (not silent success)', () => {
|
||||
writeRequirements(tmpDir, TABLE_WITHOUT_FOO);
|
||||
|
||||
const result = runGsdTools('requirements mark-complete FOO-01', tmpDir);
|
||||
assert.ok(result.success);
|
||||
|
||||
const out = JSON.parse(result.output);
|
||||
// The checkbox WAS written, so it is in marked_complete...
|
||||
assert.ok(out.marked_complete.includes('FOO-01'), 'checkbox reconcile is reported');
|
||||
// ...but the traceability table had no FOO-01 row, which must be surfaced.
|
||||
assert.ok(Array.isArray(out.table_unmatched), 'table_unmatched bucket must exist');
|
||||
assert.ok(out.table_unmatched.includes('FOO-01'),
|
||||
'an ID with a checkbox but no table row must be surfaced as table_unmatched');
|
||||
|
||||
const content = readRequirements(tmpDir);
|
||||
assert.ok(content.includes('- [x] **FOO-01**'), 'checkbox should be checked');
|
||||
// The table is untouched (no FOO-01 row synthesized).
|
||||
assert.ok(!content.includes('FOO-01 | Phase'), 'no FOO-01 row should be invented');
|
||||
});
|
||||
|
||||
test('#2140 re-run on the half-written file does NOT mask the drift as already_complete', () => {
|
||||
writeRequirements(tmpDir, TABLE_WITHOUT_FOO);
|
||||
// First run: flips the checkbox, surfaces table_unmatched.
|
||||
runGsdTools('requirements mark-complete FOO-01', tmpDir);
|
||||
// Second run on the now-[x]-checkbox-with-no-row file.
|
||||
const result = runGsdTools('requirements mark-complete FOO-01', tmpDir);
|
||||
assert.ok(result.success);
|
||||
const out = JSON.parse(result.output);
|
||||
|
||||
assert.ok(!out.already_complete.includes('FOO-01'),
|
||||
'a [x] checkbox with no table row is PARTIALLY reconciled, not already_complete');
|
||||
assert.ok(!out.marked_complete.includes('FOO-01'),
|
||||
'nothing flipped on re-run, so not marked_complete');
|
||||
assert.ok(out.table_unmatched.includes('FOO-01'),
|
||||
'the drift must still be surfaced as table_unmatched on re-run');
|
||||
});
|
||||
|
||||
test('#2140 no traceability table at all → still a clean success (no table_unmatched)', () => {
|
||||
// A REQUIREMENTS.md with no traceability table is legitimate; a checkbox-only
|
||||
// reconcile must remain an unqualified success with no table_unmatched entry.
|
||||
writeRequirements(tmpDir, '# Requirements\n\n- [ ] **NO-TABLE-01**: thing\n');
|
||||
const result = runGsdTools('requirements mark-complete NO-TABLE-01', tmpDir);
|
||||
assert.ok(result.success);
|
||||
const out = JSON.parse(result.output);
|
||||
assert.ok(out.marked_complete.includes('NO-TABLE-01'));
|
||||
assert.ok(!out.table_unmatched || !out.table_unmatched.includes('NO-TABLE-01'),
|
||||
'no table_unmatched when there is no traceability table');
|
||||
});
|
||||
|
||||
test('marks single requirement complete (checkbox + table)', () => {
|
||||
writeRequirements(tmpDir, STANDARD_REQUIREMENTS);
|
||||
|
||||
|
||||
@@ -15,6 +15,10 @@
|
||||
* now hermes, and the "All" shortcut moved from 17 to 16.
|
||||
*
|
||||
* #1925: ZCode (Z.ai) added as option 16; the "All" shortcut moved from 16 to 17.
|
||||
*
|
||||
* #2102: pi added as option 13 (alphabetical slot between opencode and qwen) —
|
||||
* qwen/trae/windsurf/zcode each shift up one slot (14/15/16/17), and the "All"
|
||||
* shortcut moves from 17 to 18.
|
||||
*/
|
||||
|
||||
process.env.GSD_TEST_MODE = '1';
|
||||
@@ -76,39 +80,43 @@ describe('multi-runtime selection parsing', () => {
|
||||
assert.deepStrictEqual(parseRuntimeInput('12'), ['opencode']);
|
||||
});
|
||||
|
||||
test('single choice for pi', () => {
|
||||
assert.deepStrictEqual(parseRuntimeInput('13'), ['pi']);
|
||||
});
|
||||
|
||||
test('single choice for qwen', () => {
|
||||
assert.deepStrictEqual(parseRuntimeInput('13'), ['qwen']);
|
||||
assert.deepStrictEqual(parseRuntimeInput('14'), ['qwen']);
|
||||
});
|
||||
|
||||
test('single choice for trae', () => {
|
||||
assert.deepStrictEqual(parseRuntimeInput('14'), ['trae']);
|
||||
assert.deepStrictEqual(parseRuntimeInput('15'), ['trae']);
|
||||
});
|
||||
|
||||
test('single choice for windsurf', () => {
|
||||
assert.deepStrictEqual(parseRuntimeInput('15'), ['windsurf']);
|
||||
assert.deepStrictEqual(parseRuntimeInput('16'), ['windsurf']);
|
||||
});
|
||||
|
||||
test('single choice for zcode', () => {
|
||||
assert.deepStrictEqual(parseRuntimeInput('16'), ['zcode']);
|
||||
assert.deepStrictEqual(parseRuntimeInput('17'), ['zcode']);
|
||||
});
|
||||
|
||||
test('single choice for kimi', () => {
|
||||
assert.deepStrictEqual(parseRuntimeInput('10'), ['kimi']);
|
||||
});
|
||||
|
||||
test('choice 17 returns all runtimes', () => {
|
||||
assert.deepStrictEqual(parseRuntimeInput('17'), allRuntimes);
|
||||
test('choice 18 returns all runtimes', () => {
|
||||
assert.deepStrictEqual(parseRuntimeInput('18'), allRuntimes);
|
||||
});
|
||||
|
||||
test('choice 17 returns all runtimes when mixed with separators or other tokens', () => {
|
||||
// CR feedback: tokenized inputs that include 17 (e.g. trailing comma, or
|
||||
test('choice 18 returns all runtimes when mixed with separators or other tokens', () => {
|
||||
// CR feedback: tokenized inputs that include 18 (e.g. trailing comma, or
|
||||
// alongside other choices) must still expand to all-runtimes — previously
|
||||
// only the bare all-runtimes option matched, so "17," or "17 1" silently installed a
|
||||
// only the bare all-runtimes option matched, so "18," or "18 1" silently installed a
|
||||
// subset.
|
||||
assert.deepStrictEqual(parseRuntimeInput('17,'), allRuntimes);
|
||||
assert.deepStrictEqual(parseRuntimeInput('17 1'), allRuntimes);
|
||||
assert.deepStrictEqual(parseRuntimeInput('1,17'), allRuntimes);
|
||||
assert.deepStrictEqual(parseRuntimeInput(' 17 '), allRuntimes);
|
||||
assert.deepStrictEqual(parseRuntimeInput('18,'), allRuntimes);
|
||||
assert.deepStrictEqual(parseRuntimeInput('18 1'), allRuntimes);
|
||||
assert.deepStrictEqual(parseRuntimeInput('1,18'), allRuntimes);
|
||||
assert.deepStrictEqual(parseRuntimeInput(' 18 '), allRuntimes);
|
||||
});
|
||||
|
||||
test('empty input defaults to claude', () => {
|
||||
@@ -117,13 +125,13 @@ describe('multi-runtime selection parsing', () => {
|
||||
});
|
||||
|
||||
test('invalid choices are ignored, falls back to claude if all invalid', () => {
|
||||
assert.deepStrictEqual(parseRuntimeInput('18'), ['claude']);
|
||||
assert.deepStrictEqual(parseRuntimeInput('19'), ['claude']);
|
||||
assert.deepStrictEqual(parseRuntimeInput('0'), ['claude']);
|
||||
assert.deepStrictEqual(parseRuntimeInput('abc'), ['claude']);
|
||||
});
|
||||
|
||||
test('invalid choices mixed with valid are filtered out', () => {
|
||||
assert.deepStrictEqual(parseRuntimeInput('1,18,7'), ['claude', 'copilot']);
|
||||
assert.deepStrictEqual(parseRuntimeInput('1,19,7'), ['claude', 'copilot']);
|
||||
assert.deepStrictEqual(parseRuntimeInput('abc 3 xyz'), ['augment']);
|
||||
});
|
||||
|
||||
@@ -152,14 +160,15 @@ describe('install.js exports multi-select runtime metadata', () => {
|
||||
'10': 'kimi',
|
||||
'11': 'kilo',
|
||||
'12': 'opencode',
|
||||
'13': 'qwen',
|
||||
'14': 'trae',
|
||||
'15': 'windsurf',
|
||||
'16': 'zcode',
|
||||
'13': 'pi',
|
||||
'14': 'qwen',
|
||||
'15': 'trae',
|
||||
'16': 'windsurf',
|
||||
'17': 'zcode',
|
||||
};
|
||||
const expectedRuntimes = [
|
||||
'claude', 'antigravity', 'augment', 'cline', 'codebuddy', 'codex',
|
||||
'copilot', 'cursor', 'hermes', 'kimi', 'kilo', 'opencode',
|
||||
'copilot', 'cursor', 'hermes', 'kimi', 'kilo', 'opencode', 'pi',
|
||||
'qwen', 'trae', 'windsurf', 'zcode',
|
||||
];
|
||||
|
||||
@@ -177,8 +186,8 @@ describe('install.js exports multi-select runtime metadata', () => {
|
||||
'allRuntimes has no duplicates');
|
||||
});
|
||||
|
||||
test('"All" shortcut (option 17) selects every runtime', () => {
|
||||
assert.deepStrictEqual(parseRuntimeInput('17'), allRuntimes);
|
||||
test('"All" shortcut (option 18) selects every runtime', () => {
|
||||
assert.deepStrictEqual(parseRuntimeInput('18'), allRuntimes);
|
||||
});
|
||||
|
||||
test('--kimi flag selects Kimi without interactive prompt', () => {
|
||||
@@ -189,6 +198,10 @@ describe('install.js exports multi-select runtime metadata', () => {
|
||||
assert.deepStrictEqual(selectRuntimesFromArgs(['--zcode']), ['zcode']);
|
||||
});
|
||||
|
||||
test('--pi flag selects pi without interactive prompt', () => {
|
||||
assert.deepStrictEqual(selectRuntimesFromArgs(['--pi']), ['pi']);
|
||||
});
|
||||
|
||||
test('--all flag includes Kimi exactly once', () => {
|
||||
const selected = selectRuntimesFromArgs(['--all']);
|
||||
assert.ok(selected.includes('kimi'), '--all includes kimi');
|
||||
@@ -203,7 +216,14 @@ describe('install.js exports multi-select runtime metadata', () => {
|
||||
'--all includes zcode exactly once');
|
||||
});
|
||||
|
||||
test('prompt lists ZCode (16), and All (17)', () => {
|
||||
test('--all flag includes pi exactly once', () => {
|
||||
const selected = selectRuntimesFromArgs(['--all']);
|
||||
assert.ok(selected.includes('pi'), '--all includes pi');
|
||||
assert.strictEqual(selected.filter((runtime) => runtime === 'pi').length, 1,
|
||||
'--all includes pi exactly once');
|
||||
});
|
||||
|
||||
test('prompt lists pi (13), ZCode (17), and All (18)', () => {
|
||||
const prompt = stripAnsi(buildRuntimePromptText());
|
||||
assert.ok(/\b9\)\s*Hermes Agent\b/.test(prompt),
|
||||
'prompt lists Hermes Agent as option 9');
|
||||
@@ -211,14 +231,16 @@ describe('install.js exports multi-select runtime metadata', () => {
|
||||
'prompt lists Kimi as option 10');
|
||||
assert.ok(/Kimi\s+\(~\/\.config\/agents, then ~\/\.agents if existing\)/.test(prompt),
|
||||
'prompt shows the Kimi first-existing generic root policy');
|
||||
assert.ok(/\b13\)\s*Qwen Code\b/.test(prompt),
|
||||
'prompt lists Qwen Code as option 13');
|
||||
assert.ok(/\b14\)\s*Trae\b/.test(prompt),
|
||||
'prompt lists Trae as option 14');
|
||||
assert.ok(/\b16\)\s*ZCode\b/.test(prompt),
|
||||
'prompt lists ZCode as option 16');
|
||||
assert.ok(/\b17\)\s*All\b/.test(prompt),
|
||||
'prompt lists All as option 17');
|
||||
assert.ok(/\b13\)\s*pi\b/.test(prompt),
|
||||
'prompt lists pi as option 13');
|
||||
assert.ok(/\b14\)\s*Qwen Code\b/.test(prompt),
|
||||
'prompt lists Qwen Code as option 14');
|
||||
assert.ok(/\b15\)\s*Trae\b/.test(prompt),
|
||||
'prompt lists Trae as option 15');
|
||||
assert.ok(/\b17\)\s*ZCode\b/.test(prompt),
|
||||
'prompt lists ZCode as option 17');
|
||||
assert.ok(/\b18\)\s*All\b/.test(prompt),
|
||||
'prompt lists All as option 18');
|
||||
});
|
||||
|
||||
test('prompt does not list Gemini (removed #1928)', () => {
|
||||
|
||||
@@ -23,7 +23,7 @@ const registry = require('../gsd-core/bin/lib/capability-registry.cjs');
|
||||
const runtimeNamePolicy = require('../gsd-core/bin/lib/runtime-name-policy.cjs');
|
||||
|
||||
const { NON_CLAUDE_RUNTIMES } = conversion;
|
||||
const { getDirName } = runtimeNamePolicy;
|
||||
const { getDirName, NO_LOCAL_CONFIG_DIR_SENTINEL } = runtimeNamePolicy;
|
||||
|
||||
// Golden oracle: hardcoded sorted known-good list of all non-Claude runtimes.
|
||||
// A pinned expected value in a TEST is correct — the test IS the oracle.
|
||||
@@ -78,3 +78,17 @@ test('DRIFT GUARD: every registry-declared non-Claude runtime has an explicit ge
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// #2103: vscode is a registry runtime (role:runtime) whose configHome.kind is
|
||||
// 'none' — it has NO file-projected config directory at all (Marketplace/VSIX
|
||||
// extension). It is covered by the generic loop above (its dir must not be
|
||||
// '.claude'), but that assertion alone would ALSO pass for a plain string
|
||||
// typo, so this pins the actual documented sentinel value honestly rather
|
||||
// than riding on the generic "not .claude" check.
|
||||
test('#2103: getDirName("vscode") returns the documented no-local-config-dir sentinel, not .claude and not a real dot-dir', () => {
|
||||
assert.ok(NON_CLAUDE_RUNTIMES.includes('vscode'), 'vscode must be a registered non-Claude runtime');
|
||||
const dir = getDirName('vscode');
|
||||
assert.equal(dir, NO_LOCAL_CONFIG_DIR_SENTINEL);
|
||||
assert.notEqual(dir, '.claude');
|
||||
assert.ok(!dir.startsWith('.'), 'the sentinel must not look like a plausible dot-dir name');
|
||||
});
|
||||
|
||||
@@ -4,14 +4,30 @@
|
||||
// Those programs ARE the runtime contract; this test extracts them verbatim from
|
||||
// the workflow and exercises the real jq (not a reimplementation) so the shipped
|
||||
// reconstruction logic is what gets property-tested.
|
||||
//
|
||||
// ARCHITECTURE (#2099): the shipped jq program is run over a WHOLE fast-check corpus
|
||||
// in ONE jq process, not once per generated case. Each generated event stream is
|
||||
// written as one compact-JSON array per line to a temp file, then `jq -c <PROGRAM>`
|
||||
// (no `-s`) applies PROGRAM to each array — `.` is that array, exactly what
|
||||
// production's `jq -rs` sees after slurping opencode's one-value-per-line stream —
|
||||
// emitting one compact-JSON result per line. This is empirically identical to the
|
||||
// per-stream `-rs` form (verified across embedded-newline/empty/quote/unicode/
|
||||
// null-drop cases) AND reads from a file like production (`jq -rs '…' <file>`), so
|
||||
// there is no stdin pipe to deadlock on large I/O. The prior design spawned ~600
|
||||
// synchronous jq subprocesses (numRuns × 3 properties); a single one freezing on a
|
||||
// contended CI runner hung the whole unit-test chunk to its 600s kill (macOS CI,
|
||||
// #2099). `node --test`'s --test-force-exit cannot interrupt a synchronous
|
||||
// execFileSync, so the cure is to stop spawning per case — not just to time-bound it.
|
||||
'use strict';
|
||||
|
||||
const { describe, test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const { execFileSync } = require('node:child_process');
|
||||
const fs = require('node:fs');
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
const fc = require('./helpers/fast-check-setup.cjs');
|
||||
const { cleanup } = require('./helpers.cjs');
|
||||
|
||||
const reviewPath = path.resolve(__dirname, '..', 'gsd-core', 'workflows', 'review.md');
|
||||
const workflow = fs.readFileSync(reviewPath, 'utf-8');
|
||||
@@ -34,20 +50,48 @@ const DIAG_PROGRAM = extractJqProgram('OPENCODE_DIAG'); // empty-output diagn
|
||||
// the suite to jq-present non-Windows hosts, mirroring golden-install-parity's win32
|
||||
// skip. The assertions run in full on every macOS/Linux CI leg.
|
||||
let jqAvailable = false;
|
||||
try { execFileSync('jq', ['--version'], { stdio: 'ignore' }); jqAvailable = true; } catch { /* no jq on PATH */ }
|
||||
try { execFileSync('jq', ['--version'], { stdio: 'ignore', timeout: 10000, killSignal: 'SIGKILL' }); jqAvailable = true; } catch { /* no jq on PATH */ }
|
||||
const skipReason = process.platform === 'win32'
|
||||
? 'jq invocation is not portable under Node child_process arg-quoting on Windows; logic is platform-independent and asserted on macOS/Linux'
|
||||
: (jqAvailable ? false : 'jq not on PATH');
|
||||
const opts = { skip: skipReason };
|
||||
|
||||
// Run a shipped jq program against a stream of events serialized exactly as
|
||||
// opencode emits them: one JSON value per line (jq -s slurps them into an array).
|
||||
// jq -r appends a single trailing newline to the (single) string result; strip it
|
||||
// to recover the value the workflow's `$(…)` capture would see.
|
||||
function runJq(program, events) {
|
||||
const jsonl = events.map((e) => JSON.stringify(e)).join('\n');
|
||||
const out = execFileSync('jq', ['-rs', program], { input: jsonl, encoding: 'utf8' });
|
||||
return out.endsWith('\n') ? out.slice(0, -1) : out;
|
||||
// Bound each jq subprocess so a frozen spawn on a contended runner fails fast +
|
||||
// diagnosably (ETIMEDOUT) rather than hanging the chunk. jq over this corpus
|
||||
// completes in ~10ms, so 30s is an enormous margin that never trips on a healthy run.
|
||||
const JQ_EXEC_OPTS = { encoding: 'utf8', timeout: 30000, killSignal: 'SIGKILL', maxBuffer: 64 * 1024 * 1024 };
|
||||
|
||||
// Run a shipped jq program over an array of event streams in a SINGLE jq process.
|
||||
// Returns one result per input stream (order preserved), decoded from jq's compact
|
||||
// (`-c`) JSON output back to the raw string production's `-r` would have captured.
|
||||
// Both shipped programs yield exactly one value per stream (join(...) / last|"...");
|
||||
// the length assertion pins that invariant so a future program change that broke it
|
||||
// (0 or >1 outputs) fails loudly instead of silently misaligning results.
|
||||
function runJqBatch(program, streams) {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-jq-batch-'));
|
||||
try {
|
||||
const file = path.join(dir, 'streams.jsonl');
|
||||
fs.writeFileSync(file, streams.map((events) => JSON.stringify(events)).join('\n') + '\n');
|
||||
const out = execFileSync('jq', ['-c', program, file], JQ_EXEC_OPTS);
|
||||
const lines = out.split('\n').filter((line) => line.length > 0);
|
||||
assert.equal(
|
||||
lines.length,
|
||||
streams.length,
|
||||
`jq must emit exactly one result per stream (got ${lines.length} for ${streams.length})`,
|
||||
);
|
||||
return lines.map((line) => JSON.parse(line));
|
||||
} finally {
|
||||
cleanup(dir);
|
||||
}
|
||||
}
|
||||
|
||||
// The intended reconstruction, computed independently of jq. jq is the unit under
|
||||
// test; this JS is the spec it must match on every generated case.
|
||||
function expectedReview(events) {
|
||||
return events
|
||||
.filter((e) => e.type === 'text' && e.part && typeof e.part.text === 'string')
|
||||
.map((e) => e.part.text)
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
// Text values safe to round-trip through JSON → jq (utf8) → string. Excludes lone
|
||||
@@ -85,69 +129,82 @@ const eventStream = fc.array(fc.oneof(textEvent, textEvent, nonTextEvent), {
|
||||
maxLength: 30,
|
||||
});
|
||||
|
||||
// Corpus size per property. Matches the prior fast-check-setup numRuns:200 so
|
||||
// coverage is unchanged; distinct seeds give each property an independent corpus,
|
||||
// and fixed seeds keep the corpus deterministic across CI runs/OS legs.
|
||||
const CORPUS = 200;
|
||||
|
||||
describe('#1936 OpenCode review reconstruction — jq properties', () => {
|
||||
test('review == the newline-join of every assistant text part (order preserved)', opts, () => {
|
||||
fc.assert(
|
||||
fc.property(eventStream, (events) => {
|
||||
const expected = events
|
||||
.filter((e) => e.type === 'text' && e.part && typeof e.part.text === 'string')
|
||||
.map((e) => e.part.text)
|
||||
.join('\n');
|
||||
assert.equal(runJq(TEXT_PROGRAM, events), expected);
|
||||
}),
|
||||
);
|
||||
test('review == the newline-join of every assistant text part, over a generated corpus (order preserved)', opts, () => {
|
||||
const streams = fc.sample(eventStream, { numRuns: CORPUS, seed: 42 });
|
||||
const actual = runJqBatch(TEXT_PROGRAM, streams); // one jq process for the whole corpus
|
||||
streams.forEach((events, i) => {
|
||||
assert.equal(
|
||||
actual[i],
|
||||
expectedReview(events),
|
||||
`case ${i}: shipped jq review must equal the spec for events=${JSON.stringify(events)}`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('a stream with no assistant text part reconstructs to empty (drives the #1936 stub)', opts, () => {
|
||||
fc.assert(
|
||||
fc.property(fc.array(nonTextEvent, { minLength: 1, maxLength: 20 }), (events) => {
|
||||
// This is the exact failure the bug describes: the agent runs tool calls
|
||||
// and ends with step_finish, emitting no text. Reconstruction must be empty
|
||||
// so the content-gate (`[ -n "$OPENCODE_REVIEW" ]`) falls through to the stub.
|
||||
assert.equal(runJq(TEXT_PROGRAM, events), '');
|
||||
}),
|
||||
);
|
||||
test('a stream with no assistant text part reconstructs to empty (drives the #1936 stub), over a corpus', opts, () => {
|
||||
// The exact failure the bug describes: the agent runs tool calls and ends with
|
||||
// step_finish, emitting no text. Reconstruction must be empty so the content-gate
|
||||
// (`[ -n "$OPENCODE_REVIEW" ]`) falls through to the stub.
|
||||
const streams = fc.sample(fc.array(nonTextEvent, { minLength: 1, maxLength: 20 }), { numRuns: CORPUS, seed: 43 });
|
||||
const actual = runJqBatch(TEXT_PROGRAM, streams);
|
||||
streams.forEach((events, i) => {
|
||||
assert.equal(actual[i], '', `case ${i}: text-free stream must reconstruct to '' for ${JSON.stringify(events)}`);
|
||||
});
|
||||
});
|
||||
|
||||
test('text parts that are null/absent are dropped, never rendered as "null"', opts, () => {
|
||||
fc.assert(
|
||||
fc.property(
|
||||
fc.array(
|
||||
fc.oneof(
|
||||
fc.record({ type: fc.constant('text'), part: fc.record({ text: fc.constant(null) }) }),
|
||||
fc.record({ type: fc.constant('text'), part: fc.record({}) }),
|
||||
),
|
||||
{ minLength: 1, maxLength: 10 },
|
||||
),
|
||||
(events) => {
|
||||
const out = runJq(TEXT_PROGRAM, events);
|
||||
assert.equal(out, '');
|
||||
assert.doesNotMatch(out, /null/);
|
||||
},
|
||||
test('text parts that are null/absent are dropped, never rendered as "null", over a corpus', opts, () => {
|
||||
const nullish = fc.array(
|
||||
fc.oneof(
|
||||
fc.record({ type: fc.constant('text'), part: fc.record({ text: fc.constant(null) }) }),
|
||||
fc.record({ type: fc.constant('text'), part: fc.record({}) }),
|
||||
),
|
||||
{ minLength: 1, maxLength: 10 },
|
||||
);
|
||||
const streams = fc.sample(nullish, { numRuns: CORPUS, seed: 44 });
|
||||
const actual = runJqBatch(TEXT_PROGRAM, streams);
|
||||
streams.forEach((events, i) => {
|
||||
assert.equal(actual[i], '', `case ${i}: null/absent text must drop to ''`);
|
||||
assert.doesNotMatch(actual[i], /null/, `case ${i}: must never render "null"`);
|
||||
});
|
||||
});
|
||||
|
||||
// Explicit boundary + happy examples (deterministic, not sampled) — all in one
|
||||
// batched jq spawn. Pins the exact contract the corpus only covers probabilistically.
|
||||
test('boundary + happy example streams reconstruct exactly (batched)', opts, () => {
|
||||
const cases = [
|
||||
{ events: [{ type: 'text', part: { text: 'only' } }], expect: 'only' }, // single text part
|
||||
{ events: [{ type: 'text', part: { text: '' } }], expect: '' }, // empty-string text is kept
|
||||
{ events: [{ type: 'text', part: { text: 'a' } }, { type: 'tool_use', part: { tool: 'r' } }, { type: 'text', part: { text: 'b' } }], expect: 'a\nb' }, // text interleaved with noise
|
||||
{ events: [{ type: 'text', part: { text: 'x\ny' } }], expect: 'x\ny' }, // embedded newline preserved
|
||||
{ events: [{ type: 'tool_use', part: { tool: 'read' } }, { type: 'step_finish', part: { reason: 'stop', tokens: { output: 3 } } }], expect: '' }, // no text at all
|
||||
{ events: Array.from({ length: 30 }, (_v, i) => ({ type: 'text', part: { text: `p${i}` } })), expect: Array.from({ length: 30 }, (_v, i) => `p${i}`).join('\n') }, // max-size all-text stream
|
||||
];
|
||||
const actual = runJqBatch(TEXT_PROGRAM, cases.map((c) => c.events));
|
||||
cases.forEach((c, i) => assert.equal(actual[i], c.expect, `example ${i}: ${JSON.stringify(c.events)}`));
|
||||
});
|
||||
|
||||
// Diagnostic path (empty-output stub). The finding calls out `missing .tokens.output`
|
||||
// and no-step_finish as real edges — pin them with examples against the shipped jq.
|
||||
// and no-step_finish as real edges — pin them with examples against the shipped jq,
|
||||
// all in one batched spawn.
|
||||
describe('diagnostic reconstruction (stop reason + output tokens)', () => {
|
||||
test('reports reason and output tokens from the LAST step_finish', opts, () => {
|
||||
const events = [
|
||||
{ type: 'step_finish', part: { reason: 'tool_calls', tokens: { output: 5 } } },
|
||||
{ type: 'tool_use', part: {} },
|
||||
{ type: 'step_finish', part: { reason: 'stop', tokens: { output: 0 } } },
|
||||
test('reports reason/tokens from the LAST step_finish and degrades missing fields to "?"', opts, () => {
|
||||
const cases = [
|
||||
{ events: [
|
||||
{ type: 'step_finish', part: { reason: 'tool_calls', tokens: { output: 5 } } },
|
||||
{ type: 'tool_use', part: {} },
|
||||
{ type: 'step_finish', part: { reason: 'stop', tokens: { output: 0 } } },
|
||||
], expect: 'stop reason=stop, output tokens=0' }, // LAST step_finish wins
|
||||
{ events: [{ type: 'step_finish', part: { reason: 'stop', tokens: {} } }], expect: 'stop reason=stop, output tokens=?' }, // missing .tokens.output → "?"
|
||||
{ events: [{ type: 'tool_use', part: { tool: 'read' } }], expect: 'stop reason=?, output tokens=?' }, // no step_finish at all → both "?"
|
||||
];
|
||||
assert.equal(runJq(DIAG_PROGRAM, events), 'stop reason=stop, output tokens=0');
|
||||
});
|
||||
|
||||
test('missing .tokens.output degrades to "?" rather than null/garbage', opts, () => {
|
||||
const events = [{ type: 'step_finish', part: { reason: 'stop', tokens: {} } }];
|
||||
assert.equal(runJq(DIAG_PROGRAM, events), 'stop reason=stop, output tokens=?');
|
||||
});
|
||||
|
||||
test('no step_finish at all degrades both fields to "?"', opts, () => {
|
||||
const events = [{ type: 'tool_use', part: { tool: 'read' } }];
|
||||
assert.equal(runJq(DIAG_PROGRAM, events), 'stop reason=?, output tokens=?');
|
||||
const actual = runJqBatch(DIAG_PROGRAM, cases.map((c) => c.events));
|
||||
cases.forEach((c, i) => assert.equal(actual[i], c.expect, `diag ${i}: ${JSON.stringify(c.events)}`));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -158,7 +215,7 @@ describe('#1936 OpenCode review reconstruction — jq properties', () => {
|
||||
test('non-JSON stdout does not masquerade as a reconstructed review', opts, () => {
|
||||
let threw = false;
|
||||
try {
|
||||
execFileSync('jq', ['-rs', TEXT_PROGRAM], { input: 'auth token expired\n', encoding: 'utf8' });
|
||||
execFileSync('jq', ['-rs', TEXT_PROGRAM], { ...JQ_EXEC_OPTS, input: 'auth token expired\n' });
|
||||
} catch {
|
||||
threw = true;
|
||||
}
|
||||
|
||||
@@ -1,57 +1,115 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* pi extension reachability test — ADR-1239 Phase D / #1944.
|
||||
* pi extension reachability test — ADR-1239 Phase D / #1944, upgraded #2102.
|
||||
*
|
||||
* Proves the pi extension is keystone-WIRED: the registered /gsd command
|
||||
* handler dispatches through the GSD command-routing hub and returns a result
|
||||
* (not just a registration on a mock). This is the "user can invoke X" proof.
|
||||
* Proves the pi extension is keystone-WIRED: the registered /gsd command's
|
||||
* `handler(args, ctx)` (pi's REAL ExtensionAPI shape — NOT the `execute(ctx)`
|
||||
* shape the original #1944 cut used) dispatches through gsd-tools.cjs
|
||||
* (subprocess-reuse — dispatchGsdCommand) and returns real output, not just a
|
||||
* registration on a mock. This is the "user can invoke X" proof.
|
||||
*
|
||||
* Dispatch is exercised with a real read-only family/subcommand
|
||||
* (progress/json) against a real temp project, matching the sibling
|
||||
* tests/vscode-extension-reachability.test.cjs pattern — no fake dispatcher
|
||||
* injected, because the whole point of "reachability" is that the real
|
||||
* engine is reached.
|
||||
*/
|
||||
|
||||
const { test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
|
||||
const gsdPiExtension = require('../pi/gsd.cjs');
|
||||
const { _internals } = require('../pi/gsd.cjs');
|
||||
const { createTempDir, cleanup } = require('./helpers.cjs');
|
||||
|
||||
function mockPi() {
|
||||
const recorded = { commands: {}, tools: {}, events: [] };
|
||||
const recorded = { commands: {}, tools: {}, events: {} };
|
||||
return {
|
||||
registerCommand(name, def) { recorded.commands[name] = def; },
|
||||
registerTool(def) { if (def && def.name) recorded.tools[def.name] = def; },
|
||||
on(event) { recorded.events.push(event); },
|
||||
on(event, handler) { (recorded.events[event] = recorded.events[event] || []).push(handler); },
|
||||
_recorded: recorded,
|
||||
};
|
||||
}
|
||||
|
||||
test('the pi extension registers /gsd + gsd_invoke + tool_call via ExtensionAPI', () => {
|
||||
test('the pi extension registers /gsd (with getArgumentCompletions + handler) + gsd_invoke + the event surface via ExtensionAPI', () => {
|
||||
const pi = mockPi();
|
||||
gsdPiExtension(pi);
|
||||
assert.ok(pi._recorded.commands['gsd'], 'registers /gsd command');
|
||||
const gsdCommand = pi._recorded.commands['gsd'];
|
||||
assert.ok(gsdCommand, 'registers /gsd command');
|
||||
assert.equal(typeof gsdCommand.handler, 'function', '/gsd registers a handler(args, ctx) — pi\'s REAL ExtensionAPI shape, not execute(ctx)');
|
||||
assert.equal(typeof gsdCommand.getArgumentCompletions, 'function', '/gsd registers getArgumentCompletions');
|
||||
assert.ok(pi._recorded.tools['gsd_invoke'], 'registers gsd_invoke tool');
|
||||
assert.ok(pi._recorded.events.includes('tool_call'), 'subscribes to tool_call');
|
||||
assert.equal(typeof pi._recorded.tools['gsd_invoke'].execute, 'function');
|
||||
assert.ok(pi._recorded.events['tool_call'], 'subscribes to tool_call');
|
||||
assert.ok(pi._recorded.events['before_provider_request'], 'subscribes to before_provider_request');
|
||||
});
|
||||
|
||||
test('REACHABILITY: the /gsd handler dispatches through the engine hub (keystone wired)', async () => {
|
||||
const pi = mockPi();
|
||||
gsdPiExtension(pi);
|
||||
// Invoke the registered /gsd handler — it must dispatch through createHub
|
||||
// and return a JSON result (not throw). This is the keystone-wired proof.
|
||||
const result = await pi._recorded.commands['gsd'].execute({
|
||||
family: 'query',
|
||||
subcommand: 'help',
|
||||
});
|
||||
assert.equal(typeof result, 'string', '/gsd handler returns a string result');
|
||||
const parsed = JSON.parse(result);
|
||||
assert.ok(parsed !== null && typeof parsed === 'object',
|
||||
'/gsd dispatch produced a result object (the engine was reached)');
|
||||
test('REACHABILITY: parseGsdCommandArgs tokenizes a raw args string into {family, subcommand, args}', () => {
|
||||
const parsed = _internals.parseGsdCommandArgs('phase add --name test');
|
||||
assert.deepEqual(parsed, { family: 'phase', subcommand: 'add', args: ['--name', 'test'] });
|
||||
});
|
||||
|
||||
test('REACHABILITY: the gsd_invoke tool dispatches through the engine hub', async () => {
|
||||
test('REACHABILITY: empty args dispatch a working default (gsd-tools.cjs --help), not the broken "query help"', () => {
|
||||
const parsed = _internals.parseGsdCommandArgs('');
|
||||
assert.equal(parsed.family, '--help');
|
||||
assert.equal(parsed.subcommand, undefined);
|
||||
});
|
||||
|
||||
test('REACHABILITY: the /gsd handler dispatches a real family through gsd-tools.cjs and returns real output (keystone wired)', async () => {
|
||||
const pi = mockPi();
|
||||
gsdPiExtension(pi);
|
||||
const result = await pi._recorded.tools['gsd_invoke'].execute();
|
||||
assert.equal(typeof result, 'string');
|
||||
JSON.parse(result); // must be valid JSON (engine was reached)
|
||||
const dir = createTempDir();
|
||||
try {
|
||||
const result = await pi._recorded.commands['gsd'].handler('progress json', { cwd: dir });
|
||||
assert.equal(typeof result, 'string', '/gsd handler returns a string result');
|
||||
const parsed = JSON.parse(result);
|
||||
assert.equal(typeof parsed.percent, 'number', '/gsd dispatch reached gsd-tools.cjs for real (the engine was reached)');
|
||||
} finally {
|
||||
cleanup(dir);
|
||||
}
|
||||
});
|
||||
|
||||
test('REACHABILITY: an unknown family surfaces a clear GSD error string, not a throw', async () => {
|
||||
const pi = mockPi();
|
||||
gsdPiExtension(pi);
|
||||
const dir = createTempDir();
|
||||
try {
|
||||
const result = await pi._recorded.commands['gsd'].handler('no-such-family-8675309', { cwd: dir });
|
||||
assert.equal(typeof result, 'string');
|
||||
assert.match(result, /GSD error:/);
|
||||
assert.match(result, /no-such-family-8675309|Unknown command/);
|
||||
} finally {
|
||||
cleanup(dir);
|
||||
}
|
||||
});
|
||||
|
||||
test('REACHABILITY: the gsd_invoke tool dispatches through the engine and returns real content', async () => {
|
||||
const pi = mockPi();
|
||||
gsdPiExtension(pi);
|
||||
const dir = createTempDir();
|
||||
try {
|
||||
const result = await pi._recorded.tools['gsd_invoke'].execute(
|
||||
'call-1',
|
||||
{ family: 'progress', subcommand: 'json' },
|
||||
null,
|
||||
null,
|
||||
{ cwd: dir },
|
||||
);
|
||||
assert.ok(result && Array.isArray(result.content), 'gsd_invoke returns {content:[...]}');
|
||||
const parsed = JSON.parse(result.content[0].text);
|
||||
assert.equal(typeof parsed.percent, 'number', 'gsd_invoke dispatch reached gsd-tools.cjs for real');
|
||||
} finally {
|
||||
cleanup(dir);
|
||||
}
|
||||
});
|
||||
|
||||
test('gsd_invoke rejects a missing "family" without dispatching', async () => {
|
||||
const pi = mockPi();
|
||||
gsdPiExtension(pi);
|
||||
const result = await pi._recorded.tools['gsd_invoke'].execute('call-2', {}, null, null, {});
|
||||
assert.match(result.content[0].text, /requires a non-empty string "family"/);
|
||||
});
|
||||
|
||||
test('gsdPiExtension throws without pi ExtensionAPI (fail-closed)', () => {
|
||||
|
||||
@@ -10,8 +10,10 @@
|
||||
* 2. pi's axes (imperative + bun) classify as 'programmatic-cli' (the reference profile).
|
||||
* 3. the reference pi host-plugin binds GSD via ExtensionAPI (registers command + tool + event).
|
||||
*
|
||||
* Full `--pi` installable-runtime integration is a larger follow-up (descriptor
|
||||
* + installer + golden parity 16→17); this slice proves the imperative binding.
|
||||
* Full `--pi` installable-runtime integration shipped in #2102 Stage 1
|
||||
* (capabilities/pi/capability.json + bin/install.js wiring + golden parity
|
||||
* 16→17, see tests/fixtures/golden-install-parity/pi.json); this slice
|
||||
* continues to prove the imperative ExtensionAPI binding in isolation.
|
||||
*/
|
||||
|
||||
const { test } = require('node:test');
|
||||
@@ -22,12 +24,13 @@ const { profileOf } = require('../gsd-core/bin/lib/host-integration.cjs');
|
||||
const gsdPiPlugin = require('./fixtures/pi-host-plugin.cjs');
|
||||
|
||||
// Mock pi ExtensionAPI: records registrations so the plugin is testable without
|
||||
// a live pi runtime.
|
||||
// a live pi runtime. Records the full command/tool definitions (not just
|
||||
// names) so #2102's handler/execute SHAPE can be asserted, not just presence.
|
||||
function mockPi() {
|
||||
const recorded = { commands: [], tools: [], events: [] };
|
||||
const recorded = { commands: [], tools: [], events: [], commandDefs: {}, toolDefs: {} };
|
||||
return {
|
||||
registerCommand(name) { recorded.commands.push(name); },
|
||||
registerTool(def) { if (def && def.name) recorded.tools.push(def.name); },
|
||||
registerCommand(name, def) { recorded.commands.push(name); recorded.commandDefs[name] = def; },
|
||||
registerTool(def) { if (def && def.name) { recorded.tools.push(def.name); recorded.toolDefs[def.name] = def; } },
|
||||
registerShortcut() {},
|
||||
registerFlag() {},
|
||||
on(event) { recorded.events.push(event); },
|
||||
@@ -58,6 +61,27 @@ test('the pi reference host-plugin binds GSD via the ExtensionAPI (command + too
|
||||
assert.ok(pi._recorded.events.includes('tool_call'), 'subscribes to the tool_call event');
|
||||
});
|
||||
|
||||
// #2102 Stage 2: pi's REAL ExtensionAPI shape is `handler(args, ctx)` for
|
||||
// registerCommand (NOT `execute(ctx)`, which the original #1682 cut used) and
|
||||
// a 5-arg `execute(toolCallId, params, signal, onUpdate, ctx)` for
|
||||
// registerTool. Locks the shape so a future drift back to the wrong contract
|
||||
// is a visible test failure.
|
||||
test('the /gsd command registers a handler(args, ctx) — not execute(ctx)', () => {
|
||||
const pi = mockPi();
|
||||
gsdPiPlugin(pi);
|
||||
const def = pi._recorded.commandDefs['gsd'];
|
||||
assert.equal(typeof def.handler, 'function', 'registerCommand takes handler(args, ctx), pi\'s real ExtensionAPI shape');
|
||||
assert.equal(def.execute, undefined, 'must not use the wrong execute(ctx) shape');
|
||||
});
|
||||
|
||||
test('the gsd_invoke tool registers a 5-arg execute(toolCallId, params, signal, onUpdate, ctx)', () => {
|
||||
const pi = mockPi();
|
||||
gsdPiPlugin(pi);
|
||||
const def = pi._recorded.toolDefs['gsd_invoke'];
|
||||
assert.equal(typeof def.execute, 'function');
|
||||
assert.equal(def.execute.length, 5, 'gsd_invoke.execute must declare pi\'s real 5-arg tool-execute signature');
|
||||
});
|
||||
|
||||
test('gsdPiPlugin throws if the pi ExtensionAPI is not provided (fail-closed)', () => {
|
||||
assert.throws(() => gsdPiPlugin(null), /ExtensionAPI is required/);
|
||||
assert.throws(() => gsdPiPlugin(undefined), /ExtensionAPI is required/);
|
||||
|
||||
172
tests/pi-upgrades.test.cjs
Normal file
172
tests/pi-upgrades.test.cjs
Normal file
@@ -0,0 +1,172 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* pi upgrades — ADR-1239 Phase D / #2102 Stage 2 (EoS/pi).
|
||||
*
|
||||
* Mirrors tests/opencode-imperative-reference.test.cjs's structure (Host-
|
||||
* Integration axes classification/negotiation + the Context7-verified
|
||||
* upgrades) for pi's three additive upgrades:
|
||||
* 1. EXTENSION_EVENT_SURFACES.pi — the full ~30-event ExtensionAPI surface
|
||||
* (was a single-event ['tool_call'] placeholder).
|
||||
* 2. Event bindings — pi/gsd.cjs actually binds session_start,
|
||||
* before_agent_start, session_before_compact (+ tool_call) via pi.on(),
|
||||
* not just declaring the surface in host-integration.cts.
|
||||
* 3. Active-model steering — before_provider_request resolves GSD's
|
||||
* tier→model via the model-catalog's pi entries (populated this stage)
|
||||
* and returns a bare anthropic model id pi's built-in models accept;
|
||||
* fails open (returns undefined) when resolution comes back null.
|
||||
*
|
||||
* Plus the command-surface completions (getArgumentCompletions) and the
|
||||
* standard fail-closed negotiation guarantee.
|
||||
*/
|
||||
|
||||
const { test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const {
|
||||
extensionEventSurfaceFor,
|
||||
negotiateHostCapabilities,
|
||||
UNDOCUMENTED,
|
||||
} = require('../gsd-core/bin/lib/host-integration.cjs');
|
||||
const { RUNTIME_PROFILE_MAP } = require('../gsd-core/bin/lib/model-catalog.cjs');
|
||||
|
||||
const gsdPiExtension = require('../pi/gsd.cjs');
|
||||
const { _internals } = require('../pi/gsd.cjs');
|
||||
|
||||
const PI_CAP = JSON.parse(
|
||||
fs.readFileSync(path.join(__dirname, '..', 'capabilities', 'pi', 'capability.json'), 'utf8'),
|
||||
);
|
||||
const PI_AXES = PI_CAP.runtime.hostIntegration;
|
||||
|
||||
function mockPi() {
|
||||
const recorded = { commands: {}, tools: {}, events: {} };
|
||||
return {
|
||||
registerCommand(name, def) { recorded.commands[name] = def; },
|
||||
registerTool(def) { if (def && def.name) recorded.tools[def.name] = def; },
|
||||
registerProvider() {
|
||||
throw new Error('gsdPiExtension must NOT call registerProvider — GSD steers pi\'s existing anthropic models, it does not add a new provider');
|
||||
},
|
||||
on(event, handler) { (recorded.events[event] = recorded.events[event] || []).push(handler); },
|
||||
_recorded: recorded,
|
||||
};
|
||||
}
|
||||
|
||||
// -- (1) EXTENSION_EVENT_SURFACES.pi has all 30 events -----------------------
|
||||
|
||||
const EXPECTED_PI_EVENTS = [
|
||||
'session_start', 'project_trust', 'resources_discover', 'input',
|
||||
'before_agent_start', 'agent_start', 'message_start', 'message_update',
|
||||
'message_end', 'turn_start', 'context', 'before_provider_request',
|
||||
'after_provider_response', 'tool_execution_start', 'tool_execution_update',
|
||||
'tool_execution_end', 'tool_call', 'tool_result', 'turn_end', 'agent_end',
|
||||
'session_before_switch', 'session_shutdown', 'session_before_fork',
|
||||
'session_info_changed', 'session_before_compact', 'session_compact',
|
||||
'session_before_tree', 'session_tree', 'thinking_level_select', 'model_select',
|
||||
];
|
||||
|
||||
test('pi extension-event surface declares all 30 documented ExtensionAPI events (#2102)', () => {
|
||||
const surface = extensionEventSurfaceFor('pi');
|
||||
assert.ok(surface, 'pi is a consumed extensionEvents dialect');
|
||||
assert.equal(surface.length, 30, `expected exactly 30 events, got ${surface.length}`);
|
||||
for (const ev of EXPECTED_PI_EVENTS) {
|
||||
assert.ok(surface.includes(ev), `expected pi extension-event surface to include "${ev}"`);
|
||||
}
|
||||
assert.deepEqual([...surface].sort(), [...EXPECTED_PI_EVENTS].sort());
|
||||
});
|
||||
|
||||
// -- (2) the binding actually binds session_start/before_agent_start/ -------
|
||||
// session_before_compact (not just declared in host-integration.cts)
|
||||
|
||||
test('gsdPiExtension binds session_start, before_agent_start, session_before_compact, tool_call, before_provider_request', () => {
|
||||
const pi = mockPi();
|
||||
gsdPiExtension(pi);
|
||||
for (const ev of ['session_start', 'before_agent_start', 'session_before_compact', 'tool_call', 'before_provider_request']) {
|
||||
assert.ok(Array.isArray(pi._recorded.events[ev]) && pi._recorded.events[ev].length > 0,
|
||||
`expected gsdPiExtension to bind pi.on("${ev}", ...)`);
|
||||
}
|
||||
});
|
||||
|
||||
test('gsdPiExtension does NOT call registerProvider (GSD steers pi\'s existing anthropic models, not a new provider)', () => {
|
||||
const pi = mockPi();
|
||||
// If gsdPiExtension called registerProvider, mockPi's registerProvider throws.
|
||||
assert.doesNotThrow(() => gsdPiExtension(pi));
|
||||
});
|
||||
|
||||
// Finding #3 (adversarial review): the tests below previously only exercised
|
||||
// buildBeforeProviderRequestHandler() directly (the builder), never the
|
||||
// handler ACTUALLY REGISTERED via pi.on('before_provider_request', ...) in
|
||||
// gsdPiExtension. This ties the bound handler (default tier = 'sonnet') to
|
||||
// the real model-catalog steering end-to-end.
|
||||
test('the ACTUALLY-REGISTERED before_provider_request handler steers to the default-tier model-catalog pi id', async () => {
|
||||
const pi = mockPi();
|
||||
gsdPiExtension(pi);
|
||||
const boundHandler = pi._recorded.events['before_provider_request'][0];
|
||||
assert.equal(typeof boundHandler, 'function');
|
||||
|
||||
const out = await boundHandler({ payload: {} }, { cwd: __dirname });
|
||||
assert.ok(out, 'expected a modified payload, not undefined');
|
||||
assert.equal(out.model, RUNTIME_PROFILE_MAP.pi.sonnet.model, 'the bound handler steers to the default (sonnet) tier\'s model-catalog pi id');
|
||||
assert.equal(out.model, 'claude-sonnet-5');
|
||||
});
|
||||
|
||||
// -- (3) before_provider_request: active-model steering ----------------------
|
||||
|
||||
test('before_provider_request resolves a tier that maps to a model → returns a payload with the bare anthropic model id (model-catalog pi ids)', async () => {
|
||||
const handler = _internals.buildBeforeProviderRequestHandler({ tier: 'sonnet' });
|
||||
const result = await handler({ payload: { existing: 'field' } }, { cwd: __dirname });
|
||||
assert.ok(result, 'expected a modified payload, not undefined');
|
||||
assert.equal(result.existing, 'field', 'original payload fields are preserved');
|
||||
assert.equal(result.model, RUNTIME_PROFILE_MAP.pi.sonnet.model, 'model id matches the model-catalog pi entry');
|
||||
assert.equal(result.model, 'claude-sonnet-5');
|
||||
});
|
||||
|
||||
test('before_provider_request resolves opus/haiku tiers to their model-catalog pi ids too', async () => {
|
||||
const opusHandler = _internals.buildBeforeProviderRequestHandler({ tier: 'opus' });
|
||||
const opusResult = await opusHandler({ payload: {} }, { cwd: __dirname });
|
||||
assert.equal(opusResult.model, RUNTIME_PROFILE_MAP.pi.opus.model);
|
||||
|
||||
const haikuHandler = _internals.buildBeforeProviderRequestHandler({ tier: 'haiku' });
|
||||
const haikuResult = await haikuHandler({ payload: {} }, { cwd: __dirname });
|
||||
assert.equal(haikuResult.model, RUNTIME_PROFILE_MAP.pi.haiku.model);
|
||||
});
|
||||
|
||||
test('before_provider_request given a tier that resolves to null returns undefined (fail-open, never a wrong/empty id)', async () => {
|
||||
const handler = _internals.buildBeforeProviderRequestHandler({ tier: 'not-a-real-tier-8675309' });
|
||||
const result = await handler({ payload: { existing: 'field' } }, { cwd: __dirname });
|
||||
assert.equal(result, undefined);
|
||||
});
|
||||
|
||||
// -- getArgumentCompletions returns family suggestions -----------------------
|
||||
|
||||
test('getArgumentCompletions filters PI_COMMAND_FAMILIES by prefix and returns null when empty', () => {
|
||||
const matches = _internals.getArgumentCompletions('mi');
|
||||
assert.ok(Array.isArray(matches) && matches.length > 0);
|
||||
assert.ok(matches.some((m) => m.value === 'milestone'));
|
||||
for (const m of matches) {
|
||||
assert.equal(typeof m.value, 'string');
|
||||
assert.equal(typeof m.label, 'string');
|
||||
}
|
||||
|
||||
const all = _internals.getArgumentCompletions('');
|
||||
assert.ok(Array.isArray(all) && all.length > 0);
|
||||
assert.deepEqual(all.map((m) => m.value), [..._internals.PI_COMMAND_FAMILIES]);
|
||||
|
||||
const none = _internals.getArgumentCompletions('zzz-no-such-family-8675309');
|
||||
assert.equal(none, null);
|
||||
});
|
||||
|
||||
// -- fail-closed negotiation for pi -------------------------------------------
|
||||
|
||||
test('negotiateHostCapabilities never throws for pi, even on an undeclared/corrupted axis', () => {
|
||||
assert.doesNotThrow(() => negotiateHostCapabilities({}));
|
||||
assert.doesNotThrow(() => negotiateHostCapabilities({ ...PI_AXES, embeddingMode: UNDOCUMENTED }));
|
||||
assert.doesNotThrow(() => negotiateHostCapabilities({ ...PI_AXES, embeddingMode: 'future-unknown-axis-value' }));
|
||||
assert.doesNotThrow(() => negotiateHostCapabilities({ ...PI_AXES, dispatch: undefined }));
|
||||
});
|
||||
|
||||
test('pi axes negotiate modelMode:"active" (the active-model steering axis)', () => {
|
||||
const result = negotiateHostCapabilities(PI_AXES);
|
||||
assert.equal(result.effective.modelMode, 'active');
|
||||
});
|
||||
@@ -1,3 +1,4 @@
|
||||
// allow-test-rule: runtime-contract-is-the-product — asserts GSD workflow/template markdown prose, the executable contract (#138, #2117)
|
||||
'use strict';
|
||||
|
||||
// Policy regression test for issue #138:
|
||||
@@ -67,3 +68,59 @@ test('legacy Nyquist config helper still detects unsafe direct reads', () => {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Issue #2117: audit-milestone could not distinguish a not-yet-validated phase
|
||||
// from a validated-but-failing one — both read Nyquist PARTIAL. The fix makes the
|
||||
// dead `status` field live (validate-phase §6 promotes draft → validated) and has
|
||||
// audit-milestone §5.5 bucket `status: draft` as a distinct NOT-VALIDATED state.
|
||||
// These assertions fail-first if either half of that two-workflow contract is
|
||||
// reverted, silently re-collapsing "not validated" and "validation failed".
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
test('#2117 validate-phase.md promotes status: draft → validated when it reconciles VALIDATION.md', () => {
|
||||
const content = readWorkflow('validate-phase.md');
|
||||
// Both the create (State B) and update (State A) paths in §6 must set the
|
||||
// terminal marker, otherwise `status` stays `draft` for the life of the file
|
||||
// and audit-milestone cannot tell an unvalidated phase from a failing one.
|
||||
const occurrences = content.match(/status: validated/g) || [];
|
||||
assert.ok(
|
||||
occurrences.length >= 2,
|
||||
'validate-phase.md must set `status: validated` in both the create (State B) and update (State A) VALIDATION.md paths',
|
||||
);
|
||||
});
|
||||
|
||||
test('#2117 audit-milestone.md buckets status: draft as NOT-VALIDATED, never PARTIAL', () => {
|
||||
const content = readWorkflow('audit-milestone.md');
|
||||
assert.ok(
|
||||
content.includes('`status`'),
|
||||
'audit-milestone.md must parse the `status` frontmatter field to detect not-yet-validated phases',
|
||||
);
|
||||
assert.ok(
|
||||
content.includes('| NOT-VALIDATED | `status: draft`'),
|
||||
'audit-milestone.md must define a NOT-VALIDATED bucket keyed on `status: draft`',
|
||||
);
|
||||
assert.ok(
|
||||
content.includes('| COMPLIANT | `status: validated`'),
|
||||
'COMPLIANT must require `status: validated` so a draft file can never be scored compliant',
|
||||
);
|
||||
assert.ok(
|
||||
content.includes('| PARTIAL | `status: validated`'),
|
||||
'PARTIAL must require `status: validated`; a `status: draft` file is NOT-VALIDATED, not PARTIAL',
|
||||
);
|
||||
assert.ok(
|
||||
content.includes('not_validated_phases'),
|
||||
'audit-milestone.md must report not_validated_phases in the nyquist audit YAML aggregate',
|
||||
);
|
||||
});
|
||||
|
||||
test('#2117 VALIDATION.md template seeds status: draft (the pre-validation state)', () => {
|
||||
const template = fs.readFileSync(
|
||||
path.join(__dirname, '..', 'gsd-core', 'templates', 'VALIDATION.md'),
|
||||
'utf8',
|
||||
);
|
||||
assert.ok(
|
||||
/^status: draft$/m.test(template),
|
||||
'VALIDATION.md template must seed `status: draft`; validate-phase promotes it to validated',
|
||||
);
|
||||
});
|
||||
|
||||
629
tests/registry-schema.test.cjs
Normal file
629
tests/registry-schema.test.cjs
Normal file
@@ -0,0 +1,629 @@
|
||||
'use strict';
|
||||
process.env.GSD_TEST_MODE = '1';
|
||||
|
||||
const { test, describe } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const path = require('node:path');
|
||||
const fc = require('fast-check');
|
||||
|
||||
const {
|
||||
LOOP_POINTS,
|
||||
HOOK_KINDS,
|
||||
INTERFACE_POINTS,
|
||||
PROFILES,
|
||||
AXES,
|
||||
AXES_FREE_STRING,
|
||||
CAPABILITY_REQUIRED,
|
||||
EOS_REQUIRED,
|
||||
isValidGsdRange,
|
||||
validateEntries,
|
||||
renderMarkdown,
|
||||
} = require(path.join(__dirname, '..', 'scripts', 'registry-schema.cjs'));
|
||||
|
||||
// ─── Fixtures ─────────────────────────────────────────────────────────────
|
||||
|
||||
function validCapabilityEntry() {
|
||||
return {
|
||||
id: 'my-capability',
|
||||
name: 'My Capability',
|
||||
type: 'capability',
|
||||
repo: 'octocat/my-capability',
|
||||
description: 'Does a useful thing for GSD users.',
|
||||
author: 'Octocat',
|
||||
license: 'MIT',
|
||||
enginesGsd: '>=1.6.0 <3.0.0',
|
||||
install: 'gsd capability install https://github.com/octocat/my-capability.git#v1.0.0',
|
||||
uninstall: 'gsd capability remove my-capability',
|
||||
interactions: {
|
||||
loopExtensionPoints: ['execute:pre'],
|
||||
hookKinds: ['step'],
|
||||
configKeys: ['myCapability.enabled'],
|
||||
requires: [],
|
||||
runtimeCompat: ['all'],
|
||||
produces: [],
|
||||
consumes: [],
|
||||
},
|
||||
discussion: 'https://github.com/octocat/my-capability/discussions/1',
|
||||
};
|
||||
}
|
||||
|
||||
function validEosEntry() {
|
||||
return {
|
||||
id: 'my-host-plugin',
|
||||
name: 'My Host Plugin',
|
||||
type: 'eos',
|
||||
repo: 'octocat/my-host-plugin',
|
||||
description: 'Embeds GSD as an orchestration engine in My Host.',
|
||||
author: 'Octocat',
|
||||
license: 'MIT',
|
||||
enginesGsd: '>=1.6.0 <3.0.0',
|
||||
install: 'See the My Host plugin marketplace listing.',
|
||||
uninstall: 'Uninstall via the My Host plugin manager.',
|
||||
protocolVersion: 1,
|
||||
interactions: {
|
||||
interfacePoints: ['command', 'state'],
|
||||
profile: 'programmatic-cli',
|
||||
axes: {
|
||||
embeddingMode: 'imperative',
|
||||
commandSurface: 'slash-file',
|
||||
dispatch: 'Supports nested background dispatch up to depth 3.',
|
||||
modelMode: 'active',
|
||||
hookBus: 'host',
|
||||
stateIO: 'filesystem',
|
||||
transport: 'mcp',
|
||||
runtime: 'node',
|
||||
},
|
||||
},
|
||||
discussion: 'https://github.com/octocat/my-host-plugin/discussions/2',
|
||||
};
|
||||
}
|
||||
|
||||
// ─── Vocabulary constants ───────────────────────────────────────────────────
|
||||
|
||||
describe('registry-schema: closed vocabulary constants', () => {
|
||||
test('LOOP_POINTS is the 12 canonical loop points (ADR-857), in order', () => {
|
||||
assert.deepEqual(LOOP_POINTS, [
|
||||
'discuss:pre',
|
||||
'discuss:post',
|
||||
'plan:pre',
|
||||
'plan:post',
|
||||
'execute:pre',
|
||||
'execute:wave:pre',
|
||||
'execute:wave:post',
|
||||
'execute:post',
|
||||
'verify:pre',
|
||||
'verify:post',
|
||||
'ship:pre',
|
||||
'ship:post',
|
||||
]);
|
||||
});
|
||||
|
||||
test('HOOK_KINDS is step/contribution/gate (ADR-857 Decision 4)', () => {
|
||||
assert.deepEqual(HOOK_KINDS, ['step', 'contribution', 'gate']);
|
||||
});
|
||||
|
||||
test('INTERFACE_POINTS is the six ADR-1239 interface points', () => {
|
||||
assert.deepEqual(INTERFACE_POINTS, ['command', 'dispatch', 'model', 'hooks', 'state', 'artifact']);
|
||||
});
|
||||
|
||||
test('PROFILES is the three ADR-1239 negotiation profiles', () => {
|
||||
assert.deepEqual(PROFILES, ['programmatic-cli', 'declarative-cli', 'ide']);
|
||||
});
|
||||
|
||||
test('AXES has exactly the eight ADR-1239 negotiated axis keys', () => {
|
||||
assert.deepEqual(
|
||||
Object.keys(AXES).sort(),
|
||||
['commandSurface', 'dispatch', 'embeddingMode', 'hookBus', 'modelMode', 'runtime', 'stateIO', 'transport'].sort(),
|
||||
);
|
||||
});
|
||||
|
||||
test('AXES.dispatch carries the free-string sentinel, not an enum array', () => {
|
||||
assert.equal(AXES.dispatch, AXES_FREE_STRING);
|
||||
assert.equal(Array.isArray(AXES.dispatch), false);
|
||||
});
|
||||
|
||||
test('every non-dispatch AXES entry is a non-empty enum array', () => {
|
||||
for (const [key, value] of Object.entries(AXES)) {
|
||||
if (key === 'dispatch') continue;
|
||||
assert.ok(Array.isArray(value), `AXES.${key} should be an array`);
|
||||
assert.ok(value.length > 0, `AXES.${key} should be non-empty`);
|
||||
}
|
||||
});
|
||||
|
||||
test('CAPABILITY_REQUIRED lists the 12 required capability entry fields', () => {
|
||||
assert.deepEqual(CAPABILITY_REQUIRED, [
|
||||
'id', 'name', 'type', 'repo', 'description', 'author', 'license',
|
||||
'enginesGsd', 'install', 'uninstall', 'interactions', 'discussion',
|
||||
]);
|
||||
});
|
||||
|
||||
test('EOS_REQUIRED lists the 13 required eos entry fields (adds protocolVersion)', () => {
|
||||
assert.deepEqual(EOS_REQUIRED, [
|
||||
'id', 'name', 'type', 'repo', 'description', 'author', 'license',
|
||||
'enginesGsd', 'install', 'uninstall', 'interactions', 'discussion', 'protocolVersion',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── validateEntries: capability ───────────────────────────────────────────
|
||||
|
||||
describe('validateEntries: capability — happy path', () => {
|
||||
test('a fully-valid capability entry passes', () => {
|
||||
const verdict = validateEntries([validCapabilityEntry()], { type: 'capability' });
|
||||
assert.equal(verdict.ok, true);
|
||||
assert.deepEqual(verdict.errors, []);
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateEntries: capability — required fields', () => {
|
||||
for (const field of CAPABILITY_REQUIRED) {
|
||||
test(`missing required field "${field}" fails`, () => {
|
||||
const entry = validCapabilityEntry();
|
||||
delete entry[field];
|
||||
const verdict = validateEntries([entry], { type: 'capability' });
|
||||
assert.equal(verdict.ok, false);
|
||||
assert.ok(verdict.errors.length > 0, 'expected at least one error');
|
||||
assert.ok(
|
||||
verdict.errors.some((e) => e.field === field),
|
||||
`expected an error referencing field "${field}", got: ${JSON.stringify(verdict.errors)}`,
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('validateEntries: capability — field shape violations', () => {
|
||||
test('bad id (not kebab-case) fails', () => {
|
||||
const entry = validCapabilityEntry();
|
||||
entry.id = 'Not_Kebab_Case';
|
||||
const verdict = validateEntries([entry], { type: 'capability' });
|
||||
assert.equal(verdict.ok, false);
|
||||
assert.ok(verdict.errors.some((e) => e.field === 'id'));
|
||||
});
|
||||
|
||||
test('bad repo (not owner/repo form) fails', () => {
|
||||
const entry = validCapabilityEntry();
|
||||
entry.repo = 'not-a-valid-repo';
|
||||
const verdict = validateEntries([entry], { type: 'capability' });
|
||||
assert.equal(verdict.ok, false);
|
||||
assert.ok(verdict.errors.some((e) => e.field === 'repo'));
|
||||
});
|
||||
|
||||
test('bad enginesGsd (malformed range) fails', () => {
|
||||
const entry = validCapabilityEntry();
|
||||
entry.enginesGsd = 'not-a-semver-range';
|
||||
const verdict = validateEntries([entry], { type: 'capability' });
|
||||
assert.equal(verdict.ok, false);
|
||||
assert.ok(verdict.errors.some((e) => e.field === 'enginesGsd'));
|
||||
});
|
||||
|
||||
test('bad discussion URL fails', () => {
|
||||
const entry = validCapabilityEntry();
|
||||
entry.discussion = 'https://example.com/not-a-discussion';
|
||||
const verdict = validateEntries([entry], { type: 'capability' });
|
||||
assert.equal(verdict.ok, false);
|
||||
assert.ok(verdict.errors.some((e) => e.field === 'discussion'));
|
||||
});
|
||||
|
||||
test('bad license fails', () => {
|
||||
const entry = validCapabilityEntry();
|
||||
entry.license = 'Not A Valid License!!';
|
||||
const verdict = validateEntries([entry], { type: 'capability' });
|
||||
assert.equal(verdict.ok, false);
|
||||
assert.ok(verdict.errors.some((e) => e.field === 'license'));
|
||||
});
|
||||
|
||||
test('unknown top-level key fails (strict schema)', () => {
|
||||
const entry = validCapabilityEntry();
|
||||
entry.extraUnknownField = 'nope';
|
||||
const verdict = validateEntries([entry], { type: 'capability' });
|
||||
assert.equal(verdict.ok, false);
|
||||
assert.ok(verdict.errors.some((e) => e.field === 'extraUnknownField'));
|
||||
});
|
||||
|
||||
test('duplicate id across two entries fails', () => {
|
||||
const a = validCapabilityEntry();
|
||||
const b = validCapabilityEntry();
|
||||
b.name = 'A Different Name';
|
||||
b.repo = 'octocat/another-capability';
|
||||
b.discussion = 'https://github.com/octocat/another-capability/discussions/2';
|
||||
// b.id intentionally left the same as a.id
|
||||
const verdict = validateEntries([a, b], { type: 'capability' });
|
||||
assert.equal(verdict.ok, false);
|
||||
assert.ok(verdict.errors.some((e) => e.field === 'id' && /duplicate/i.test(e.reason)));
|
||||
});
|
||||
|
||||
test('empty loopExtensionPoints fails (AC3 — must be non-empty)', () => {
|
||||
const entry = validCapabilityEntry();
|
||||
entry.interactions.loopExtensionPoints = [];
|
||||
const verdict = validateEntries([entry], { type: 'capability' });
|
||||
assert.equal(verdict.ok, false);
|
||||
assert.ok(verdict.errors.some((e) => e.field === 'interactions.loopExtensionPoints'));
|
||||
});
|
||||
|
||||
test('invalid loop point fails', () => {
|
||||
const entry = validCapabilityEntry();
|
||||
entry.interactions.loopExtensionPoints = ['not:a:real:point'];
|
||||
const verdict = validateEntries([entry], { type: 'capability' });
|
||||
assert.equal(verdict.ok, false);
|
||||
assert.ok(verdict.errors.some((e) => e.field === 'interactions.loopExtensionPoints'));
|
||||
});
|
||||
|
||||
test('invalid hook kind fails', () => {
|
||||
const entry = validCapabilityEntry();
|
||||
entry.interactions.hookKinds = ['not-a-real-kind'];
|
||||
const verdict = validateEntries([entry], { type: 'capability' });
|
||||
assert.equal(verdict.ok, false);
|
||||
assert.ok(verdict.errors.some((e) => e.field === 'interactions.hookKinds'));
|
||||
});
|
||||
});
|
||||
|
||||
// ─── validateEntries: eos ───────────────────────────────────────────────────
|
||||
|
||||
describe('validateEntries: eos — happy path', () => {
|
||||
test('a fully-valid eos entry passes', () => {
|
||||
const verdict = validateEntries([validEosEntry()], { type: 'eos' });
|
||||
assert.equal(verdict.ok, true);
|
||||
assert.deepEqual(verdict.errors, []);
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateEntries: eos — field shape violations', () => {
|
||||
test('bad interfacePoint fails', () => {
|
||||
const entry = validEosEntry();
|
||||
entry.interactions.interfacePoints = ['not-a-real-point'];
|
||||
const verdict = validateEntries([entry], { type: 'eos' });
|
||||
assert.equal(verdict.ok, false);
|
||||
assert.ok(verdict.errors.some((e) => e.field === 'interactions.interfacePoints'));
|
||||
});
|
||||
|
||||
test('bad profile fails', () => {
|
||||
const entry = validEosEntry();
|
||||
entry.interactions.profile = 'not-a-real-profile';
|
||||
const verdict = validateEntries([entry], { type: 'eos' });
|
||||
assert.equal(verdict.ok, false);
|
||||
assert.ok(verdict.errors.some((e) => e.field === 'interactions.profile'));
|
||||
});
|
||||
|
||||
test('bad axis value fails', () => {
|
||||
const entry = validEosEntry();
|
||||
entry.interactions.axes.embeddingMode = 'not-a-real-value';
|
||||
const verdict = validateEntries([entry], { type: 'eos' });
|
||||
assert.equal(verdict.ok, false);
|
||||
assert.ok(verdict.errors.some((e) => e.field === 'interactions.axes.embeddingMode'));
|
||||
});
|
||||
|
||||
test('protocolVersion < 1 fails', () => {
|
||||
const entry = validEosEntry();
|
||||
entry.protocolVersion = 0;
|
||||
const verdict = validateEntries([entry], { type: 'eos' });
|
||||
assert.equal(verdict.ok, false);
|
||||
assert.ok(verdict.errors.some((e) => e.field === 'protocolVersion'));
|
||||
});
|
||||
|
||||
test('missing axis key fails', () => {
|
||||
const entry = validEosEntry();
|
||||
delete entry.interactions.axes.runtime;
|
||||
const verdict = validateEntries([entry], { type: 'eos' });
|
||||
assert.equal(verdict.ok, false);
|
||||
assert.ok(verdict.errors.some((e) => e.field === 'interactions.axes'));
|
||||
});
|
||||
|
||||
test('extra axis key fails', () => {
|
||||
const entry = validEosEntry();
|
||||
entry.interactions.axes.notARealAxis = 'x';
|
||||
const verdict = validateEntries([entry], { type: 'eos' });
|
||||
assert.equal(verdict.ok, false);
|
||||
assert.ok(verdict.errors.some((e) => e.field === 'interactions.axes'));
|
||||
});
|
||||
});
|
||||
|
||||
// ─── renderMarkdown ─────────────────────────────────────────────────────────
|
||||
|
||||
describe('renderMarkdown', () => {
|
||||
test('is deterministic across two calls regardless of input entry order', () => {
|
||||
const a = validCapabilityEntry();
|
||||
const b = { ...validCapabilityEntry(), id: 'zzz-capability', name: 'ZZZ Capability' };
|
||||
const first = renderMarkdown([a, b], { type: 'capability', sourceFile: 'capabilities.json' });
|
||||
const second = renderMarkdown([b, a], { type: 'capability', sourceFile: 'capabilities.json' });
|
||||
assert.equal(first, second);
|
||||
});
|
||||
|
||||
test('contains the shields.io release badge for a populated registry', () => {
|
||||
const rendered = renderMarkdown([validCapabilityEntry()], { type: 'capability', sourceFile: 'capabilities.json' });
|
||||
assert.match(rendered, /img\.shields\.io\/github\/v\/release/);
|
||||
});
|
||||
|
||||
test('contains the entry discussion URL for a populated registry', () => {
|
||||
const entry = validCapabilityEntry();
|
||||
const rendered = renderMarkdown([entry], { type: 'capability', sourceFile: 'capabilities.json' });
|
||||
assert.ok(rendered.includes(entry.discussion), 'expected rendered output to include the discussion URL');
|
||||
});
|
||||
|
||||
test('renders the author for a populated registry', () => {
|
||||
const entry = validCapabilityEntry();
|
||||
const rendered = renderMarkdown([entry], { type: 'capability', sourceFile: 'capabilities.json' });
|
||||
assert.match(rendered, /- \*\*Author:\*\* Octocat/);
|
||||
});
|
||||
|
||||
test('contains the empty-state text for zero entries', () => {
|
||||
const rendered = renderMarkdown([], { type: 'capability', sourceFile: 'capabilities.json' });
|
||||
assert.match(rendered, /No entries yet/);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── isValidGsdRange ────────────────────────────────────────────────────────
|
||||
|
||||
describe('isValidGsdRange', () => {
|
||||
test('fast-check property: well-formed operator+M.N.P ranges are valid', () => {
|
||||
fc.assert(
|
||||
fc.property(
|
||||
fc.constantFrom('', '>=', '>', '<=', '<', '=', '^', '~'),
|
||||
fc.integer({ min: 0, max: 999 }),
|
||||
fc.integer({ min: 0, max: 999 }),
|
||||
fc.integer({ min: 0, max: 999 }),
|
||||
(op, major, minor, patch) => {
|
||||
const range = `${op}${major}.${minor}.${patch}`;
|
||||
assert.equal(isValidGsdRange(range), true, range);
|
||||
},
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test('fast-check property: a non-numeric major segment is always invalid', () => {
|
||||
// Replacing a numeric segment with letters can never be a well-formed range.
|
||||
// Letters-only (not arbitrary garbage) keeps the generator from accidentally
|
||||
// producing a valid semver-with-prerelease like `1.2.3-rc` — `>=1.2.3-rc.0.0`
|
||||
// IS a legitimate prerelease range the validator accepts, which would make an
|
||||
// "always invalid" assertion intermittently fail (a hidden flake).
|
||||
fc.assert(
|
||||
fc.property(
|
||||
fc.constantFrom('', '>=', '>', '<=', '<', '=', '^', '~'),
|
||||
fc
|
||||
.array(fc.constantFrom(...'abcdefghijklmnopqrstuvwxyz'.split('')), { minLength: 1, maxLength: 6 })
|
||||
.map((chars) => chars.join('')),
|
||||
(op, letters) => {
|
||||
assert.equal(isValidGsdRange(`${op}${letters}.0.0`), false, `${op}${letters}.0.0`);
|
||||
},
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test('boundary: valid range strings', () => {
|
||||
for (const good of ['1.0.0', '>=1.0.0', '^1.0.0 <2.0.0', '*']) {
|
||||
assert.equal(isValidGsdRange(good), true, good);
|
||||
}
|
||||
});
|
||||
|
||||
test('boundary: invalid range strings', () => {
|
||||
for (const bad of ['1.0', '>=abc', '']) {
|
||||
assert.equal(isValidGsdRange(bad), false, bad);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ─── renderMarkdown: Markdown-injection escaping (adversarial-review hardening) ──
|
||||
|
||||
describe('renderMarkdown: mdInline escaping neutralizes untrusted free text', () => {
|
||||
test('description containing a table-breakout + link-hijack payload is escaped', () => {
|
||||
const entry = validCapabilityEntry();
|
||||
entry.description = 'Good stuff |  | text';
|
||||
const rendered = renderMarkdown([entry], { type: 'capability', sourceFile: 'capabilities.json' });
|
||||
assert.ok(rendered.includes('\\|'), 'expected an escaped pipe (\\|) in the rendered output');
|
||||
assert.ok(
|
||||
!rendered.includes(''),
|
||||
'expected the raw unescaped link-hijack payload to NOT appear verbatim',
|
||||
);
|
||||
assert.ok(rendered.includes('\\['), 'expected an escaped [ (\\[), proving the hijack bracket was neutralized');
|
||||
});
|
||||
|
||||
test('name containing a link-hijack payload is escaped (no raw ](url) survives)', () => {
|
||||
const entry = validCapabilityEntry();
|
||||
entry.name = 'Evil] (https://evil.example) [';
|
||||
const rendered = renderMarkdown([entry], { type: 'capability', sourceFile: 'capabilities.json' });
|
||||
assert.ok(
|
||||
!rendered.includes('](https://evil.example)'),
|
||||
'expected the ] to be escaped, breaking the hijacked link destination pairing',
|
||||
);
|
||||
});
|
||||
|
||||
test('install containing an embedded ``` run gets a longer fence, keeping injected content inside the block', () => {
|
||||
const entry = validCapabilityEntry();
|
||||
entry.install = 'echo a\n```\n## FAKE\n```sh\nbad';
|
||||
const rendered = renderMarkdown([entry], { type: 'capability', sourceFile: 'capabilities.json' });
|
||||
|
||||
const openIdx = rendered.indexOf('````sh');
|
||||
assert.ok(openIdx !== -1, 'expected a 4-backtick opening fence (longer than the embedded 3-backtick run)');
|
||||
|
||||
const afterOpen = rendered.slice(openIdx + '````sh'.length);
|
||||
const closeIdx = afterOpen.indexOf('````');
|
||||
assert.ok(closeIdx !== -1, 'expected a matching 4-backtick closing fence');
|
||||
|
||||
const blockBody = afterOpen.slice(0, closeIdx);
|
||||
assert.ok(
|
||||
blockBody.includes('## FAKE'),
|
||||
'expected the injected "## FAKE" heading to remain INSIDE the fenced block, not escape it',
|
||||
);
|
||||
});
|
||||
|
||||
test('name/description with a raw newline: validateEntries rejects it, and if rendered anyway the newline collapses', () => {
|
||||
const nameEntry = validCapabilityEntry();
|
||||
nameEntry.name = 'Evil\nName';
|
||||
assert.equal(validateEntries([nameEntry], { type: 'capability' }).ok, false);
|
||||
|
||||
const descEntry = validCapabilityEntry();
|
||||
descEntry.description = 'Evil\nDescription';
|
||||
assert.equal(validateEntries([descEntry], { type: 'capability' }).ok, false);
|
||||
|
||||
// Defense in depth: renderMarkdown does not itself call validateEntries, so
|
||||
// confirm mdInline still collapses an embedded newline to a single space —
|
||||
// no raw newline lands inside a rendered table row.
|
||||
const rendered = renderMarkdown([nameEntry], { type: 'capability', sourceFile: 'capabilities.json' });
|
||||
const matchingRows = rendered.split('\n').filter((line) => line.startsWith('| [Evil'));
|
||||
assert.equal(matchingRows.length, 1, 'expected the newline-containing name to collapse into a single table row');
|
||||
assert.ok(matchingRows[0].includes('Evil Name'), `expected collapsed "Evil Name", got: ${matchingRows[0]}`);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── validateEntries: null/non-object element guard (F2) ──────────────────────
|
||||
|
||||
describe('validateEntries: null/non-object element guard (F2)', () => {
|
||||
test('a null entry fails without throwing', () => {
|
||||
let verdict;
|
||||
assert.doesNotThrow(() => {
|
||||
verdict = validateEntries([null], { type: 'capability' });
|
||||
});
|
||||
assert.equal(verdict.ok, false);
|
||||
assert.ok(verdict.errors.some((e) => e.field === '(entry)'));
|
||||
});
|
||||
|
||||
test('an undefined entry fails without throwing', () => {
|
||||
let verdict;
|
||||
assert.doesNotThrow(() => {
|
||||
verdict = validateEntries([undefined], { type: 'capability' });
|
||||
});
|
||||
assert.equal(verdict.ok, false);
|
||||
assert.ok(verdict.errors.some((e) => e.field === '(entry)'));
|
||||
});
|
||||
|
||||
test('primitive and array elements fail without throwing', () => {
|
||||
const verdict = validateEntries(['a string', [1, 2, 3], 42], { type: 'capability' });
|
||||
assert.equal(verdict.ok, false);
|
||||
assert.equal(verdict.errors.filter((e) => e.field === '(entry)').length, 3);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── renderMarkdown: eos registry (F4) ─────────────────────────────────────────
|
||||
|
||||
describe('renderMarkdown: eos registry (F4)', () => {
|
||||
test('renders the eos heading, the free-form dispatch text, protocol wording, and integration wording', () => {
|
||||
const rendered = renderMarkdown([validEosEntry()], { type: 'eos', sourceFile: 'eos.json' });
|
||||
assert.match(rendered, /# GSD EoS Registry/);
|
||||
assert.ok(rendered.includes('Supports nested background dispatch up to depth 3.'));
|
||||
assert.match(rendered, /protocol v1/);
|
||||
assert.match(rendered, /integration/);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── validateEntries: interactions guards (F4) ─────────────────────────────────
|
||||
|
||||
describe('validateEntries: interactions guards (F4)', () => {
|
||||
test('capability interactions.someUnknownKey fails at the qualified field', () => {
|
||||
const entry = validCapabilityEntry();
|
||||
entry.interactions.someUnknownKey = 'x';
|
||||
const verdict = validateEntries([entry], { type: 'capability' });
|
||||
assert.equal(verdict.ok, false);
|
||||
assert.ok(verdict.errors.some((e) => e.field === 'interactions.someUnknownKey'));
|
||||
});
|
||||
|
||||
test('eos interactions.someUnknownKey fails at the qualified field', () => {
|
||||
const entry = validEosEntry();
|
||||
entry.interactions.someUnknownKey = 'x';
|
||||
const verdict = validateEntries([entry], { type: 'eos' });
|
||||
assert.equal(verdict.ok, false);
|
||||
assert.ok(verdict.errors.some((e) => e.field === 'interactions.someUnknownKey'));
|
||||
});
|
||||
|
||||
test('interactions.configKeys as a non-array string fails', () => {
|
||||
const entry = validCapabilityEntry();
|
||||
entry.interactions.configKeys = 'nope';
|
||||
const verdict = validateEntries([entry], { type: 'capability' });
|
||||
assert.equal(verdict.ok, false);
|
||||
assert.ok(verdict.errors.some((e) => e.field === 'interactions.configKeys'));
|
||||
});
|
||||
|
||||
test('interactions.configKeys with non-string elements fails', () => {
|
||||
const entry = validCapabilityEntry();
|
||||
entry.interactions.configKeys = [123];
|
||||
const verdict = validateEntries([entry], { type: 'capability' });
|
||||
assert.equal(verdict.ok, false);
|
||||
assert.ok(verdict.errors.some((e) => e.field === 'interactions.configKeys'));
|
||||
});
|
||||
|
||||
test('eos interactions.axes as a non-object string fails', () => {
|
||||
const entry = validEosEntry();
|
||||
entry.interactions.axes = 'nope';
|
||||
const verdict = validateEntries([entry], { type: 'eos' });
|
||||
assert.equal(verdict.ok, false);
|
||||
assert.ok(verdict.errors.some((e) => e.field === 'interactions.axes'));
|
||||
});
|
||||
});
|
||||
|
||||
// ─── validateEntries: new hardening checks (length caps, tightened regexes) ────
|
||||
|
||||
describe('validateEntries: description length cap (max 1000)', () => {
|
||||
test('999 chars (limit-1) passes the cap', () => {
|
||||
const entry = validCapabilityEntry();
|
||||
entry.description = 'x'.repeat(999);
|
||||
const verdict = validateEntries([entry], { type: 'capability' });
|
||||
assert.ok(!verdict.errors.some((e) => e.field === 'description' && /exceeds max length/.test(e.reason)));
|
||||
});
|
||||
|
||||
test('1000 chars (limit) passes the cap', () => {
|
||||
const entry = validCapabilityEntry();
|
||||
entry.description = 'x'.repeat(1000);
|
||||
const verdict = validateEntries([entry], { type: 'capability' });
|
||||
assert.ok(!verdict.errors.some((e) => e.field === 'description' && /exceeds max length/.test(e.reason)));
|
||||
});
|
||||
|
||||
test('1001 chars (limit+1) fails the cap', () => {
|
||||
const entry = validCapabilityEntry();
|
||||
entry.description = 'x'.repeat(1001);
|
||||
const verdict = validateEntries([entry], { type: 'capability' });
|
||||
assert.equal(verdict.ok, false);
|
||||
assert.ok(verdict.errors.some((e) => e.field === 'description' && /exceeds max length 1000/.test(e.reason)));
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateEntries: entry-count cap (max 2000)', () => {
|
||||
function makeEntries(n) {
|
||||
return Array.from({ length: n }, (_, i) => ({
|
||||
...validCapabilityEntry(),
|
||||
id: `cap-${i}`,
|
||||
repo: `octocat/cap-${i}`,
|
||||
discussion: `https://github.com/octocat/cap-${i}/discussions/1`,
|
||||
}));
|
||||
}
|
||||
|
||||
test('1999 entries (limit-1) does not trip the cap', () => {
|
||||
const verdict = validateEntries(makeEntries(1999), { type: 'capability' });
|
||||
assert.ok(!verdict.errors.some((e) => e.field === '(root)'));
|
||||
});
|
||||
|
||||
test('2000 entries (limit) does not trip the cap', () => {
|
||||
const verdict = validateEntries(makeEntries(2000), { type: 'capability' });
|
||||
assert.ok(!verdict.errors.some((e) => e.field === '(root)'));
|
||||
});
|
||||
|
||||
test('2001 entries (limit+1) trips the cap with a single root error', () => {
|
||||
const verdict = validateEntries(makeEntries(2001), { type: 'capability' });
|
||||
assert.equal(verdict.ok, false);
|
||||
assert.equal(verdict.errors.length, 1);
|
||||
assert.equal(verdict.errors[0].field, '(root)');
|
||||
assert.match(verdict.errors[0].reason, /max 2000/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateEntries: tightened discussion/license regexes', () => {
|
||||
test('discussion URL containing an injection char ([) fails the tightened regex', () => {
|
||||
const entry = validCapabilityEntry();
|
||||
entry.discussion = 'https://github.com/a[b/c/discussions/1';
|
||||
const verdict = validateEntries([entry], { type: 'capability' });
|
||||
assert.equal(verdict.ok, false);
|
||||
assert.ok(verdict.errors.some((e) => e.field === 'discussion'));
|
||||
});
|
||||
|
||||
test('license containing a newline fails the tightened regex', () => {
|
||||
const entry = validCapabilityEntry();
|
||||
entry.license = 'MIT\nEVIL';
|
||||
const verdict = validateEntries([entry], { type: 'capability' });
|
||||
assert.equal(verdict.ok, false);
|
||||
assert.ok(verdict.errors.some((e) => e.field === 'license'));
|
||||
});
|
||||
|
||||
test('a compound SPDX license ("MIT OR Apache-2.0") still passes', () => {
|
||||
const entry = validCapabilityEntry();
|
||||
entry.license = 'MIT OR Apache-2.0';
|
||||
const verdict = validateEntries([entry], { type: 'capability' });
|
||||
assert.ok(!verdict.errors.some((e) => e.field === 'license'));
|
||||
});
|
||||
});
|
||||
@@ -396,6 +396,88 @@ describe('roadmap-parser: getMilestoneInfo', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// ─── getMilestoneInfo — #2135 milestone_name clobber ──────────────────────────
|
||||
// The `##` heading regex was unanchored (no `^`/`m`), so it matched a `##`
|
||||
// quoted mid-line inside a Milestones bullet and captured a delimiter-led
|
||||
// fragment into `milestone_name`. The fix: consult the 🚧 name-bearing marker
|
||||
// FIRST, anchor the `##` regex to line start, and strip a leading delimiter.
|
||||
|
||||
describe('roadmap-parser: getMilestoneInfo #2135 — milestone_name clobber', () => {
|
||||
let tmpDir;
|
||||
|
||||
beforeEach(() => { tmpDir = createTempProject(); });
|
||||
afterEach(() => { cleanup(tmpDir); });
|
||||
|
||||
test('case A: 🚧 bullet quoting a nameless ## heading in backticks', () => {
|
||||
writeState(tmpDir, { gsd_state_version: '1.0', milestone: 'v1.8' });
|
||||
writeRoadmap(tmpDir, [
|
||||
'# Roadmap',
|
||||
'',
|
||||
'## Milestones',
|
||||
'',
|
||||
'- 🚧 **v1.8 user session cleanup** — Phases 36-41 — see `## v1.8 — Active Milestone` below',
|
||||
'',
|
||||
'## v1.8 — Active Milestone',
|
||||
'',
|
||||
'### Phase 36: Something',
|
||||
].join('\n'));
|
||||
const info = getMilestoneInfo(tmpDir);
|
||||
assert.strictEqual(info.version, 'v1.8');
|
||||
assert.strictEqual(info.name, 'user session cleanup');
|
||||
});
|
||||
|
||||
test('case B: nameless ## heading + 🚧 marker carries the real name', () => {
|
||||
writeState(tmpDir, { gsd_state_version: '1.0', milestone: 'v1.9' });
|
||||
writeRoadmap(tmpDir, [
|
||||
'## v1.9 — Active Milestone',
|
||||
'',
|
||||
'### 🚧 v1.9 — Falsifiability',
|
||||
'',
|
||||
'### Phase 1: Hypothesis',
|
||||
].join('\n'));
|
||||
const info = getMilestoneInfo(tmpDir);
|
||||
assert.strictEqual(info.version, 'v1.9');
|
||||
assert.strictEqual(info.name, 'Falsifiability');
|
||||
});
|
||||
|
||||
test('case C: canonical ## vX.Y: Name (no regression)', () => {
|
||||
writeState(tmpDir, { gsd_state_version: '1.0', milestone: 'v2.0' });
|
||||
writeRoadmap(tmpDir, '## v2.0: The Big Launch\n### Phase 1: Setup\n');
|
||||
const info = getMilestoneInfo(tmpDir);
|
||||
assert.strictEqual(info.version, 'v2.0');
|
||||
assert.strictEqual(info.name, 'The Big Launch');
|
||||
});
|
||||
|
||||
test('case D: canonical ## vX.Y — Name (em-dash delimiter stripped)', () => {
|
||||
writeState(tmpDir, { gsd_state_version: '1.0', milestone: 'v2.5' });
|
||||
writeRoadmap(tmpDir, '## v2.5 — Galaxy Release\n### Phase 1: Start\n');
|
||||
const info = getMilestoneInfo(tmpDir);
|
||||
assert.strictEqual(info.version, 'v2.5');
|
||||
assert.strictEqual(info.name, 'Galaxy Release');
|
||||
});
|
||||
|
||||
test('case E: 🚧 bullet only, no ## heading (no regression)', () => {
|
||||
writeState(tmpDir, { gsd_state_version: '1.0', milestone: 'v1.5' });
|
||||
writeRoadmap(tmpDir, 'Some intro text.\n\n- 🚧 **v1.5 Quick Fix** — minor\n');
|
||||
const info = getMilestoneInfo(tmpDir);
|
||||
assert.strictEqual(info.version, 'v1.5');
|
||||
assert.strictEqual(info.name, 'Quick Fix');
|
||||
});
|
||||
|
||||
test('anchored regex never matches a ## heading quoted inside backticks mid-line', () => {
|
||||
writeState(tmpDir, { gsd_state_version: '1.0', milestone: 'v3.0' });
|
||||
writeRoadmap(tmpDir, [
|
||||
'# Roadmap',
|
||||
'',
|
||||
'See `## v3.0 — Active Milestone` referenced here.',
|
||||
'',
|
||||
'## v3.0: Real Name',
|
||||
].join('\n'));
|
||||
const info = getMilestoneInfo(tmpDir);
|
||||
assert.strictEqual(info.name, 'Real Name');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── getMilestonePhaseFilter ──────────────────────────────────────────────────
|
||||
|
||||
describe('roadmap-parser: getMilestonePhaseFilter', () => {
|
||||
|
||||
@@ -197,15 +197,26 @@ describe('resolveRuntimeConfigIntent — fresh object each call', () => {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('ALLOWED_CONFIG_RUNTIMES completeness', () => {
|
||||
test('ALLOWED_CONFIG_RUNTIMES equals the registry runtimes that declare an installSurface', () => {
|
||||
test('ALLOWED_CONFIG_RUNTIMES equals the registry runtimes that declare a real (non-"none") installSurface', () => {
|
||||
// #2103: installSurface 'none' means "no CLI install surface at all"
|
||||
// (e.g. vscode — Marketplace/VSIX-distributed, never CLI-installed), so
|
||||
// it is excluded from the config-adapter runtime set by definition — this
|
||||
// mirrors the exclusion already baked into the production
|
||||
// ALLOWED_CONFIG_RUNTIMES filter (src/runtime-config-adapter-registry.cts).
|
||||
const descriptorAllowed = new Set(
|
||||
Object.entries(registry.runtimes)
|
||||
.filter(([, cap]) => cap && cap.runtime && typeof cap.runtime.installSurface === 'string')
|
||||
.filter(([, cap]) => cap && cap.runtime && typeof cap.runtime.installSurface === 'string' && cap.runtime.installSurface !== 'none')
|
||||
.map(([id]) => id),
|
||||
);
|
||||
assert.deepStrictEqual(new Set(ALLOWED_CONFIG_RUNTIMES), descriptorAllowed);
|
||||
});
|
||||
|
||||
test('#2103: vscode declares installSurface "none" and is registered but intentionally excluded from ALLOWED_CONFIG_RUNTIMES', () => {
|
||||
assert.strictEqual(registry.runtimes.vscode.runtime.installSurface, 'none');
|
||||
assert.ok(!ALLOWED_CONFIG_RUNTIMES.has('vscode'),
|
||||
'vscode must not be a config-adapter runtime — it has no CLI install surface');
|
||||
});
|
||||
|
||||
test('every member of ALLOWED_CONFIG_RUNTIMES resolves without throwing', () => {
|
||||
for (const runtime of ALLOWED_CONFIG_RUNTIMES) {
|
||||
assert.doesNotThrow(() => resolveRuntimeConfigIntent(runtime), `${runtime} should resolve without throwing`);
|
||||
@@ -225,9 +236,11 @@ describe('INSTALL_SURFACES export', () => {
|
||||
'cline-rules',
|
||||
'cursor-hooks-json',
|
||||
'profile-marker-only',
|
||||
// 'none' added #2103 — vscode has no CLI install surface at all.
|
||||
'none',
|
||||
]);
|
||||
|
||||
test('INSTALL_SURFACES contains exactly the 6 surface strings', () => {
|
||||
test('INSTALL_SURFACES contains exactly the 7 surface strings', () => {
|
||||
assert.deepStrictEqual(new Set(INSTALL_SURFACES), EXPECTED_SURFACES);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,9 +14,17 @@ const registry = require('../gsd-core/bin/lib/capability-registry.cjs');
|
||||
const EXPECTED_FLAGS = [
|
||||
'isOpencode', 'isKilo', 'isCodex', 'isCopilot', 'isAntigravity',
|
||||
'isCursor', 'isWindsurf', 'isAugment', 'isTrae', 'isQwen', 'isHermes',
|
||||
'isCodebuddy', 'isCline', 'isKimi', 'isZcode',
|
||||
'isCodebuddy', 'isCline', 'isKimi', 'isZcode', 'isPi',
|
||||
];
|
||||
|
||||
// #2103: registry runtimes that are NEVER CLI-installed via bin/install.js
|
||||
// (Marketplace/VSIX-distributed, no --<rt> flag, not in allRuntimes) — these
|
||||
// have no runtimeFlags entry by design, not by drift. vscode is the first
|
||||
// (and, today, only) member: it enters `registry.runtimes` (role:runtime, for
|
||||
// validator/host-integration coverage) but never enters bin/install.js's
|
||||
// allRuntimes, so it must not be required to have an isVscode flag here.
|
||||
const NON_INSTALLABLE_RUNTIMES = new Set(['vscode']);
|
||||
|
||||
test('runtimeFlags: every known non-claude runtime sets exactly its own flag true', () => {
|
||||
const ids = EXPECTED_FLAGS.map((f) => f.slice(2).toLowerCase());
|
||||
for (const id of ids) {
|
||||
@@ -35,20 +43,36 @@ test('runtimeFlags: claude / unknown / empty → all flags false (fail-closed)',
|
||||
}
|
||||
});
|
||||
|
||||
test('runtimeFlags: all 15 flags present + boolean + the object is frozen', () => {
|
||||
test('runtimeFlags: all 16 flags present + boolean + the object is frozen', () => {
|
||||
const flags = runtimeFlags('opencode');
|
||||
for (const f of EXPECTED_FLAGS) {
|
||||
assert.strictEqual(typeof flags[f], 'boolean', `${f} must be boolean`);
|
||||
}
|
||||
assert.deepStrictEqual(Object.keys(flags).sort(), [...EXPECTED_FLAGS].sort(), 'exactly the 15 flags');
|
||||
assert.deepStrictEqual(Object.keys(flags).sort(), [...EXPECTED_FLAGS].sort(), 'exactly the 16 flags');
|
||||
assert.ok(Object.isFrozen(flags), 'flags object must be frozen');
|
||||
});
|
||||
|
||||
test('runtimeFlags drift guard: covers every registry runtime except claude', () => {
|
||||
test('runtimeFlags drift guard: covers every registry runtime except claude and the non-installable set', () => {
|
||||
// Adding a registry runtime that is not claude must get a flag or be added to
|
||||
// RUNTIME_FLAG_IDS — pin the set so a new runtime forces a deliberate update.
|
||||
const registryNonClaude = Object.keys(registry.runtimes).filter((r) => r !== 'claude').sort();
|
||||
// NON_INSTALLABLE_RUNTIMES (#2103) is filtered out first: a runtime that is
|
||||
// never CLI-installed (e.g. vscode — Marketplace/VSIX only) has no --<rt>
|
||||
// flag by design and must not trip this guard.
|
||||
const registryNonClaude = Object.keys(registry.runtimes)
|
||||
.filter((r) => r !== 'claude' && !NON_INSTALLABLE_RUNTIMES.has(r))
|
||||
.sort();
|
||||
const flagIds = EXPECTED_FLAGS.map((f) => f.slice(2).toLowerCase()).sort();
|
||||
const missing = registryNonClaude.filter((r) => !flagIds.includes(r));
|
||||
assert.deepEqual(missing, [], `registry runtimes missing a runtimeFlags entry: ${missing.join(', ')} — add to RUNTIME_FLAG_IDS`);
|
||||
});
|
||||
|
||||
test('#2103: vscode is registered but intentionally excluded from runtimeFlags (Marketplace-distributed, never CLI-installed)', () => {
|
||||
assert.ok(registry.runtimes.vscode, 'vscode must be present in the registry (role:runtime)');
|
||||
assert.ok(NON_INSTALLABLE_RUNTIMES.has('vscode'));
|
||||
const flags = runtimeFlags('vscode');
|
||||
for (const f of EXPECTED_FLAGS) {
|
||||
assert.strictEqual(flags[f], false, `runtime 'vscode': ${f} must be false (no isVscode flag exists)`);
|
||||
}
|
||||
assert.deepStrictEqual(Object.keys(flags).sort(), [...EXPECTED_FLAGS].sort(),
|
||||
'runtimeFlags(\'vscode\') must NOT introduce a new isVscode key — still exactly the 16 flags');
|
||||
});
|
||||
|
||||
90
tests/security-dead-exports.regression.test.cjs
Normal file
90
tests/security-dead-exports.regression.test.cjs
Normal file
@@ -0,0 +1,90 @@
|
||||
/**
|
||||
* Regression test for #2198 — advertised base64/entropy/homoglyph scanning
|
||||
* never runs live. `scanEntropyAnomalies` + `shannonEntropy` were dead exports
|
||||
* (zero callers outside their own unit tests). The live hooks inline their
|
||||
* own pattern subsets "for hook independence" and never call these functions.
|
||||
*
|
||||
* This test asserts the chosen contract: the dead export was removed and the
|
||||
* docs no longer over-claim entropy analysis as a live MUST.
|
||||
*
|
||||
* Contract: `scanForInjection` is retained — it serves as the CI codebase
|
||||
* scanner engine (tests/prompt-injection-scan.security.test.cjs). It is NOT
|
||||
* called from live hooks; hooks inline their own patterns.
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
const { describe, test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const PROJECT_ROOT = path.join(__dirname, '..');
|
||||
|
||||
describe('#2198 regression: dead scan exports removed, docs corrected', () => {
|
||||
test('scanEntropyAnomalies is no longer exported from security.cjs', () => {
|
||||
const security = require('../gsd-core/bin/lib/security.cjs');
|
||||
assert.equal(
|
||||
security.scanEntropyAnomalies,
|
||||
undefined,
|
||||
'scanEntropyAnomalies should have been removed as a dead export (#2198)'
|
||||
);
|
||||
});
|
||||
|
||||
test('shannonEntropy is not accessible from the security module', () => {
|
||||
const security = require('../gsd-core/bin/lib/security.cjs');
|
||||
assert.equal(
|
||||
security.shannonEntropy,
|
||||
undefined,
|
||||
'shannonEntropy was the private helper for the removed scanEntropyAnomalies'
|
||||
);
|
||||
});
|
||||
|
||||
test('scanForInjection is retained (CI codebase scanner uses it)', () => {
|
||||
const security = require('../gsd-core/bin/lib/security.cjs');
|
||||
assert.equal(
|
||||
typeof security.scanForInjection,
|
||||
'function',
|
||||
'scanForInjection is retained: it serves as the CI codebase scanner engine'
|
||||
);
|
||||
});
|
||||
|
||||
test('FEATURES.md does not over-claim entropy analysis as a live MUST', () => {
|
||||
const features = fs.readFileSync(
|
||||
path.join(PROJECT_ROOT, 'docs', 'FEATURES.md'),
|
||||
'utf-8'
|
||||
);
|
||||
assert.ok(
|
||||
!features.includes('REQ-SCAN-INJ-03: Scanner MUST apply entropy analysis'),
|
||||
'REQ-SCAN-INJ-03 should not claim entropy analysis runs as a live MUST — ' +
|
||||
'the implementation was dead code (#2198)'
|
||||
);
|
||||
});
|
||||
|
||||
test('FEATURES.md documents that base64-decode is CI-only, not live', () => {
|
||||
const features = fs.readFileSync(
|
||||
path.join(PROJECT_ROOT, 'docs', 'FEATURES.md'),
|
||||
'utf-8'
|
||||
);
|
||||
assert.ok(
|
||||
features.includes('CI-time control'),
|
||||
'FEATURES.md should note base64-decode is a CI-time control, not a live hook (#2198)'
|
||||
);
|
||||
});
|
||||
|
||||
test('live hooks inline patterns independently (do not import security.cjs)', () => {
|
||||
const hookFiles = [
|
||||
'hooks/gsd-prompt-guard.js',
|
||||
'hooks/gsd-read-injection-scanner.js',
|
||||
];
|
||||
|
||||
for (const relPath of hookFiles) {
|
||||
const fullPath = path.join(PROJECT_ROOT, relPath);
|
||||
const source = fs.readFileSync(fullPath, 'utf-8');
|
||||
assert.ok(
|
||||
!source.match(/require\s*\(\s*['"][^'"]*security\.(cjs|js)['"]\s*\)/) &&
|
||||
!source.match(/import\s+.*from\s+['"][^'"]*security\.(cjs|js)['"]\s*;?/),
|
||||
`${relPath} must not require/import security.cjs — hooks inline patterns for independence`
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -21,7 +21,6 @@ const {
|
||||
validateFieldName,
|
||||
validateShellArg,
|
||||
validatePromptStructure,
|
||||
scanEntropyAnomalies,
|
||||
} = require('../gsd-core/bin/lib/security.cjs');
|
||||
|
||||
// ─── Path Traversal Prevention ──────────────────────────────────────────────
|
||||
@@ -765,79 +764,8 @@ describe('validatePromptStructure', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Layer 4: Paragraph-Level Entropy Anomaly Detection ─────────────────────
|
||||
|
||||
describe('scanEntropyAnomalies', () => {
|
||||
test('is exported from security.cjs', () => {
|
||||
assert.equal(typeof scanEntropyAnomalies, 'function');
|
||||
});
|
||||
|
||||
test('returns { clean, findings } shape', () => {
|
||||
const result = scanEntropyAnomalies('Normal text here.');
|
||||
assert.ok(typeof result.clean === 'boolean');
|
||||
assert.ok(Array.isArray(result.findings));
|
||||
});
|
||||
|
||||
test('clean natural language text passes', () => {
|
||||
const text = [
|
||||
'Build an authentication system with JWT tokens.',
|
||||
'',
|
||||
'The system should support login, logout, and token refresh.',
|
||||
].join('\n');
|
||||
const result = scanEntropyAnomalies(text);
|
||||
assert.ok(result.clean, `Expected clean but got: ${result.findings.join(', ')}`);
|
||||
});
|
||||
|
||||
test('detects high-entropy paragraph (random-character content)', () => {
|
||||
// A string cycling through 90 distinct chars has entropy ~6.4 bits/char, well above 5.5 threshold
|
||||
const highEntropyPara = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=!@#$%^&*()_-[]{}|;:,.<>?ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqr';
|
||||
const result = scanEntropyAnomalies(highEntropyPara);
|
||||
assert.ok(!result.clean, 'should detect high-entropy paragraph');
|
||||
assert.ok(
|
||||
result.findings.some(f => f.includes('High-entropy paragraph')),
|
||||
'finding should mention high-entropy paragraph'
|
||||
);
|
||||
});
|
||||
|
||||
test('finding includes entropy value in bits/char', () => {
|
||||
const highEntropyPara = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=!@#$%^&*()_-[]{}|;:,.<>?ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqr';
|
||||
const result = scanEntropyAnomalies(highEntropyPara);
|
||||
assert.ok(result.findings.some(f => f.includes('bits/char')));
|
||||
});
|
||||
|
||||
test('skips paragraphs shorter than or equal to 50 chars', () => {
|
||||
// Even a high-entropy short paragraph should not be flagged
|
||||
const shortPara = 'SGVsbG8gV29ybGQ='; // 16 chars — under 50
|
||||
const result = scanEntropyAnomalies(shortPara);
|
||||
assert.ok(result.clean, 'short paragraphs should be skipped');
|
||||
});
|
||||
|
||||
test('handles empty text gracefully', () => {
|
||||
const result = scanEntropyAnomalies('');
|
||||
assert.ok(result.clean);
|
||||
assert.equal(result.findings.length, 0);
|
||||
});
|
||||
|
||||
test('handles null gracefully', () => {
|
||||
const result = scanEntropyAnomalies(null);
|
||||
assert.ok(result.clean);
|
||||
assert.equal(result.findings.length, 0);
|
||||
});
|
||||
|
||||
test('multiple paragraphs — flags only high-entropy ones', () => {
|
||||
const highEntropyPara = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=!@#$%^&*()_-[]{}|;:,.<>?ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqr';
|
||||
const text = [
|
||||
'This is a perfectly normal English sentence describing a feature.',
|
||||
'',
|
||||
highEntropyPara,
|
||||
'',
|
||||
'Another clean sentence about the authentication requirements.',
|
||||
].join('\n');
|
||||
const result = scanEntropyAnomalies(text);
|
||||
assert.ok(!result.clean);
|
||||
assert.equal(result.findings.length, 1, 'only 1 high-entropy paragraph should be flagged');
|
||||
});
|
||||
});
|
||||
// NOTE (#2198): scanEntropyAnomalies test block removed — the function was a
|
||||
// dead export (zero production callers) and has been deleted from security.cts.
|
||||
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -14,6 +14,8 @@ const {
|
||||
platformWriteSync,
|
||||
platformReadSync,
|
||||
platformEnsureDir,
|
||||
dispatchGsdCommand,
|
||||
resolveGsdToolsPath,
|
||||
} = require(path.join(__dirname, '..', 'gsd-core', 'bin', 'lib', 'shell-command-projection.cjs'));
|
||||
|
||||
const { createTempGitProject, createTempDir, cleanup } = require('./helpers.cjs');
|
||||
@@ -99,6 +101,78 @@ describe('execTool', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// ─── dispatchGsdCommand (#2102 Stage 2 — subprocess-shim dispatch to gsd-tools.cjs) ──
|
||||
//
|
||||
// The command-routing hub (`createHub()`) has no fully-populated factory
|
||||
// anywhere in the tree — every caller builds a single-family hub — so the
|
||||
// only dispatch path covering the FULL family/subcommand surface is the
|
||||
// gsd-tools.cjs CLI itself. This is the shared helper pi/gsd.cjs and the
|
||||
// companion MCP server both dispatch through.
|
||||
|
||||
describe('dispatchGsdCommand', () => {
|
||||
let tmpDir;
|
||||
|
||||
beforeEach(() => { tmpDir = createTempDir(); });
|
||||
afterEach(() => { cleanup(tmpDir); });
|
||||
|
||||
test('resolveGsdToolsPath resolves to the real gsd-tools.cjs on disk', () => {
|
||||
const toolsPath = resolveGsdToolsPath();
|
||||
assert.ok(fs.existsSync(toolsPath), `expected gsd-tools.cjs to exist at ${toolsPath}`);
|
||||
assert.equal(path.basename(toolsPath), 'gsd-tools.cjs');
|
||||
});
|
||||
|
||||
test('a valid read-only family/subcommand dispatches for real and returns ok:true + non-empty stdout', () => {
|
||||
const result = dispatchGsdCommand({ family: 'progress', subcommand: 'json', cwd: tmpDir });
|
||||
assert.equal(result.ok, true, `expected ok:true, got: ${JSON.stringify(result)}`);
|
||||
assert.equal(typeof result.stdout, 'string');
|
||||
assert.ok(result.stdout.length > 0, 'stdout must be non-empty');
|
||||
const parsed = JSON.parse(result.stdout);
|
||||
assert.equal(typeof parsed.percent, 'number', 'the real progress command ran (proves the engine was reached)');
|
||||
assert.equal(result.code, 0);
|
||||
assert.equal(result.timedOut, false);
|
||||
});
|
||||
|
||||
test('an unknown family returns ok:false without throwing', () => {
|
||||
assert.doesNotThrow(() => {
|
||||
const result = dispatchGsdCommand({ family: 'no-such-family-8675309', cwd: tmpDir });
|
||||
assert.equal(result.ok, false);
|
||||
assert.notEqual(result.code, 0);
|
||||
assert.equal(typeof result.stderr, 'string');
|
||||
assert.ok(result.stderr.length > 0);
|
||||
// --json-errors gives a structured, parseable error envelope.
|
||||
const parsedErr = JSON.parse(result.stderr);
|
||||
assert.equal(parsedErr.ok, false);
|
||||
});
|
||||
});
|
||||
|
||||
test('a missing/bogus gsd-tools.cjs path degrades to ok:false without throwing', () => {
|
||||
assert.doesNotThrow(() => {
|
||||
const result = dispatchGsdCommand({
|
||||
family: 'progress',
|
||||
cwd: tmpDir,
|
||||
gsdToolsPath: path.join(tmpDir, 'definitely-not-a-real-gsd-tools-8675309.cjs'),
|
||||
});
|
||||
assert.equal(result.ok, false);
|
||||
assert.equal(result.timedOut, false);
|
||||
});
|
||||
});
|
||||
|
||||
test('a missing/empty "family" is rejected locally without spawning a subprocess', () => {
|
||||
const result = dispatchGsdCommand({ cwd: tmpDir });
|
||||
assert.equal(result.ok, false);
|
||||
assert.equal(result.code, null);
|
||||
assert.match(result.stderr, /requires a non-empty string "family"/);
|
||||
});
|
||||
|
||||
test('a wall-clock timeout is reported via timedOut:true, ok:false — never throws', () => {
|
||||
assert.doesNotThrow(() => {
|
||||
const result = dispatchGsdCommand({ family: 'progress', subcommand: 'json', cwd: tmpDir, timeout: 1 });
|
||||
assert.equal(result.ok, false);
|
||||
assert.equal(result.timedOut, true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ─── probeTty ────────────────────────────────────────────────────────────────
|
||||
|
||||
describe('probeTty', () => {
|
||||
|
||||
@@ -468,16 +468,21 @@ describe('stale-bake-guard parity with bin/install.js bake paths', () => {
|
||||
|
||||
test('readGsdEffectiveModelOverrides resolves codex + opencode overrides from .planning/config.json', () => {
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-parity-'));
|
||||
// #2152: sandbox HOME so the real ~/.gsd/defaults.json cannot bleed its global
|
||||
// model_overrides into this project-only assertion (hermeticity). Mirrors the
|
||||
// sibling subtests that pass a homedir option to warnIfStaleBake.
|
||||
const sandboxHome = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-parity-home-'));
|
||||
try {
|
||||
fs.mkdirSync(path.join(tmp, '.planning'), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(tmp, '.planning', 'config.json'),
|
||||
JSON.stringify({ model_overrides: { 'gsd-executor': 'opencode-go/flash', 'gsd-planner': 'openai/gpt-5' } }),
|
||||
);
|
||||
const resolved = install.readGsdEffectiveModelOverrides(tmp);
|
||||
const resolved = install.readGsdEffectiveModelOverrides(tmp, { homedir: () => sandboxHome });
|
||||
assert.deepEqual(resolved, { 'gsd-executor': 'opencode-go/flash', 'gsd-planner': 'openai/gpt-5' });
|
||||
} finally {
|
||||
cleanup(tmp);
|
||||
cleanup(sandboxHome);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -25,6 +25,7 @@ const { stateExtractField } = require('../gsd-core/bin/lib/state-document.cjs');
|
||||
|
||||
const fixedClock = Object.freeze({
|
||||
today: () => '2026-06-29',
|
||||
localToday: () => '2026-06-29',
|
||||
nowIso: () => '2026-06-29T12:00:00.000Z',
|
||||
});
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ const { stateExtractField } = require('../gsd-core/bin/lib/state-document.cjs');
|
||||
|
||||
const fixedClock = Object.freeze({
|
||||
today: () => '2026-06-27',
|
||||
localToday: () => '2026-06-27',
|
||||
nowIso: () => '2026-06-27T12:00:00.000Z',
|
||||
});
|
||||
|
||||
|
||||
@@ -1295,6 +1295,85 @@ describe('cmdStateUpdateProgress (state update-progress)', () => {
|
||||
assert.ok(output.reason !== undefined, 'should have a reason');
|
||||
});
|
||||
|
||||
// ── #2177: frontmatter `progress:` key must not shadow the body Progress: line ──
|
||||
|
||||
test('#2177 frontmatter progress: key is not matched — body Progress: line is the target', () => {
|
||||
// A STATE.md carrying YAML frontmatter (which writeStateMd adds to every
|
||||
// STATE.md). The lowercase `progress:` key used to be matched first by the
|
||||
// case-insensitive pattern, mangling the frontmatter and leaving the body
|
||||
// line stale.
|
||||
fs.writeFileSync(
|
||||
path.join(tmpDir, '.planning', 'STATE.md'),
|
||||
[
|
||||
'---',
|
||||
'gsd_state_version: 1.0',
|
||||
'status: executing',
|
||||
'progress:',
|
||||
' total_phases: 1',
|
||||
' completed_phases: 0',
|
||||
' total_plans: 2',
|
||||
' completed_plans: 1',
|
||||
' percent: 20',
|
||||
'---',
|
||||
'',
|
||||
'# Project State',
|
||||
'',
|
||||
'Progress: [██░░░░░░░░] 20% (1/2 plans complete)',
|
||||
'',
|
||||
].join('\n')
|
||||
);
|
||||
// 1 of 2 plans complete → 50%.
|
||||
const phaseDir = path.join(tmpDir, '.planning', 'phases', '01');
|
||||
fs.mkdirSync(phaseDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(phaseDir, '01-01-PLAN.md'), '# Plan\n');
|
||||
fs.writeFileSync(path.join(phaseDir, '01-01-SUMMARY.md'), '# Summary\n');
|
||||
fs.writeFileSync(path.join(phaseDir, '01-02-PLAN.md'), '# Plan\n');
|
||||
|
||||
const result = runGsdTools('state update-progress', tmpDir);
|
||||
assert.ok(result.success, `Command failed: ${result.error}`);
|
||||
const out = JSON.parse(result.output);
|
||||
assert.strictEqual(out.updated, true);
|
||||
assert.strictEqual(out.percent, 50);
|
||||
|
||||
const updated = fs.readFileSync(path.join(tmpDir, '.planning', 'STATE.md'), 'utf-8');
|
||||
// The body line advanced to 50% AND its descriptive suffix survived.
|
||||
assert.ok(updated.includes('[█████░░░░░] 50% (1/2 plans complete)'),
|
||||
'body Progress line must update to 50% with suffix preserved');
|
||||
// The frontmatter block is intact (not mangled by the old \s*-crosses-newline match).
|
||||
assert.ok(updated.includes('total_phases: 1'), 'frontmatter total_phases key must survive');
|
||||
assert.ok(updated.includes('percent:'), 'frontmatter percent key must survive');
|
||||
});
|
||||
|
||||
test('#2177 descriptive suffix after the machine segment is preserved', () => {
|
||||
fs.writeFileSync(
|
||||
path.join(tmpDir, '.planning', 'STATE.md'),
|
||||
'# Project State\n\n**Progress:** [█████░░░░░] 50% (2/4 plans done; blocked on API keys)\n'
|
||||
);
|
||||
// 1 of 1 plan complete → 100%.
|
||||
const phaseDir = path.join(tmpDir, '.planning', 'phases', '01');
|
||||
fs.mkdirSync(phaseDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(phaseDir, '01-01-PLAN.md'), '# Plan\n');
|
||||
fs.writeFileSync(path.join(phaseDir, '01-01-SUMMARY.md'), '# Summary\n');
|
||||
|
||||
const result = runGsdTools('state update-progress', tmpDir);
|
||||
assert.ok(result.success);
|
||||
const updated = fs.readFileSync(path.join(tmpDir, '.planning', 'STATE.md'), 'utf-8');
|
||||
assert.ok(/\[██████████\] 100% \(2\/4 plans done; blocked on API keys\)/.test(updated),
|
||||
'the machine segment updates to 100% while the suffix is preserved verbatim');
|
||||
});
|
||||
|
||||
test('#2177 no body Progress: line → updated:false even if frontmatter has a progress: key', () => {
|
||||
fs.writeFileSync(
|
||||
path.join(tmpDir, '.planning', 'STATE.md'),
|
||||
['---', 'progress:', ' percent: 0', '---', '', '# Project State', '', '**Status:** Active', ''].join('\n')
|
||||
);
|
||||
const result = runGsdTools('state update-progress', tmpDir);
|
||||
assert.ok(result.success);
|
||||
const out = JSON.parse(result.output);
|
||||
assert.strictEqual(out.updated, false,
|
||||
'a frontmatter progress: key with no body Progress: line must not report a false success');
|
||||
});
|
||||
|
||||
test('returns error when STATE.md missing', () => {
|
||||
const result = runGsdTools('state update-progress', tmpDir);
|
||||
assert.ok(result.success, `Command should exit 0: ${result.error}`);
|
||||
|
||||
41
tests/surface-md-paths.regression.test.cjs
Normal file
41
tests/surface-md-paths.regression.test.cjs
Normal file
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* Regression test for #2116 — commands/gsd/surface.md had unresolvable bare
|
||||
* `require('gsd-core/...')` specifiers and a wrong-package reinstall hint
|
||||
* (`npm i -g gsd-core` instead of `@opengsd/gsd-core`).
|
||||
*
|
||||
* This test asserts the doc uses resolvable paths (derived from runtimeConfigDir)
|
||||
* and the correct scoped package name in the reinstall hint.
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
const { describe, test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const SURFACE_MD = path.join(__dirname, '..', 'commands', 'gsd', 'surface.md');
|
||||
|
||||
describe('#2116 regression: surface.md resolvable require + correct package', () => {
|
||||
const content = fs.readFileSync(SURFACE_MD, 'utf-8');
|
||||
|
||||
test('no bare require("gsd-core/...") specifiers', () => {
|
||||
assert.ok(
|
||||
!content.match(/require\s*\(\s*['"]gsd-core\//),
|
||||
'surface.md should not use bare require(\'gsd-core/...\') — use runtimeConfigDir-derived path (#2116)'
|
||||
);
|
||||
});
|
||||
|
||||
test('reinstall hint uses scoped package name', () => {
|
||||
assert.ok(
|
||||
content.includes('@opengsd/gsd-core'),
|
||||
'surface.md reinstall hint should reference @opengsd/gsd-core, not bare gsd-core (#2116)'
|
||||
);
|
||||
});
|
||||
|
||||
test('require examples use runtimeConfigDir-derived paths', () => {
|
||||
assert.ok(
|
||||
content.includes("require(runtimeConfigDir + '/gsd-core/bin/lib/capability-registry.cjs')"),
|
||||
'surface.md require examples should derive the path from runtimeConfigDir (#2116)'
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -80,4 +80,46 @@ describe('checkUiPresence', () => {
|
||||
assert.ok(result.tokens.includes('dashboard'));
|
||||
assert.ok(result.tokens.includes('form'));
|
||||
});
|
||||
|
||||
// ── #2150: the `**UI hint**: yes|no` metadata line must not false-positive ──
|
||||
|
||||
test('#2150 `**UI hint**: no` is authoritative non-frontend (no false positive)', () => {
|
||||
const result = checkUiPresence('**UI hint**: no\n\nBackend-only spike for RBAC/Entra.\n');
|
||||
assert.strictEqual(result.hasUI, false,
|
||||
'a phase that explicitly declares UI hint: no must not be flagged as UI');
|
||||
assert.deepStrictEqual(result.tokens, []);
|
||||
});
|
||||
|
||||
test('#2150 `**UI hint**: yes` is authoritative frontend', () => {
|
||||
const result = checkUiPresence('**UI hint**: yes\n\nRefactor the login screen layout.\n');
|
||||
assert.strictEqual(result.hasUI, true,
|
||||
'a phase that explicitly declares UI hint: yes must be flagged as UI');
|
||||
});
|
||||
|
||||
test('#2150 `**UI hint**: yes` over a pure-backend body still flags UI', () => {
|
||||
const result = checkUiPresence('**UI hint**: yes\n\nBackend API refactor.\n');
|
||||
assert.strictEqual(result.hasUI, true, 'hint:yes is authoritative even with no UI tokens');
|
||||
assert.deepStrictEqual(result.tokens, []);
|
||||
});
|
||||
|
||||
test('#2150 hint value is whole-word matched (nope/not do not mean no)', () => {
|
||||
const result = checkUiPresence('**UI hint**: nope\n\nBuild a dashboard component.\n');
|
||||
assert.strictEqual(result.hasUI, true,
|
||||
'a malformed hint value like "nope" must not be read as "no"; fall through to token-sniffing');
|
||||
});
|
||||
|
||||
test('#2150 a hint line without yes/no is stripped (bare UI token does not fire)', () => {
|
||||
// A malformed hint (`UI hint: maybe`) must not false-positive on the bare
|
||||
// `UI` token in the line itself; other UI tokens elsewhere still detect.
|
||||
const result = checkUiPresence('**UI hint**: maybe\n\nBackend REST API only.\n');
|
||||
assert.strictEqual(result.hasUI, false,
|
||||
'the bare UI token in a UI hint line must not count as a UI indicator');
|
||||
});
|
||||
|
||||
test('#2150 hint: no overrides even genuine UI language elsewhere', () => {
|
||||
// The explicit declaration is authoritative — the author owns it.
|
||||
const result = checkUiPresence('**UI hint**: no\n\nBuild a dashboard component.\n');
|
||||
assert.strictEqual(result.hasUI, false,
|
||||
'an explicit UI hint: no overrides token-sniffing');
|
||||
});
|
||||
});
|
||||
|
||||
117
tests/validate-registry.test.cjs
Normal file
117
tests/validate-registry.test.cjs
Normal file
@@ -0,0 +1,117 @@
|
||||
'use strict';
|
||||
process.env.GSD_TEST_MODE = '1';
|
||||
|
||||
const { test, describe } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
const { spawnSync } = require('node:child_process');
|
||||
const { cleanup } = require('./helpers.cjs');
|
||||
|
||||
const SCRIPT_PATH = path.join(__dirname, '..', 'scripts', 'validate-registry.cjs');
|
||||
|
||||
// validate-registry.cjs resolves docs/registries/ from process.cwd(), so
|
||||
// tests drive it as a subprocess with `cwd` pointed at an isolated temp
|
||||
// fixture directory — this covers main() end-to-end without touching the
|
||||
// real repo's docs/registries/capabilities.json.
|
||||
|
||||
function validCapabilityEntry() {
|
||||
return {
|
||||
id: 'my-capability',
|
||||
name: 'My Capability',
|
||||
type: 'capability',
|
||||
repo: 'octocat/my-capability',
|
||||
description: 'Does a useful thing for GSD users.',
|
||||
author: 'Octocat',
|
||||
license: 'MIT',
|
||||
enginesGsd: '>=1.6.0 <3.0.0',
|
||||
install: 'gsd capability install https://github.com/octocat/my-capability.git#v1.0.0',
|
||||
uninstall: 'gsd capability remove my-capability',
|
||||
interactions: {
|
||||
loopExtensionPoints: ['execute:pre'],
|
||||
hookKinds: ['step'],
|
||||
configKeys: [],
|
||||
requires: [],
|
||||
runtimeCompat: ['all'],
|
||||
produces: [],
|
||||
consumes: [],
|
||||
},
|
||||
discussion: 'https://github.com/octocat/my-capability/discussions/1',
|
||||
};
|
||||
}
|
||||
|
||||
function withFixture(entries, fn) {
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-validate-registry-'));
|
||||
try {
|
||||
const registriesDir = path.join(tmp, 'docs', 'registries');
|
||||
fs.mkdirSync(registriesDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(registriesDir, 'capabilities.json'), JSON.stringify(entries, null, 2) + '\n');
|
||||
fn(tmp);
|
||||
} finally {
|
||||
cleanup(tmp);
|
||||
}
|
||||
}
|
||||
|
||||
function runValidate(cwd, args = []) {
|
||||
return spawnSync(process.execPath, [SCRIPT_PATH, ...args], { cwd, encoding: 'utf8' });
|
||||
}
|
||||
|
||||
describe('validate-registry CLI (subprocess)', () => {
|
||||
test('a good capabilities.json fixture exits 0', () => {
|
||||
withFixture([validCapabilityEntry()], (tmp) => {
|
||||
const result = runValidate(tmp);
|
||||
assert.equal(result.status, 0, `stderr: ${result.stderr}`);
|
||||
});
|
||||
});
|
||||
|
||||
test('a bad capabilities.json fixture (missing required field) exits non-zero', () => {
|
||||
const bad = validCapabilityEntry();
|
||||
delete bad.discussion;
|
||||
withFixture([bad], (tmp) => {
|
||||
const result = runValidate(tmp);
|
||||
assert.notEqual(result.status, 0);
|
||||
});
|
||||
});
|
||||
|
||||
test('a bad capabilities.json fixture (bad id) exits non-zero', () => {
|
||||
const bad = validCapabilityEntry();
|
||||
bad.id = 'Not_Kebab_Case';
|
||||
withFixture([bad], (tmp) => {
|
||||
const result = runValidate(tmp);
|
||||
assert.notEqual(result.status, 0);
|
||||
});
|
||||
});
|
||||
|
||||
test('--json prints a parseable verdict for a good fixture', () => {
|
||||
withFixture([validCapabilityEntry()], (tmp) => {
|
||||
const result = runValidate(tmp, ['--json']);
|
||||
const parsed = JSON.parse(result.stdout);
|
||||
assert.equal(typeof parsed.ok, 'boolean');
|
||||
assert.ok(Array.isArray(parsed.results));
|
||||
assert.ok(parsed.results.some((r) => r.file === 'capabilities.json'));
|
||||
});
|
||||
});
|
||||
|
||||
test('--json prints a parseable verdict for a bad fixture', () => {
|
||||
const bad = validCapabilityEntry();
|
||||
delete bad.license;
|
||||
withFixture([bad], (tmp) => {
|
||||
const result = runValidate(tmp, ['--json']);
|
||||
const parsed = JSON.parse(result.stdout);
|
||||
assert.equal(typeof parsed.ok, 'boolean');
|
||||
assert.equal(parsed.ok, false);
|
||||
});
|
||||
});
|
||||
|
||||
test('missing capabilities.json entirely exits non-zero', () => {
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-validate-registry-empty-'));
|
||||
try {
|
||||
fs.mkdirSync(path.join(tmp, 'docs', 'registries'), { recursive: true });
|
||||
const result = runValidate(tmp);
|
||||
assert.notEqual(result.status, 0);
|
||||
} finally {
|
||||
cleanup(tmp);
|
||||
}
|
||||
});
|
||||
});
|
||||
128
tests/vscode-browser-no-node-api.test.cjs
Normal file
128
tests/vscode-browser-no-node-api.test.cjs
Normal file
@@ -0,0 +1,128 @@
|
||||
// allow-test-rule: source-text-is-the-product (#2103). browser.js's entire
|
||||
// contract IS "zero Node APIs" — a VS Code Web/webworker host has no Node
|
||||
// core modules at all, so this is a runtime-contract source check, not a
|
||||
// behavioral proxy for something observable another way (there is no Node
|
||||
// runtime to observe failing against in CI). Mirrors the existing
|
||||
// phase6-capstone-conformance.test.cjs precedent for the same exemption class.
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* VS Code browser (Web Extension) entry static guard — #2103.
|
||||
*
|
||||
* `vscode/browser.js` is the `browser` field entry VS Code Web / vscode.dev
|
||||
* loads in a webworker context, which has NO Node core modules at all
|
||||
* (`fs`, `path`, `child_process`) and no Node globals (`process`, `Buffer`,
|
||||
* `__dirname`, `__filename`). Requiring one throws immediately at load time.
|
||||
*
|
||||
* This is the reason browser.js does NOT require `./host-binding.js` (whose
|
||||
* transitive engine-lib dependencies pull in `node:fs` — see host-binding.js's
|
||||
* and browser.js's own header comments for the full chain) — it implements an
|
||||
* independent, minimal composition directly against `vscode.lm`.
|
||||
*
|
||||
* No real browser or VS Code host is available in CI (mock vscode only, per
|
||||
* every other vscode-*.test.cjs in this suite).
|
||||
*/
|
||||
|
||||
const { test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const BROWSER_PATH = path.join(__dirname, '..', 'vscode', 'browser.js');
|
||||
|
||||
test('vscode/browser.js source contains ZERO require("fs" | "path" | "child_process") (or "node:" prefixed forms)', () => {
|
||||
const src = fs.readFileSync(BROWSER_PATH, 'utf8');
|
||||
const bannedRequires = [
|
||||
/require\(\s*['"]fs['"]\s*\)/,
|
||||
/require\(\s*['"]node:fs['"]\s*\)/,
|
||||
/require\(\s*['"]path['"]\s*\)/,
|
||||
/require\(\s*['"]node:path['"]\s*\)/,
|
||||
/require\(\s*['"]child_process['"]\s*\)/,
|
||||
/require\(\s*['"]node:child_process['"]\s*\)/,
|
||||
];
|
||||
const offenders = [];
|
||||
for (const line of src.split(/\r?\n/)) {
|
||||
// Skip comment lines (this file's own header documents the constraint in
|
||||
// prose, which legitimately contains the string `require('fs')` etc.).
|
||||
const trimmed = line.trim();
|
||||
if (trimmed.startsWith('*') || trimmed.startsWith('//')) continue;
|
||||
for (const re of bannedRequires) {
|
||||
if (re.test(line)) offenders.push(line.trim());
|
||||
}
|
||||
}
|
||||
assert.deepEqual(offenders, [],
|
||||
`vscode/browser.js must never require a Node core module outside a comment; found: ${JSON.stringify(offenders)}`);
|
||||
});
|
||||
|
||||
test('vscode/browser.js does NOT require ./host-binding.js or ./extension.js (both pull in fs-heavy engine-lib modules transitively)', () => {
|
||||
const src = fs.readFileSync(BROWSER_PATH, 'utf8');
|
||||
const codeLines = src.split(/\r?\n/).filter((l) => {
|
||||
const t = l.trim();
|
||||
return !(t.startsWith('*') || t.startsWith('//'));
|
||||
}).join('\n');
|
||||
assert.doesNotMatch(codeLines, /require\(\s*['"]\.\/(host-binding|extension)\.js['"]\s*\)/,
|
||||
'browser.js must compose its own zero-Node-API surface, not require a module with fs-pulling transitive deps');
|
||||
});
|
||||
|
||||
test('vscode/browser.js source contains no Node-only globals (process/Buffer/__dirname/__filename) outside comments', () => {
|
||||
const src = fs.readFileSync(BROWSER_PATH, 'utf8');
|
||||
const offenders = [];
|
||||
for (const line of src.split(/\r?\n/)) {
|
||||
const trimmed = line.trim();
|
||||
if (trimmed.startsWith('*') || trimmed.startsWith('//')) continue;
|
||||
if (/\bprocess\.|\bBuffer\.|__dirname\b|__filename\b/.test(line)) offenders.push(line.trim());
|
||||
}
|
||||
assert.deepEqual(offenders, []);
|
||||
});
|
||||
|
||||
test('vscode/browser.js is valid, loadable JS (node --check equivalent — require does not throw)', () => {
|
||||
assert.doesNotThrow(() => require(BROWSER_PATH));
|
||||
});
|
||||
|
||||
test('REACHABILITY: browser.js activate() runs against a mock vscode host without throwing (no real VS Code/browser in CI)', () => {
|
||||
const Module = require('module');
|
||||
const originalLoad = Module._load;
|
||||
const registeredCommands = {};
|
||||
let chatCreated = false;
|
||||
const toolNames = [];
|
||||
const mockVscode = {
|
||||
commands: {
|
||||
registerCommand(id, handler) {
|
||||
registeredCommands[id] = handler;
|
||||
return { dispose() {} };
|
||||
},
|
||||
},
|
||||
chat: {
|
||||
createChatParticipant(id, handler) {
|
||||
chatCreated = true;
|
||||
return { id, handler, dispose() {} };
|
||||
},
|
||||
},
|
||||
lm: {
|
||||
registerTool(name) {
|
||||
toolNames.push(name);
|
||||
return { dispose() {} };
|
||||
},
|
||||
},
|
||||
workspace: { getConfiguration: () => ({ get: () => undefined }) },
|
||||
LanguageModelTextPart: class { constructor(t) { this.text = t; } },
|
||||
LanguageModelToolResult: class { constructor(p) { this.parts = p; } },
|
||||
};
|
||||
Module._load = function (request, ...rest) {
|
||||
if (request === 'vscode') return mockVscode;
|
||||
return originalLoad.call(this, request, ...rest);
|
||||
};
|
||||
try {
|
||||
delete require.cache[BROWSER_PATH];
|
||||
const browser = require(BROWSER_PATH);
|
||||
const context = { subscriptions: [] };
|
||||
assert.doesNotThrow(() => browser.activate(context));
|
||||
assert.ok(registeredCommands['gsd.invoke'], 'gsd.invoke command registered on web too');
|
||||
assert.ok(chatCreated, 'chat participant registered on web');
|
||||
assert.ok(toolNames.length > 0, 'LM tools registered on web');
|
||||
assert.ok(context.subscriptions.length > 0);
|
||||
} finally {
|
||||
Module._load = originalLoad;
|
||||
delete require.cache[BROWSER_PATH];
|
||||
}
|
||||
});
|
||||
@@ -1,18 +1,34 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* VS Code extension reachability test — ADR-1239 Phase D / #1942.
|
||||
* VS Code extension reachability test — ADR-1239 Phase D / #1942, upgraded #2103.
|
||||
*
|
||||
* Proves the VS Code extension is keystone-WIRED: the gsd.invoke handler
|
||||
* dispatches through the GSD command-routing hub and returns a result (not just
|
||||
* a stub). The handler is exported separately from activate() so it is testable
|
||||
* WITHOUT a VS Code host.
|
||||
* dispatches through gsd-tools.cjs (subprocess-reuse — the shared
|
||||
* `dispatchGsdCommand` in gsd-core/bin/lib/shell-command-projection.cjs) and
|
||||
* returns REAL output, not just a registration on a stub. The handler is
|
||||
* exported separately from activate() so it is testable WITHOUT a VS Code host.
|
||||
*
|
||||
* The original cut called `createHub()` with NO args — no hub factory in the
|
||||
* tree fully populates a hub, so every dispatch silently answered
|
||||
* UnknownCommand. The prior version of this test only asserted the result was
|
||||
* "a JSON object" — a VACUOUS assertion that passed whether or not dispatch
|
||||
* actually worked. Dispatch is now exercised with a real read-only
|
||||
* family/subcommand (progress/json) against a real temp project, matching the
|
||||
* sibling tests/pi-extension-reachability.test.cjs pattern — no fake
|
||||
* dispatcher injected, because the whole point of "reachability" is that the
|
||||
* real engine is reached.
|
||||
*/
|
||||
|
||||
const { test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const Module = require('node:module');
|
||||
const path = require('node:path');
|
||||
|
||||
const { activate, dispatchGsdCommand, resolveEngineRoot } = require('../vscode/extension.js');
|
||||
const extension = require('../vscode/extension.js');
|
||||
const { activate, dispatchGsdCommand, resolveEngineRoot, resolveWorkspaceCwd } = extension;
|
||||
const { createTempDir, cleanup } = require('./helpers.cjs');
|
||||
const shellCommandProjection = require('../gsd-core/bin/lib/shell-command-projection.cjs');
|
||||
|
||||
test('the extension exports activate + dispatchGsdCommand + resolveEngineRoot', () => {
|
||||
assert.equal(typeof activate, 'function');
|
||||
@@ -20,18 +36,43 @@ test('the extension exports activate + dispatchGsdCommand + resolveEngineRoot',
|
||||
assert.equal(typeof resolveEngineRoot, 'function');
|
||||
});
|
||||
|
||||
test('REACHABILITY: dispatchGsdCommand dispatches through the engine hub (keystone wired)', async () => {
|
||||
const result = await dispatchGsdCommand({ family: 'query', subcommand: 'help' });
|
||||
assert.equal(typeof result, 'string', 'returns a string result');
|
||||
const parsed = JSON.parse(result);
|
||||
assert.ok(parsed !== null && typeof parsed === 'object',
|
||||
'dispatch produced a result object (the engine was reached)');
|
||||
test('REACHABILITY: dispatchGsdCommand dispatches a real family/subcommand through gsd-tools.cjs and returns REAL output (keystone wired, not UnknownCommand)', async () => {
|
||||
const dir = createTempDir();
|
||||
try {
|
||||
const result = await dispatchGsdCommand({ family: 'progress', subcommand: 'json', cwd: dir });
|
||||
assert.equal(typeof result, 'string', 'returns a string result');
|
||||
const parsed = JSON.parse(result);
|
||||
assert.ok(parsed !== null && typeof parsed === 'object', 'dispatch produced a result object');
|
||||
assert.equal(parsed.ok, true, `expected ok:true (real dispatch), got: ${result}`);
|
||||
assert.equal(typeof parsed.stdout, 'string');
|
||||
assert.ok(parsed.stdout.length > 0, 'stdout must be non-empty');
|
||||
const inner = JSON.parse(parsed.stdout);
|
||||
assert.equal(typeof inner.percent, 'number',
|
||||
'the real progress command ran (proves the engine was reached — not UnknownCommand)');
|
||||
assert.equal(parsed.code, 0);
|
||||
} finally {
|
||||
cleanup(dir);
|
||||
}
|
||||
});
|
||||
|
||||
test('dispatchGsdCommand works with default args (no args → query/help)', async () => {
|
||||
test('REACHABILITY: an unknown family surfaces ok:false without throwing (not a silent UnknownCommand success)', async () => {
|
||||
const dir = createTempDir();
|
||||
try {
|
||||
const result = await dispatchGsdCommand({ family: 'no-such-family-8675309', cwd: dir });
|
||||
const parsed = JSON.parse(result);
|
||||
assert.equal(parsed.ok, false);
|
||||
assert.match(parsed.stderr, /no-such-family-8675309|Unknown command/);
|
||||
} finally {
|
||||
cleanup(dir);
|
||||
}
|
||||
});
|
||||
|
||||
test('dispatchGsdCommand works with default args (no args → gsd-tools.cjs --help, a real working default)', async () => {
|
||||
const result = await dispatchGsdCommand();
|
||||
assert.equal(typeof result, 'string');
|
||||
JSON.parse(result); // must be valid JSON
|
||||
const parsed = JSON.parse(result); // must be valid JSON
|
||||
assert.equal(parsed.ok, true, `expected the --help default to be ok:true, got: ${result}`);
|
||||
assert.match(parsed.stdout, /Usage: gsd-tools/, 'the --help default produced real usage output');
|
||||
});
|
||||
|
||||
test('resolveEngineRoot finds the gsd-core/ dir from the extension location', () => {
|
||||
@@ -49,3 +90,108 @@ test('the extension manifest declares the gsd.invoke command', () => {
|
||||
assert.ok(pkg.engines && pkg.engines.vscode, 'manifest declares VS Code engine');
|
||||
assert.equal(pkg.main, './extension.js', 'manifest main points to extension.js');
|
||||
});
|
||||
|
||||
// ── #2103 FIX (adversarial review, MAJOR): all three desktop dispatch
|
||||
// surfaces previously called dispatchGsdCommand WITHOUT a cwd, silently
|
||||
// defaulting to the extension host's own process.cwd() instead of the user's
|
||||
// project — meaning GSD ran against the wrong directory. resolveWorkspaceCwd
|
||||
// resolves vscode.workspace.workspaceFolders[0].uri.fsPath, computed fresh
|
||||
// per-invocation (never cached at activate() time). ─────────────────────────
|
||||
|
||||
test('resolveWorkspaceCwd resolves workspaceFolders[0].uri.fsPath when a workspace is open', () => {
|
||||
const mockVscode = { workspace: { workspaceFolders: [{ uri: { fsPath: '/tmp/some-workspace' } }] } };
|
||||
assert.equal(resolveWorkspaceCwd(mockVscode), '/tmp/some-workspace');
|
||||
});
|
||||
|
||||
test('resolveWorkspaceCwd falls back to process.cwd() when no workspace folder is open (fail-soft, never throws)', () => {
|
||||
assert.doesNotThrow(() => {
|
||||
assert.equal(resolveWorkspaceCwd({ workspace: { workspaceFolders: [] } }), process.cwd());
|
||||
assert.equal(resolveWorkspaceCwd({ workspace: {} }), process.cwd());
|
||||
assert.equal(resolveWorkspaceCwd({}), process.cwd());
|
||||
});
|
||||
});
|
||||
|
||||
test('#2103 FIX: all three desktop dispatch surfaces (gsd.invoke command, @gsd chat participant, LM tool invoke) thread the resolved workspace folder as cwd — not process.cwd()', async () => {
|
||||
// Spy on the SHARED subprocess-shim dispatchGsdCommand (the one every
|
||||
// surface ultimately calls via extension.js's own dispatchGsdCommand) so we
|
||||
// observe the actual `cwd` each surface passes, without needing a real
|
||||
// gsd-tools.cjs project rooted at the mock workspace path.
|
||||
const originalShimDispatch = shellCommandProjection.dispatchGsdCommand;
|
||||
const calls = [];
|
||||
shellCommandProjection.dispatchGsdCommand = (args) => {
|
||||
calls.push(args);
|
||||
return originalShimDispatch(args);
|
||||
};
|
||||
|
||||
const registeredCommands = {};
|
||||
let chatHandler = null;
|
||||
const toolImpls = [];
|
||||
const mockVscode = {
|
||||
commands: {
|
||||
registerCommand(id, handler) { registeredCommands[id] = handler; return { dispose() {} }; },
|
||||
},
|
||||
chat: {
|
||||
createChatParticipant(id, handler) { chatHandler = handler; return { id, dispose() {} }; },
|
||||
},
|
||||
lm: {
|
||||
registerTool(name, impl) { toolImpls.push({ name, impl }); return { dispose() {} }; },
|
||||
},
|
||||
workspace: {
|
||||
workspaceFolders: [{ uri: { fsPath: '/tmp/mock-gsd-workspace' } }],
|
||||
getConfiguration: () => ({ get: () => undefined }),
|
||||
},
|
||||
LanguageModelTextPart: class { constructor(t) { this.text = t; } },
|
||||
LanguageModelToolResult: class { constructor(parts) { this.parts = parts; } },
|
||||
};
|
||||
|
||||
const originalLoad = Module._load;
|
||||
Module._load = function (request, ...rest) {
|
||||
if (request === 'vscode') return mockVscode;
|
||||
return originalLoad.call(this, request, ...rest);
|
||||
};
|
||||
|
||||
const extensionPath = path.join(__dirname, '..', 'vscode', 'extension.js');
|
||||
const hostBindingPath = path.join(__dirname, '..', 'vscode', 'host-binding.js');
|
||||
try {
|
||||
delete require.cache[extensionPath];
|
||||
delete require.cache[hostBindingPath];
|
||||
const freshExtension = require(extensionPath);
|
||||
const context = {
|
||||
subscriptions: [],
|
||||
globalState: { get: () => undefined, update: () => Promise.resolve() },
|
||||
};
|
||||
freshExtension.activate(context);
|
||||
|
||||
// Surface 1: gsd.invoke command handler.
|
||||
calls.length = 0;
|
||||
await registeredCommands['gsd.invoke']({ family: 'progress', subcommand: 'json' });
|
||||
assert.equal(calls.length, 1, 'gsd.invoke handler must dispatch exactly once');
|
||||
assert.equal(calls[0].cwd, '/tmp/mock-gsd-workspace', 'gsd.invoke must thread the workspace folder as cwd');
|
||||
|
||||
// Surface 1b: an explicit args.cwd still takes precedence over the resolved workspace.
|
||||
calls.length = 0;
|
||||
await registeredCommands['gsd.invoke']({ family: 'progress', subcommand: 'json', cwd: '/explicit/override' });
|
||||
assert.equal(calls[0].cwd, '/explicit/override', 'an explicit cwd argument must still override the resolved workspace folder');
|
||||
|
||||
// Surface 2: @gsd chat participant handler.
|
||||
calls.length = 0;
|
||||
let markdownOut = '';
|
||||
await chatHandler({ prompt: 'progress json' }, {}, { markdown: (t) => { markdownOut += t; } }, {});
|
||||
assert.equal(calls.length, 1, 'the chat participant handler must dispatch exactly once');
|
||||
assert.equal(calls[0].cwd, '/tmp/mock-gsd-workspace', 'the chat participant must thread the workspace folder as cwd');
|
||||
assert.ok(markdownOut.length > 0);
|
||||
|
||||
// Surface 3: Language Model Tool invoke().
|
||||
calls.length = 0;
|
||||
const progressTool = toolImpls.find((t) => t.name === 'gsd_progress');
|
||||
assert.ok(progressTool, 'gsd_progress tool must be registered');
|
||||
await progressTool.impl.invoke({ input: {} }, {});
|
||||
assert.equal(calls.length, 1, 'the LM tool invoke() must dispatch exactly once');
|
||||
assert.equal(calls[0].cwd, '/tmp/mock-gsd-workspace', 'the LM tool invoke() must thread the workspace folder as cwd');
|
||||
} finally {
|
||||
Module._load = originalLoad;
|
||||
shellCommandProjection.dispatchGsdCommand = originalShimDispatch;
|
||||
delete require.cache[extensionPath];
|
||||
delete require.cache[hostBindingPath];
|
||||
}
|
||||
});
|
||||
|
||||
@@ -18,8 +18,9 @@
|
||||
const { test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
|
||||
const { profileOf } = require('../gsd-core/bin/lib/host-integration.cjs');
|
||||
const { profileOf, negotiateHostCapabilities } = require('../gsd-core/bin/lib/host-integration.cjs');
|
||||
const bindGsdToVscode = require('./fixtures/vscode-host-binding.cjs');
|
||||
const registry = require('../gsd-core/bin/lib/capability-registry.cjs');
|
||||
|
||||
test('VS Code IDE axes classify as the ide profile', () => {
|
||||
// ide baseline (host-integration.cts PROFILE_BASELINES): imperative + sandboxed-web.
|
||||
@@ -27,10 +28,17 @@ test('VS Code IDE axes classify as the ide profile', () => {
|
||||
assert.notEqual(profileOf({ embeddingMode: 'imperative', runtime: 'node' }), 'ide');
|
||||
});
|
||||
|
||||
test('bindGsdToVscode composes the full IDE profile (active model + engine bus + sandboxed state + imperative adapter)', () => {
|
||||
test('bindGsdToVscode composes the full IDE profile (active model + engine bus + sandboxed state + imperative adapter)', async () => {
|
||||
// Real API: vscode.lm.selectChatModels() (async → LanguageModelChat[]); the
|
||||
// SELECTED MODEL has .sendRequest, NOT vscode.lm itself (#2103 correction —
|
||||
// there is no vscode.lm.sendRequest).
|
||||
let lastLmReq = null;
|
||||
const vscode = {
|
||||
lm: { sendRequest: (req) => { lastLmReq = req; return 'lm-response'; } },
|
||||
lm: {
|
||||
selectChatModels: async () => [
|
||||
{ sendRequest: (req) => { lastLmReq = req; return 'lm-response'; } },
|
||||
],
|
||||
},
|
||||
};
|
||||
const storageWrites = [];
|
||||
const hostStorage = {
|
||||
@@ -41,9 +49,10 @@ test('bindGsdToVscode composes the full IDE profile (active model + engine bus +
|
||||
const host = bindGsdToVscode(vscode, hostStorage);
|
||||
assert.equal(host.runtime, 'vscode');
|
||||
|
||||
// Active model routes through vscode.lm (no system messages — User role only).
|
||||
// Active model routes through vscode.lm.selectChatModels() → model.sendRequest
|
||||
// (no system messages — User role only).
|
||||
assert.equal(host.model.mode, 'active');
|
||||
assert.equal(host.model.sendRequest({ prompt: 'hi' }), 'lm-response');
|
||||
assert.equal(await host.model.sendRequest({ prompt: 'hi' }), 'lm-response');
|
||||
assert.deepEqual(lastLmReq, { prompt: 'hi' });
|
||||
|
||||
// Engine-owned hook bus: in-process pub/sub (VS Code has no host bus).
|
||||
@@ -67,9 +76,30 @@ test('bindGsdToVscode composes the full IDE profile (active model + engine bus +
|
||||
assert.ok(host.commands['gsd.invoke'], 'palette/chat command surface present');
|
||||
});
|
||||
|
||||
// #2103: proves the binding actually COMPOSES (does not throw) against a
|
||||
// realistic desktop VS Code `vscode.lm` shape — the whole point of the guard
|
||||
// fix: a real host has `selectChatModels`, never `sendRequest` directly, and
|
||||
// the binding must succeed, not silently fail-open at the extension.js layer.
|
||||
test('#2103: bindGsdToVscode composes successfully against a REALISTIC vscode.lm (selectChatModels only, no vscode.lm.sendRequest)', async () => {
|
||||
const vscode = {
|
||||
lm: {
|
||||
selectChatModels: async () => [{ sendRequest: (req) => Promise.resolve({ echoed: req }) }],
|
||||
// Deliberately no top-level sendRequest — matches the real API surface.
|
||||
},
|
||||
};
|
||||
const hostStorage = { read: () => '', write: () => {} };
|
||||
|
||||
let host;
|
||||
assert.doesNotThrow(() => { host = bindGsdToVscode(vscode, hostStorage); },
|
||||
'bindGsdToVscode must succeed on a realistic host (selectChatModels-only vscode.lm)');
|
||||
assert.equal(host.model.mode, 'active');
|
||||
const response = await host.model.sendRequest({ prompt: 'hello' });
|
||||
assert.deepEqual(response, { echoed: { prompt: 'hello' } });
|
||||
});
|
||||
|
||||
test('bindGsdToVscode is fail-closed without vscode.lm or hostStorage', () => {
|
||||
const okStorage = { read() {}, write() {} };
|
||||
const okVscode = { lm: { sendRequest() {} } };
|
||||
const okVscode = { lm: { selectChatModels: async () => [] } };
|
||||
// vscode.lm missing or incomplete → vscode.lm error
|
||||
assert.throws(() => bindGsdToVscode({}, okStorage), /vscode\.lm/);
|
||||
assert.throws(() => bindGsdToVscode({ lm: {} }, okStorage), /vscode\.lm/);
|
||||
@@ -77,3 +107,73 @@ test('bindGsdToVscode is fail-closed without vscode.lm or hostStorage', () => {
|
||||
assert.throws(() => bindGsdToVscode(okVscode, null), /hostStorage/);
|
||||
assert.throws(() => bindGsdToVscode(okVscode, { read() {} }), /hostStorage/);
|
||||
});
|
||||
|
||||
test('#2103: bindGsdToVscode still throws when vscode.lm.selectChatModels is ABSENT (a vscode.lm.sendRequest-only mock, matching the OLD incorrect API assumption, must be rejected)', () => {
|
||||
const okStorage = { read() {}, write() {} };
|
||||
// The pre-#2103 (wrong) shape: sendRequest directly on vscode.lm, no selectChatModels.
|
||||
const staleShapeVscode = { lm: { sendRequest: () => 'stale' } };
|
||||
assert.throws(() => bindGsdToVscode(staleShapeVscode, okStorage), /selectChatModels/);
|
||||
});
|
||||
|
||||
// ── #2103: negotiate fail-closed for an UNDECLARED vscode axis ──────────────
|
||||
// negotiateHostCapabilities must never throw and must degrade an undeclared
|
||||
// (missing) axis to its most-restrictive documented default — the same
|
||||
// fail-closed contract already proven for the "undocumented" sentinel
|
||||
// (tests/host-integration-descriptors.test.cjs), but exercised here against a
|
||||
// genuinely ABSENT key (not the string "undocumented") on vscode's real
|
||||
// hostIntegration object, to prove the negotiation seam is defensive against
|
||||
// both failure modes.
|
||||
//
|
||||
// #2103 FIX (adversarial review, MINOR): strengthened from a weak
|
||||
// `!== undefined` check to a STRICT equality against the actual
|
||||
// most-restrictive value negotiateHostCapabilities produces — the fail-closed
|
||||
// floor pinned as `SAFE_DEFAULTS` in gsd-core/bin/lib/host-integration.cjs
|
||||
// (not exported, so the values are pinned here verbatim, verified via node -e
|
||||
// against the real negotiation output before writing this test — see the PR
|
||||
// report). A `!== undefined` check would pass even if negotiation regressed
|
||||
// to a WRONG-but-defined value (e.g. a less-restrictive default) — this
|
||||
// assertion pins the exact known floor per the AC's own wording
|
||||
// ("most-restrictive known value").
|
||||
const HOST_INTEGRATION_SAFE_DEFAULTS = {
|
||||
embeddingMode: 'declarative',
|
||||
commandSurface: 'prose-only',
|
||||
modelMode: 'passive',
|
||||
hookBus: 'none',
|
||||
stateIO: 'session-log-append',
|
||||
transport: 'mcp',
|
||||
runtime: 'node',
|
||||
};
|
||||
const HOST_INTEGRATION_DISPATCH_SAFE_DEFAULTS = {
|
||||
namedDispatch: false,
|
||||
nested: false,
|
||||
maxDepth: 0,
|
||||
background: false,
|
||||
subagentToolkit: 'read-only',
|
||||
backgroundDispatch: false,
|
||||
};
|
||||
|
||||
test('#2103: negotiateHostCapabilities never throws for vscode with an UNDECLARED axis, and degrades to the EXACT most-restrictive SAFE_DEFAULTS value', () => {
|
||||
const realHi = registry.runtimes.vscode.runtime.hostIntegration;
|
||||
for (const axis of Object.keys(HOST_INTEGRATION_SAFE_DEFAULTS)) {
|
||||
const undeclared = { ...realHi, dispatch: { ...realHi.dispatch } };
|
||||
delete undeclared[axis];
|
||||
let result;
|
||||
assert.doesNotThrow(() => { result = negotiateHostCapabilities(undeclared); },
|
||||
`negotiateHostCapabilities must not throw when vscode's "${axis}" axis is entirely absent`);
|
||||
assert.strictEqual(result.effective[axis], HOST_INTEGRATION_SAFE_DEFAULTS[axis],
|
||||
`effective.${axis} must resolve to the exact most-restrictive SAFE_DEFAULTS value "${HOST_INTEGRATION_SAFE_DEFAULTS[axis]}", got: ${JSON.stringify(result.effective[axis])}`);
|
||||
}
|
||||
});
|
||||
|
||||
test('#2103: negotiateHostCapabilities never throws for vscode with an UNDECLARED dispatch sub-axis, and degrades to the EXACT most-restrictive value', () => {
|
||||
const realHi = registry.runtimes.vscode.runtime.hostIntegration;
|
||||
for (const key of Object.keys(HOST_INTEGRATION_DISPATCH_SAFE_DEFAULTS)) {
|
||||
const undeclared = { ...realHi, dispatch: { ...realHi.dispatch } };
|
||||
delete undeclared.dispatch[key];
|
||||
let result;
|
||||
assert.doesNotThrow(() => { result = negotiateHostCapabilities(undeclared); },
|
||||
`negotiateHostCapabilities must not throw when vscode's dispatch.${key} is entirely absent`);
|
||||
assert.strictEqual(result.effective.dispatch[key], HOST_INTEGRATION_DISPATCH_SAFE_DEFAULTS[key],
|
||||
`effective.dispatch.${key} must resolve to the exact most-restrictive value "${HOST_INTEGRATION_DISPATCH_SAFE_DEFAULTS[key]}", got: ${JSON.stringify(result.effective.dispatch[key])}`);
|
||||
}
|
||||
});
|
||||
|
||||
155
tests/vscode-lm-tools.test.cjs
Normal file
155
tests/vscode-lm-tools.test.cjs
Normal file
@@ -0,0 +1,155 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* VS Code Language Model Tools test — #2103 UPGRADE 1.
|
||||
*
|
||||
* Proves the GSD extension's Language Model Tools are keystone-WIRED:
|
||||
* 1. contributes.languageModelTools is present in package.json and its
|
||||
* `name` entries match the runtime registration names (extension.js's
|
||||
* LM_TOOLS / browser.js's LM_TOOL_NAMES) exactly — a mismatch here would
|
||||
* mean VS Code rejects the tool registration at activation.
|
||||
* 2. registerLanguageModelTools() calls vscode.lm.registerTool for every
|
||||
* manifest entry (mock vscode.lm — no real VS Code host available in CI).
|
||||
* 3. A registered tool's invoke() dispatches through the SAME shared
|
||||
* dispatchGsdCommand as gsd.invoke (desktop) and returns REAL output —
|
||||
* the "user can invoke X" proof, matching the reachability-test pattern.
|
||||
* 4. The desktop and web entries register the identical tool NAME set (only
|
||||
* the invoke() behavior differs — real dispatch vs. honest web-mode message).
|
||||
*/
|
||||
|
||||
const { test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
|
||||
const pkg = require('../vscode/package.json');
|
||||
const extension = require('../vscode/extension.js');
|
||||
const browser = require('../vscode/browser.js');
|
||||
const { createTempDir, cleanup } = require('./helpers.cjs');
|
||||
|
||||
class FakeTextPart {
|
||||
constructor(text) { this.text = text; }
|
||||
}
|
||||
class FakeToolResult {
|
||||
constructor(parts) { this.parts = parts; }
|
||||
}
|
||||
|
||||
function mockVscodeLm() {
|
||||
const registered = [];
|
||||
return {
|
||||
lm: {
|
||||
registerTool(name, impl) {
|
||||
registered.push({ name, impl });
|
||||
return { dispose() {} };
|
||||
},
|
||||
},
|
||||
LanguageModelTextPart: FakeTextPart,
|
||||
LanguageModelToolResult: FakeToolResult,
|
||||
registered,
|
||||
};
|
||||
}
|
||||
|
||||
test('package.json contributes.languageModelTools is present with well-formed entries', () => {
|
||||
assert.ok(pkg.contributes && Array.isArray(pkg.contributes.languageModelTools),
|
||||
'contributes.languageModelTools must be an array');
|
||||
assert.ok(pkg.contributes.languageModelTools.length > 0, 'must declare at least one tool');
|
||||
for (const tool of pkg.contributes.languageModelTools) {
|
||||
assert.equal(typeof tool.name, 'string');
|
||||
assert.ok(tool.name.length > 0);
|
||||
assert.equal(typeof tool.toolReferenceName, 'string');
|
||||
assert.equal(typeof tool.displayName, 'string');
|
||||
assert.equal(typeof tool.modelDescription, 'string');
|
||||
assert.equal(typeof tool.userDescription, 'string');
|
||||
assert.equal(tool.canBeReferencedInPrompt, true);
|
||||
assert.ok(Array.isArray(tool.tags));
|
||||
assert.equal(typeof tool.inputSchema, 'object');
|
||||
}
|
||||
});
|
||||
|
||||
test('manifest tool names exactly match extension.js LM_TOOLS registration names', () => {
|
||||
const manifestNames = pkg.contributes.languageModelTools.map((t) => t.name).sort();
|
||||
const runtimeNames = extension.LM_TOOLS.map((t) => t.name).sort();
|
||||
assert.deepEqual(runtimeNames, manifestNames,
|
||||
'a mismatch here means VS Code would reject the runtime registerTool call against the manifest');
|
||||
});
|
||||
|
||||
test('manifest tool names exactly match browser.js LM_TOOL_NAMES (web entry registers the same surface)', () => {
|
||||
const manifestNames = pkg.contributes.languageModelTools.map((t) => t.name).sort();
|
||||
assert.deepEqual([...browser.LM_TOOL_NAMES].sort(), manifestNames);
|
||||
});
|
||||
|
||||
test('REACHABILITY (desktop): registerLanguageModelTools registers every manifest tool via vscode.lm.registerTool', () => {
|
||||
const mock = mockVscodeLm();
|
||||
const context = { subscriptions: [] };
|
||||
const count = extension.registerLanguageModelTools(mock, context);
|
||||
assert.equal(count, pkg.contributes.languageModelTools.length);
|
||||
assert.equal(mock.registered.length, pkg.contributes.languageModelTools.length);
|
||||
assert.equal(context.subscriptions.length, pkg.contributes.languageModelTools.length);
|
||||
assert.deepEqual(mock.registered.map((r) => r.name).sort(), extension.LM_TOOLS.map((t) => t.name).sort());
|
||||
});
|
||||
|
||||
test('REACHABILITY (desktop): gsd_progress tool.invoke() dispatches through the hub and returns REAL output', async () => {
|
||||
const dir = createTempDir();
|
||||
try {
|
||||
const mock = mockVscodeLm();
|
||||
extension.registerLanguageModelTools(mock, { subscriptions: [] });
|
||||
const progressTool = mock.registered.find((r) => r.name === 'gsd_progress');
|
||||
assert.ok(progressTool, 'gsd_progress must be registered');
|
||||
const result = await progressTool.impl.invoke({ input: {}, cwd: dir }, {});
|
||||
assert.ok(result instanceof FakeToolResult, 'invoke must return a LanguageModelToolResult');
|
||||
assert.ok(Array.isArray(result.parts) && result.parts.length === 1);
|
||||
assert.ok(result.parts[0] instanceof FakeTextPart, 'result part must be a LanguageModelTextPart');
|
||||
const parsed = JSON.parse(result.parts[0].text);
|
||||
assert.equal(typeof parsed.percent, 'number', 'the real progress command ran (engine reached, not a stub)');
|
||||
} finally {
|
||||
cleanup(dir);
|
||||
}
|
||||
});
|
||||
|
||||
test('REACHABILITY (desktop): gsd_plan_phase tool.invoke() forwards the "phase" input through dispatch (real, not UnknownCommand)', async () => {
|
||||
const dir = createTempDir();
|
||||
try {
|
||||
const mock = mockVscodeLm();
|
||||
extension.registerLanguageModelTools(mock, { subscriptions: [] });
|
||||
const planPhaseTool = mock.registered.find((r) => r.name === 'gsd_plan_phase');
|
||||
assert.ok(planPhaseTool);
|
||||
const result = await planPhaseTool.impl.invoke({ input: { phase: 'nonexistent-phase-8675309' }, cwd: dir }, {});
|
||||
const parsed = JSON.parse(result.parts[0].text);
|
||||
// Real dispatch reaches gsd-tools.cjs and returns a structured "phase not
|
||||
// found" response (proves the engine was reached) — not the manifest's
|
||||
// own family/subcommand rejected as unknown.
|
||||
assert.equal(parsed.phase, 'nonexistent-phase-8675309');
|
||||
assert.ok('error' in parsed || 'plans' in parsed, 'expected a real phase-plan-index response shape');
|
||||
} finally {
|
||||
cleanup(dir);
|
||||
}
|
||||
});
|
||||
|
||||
test('gsd_workstreams tool.invoke() dispatches through the hub and returns REAL output', async () => {
|
||||
const dir = createTempDir();
|
||||
try {
|
||||
const mock = mockVscodeLm();
|
||||
extension.registerLanguageModelTools(mock, { subscriptions: [] });
|
||||
const wsTool = mock.registered.find((r) => r.name === 'gsd_workstreams');
|
||||
const result = await wsTool.impl.invoke({ input: {}, cwd: dir }, {});
|
||||
const parsed = JSON.parse(result.parts[0].text);
|
||||
assert.ok('workstreams' in parsed || 'mode' in parsed, 'expected a real workstream list response shape');
|
||||
} finally {
|
||||
cleanup(dir);
|
||||
}
|
||||
});
|
||||
|
||||
test('registerLanguageModelTools fails soft (returns 0, does not throw) when vscode.lm is absent', () => {
|
||||
assert.doesNotThrow(() => {
|
||||
const count = extension.registerLanguageModelTools({}, { subscriptions: [] });
|
||||
assert.equal(count, 0);
|
||||
});
|
||||
});
|
||||
|
||||
test('WEB MODE: browser.js registerLanguageModelTools registers the same names but invoke() returns an honest web-mode message (no engine dispatch)', async () => {
|
||||
const mock = mockVscodeLm();
|
||||
const count = browser.registerLanguageModelTools(mock, { subscriptions: [] });
|
||||
assert.equal(count, browser.LM_TOOL_NAMES.length);
|
||||
const progressTool = mock.registered.find((r) => r.name === 'gsd_progress');
|
||||
const result = await progressTool.impl.invoke({ input: {} }, {});
|
||||
assert.match(result.parts[0].text, /web mode/i);
|
||||
assert.match(result.parts[0].text, /MCP server/);
|
||||
});
|
||||
148
tests/vscode-subagent-dispatch.test.cjs
Normal file
148
tests/vscode-subagent-dispatch.test.cjs
Normal file
@@ -0,0 +1,148 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* VS Code #runSubagent wiring test — #2103 UPGRADE 2.
|
||||
*
|
||||
* VS Code 1.105+ lets the primary chat agent invoke registered chat
|
||||
* participants / Language Model Tools as a nested agent turn via
|
||||
* `#runSubagent`, gated by the `chat.subagents.allowInvocationsFromSubagents`
|
||||
* setting (default off; nested depth max 5 when enabled — docs/agents/subagents.md,
|
||||
* release-notes/v1_105.md). There is no separate extension-side "subagent
|
||||
* contribution" registration API: VS Code's chat engine surfaces the already
|
||||
* -registered chat participant + languageModelTools directly. This extension's
|
||||
* own contribution is `registerSubagentDispatch` — availability detection
|
||||
* (fail-soft on older/Insiders-gated hosts) plus a belt-and-suspenders
|
||||
* maxDepth:5 ceiling (dispatchAsSubagent) that mirrors
|
||||
* capabilities/vscode/capability.json's hostIntegration.dispatch.maxDepth,
|
||||
* independent of whatever the host itself enforces.
|
||||
*
|
||||
* Mock vscode only — no real VS Code host in CI.
|
||||
*/
|
||||
|
||||
const { test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
|
||||
const extension = require('../vscode/extension.js');
|
||||
const { createTempDir, cleanup } = require('./helpers.cjs');
|
||||
|
||||
function mockVscodeWithSubagentSupport(allow) {
|
||||
return {
|
||||
workspace: {
|
||||
getConfiguration(section) {
|
||||
return {
|
||||
get(key) {
|
||||
if (section === 'chat.subagents' && key === 'allowInvocationsFromSubagents') return allow;
|
||||
return undefined;
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
test('registerSubagentDispatch reports maxDepth matching capabilities/vscode/capability.json (dispatch.maxDepth:5)', () => {
|
||||
const cap = require('../capabilities/vscode/capability.json');
|
||||
const wiring = extension.registerSubagentDispatch(mockVscodeWithSubagentSupport(true));
|
||||
assert.equal(wiring.maxDepth, cap.runtime.hostIntegration.dispatch.maxDepth,
|
||||
'the extension-side depth cap must mirror the descriptor-declared maxDepth');
|
||||
assert.equal(extension.GSD_MAX_SUBAGENT_DEPTH, cap.runtime.hostIntegration.dispatch.maxDepth);
|
||||
});
|
||||
|
||||
test('registerSubagentDispatch: available:true when chat.subagents.allowInvocationsFromSubagents is configured', () => {
|
||||
const wiring = extension.registerSubagentDispatch(mockVscodeWithSubagentSupport(true));
|
||||
assert.equal(wiring.available, true);
|
||||
assert.equal(typeof wiring.dispatchAsSubagent, 'function');
|
||||
});
|
||||
|
||||
test('registerSubagentDispatch: available:false (fail-soft, no throw) when the setting is absent (older/Insiders-gated VS Code)', () => {
|
||||
assert.doesNotThrow(() => {
|
||||
const wiring = extension.registerSubagentDispatch({});
|
||||
assert.equal(wiring.available, false);
|
||||
});
|
||||
});
|
||||
|
||||
test('registerSubagentDispatch: available:false (fail-soft) when vscode.workspace.getConfiguration itself throws', () => {
|
||||
const throwingVscode = {
|
||||
workspace: { getConfiguration() { throw new Error('simulated host failure'); } },
|
||||
};
|
||||
assert.doesNotThrow(() => {
|
||||
const wiring = extension.registerSubagentDispatch(throwingVscode);
|
||||
assert.equal(wiring.available, false);
|
||||
});
|
||||
});
|
||||
|
||||
test('REACHABILITY: a background-eligible dispatchAsSubagent call at depth 0 dispatches through the shared hub and returns REAL output', async () => {
|
||||
const dir = createTempDir();
|
||||
try {
|
||||
const result = JSON.parse(await extension.dispatchAsSubagent({
|
||||
family: 'progress', subcommand: 'json', cwd: dir, depth: 0,
|
||||
}));
|
||||
assert.equal(result.ok, true);
|
||||
const parsed = JSON.parse(result.stdout);
|
||||
assert.equal(typeof parsed.percent, 'number', 'the real progress command ran (engine reached)');
|
||||
} finally {
|
||||
cleanup(dir);
|
||||
}
|
||||
});
|
||||
|
||||
// #2103 FIX (adversarial review, MINOR — boundary gap): the repo's own
|
||||
// TESTING-STANDARDS mandate exercising limit-1/limit/limit+1 around a boundary.
|
||||
// The maxDepth:5 ceiling previously covered only 5 (limit) and 6 (limit+1) —
|
||||
// this completes the triple with depth 4 (limit-1).
|
||||
test('dispatchAsSubagent enforces the maxDepth:5 ceiling — depth 4 (limit-1) still dispatches', async () => {
|
||||
const dir = createTempDir();
|
||||
try {
|
||||
const result = JSON.parse(await extension.dispatchAsSubagent({
|
||||
family: 'progress', subcommand: 'json', cwd: dir, depth: 4,
|
||||
}));
|
||||
assert.equal(result.ok, true, 'depth one below the ceiling must be allowed');
|
||||
} finally {
|
||||
cleanup(dir);
|
||||
}
|
||||
});
|
||||
|
||||
test('dispatchAsSubagent enforces the maxDepth:5 ceiling — depth 5 (at limit) still dispatches', async () => {
|
||||
const dir = createTempDir();
|
||||
try {
|
||||
const result = JSON.parse(await extension.dispatchAsSubagent({
|
||||
family: 'progress', subcommand: 'json', cwd: dir, depth: 5,
|
||||
}));
|
||||
assert.equal(result.ok, true, 'depth exactly at the ceiling must still be allowed');
|
||||
} finally {
|
||||
cleanup(dir);
|
||||
}
|
||||
});
|
||||
|
||||
test('dispatchAsSubagent enforces the maxDepth:5 ceiling — depth 6 (over limit) is refused, never throws', async () => {
|
||||
const result = JSON.parse(await extension.dispatchAsSubagent({
|
||||
family: 'progress', subcommand: 'json', depth: 6,
|
||||
}));
|
||||
assert.equal(result.ok, false);
|
||||
assert.match(result.stderr, /exceeds maxDepth/);
|
||||
assert.equal(result.code, null);
|
||||
});
|
||||
|
||||
test('dispatchAsSubagent defaults depth to 0 when omitted (a direct, non-nested call is always allowed)', async () => {
|
||||
const dir = createTempDir();
|
||||
try {
|
||||
const result = JSON.parse(await extension.dispatchAsSubagent({ family: 'progress', subcommand: 'json', cwd: dir }));
|
||||
assert.equal(result.ok, true);
|
||||
} finally {
|
||||
cleanup(dir);
|
||||
}
|
||||
});
|
||||
|
||||
// ── Web mode: #runSubagent availability detection is registered identically,
|
||||
// but there is no dispatchAsSubagent on web (no engine dispatch at all — see
|
||||
// browser.js's header comment). ─────────────────────────────────────────────
|
||||
test('WEB MODE: browser.js detectSubagentSupport uses the same fail-soft availability contract', () => {
|
||||
const browser = require('../vscode/browser.js');
|
||||
assert.doesNotThrow(() => {
|
||||
const wiring = browser.detectSubagentSupport(mockVscodeWithSubagentSupport(true));
|
||||
assert.equal(wiring.available, true);
|
||||
});
|
||||
assert.doesNotThrow(() => {
|
||||
const wiring = browser.detectSubagentSupport({});
|
||||
assert.equal(wiring.available, false);
|
||||
});
|
||||
});
|
||||
374
tests/windsurf-hooks-bridge.test.cjs
Normal file
374
tests/windsurf-hooks-bridge.test.cjs
Normal file
@@ -0,0 +1,374 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Windsurf/Cascade HOOK-BRIDGE upgrade — ADR-1239 / #2100 Stage 2.
|
||||
*
|
||||
* Stage 1 (folds) drove Windsurf's install onto the declarative capability
|
||||
* descriptor (hostBehaviors) while leaving `hooksSurface: "none"`. Stage 2
|
||||
* wires GSD's guard logic into Cascade's native hook bus: `.windsurf/hooks.json`
|
||||
* (local) / `~/.codeium/windsurf/hooks.json` (global), with TWO blocking
|
||||
* events — `pre_write_code` and `pre_run_command`. Cascade blocks via EXIT
|
||||
* CODE 2 (+ a stderr reason), unlike Cursor's stdout-JSON `{ block, reason }`
|
||||
* form — so this is a DIFFERENT protocol wired through the SAME infra shape
|
||||
* as writeCursorHooksJson/removeCursorHooksJson (mirrors
|
||||
* tests/cursor-hooks.test.cjs + tests/cursor-hook-bus-upgrade.test.cjs).
|
||||
*
|
||||
* Cascade has no context-injection channel (no `additional_context`-style
|
||||
* advisory response), so the 4 advisory hooks GSD registers on Cursor
|
||||
* (sessionStart, postToolUse, stop, subagentStart/subagentStop) have no
|
||||
* Windsurf counterpart and are deliberately NOT ported.
|
||||
*
|
||||
* Test plan:
|
||||
* Guard scripts (spawned directly, real process, real exit codes):
|
||||
* G1 pre-write: file resolves to a different git root than cwd -> exit 2 + stderr
|
||||
* G2 pre-write: file resolves to the SAME git root as cwd -> exit 0
|
||||
* G3 pre-write: malformed JSON on stdin -> fail-open exit 0
|
||||
* G4 pre-write: empty stdin -> fail-open exit 0
|
||||
* G5 pre-command: a destructive command_line -> exit 2 + stderr
|
||||
* G6 pre-command: a benign command_line -> exit 0
|
||||
* G8 pre-command: prefixed/evasive rm -rf and force-push refspec forms -> exit 2
|
||||
* G9 pre-command: force-push false-positive forms (comment/branch-name-contains) -> exit 0
|
||||
* G10 pre-command: ReDoS-guard — oversized rm-shaped payload -> exit 0 in well under 1s
|
||||
* G7 pre-command: malformed JSON on stdin -> fail-open exit 0
|
||||
* Writer/reconcile (in-process, pure + one real install):
|
||||
* W1 writeWindsurfHooksJson installs both scripts + both hooks.json entries
|
||||
* W2 reconcileWindsurfHooksJson preserves user-owned entries
|
||||
* W3 removeWindsurfHooksJson removes hooks.json when it becomes empty
|
||||
* W4 removeWindsurfHooksJson preserves hooks.json when user entries remain
|
||||
* W5 WINDSURF_HOOK_EVENTS / WINDSURF_EVENT_SCRIPT_MAP shape
|
||||
* W6 hook scripts exist under hooks/
|
||||
*/
|
||||
|
||||
const { test, describe } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const os = require('node:os');
|
||||
const { spawnSync, execFileSync } = require('node:child_process');
|
||||
|
||||
const { createTempDir, cleanup } = require('./helpers.cjs');
|
||||
|
||||
const {
|
||||
WINDSURF_HOOK_EVENTS,
|
||||
WINDSURF_EVENT_SCRIPT_MAP,
|
||||
GSD_WINDSURF_HOOK_MARKER,
|
||||
GSD_WINDSURF_PRE_WRITE_HOOK_SCRIPT,
|
||||
GSD_WINDSURF_PRE_COMMAND_HOOK_SCRIPT,
|
||||
isManagedWindsurfHookEntry,
|
||||
reconcileWindsurfHooksJson,
|
||||
writeWindsurfHooksJson,
|
||||
removeWindsurfHooksJson,
|
||||
} = require('../gsd-core/bin/lib/runtime-hooks-surface.cjs');
|
||||
|
||||
const HOOKS_DIR = path.join(__dirname, '..', 'hooks');
|
||||
const PRE_WRITE_SCRIPT = path.join(HOOKS_DIR, GSD_WINDSURF_PRE_WRITE_HOOK_SCRIPT);
|
||||
const PRE_COMMAND_SCRIPT = path.join(HOOKS_DIR, GSD_WINDSURF_PRE_COMMAND_HOOK_SCRIPT);
|
||||
|
||||
function runHook(scriptPath, payload, opts = {}) {
|
||||
const input = payload === undefined ? '' : (typeof payload === 'string' ? payload : JSON.stringify(payload));
|
||||
return spawnSync(process.execPath, [scriptPath], {
|
||||
input,
|
||||
encoding: 'utf8',
|
||||
timeout: 10000,
|
||||
cwd: opts.cwd || os.tmpdir(),
|
||||
});
|
||||
}
|
||||
|
||||
function initGitRepo(dir) {
|
||||
execFileSync('git', ['init', '-q', '.'], { cwd: dir, stdio: 'pipe' });
|
||||
execFileSync('git', ['config', 'user.email', 'gsd-test@example.com'], { cwd: dir, stdio: 'pipe' });
|
||||
execFileSync('git', ['config', 'user.name', 'gsd-test'], { cwd: dir, stdio: 'pipe' });
|
||||
execFileSync('git', ['commit', '--allow-empty', '-q', '-m', 'init'], { cwd: dir, stdio: 'pipe' });
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Guard scripts — spawned directly, real exit codes
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('gsd-windsurf-pre-write.js (pre_write_code guard)', () => {
|
||||
test('G1: a write resolving to a DIFFERENT git root than cwd -> exit 2 + stderr reason', (t) => {
|
||||
const cwdRepo = createTempDir('gsd-windsurf-write-cwd-');
|
||||
const otherRepo = createTempDir('gsd-windsurf-write-other-');
|
||||
t.after(() => { cleanup(cwdRepo); cleanup(otherRepo); });
|
||||
initGitRepo(cwdRepo);
|
||||
initGitRepo(otherRepo);
|
||||
fs.writeFileSync(path.join(otherRepo, 'target.txt'), 'x');
|
||||
|
||||
const result = runHook(PRE_WRITE_SCRIPT, {
|
||||
agent_action_name: 'pre_write_code',
|
||||
tool_info: { file_path: path.join(otherRepo, 'target.txt') },
|
||||
}, { cwd: cwdRepo });
|
||||
|
||||
assert.equal(result.status, 2, `expected exit 2, got ${result.status} (stderr: ${result.stderr})`);
|
||||
assert.match(result.stderr, /differs from the active project root|inside a git internal/);
|
||||
});
|
||||
|
||||
test('G1b: a write inside a DIFFERENT repo\'s .git internals -> exit 2 + stderr reason', (t) => {
|
||||
const cwdRepo = createTempDir('gsd-windsurf-write-cwd-');
|
||||
const otherRepo = createTempDir('gsd-windsurf-write-other-');
|
||||
t.after(() => { cleanup(cwdRepo); cleanup(otherRepo); });
|
||||
initGitRepo(cwdRepo);
|
||||
initGitRepo(otherRepo);
|
||||
|
||||
const result = runHook(PRE_WRITE_SCRIPT, {
|
||||
tool_info: { file_path: path.join(otherRepo, '.git', 'config') },
|
||||
}, { cwd: cwdRepo });
|
||||
|
||||
assert.equal(result.status, 2, `expected exit 2, got ${result.status} (stderr: ${result.stderr})`);
|
||||
assert.match(result.stderr, /inside a git internal \(\.git\) directory/);
|
||||
});
|
||||
|
||||
test('G2: a write resolving to the SAME git root as cwd -> exit 0 (allowed)', (t) => {
|
||||
const repo = createTempDir('gsd-windsurf-write-same-');
|
||||
t.after(() => cleanup(repo));
|
||||
initGitRepo(repo);
|
||||
fs.mkdirSync(path.join(repo, 'sub'));
|
||||
|
||||
const result = runHook(PRE_WRITE_SCRIPT, {
|
||||
tool_info: { file_path: 'sub/new-file.txt' },
|
||||
}, { cwd: repo });
|
||||
|
||||
assert.equal(result.status, 0, `expected exit 0, got ${result.status} (stderr: ${result.stderr})`);
|
||||
});
|
||||
|
||||
test('G3: malformed JSON on stdin -> fail-open exit 0', () => {
|
||||
const result = runHook(PRE_WRITE_SCRIPT, 'not-json-at-all{{{');
|
||||
assert.equal(result.status, 0);
|
||||
});
|
||||
|
||||
test('G4: empty stdin -> fail-open exit 0', () => {
|
||||
const result = runHook(PRE_WRITE_SCRIPT, '');
|
||||
assert.equal(result.status, 0);
|
||||
});
|
||||
|
||||
test('missing tool_info.file_path -> fail-open exit 0', () => {
|
||||
const result = runHook(PRE_WRITE_SCRIPT, { tool_info: {} });
|
||||
assert.equal(result.status, 0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('gsd-windsurf-pre-command.js (pre_run_command guard)', () => {
|
||||
test('G5: a destructive command_line (rm -rf /) -> exit 2 + stderr reason', () => {
|
||||
const result = runHook(PRE_COMMAND_SCRIPT, { tool_info: { command_line: 'rm -rf /' } });
|
||||
assert.equal(result.status, 2, `expected exit 2, got ${result.status} (stderr: ${result.stderr})`);
|
||||
assert.match(result.stderr, /rm -rf targeting the filesystem root/);
|
||||
});
|
||||
|
||||
test('G5b: git push --force targeting a protected branch -> exit 2 + stderr reason', () => {
|
||||
const result = runHook(PRE_COMMAND_SCRIPT, { tool_info: { command_line: 'git push --force origin main' } });
|
||||
assert.equal(result.status, 2, `expected exit 2, got ${result.status} (stderr: ${result.stderr})`);
|
||||
assert.match(result.stderr, /protected branch 'main'/);
|
||||
});
|
||||
|
||||
test('G6: a benign command_line -> exit 0 (allowed)', () => {
|
||||
const result = runHook(PRE_COMMAND_SCRIPT, { tool_info: { command_line: 'npm test' } });
|
||||
assert.equal(result.status, 0, `expected exit 0, got ${result.status} (stderr: ${result.stderr})`);
|
||||
});
|
||||
|
||||
test('G6b: rm -rf against an ordinary project path -> exit 0 (allowed, not a root wipe)', () => {
|
||||
const result = runHook(PRE_COMMAND_SCRIPT, { tool_info: { command_line: 'rm -rf /tmp/gsd-scratch-dir' } });
|
||||
assert.equal(result.status, 0, `expected exit 0, got ${result.status} (stderr: ${result.stderr})`);
|
||||
});
|
||||
|
||||
test('G6c: git push --force to a feature branch -> exit 0 (allowed, not protected)', () => {
|
||||
const result = runHook(PRE_COMMAND_SCRIPT, { tool_info: { command_line: 'git push --force origin feature/123' } });
|
||||
assert.equal(result.status, 0, `expected exit 0, got ${result.status} (stderr: ${result.stderr})`);
|
||||
});
|
||||
|
||||
test('G8: sudo-prefixed rm -rf / -> exit 2 (evasion via command prefix)', () => {
|
||||
const result = runHook(PRE_COMMAND_SCRIPT, { tool_info: { command_line: 'sudo rm -rf /' } });
|
||||
assert.equal(result.status, 2, `expected exit 2, got ${result.status} (stderr: ${result.stderr})`);
|
||||
assert.match(result.stderr, /rm -rf targeting the filesystem root/);
|
||||
});
|
||||
|
||||
test('G8b: absolute-path rm -rf / (/bin/rm) -> exit 2 (evasion via basename)', () => {
|
||||
const result = runHook(PRE_COMMAND_SCRIPT, { tool_info: { command_line: '/bin/rm -rf /' } });
|
||||
assert.equal(result.status, 2, `expected exit 2, got ${result.status} (stderr: ${result.stderr})`);
|
||||
assert.match(result.stderr, /rm -rf targeting the filesystem root/);
|
||||
});
|
||||
|
||||
test('G8c: git push -f origin HEAD:main -> exit 2 (refspec targeting protected branch)', () => {
|
||||
const result = runHook(PRE_COMMAND_SCRIPT, { tool_info: { command_line: 'git push -f origin HEAD:main' } });
|
||||
assert.equal(result.status, 2, `expected exit 2, got ${result.status} (stderr: ${result.stderr})`);
|
||||
assert.match(result.stderr, /protected branch 'main'/);
|
||||
});
|
||||
|
||||
test('G8d: git push origin +main -> exit 2 (+-prefixed force refspec)', () => {
|
||||
const result = runHook(PRE_COMMAND_SCRIPT, { tool_info: { command_line: 'git push origin +main' } });
|
||||
assert.equal(result.status, 2, `expected exit 2, got ${result.status} (stderr: ${result.stderr})`);
|
||||
assert.match(result.stderr, /protected branch 'main'/);
|
||||
});
|
||||
|
||||
test('G9: git push --force to a feature branch with a trailing comment mentioning main -> exit 0 (not a false positive)', () => {
|
||||
const result = runHook(PRE_COMMAND_SCRIPT, {
|
||||
tool_info: { command_line: 'git push --force origin feature/foo # deploy to main' },
|
||||
});
|
||||
assert.equal(result.status, 0, `expected exit 0, got ${result.status} (stderr: ${result.stderr})`);
|
||||
});
|
||||
|
||||
test('G9b: git push --force to feature/main-fix -> exit 0 (branch name only CONTAINS "main", not a false positive)', () => {
|
||||
const result = runHook(PRE_COMMAND_SCRIPT, { tool_info: { command_line: 'git push --force origin feature/main-fix' } });
|
||||
assert.equal(result.status, 0, `expected exit 0, got ${result.status} (stderr: ${result.stderr})`);
|
||||
});
|
||||
|
||||
test('G9c: env-prefixed benign command -> exit 0 (env prefix alone is not destructive)', () => {
|
||||
const result = runHook(PRE_COMMAND_SCRIPT, { tool_info: { command_line: 'env FOO=1 npm test' } });
|
||||
assert.equal(result.status, 0, `expected exit 0, got ${result.status} (stderr: ${result.stderr})`);
|
||||
});
|
||||
|
||||
test('G10: ReDoS-guard — a 200000+-char rm -rf-shaped payload completes in well under 1s via the length cap', () => {
|
||||
const payload = `rm -${'r'.repeat(200000)}!`;
|
||||
const start = Date.now();
|
||||
const result = runHook(PRE_COMMAND_SCRIPT, { tool_info: { command_line: payload } });
|
||||
const elapsedMs = Date.now() - start;
|
||||
assert.equal(result.status, 0, `expected exit 0 (length-capped allow), got ${result.status} (stderr: ${result.stderr})`);
|
||||
assert.ok(elapsedMs < 1000, `expected < 1000ms, took ${elapsedMs}ms`);
|
||||
});
|
||||
|
||||
test('G7: malformed JSON on stdin -> fail-open exit 0', () => {
|
||||
const result = runHook(PRE_COMMAND_SCRIPT, 'not-json-at-all{{{');
|
||||
assert.equal(result.status, 0);
|
||||
});
|
||||
|
||||
test('empty stdin -> fail-open exit 0', () => {
|
||||
const result = runHook(PRE_COMMAND_SCRIPT, '');
|
||||
assert.equal(result.status, 0);
|
||||
});
|
||||
|
||||
test('missing tool_info.command_line -> fail-open exit 0', () => {
|
||||
const result = runHook(PRE_COMMAND_SCRIPT, { tool_info: {} });
|
||||
assert.equal(result.status, 0);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// W5/W6: constants + on-disk scripts
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test('W5: WINDSURF_HOOK_EVENTS is exactly the 2 wired Cascade events', () => {
|
||||
assert.deepEqual([...WINDSURF_HOOK_EVENTS].sort(), ['pre_run_command', 'pre_write_code']);
|
||||
});
|
||||
|
||||
test('W5b: WINDSURF_EVENT_SCRIPT_MAP maps every event to a .js script', () => {
|
||||
for (const ev of WINDSURF_HOOK_EVENTS) {
|
||||
const script = WINDSURF_EVENT_SCRIPT_MAP[ev];
|
||||
assert.ok(typeof script === 'string' && script.endsWith('.js'), `${ev} must map to a .js script, got: ${script}`);
|
||||
}
|
||||
});
|
||||
|
||||
test('W6: both hook scripts exist under hooks/', () => {
|
||||
assert.ok(fs.existsSync(PRE_WRITE_SCRIPT), 'hooks/gsd-windsurf-pre-write.js must exist');
|
||||
assert.ok(fs.existsSync(PRE_COMMAND_SCRIPT), 'hooks/gsd-windsurf-pre-command.js must exist');
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// W1: writeWindsurfHooksJson — direct writer call
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('writeWindsurfHooksJson', () => {
|
||||
test('W1: installs both scripts and both hooks.json entries with the marker', (t) => {
|
||||
const targetDir = createTempDir('gsd-windsurf-writer-');
|
||||
t.after(() => cleanup(targetDir));
|
||||
const src = path.join(__dirname, '..');
|
||||
|
||||
const result = writeWindsurfHooksJson(targetDir, src, { platform: process.platform });
|
||||
assert.equal(result.hooksJsonPath, path.join(targetDir, 'hooks.json'));
|
||||
assert.ok(result.changed, 'first write must report changed=true');
|
||||
|
||||
assert.ok(fs.existsSync(path.join(targetDir, 'hooks', GSD_WINDSURF_PRE_WRITE_HOOK_SCRIPT)), 'pre-write script must be installed');
|
||||
assert.ok(fs.existsSync(path.join(targetDir, 'hooks', GSD_WINDSURF_PRE_COMMAND_HOOK_SCRIPT)), 'pre-command script must be installed');
|
||||
|
||||
const written = JSON.parse(fs.readFileSync(result.hooksJsonPath, 'utf8'));
|
||||
assert.ok(written.hooks && typeof written.hooks === 'object', 'hooks.json must have a nested hooks table');
|
||||
for (const ev of WINDSURF_HOOK_EVENTS) {
|
||||
assert.ok(Array.isArray(written.hooks[ev]), `hooks.json must have a ${ev} array`);
|
||||
assert.equal(written.hooks[ev].length, 1, `${ev} must have exactly 1 managed entry`);
|
||||
assert.equal(written.hooks[ev][0][GSD_WINDSURF_HOOK_MARKER], true, `${ev} entry must carry the GSD managed marker`);
|
||||
assert.equal(typeof written.hooks[ev][0].command, 'string', `${ev} entry command must be a bare string (Cascade shape, not Cursor's {type,command})`);
|
||||
assert.equal(written.hooks[ev][0].type, undefined, `${ev} entry must NOT have Cursor's 'type' field`);
|
||||
}
|
||||
});
|
||||
|
||||
test('W1b: is idempotent (second write reports changed=false)', (t) => {
|
||||
const targetDir = createTempDir('gsd-windsurf-writer-idem-');
|
||||
t.after(() => cleanup(targetDir));
|
||||
const src = path.join(__dirname, '..');
|
||||
|
||||
writeWindsurfHooksJson(targetDir, src, { platform: process.platform });
|
||||
const second = writeWindsurfHooksJson(targetDir, src, { platform: process.platform });
|
||||
assert.equal(second.changed, false, 're-running the writer with no changes must report changed=false');
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// W2/W3/W4: reconcileWindsurfHooksJson / removeWindsurfHooksJson
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('reconcileWindsurfHooksJson / removeWindsurfHooksJson', () => {
|
||||
function managedEntry(command) {
|
||||
return { command, [GSD_WINDSURF_HOOK_MARKER]: true };
|
||||
}
|
||||
function userEntry(command) {
|
||||
return { command };
|
||||
}
|
||||
|
||||
test('W2: preserves user-owned entries across both events', (t) => {
|
||||
const dir = createTempDir('gsd-windsurf-reconcile-');
|
||||
t.after(() => cleanup(dir));
|
||||
const hooksJsonPath = path.join(dir, 'hooks.json');
|
||||
fs.writeFileSync(hooksJsonPath, JSON.stringify({
|
||||
hooks: {
|
||||
pre_write_code: [userEntry('echo user-write-hook')],
|
||||
pre_run_command: [userEntry('echo user-command-hook')],
|
||||
},
|
||||
}, null, 2) + '\n');
|
||||
|
||||
const managedEntries = {
|
||||
pre_write_code: managedEntry('node /gsd/pre-write.js'),
|
||||
pre_run_command: managedEntry('node /gsd/pre-command.js'),
|
||||
};
|
||||
reconcileWindsurfHooksJson(hooksJsonPath, managedEntries);
|
||||
|
||||
const written = JSON.parse(fs.readFileSync(hooksJsonPath, 'utf8'));
|
||||
assert.equal(written.hooks.pre_write_code.length, 2, 'pre_write_code: 1 user + 1 managed');
|
||||
assert.equal(written.hooks.pre_run_command.length, 2, 'pre_run_command: 1 user + 1 managed');
|
||||
assert.ok(written.hooks.pre_write_code.some((e) => e.command === 'echo user-write-hook'));
|
||||
assert.ok(written.hooks.pre_write_code.some((e) => isManagedWindsurfHookEntry(e)));
|
||||
});
|
||||
|
||||
test('W3: removeWindsurfHooksJson removes hooks.json when it becomes empty', (t) => {
|
||||
const dir = createTempDir('gsd-windsurf-remove-empty-');
|
||||
t.after(() => cleanup(dir));
|
||||
const hooksJsonPath = path.join(dir, 'hooks.json');
|
||||
reconcileWindsurfHooksJson(hooksJsonPath, {
|
||||
pre_write_code: managedEntry('node /gsd/pre-write.js'),
|
||||
pre_run_command: managedEntry('node /gsd/pre-command.js'),
|
||||
});
|
||||
assert.ok(fs.existsSync(hooksJsonPath));
|
||||
|
||||
const result = removeWindsurfHooksJson(dir);
|
||||
assert.equal(result.changed, true);
|
||||
assert.equal(fs.existsSync(hooksJsonPath), false, 'empty hooks.json must be removed');
|
||||
});
|
||||
|
||||
test('W4: removeWindsurfHooksJson preserves hooks.json when user entries remain', (t) => {
|
||||
const dir = createTempDir('gsd-windsurf-remove-preserve-');
|
||||
t.after(() => cleanup(dir));
|
||||
const hooksJsonPath = path.join(dir, 'hooks.json');
|
||||
fs.writeFileSync(hooksJsonPath, JSON.stringify({
|
||||
hooks: {
|
||||
pre_write_code: [
|
||||
managedEntry('node /gsd/pre-write.js'),
|
||||
userEntry('echo user-write-hook'),
|
||||
],
|
||||
},
|
||||
}, null, 2) + '\n');
|
||||
|
||||
removeWindsurfHooksJson(dir);
|
||||
|
||||
assert.ok(fs.existsSync(hooksJsonPath), 'hooks.json must remain (user entries present)');
|
||||
const written = JSON.parse(fs.readFileSync(hooksJsonPath, 'utf8'));
|
||||
assert.equal(written.hooks.pre_write_code.length, 1);
|
||||
assert.equal(written.hooks.pre_write_code[0].command, 'echo user-write-hook');
|
||||
});
|
||||
});
|
||||
@@ -43,6 +43,12 @@ const MODULE_OWNED_HOOKS = new Set([
|
||||
'gsd-cursor-stop.js',
|
||||
'gsd-cursor-subagent-start.js',
|
||||
'gsd-cursor-subagent-stop.js',
|
||||
// Windsurf/Cascade blocking hooks — registered by writeWindsurfHooksJson via the
|
||||
// WINDSURF_EVENT_SCRIPT_MAP indirection (src/runtime-hooks-surface.cts), never a
|
||||
// literal buildHookCommand(..., '<hook-name>', ...) call this source-scan matches.
|
||||
// Validated behaviorally by tests/windsurf-hooks-bridge.test.cjs.
|
||||
'gsd-windsurf-pre-write.js',
|
||||
'gsd-windsurf-pre-command.js',
|
||||
// gsd-check-update-worker.js is an implementation detail of gsd-check-update.js
|
||||
// (spawned internally via child_process.spawn), never itself registered as a
|
||||
// hook entry point.
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"ai-integration-phase.md": 14805,
|
||||
"analyze-dependencies.md": 3887,
|
||||
"audit-fix.md": 11717,
|
||||
"audit-milestone.md": 17681,
|
||||
"audit-milestone.md": 18448,
|
||||
"audit-uat.md": 7469,
|
||||
"autonomous.md": 42474,
|
||||
"check-todos.md": 9475,
|
||||
@@ -28,7 +28,7 @@
|
||||
"execute-plan.md": 32655,
|
||||
"explore.md": 10541,
|
||||
"extract-learnings.md": 12893,
|
||||
"fast.md": 4149,
|
||||
"fast.md": 4790,
|
||||
"forensics.md": 12531,
|
||||
"graduation.md": 11622,
|
||||
"health.md": 11868,
|
||||
@@ -64,14 +64,14 @@
|
||||
"remove-phase.md": 8513,
|
||||
"remove-workspace.md": 7551,
|
||||
"resume-project.md": 17270,
|
||||
"review.md": 46297,
|
||||
"review.md": 47168,
|
||||
"scan.md": 7732,
|
||||
"secure-phase.md": 13520,
|
||||
"session-report.md": 4044,
|
||||
"settings-advanced.md": 39908,
|
||||
"settings-advanced.md": 40019,
|
||||
"settings-integrations.md": 15892,
|
||||
"settings.md": 33467,
|
||||
"ship.md": 24691,
|
||||
"ship.md": 25222,
|
||||
"sketch-wrap-up.md": 14267,
|
||||
"sketch.md": 20004,
|
||||
"smart-entry.md": 11124,
|
||||
@@ -87,7 +87,7 @@
|
||||
"ultraplan-phase.md": 10512,
|
||||
"undo.md": 10431,
|
||||
"update.md": 20914,
|
||||
"validate-phase.md": 10789,
|
||||
"validate-phase.md": 10849,
|
||||
"verify-phase.md": 40923,
|
||||
"verify-work.md": 40247
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user