Skip to content

Commit c713780

Browse files
committed
test(subagent): cover escalation policy boundary and outcomes
- ask_director without escalation never parks; routine is recorded and never registers or wakes - verification_blocked is readable through get() and listForStrip() after declined and unavailable asks and survives complete and resume - minimum-grant decisions leave the denied-call envelope unconsumed and never clear a blocker; stale questionId resolutions are rejected - long-field rendering keeps the authority and send_input lines without splitting multi-byte characters; the question stays in wake and panel - subscribers are notified once per resolution
1 parent a9f988b commit c713780

13 files changed

Lines changed: 987 additions & 36 deletions

‎src/permission/worker-grant-flow.test.ts‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,23 @@ const shellCall = (id: string, command: string): ToolCall => ({
2020
arguments: { command },
2121
});
2222

23+
/** Valid assessed ask payload for the grant-flow fixture (Blocker 1). */
24+
const assessedAsk = {
25+
policyVersion: "1",
26+
classification: "operator_decision_required",
27+
blockedOutcome: "cannot produce the authenticated verification result",
28+
unavailableDirectorPath: "the director has no credential authority",
29+
permittedAlternatives: [
30+
{
31+
attempted: "focused local test",
32+
result: "requires the missing credential",
33+
comparableConfidence: false,
34+
},
35+
],
36+
minimumAddition: "allow an authenticated read-only snapshot",
37+
declineConsequence: "implementation is complete but verification is blocked",
38+
} as const;
39+
2340
function makeParentGate(cwd: string, approve: boolean) {
2441
return createPermissionGate({
2542
approvals: [],
@@ -195,6 +212,7 @@ describe("worker grant-request flow: deny → parent replay grant → one retry"
195212
sessions.registerAsk(session.id, {
196213
question: `blocked; quote request ${envelope.requestId}`,
197214
questionId: "ask-1",
215+
assessment: assessedAsk,
198216
resolve: (answer) => {
199217
resolved = answer;
200218
},

‎src/subagent/agent-fleet.test.ts‎

Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@ async function spawnParkedAsk(
150150
askReply = params.askDirectorPort?.register({
151151
question: "which file should I edit?",
152152
questionId: "ask-1",
153+
assessment: assessedAsk,
153154
});
154155
void askReply?.catch(() => undefined);
155156
return gate.promise;
@@ -255,6 +256,31 @@ async function expectListShows(
255256
expect(defined(parsed.agents[0]).status).not.toBe("awaiting_director");
256257
}
257258

259+
/** Valid assessed ask payload for the escalation projection tests. */
260+
const assessedAsk = {
261+
policyVersion: "1",
262+
classification: "operator_decision_required",
263+
blockedOutcome: "cannot produce the authenticated verification result",
264+
unavailableDirectorPath: "the director has no credential authority",
265+
permittedAlternatives: [
266+
{
267+
attempted: "focused local test",
268+
result: "requires the missing credential",
269+
comparableConfidence: false,
270+
},
271+
],
272+
minimumAddition: "allow an authenticated read-only snapshot",
273+
declineConsequence: "implementation is complete but verification is blocked",
274+
requestedMechanism: "broad web access",
275+
minimumAuthority: "authenticated read-only snapshot",
276+
verification: {
277+
verificationOutcome: "integration verification",
278+
rootCause: "missing test credential",
279+
attemptedNarrowChecks: ["focused local test"],
280+
reducedConfidence: "integration behavior remains unverified",
281+
},
282+
} as const;
283+
258284
function parkAsk(
259285
sessions: ReturnType<typeof createSubAgentSessionStore>,
260286
mailbox: ReturnType<typeof createFleetMailbox>,
@@ -267,6 +293,7 @@ function parkAsk(
267293
sessions.registerAsk(id, {
268294
question: "which file?",
269295
questionId: "ask-1",
296+
assessment: assessedAsk,
270297
resolve: () => undefined,
271298
reject: () => undefined,
272299
}),
@@ -1752,6 +1779,62 @@ describe("list_agents", () => {
17521779
gate.resolve({ report: "done" });
17531780
});
17541781

1782+
test("wait and list JSON emit assessment and terminal_outcome with stable field names (SF1)", async () => {
1783+
const { gate, deps, list, id } = await spawnParkedAsk();
1784+
// list first: surfacing via wait would refuse a duplicate list (anti-poll).
1785+
const listed = await callFleetToolRaw(list, {});
1786+
expect(listed.isError).not.toBe(true);
1787+
const parsed = parseFleetJson(listed.content) as {
1788+
agents: {
1789+
agent_id: string;
1790+
status: string;
1791+
assessment?: Record<string, unknown>;
1792+
terminal_outcome?: Record<string, unknown>;
1793+
}[];
1794+
};
1795+
expect(defined(parsed.agents[0]).assessment).toMatchObject({
1796+
policyVersion: "1",
1797+
classification: "operator_decision_required",
1798+
minimumAuthority: "authenticated read-only snapshot",
1799+
});
1800+
const wait = waitTool(deps);
1801+
const waited = await callFleetTool(wait, {
1802+
targets: [id],
1803+
timeout_ms: 2000,
1804+
});
1805+
expect(waited.timed_out).toBe(false);
1806+
const waitRow = defined((waited.results as Record<string, unknown>[])[0]);
1807+
expect(waitRow.assessment).toMatchObject({
1808+
policyVersion: "1",
1809+
classification: "operator_decision_required",
1810+
minimumAuthority: "authenticated read-only snapshot",
1811+
});
1812+
// Resolve through the public store API; the terminal outcome must then
1813+
// surface in list JSON (Blocker 2 visibility).
1814+
expect(
1815+
deps.sessions.resolveEscalationAsk(id, {
1816+
kind: "declined",
1817+
answer: "No credential grant.",
1818+
questionId: "ask-1",
1819+
}),
1820+
).toBe(true);
1821+
const after = await callFleetToolRaw(list, {});
1822+
expect(after.isError).not.toBe(true);
1823+
const parsedAfter = parseFleetJson(after.content) as {
1824+
agents: {
1825+
agent_id: string;
1826+
status: string;
1827+
assessment?: Record<string, unknown>;
1828+
terminal_outcome?: Record<string, unknown>;
1829+
}[];
1830+
};
1831+
expect(defined(parsedAfter.agents[0]).terminal_outcome).toMatchObject({
1832+
kind: "verification_blocked",
1833+
resolution: "declined",
1834+
});
1835+
gate.resolve({ report: "done" });
1836+
});
1837+
17551838
test("errors after wait_agents surfaces awaiting_director with a question", async () => {
17561839
const { gate, deps, list, id } = await spawnParkedAsk();
17571840
const wait = waitTool(deps);
@@ -1839,6 +1922,7 @@ describe("list_agents", () => {
18391922
.register({
18401923
question: "which test should I add?",
18411924
questionId: "ask-2",
1925+
assessment: assessedAsk,
18421926
})
18431927
.catch(() => undefined);
18441928
await waitUntilAwaitingDirector(deps.fleetRecords, deps.sessions, id);
@@ -2162,6 +2246,7 @@ describe("ask_director wait handshake", () => {
21622246
?.register({
21632247
question: "which file?",
21642248
questionId: "ask-1",
2249+
assessment: assessedAsk,
21652250
})
21662251
.catch(() => {
21672252
// Session settlement rejects an unanswered ask; this test does not await it.
@@ -2241,7 +2326,11 @@ describe("ask_director wait handshake", () => {
22412326
process.on("unhandledRejection", onUnhandled);
22422327
try {
22432328
expect(() =>
2244-
defined(port).register({ question: "late?", questionId: "ask-1" }),
2329+
defined(port).register({
2330+
question: "late?",
2331+
questionId: "ask-1",
2332+
assessment: assessedAsk,
2333+
}),
22452334
).toThrow("could not register");
22462335
await new Promise((resolve) => setTimeout(resolve, 0));
22472336
expect(unhandled).toEqual([]);

0 commit comments

Comments
 (0)