Files
ci/src/core/decision-engine.test.ts
T
CI fb3f1df13e release(v0.4.0): purge learnship, migrate .planning→.ci, fix backends, add test coverage
- Remove all learnship references: Decision.learnship_equivalent field,
  agent persona prompts, opencode.json permissions, test fixtures
- Migrate verification layers from .planning/ to .ci/: structural
  checks .ci/ dir + ROADMAP.md, behavioral checks ROADMAP.md
- Fix ollama-local: remove sync require+curl blocking, use async
  fetchAvailableModels() in callModel
- Fix opencode.json: use __OPENCODE_DIR__ template tokens, remove
  legacy learnship permission entries
- Remove duplicate install script from package.json (keep postinstall)
- Fix quality any-regex false positives (target type annotations only)
- Add backends test coverage: backends.test.ts, tool-registry.test.ts
- Version bump 0.3.0 → 0.4.0
- Artifacts module: rename .planning→.ci internal paths
- Remove dead TODO_PATTERN/FIXME_PATTERN constants

---ci---
phase: 3
milestone: v0.4
status: complete
requirements:
  covered: [REQ-09, REQ-10, REQ-11, REQ-13, REQ-14, REQ-17]
  partial: []
decisions:
  - id: D-001
    decision: purge all learnship references from codebase
    rationale: project is CI-only, learnship is no longer a dependency
    confidence: 0.99
    category: scope
    alternatives: [keep for historical reference]
  - id: D-002
    decision: migrate verification from .planning/ to .ci/ paths
    rationale: .planning/ is removed schema, all current state lives in .ci/
    confidence: 0.95
    category: architecture
    alternatives: [keep dual-path support]
  - id: D-003
    decision: use __OPENCODE_DIR__ template tokens in opencode.json
    rationale: hardcoded ~ paths fail in containers and non-standard homes
    confidence: 0.90
    category: implementation_approach
    alternatives: [keep tilde expansion]
---/ci---
2026-05-29 16:18:30 +00:00

163 lines
5.6 KiB
TypeScript

import * as fs from "node:fs";
import * as path from "node:path";
import * as os from "node:os";
import { DecisionEngine, DecisionInput } from "../core/decision-engine.js";
import { DEFAULT_CI_CONFIG } from "../types/config.js";
describe("DecisionEngine", () => {
let tempDir: string;
let engine: DecisionEngine;
beforeEach(() => {
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "ci-decision-test-"));
engine = new DecisionEngine(DEFAULT_CI_CONFIG, tempDir);
});
afterEach(() => {
fs.rmSync(tempDir, { recursive: true, force: true });
});
const baseInput: DecisionInput = {
decision: "Use PostgreSQL for storage",
rationale: "Strong ecosystem, ACID compliance needed",
confidence: 0.95,
category: "technology_choice",
alternatives_considered: [
{ option: "MongoDB", rejected_reason: "No ACID transactions" },
{ option: "SQLite", rejected_reason: "No concurrent writes" },
],
};
describe("makeDecision", () => {
it("auto-decides with high confidence (above threshold)", () => {
const result = engine.makeDecision(baseInput);
expect(result.escalated).toBe(false);
expect(result.decision.id).toMatch(/^D-\d{3}$/);
expect(result.decision.confidence).toBe(0.95);
expect(result.decision.category).toBe("technology_choice");
});
it("escalates with low confidence (below threshold)", () => {
const result = engine.makeDecision({
...baseInput,
confidence: 0.4,
});
expect(result.escalated).toBe(true);
expect(result.reason).toContain("below threshold");
});
it("auto-decides at exactly threshold confidence", () => {
const result = engine.makeDecision({
...baseInput,
confidence: 0.6,
});
expect(result.escalated).toBe(false);
});
it("increments decision IDs sequentially", () => {
const result1 = engine.makeDecision(baseInput);
const result2 = engine.makeDecision(baseInput);
expect(result1.decision.id).toBe("D-001");
expect(result2.decision.id).toBe("D-002");
});
it("generates commit message for git-native audit trail", () => {
const result = engine.makeDecision(baseInput);
expect(result.commitMessage).toBeDefined();
expect(result.commitMessage).toContain("---ci---");
expect(result.commitMessage).toContain("D-001");
expect(result.commitMessage).toContain("Use PostgreSQL for storage");
});
it("preserves alternatives in the decision", () => {
const result = engine.makeDecision(baseInput);
expect(result.decision.alternatives_considered).toHaveLength(2);
expect(result.decision.alternatives_considered![0].option).toBe("MongoDB");
});
it("sets human_override to null by default", () => {
const result = engine.makeDecision(baseInput);
expect(result.decision.human_override).toBeNull();
});
});
describe("makeHighConfidenceDecision", () => {
it("creates a decision with 0.95 confidence", () => {
const result = engine.makeHighConfidenceDecision(
"Use REST API",
"REST is well-understood and has wide tooling support",
"architecture"
);
expect(result.escalated).toBe(false);
expect(result.decision.confidence).toBe(0.95);
});
});
describe("makeMediumConfidenceDecision", () => {
it("creates a decision with 0.7 confidence", () => {
const result = engine.makeMediumConfidenceDecision(
"Use JWT for auth",
"JWT is standard for stateless APIs",
"implementation_approach"
);
expect(result.escalated).toBe(false);
expect(result.decision.confidence).toBe(0.7);
});
it("escalates if threshold is raised above 0.7", () => {
const strictConfig = {
...DEFAULT_CI_CONFIG,
autonomy: { ...DEFAULT_CI_CONFIG.autonomy, decision_confidence_threshold: 0.8 },
};
const strictEngine = new DecisionEngine(strictConfig, tempDir);
const result = strictEngine.makeMediumConfidenceDecision(
"Use JWT for auth",
"JWT is standard",
"implementation_approach"
);
expect(result.escalated).toBe(true);
});
});
describe("shouldAutoDecide", () => {
it("returns true when confidence meets threshold", () => {
expect(engine.shouldAutoDecide(0.6)).toBe(true);
expect(engine.shouldAutoDecide(0.8)).toBe(true);
expect(engine.shouldAutoDecide(1.0)).toBe(true);
});
it("returns false when confidence is below threshold", () => {
expect(engine.shouldAutoDecide(0.59)).toBe(false);
expect(engine.shouldAutoDecide(0.3)).toBe(false);
expect(engine.shouldAutoDecide(0.0)).toBe(false);
});
});
describe("isIrreversibleAction", () => {
it("detects irreversible actions from escalation_hooks", () => {
expect(engine.isIrreversibleAction("deploy to production")).toBe(true);
expect(engine.isIrreversibleAction("delete_data in database")).toBe(true);
expect(engine.isIrreversibleAction("merge_to_main branch")).toBe(true);
});
it("returns false for non-irreversible actions", () => {
expect(engine.isIrreversibleAction("create file")).toBe(false);
expect(engine.isIrreversibleAction("run tests")).toBe(false);
expect(engine.isIrreversibleAction("refactor code")).toBe(false);
});
});
describe("setPhase", () => {
it("updates the current phase", () => {
engine.setPhase(3);
expect(engine.setPhase).toBeDefined();
});
});
describe("setMilestone", () => {
it("updates the current milestone", () => {
engine.setMilestone("v2.0");
expect(engine.setMilestone).toBeDefined();
});
});
});