test(core): cover unfinished invocation guard before turn reclamation (#13257)

* test(core): cover unfinished invocation guard before turn reclamation

* test(core): also pin the prepared sub-case of the beginTurn unfinished guard

* test(core): pin the cancel_requested sub-case and reflow for Prettier

* test(core): harden the cancel_requested test per review round 3

- release the held onConfirm gate in a finally block so a failing
  assertion cannot stall afterEach dispose() behind the gate
- assert the confirmation rejection reason instead of a bare await,
  matching the sibling cancellation tests

* chore(ci): retrigger validation after review

---------

Co-authored-by: Jay Feng <[email protected]>
This commit is contained in:
jayandJay Feng authored and GitHub committed 2026-10-13 09:17:26 +00:00
1 parent fb01aa6adc
commit da19e5c283
1 file changed
+62
@@ -718,6 +718,68 @@ describe('ManagedToolRuntime', () => {
expect(tool.invocations).toHaveLength(1025);
});
it('refuses a new turn while an invocation is prepared but not yet executed', async () => {
const ref = await prepare();
await runtime.preflight(ref);
expect(() =>
runtime.beginTurn({ ...identity, promptId: 'prompt-2' }),
).toThrow('unfinished');
expect(runtime.status(ref).state).toBe('prepared');
expect(runtime.hasActiveWork()).toBe(true);
});
it('refuses a new turn while an invocation is still executing and preserves access to it', async () => {
const gate = deferred<ToolResult>();
tool.setup = (invocation) =>
invocation.execute.mockReturnValue(gate.promise);
const ref = await prepare();
await runtime.preflight(ref);
const executing = runtime.execute(ref);
const next = { ...identity, promptId: 'prompt-2' };
try {
expect(() => runtime.beginTurn(next)).toThrow('unfinished');
expect(runtime.status(ref).state).toBe('executing');
expect(runtime.hasActiveWork()).toBe(true);
} finally {
gate.resolve(rawResult);
await executing;
}
expect((await executing).executionStatus).toBe('success');
expect(runtime.status(ref).state).toBe('settled');
await runtime.beginTurn(next);
expect(() => runtime.status(ref)).toThrow('identity does not match');
});
it('refuses a new turn while a cancellation is confirming', async () => {
const gate = deferred<void>();
tool.setup = (invocation) =>
invocation.onConfirm.mockReturnValue(gate.promise);
const ref = await prepare();
const confirming = runtime.confirm(
ref,
ToolConfirmationOutcome.ProceedOnce,
);
const rejection = confirming.catch((error: unknown) => error);
try {
await vi.waitFor(() =>
expect(tool.invocations[0].onConfirm).toHaveBeenCalledTimes(1),
);
expect(runtime.cancel(ref).state).toBe('cancel_requested');
expect(() =>
runtime.beginTurn({ ...identity, promptId: 'prompt-2' }),
).toThrow('unfinished');
expect(runtime.status(ref).state).toBe('cancel_requested');
expect(runtime.hasActiveWork()).toBe(true);
} finally {
gate.resolve();
}
expect(await rejection).toMatchObject({
message: expect.stringContaining('cancelled'),
});
await vi.waitFor(() => expect(runtime.status(ref).state).toBe('settled'));
expect(runtime.status(ref).result?.executionStatus).toBe('not_started');
});
it.each([
['a replacement', { shouldProceed: true }, { value: 'rewritten' }],
['an empty replacement', { shouldProceed: true }, {}],