feat(P06): docs & hardening — AGENTS.md/README fixes, agent tests, Gitea tests, multi-project tests, version 0.7.0

---ci---
phase: 6
milestone: v0.7.0
plan: 06
task: P06-all
status: execute
---/ci---
This commit is contained in:
Jon Chery
2026-05-29 18:20:46 +00:00
parent e8c6c5c917
commit a416413c7d
15 changed files with 1031 additions and 21 deletions
+208
View File
@@ -0,0 +1,208 @@
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");
});
});