import * as fs from "node:fs"; import * as path from "node:path"; import * as os from "node:os"; import { ResearcherAgent } from "../agents/researcher.js"; import { AgentContext } from "../agents/base.js"; import { IntelligenceBackend, BackendRequest, BackendResult } from "../backends/types.js"; import { emptyTokenUsage } from "../backends/types.js"; class MockBackend implements IntelligenceBackend { readonly name = "mock"; readonly type = "llm" as const; async isAvailable(): Promise { return true; } async execute(request: BackendRequest): Promise { return { success: true, output: `Mock backend executed: ${request.task.slice(0, 50)}`, artifacts: [], decisions: [], escalations: [], usage: emptyTokenUsage(), }; } } function createTempDir(): string { return fs.mkdtempSync(path.join(os.tmpdir(), "ciagent-researcher-test-")); } function cleanup(dir: string): void { fs.rmSync(dir, { recursive: true, force: true }); } function makeContext(dir: string, backend?: IntelligenceBackend): AgentContext { return { project_path: dir, phase: 1, stage: "research", specification: "Build a REST API for task management", config_path: path.join(dir, ".ciagent", "config.json"), backend, }; } function setupCIAgentDir(dir: string): void { const ciDir = path.join(dir, ".ciagent"); fs.mkdirSync(ciDir, { recursive: true }); fs.writeFileSync(path.join(ciDir, "config.json"), '{"projects":[],"active_project":""}'); } function writeProjectMd(dir: string): void { const ciDir = path.join(dir, ".ciagent"); const content = [ "# Task API", "", "## What This Is", "", "A REST API for managing tasks", "", "## Requirements", "", "### Validated", "", "- ✓ User authentication", "", "### Active", "", "- [ ] Task CRUD", "", "### Out of Scope", "", "- Admin dashboard", "", "## Context", "", "Node.js project", "", "## Constraints", "", "- Must use Node.js", "", "## Key Decisions", "", "| Decision | Rationale | Outcome |", "|----------|-----------|---------|", ].join("\n"); fs.writeFileSync(path.join(ciDir, "PROJECT.md"), content); } function writeArchitectureMd(dir: string): void { const ciDir = path.join(dir, ".ciagent"); const content = [ "# Architecture", "", "## Overview", "", "Task management system architecture", "", "## Components", "", "### Core", "- **Description**: Core module", "- **Boundaries**: src/core/ — internal module", "- **Depends on**: None", "", "## Data Flow", "", "Request → Handler → Service → Database", "", "## Build Order", "", "1. Build core module", ].join("\n"); fs.writeFileSync(path.join(ciDir, "ARCHITECTURE.md"), content); } function setupSourceDir(dir: string): void { const srcDir = path.join(dir, "src"); fs.mkdirSync(srcDir, { recursive: true }); fs.mkdirSync(path.join(srcDir, "core"), { recursive: true }); fs.mkdirSync(path.join(srcDir, "agents"), { recursive: true }); fs.writeFileSync(path.join(srcDir, "core", "index.ts"), "export {};\n"); fs.writeFileSync(path.join(srcDir, "agents", "base.ts"), "export {};\n"); } describe("ResearcherAgent", () => { let dir: string; beforeEach(() => { dir = createTempDir(); }); afterEach(() => { cleanup(dir); }); it("reads .ciagent/ files without backend", async () => { setupCIAgentDir(dir); writeProjectMd(dir); writeArchitectureMd(dir); const researcher = new ResearcherAgent(); const result = await researcher.execute(makeContext(dir)); expect(result.success).toBe(true); expect(result.output).toContain("findingsCount"); }); it("only modifies .ciagent/ files", async () => { setupCIAgentDir(dir); writeProjectMd(dir); writeArchitectureMd(dir); setupSourceDir(dir); const srcDir = path.join(dir, "src"); const filesBefore = new Set(); function collectFiles(d: string): void { for (const entry of fs.readdirSync(d, { withFileTypes: true })) { const full = path.join(d, entry.name); if (entry.isDirectory() && entry.name !== "node_modules") { collectFiles(full); } else { filesBefore.add(full); } } } collectFiles(srcDir); const researcher = new ResearcherAgent(); await researcher.execute(makeContext(dir)); collectFiles(srcDir); for (const f of filesBefore) { expect(fs.existsSync(f)).toBe(true); } }); it("updates ARCHITECTURE.md from source scan", async () => { setupCIAgentDir(dir); writeProjectMd(dir); setupSourceDir(dir); const researcher = new ResearcherAgent(); const result = await researcher.execute(makeContext(dir)); if (result.success) { const parsed = JSON.parse(result.output); expect(parsed.filesUpdated).toContain(".ciagent/ARCHITECTURE.md"); } }); it("delegates to backend when available", async () => { setupCIAgentDir(dir); const mockBackend = new MockBackend(); const researcher = new ResearcherAgent(); const result = await researcher.execute(makeContext(dir, mockBackend)); expect(result.success).toBe(true); expect(result.output).toContain("Mock backend executed"); }); it("has correct agent name", () => { const researcher = new ResearcherAgent(); expect(researcher.name).toBe("researcher"); }); it("has correct workflow", () => { const researcher = new ResearcherAgent(); expect(researcher.workflow).toBe("research"); }); });