Skip to content

Commit 2ccde1e

Browse files
committed
test: add Phase 1 test suite for select_active_intent with trace entry lookup
1 parent 379a61e commit 2ccde1e

File tree

1 file changed

+323
-0
lines changed

1 file changed

+323
-0
lines changed
Lines changed: 323 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,323 @@
1+
// npx vitest run src/core/tools/__tests__/selectActiveIntentTool.spec.ts
2+
3+
import * as fs from "fs/promises"
4+
import * as path from "path"
5+
import * as os from "os"
6+
7+
import { selectActiveIntentTool } from "../SelectActiveIntentTool"
8+
import type { ToolUse } from "../../../shared/tools"
9+
import type { AgentTraceEntry } from "../../orchestration/OrchestrationDataModel"
10+
11+
describe("SelectActiveIntentTool - Phase 1 End-to-End Test", () => {
12+
let testWorkspaceDir: string
13+
let mockTask: any
14+
let mockPushToolResult: ReturnType<typeof vi.fn>
15+
let mockHandleError: ReturnType<typeof vi.fn>
16+
let mockSayAndCreateMissingParamError: ReturnType<typeof vi.fn>
17+
18+
beforeEach(async () => {
19+
// Create a temporary directory for testing
20+
testWorkspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "roo-test-"))
21+
22+
// Setup mock task
23+
mockTask = {
24+
cwd: testWorkspaceDir,
25+
consecutiveMistakeCount: 0,
26+
recordToolError: vi.fn(),
27+
sayAndCreateMissingParamError: vi.fn().mockResolvedValue("Missing parameter error"),
28+
}
29+
30+
mockPushToolResult = vi.fn()
31+
mockHandleError = vi.fn()
32+
mockSayAndCreateMissingParamError = vi.fn().mockResolvedValue("Missing parameter error")
33+
mockTask.sayAndCreateMissingParamError = mockSayAndCreateMissingParamError
34+
35+
// Initialize .orchestration directory
36+
const orchestrationDir = path.join(testWorkspaceDir, ".orchestration")
37+
await fs.mkdir(orchestrationDir, { recursive: true })
38+
})
39+
40+
afterEach(async () => {
41+
// Clean up temporary directory
42+
try {
43+
await fs.rm(testWorkspaceDir, { recursive: true, force: true })
44+
} catch (error) {
45+
// Ignore cleanup errors
46+
}
47+
})
48+
49+
describe("Phase 1: Context Loader with Trace Entries", () => {
50+
it("should load intent and include trace entries in context XML", async () => {
51+
// Setup: Create active_intents.yaml
52+
const intentsYaml = `active_intents:
53+
- id: INT-001
54+
name: Test Intent
55+
status: IN_PROGRESS
56+
owned_scope:
57+
- src/test/**
58+
constraints:
59+
- Must follow test patterns
60+
acceptance_criteria:
61+
- All tests pass
62+
`
63+
64+
const intentsPath = path.join(testWorkspaceDir, ".orchestration", "active_intents.yaml")
65+
await fs.writeFile(intentsPath, intentsYaml, "utf-8")
66+
67+
// Setup: Create agent_trace.jsonl with entries for INT-001
68+
const traceEntry1: AgentTraceEntry = {
69+
id: "trace-1",
70+
timestamp: "2026-02-18T10:00:00Z",
71+
vcs: { revision_id: "abc123" },
72+
files: [
73+
{
74+
relative_path: "src/test/file1.ts",
75+
conversations: [
76+
{
77+
url: "task-1",
78+
contributor: { entity_type: "AI", model_identifier: "claude-3-5-sonnet" },
79+
ranges: [{ start_line: 10, end_line: 20, content_hash: "sha256:hash1" }],
80+
related: [{ type: "intent", value: "INT-001" }],
81+
},
82+
],
83+
},
84+
],
85+
}
86+
87+
const traceEntry2: AgentTraceEntry = {
88+
id: "trace-2",
89+
timestamp: "2026-02-18T11:00:00Z",
90+
vcs: { revision_id: "def456" },
91+
files: [
92+
{
93+
relative_path: "src/test/file2.ts",
94+
conversations: [
95+
{
96+
url: "task-2",
97+
contributor: { entity_type: "AI", model_identifier: "claude-3-5-sonnet" },
98+
ranges: [{ start_line: 5, end_line: 15, content_hash: "sha256:hash2" }],
99+
related: [{ type: "intent", value: "INT-001" }],
100+
},
101+
],
102+
},
103+
],
104+
}
105+
106+
const tracePath = path.join(testWorkspaceDir, ".orchestration", "agent_trace.jsonl")
107+
await fs.writeFile(
108+
tracePath,
109+
JSON.stringify(traceEntry1) + "\n" + JSON.stringify(traceEntry2) + "\n",
110+
"utf-8",
111+
)
112+
113+
// Execute: Call select_active_intent
114+
const toolUse: ToolUse<"select_active_intent"> = {
115+
type: "tool_use",
116+
id: "tool-1",
117+
name: "select_active_intent",
118+
params: { intent_id: "INT-001" },
119+
}
120+
121+
await selectActiveIntentTool.execute(
122+
{ intent_id: "INT-001" },
123+
mockTask,
124+
{
125+
askApproval: vi.fn(),
126+
handleError: mockHandleError,
127+
pushToolResult: mockPushToolResult,
128+
},
129+
)
130+
131+
// Verify: pushToolResult was called with XML context
132+
expect(mockPushToolResult).toHaveBeenCalledTimes(1)
133+
const contextXml = mockPushToolResult.mock.calls[0][0]
134+
135+
// Verify: XML contains intent information
136+
expect(contextXml).toContain("<intent_id>INT-001</intent_id>")
137+
expect(contextXml).toContain("<intent_name>Test Intent</intent_name>")
138+
expect(contextXml).toContain("<status>IN_PROGRESS</status>")
139+
expect(contextXml).toContain("src/test/**")
140+
expect(contextXml).toContain("Must follow test patterns")
141+
expect(contextXml).toContain("All tests pass")
142+
143+
// Verify: XML contains recent history from trace entries
144+
expect(contextXml).toContain("<recent_history>")
145+
expect(contextXml).toContain("src/test/file1.ts")
146+
expect(contextXml).toContain("src/test/file2.ts")
147+
expect(contextXml).toContain("lines 10-20")
148+
expect(contextXml).toContain("lines 5-15")
149+
expect(contextXml).toContain("2026-02-18")
150+
151+
// Verify: Task has active intent stored
152+
expect((mockTask as any).activeIntentId).toBe("INT-001")
153+
expect((mockTask as any).activeIntent).toBeDefined()
154+
expect((mockTask as any).activeIntent.id).toBe("INT-001")
155+
156+
// Verify: No errors occurred
157+
expect(mockHandleError).not.toHaveBeenCalled()
158+
expect(mockTask.consecutiveMistakeCount).toBe(0)
159+
})
160+
161+
it("should handle intent with no trace entries", async () => {
162+
// Setup: Create active_intents.yaml
163+
const intentsYaml = `active_intents:
164+
- id: INT-002
165+
name: New Intent
166+
status: TODO
167+
owned_scope:
168+
- src/new/**
169+
constraints: []
170+
acceptance_criteria: []
171+
`
172+
173+
const intentsPath = path.join(testWorkspaceDir, ".orchestration", "active_intents.yaml")
174+
await fs.writeFile(intentsPath, intentsYaml, "utf-8")
175+
176+
// Setup: Create empty agent_trace.jsonl
177+
const tracePath = path.join(testWorkspaceDir, ".orchestration", "agent_trace.jsonl")
178+
await fs.writeFile(tracePath, "", "utf-8")
179+
180+
// Execute
181+
await selectActiveIntentTool.execute(
182+
{ intent_id: "INT-002" },
183+
mockTask,
184+
{
185+
askApproval: vi.fn(),
186+
handleError: mockHandleError,
187+
pushToolResult: mockPushToolResult,
188+
},
189+
)
190+
191+
// Verify: XML contains "No recent changes" message
192+
const contextXml = mockPushToolResult.mock.calls[0][0]
193+
expect(contextXml).toContain("<recent_history>")
194+
expect(contextXml).toContain("No recent changes found for this intent")
195+
})
196+
197+
it("should filter trace entries by intent ID", async () => {
198+
// Setup: Create active_intents.yaml
199+
const intentsYaml = `active_intents:
200+
- id: INT-001
201+
name: Intent One
202+
status: IN_PROGRESS
203+
owned_scope: []
204+
constraints: []
205+
acceptance_criteria: []
206+
- id: INT-002
207+
name: Intent Two
208+
status: IN_PROGRESS
209+
owned_scope: []
210+
constraints: []
211+
acceptance_criteria: []
212+
`
213+
214+
const intentsPath = path.join(testWorkspaceDir, ".orchestration", "active_intents.yaml")
215+
await fs.writeFile(intentsPath, intentsYaml, "utf-8")
216+
217+
// Setup: Create trace entries for different intents
218+
const traceEntry1: AgentTraceEntry = {
219+
id: "trace-1",
220+
timestamp: "2026-02-18T10:00:00Z",
221+
vcs: { revision_id: "abc123" },
222+
files: [
223+
{
224+
relative_path: "src/file1.ts",
225+
conversations: [
226+
{
227+
url: "task-1",
228+
contributor: { entity_type: "AI" },
229+
ranges: [{ start_line: 1, end_line: 10, content_hash: "sha256:hash1" }],
230+
related: [{ type: "intent", value: "INT-001" }],
231+
},
232+
],
233+
},
234+
],
235+
}
236+
237+
const traceEntry2: AgentTraceEntry = {
238+
id: "trace-2",
239+
timestamp: "2026-02-18T11:00:00Z",
240+
vcs: { revision_id: "def456" },
241+
files: [
242+
{
243+
relative_path: "src/file2.ts",
244+
conversations: [
245+
{
246+
url: "task-2",
247+
contributor: { entity_type: "AI" },
248+
ranges: [{ start_line: 1, end_line: 10, content_hash: "sha256:hash2" }],
249+
related: [{ type: "intent", value: "INT-002" }],
250+
},
251+
],
252+
},
253+
],
254+
}
255+
256+
const tracePath = path.join(testWorkspaceDir, ".orchestration", "agent_trace.jsonl")
257+
await fs.writeFile(
258+
tracePath,
259+
JSON.stringify(traceEntry1) + "\n" + JSON.stringify(traceEntry2) + "\n",
260+
"utf-8",
261+
)
262+
263+
// Execute: Select INT-001
264+
await selectActiveIntentTool.execute(
265+
{ intent_id: "INT-001" },
266+
mockTask,
267+
{
268+
askApproval: vi.fn(),
269+
handleError: mockHandleError,
270+
pushToolResult: mockPushToolResult,
271+
},
272+
)
273+
274+
// Verify: Only INT-001 trace entry is included
275+
const contextXml = mockPushToolResult.mock.calls[0][0]
276+
expect(contextXml).toContain("src/file1.ts")
277+
expect(contextXml).not.toContain("src/file2.ts")
278+
})
279+
280+
it("should return error for non-existent intent", async () => {
281+
// Setup: Create empty active_intents.yaml
282+
const intentsYaml = `active_intents: []`
283+
284+
const intentsPath = path.join(testWorkspaceDir, ".orchestration", "active_intents.yaml")
285+
await fs.writeFile(intentsPath, intentsYaml, "utf-8")
286+
287+
// Execute
288+
await selectActiveIntentTool.execute(
289+
{ intent_id: "INT-999" },
290+
mockTask,
291+
{
292+
askApproval: vi.fn(),
293+
handleError: mockHandleError,
294+
pushToolResult: mockPushToolResult,
295+
},
296+
)
297+
298+
// Verify: Error was returned
299+
expect(mockPushToolResult).toHaveBeenCalled()
300+
const errorMessage = mockPushToolResult.mock.calls[0][0]
301+
expect(errorMessage).toContain("not found in active_intents.yaml")
302+
expect(mockTask.consecutiveMistakeCount).toBeGreaterThan(0)
303+
})
304+
305+
it("should handle missing intent_id parameter", async () => {
306+
// Execute without intent_id
307+
await selectActiveIntentTool.execute(
308+
{ intent_id: "" },
309+
mockTask,
310+
{
311+
askApproval: vi.fn(),
312+
handleError: mockHandleError,
313+
pushToolResult: mockPushToolResult,
314+
},
315+
)
316+
317+
// Verify: Missing parameter error
318+
expect(mockSayAndCreateMissingParamError).toHaveBeenCalledWith("select_active_intent", "intent_id")
319+
expect(mockTask.consecutiveMistakeCount).toBeGreaterThan(0)
320+
})
321+
})
322+
})
323+

0 commit comments

Comments
 (0)