Skip to content

Commit 757eb59

Browse files
committed
test(e2e): extract the park step from the approval reload helper
1 parent 31a5741 commit 757eb59

1 file changed

Lines changed: 77 additions & 64 deletions

File tree

‎e2e/reactor-approval-reload.test.ts‎

Lines changed: 77 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -253,19 +253,14 @@ interface DecideThenReloadOpts {
253253
}
254254

255255
/**
256-
* Parks `call` on an ask, settles it per `opts.decision` (approve by
257-
* default), and once `reloadWhen` holds for the settled run, rebuilds the
258-
* agent on the same store. `reloadWhen` sees only events from after the
259-
* decision.
256+
* Opens a session and parks `call` on an ask. `live.session` is the agent's
257+
* current session; a rebuild replaces it, and cleanup closes whichever is
258+
* current.
260259
*/
261-
async function decideThenReload(
260+
async function parkOnAsk(
262261
call: { name: string; args: Record<string, unknown> },
263-
reloadWhen: (
264-
session: IntegrationSession,
265-
events: ReactorEmittedEvent[],
266-
) => boolean,
267-
opts: DecideThenReloadOpts = {},
268-
): Promise<Reloaded> {
262+
opts: Pick<DecideThenReloadOpts, "replyAfterDecision" | "prepare">,
263+
) {
269264
// Held from before the park so the expire case can fire the original gate
270265
// timer; the approve and reject cases clear it unfired.
271266
const clock = holdLongTimers();
@@ -276,61 +271,97 @@ async function decideThenReload(
276271
authorize: createReactorAuthorize(ctx.gate),
277272
pinToolSetAtBuild: true,
278273
};
279-
let session = await openIntegrationSession(sessionOpts);
280-
cleanups.push(() => closeIntegrationSession(session));
281-
await opts.prepare?.(session);
274+
const live = { session: await openIntegrationSession(sessionOpts) };
275+
cleanups.push(() => closeIntegrationSession(live.session));
276+
await opts.prepare?.(live.session);
282277

283-
session.harness.scenario.replyOnce("anthropic", {
278+
live.session.harness.scenario.replyOnce("anthropic", {
284279
toolCalls: [call],
285280
});
286-
if (opts.replyAfterDecision === undefined) session.harness.scenario.stall();
281+
if (opts.replyAfterDecision === undefined)
282+
live.session.harness.scenario.stall();
287283
else
288-
session.harness.scenario.replyOnce("anthropic", {
284+
live.session.harness.scenario.replyOnce("anthropic", {
289285
text: opts.replyAfterDecision,
290286
});
291287

292-
const turn = await runUntilSuspended(session, "Probe the local port.");
293-
if (turn.result.type !== "suspended") throw new Error("did not park");
294-
const { correlationId } = turn.result;
295-
const parked = (await session.storage.load()).pendingOperations.find(
296-
(op) => op.correlationId === correlationId,
288+
const turn = await runUntilSuspended(live.session, "Probe the local port.");
289+
const suspended = turn.result;
290+
if (suspended.type !== "suspended") throw new Error("did not park");
291+
const parked = (await live.session.storage.load()).pendingOperations.find(
292+
(op) => op.correlationId === suspended.correlationId,
297293
);
298294
const callId = parked?.suspendedCall?.id;
299295
const deadline = parked?.timeoutAt;
300296
if (callId === undefined || deadline === undefined) {
301297
throw new Error("parked call has no persisted call id or deadline");
302298
}
299+
return {
300+
live,
301+
sessionOpts,
302+
ctx,
303+
clock,
304+
suspended,
305+
events: turn.events,
306+
callId,
307+
deadline,
308+
};
309+
}
310+
311+
/**
312+
* Parks `call` on an ask, settles it per `opts.decision` (approve by
313+
* default), and once `reloadWhen` holds for the settled run, rebuilds the
314+
* agent on the same store. `reloadWhen` sees only events from after the
315+
* decision.
316+
*/
317+
async function decideThenReload(
318+
call: { name: string; args: Record<string, unknown> },
319+
reloadWhen: (
320+
session: IntegrationSession,
321+
events: ReactorEmittedEvent[],
322+
) => boolean,
323+
opts: DecideThenReloadOpts = {},
324+
): Promise<Reloaded> {
325+
const { live, sessionOpts, ctx, clock, suspended, events, callId, deadline } =
326+
await parkOnAsk(call, opts);
303327

304328
const decision = opts.decision ?? "approve";
305-
const decidedAt = turn.events.length;
329+
const decidedAt = events.length;
306330
if (decision === "expire") {
307331
clock.advanceTo(deadline + 1_000);
308332
} else {
309333
const resume = createApprovalResume({
310-
getAgent: () => session.agent,
334+
getAgent: () => live.session.agent,
311335
gate: ctx.gate,
312336
resolveParkedCallId: (id) =>
313-
resolveParkedCallIdFromStore(session.storage, id),
337+
resolveParkedCallIdFromStore(live.session.storage, id),
314338
});
315-
const handling = resume.handle(turn.result);
339+
const handling = resume.handle(suspended);
316340
await waitFor(ctx.asked, "approval prompt");
317-
opts.afterApprovalOpened?.(session);
341+
opts.afterApprovalOpened?.(live.session);
318342
if (decision === "approve") ctx.approve();
319343
else ctx.reject();
320344
expect(await handling).toBe(true);
321345
}
322-
void session.harness
346+
void live.session.harness
323347
.run({ wallClockBudgetMs: 30_000 })
324348
.catch(() => undefined);
325-
const before = session;
349+
const before = live.session;
326350
await waitFor(
327-
() => reloadWhen(before, turn.events.slice(decidedAt)),
351+
() => reloadWhen(before, events.slice(decidedAt)),
328352
"reload point",
329353
);
330354

331-
if (opts.rebuildInPlace === true) await session.rebuild();
332-
else session = await rebuildIntegrationSession(session, sessionOpts);
333-
return { session, events: collect(session), callId, deadline, clock };
355+
if (opts.rebuildInPlace === true) await live.session.rebuild();
356+
else
357+
live.session = await rebuildIntegrationSession(live.session, sessionOpts);
358+
return {
359+
session: live.session,
360+
events: collect(live.session),
361+
callId,
362+
deadline,
363+
clock,
364+
};
334365
}
335366

336367
// Past both the original deadline and a full approval window freshly armed
@@ -601,37 +632,19 @@ describe("integration: a rejection the reactor accepted but never ran, then reop
601632
test.serial(
602633
"the reopened agent loads the denial once and no parked call, and the old deadline adds no result",
603634
async () => {
604-
const clock = holdLongTimers();
605-
cleanups.push(() => clock.restore());
606-
const ctx = deferredApprovalGate();
607-
const sessionOpts = {
608-
permissionGate: ctx.gate,
609-
authorize: createReactorAuthorize(ctx.gate),
610-
pinToolSetAtBuild: true,
611-
};
612-
const live = { session: await openIntegrationSession(sessionOpts) };
613-
cleanups.push(() => closeIntegrationSession(live.session));
614-
live.session.harness.scenario.replyOnce("anthropic", {
615-
toolCalls: [
616-
{ name: "run_shell", args: { command: CLOSED_PORT_PROBE } },
617-
],
618-
});
619-
live.session.harness.scenario.stall();
620-
621-
const turn = await runUntilSuspended(
622-
live.session,
623-
"Probe the local port.",
624-
);
625-
const suspended = turn.result;
626-
if (suspended.type !== "suspended") throw new Error("did not park");
627-
const parked = (await live.session.storage.load()).pendingOperations.find(
628-
(op) => op.correlationId === suspended.correlationId,
635+
const {
636+
live,
637+
sessionOpts,
638+
ctx,
639+
clock,
640+
suspended,
641+
events,
642+
callId,
643+
deadline,
644+
} = await parkOnAsk(
645+
{ name: "run_shell", args: { command: CLOSED_PORT_PROBE } },
646+
{},
629647
);
630-
const callId = parked?.suspendedCall?.id;
631-
const deadline = parked?.timeoutAt;
632-
if (callId === undefined || deadline === undefined) {
633-
throw new Error("parked call has no persisted call id or deadline");
634-
}
635648

636649
void live.session.agent.send("Still there?").catch(() => undefined);
637650
await waitFor(
@@ -649,7 +662,7 @@ describe("integration: a rejection the reactor accepted but never ran, then reop
649662
ctx.reject();
650663
expect(await handling).toBe(true);
651664
await waitFor(
652-
() => turn.events.some((e) => e.type === "message.correlated"),
665+
() => events.some((e) => e.type === "message.correlated"),
653666
"rejection accepted",
654667
);
655668

0 commit comments

Comments
 (0)