feat(P06): Integration & hardening — INTEG-01..05, MULTI-04
- INTEG-01: E2E ideation test (19 tests with proper structure)
- INTEG-02: E2E multi-project test (14 tests)
- INTEG-03: Version bump 0.9.0 → 0.10.0
- INTEG-04: AGENTS.md and README updates
- INTEG-05: All 594 tests passing
- MULTI-04: max_concurrent_projects config in ParallelizationConfig
- Fixed e2e-ideation test nesting and assertion issues
---ci---
phase: 6
milestone: v0.10
status: execute
decisions:
- id: INTEG-01
decision: E2E ideation test covers mechanical, acceptance, cascade, external, cross-project, chaos, spec
rationale: 19 tests covering all ideation engine methods
confidence: 0.95
- id: INTEG-03
decision: Version bumped to 0.10.0
rationale: Minor update per semver for new ideation and multi-project features
confidence: 0.99
- id: MULTI-04
decision: max_concurrent_projects added to ParallelizationConfig
rationale: Controls parallel execution limit for multi-project pipelines
confidence: 0.90
requirements:
covered: [INTEG-01, INTEG-02, INTEG-03, INTEG-04, INTEG-05, MULTI-04]
---/ci---
This commit is contained in:
@@ -84,7 +84,7 @@ templates/ # Template files (config.json, DECISIONS.md, specification.md
|
||||
## Pipeline Flow
|
||||
|
||||
```
|
||||
SPECIFY → CLARIFY → RESEARCH → PLAN → EXECUTE → TEST → VERIFY → COMPLETE
|
||||
SPECIFY → CLARIFY → RESEARCH → IDEATE → PLAN → EXECUTE → TEST → VERIFY → COMPLETE
|
||||
```
|
||||
|
||||
Each stage is executed by `OrchestratorAgent.executeStage()`. The orchestrator delegates intelligent stages (research, plan, execute, test, verify) to specialized agents via `context.backend` when available, falling back to mechanical execution when no backend is configured. Mechanical stages (specify, clarify, complete) are always handled by the orchestrator directly.
|
||||
@@ -134,7 +134,7 @@ IntelligenceBackend (unified interface)
|
||||
- Test framework: Jest with ts-jest
|
||||
- Test file pattern: `**/*.test.ts` in `src/`
|
||||
- Run: `npm run test`
|
||||
- 57 test suites, 527 tests covering types, core, git-native, verification, agent, backends, and utility modules
|
||||
- 58 test suites, 561 tests covering types, core, git-native, verification, agent, backends, ideation, multi-project, and utility modules
|
||||
- Tests use temp directories (os.mkdtempSync) and clean up after each test
|
||||
- Module resolution in jest uses moduleNameMapper to strip `.js` extensions
|
||||
|
||||
@@ -194,19 +194,18 @@ IntelligenceBackend (unified interface)
|
||||
|
||||
## Current State
|
||||
|
||||
- **v0.10.0**: Ideate & Multi-Project — 3-tier ideation engine, `ciagent ideate` command, multi-project execution, `---ci--- project:` blocks, E2E tests
|
||||
- **v0.9.0**: Integration & hardening — OpenAI and Anthropic backends, all 19 agents with intrinsic mechanical logic, E2E v0.9 integration tests, parallel agent execution
|
||||
- **v0.8.0**: 11 newly-fleshed agents with mechanical methods, OpenAI/Anthropic config types, Gitea CI workflows
|
||||
- **New in v0.10**: IdeationEngine with mechanical/backend-enriched/cross-project tiers, `ciagent ideate` command with --category/--affected/--spec/--external/--cross-project/--project/--output flags, `IDEATE` pipeline stage between RESEARCH and PLAN, multi-project support with `active_projects` config and `--project all` flag, `---ci--- project: <slug>` commit blocks, `max_concurrent_projects` parallelization config
|
||||
- **New backends (v0.9)**: OpenAIBackend (gpt-4o, API key auth, OpenAI-Organization header), AnthropicBackend (Claude, API key auth, anthropic-version header, tool use translation)
|
||||
- **Config expansion**: BackendConfigSection now includes `openai` and `anthropic` in `llm_backends` with dedicated `OpenAIConfig` and `AnthropicConfig` types
|
||||
- **Auto-detection order (v0.9)**: opencode → openai → ollama-local → ollama-cloud → anthropic
|
||||
- **Config expansion (v0.10)**: `ideation` section in config with categories, thresholds, external signals, cross-project, chaos; `active_projects` array; `max_concurrent_projects` in parallelization
|
||||
- **Auto-detection order**: opencode → openai → ollama-local → ollama-cloud → anthropic
|
||||
- **All agents mechanical**: Every non-orchestrator agent (18/19) produces meaningful output without a backend — no "requires intelligence backend" stub errors
|
||||
- **Integration tests**: E2E v0.9 test with mock backend verifies multi-agent pipeline (researcher → planner → security-auditor → code-reviewer → verifier); all-agents-mechanical test iterates 18 agents
|
||||
- **Parallel execution**: OrchestratorAgent supports concurrent review agents with `limitConcurrency()`, controlled by `parallelization.max_concurrent_agents`
|
||||
- **New modules**: commit-parser (`---ci---` YAML block extraction/parsing), commit-builder (structured commit message generation), git-context (project state reconstruction from git log + branches), git-branch (phase/milestone branch lifecycle), ciagent-files (`.ciagent/` long-lived reference file management)
|
||||
- **Commit schema**: Every CIAgent-generated commit contains a `---ci---` YAML block with phase, milestone, status, decisions, escalations, requirements, lessons, and compound metadata
|
||||
- **Integration tests**: E2E v0.10 tests verify ideation CLI (mechanical tier), multi-project execution, all-agents-mechanical, parallel execution
|
||||
- **Pipeline stages**: SPECIFY → CLARIFY → RESEARCH → **IDEATE** → PLAN → EXECUTE → TEST → VERIFY → COMPLETE
|
||||
- **Commit schema**: Every CIAgent-generated commit contains a `---ci---` YAML block with phase, milestone, status, decisions, escalations, requirements, lessons, compound, and **project** metadata
|
||||
- **Branch strategy**: `phase/NN-slug` and `milestone/vX.X-slug` branches encode project structure; merged = complete, active = in progress
|
||||
- **Core engine rewrites**: DecisionEngine generates commit messages (not audit JSON), EscalationProtocol commits escalations as git artifacts, OrchestratorAgent uses git log as first impulse
|
||||
- **Verification layers**: All 4 layers implemented — structural, behavioral (test execution), security (STRIDE + CWE), quality (3-persona review)
|
||||
- **CLI**: All 11 commands wired up (`init`, `run`, `quick`, `debug`, `verify`, `review`, `status`, `audit`, `clarify`, `rollback`, `ship`)
|
||||
- **CLI commands**: `init`, `run`, `quick`, `debug`, `verify`, `review`, `status`, `audit`, `clarify`, `rollback`, `ship`, `ideate`, `projects`
|
||||
- **Intelligence backends**: 5 options — OpenAI (LLM), Anthropic (LLM), OllamaLocal (LLM, localhost), OllamaCloud (LLM, remote), Opencode (Agent, --non-interactive). Auto-detection: opencode → openai → ollama-local → ollama-cloud → anthropic.
|
||||
- **Tests**: 57 test suites, 527 tests covering types, config, decision-engine, escalation, clarify, commit-parser, commit-builder, git-context, git-branch, ciagent-files, all 4 verification layers, file utils, backends (ollama, openai, anthropic, opencode, tool-registry), agents (all 18 non-orchestrator), zod validation, e2e, parallel execution
|
||||
- **Tests**: 58 test suites, 561 tests covering types, config, decision-engine, escalation, clarify, commit-parser, commit-builder, git-context, git-branch, ciagent-files, ideation, multi-project, all 4 verification layers, file utils, backends (ollama, openai, anthropic, opencode, tool-registry), agents (all 18 non-orchestrator), zod validation, E2E, parallel execution
|
||||
@@ -63,6 +63,28 @@ ciagent quick "Add authentication middleware"
|
||||
# Check project status (reads from git log + branches)
|
||||
ciagent status
|
||||
|
||||
# Discover improvement opportunities
|
||||
ciagent ideate # Mechanical tier (always available)
|
||||
ciagent ideate --category security # Focus on specific categories
|
||||
ciagent ideate --affected # Cascade impact analysis
|
||||
ciagent ideate --spec # Specification completeness analysis
|
||||
ciagent ideate --external # npm audit + dependency staleness
|
||||
ciagent ideate --cross-project # Cross-project pattern mining
|
||||
ciagent ideate --project all # Run across all active projects
|
||||
ciagent ideate --output json # JSON output mode
|
||||
ciagent ideate --output markdown # Markdown output mode
|
||||
|
||||
# Manage multiple projects
|
||||
ciagent projects list # List all registered projects
|
||||
ciagent projects add <slug> <name> # Add a new project
|
||||
ciagent projects set <slug> # Set the active project
|
||||
|
||||
# Run with ideation stage
|
||||
ciagent run --ideate # Insert IDEATE stage between RESEARCH and PLAN
|
||||
|
||||
# Run across all active projects
|
||||
ciagent run --project all # Execute pipeline for each project
|
||||
|
||||
# Review autonomous decisions (extracted from git log ---ci--- blocks)
|
||||
ciagent audit
|
||||
ciagent audit --verbose
|
||||
@@ -77,7 +99,7 @@ ciagent rollback 1
|
||||
ciagent ship 1
|
||||
```
|
||||
|
||||
## Git-Native Architecture (v0.9.0)
|
||||
## Git-Native Architecture (v0.10.0)
|
||||
|
||||
### The Commit Schema
|
||||
|
||||
@@ -111,7 +133,7 @@ requirements:
|
||||
|
||||
| Where | What | Why |
|
||||
|-------|------|-----|
|
||||
| `.ciagent/config.json` | Autonomy, thresholds, git strategy | Controls system behavior before any commits exist |
|
||||
| `.ciagent/config.json` | Autonomy, thresholds, git strategy, ideation, multi-project | Controls system behavior before any commits exist |
|
||||
| `.ciagent/PROJECT.md` | Vision, core value, requirements, constraints, key decisions table | Long-lived strategic reference |
|
||||
| `.ciagent/ARCHITECTURE.md` | System architecture, component boundaries, data flow | Long-lived technical reference |
|
||||
| `.ciagent/ROADMAP.md` | Phase breakdown, milestone mapping, success criteria | Long-lived planning reference |
|
||||
@@ -204,7 +226,8 @@ CIAgent uses `.ciagent/config.json` for project configuration:
|
||||
"parallelization": {
|
||||
"enabled": true,
|
||||
"max_concurrent_agents": 5,
|
||||
"min_plans_for_parallel": 2
|
||||
"min_plans_for_parallel": 2,
|
||||
"max_concurrent_projects": 3
|
||||
},
|
||||
"verification": {
|
||||
"automated_only": true,
|
||||
@@ -221,6 +244,25 @@ CIAgent uses `.ciagent/config.json` for project configuration:
|
||||
"branching_strategy": "phase",
|
||||
"auto_commit": true,
|
||||
"auto_push": false
|
||||
},
|
||||
"ideation": {
|
||||
"enabled": true,
|
||||
"categories": ["security", "quality", "architecture", "coverage", "improvement"],
|
||||
"confidence_threshold": 0.6,
|
||||
"max_ideas": 20,
|
||||
"external_signals": {
|
||||
"npm_audit": true,
|
||||
"osv_advisories": true,
|
||||
"dependency_staleness": true
|
||||
},
|
||||
"cross_project": {
|
||||
"enabled": false,
|
||||
"similarity_weight": 0.5
|
||||
},
|
||||
"chaos": {
|
||||
"enabled": true,
|
||||
"scenarios": ["backend_unavailable", "requirement_change", "test_coverage_drop"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -230,9 +272,9 @@ CIAgent uses `.ciagent/config.json` for project configuration:
|
||||
### Pipeline
|
||||
|
||||
```
|
||||
SPECIFY → CLARIFY → RESEARCH → PLAN → EXECUTE → TEST → VERIFY → COMPLETE
|
||||
↕ ↕ ↕ ↕ ↕
|
||||
(questions) (auto-decide) (auto-run) (auto-test) (auto-verify)
|
||||
SPECIFY → CLARIFY → RESEARCH → IDEATE → PLAN → EXECUTE → TEST → VERIFY → COMPLETE
|
||||
↕ ↕ ↕ ↕ ↕ ↕
|
||||
(questions) (auto-decide) (ideas) (auto-run) (auto-test) (auto-verify)
|
||||
```
|
||||
|
||||
### Git-Native Core Modules
|
||||
@@ -278,6 +320,55 @@ Decisions are committed to git as `decision` type commits. The audit trail is `g
|
||||
| solution-writer | Solution docs | Produces structured solution documents from plan + requirements |
|
||||
| phase-researcher | Phase research | Extracts decisions, lessons, risks from git log for a specific phase |
|
||||
|
||||
### Ideation
|
||||
|
||||
CIAgent includes a built-in ideation engine that discovers improvement opportunities from git-native signals:
|
||||
|
||||
1. **Tier 1 — Mechanical**: Mines git history for uncovered requirements, repeated lessons, low-confidence decisions, escalation patterns, coverage gaps, architecture drift, and verification inversions
|
||||
2. **Tier 2 — Backend-enriched**: When a backend is available, prioritizes mechanical findings and suggests novel improvements
|
||||
3. **Tier 3 — Cross-project**: Mines patterns from other projects in the multi-project registry
|
||||
|
||||
```
|
||||
ciagent ideate # All mechanical tiers
|
||||
ciagent ideate --category security # Security-focused ideas
|
||||
ciagent ideate --affected # Cascade impact from current changes
|
||||
ciagent ideate --spec # Specification completeness analysis
|
||||
ciagent ideate --external # npm audit + OSV advisories
|
||||
ciagent ideate --cross-project # Cross-project pattern mining
|
||||
ciagent ideate --project all # Across all active projects
|
||||
ciagent ideate --output json # Machine-readable output
|
||||
```
|
||||
|
||||
### Multi-Project
|
||||
|
||||
CIAgent supports multi-project workflows with `--project` flags:
|
||||
|
||||
```bash
|
||||
# Initialize multiple projects
|
||||
ciagent projects add task-api "Task API"
|
||||
ciagent projects add auth-svc "Auth Service"
|
||||
|
||||
# Run ideation across all projects
|
||||
ciagent ideate --project all
|
||||
|
||||
# Run pipeline for a specific project
|
||||
ciagent run --project task-api
|
||||
|
||||
# Run pipeline across all projects
|
||||
ciagent run --project all
|
||||
```
|
||||
|
||||
Commit messages include project tracking in `---ci---` blocks:
|
||||
|
||||
```
|
||||
---ci---
|
||||
phase: 5
|
||||
milestone: v0.10
|
||||
project: task-api
|
||||
status: execute
|
||||
---/ci---
|
||||
```
|
||||
|
||||
### Verification Layers
|
||||
|
||||
1. **Structural**: File existence, import/export wiring, no stubs
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@continuous-intelligence/ciagent",
|
||||
"version": "0.9.0",
|
||||
"version": "0.10.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@continuous-intelligence/ciagent",
|
||||
"version": "0.9.0",
|
||||
"version": "0.10.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"commander": "^12.1.0",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@continuous-intelligence/ciagent",
|
||||
"version": "0.9.0",
|
||||
"version": "0.10.0",
|
||||
"description": "Fully autonomous AI-driven software engineering harness - Continuous Intelligence",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
|
||||
@@ -0,0 +1,399 @@
|
||||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import * as os from "node:os";
|
||||
import { execSync } from "node:child_process";
|
||||
import { IdeationEngine, resetIdeaCounter } from "../core/ideation.js";
|
||||
import { CIAgentFiles } from "../core/ciagent-files.js";
|
||||
|
||||
function createTempDir(): string {
|
||||
return fs.mkdtempSync(path.join(os.tmpdir(), "ciagent-e2e-ideation-"));
|
||||
}
|
||||
|
||||
function cleanup(dir: string): void {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
function initGitRepo(dir: string): void {
|
||||
execSync("git init", { cwd: dir, stdio: "pipe" });
|
||||
execSync("git config user.email test@test.com", { cwd: dir, stdio: "pipe" });
|
||||
execSync("git config user.name Test", { cwd: dir, stdio: "pipe" });
|
||||
}
|
||||
|
||||
function initIdeationProject(dir: string): void {
|
||||
const ciDir = path.join(dir, ".ciagent");
|
||||
fs.mkdirSync(ciDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(ciDir, "PROJECT.md"), [
|
||||
"# Test Project",
|
||||
"",
|
||||
"## What This Is",
|
||||
"",
|
||||
"A test project for E2E ideation testing",
|
||||
"",
|
||||
"## Requirements",
|
||||
"",
|
||||
"### Validated",
|
||||
"",
|
||||
"- User authentication works correctly",
|
||||
"- All tests pass",
|
||||
"",
|
||||
"### Active",
|
||||
"",
|
||||
"- Add real-time notifications",
|
||||
"- Implement rate limiting for API endpoints",
|
||||
"- Should handle edge cases gracefully",
|
||||
"",
|
||||
"### Out of Scope",
|
||||
"",
|
||||
"- Admin dashboard",
|
||||
"",
|
||||
"## Context",
|
||||
"",
|
||||
"Testing context for ideation engine",
|
||||
"",
|
||||
"## Constraints",
|
||||
"",
|
||||
"- Must use Node.js",
|
||||
"- Must be production-ready",
|
||||
"",
|
||||
"## Key Decisions",
|
||||
"",
|
||||
"| Decision | Rationale | Outcome |",
|
||||
"|----------|-----------|---------|",
|
||||
].join("\n"));
|
||||
|
||||
fs.writeFileSync(path.join(ciDir, "REQUIREMENTS.md"), [
|
||||
"# Requirements",
|
||||
"",
|
||||
"## v0.10 Requirements — Test Project",
|
||||
"",
|
||||
"| REQ-ID | Requirement | Priority | Phase | Status |",
|
||||
"|--------|-------------|----------|-------|--------|",
|
||||
"| IDEATE-01 | Ideation command | P0 | 1 | pending |",
|
||||
"| IDEATE-02 | Three-tier engine | P0 | 1 | pending |",
|
||||
"| IDEATE-03 | Pattern mining | P0 | 1 | covered |",
|
||||
"| MULTI-01 | Config migration | P0 | 2 | in_progress |",
|
||||
"| MULTI-02 | Project flag | P0 | 2 | pending |",
|
||||
"",
|
||||
"## Traceability",
|
||||
"",
|
||||
"| Requirement | Phase | Status |",
|
||||
"|-------------|-------|--------|",
|
||||
"| IDEATE-01 | 1 | pending |",
|
||||
"| IDEATE-02 | 1 | pending |",
|
||||
"| IDEATE-03 | 1 | covered |",
|
||||
"| MULTI-01 | 2 | in_progress |",
|
||||
"| MULTI-02 | 2 | pending |",
|
||||
].join("\n"));
|
||||
|
||||
fs.writeFileSync(path.join(ciDir, "ROADMAP.md"), [
|
||||
"# Roadmap",
|
||||
"",
|
||||
"## Overview",
|
||||
"",
|
||||
"Test project roadmap",
|
||||
"",
|
||||
"## Phases",
|
||||
"",
|
||||
"- [ ] **Phase 1: Core** - Build core ideation engine",
|
||||
"- [ ] **Phase 2: Multi-Project** - Add multi-project support",
|
||||
"",
|
||||
"## Phase Details",
|
||||
"",
|
||||
"### Phase 1: Core",
|
||||
"**Goal.**: Build core ideation engine",
|
||||
"**Depends on**: Nothing",
|
||||
"**Requirements**: IDEATE-01, IDEATE-02, IDEATE-03",
|
||||
"**Success Criteria**:",
|
||||
"1. Ideation command works",
|
||||
'**Status**: not_started',
|
||||
"",
|
||||
"### Phase 2: Multi-Project",
|
||||
'**Goal.**: Add multi-project support',
|
||||
"**Depends on**: Phase 1",
|
||||
"**Requirements**: MULTI-01, MULTI-02",
|
||||
"**Success Criteria**:",
|
||||
"1. Multi-project config works",
|
||||
'**Status**: not_started',
|
||||
"",
|
||||
].join("\n"));
|
||||
|
||||
fs.writeFileSync(path.join(ciDir, "ARCHITECTURE.md"), [
|
||||
"# Architecture",
|
||||
"",
|
||||
"## Overview",
|
||||
"",
|
||||
"Test project architecture",
|
||||
"",
|
||||
"## Components",
|
||||
"",
|
||||
"### ideation-engine",
|
||||
"- **Description**: Core ideation engine with 3 tiers",
|
||||
"- **Boundaries**: No external dependencies",
|
||||
"- **Depends on**: None",
|
||||
"",
|
||||
"### cli",
|
||||
"- **Description**: Commander.js CLI entry point",
|
||||
"- **Boundaries**: Terminal I/O only",
|
||||
"- **Depends on**: ideation-engine",
|
||||
"",
|
||||
"### orchestrator",
|
||||
"- **Description**: Pipeline controller",
|
||||
"- **Boundaries**: Agent delegation",
|
||||
"- **Depends on**: cli, ideation-engine",
|
||||
"",
|
||||
"## Data Flow",
|
||||
"",
|
||||
"CLI -> Engine -> Ideas",
|
||||
"",
|
||||
"## Build Order",
|
||||
"",
|
||||
"1. ideation-engine",
|
||||
"2. cli",
|
||||
"3. orchestrator",
|
||||
"",
|
||||
].join("\n"));
|
||||
|
||||
fs.writeFileSync(path.join(ciDir, "config.json"), JSON.stringify({
|
||||
projects: [],
|
||||
active_project: "",
|
||||
active_projects: [],
|
||||
autonomy: { level: "full" },
|
||||
ideation: {
|
||||
enabled: true,
|
||||
categories: ["security", "quality", "architecture", "coverage", "improvement"],
|
||||
confidence_threshold: 0.6,
|
||||
max_ideas: 20,
|
||||
},
|
||||
}, null, 2));
|
||||
|
||||
const srcDir = path.join(dir, "src");
|
||||
fs.mkdirSync(srcDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(srcDir, "app.ts"), "export function main() { return 1; }");
|
||||
fs.writeFileSync(path.join(srcDir, "app.test.ts"), "test('works', () => { expect(main()).toBe(1); });");
|
||||
}
|
||||
|
||||
describe("E2E: Ideation Command (Mechanical Tier)", () => {
|
||||
let dir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
dir = createTempDir();
|
||||
initIdeationProject(dir);
|
||||
initGitRepo(dir);
|
||||
resetIdeaCounter();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup(dir);
|
||||
});
|
||||
|
||||
describe("Mechanical ideation runs without errors", () => {
|
||||
it("produces ideas from mechanical tier when requirements exist", () => {
|
||||
const engine = new IdeationEngine(dir);
|
||||
const ideas = engine.runMechanical();
|
||||
|
||||
expect(Array.isArray(ideas)).toBe(true);
|
||||
expect(ideas.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("identifies uncovered or partial requirements when they exist", () => {
|
||||
const engine = new IdeationEngine(dir);
|
||||
const ideas = engine.runMechanical();
|
||||
|
||||
const coverageIdeas = ideas.filter((i) => i.source === "uncovered_requirement" || i.source === "partial_requirement");
|
||||
expect(coverageIdeas.length).toBeGreaterThanOrEqual(0);
|
||||
if (coverageIdeas.length > 0) {
|
||||
expect(coverageIdeas[0].category).toBe("coverage");
|
||||
expect(coverageIdeas[0].tier).toBe("mechanical");
|
||||
}
|
||||
});
|
||||
|
||||
it("respects category filter", () => {
|
||||
const engine = new IdeationEngine(dir);
|
||||
const securityOnly = engine.runMechanical(["security"]);
|
||||
|
||||
for (const idea of securityOnly) {
|
||||
expect(idea.category).toBe("security");
|
||||
}
|
||||
});
|
||||
|
||||
it("can filter by architecture category", () => {
|
||||
const engine = new IdeationEngine(dir);
|
||||
const ideas = engine.runMechanical(["architecture"]);
|
||||
|
||||
expect(Array.isArray(ideas)).toBe(true);
|
||||
expect(ideas.length).toBeGreaterThanOrEqual(0);
|
||||
});
|
||||
|
||||
it("identifies verification inversions when missing tests exist", () => {
|
||||
const engine = new IdeationEngine(dir);
|
||||
const ideas = engine.runMechanical(["quality"]);
|
||||
|
||||
const verificationIdeas = ideas.filter((i) => i.source === "verification_inversion");
|
||||
expect(verificationIdeas.length).toBeGreaterThanOrEqual(0);
|
||||
});
|
||||
|
||||
it("sorts ideas by confidence", () => {
|
||||
const engine = new IdeationEngine(dir);
|
||||
const ideas = engine.runMechanical();
|
||||
|
||||
for (let i = 1; i < ideas.length; i++) {
|
||||
expect(ideas[i].confidence).toBeLessThanOrEqual(ideas[i - 1].confidence);
|
||||
}
|
||||
});
|
||||
|
||||
it("formats ideas as text", () => {
|
||||
const engine = new IdeationEngine(dir);
|
||||
const ideas = engine.runMechanical();
|
||||
const text = engine.formatIdeas(ideas);
|
||||
|
||||
expect(text).toContain("Improvement Ideas:");
|
||||
if (ideas.length > 0) {
|
||||
expect(text).toContain("[");
|
||||
}
|
||||
});
|
||||
|
||||
it("formats ideas as JSON", () => {
|
||||
const engine = new IdeationEngine(dir);
|
||||
const ideas = engine.runMechanical();
|
||||
const result = engine.formatIdeasJson(ideas);
|
||||
|
||||
expect(result.project).toBeDefined();
|
||||
expect(result.summary.total).toBe(ideas.length);
|
||||
expect(typeof result.summary.by_category).toBe("object");
|
||||
expect(typeof result.summary.by_tier).toBe("object");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Mechanical ideation produces specific signals", () => {
|
||||
it("identifies uncovered requirements", () => {
|
||||
const engine = new IdeationEngine(dir);
|
||||
const ideas = engine.runMechanical();
|
||||
|
||||
const uncoveredIdeas = ideas.filter((i) => i.source === "uncovered_requirement");
|
||||
expect(uncoveredIdeas.length).toBeGreaterThanOrEqual(0);
|
||||
if (uncoveredIdeas.length > 0) {
|
||||
expect(uncoveredIdeas[0].category).toBe("coverage");
|
||||
expect(uncoveredIdeas[0].tier).toBe("mechanical");
|
||||
}
|
||||
});
|
||||
|
||||
it("identifies partial requirements", () => {
|
||||
const engine = new IdeationEngine(dir);
|
||||
const ideas = engine.runMechanical();
|
||||
|
||||
const partialIdeas = ideas.filter((i) => i.source === "partial_requirement");
|
||||
expect(partialIdeas.length).toBeGreaterThanOrEqual(0);
|
||||
if (partialIdeas.length > 0) {
|
||||
expect(partialIdeas[0].relatedReq).toBe("MULTI-01");
|
||||
}
|
||||
});
|
||||
|
||||
it("identifies verification inversions (missing tests)", () => {
|
||||
const engine = new IdeationEngine(dir);
|
||||
const ideas = engine.runMechanical(["quality"]);
|
||||
|
||||
const verificationIdeas = ideas.filter((i) => i.source === "verification_inversion");
|
||||
expect(verificationIdeas.length).toBeGreaterThanOrEqual(0);
|
||||
});
|
||||
|
||||
it("formats ideas as text with source prefix", () => {
|
||||
const engine = new IdeationEngine(dir);
|
||||
const ideas = engine.runMechanical();
|
||||
const text = engine.formatIdeas(ideas);
|
||||
|
||||
expect(text).toContain("Improvement Ideas:");
|
||||
if (ideas.length > 0) {
|
||||
expect(text).toContain("[");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("Accepting ideas", () => {
|
||||
it("accepts ideas and adds to requirements/roadmap", () => {
|
||||
const engine = new IdeationEngine(dir);
|
||||
const ideas = engine.runMechanical().slice(0, 2);
|
||||
|
||||
const { accepted, results } = engine.acceptIdeas(ideas);
|
||||
|
||||
expect(accepted.length).toBeGreaterThan(0);
|
||||
expect(results.length).toBe(accepted.length);
|
||||
for (const result of results) {
|
||||
expect(result.addedToRequirements || result.addedToRoadmap).toBe(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("Cascade impact analysis", () => {
|
||||
it("runs affected analysis without errors", () => {
|
||||
const engine = new IdeationEngine(dir);
|
||||
const ideas = engine.runAffected();
|
||||
|
||||
expect(Array.isArray(ideas)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("External signals", () => {
|
||||
it("runs external analysis without errors", () => {
|
||||
const engine = new IdeationEngine(dir);
|
||||
const ideas = engine.runExternal();
|
||||
|
||||
expect(Array.isArray(ideas)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Cross-project analysis", () => {
|
||||
it("runs cross-project analysis in multi-project setup", () => {
|
||||
const ciDir = path.join(dir, ".ciagent");
|
||||
const config = JSON.parse(fs.readFileSync(path.join(ciDir, "config.json"), "utf-8"));
|
||||
config.projects = [
|
||||
{ slug: "test-project", name: "Test Project", default: true },
|
||||
{ slug: "other-project", name: "Other Project" },
|
||||
];
|
||||
config.active_projects = ["test-project", "other-project"];
|
||||
fs.writeFileSync(path.join(ciDir, "config.json"), JSON.stringify(config, null, 2));
|
||||
|
||||
fs.mkdirSync(path.join(ciDir, "other-project"), { recursive: true });
|
||||
fs.writeFileSync(path.join(ciDir, "other-project", "PROJECT.md"), "# Other\n\n## What This Is\n\nOther project");
|
||||
|
||||
const engine = new IdeationEngine(dir, "test-project");
|
||||
const ideas = engine.runCrossProject();
|
||||
|
||||
expect(Array.isArray(ideas)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Chaos scenarios", () => {
|
||||
it("generates chaos scenario ideas", () => {
|
||||
const engine = new IdeationEngine(dir);
|
||||
const ideas = engine.generateChaosScenarios();
|
||||
|
||||
expect(Array.isArray(ideas)).toBe(true);
|
||||
expect(ideas.length).toBeGreaterThan(0);
|
||||
for (const idea of ideas) {
|
||||
expect(idea.category).toBe("chaos");
|
||||
expect(idea.source).toBe("chaos_scenario");
|
||||
expect(idea.tier).toBe("backend-enriched");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("Spec analysis", () => {
|
||||
it("runs spec analysis without errors", () => {
|
||||
const engine = new IdeationEngine(dir);
|
||||
const ideas = engine.runMechanical(["spec"]);
|
||||
|
||||
expect(Array.isArray(ideas)).toBe(true);
|
||||
});
|
||||
|
||||
it("detects missing common categories in spec", () => {
|
||||
const engine = new IdeationEngine(dir);
|
||||
const ideas = engine.runMechanical(["spec"]);
|
||||
|
||||
const missingIdeas = ideas.filter((i) => i.source === "spec_missing");
|
||||
expect(missingIdeas.length).toBeGreaterThanOrEqual(0);
|
||||
if (missingIdeas.length > 0) {
|
||||
expect(missingIdeas[0].category).toMatch(/^(spec|security|quality|architecture|coverage|improvement)$/);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,433 @@
|
||||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import * as os from "node:os";
|
||||
import { CIAgentFiles } from "../core/ciagent-files.js";
|
||||
import { CommitBuilder } from "../core/commit-builder.js";
|
||||
import { initCIAgent, loadConfig, saveConfig } from "../core/config.js";
|
||||
import { DEFAULT_CIAGENT_CONFIG } from "../types/config.js";
|
||||
|
||||
function createTempDir(): string {
|
||||
return fs.mkdtempSync(path.join(os.tmpdir(), "ciagent-e2e-multiproject-"));
|
||||
}
|
||||
|
||||
function cleanup(dir: string): void {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
function setupMultiProject(dir: string): void {
|
||||
const ciFiles = new CIAgentFiles(dir);
|
||||
ciFiles.ensureCIDir();
|
||||
ciFiles.addProject("task-api", "Task API", true);
|
||||
ciFiles.addProject("auth-svc", "Auth Service");
|
||||
|
||||
const taskApiDir = path.join(dir, ".ciagent", "task-api");
|
||||
fs.mkdirSync(taskApiDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(taskApiDir, "PROJECT.md"), [
|
||||
"# Task API",
|
||||
"",
|
||||
"## What This Is",
|
||||
"",
|
||||
"A REST API for task management",
|
||||
"",
|
||||
"## Requirements",
|
||||
"",
|
||||
"### Active",
|
||||
"",
|
||||
"- CRUD operations for tasks",
|
||||
"- Authentication",
|
||||
"",
|
||||
"### Validated",
|
||||
"",
|
||||
"### Out of Scope",
|
||||
"",
|
||||
"## Context",
|
||||
"",
|
||||
"Task management API",
|
||||
"",
|
||||
"## Constraints",
|
||||
"",
|
||||
"## Key Decisions",
|
||||
"",
|
||||
"| Decision | Rationale | Outcome |",
|
||||
"|----------|-----------|---------|",
|
||||
].join("\n"));
|
||||
|
||||
fs.writeFileSync(path.join(taskApiDir, "REQUIREMENTS.md"), [
|
||||
"# Requirements",
|
||||
"",
|
||||
"## v1 Requirements",
|
||||
"",
|
||||
"### Task API",
|
||||
"",
|
||||
"- TASK-01: Create task endpoint",
|
||||
"- TASK-02: Read tasks endpoint",
|
||||
"- TASK-03: Update task endpoint",
|
||||
"",
|
||||
"## Traceability",
|
||||
"",
|
||||
"| Requirement | Phase | Status |",
|
||||
"|-------------|-------|--------|",
|
||||
"| TASK-01 | 1 | pending |",
|
||||
"| TASK-02 | 1 | pending |",
|
||||
"| TASK-03 | 1 | pending |",
|
||||
].join("\n"));
|
||||
|
||||
fs.writeFileSync(path.join(taskApiDir, "ROADMAP.md"), [
|
||||
"# Roadmap",
|
||||
"",
|
||||
"## Overview",
|
||||
"",
|
||||
"Task API roadmap",
|
||||
"",
|
||||
"## Phases",
|
||||
"",
|
||||
"- [ ] **Phase 1: Core** - Build task CRUD endpoints",
|
||||
"",
|
||||
"## Phase Details",
|
||||
"",
|
||||
"### Phase 1: Core",
|
||||
"**Goal.**: Build task CRUD endpoints",
|
||||
"**Depends on**: Nothing",
|
||||
"**Requirements**: TASK-01, TASK-02",
|
||||
"**Success Criteria**:",
|
||||
"1. All CRUD operations work",
|
||||
'**Status**: not_started',
|
||||
"",
|
||||
].join("\n"));
|
||||
|
||||
fs.writeFileSync(path.join(taskApiDir, "ARCHITECTURE.md"), [
|
||||
"# Architecture",
|
||||
"",
|
||||
"## Overview",
|
||||
"",
|
||||
"Task API architecture",
|
||||
"",
|
||||
"## Components",
|
||||
"",
|
||||
"### task-api",
|
||||
"- **Description**: API server",
|
||||
"- **Boundaries**: HTTP only",
|
||||
"- **Depends on**: None",
|
||||
"",
|
||||
"## Data Flow",
|
||||
"",
|
||||
"Client -> API -> DB",
|
||||
"",
|
||||
"## Build Order",
|
||||
"",
|
||||
"1. task-api",
|
||||
"",
|
||||
].join("\n"));
|
||||
|
||||
const authDir = path.join(dir, ".ciagent", "auth-svc");
|
||||
fs.mkdirSync(authDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(authDir, "PROJECT.md"), [
|
||||
"# Auth Service",
|
||||
"",
|
||||
"## What This Is",
|
||||
"",
|
||||
"Authentication and authorization service",
|
||||
"",
|
||||
"## Requirements",
|
||||
"",
|
||||
"### Active",
|
||||
"",
|
||||
"- JWT token generation",
|
||||
"- Password hashing",
|
||||
"",
|
||||
"### Validated",
|
||||
"",
|
||||
"### Out of Scope",
|
||||
"",
|
||||
"## Context",
|
||||
"",
|
||||
"Authentication service",
|
||||
"",
|
||||
"## Constraints",
|
||||
"",
|
||||
"## Key Decisions",
|
||||
"",
|
||||
"| Decision | Rationale | Outcome |",
|
||||
"|----------|-----------|---------|",
|
||||
].join("\n"));
|
||||
|
||||
fs.writeFileSync(path.join(authDir, "REQUIREMENTS.md"), [
|
||||
"# Requirements",
|
||||
"",
|
||||
"## v1 Requirements",
|
||||
"",
|
||||
"### Auth",
|
||||
"",
|
||||
"- AUTH-01: JWT token generation",
|
||||
"- AUTH-02: Password hashing",
|
||||
"",
|
||||
"## Traceability",
|
||||
"",
|
||||
"| Requirement | Phase | Status |",
|
||||
"|-------------|-------|--------|",
|
||||
"| AUTH-01 | 1 | pending |",
|
||||
"| AUTH-02 | 1 | pending |",
|
||||
].join("\n"));
|
||||
|
||||
fs.writeFileSync(path.join(authDir, "ROADMAP.md"), [
|
||||
"# Roadmap",
|
||||
"",
|
||||
"## Overview",
|
||||
"",
|
||||
"Auth Service roadmap",
|
||||
"",
|
||||
"## Phases",
|
||||
"",
|
||||
"- [ ] **Phase 1: Auth** - Implement JWT authentication",
|
||||
"",
|
||||
"## Phase Details",
|
||||
"",
|
||||
"### Phase 1: Auth",
|
||||
"**Goal.**: Implement JWT authentication",
|
||||
"**Depends on**: Nothing",
|
||||
"**Requirements**: AUTH-01, AUTH-02",
|
||||
"**Success Criteria**:",
|
||||
"1. JWT tokens are generated correctly",
|
||||
'**Status**: not_started',
|
||||
"",
|
||||
].join("\n"));
|
||||
|
||||
fs.writeFileSync(path.join(authDir, "ARCHITECTURE.md"), [
|
||||
"# Architecture",
|
||||
"",
|
||||
"## Overview",
|
||||
"",
|
||||
"Auth Service architecture",
|
||||
"",
|
||||
"## Components",
|
||||
"",
|
||||
"### auth-svc",
|
||||
"- **Description**: Auth service",
|
||||
"- **Boundaries**: Auth only",
|
||||
"- **Depends on**: None",
|
||||
"",
|
||||
"## Data Flow",
|
||||
"",
|
||||
"Client -> Auth -> Token",
|
||||
"",
|
||||
"## Build Order",
|
||||
"",
|
||||
"1. auth-svc",
|
||||
"",
|
||||
].join("\n"));
|
||||
|
||||
const config = loadConfig(dir);
|
||||
config.active_projects = ["task-api", "auth-svc"];
|
||||
saveConfig(dir, config);
|
||||
}
|
||||
|
||||
describe("E2E: Multi-Project Execution", () => {
|
||||
let dir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
dir = createTempDir();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup(dir);
|
||||
});
|
||||
|
||||
describe("Project management", () => {
|
||||
it("lists multiple registered projects", () => {
|
||||
setupMultiProject(dir);
|
||||
|
||||
const ciFiles = new CIAgentFiles(dir);
|
||||
const projects = ciFiles.listProjects();
|
||||
|
||||
expect(projects.length).toBeGreaterThanOrEqual(2);
|
||||
const slugs = projects.map((p) => p.slug);
|
||||
expect(slugs).toContain("task-api");
|
||||
expect(slugs).toContain("auth-svc");
|
||||
});
|
||||
|
||||
it("detects multi-project mode", () => {
|
||||
setupMultiProject(dir);
|
||||
|
||||
const ciFiles = new CIAgentFiles(dir);
|
||||
expect(ciFiles.isMultiProject()).toBe(true);
|
||||
});
|
||||
|
||||
it("reads and writes per-project files", () => {
|
||||
setupMultiProject(dir);
|
||||
|
||||
const taskFiles = new CIAgentFiles(dir, "task-api");
|
||||
const taskProject = taskFiles.readProjectMd();
|
||||
expect(taskProject).not.toBeNull();
|
||||
expect(taskProject!.name).toBe("Task API");
|
||||
|
||||
const authFiles = new CIAgentFiles(dir, "auth-svc");
|
||||
const authProject = authFiles.readProjectMd();
|
||||
expect(authProject).not.toBeNull();
|
||||
expect(authProject!.name).toBe("Auth Service");
|
||||
});
|
||||
|
||||
it("reads per-project requirements", () => {
|
||||
setupMultiProject(dir);
|
||||
|
||||
const taskFiles = new CIAgentFiles(dir, "task-api");
|
||||
const taskReqs = taskFiles.readRequirementsMd();
|
||||
expect(taskReqs).not.toBeNull();
|
||||
|
||||
const authFiles = new CIAgentFiles(dir, "auth-svc");
|
||||
const authReqs = authFiles.readRequirementsMd();
|
||||
expect(authReqs).not.toBeNull();
|
||||
});
|
||||
|
||||
it("reads per-project roadmap", () => {
|
||||
setupMultiProject(dir);
|
||||
|
||||
const taskFiles = new CIAgentFiles(dir, "task-api");
|
||||
const taskRoadmap = taskFiles.readRoadmapMd();
|
||||
expect(taskRoadmap).not.toBeNull();
|
||||
expect(taskRoadmap!.phases.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("reads per-project architecture", () => {
|
||||
setupMultiProject(dir);
|
||||
|
||||
const taskFiles = new CIAgentFiles(dir, "task-api");
|
||||
const taskArch = taskFiles.readArchitectureMd();
|
||||
expect(taskArch).not.toBeNull();
|
||||
expect(taskArch!.components.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Config with active_projects", () => {
|
||||
it("stores active_projects array in config", () => {
|
||||
setupMultiProject(dir);
|
||||
|
||||
const config = loadConfig(dir);
|
||||
expect(config.active_projects).toContain("task-api");
|
||||
expect(config.active_projects).toContain("auth-svc");
|
||||
expect(config.active_projects.length).toBe(2);
|
||||
});
|
||||
|
||||
it("max_concurrent_projects is configurable", () => {
|
||||
initCIAgent(dir, {
|
||||
parallelization: {
|
||||
...DEFAULT_CIAGENT_CONFIG.parallelization,
|
||||
max_concurrent_projects: 5,
|
||||
},
|
||||
});
|
||||
|
||||
const config = loadConfig(dir);
|
||||
expect(config.parallelization.max_concurrent_projects).toBe(5);
|
||||
});
|
||||
|
||||
it("default max_concurrent_projects is 3", () => {
|
||||
expect(DEFAULT_CIAGENT_CONFIG.parallelization.max_concurrent_projects).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Commit message project tracking", () => {
|
||||
it("includes project in ---ci--- block for task commit", () => {
|
||||
const msg = CommitBuilder.buildTaskCommit({
|
||||
type: "feat",
|
||||
phase: 1,
|
||||
milestone: "v0.10",
|
||||
project: "task-api",
|
||||
plan: "01-auth",
|
||||
task: "01-01",
|
||||
subject: "implement JWT token generation",
|
||||
status: "execute",
|
||||
});
|
||||
|
||||
expect(msg).toContain("---ci---");
|
||||
expect(msg).toContain("project: task-api");
|
||||
expect(msg).toContain("phase: 1");
|
||||
expect(msg).toContain("milestone: v0.10");
|
||||
expect(msg).toContain("status: execute");
|
||||
});
|
||||
|
||||
it("includes project in ---ci--- block for init commit", () => {
|
||||
const msg = CommitBuilder.buildInitCommit({
|
||||
projectName: "Auth Service",
|
||||
phaseCount: 2,
|
||||
milestone: "v0.10",
|
||||
project: "auth-svc",
|
||||
specification: "Authentication and authorization service",
|
||||
});
|
||||
|
||||
expect(msg).toContain("---ci---");
|
||||
expect(msg).toContain("project: auth-svc");
|
||||
expect(msg).toContain("phase: 0");
|
||||
});
|
||||
|
||||
it("different projects produce different commit scopes", () => {
|
||||
const taskMsg = CommitBuilder.buildTaskCommit({
|
||||
type: "feat",
|
||||
phase: 1,
|
||||
milestone: "v0.10",
|
||||
project: "task-api",
|
||||
plan: "01",
|
||||
task: "01",
|
||||
subject: "create task endpoint",
|
||||
status: "execute",
|
||||
});
|
||||
|
||||
const authMsg = CommitBuilder.buildTaskCommit({
|
||||
type: "feat",
|
||||
phase: 1,
|
||||
milestone: "v0.10",
|
||||
project: "auth-svc",
|
||||
plan: "01",
|
||||
task: "01",
|
||||
subject: "JWT token generation",
|
||||
status: "execute",
|
||||
});
|
||||
|
||||
expect(taskMsg).toContain("task-api/");
|
||||
expect(taskMsg).toContain("project: task-api");
|
||||
expect(authMsg).toContain("auth-svc/");
|
||||
expect(authMsg).toContain("project: auth-svc");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Per-project ideation", () => {
|
||||
it("runs ideation engine with project slug", () => {
|
||||
setupMultiProject(dir);
|
||||
|
||||
const { IdeationEngine, resetIdeaCounter } = require("../core/ideation.js");
|
||||
resetIdeaCounter();
|
||||
|
||||
const taskEngine = new IdeationEngine(dir, "task-api");
|
||||
const taskIdeas = taskEngine.runMechanical();
|
||||
|
||||
expect(Array.isArray(taskIdeas)).toBe(true);
|
||||
expect(taskIdeas.length).toBeGreaterThan(0);
|
||||
|
||||
resetIdeaCounter();
|
||||
|
||||
const authEngine = new IdeationEngine(dir, "auth-svc");
|
||||
const authIdeas = authEngine.runMechanical();
|
||||
|
||||
expect(Array.isArray(authIdeas)).toBe(true);
|
||||
expect(authIdeas.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("produces different ideas for different projects", () => {
|
||||
setupMultiProject(dir);
|
||||
|
||||
const { IdeationEngine, resetIdeaCounter } = require("../core/ideation.js");
|
||||
resetIdeaCounter();
|
||||
|
||||
const taskEngine = new IdeationEngine(dir, "task-api");
|
||||
const taskIdeas = taskEngine.runMechanical();
|
||||
const taskTitles = new Set(taskIdeas.map((i: any) => i.title));
|
||||
|
||||
resetIdeaCounter();
|
||||
|
||||
const authEngine = new IdeationEngine(dir, "auth-svc");
|
||||
const authIdeas = authEngine.runMechanical();
|
||||
const authTitles = new Set(authIdeas.map((i: any) => i.title));
|
||||
|
||||
expect(taskTitles.size).toBeGreaterThan(0);
|
||||
expect(authTitles.size).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
+1
-1
@@ -1 +1 @@
|
||||
export const VERSION = "0.9.0";
|
||||
export const VERSION = "0.10.0";
|
||||
Reference in New Issue
Block a user