Skip to content

Commit 0a176f5

Browse files
FE-867: tighten agent-extension-host neutrality & witness proofs
Lean on no-imports as the load-bearing neutrality guarantee and drop the redundant forbidden-substring denylist (a dependency-free module cannot reference an execute-only or SDK type, so neutrality is structural, not a name list to maintain). Make the interview exploration plugin proof bidirectional — its capability ids must exactly equal Object.keys(createExplorationTools(...)), catching phantom as well as missing tools. Document that the three native interviewer tools are covered type-level only (superset) because constructing them needs a live DB. Review findings #1 and #2 from ln-review. Zero behavior change. Amp-Thread-ID: https://ampcode.com/threads/T-019ecb9a-9a08-733b-833d-76885fc8243a Co-authored-by: Amp <amp@ampcode.com>
1 parent a798f1d commit 0a176f5

1 file changed

Lines changed: 17 additions & 30 deletions

File tree

src/agent-extension-host.test.ts

Lines changed: 17 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -94,30 +94,13 @@ const interviewWitness = {
9494
} as const satisfies AgentExtensionConsumerWitness;
9595

9696
describe('agent-extension-host contract is a mode-neutral core', () => {
97-
it('the contract module is dependency-free and names no execute-only concept', () => {
97+
it('the contract module is dependency-free, which is what keeps it mode-neutral', () => {
9898
const src = readFileSync(join(here, 'agent-extension-host.ts'), 'utf8');
99-
// Mode-neutral and SDK-agnostic ⇒ no imports at all.
99+
// No imports is the load-bearing guarantee: a module that imports nothing
100+
// cannot reference an `execute`-only type (Slice/Epic/Plan/Toolchain/worktree…)
101+
// or an SDK type. That makes neutrality structural rather than a denylist of
102+
// names we have to remember to update.
100103
expect(src).not.toMatch(/^\s*import[\s{*]/m);
101-
// No `execute`-only domain concepts may leak into the neutral core. Tokens are
102-
// checked outside the doc comment so the explanatory prose above can name them.
103-
const code = src
104-
.split('\n')
105-
.filter((line) => !line.trimStart().startsWith('//'))
106-
.join('\n');
107-
const forbidden = [
108-
'Slice',
109-
'Epic',
110-
'Plan',
111-
'TestRunner',
112-
'Toolchain',
113-
'worktree',
114-
'sandboxDir',
115-
'pi-coding-agent',
116-
'ToolLoopAgent',
117-
];
118-
for (const token of forbidden) {
119-
expect(code, `neutral core must not mention "${token}"`).not.toContain(token);
120-
}
121104
});
122105

123106
it('a consumer witness only loads plugins of its own mode (per-mode registration)', () => {
@@ -136,14 +119,18 @@ describe('two-consumer proof — both real surfaces fit the host contract', () =
136119
expect(registered).toEqual(actual);
137120
});
138121

139-
it('the interview elicit exploration family matches the real tool surface', () => {
140-
// `createExplorationTools` is DB-free, so the exploration capability ids are
141-
// proven against live code rather than hardcoded — guarding against drift.
142-
const actualExploration = Object.keys(createExplorationTools(here));
143-
const registered = new Set(flattenCapabilityIds(interviewWitness));
144-
for (const id of actualExploration) {
145-
expect(registered.has(id), `witness missing interview tool "${id}"`).toBe(true);
146-
}
122+
it('the interview exploration plugin matches the real tool surface exactly', () => {
123+
// `createExplorationTools` is DB-free, so this family is proven bidirectionally
124+
// against live code: the witness may neither omit a real tool nor invent a
125+
// phantom one. The three native interviewer tools (ask_question /
126+
// present_preface / propose_phase_closure) can't be checked this way —
127+
// constructing them needs a live DB — so their coverage is type-level only
128+
// (the `keyof InterviewerTools` assertion below), which is superset-only: it
129+
// proves the witness omits no real tool, not that it invents none.
130+
const explorationPlugin = interviewWitness.plugins.find((p) => p.id === 'elicit.workspace-exploration');
131+
const witnessed = new Set(explorationPlugin?.capabilities.map((c) => c.id));
132+
const actual = new Set(Object.keys(createExplorationTools(here)));
133+
expect(witnessed).toEqual(actual);
147134
});
148135

149136
it('the interview witness covers every interviewer tool id (type-enforced under lint --type-check)', () => {

0 commit comments

Comments
 (0)