Files
msd-core/tests/claude-imperative-reference.test.cjs
Tom Boucher c66b010052 chore(#3508): site-scoped allow-test-rule suppression (#3512)
Phase 4 of #3464, following #3465, #3466 and #3502. Those cut the ceiling
305 -> 278 and made the rule accurate. This closes the remaining structural
weakness: suppression was FILE-WIDE, so a single justified exemption silently
absolved every other source-grep in that file, forever, including ones added
later by someone else.

hasAllowAnnotation did comments.some(...) over the whole file and returned {}
early. A marker is now checked per report: a violation is suppressed only by a
marker on its own line, or on a line above it with nothing but blank lines and
other comments in between, bounded by MAX_MARKER_LOOKAHEAD_LINES = 8.

The bound is comment-purity rather than raw distance, and that distinction is
load-bearing: an intervening line of real code (a `test(...)` opener, say)
ends the window even when the marker is physically close. Chosen from the
actual placements in the affected files rather than picked a priori -- the
repo's convention puts several lines of prose rationale between the marker and
the code, so a tighter rule would have invalidated legitimate existing markers
and forced churn for no correctness gain.

Measured before writing any code, by running the real rule with the
suppression check neutralized across all 1194 files its globs match: 14
violation sites in 8 files, and ZERO in files carrying no marker -- so the
green build was legitimate, and the entire migration surface was those 14.

11 sites were mechanical: an existing marker already stated the right reason,
it just sat too far away. Those were relocated to their call sites with the
original #NNN citations preserved.

Three were orphans -- the file's markers were about an entirely different
concern and nobody had ever justified these reads. All three are fixed
BEHAVIORALLY, with no new markers:

  install-minimal-hooks.test.cjs:975 asserted
  src.includes('gsd-update-check') && src.includes('replace(') against
  bin/install.js. It now calls the exported stripStaleGsdHookBlocks() on a
  legacy TOML fixture and asserts the actual stripped output. This is the
  case this phase was opened around: it could be added with no review friction
  and stay invisible indefinitely under file-wide amnesty.

  config.test.cjs:1917 regex-tested src/init.cts for detectGitCreateTag. It now
  drives `init complete-milestone` and asserts the git_create_tag field.

  config-schema.property.test.cjs:1107 did the same for detectFallowConfig; it
  now drives `init code-review` and asserts fallow_enabled.

Each was proven RED against a broken production file and GREEN against the real
one, with src/init.cts and bin/install.js confirmed byte-identical afterwards.

Marker lines in the 8 files went 20 -> 24, against a filed expectation of
"must not increase" (projected 14). That projection was wrong and is corrected
on #3508 rather than met by deletion. It assumed every existing marker was a
distant blanket that site-scoping would consolidate. Some are already
site-adjacent and guard real source-greps the rule CANNOT detect -- verified in
install-minimal-hooks.test.cjs:2686-2757, where seven markers each sit directly
above a readFileSync(reloadScript) + .includes() pair reading
hooks/gsd-config-reload.js. Removing them to hit a number would have repeated
the Phase 1 mistake: deleting markers on "the rule doesn't fire" evidence when
the rule provably cannot see the violation.

An earlier revision of this commit message attributed that invisibility to the
#3502 dynamic-path blind spot, on the grounds that reloadScript is a variable.
Adversarial review caught that as a false causal claim and it is corrected
here. looksLikeSourcePath's hasSourceDir regex is
/['"](?:bin|lib|gsd-core|src)['"]/i, and those reads target hooks/ -- so a
fully literal path.join(ROOT,'hooks','gsd-config-reload.js') is equally
invisible. The variable indirection is irrelevant. This is a FIFTH, distinct
blind spot: the source-dir allowlist omits hooks/, which is a real shipped
production directory (eslint.config.mjs registers its own rule block for
hooks/**/*.js). Recorded in 40-design.md Known limits and left for a follow-on
phase -- widening the allowlist is unmeasured, and measuring before widening is
the discipline #3502 established. The conclusion was right; the stated
mechanism was not, and asserting an unverified cause is the error being
corrected.

The honest metric is not fewer markers. It is that every marker now sits
adjacent to the specific read it justifies instead of absolving a whole file.
Site-scoping turns one blanket marker covering N sites into N site markers by
design; the count rising is the mechanism working.

A second review finding is fixed here too. Suppression originally keyed only
off the text-search line, so a marker placed directly above the readFileSync()
call -- the intuitive place to annotate "this read is fine" -- did NOT suppress
when the search sat on the following line, because the read's own assignment
line breaks comment-purity. It failed safe (a loud error, never silent
suppression), but it was a trap contributors would hit, and it contradicted
this change's own claim that the placement rule would not force churn. A
violation is now suppressed by a marker adjacent to EITHER the search site or
the originating read. The violation is fundamentally the read+search pair, so
annotating either half is legitimate, and it stays strictly site-scoped -- the
decisive isolation row still holds.

17 RuleTester rows cover the new semantics. The decisive one asserts that a
marker adjacent to one violation does NOT suppress an unrelated violation
elsewhere in the same file -- exactly 1 error, reported at the second site.
Teeth-checked by reverting the predicate to file-wide, confirming that row and
two others flip pass->fail, then restoring. Two pre-existing RuleTester cases
that asserted the old file-wide semantics were corrected.

Compatibility held where it matters: 277 marker-bearing files have no
detectable violation at all, and site-scoping makes their markers no-ops rather
than errors. All stay green, untouched.

Ceiling unchanged at 278; lint-allow-test-rule-refs reports 278/278.

Deliberately not done here, and recorded for the follow-on phase: the same
measurement found only 8 of 285 marker-bearing files contain a detectable
violation. That suggests a large honest ceiling drop, but "the rule doesn't
fire" is the unsound oracle that forced the Phase 1 revert of 295 files, and
the rule still has documented blind spots -- as install-minimal-hooks itself
demonstrates above. It needs two independent signals agreeing, which is only
credible now that the rule is accurate. With suppression site-scoped, "an
effective exemption" is finally well-defined, which is what makes re-pointing
the ratchet at effective exemptions -- rather than at marker-text presence --
the natural next step.

Closes #3508

Co-authored-by: sim <sim@local>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 19:02:14 -04:00

188 lines
10 KiB
JavaScript

'use strict';
/**
* claude imperative reference host — ADR-1239 Phase D / #2086 (EoS/claude).
*
* claude is GSD's tier-1 reference host — "golden parity vs. the Claude reference
* host" (ADR-1239 line 146). This proves claude is driven through the PUBLIC
* Host-Integration Interface (the imperative adapter), that its negotiated axes
* classify + negotiate correctly, that negotiation FAILS CLOSED on a corrupted
* descriptor, and that the migration retired the hardcoded `runtime === 'claude'`
* string-equality branches in bin/install.js (folded into descriptor-driven
* `runtime.hostBehaviors`).
*
* Mirrors tests/pi-imperative-reference.test.cjs + tests/vscode-ide-reference.test.cjs
* but binds against the REAL descriptor + the REAL installer source, since claude
* (unlike pi/vscode) has a real production install being folded in.
*/
const { test } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const { createImperativeAdapter } = require('../gsd-core/bin/lib/adapter-imperative.cjs');
const {
profileOf,
negotiateHostCapabilities,
PROTOCOL_VERSION,
PROFILE_BASELINES,
UNDOCUMENTED,
} = require('../gsd-core/bin/lib/host-integration.cjs');
const CLAUDE_CAP = JSON.parse(
fs.readFileSync(path.join(__dirname, '..', 'capabilities', 'claude', 'capability.json'), 'utf8'),
);
const CLAUDE_AXES = CLAUDE_CAP.runtime.hostIntegration;
// Requiring the installer (not as main) never runs the CLI; GSD_TEST_MODE is set
// defensively to match the install-test convention.
process.env.GSD_TEST_MODE = process.env.GSD_TEST_MODE || '1';
const installMod = require('../bin/install.js');
// -- AC2: driven through the public interface (imperative adapter) -----------
test('createImperativeAdapter classifies claude as imperative + composes the registry', () => {
const adapter = createImperativeAdapter({ runtime: 'claude' });
assert.equal(adapter.kind, 'imperative', "claude embeddingMode is imperative -> adapter kind must be 'imperative'");
assert.equal(adapter.runtime, 'claude');
assert.ok(adapter.registry && typeof adapter.registry === 'object', 'imperative adapter exposes the composed capability registry');
assert.equal(typeof adapter.install, 'function');
assert.equal(typeof adapter.uninstall, 'function');
});
test('claude descriptor embeddingMode agrees with the imperative adapter kind', () => {
assert.equal(CLAUDE_AXES.embeddingMode, 'imperative', 'capability.json must declare embeddingMode: imperative for the imperative binding');
});
test('claude axes classify as the programmatic-cli reference profile', () => {
assert.equal(profileOf(CLAUDE_AXES), 'programmatic-cli');
assert.notEqual(profileOf(CLAUDE_AXES), 'ide');
});
// -- AC3: every negotiated axis populated, negotiation is clean ---------------
test('claude negotiates its declared axes verbatim (no degradation of documented values)', () => {
const result = negotiateHostCapabilities({ ...CLAUDE_AXES, protocolVersion: PROTOCOL_VERSION });
assert.equal(result.protocolVersion, PROTOCOL_VERSION);
// Every declared scalar axis survives negotiation unchanged (all are known+documented).
assert.equal(result.effective.embeddingMode, 'imperative');
assert.equal(result.effective.commandSurface, CLAUDE_AXES.commandSurface);
assert.equal(result.effective.modelMode, CLAUDE_AXES.modelMode);
assert.equal(result.effective.hookBus, CLAUDE_AXES.hookBus);
assert.equal(result.effective.stateIO, CLAUDE_AXES.stateIO);
assert.equal(result.effective.transport, CLAUDE_AXES.transport);
assert.equal(result.effective.runtime, CLAUDE_AXES.runtime);
// No `undocumented` sentinel anywhere in claude's declared axes.
assert.ok(
!JSON.stringify(CLAUDE_AXES).includes(UNDOCUMENTED),
'claude descriptor must carry no `undocumented` sentinel (fully doc-sourced)',
);
});
// -- AC5: negotiation fails CLOSED on a corrupted / partial descriptor --------
test('negotiateHostCapabilities never throws for claude — even fully corrupted input', () => {
assert.doesNotThrow(() => negotiateHostCapabilities({}));
assert.doesNotThrow(() => negotiateHostCapabilities({ embeddingMode: UNDOCUMENTED }));
assert.doesNotThrow(() => negotiateHostCapabilities({ embeddingMode: 'wildly-unknown-future-value' }));
});
test('a partial/empty claude descriptor degrades to the safe floor — NOT the full programmatic-cli baseline', () => {
const result = negotiateHostCapabilities({});
// Fail-closed floor (SAFE_DEFAULTS), not the rich profile baseline.
assert.equal(result.effective.embeddingMode, 'declarative', 'omitted embeddingMode degrades closed to declarative');
assert.equal(result.effective.hookBus, 'none', 'omitted hookBus degrades closed to none');
assert.equal(result.effective.commandSurface, 'prose-only', 'omitted commandSurface degrades closed to prose-only');
assert.equal(result.effective.dispatch.namedDispatch, false, 'omitted dispatch degrades closed (no named dispatch)');
assert.notDeepEqual(
result.effective,
PROFILE_BASELINES['programmatic-cli'],
'a corrupted descriptor MUST NOT silently reuse the full programmatic-cli baseline',
);
assert.ok(result.warnings.length > 0, 'degrade-closed must surface warnings');
});
test('an undocumented/unknown claude axis value is not trusted (degraded closed per-axis)', () => {
const corrupted = { ...CLAUDE_AXES, embeddingMode: UNDOCUMENTED, hookBus: 'unknown-bus-kind' };
const result = negotiateHostCapabilities(corrupted);
assert.equal(result.effective.embeddingMode, 'declarative', 'undocumented embeddingMode -> safe floor');
assert.equal(result.effective.hookBus, 'none', 'unknown hookBus value -> safe floor');
// Untouched axes still negotiate to their declared (documented) values.
assert.equal(result.effective.stateIO, CLAUDE_AXES.stateIO);
});
// -- AC2: the hardcoded string-equality branches are retired ------------------
test('claude descriptor declares runtime.hostBehaviors (the folded-in host behaviors)', () => {
const hb = CLAUDE_CAP.runtime.hostBehaviors;
assert.ok(hb && typeof hb === 'object', 'capabilities/claude/capability.json must declare runtime.hostBehaviors');
// The behaviors that replaced the 13 `runtime === 'claude'` branches.
assert.equal(hb.permissionsSchema, 'claude');
assert.equal(hb.localInstallStyle, 'legacy-flat');
assert.equal(hb.sourceMarkerFile, '.gsd-source');
assert.equal(hb.authorsCanonicalWorkflow, true);
assert.equal(hb.ownsClaudePaths, true);
assert.equal(hb.nativeModelAliases, true);
assert.equal(hb.skillsGlobalOnboarding, true);
assert.equal(hb.attributionSource, 'settings-json-commit');
assert.deepEqual(hb.agentFrontmatterExtensions, ['effort']);
assert.equal(hb.settingsFileByScope.local, 'settings.local.json');
assert.equal(hb.settingsFileByScope.global, 'settings.json');
});
test('bin/install.js contains no `runtime === "claude"` / `runtime !== "claude"` string-equality branches (AC2)', () => {
const src = fs.readFileSync(path.join(__dirname, '..', 'bin', 'install.js'), 'utf8');
// Strip comments + backtick/inline-code spans so PROSE mentions of the old
// pattern (a comment explaining "not a string-equality branch") do not
// false-positive — only LIVE code counts.
// allow-test-rule: structural-regression-guard — AC2 requires asserting no `runtime === 'claude'` string-equality branch remains in bin/install.js — the descriptor-migration contract is a property of the source text, so a source-grep is the only faithful check (#2086)
const codeOnly = src
// eslint-disable-next-line local/no-unbounded-quantifier -- parses this repo's own bounded bin/install.js source, not adversarial input
.replace(/\/\*[\s\S]*?\*\//g, '') // block comments
// eslint-disable-next-line local/no-unbounded-quantifier -- parses this repo's own bounded bin/install.js source, not adversarial input
.replace(/\/\/[^\r\n]*/g, '') // line comments (CRLF-safe)
// eslint-disable-next-line local/no-unbounded-quantifier -- parses this repo's own bounded bin/install.js source, not adversarial input
.replace(/`[^`]*`/g, ''); // backtick / inline-code spans
// allow-test-rule: structural-regression-guard — same #2086 source-grep as above; `.match()` is how the stripped source is scanned for the retired string-equality branch (#2086)
const offenders = codeOnly.match(/runtime\s*[!=]==\s*'claude'/g) || [];
assert.deepEqual(
offenders,
[],
`AC2: every hardcoded runtime==='claude'/!=='claude' branch must be descriptor-driven; found: ${offenders.join(', ')}`,
);
});
// -- Reviewer #2106 (elevated): #338 privacy fail-safe on registry-load failure --
// If the first-party capability registry fails to load, `_hostBehaviors('claude')`
// would return {} and route a claude LOCAL install to the repo-shared settings.json
// instead of the gitignored settings.local.json — silently reintroducing #338. The
// reference host must degrade CLOSED (safe) for its privacy-critical keys.
test('claude #338-critical host behaviors degrade CLOSED when the capability registry cannot load', () => {
// Simulate a broken bundle: registry is undefined.
const degraded = installMod._resolveHostBehaviors('claude', undefined);
assert.equal(degraded.settingsFileByScope.local, 'settings.local.json',
'#338: a claude LOCAL install must still route to the gitignored settings.local.json');
assert.equal(degraded.settingsFileByScope.global, 'settings.json');
assert.equal(degraded.permissionsSchema, 'claude', 'permission cleanup/merge must still apply');
assert.equal(degraded.sourceMarkerFile, '.gsd-source');
});
test('with the registry present, claude host behaviors come from the live descriptor (superset of the fail-safe floor)', () => {
const reg = require('../gsd-core/bin/lib/capability-registry.cjs');
const declared = installMod._resolveHostBehaviors('claude', reg);
assert.equal(declared.settingsFileByScope.local, 'settings.local.json');
assert.equal(declared.localInstallStyle, 'legacy-flat');
assert.equal(declared.authorsCanonicalWorkflow, true);
// The fail-safe floor is a strict subset of what the descriptor declares.
for (const k of Object.keys(installMod.FALLBACK_HOST_BEHAVIORS.claude)) {
assert.ok(k in declared, `descriptor must still declare the #338-critical key '${k}'`);
}
});
test('a non-reference runtime has no fail-safe fallback (degrades to the generic path)', () => {
assert.deepEqual(installMod._resolveHostBehaviors('opencode', undefined), {});
assert.deepEqual(installMod._resolveHostBehaviors('codex', undefined), {});
});