b7d02ee4a4
---ci---
phase: 1
milestone: v0.10
status: execute
decisions:
- id: D-083
decision: Interactive one-at-a-time validation with accept/skip/modify
rationale: Gives user full control over ideation results
confidence: 0.87
- id: D-085
decision: Ask-after-validation kickoff of run workflow
rationale: Balances automation with user control
confidence: 0.85
- id: D-091
decision: Full multi-project support with active_projects array + parallel execution
rationale: User wants complete multi-project capability
confidence: 0.85
requirements:
covered:
- IDEATE-12
- IDEATE-13
- IDEATE-14
- MULTI-02
- MULTI-06
---/ci---
- IDEATE-12: Interactive accept/skip/modify validation with readline
- IDEATE-13: acceptIdea/acceptIdeas methods update REQUIREMENTS.md and ROADMAP.md
- IDEATE-14: Ask-after-validation kickoff prompt for
- MULTI-02: --project flag accepts comma-separated or 'all' in pre-action hook
- MULTI-06: ciagent status shows active_projects and ideation config
- projects list shows all active projects with multi-marker
- projects set updates both active_project and active_projects
247 lines
12 KiB
TypeScript
247 lines
12 KiB
TypeScript
import * as fs from "node:fs";
|
|
import * as path from "node:path";
|
|
import * as os from "node:os";
|
|
import { IdeationAgent } from "../agents/ideation-agent.js";
|
|
import { IdeationEngine, resetIdeaCounter, Idea } from "../core/ideation.js";
|
|
|
|
describe("IdeationAgent", () => {
|
|
let tempDir: string;
|
|
|
|
beforeEach(() => {
|
|
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "ciagent-ideation-test-"));
|
|
});
|
|
|
|
afterEach(() => {
|
|
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
});
|
|
|
|
it("agent name is ideation-agent", () => {
|
|
const agent = new IdeationAgent();
|
|
expect(agent.name).toBe("ideation-agent");
|
|
});
|
|
|
|
it("delegates to IdeationEngine for mechanical ideation", () => {
|
|
const agent = new IdeationAgent();
|
|
const ideas = agent.mechanicalIdeate(tempDir);
|
|
expect(Array.isArray(ideas)).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("IdeationEngine", () => {
|
|
let tempDir: string;
|
|
|
|
beforeEach(() => {
|
|
resetIdeaCounter();
|
|
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "ciagent-engine-test-"));
|
|
});
|
|
|
|
afterEach(() => {
|
|
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
});
|
|
|
|
it("generates ideas from uncovered requirements", () => {
|
|
const ciagentDir = path.join(tempDir, ".ciagent");
|
|
fs.mkdirSync(ciagentDir, { recursive: true });
|
|
fs.writeFileSync(
|
|
path.join(ciagentDir, "config.json"),
|
|
JSON.stringify({ projects: [], active_project: "default" })
|
|
);
|
|
fs.writeFileSync(
|
|
path.join(ciagentDir, "REQUIREMENTS.md"),
|
|
"# Requirements\n\n## v1 Requirements\n\n### Core\n\n- **REQ-01**: First requirement\n- **REQ-02**: Second requirement\n\n## Traceability\n\n| Requirement | Phase | Status |\n|-------------|-------|--------|\n| REQ-01 | Phase 1 | pending |\n| REQ-02 | Phase 1 | pending |\n"
|
|
);
|
|
fs.writeFileSync(
|
|
path.join(ciagentDir, "PROJECT.md"),
|
|
"# Test Project\n\n## What This Is\n\nA test project.\n\n## Requirements\n\n### Validated\n\n- REQ-01: First\n\n### Active\n\n- [ ] REQ-02: Second\n\n## Constraints\n\n- Must work\n\n## Key Decisions\n\n| Decision | Rationale | Outcome |\n|----------|-----------|--------|\n"
|
|
);
|
|
fs.writeFileSync(
|
|
path.join(ciagentDir, "ROADMAP.md"),
|
|
"# Roadmap\n\n## Overview\n\nTest roadmap.\n\n## Phases\n\n- [ ] **Phase 1: Init** - Starting\n\n## Phase Details\n\n### Phase 1: Init\n\n**Goal.**: Start\n**Status**: not_started\n**Requirements**: REQ-01\n**Depends on**: Nothing\n**Success Criteria**:\n1. Project initialized\n"
|
|
);
|
|
fs.writeFileSync(
|
|
path.join(ciagentDir, "ARCHITECTURE.md"),
|
|
"# Architecture\n\n## Overview\n\nTest architecture.\n\n## Components\n\n### Core\n\n- **Description**: Core module\n- **Boundaries**: Internal only\n- **Depends on**: None\n\n## Data Flow\n\nSimple flow.\n\n## Build Order\n\n1. Core\n"
|
|
);
|
|
|
|
const engine = new IdeationEngine(tempDir);
|
|
const ideas = engine.runMechanical(["coverage"]);
|
|
const reqIdeas = ideas.filter((i) => i.source === "uncovered_requirement");
|
|
expect(reqIdeas.length).toBeGreaterThanOrEqual(1);
|
|
});
|
|
|
|
it("detects architecture drift when documented components are missing", () => {
|
|
const ciagentDir = path.join(tempDir, ".ciagent");
|
|
fs.mkdirSync(ciagentDir, { recursive: true });
|
|
fs.writeFileSync(
|
|
path.join(ciagentDir, "config.json"),
|
|
JSON.stringify({ projects: [], active_project: "default" })
|
|
);
|
|
fs.writeFileSync(
|
|
path.join(ciagentDir, "ARCHITECTURE.md"),
|
|
"# Architecture\n\n## Overview\n\nTest.\n\n## Components\n\n### NonExistentModule\n\n- **Description**: A module that does not exist\n- **Boundaries**: None\n- **Depends on**: None\n\n## Data Flow\n\nFlow.\n\n## Build Order\n\n1. Core\n"
|
|
);
|
|
|
|
const engine = new IdeationEngine(tempDir);
|
|
const ideas = engine.runMechanical(["architecture"]);
|
|
const driftIdeas = ideas.filter((i) => i.source === "architecture_drift");
|
|
expect(driftIdeas.length).toBeGreaterThanOrEqual(1);
|
|
});
|
|
|
|
it("detects spec ambiguity or spec missing", () => {
|
|
const ciagentDir = path.join(tempDir, ".ciagent");
|
|
fs.mkdirSync(ciagentDir, { recursive: true });
|
|
fs.writeFileSync(
|
|
path.join(ciagentDir, "config.json"),
|
|
JSON.stringify({ projects: [], active_project: "default" })
|
|
);
|
|
fs.writeFileSync(
|
|
path.join(ciagentDir, "PROJECT.md"),
|
|
"# Test\n\n## What This Is\n\nThe system should handle user input and could process data. It might also log events.\n\n## Requirements\n\n### Validated\n\n\n### Active\n\n- [ ] The system should handle errors\n- [ ] Users could configure settings\n- [ ] It might send notifications\n\n## Constraints\n\n- Must work\n\n## Key Decisions\n\n| Decision | Rationale | Outcome |\n|----------|-----------|--------|\n"
|
|
);
|
|
fs.writeFileSync(
|
|
path.join(ciagentDir, "REQUIREMENTS.md"),
|
|
"# Requirements\n\n## v1\n\n- REQ-01: Test\n\n## Traceability\n\n| Requirement | Phase | Status |\n|-------------|-------|--------|\n"
|
|
);
|
|
fs.writeFileSync(
|
|
path.join(ciagentDir, "ROADMAP.md"),
|
|
"# Roadmap\n\n## Overview\n\nTest\n\n## Phases\n\n\n## Phase Details\n\n"
|
|
);
|
|
fs.writeFileSync(
|
|
path.join(ciagentDir, "ARCHITECTURE.md"),
|
|
"# Architecture\n\n## Overview\n\nTest\n\n## Components\n\n## Data Flow\n\nTest\n\n## Build Order\n\n1. Test\n"
|
|
);
|
|
|
|
const engine = new IdeationEngine(tempDir);
|
|
const ideas = engine.runMechanical(["spec"]);
|
|
const specIdeas = ideas.filter((i) => i.source === "spec_ambiguity" || i.source === "spec_missing" || i.source === "spec_contradiction");
|
|
expect(specIdeas.length).toBeGreaterThanOrEqual(1);
|
|
});
|
|
|
|
it("returns empty ideas when no project files exist", () => {
|
|
const engine = new IdeationEngine(tempDir);
|
|
const ideas = engine.runMechanical();
|
|
expect(Array.isArray(ideas)).toBe(true);
|
|
});
|
|
|
|
it("formats ideas as readable text", () => {
|
|
const ciagentDir = path.join(tempDir, ".ciagent");
|
|
fs.mkdirSync(ciagentDir, { recursive: true });
|
|
fs.writeFileSync(
|
|
path.join(ciagentDir, "config.json"),
|
|
JSON.stringify({ projects: [], active_project: "default" })
|
|
);
|
|
fs.writeFileSync(path.join(ciagentDir, "PROJECT.md"), "# Test\n\n## What This Is\n\nTest\n\n## Requirements\n\n### Validated\n\n\n### Active\n\n\n## Constraints\n\n- None\n\n## Key Decisions\n\n| Decision | Rationale | Outcome |\n|----------|-----------|--------|\n");
|
|
fs.writeFileSync(path.join(ciagentDir, "REQUIREMENTS.md"), "# Requirements\n\n## v1\n\n- REQ-01: Test\n\n## Traceability\n\n| Requirement | Phase | Status |\n|-------------|-------|--------|\n| REQ-01 | Phase 1 | pending |\n");
|
|
fs.writeFileSync(path.join(ciagentDir, "ROADMAP.md"), "# Roadmap\n\n## Overview\n\nTest\n\n## Phases\n\n\n## Phase Details\n\n");
|
|
fs.writeFileSync(path.join(ciagentDir, "ARCHITECTURE.md"), "# Architecture\n\n## Overview\n\nTest\n\n## Components\n\n## Data Flow\n\nTest\n\n## Build Order\n\n1. Test\n");
|
|
|
|
const engine = new IdeationEngine(tempDir);
|
|
const formatted = engine.formatIdeas(engine.runMechanical());
|
|
expect(typeof formatted).toBe("string");
|
|
});
|
|
|
|
it("formats ideas as JSON", () => {
|
|
const ciagentDir = path.join(tempDir, ".ciagent");
|
|
fs.mkdirSync(ciagentDir, { recursive: true });
|
|
fs.writeFileSync(
|
|
path.join(ciagentDir, "config.json"),
|
|
JSON.stringify({ projects: [], active_project: "default" })
|
|
);
|
|
fs.writeFileSync(path.join(ciagentDir, "PROJECT.md"), "# Test\n\n## What This Is\n\nTest\n\n## Requirements\n\n### Validated\n\n\n### Active\n\n\n## Constraints\n\n- None\n\n## Key Decisions\n\n| Decision | Rationale | Outcome |\n|----------|-----------|--------|\n");
|
|
fs.writeFileSync(path.join(ciagentDir, "REQUIREMENTS.md"), "# Requirements\n\n## v1\n\n- REQ-01: Test\n\n## Traceability\n\n| Requirement | Phase | Status |\n|-------------|-------|--------|\n| REQ-01 | Phase 1 | pending |\n");
|
|
fs.writeFileSync(path.join(ciagentDir, "ROADMAP.md"), "# Roadmap\n\n## Overview\n\nTest\n\n## Phases\n\n\n## Phase Details\n\n");
|
|
fs.writeFileSync(path.join(ciagentDir, "ARCHITECTURE.md"), "# Architecture\n\n## Overview\n\nTest\n\n## Components\n\n## Data Flow\n\nTest\n\n## Build Order\n\n1. Test\n");
|
|
|
|
const engine = new IdeationEngine(tempDir);
|
|
const result = engine.formatIdeasJson(engine.runMechanical());
|
|
expect(result).toHaveProperty("ideas");
|
|
expect(result).toHaveProperty("summary");
|
|
expect(result).toHaveProperty("project");
|
|
});
|
|
|
|
describe("acceptIdea", () => {
|
|
let acceptDir: string;
|
|
|
|
beforeEach(() => {
|
|
resetIdeaCounter();
|
|
acceptDir = fs.mkdtempSync(path.join(os.tmpdir(), "ciagent-accept-test-"));
|
|
const ciagentDir = path.join(acceptDir, ".ciagent");
|
|
fs.mkdirSync(ciagentDir, { recursive: true });
|
|
fs.writeFileSync(
|
|
path.join(ciagentDir, "config.json"),
|
|
JSON.stringify({ projects: [], active_project: "default" })
|
|
);
|
|
fs.writeFileSync(
|
|
path.join(ciagentDir, "REQUIREMENTS.md"),
|
|
"# Requirements\n\n## v1 Requirements\n\n### Core\n\n- **CORE-01**: Test core requirement\n\n## Traceability\n\n| Requirement | Phase | Status |\n|-------------|-------|--------|\n| CORE-01 | Phase 1 | pending |\n"
|
|
);
|
|
fs.writeFileSync(
|
|
path.join(ciagentDir, "ROADMAP.md"),
|
|
"# Roadmap\n\n## Overview\n\nTest roadmap.\n\n## Phases\n\n- [x] **Phase 1: Init** - Starting\n\n## Phase Details\n\n### Phase 1: Init\n\n**Goal.**: Start\n**Status**: complete\n**Requirements**: CORE-01\n**Depends on**: Nothing\n**Success Criteria**:\n1. Project initialized\n"
|
|
);
|
|
fs.writeFileSync(
|
|
path.join(ciagentDir, "PROJECT.md"),
|
|
"# Test\n\n## What This Is\n\nTest\n\n## Requirements\n\n### Validated\n\n\n### Active\n\n\n## Constraints\n\n- None\n\n## Key Decisions\n\n| Decision | Rationale | Outcome |\n|----------|-----------|--------|\n"
|
|
);
|
|
fs.writeFileSync(
|
|
path.join(ciagentDir, "ARCHITECTURE.md"),
|
|
"# Architecture\n\n## Overview\n\nTest\n\n## Components\n\n## Data Flow\n\nTest\n\n## Build Order\n\n1. Test\n"
|
|
);
|
|
});
|
|
|
|
afterEach(() => {
|
|
fs.rmSync(acceptDir, { recursive: true, force: true });
|
|
});
|
|
|
|
it("accepts an idea and updates REQUIREMENTS.md and ROADMAP.md", () => {
|
|
const engine = new IdeationEngine(acceptDir);
|
|
const idea: Idea = {
|
|
id: "IDEATE-01",
|
|
source: "uncovered_requirement",
|
|
category: "coverage",
|
|
title: "Add rate limiting to cloud backends",
|
|
rationale: "No rate limiting REQ exists for cloud backends.",
|
|
confidence: 0.92,
|
|
actions: ["add_requirement", "update_roadmap"],
|
|
tier: "mechanical",
|
|
};
|
|
|
|
const result = engine.acceptIdea(idea);
|
|
|
|
expect(result.addedToRequirements).toBe(true);
|
|
expect(result.addedToRoadmap).toBe(true);
|
|
expect(result.reqId).toBe("IDEATE-01");
|
|
});
|
|
|
|
it("acceptIdeas accepts multiple ideas", () => {
|
|
const engine = new IdeationEngine(acceptDir);
|
|
const ideas: Idea[] = [
|
|
{
|
|
id: "IDEATE-01",
|
|
source: "uncovered_requirement",
|
|
category: "coverage",
|
|
title: "Add rate limiting",
|
|
rationale: "No rate limiting.",
|
|
confidence: 0.9,
|
|
actions: ["add_requirement"],
|
|
tier: "mechanical",
|
|
},
|
|
{
|
|
id: "IDEATE-02",
|
|
source: "architecture_drift",
|
|
category: "architecture",
|
|
title: "Fix architecture drift",
|
|
rationale: "Component documented but missing.",
|
|
confidence: 0.8,
|
|
actions: ["update_architecture"],
|
|
tier: "mechanical",
|
|
},
|
|
];
|
|
|
|
const { accepted, results } = engine.acceptIdeas(ideas);
|
|
expect(accepted.length).toBe(2);
|
|
expect(results.length).toBe(2);
|
|
expect(results.every((r) => r.addedToRequirements || r.addedToRoadmap)).toBe(true);
|
|
});
|
|
});
|
|
}); |