feat(FN-902): add deterministic createAgentWithTaskDone helper and testing docs

- Add createAgentWithTaskDone helper for deterministic test patterns with task_done tool
- Rewrite restart integration tests to use the new helper instead of verbose mock setup
- Document task_done testing pattern in README with code example
- Reduce test boilerplate by encapsulating agent creation and tool stubbing
This commit is contained in:
gsxdsm
2026-04-04 21:07:14 -07:00
parent f12c11160e
commit 2b40605203
2 changed files with 38 additions and 11 deletions

View File

@@ -168,6 +168,33 @@ function mockAgentFailure(error = "agent crashed") {
mockedCreateHaiAgent.mockRejectedValue(new Error(error));
}
/**
* Create a mock agent that auto-triggers the task_done tool when prompt is called.
* This simulates a successful task execution where the agent calls task_done(),
* preventing the executor from entering the "finished without task_done — retrying
* with new session" branch. Follows the same pattern as executor.test.ts.
*
* Uses per-session local customTools capture to avoid race conditions when
* multiple tasks execute concurrently.
*/
function createAgentWithTaskDone() {
mockedCreateHaiAgent.mockImplementation((async (opts: any) => {
// Capture tools per-session to avoid race conditions with concurrent tasks
const localCustomTools = opts.customTools || [];
const session = {
prompt: vi.fn().mockImplementation(async () => {
// Find and execute task_done tool to set taskDone = true
const taskDoneTool = localCustomTools.find((t: any) => t.name === "task_done");
if (taskDoneTool) {
await taskDoneTool.execute("tool-1", {});
}
}),
dispose: vi.fn(),
};
return { session };
}) as any);
}
// ── Tests begin ───────────────────────────────────────────────────────────
beforeEach(() => {
@@ -186,7 +213,8 @@ describe("In-progress task resume after restart", () => {
const taskDone = makeTask("FN-003", "done");
store.listTasks.mockResolvedValue([task1, task2, taskDone]);
mockAgentSuccess();
// Use deterministic mock that calls task_done to prevent retry-with-new-session
createAgentWithTaskDone();
const executor = new TaskExecutor(store, "/tmp/test");
await executor.resumeOrphaned();
@@ -194,11 +222,8 @@ describe("In-progress task resume after restart", () => {
// Wait for async execute calls to complete
await new Promise((r) => setTimeout(r, 50));
// Each in-progress task should have been dispatched for execution.
// We assert at least 2 agent creations (one per in-progress task) without
// coupling to the internal retry-with-new-session count, which is an
// implementation detail of the executor not relevant to restart semantics.
expect(mockedCreateHaiAgent.mock.calls.length).toBeGreaterThanOrEqual(2);
// Exactly one agent session per in-progress task (no retry inflation)
expect(mockedCreateHaiAgent).toHaveBeenCalledTimes(2);
// Both tasks should have received resume log entries (behavioral guarantee)
expect(store.logEntry).toHaveBeenCalledWith("FN-001", "Resumed after engine restart");
@@ -647,15 +672,15 @@ describe("Crash scenario edge cases", () => {
vi.clearAllMocks();
store.listTasks.mockResolvedValue([task]);
store.getTask.mockResolvedValue(makeTaskDetail("FN-090", "in-progress"));
mockAgentSuccess();
// Use deterministic mock that calls task_done to prevent retry-with-new-session
createAgentWithTaskDone();
await executor.resumeOrphaned();
await new Promise((r) => setTimeout(r, 50));
// Agent was created for the re-resume, proving the task was eligible.
// We don't assert exact call count — the retry-with-new-session count is
// an executor implementation detail, not a restart-resilience concern.
expect(mockedCreateHaiAgent).toHaveBeenCalled();
// Exactly one agent created for the re-resume, proving the task was eligible
// and completed without retry inflation.
expect(mockedCreateHaiAgent).toHaveBeenCalledTimes(1);
// Re-resume should have logged again (behavioral guarantee)
expect(store.logEntry).toHaveBeenCalledWith("FN-090", "Resumed after engine restart");