Files
msd-core/tests/bug-1760-state-prune-noop-template-field.test.cjs
Tom Boucher da368311ea feat(#1680): ADR-1239 Phase C-1 — imperative embedding adapter (composes loadRegistry) [AC2] (#1803)
* feat(#1680): ADR-1239 Phase C-1 — imperative embedding adapter (composes loadRegistry) [AC2]

Phase 3 slice 2 (AC2). The engine-as-library path: createImperativeAdapter
composes loadRegistry({includeInstalled:true}) — first-party-wins + consent +
fail-closed gates, identical trust semantics to the CLI — and binds the engine
surface behind the SAME HostIntegrationInterface the declarative adapter (AC1)
satisfies, plus a registry accessor for the composed capability set.

- src/adapter-imperative.cts: createImperativeAdapter({runtime}, {loadOptions})
  → ImperativeAdapter (kind:'imperative' + .registry + install/uninstall
  delegating to install-engine). Thin: delegates the loop, does not reimplement.
- tests/adapter-imperative.test.cjs: kind (16 runtimes), registry composition
  (loadRegistry called with includeInstalled:true), loadOptions pass-through,
  install/uninstall delegation, fail-closed construction.
- eslint.config.mjs + docs/INVENTORY-MANIFEST.json: ADR-457 ignores entry +
  cli_modules entry for the new emitted .cjs (the two drift gates that bit AC1,
  applied proactively here).

Concrete host binding (OpenCode/VS Code/pi) deferred to Phase 5 (#1682).

* test: remove dead readStateMd helper from bug-1760 test

readStateMd was defined but never called (writeStateMd is the only state-md
helper this test uses). Clears the lone no-unused-vars warning so the repo
lints fully clean (0 problems). No behavior change — test still passes 2/2.

* chore(changeset): add Changed fragment for imperative embedding adapter (#1680)

* fix(adapter-imperative): reword comment to avoid injection-scan substring match

The prompt-injection scan regex 'act\s+as\s+(?:a|an|the)' was matching the
'act as the' substring inside 'contract as the declarative adapter' (contrACT
AS THE). Reword 'contract as' -> 'shape as' — no 'act' substring, identical
meaning. Clears the 'lib source files are clean' + 'codebase prompt injection
scan' security-gate failures.
2026-06-28 11:10:59 -04:00

78 lines
2.9 KiB
JavaScript

'use strict';
// Regression test for issue #1760 — `state prune` no-ops on template-conformant
// STATE.md because it reads `Current Phase` only, never the `Phase: X of Y` line
// the canonical template emits.
//
// ADR-1769 Phase 7 fix: derive the current phase with a `Phase` / `Current Phase`
// fallback (mirroring buildStateFrontmatter), so prune engages on template STATE.md.
const { describe, test, beforeEach, afterEach } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const { runGsdTools, createTempProject, cleanup } = require('./helpers.cjs');
function writeStateMd(tmpDir, content) {
fs.writeFileSync(path.join(tmpDir, '.planning', 'STATE.md'), content);
}
describe('#1760: state prune engages on template-conformant STATE.md (Phase: X of Y)', () => {
let tmpDir;
beforeEach(() => { tmpDir = createTempProject(); });
afterEach(() => { cleanup(tmpDir); });
test('prune engages when STATE.md uses "Phase: N of M" (no Current Phase field)', () => {
// Template-conformant STATE.md: Current Position uses `Phase: 10 of 15`, and
// there is NO `**Current Phase:**` body field. Pre-fix this made prune bail
// with "Only 0 phases — nothing to prune".
writeStateMd(tmpDir, [
'# Session State',
'',
'## Current Position',
'',
'Phase: 10 of 15',
'Plan: 2 of 4',
'Status: Executing Phase 10',
'',
'## Decisions',
'',
'- [Phase 1]: Old decision',
'- [Phase 3]: Older decision',
'- [Phase 9]: Recent decision',
'',
].join('\n'));
const result = runGsdTools('state prune --keep-recent 3 --dry-run', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const out = JSON.parse(result.output);
// Pre-fix: { pruned: false, reason: 'Only 0 phases — nothing to prune...' }.
// Post-fix: prune engages and reports a real cutoff_phase (10 - 3 = 7).
assert.strictEqual(out.pruned, false, 'dry-run must report pruned:false');
assert.ok(out.reason === undefined || !/Only 0 phases/i.test(String(out.reason)),
`prune must not bail with "Only 0 phases" on template STATE.md; got reason=${JSON.stringify(out.reason)}`);
assert.strictEqual(out.cutoff_phase, 7,
`cutoff_phase must be 7 (current 10 - keep-recent 3); got ${JSON.stringify(out.cutoff_phase)}`);
});
test('prune still engages when "Current Phase:" IS present (no regression)', () => {
writeStateMd(tmpDir, [
'# Session State',
'',
'**Current Phase:** 10',
'',
'## Decisions',
'',
'- [Phase 1]: Old decision',
'- [Phase 9]: Recent decision',
'',
].join('\n'));
const result = runGsdTools('state prune --keep-recent 3 --dry-run', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const out = JSON.parse(result.output);
assert.strictEqual(out.cutoff_phase, 7);
});
});