a416413c7d
---ci--- phase: 6 milestone: v0.7.0 plan: 06 task: P06-all status: execute ---/ci---
208 lines
5.6 KiB
TypeScript
208 lines
5.6 KiB
TypeScript
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<boolean> { return true; }
|
|
async execute(request: BackendRequest): Promise<BackendResult> {
|
|
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<string>();
|
|
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");
|
|
});
|
|
}); |