fb3f1df13e
- 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---
163 lines
5.6 KiB
TypeScript
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();
|
|
});
|
|
});
|
|
}); |