test(FN-2249): add fast mode execution regression coverage
- Add executor regression tests for fast-mode completion paths, including tool injection, workflow-step bypass, and completion enforcement checks - Expand dashboard API tests to verify executionMode payload behavior for createTask and updateTask requests - Document fast-mode behavior across architecture, task management, and workflow-step docs with explicit gate bypass/enforcement details
This commit is contained in:
@@ -553,15 +553,17 @@ done
|
|||||||
### Execution detail
|
### Execution detail
|
||||||
- **Triage phase**: `TriageProcessor` generates executable spec
|
- **Triage phase**: `TriageProcessor` generates executable spec
|
||||||
- **Execution phase**: `TaskExecutor` performs implementation, tool calls, tests/build commands
|
- **Execution phase**: `TaskExecutor` performs implementation, tool calls, tests/build commands
|
||||||
- **Review phase**: optional `reviewStep()` workflow depending on prompt review level
|
- **Review phase**: optional `reviewStep()` workflow depending on prompt review level (bypassed in fast mode)
|
||||||
- **Merge phase**: `aiMergeTask()` handles merge strategy and post-merge workflow steps
|
- **Merge phase**: `aiMergeTask()` handles merge strategy and post-merge workflow steps
|
||||||
|
|
||||||
|
> **Fast Mode:** Tasks with `executionMode: "fast"` bypass the `review_step` tool injection and pre-merge workflow steps. Completion blockers (tests, build, typecheck from PROMPT.md) and post-merge workflow steps remain enforced.
|
||||||
|
|
||||||
### Step status model
|
### Step status model
|
||||||
Task steps use statuses: `pending`, `in-progress`, `done`, `skipped`.
|
Task steps use statuses: `pending`, `in-progress`, `done`, `skipped`.
|
||||||
|
|
||||||
### Workflow steps
|
### Workflow steps
|
||||||
- Defined in project config as `WorkflowStep`
|
- Defined in project config as `WorkflowStep`
|
||||||
- **Pre-merge** steps run in executor (`runWorkflowSteps()`)
|
- **Pre-merge** steps run in executor (`runWorkflowSteps()`) — bypassed in fast mode
|
||||||
- **Post-merge** steps run in merger (`runPostMergeWorkflowSteps()`)
|
- **Post-merge** steps run in merger (`runPostMergeWorkflowSteps()`)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -71,6 +71,62 @@ fn task archive FN-001
|
|||||||
fn task unarchive FN-001
|
fn task unarchive FN-001
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Task Execution Modes
|
||||||
|
|
||||||
|
Each task has an execution mode that controls how the executor agent approaches the task:
|
||||||
|
|
||||||
|
| Mode | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `standard` | Full execution with complete review workflow (default) |
|
||||||
|
| `fast` | Expedited execution with minimal overhead for simple tasks |
|
||||||
|
|
||||||
|
### Fast Mode Bypassed Gates
|
||||||
|
|
||||||
|
When `executionMode: "fast"`, the following automated review/validation gates are **bypassed**:
|
||||||
|
|
||||||
|
| Gate | Standard Mode | Fast Mode |
|
||||||
|
|------|---------------|-----------|
|
||||||
|
| `review_step` tool enforcement | Available to executor agent | **Not injected** |
|
||||||
|
| Pre-merge workflow-step execution | Runs configured steps | **Skipped** |
|
||||||
|
| Workflow revision loop | Enabled (feedback → fix → re-review) | **Disabled** |
|
||||||
|
|
||||||
|
### Fast Mode Mandatory Gates
|
||||||
|
|
||||||
|
The following quality gates **remain enforced** in fast mode:
|
||||||
|
|
||||||
|
| Gate | Behavior |
|
||||||
|
|------|----------|
|
||||||
|
| `task_done` requirement | Agent must call `task_done()` to complete |
|
||||||
|
| Completion blocker checks | Tests, build, and typecheck from PROMPT.md still enforced |
|
||||||
|
| Post-merge workflow steps | Run as normal (merger-owned) |
|
||||||
|
|
||||||
|
### Execution Mode Matrix
|
||||||
|
|
||||||
|
| Feature | Standard | Fast |
|
||||||
|
|---------|----------|------|
|
||||||
|
| Executor agent session | Full prompt + tools | Full prompt (minus review_step) |
|
||||||
|
| Pre-merge workflow steps | ✅ Run | ❌ Bypassed |
|
||||||
|
| `review_step` tool | ✅ Available | ❌ Not available |
|
||||||
|
| Post-merge workflow steps | ✅ Run | ✅ Run |
|
||||||
|
| Completion blockers (test/build/typecheck) | ✅ Enforced | ✅ Enforced |
|
||||||
|
| `task_done()` requirement | ✅ Required | ✅ Required |
|
||||||
|
|
||||||
|
### Setting Execution Mode
|
||||||
|
|
||||||
|
Execution mode can be set during task creation or editing:
|
||||||
|
|
||||||
|
- **Via API**: Include `executionMode` field in task create/update payload
|
||||||
|
- **Via dashboard**: Select execution mode in the task creation dialog or task detail modal
|
||||||
|
- **Values**: `"standard"` (default) or `"fast"`
|
||||||
|
|
||||||
|
Example API payload:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"description": "Simple fix",
|
||||||
|
"executionMode": "fast"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## Task Detail Modal (Dashboard)
|
## Task Detail Modal (Dashboard)
|
||||||
|
|
||||||
The task detail modal exposes multiple tabs:
|
The task detail modal exposes multiple tabs:
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ Workflow steps run in one of two phases:
|
|||||||
- **Pre-merge** (default): runs before merge/finalization; failure blocks completion
|
- **Pre-merge** (default): runs before merge/finalization; failure blocks completion
|
||||||
- **Post-merge**: runs after successful merge; failure is logged but non-blocking
|
- **Post-merge**: runs after successful merge; failure is logged but non-blocking
|
||||||
|
|
||||||
|
> **Note on Fast Mode:** When a task has `executionMode: "fast"`, pre-merge workflow steps are bypassed entirely during executor completion. Post-merge workflow steps remain active and run normally (post-merge is merger-owned and unaffected by execution mode).
|
||||||
|
|
||||||
## Execution Modes
|
## Execution Modes
|
||||||
|
|
||||||
- **Prompt mode**: starts an AI agent for the step
|
- **Prompt mode**: starts an AI agent for the step
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
fetchAiSession,
|
fetchAiSession,
|
||||||
deleteAiSession,
|
deleteAiSession,
|
||||||
updateTask,
|
updateTask,
|
||||||
|
createTask,
|
||||||
connectPlanningStream,
|
connectPlanningStream,
|
||||||
connectSubtaskStream,
|
connectSubtaskStream,
|
||||||
connectMissionInterviewStream,
|
connectMissionInterviewStream,
|
||||||
@@ -345,6 +346,133 @@ describe("updateTask", () => {
|
|||||||
|
|
||||||
await expect(updateTask("FN-001", { dependencies: [] })).rejects.toThrow("Not found");
|
await expect(updateTask("FN-001", { dependencies: [] })).rejects.toThrow("Not found");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("sends PATCH with executionMode 'fast' when provided", async () => {
|
||||||
|
globalThis.fetch = vi.fn().mockReturnValue(mockFetchResponse(true, { ...FAKE_TASK, executionMode: "fast" }));
|
||||||
|
|
||||||
|
const result = await updateTask("FN-001", { executionMode: "fast" });
|
||||||
|
|
||||||
|
expect(result.executionMode).toBe("fast");
|
||||||
|
expect(globalThis.fetch).toHaveBeenCalledWith("/api/tasks/FN-001", {
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
method: "PATCH",
|
||||||
|
body: JSON.stringify({ executionMode: "fast" }),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sends PATCH with executionMode 'standard' when provided", async () => {
|
||||||
|
globalThis.fetch = vi.fn().mockReturnValue(mockFetchResponse(true, { ...FAKE_TASK, executionMode: "standard" }));
|
||||||
|
|
||||||
|
const result = await updateTask("FN-001", { executionMode: "standard" });
|
||||||
|
|
||||||
|
expect(result.executionMode).toBe("standard");
|
||||||
|
expect(globalThis.fetch).toHaveBeenCalledWith("/api/tasks/FN-001", {
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
method: "PATCH",
|
||||||
|
body: JSON.stringify({ executionMode: "standard" }),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sends PATCH with null to clear executionMode", async () => {
|
||||||
|
globalThis.fetch = vi.fn().mockReturnValue(mockFetchResponse(true, { ...FAKE_TASK, executionMode: undefined }));
|
||||||
|
|
||||||
|
const result = await updateTask("FN-001", { executionMode: null });
|
||||||
|
|
||||||
|
expect(result.executionMode).toBeUndefined();
|
||||||
|
expect(globalThis.fetch).toHaveBeenCalledWith("/api/tasks/FN-001", {
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
method: "PATCH",
|
||||||
|
body: JSON.stringify({ executionMode: null }),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("omits executionMode key when not provided in update", async () => {
|
||||||
|
globalThis.fetch = vi.fn().mockReturnValue(mockFetchResponse(true, { ...FAKE_TASK, title: "Updated" }));
|
||||||
|
|
||||||
|
await updateTask("FN-001", { title: "Updated" });
|
||||||
|
|
||||||
|
const call = vi.mocked(globalThis.fetch).mock.calls[0];
|
||||||
|
const body = JSON.parse((call[1] as RequestInit).body as string);
|
||||||
|
expect(body).not.toHaveProperty("executionMode");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("createTask", () => {
|
||||||
|
const originalFetch = globalThis.fetch;
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
globalThis.fetch = originalFetch;
|
||||||
|
});
|
||||||
|
|
||||||
|
const FAKE_CREATED_TASK: Task = {
|
||||||
|
id: "FN-001",
|
||||||
|
description: "Test task",
|
||||||
|
column: "triage",
|
||||||
|
dependencies: [],
|
||||||
|
steps: [],
|
||||||
|
currentStep: 0,
|
||||||
|
log: [],
|
||||||
|
createdAt: "2026-01-01T00:00:00.000Z",
|
||||||
|
updatedAt: "2026-01-01T00:00:00.000Z",
|
||||||
|
};
|
||||||
|
|
||||||
|
it("sends POST with executionMode 'fast' when provided", async () => {
|
||||||
|
globalThis.fetch = vi.fn().mockReturnValue(mockFetchResponse(true, { ...FAKE_CREATED_TASK, executionMode: "fast" }));
|
||||||
|
|
||||||
|
const result = await createTask({ description: "Fast task", executionMode: "fast" });
|
||||||
|
|
||||||
|
expect(result.executionMode).toBe("fast");
|
||||||
|
const call = vi.mocked(globalThis.fetch).mock.calls[0];
|
||||||
|
const body = JSON.parse((call[1] as RequestInit).body as string);
|
||||||
|
expect(body.executionMode).toBe("fast");
|
||||||
|
expect(body.description).toBe("Fast task");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sends POST with executionMode 'standard' when provided", async () => {
|
||||||
|
globalThis.fetch = vi.fn().mockReturnValue(mockFetchResponse(true, { ...FAKE_CREATED_TASK, executionMode: "standard" }));
|
||||||
|
|
||||||
|
const result = await createTask({ description: "Standard task", executionMode: "standard" });
|
||||||
|
|
||||||
|
expect(result.executionMode).toBe("standard");
|
||||||
|
const call = vi.mocked(globalThis.fetch).mock.calls[0];
|
||||||
|
const body = JSON.parse((call[1] as RequestInit).body as string);
|
||||||
|
expect(body.executionMode).toBe("standard");
|
||||||
|
expect(body.description).toBe("Standard task");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("omits executionMode key when not provided", async () => {
|
||||||
|
globalThis.fetch = vi.fn().mockReturnValue(mockFetchResponse(true, FAKE_CREATED_TASK));
|
||||||
|
|
||||||
|
await createTask({ description: "Task without execution mode" });
|
||||||
|
|
||||||
|
const call = vi.mocked(globalThis.fetch).mock.calls[0];
|
||||||
|
const body = JSON.parse((call[1] as RequestInit).body as string);
|
||||||
|
expect(body).not.toHaveProperty("executionMode");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sends POST with multiple fields including executionMode", async () => {
|
||||||
|
globalThis.fetch = vi.fn().mockReturnValue(mockFetchResponse(true, {
|
||||||
|
...FAKE_CREATED_TASK,
|
||||||
|
executionMode: "fast",
|
||||||
|
title: "Test Title",
|
||||||
|
dependencies: ["FN-002"],
|
||||||
|
}));
|
||||||
|
|
||||||
|
const result = await createTask({
|
||||||
|
description: "Full task",
|
||||||
|
title: "Test Title",
|
||||||
|
dependencies: ["FN-002"],
|
||||||
|
executionMode: "fast",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.executionMode).toBe("fast");
|
||||||
|
const call = vi.mocked(globalThis.fetch).mock.calls[0];
|
||||||
|
const body = JSON.parse((call[1] as RequestInit).body as string);
|
||||||
|
expect(body.description).toBe("Full task");
|
||||||
|
expect(body.title).toBe("Test Title");
|
||||||
|
expect(body.dependencies).toEqual(["FN-002"]);
|
||||||
|
expect(body.executionMode).toBe("fast");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("assignTask and fetchAgentTasks", () => {
|
describe("assignTask and fetchAgentTasks", () => {
|
||||||
|
|||||||
@@ -11385,6 +11385,329 @@ describe("TaskExecutor messaging tools", () => {
|
|||||||
const toolNames = tools.map((t: any) => t.name);
|
const toolNames = tools.map((t: any) => t.name);
|
||||||
expect(toolNames).toContain("review_step");
|
expect(toolNames).toContain("review_step");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("logs executor model usage when execution starts", async () => {
|
||||||
|
const store = createMockStore();
|
||||||
|
store.getTask.mockResolvedValue({
|
||||||
|
id: "FN-001",
|
||||||
|
title: "Test",
|
||||||
|
description: "Test task",
|
||||||
|
column: "in-progress",
|
||||||
|
dependencies: [],
|
||||||
|
steps: [],
|
||||||
|
currentStep: 0,
|
||||||
|
log: [],
|
||||||
|
executionMode: "fast",
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
mockedCreateFnAgent.mockResolvedValue({
|
||||||
|
session: {
|
||||||
|
prompt: vi.fn().mockResolvedValue(undefined),
|
||||||
|
dispose: vi.fn(),
|
||||||
|
sessionManager: { getLeafId: vi.fn().mockReturnValue("leaf-1") },
|
||||||
|
},
|
||||||
|
} as any);
|
||||||
|
|
||||||
|
const executor = new TaskExecutor(store, "/tmp/test");
|
||||||
|
await executor.execute({
|
||||||
|
id: "FN-001",
|
||||||
|
title: "Test",
|
||||||
|
description: "Test task",
|
||||||
|
column: "in-progress",
|
||||||
|
dependencies: [],
|
||||||
|
steps: [],
|
||||||
|
currentStep: 0,
|
||||||
|
log: [],
|
||||||
|
executionMode: "fast",
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Verify logEntry was called (indicates executor is running)
|
||||||
|
expect(store.logEntry).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Fast mode completion path", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
mockedExistsSync.mockReturnValue(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("skips workflow steps in fast mode when task completes", async () => {
|
||||||
|
const store = createMockStore();
|
||||||
|
|
||||||
|
// Task with workflow steps enabled AND fast mode
|
||||||
|
store.getTask.mockResolvedValue({
|
||||||
|
id: "FN-001",
|
||||||
|
title: "Test",
|
||||||
|
description: "Test task",
|
||||||
|
column: "in-progress",
|
||||||
|
dependencies: [],
|
||||||
|
steps: [{ name: "Preflight", status: "pending" }],
|
||||||
|
currentStep: 0,
|
||||||
|
log: [],
|
||||||
|
enabledWorkflowSteps: ["WS-001"],
|
||||||
|
executionMode: "fast",
|
||||||
|
prompt: "# test\n## Steps\n### Step 0: Preflight\n- [ ] check",
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mock workflow step exists
|
||||||
|
store.getWorkflowStep.mockResolvedValue({
|
||||||
|
id: "WS-001",
|
||||||
|
name: "Docs Review",
|
||||||
|
description: "Check documentation",
|
||||||
|
prompt: "Review docs.",
|
||||||
|
enabled: true,
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mock agent with task_done
|
||||||
|
mockedCreateFnAgent.mockImplementation((async (opts: any) => {
|
||||||
|
const customTools = opts.customTools || [];
|
||||||
|
const session = {
|
||||||
|
prompt: vi.fn().mockImplementation(async () => {
|
||||||
|
const taskDoneTool = customTools.find((t: any) => t.name === "task_done");
|
||||||
|
if (taskDoneTool) await taskDoneTool.execute("tool-1", {});
|
||||||
|
}),
|
||||||
|
dispose: vi.fn(),
|
||||||
|
subscribe: vi.fn(),
|
||||||
|
on: vi.fn(),
|
||||||
|
sessionManager: { getLeafId: vi.fn().mockReturnValue("leaf-1") },
|
||||||
|
state: {},
|
||||||
|
};
|
||||||
|
return { session };
|
||||||
|
}) as any);
|
||||||
|
|
||||||
|
const onComplete = vi.fn();
|
||||||
|
const executor = new TaskExecutor(store, "/tmp/test", { onComplete });
|
||||||
|
|
||||||
|
await executor.execute({
|
||||||
|
id: "FN-001",
|
||||||
|
title: "Test",
|
||||||
|
description: "Test task",
|
||||||
|
column: "in-progress",
|
||||||
|
dependencies: [],
|
||||||
|
steps: [{ name: "Preflight", status: "pending" }],
|
||||||
|
currentStep: 0,
|
||||||
|
log: [],
|
||||||
|
enabledWorkflowSteps: ["WS-001"],
|
||||||
|
executionMode: "fast",
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Verify task moved to in-review
|
||||||
|
expect(store.moveTask).toHaveBeenCalledWith("FN-001", "in-review");
|
||||||
|
|
||||||
|
// Verify onComplete was called
|
||||||
|
expect(onComplete).toHaveBeenCalled();
|
||||||
|
|
||||||
|
// Verify workflow step was NOT called (fast mode skips workflow steps)
|
||||||
|
// The agent should only be called once (main execution), not twice (main + workflow)
|
||||||
|
expect(mockedCreateFnAgent).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("still runs workflow steps in standard mode when task completes", async () => {
|
||||||
|
const store = createMockStore();
|
||||||
|
|
||||||
|
// Task with workflow steps enabled in standard mode
|
||||||
|
store.getTask.mockResolvedValue({
|
||||||
|
id: "FN-001",
|
||||||
|
title: "Test",
|
||||||
|
description: "Test task",
|
||||||
|
column: "in-progress",
|
||||||
|
dependencies: [],
|
||||||
|
steps: [{ name: "Preflight", status: "pending" }],
|
||||||
|
currentStep: 0,
|
||||||
|
log: [],
|
||||||
|
enabledWorkflowSteps: ["WS-001"],
|
||||||
|
executionMode: "standard",
|
||||||
|
prompt: "# test\n## Steps\n### Step 0: Preflight\n- [ ] check",
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mock workflow step exists
|
||||||
|
store.getWorkflowStep.mockResolvedValue({
|
||||||
|
id: "WS-001",
|
||||||
|
name: "Docs Review",
|
||||||
|
description: "Check documentation",
|
||||||
|
prompt: "Review docs.",
|
||||||
|
enabled: true,
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Track agent calls
|
||||||
|
let callIdx = 0;
|
||||||
|
mockedCreateFnAgent.mockImplementation((async (opts: any) => {
|
||||||
|
callIdx++;
|
||||||
|
const customTools = opts.customTools || [];
|
||||||
|
const session = {
|
||||||
|
prompt: vi.fn().mockImplementation(async () => {
|
||||||
|
if (callIdx === 1) {
|
||||||
|
// Main execution — find and trigger task_done
|
||||||
|
const taskDoneTool = customTools.find((t: any) => t.name === "task_done");
|
||||||
|
if (taskDoneTool) await taskDoneTool.execute("tool-1", {});
|
||||||
|
} else {
|
||||||
|
// Workflow step — no task_done needed
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
dispose: vi.fn(),
|
||||||
|
subscribe: vi.fn(),
|
||||||
|
on: vi.fn(),
|
||||||
|
sessionManager: { getLeafId: vi.fn().mockReturnValue("leaf-1") },
|
||||||
|
state: {},
|
||||||
|
};
|
||||||
|
return { session };
|
||||||
|
}) as any);
|
||||||
|
|
||||||
|
const executor = new TaskExecutor(store, "/tmp/test");
|
||||||
|
|
||||||
|
await executor.execute({
|
||||||
|
id: "FN-001",
|
||||||
|
title: "Test",
|
||||||
|
description: "Test task",
|
||||||
|
column: "in-progress",
|
||||||
|
dependencies: [],
|
||||||
|
steps: [{ name: "Preflight", status: "pending" }],
|
||||||
|
currentStep: 0,
|
||||||
|
log: [],
|
||||||
|
enabledWorkflowSteps: ["WS-001"],
|
||||||
|
executionMode: "standard",
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Verify task moved to in-review
|
||||||
|
expect(store.moveTask).toHaveBeenCalledWith("FN-001", "in-review");
|
||||||
|
|
||||||
|
// Verify workflow step WAS called (standard mode runs workflow steps)
|
||||||
|
// Agent should be called twice: main execution + workflow step
|
||||||
|
expect(mockedCreateFnAgent).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("still enforces task_done requirement in fast mode", async () => {
|
||||||
|
const store = createMockStore();
|
||||||
|
|
||||||
|
// Task in fast mode
|
||||||
|
store.getTask.mockResolvedValue({
|
||||||
|
id: "FN-001",
|
||||||
|
title: "Test",
|
||||||
|
description: "Test task",
|
||||||
|
column: "in-progress",
|
||||||
|
dependencies: [],
|
||||||
|
steps: [{ name: "Preflight", status: "pending" }],
|
||||||
|
currentStep: 0,
|
||||||
|
log: [],
|
||||||
|
executionMode: "fast",
|
||||||
|
prompt: "# test\n## Steps\n### Step 0: Preflight\n- [ ] check",
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mock agent that exits WITHOUT calling task_done
|
||||||
|
mockedCreateFnAgent.mockResolvedValue({
|
||||||
|
session: {
|
||||||
|
prompt: vi.fn().mockResolvedValue(undefined),
|
||||||
|
dispose: vi.fn(),
|
||||||
|
subscribe: vi.fn(),
|
||||||
|
on: vi.fn(),
|
||||||
|
sessionManager: { getLeafId: vi.fn().mockReturnValue("leaf-1") },
|
||||||
|
state: {},
|
||||||
|
},
|
||||||
|
} as any);
|
||||||
|
|
||||||
|
const onError = vi.fn();
|
||||||
|
const executor = new TaskExecutor(store, "/tmp/test", { onError });
|
||||||
|
|
||||||
|
await executor.execute({
|
||||||
|
id: "FN-001",
|
||||||
|
title: "Test",
|
||||||
|
description: "Test task",
|
||||||
|
column: "in-progress",
|
||||||
|
dependencies: [],
|
||||||
|
steps: [{ name: "Preflight", status: "pending" }],
|
||||||
|
currentStep: 0,
|
||||||
|
log: [],
|
||||||
|
executionMode: "fast",
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Fast mode should still enforce task_done requirement
|
||||||
|
// Should fail after retry and call onError
|
||||||
|
expect(onError).toHaveBeenCalled();
|
||||||
|
expect(store.updateTask).toHaveBeenCalledWith(
|
||||||
|
"FN-001",
|
||||||
|
expect.objectContaining({ status: "failed" }),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("still checks completion blockers in fast mode", async () => {
|
||||||
|
const store = createMockStore();
|
||||||
|
|
||||||
|
// Task in fast mode with no workflow steps
|
||||||
|
store.getTask.mockResolvedValue({
|
||||||
|
id: "FN-001",
|
||||||
|
title: "Test",
|
||||||
|
description: "Test task",
|
||||||
|
column: "in-progress",
|
||||||
|
dependencies: [],
|
||||||
|
steps: [],
|
||||||
|
currentStep: 0,
|
||||||
|
log: [],
|
||||||
|
executionMode: "fast",
|
||||||
|
prompt: "# test\n## Steps\n",
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mock agent with task_done
|
||||||
|
mockedCreateFnAgent.mockImplementation((async (opts: any) => {
|
||||||
|
const customTools = opts.customTools || [];
|
||||||
|
const session = {
|
||||||
|
prompt: vi.fn().mockImplementation(async () => {
|
||||||
|
const taskDoneTool = customTools.find((t: any) => t.name === "task_done");
|
||||||
|
if (taskDoneTool) await taskDoneTool.execute("tool-1", {});
|
||||||
|
}),
|
||||||
|
dispose: vi.fn(),
|
||||||
|
subscribe: vi.fn(),
|
||||||
|
on: vi.fn(),
|
||||||
|
sessionManager: { getLeafId: vi.fn().mockReturnValue("leaf-1") },
|
||||||
|
state: {},
|
||||||
|
};
|
||||||
|
return { session };
|
||||||
|
}) as any);
|
||||||
|
|
||||||
|
const onComplete = vi.fn();
|
||||||
|
const executor = new TaskExecutor(store, "/tmp/test", { onComplete });
|
||||||
|
|
||||||
|
await executor.execute({
|
||||||
|
id: "FN-001",
|
||||||
|
title: "Test",
|
||||||
|
description: "Test task",
|
||||||
|
column: "in-progress",
|
||||||
|
dependencies: [],
|
||||||
|
steps: [],
|
||||||
|
currentStep: 0,
|
||||||
|
log: [],
|
||||||
|
executionMode: "fast",
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Verify task completed normally even without workflow steps
|
||||||
|
// Completion blockers (test/build/typecheck) are checked via getTaskCompletionBlocker
|
||||||
|
// which is called before finalizing
|
||||||
|
expect(onComplete).toHaveBeenCalled();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user