Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions test/active-pointer-safety.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@ import { mkdir, mkdtemp, readFile, rm, symlink, writeFile } from "node:fs/promis
import os from "node:os";
import path from "node:path";
import { test } from "node:test";
import { FileKapiStore } from "../src/adapters/file-store.js";
import { FileWorkflowStore } from "../src/adapters/file-store.js";
import { KapiService } from "../src/application/workflow-service.js";

test("workflow state saves reject symlinked state files", async () => {
const workspace = await mkdtemp(path.join(os.tmpdir(), "kapi-state-symlink-safety-"));
const outside = path.join(workspace, "outside-state.json");
try {
const store = new FileKapiStore();
const store = new FileWorkflowStore();
const service = new KapiService(store);
const started = await service.startWorkflow({ workspace, workflowId: "kapi-ralph", task: "State symlink safety" });
const statePath = path.join(started.state.artifactRoot, "state.json");
Expand All @@ -29,7 +29,7 @@ test("active pointer saves reject symlinked active pointers", async () => {
const workspace = await mkdtemp(path.join(os.tmpdir(), "kapi-active-symlink-safety-"));
const outside = path.join(workspace, "outside-active.json");
try {
const store = new FileKapiStore();
const store = new FileWorkflowStore();
const service = new KapiService(store);
const started = await service.startWorkflow({ workspace, workflowId: "kapi-ralph", task: "Active symlink safety" });
const activePath = path.join(workspace, ".ilchul", "active.json");
Expand All @@ -51,7 +51,7 @@ test("workflow starts reject symlinked Kapi state directories", async () => {
await mkdir(outsideKapi);
await symlink(outsideKapi, path.join(workspace, ".ilchul"));

const service = new KapiService(new FileKapiStore());
const service = new KapiService(new FileWorkflowStore());
await assert.rejects(
service.startWorkflow({ workspace, workflowId: "kapi-ralph", task: "Ancestor symlink safety" }),
/state directories must not be symbolic links: \.ilchul/i,
Expand All @@ -65,7 +65,7 @@ test("active pointer reads reject symlinked active pointers", async () => {
const workspace = await mkdtemp(path.join(os.tmpdir(), "kapi-active-read-symlink-safety-"));
const outside = path.join(workspace, "outside-active.json");
try {
const service = new KapiService(new FileKapiStore());
const service = new KapiService(new FileWorkflowStore());
await service.startWorkflow({ workspace, workflowId: "kapi-ralph", task: "Active read symlink safety" });
const activePath = path.join(workspace, ".ilchul", "active.json");
await rm(activePath);
Expand All @@ -82,7 +82,7 @@ test("active pointer reads reject symlinked workflow state files", async () => {
const workspace = await mkdtemp(path.join(os.tmpdir(), "kapi-state-read-symlink-safety-"));
const outside = path.join(workspace, "outside-state.json");
try {
const service = new KapiService(new FileKapiStore());
const service = new KapiService(new FileWorkflowStore());
const started = await service.startWorkflow({ workspace, workflowId: "kapi-ralph", task: "State read symlink safety" });
const statePath = path.join(started.state.artifactRoot, "state.json");
await rm(statePath);
Expand All @@ -98,7 +98,7 @@ test("active pointer reads reject symlinked workflow state files", async () => {
test("active pointers with mismatched embedded workspace state are cleared instead of loaded", async () => {
const workspace = await mkdtemp(path.join(os.tmpdir(), "kapi-active-pointer-mismatch-"));
try {
const service = new KapiService(new FileKapiStore());
const service = new KapiService(new FileWorkflowStore());
const started = await service.startWorkflow({ workspace, workflowId: "kapi-ralph", task: "Mismatched embedded workspace" });
const statePath = path.join(started.state.artifactRoot, "state.json");
await writeFile(
Expand Down Expand Up @@ -133,7 +133,7 @@ test("stale active pointers outside the workspace are cleared instead of loaded"
"utf8",
);

const service = new KapiService(new FileKapiStore());
const service = new KapiService(new FileWorkflowStore());
assert.equal(await service.getActiveStatus(workspace), undefined);
await assert.rejects(() => readFile(path.join(workspace, ".ilchul", "active.json"), "utf8"), /ENOENT/);
} finally {
Expand Down
10 changes: 5 additions & 5 deletions test/active-replacement.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@ import { mkdtemp, rm } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { test } from "node:test";
import { FileKapiStore } from "../src/adapters/file-store.js";
import { FileWorkflowStore } from "../src/adapters/file-store.js";
import { KapiService } from "../src/application/workflow-service.js";
import { FixedClock } from "./support/fixtures.js";

test("starting a new workflow requires explicit replacement when another workflow is active", async () => {
const workspace = await mkdtemp(path.join(os.tmpdir(), "kapi-replace-"));
try {
const service = new KapiService(new FileKapiStore(), new FixedClock());
const service = new KapiService(new FileWorkflowStore(), new FixedClock());
const plan = await service.startWorkflow({ workspace, workflowId: "kapi-ralph", task: "Plan the change" });

await assert.rejects(
Expand All @@ -34,7 +34,7 @@ test("starting a new workflow requires explicit replacement when another workflo
test("explicit slugs reject detached workflow history overwrite", async () => {
const workspace = await mkdtemp(path.join(os.tmpdir(), "kapi-explicit-slug-overwrite-"));
try {
const service = new KapiService(new FileKapiStore(), new FixedClock());
const service = new KapiService(new FileWorkflowStore(), new FixedClock());
const first = await service.startWorkflow({ workspace, workflowId: "kapi-deep-interview", task: "Original explicit run", slug: "stable-history" });
await service.clear(workspace, "detach first run");

Expand All @@ -54,7 +54,7 @@ test("explicit slugs reject detached workflow history overwrite", async () => {
test("generated slugs avoid overwriting detached workflow history", async () => {
const workspace = await mkdtemp(path.join(os.tmpdir(), "kapi-generated-slug-unique-"));
try {
const service = new KapiService(new FileKapiStore(), new FixedClock());
const service = new KapiService(new FileWorkflowStore(), new FixedClock());
const first = await service.startWorkflow({ workspace, workflowId: "kapi-deep-interview", task: "Repeatable task" });
await service.clear(workspace, "detach first run");

Expand All @@ -73,7 +73,7 @@ test("generated slugs avoid overwriting detached workflow history", async () =>
test("targeted clear detaches only when the selected recorded workflow is active", async () => {
const workspace = await mkdtemp(path.join(os.tmpdir(), "kapi-target-clear-"));
try {
const store = new FileKapiStore();
const store = new FileWorkflowStore();
const service = new KapiService(store, new FixedClock());
const stale = await service.startWorkflow({ workspace, workflowId: "kapi-ralph", task: "Stale blocked plan", slug: "stale-plan" });
await store.clearActive(workspace);
Expand Down
4 changes: 2 additions & 2 deletions test/artifact-empty-name.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ import { mkdtemp, rm } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { test } from "node:test";
import { FileKapiStore } from "../src/adapters/file-store.js";
import { FileWorkflowStore } from "../src/adapters/file-store.js";
import { KapiService } from "../src/application/workflow-service.js";

test("artifact operations reject blank artifact names with a clear error", async () => {
const workspace = await mkdtemp(path.join(os.tmpdir(), "kapi-empty-artifact-"));
try {
const service = new KapiService(new FileKapiStore());
const service = new KapiService(new FileWorkflowStore());
await service.startWorkflow({ workspace, workflowId: "kapi-ralph", task: "Reject blank artifact names" });

await assert.rejects(
Expand Down
4 changes: 2 additions & 2 deletions test/artifact-list.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@ import { mkdtemp, rm } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { test } from "node:test";
import { FileKapiStore } from "../src/adapters/file-store.js";
import { FileWorkflowStore } from "../src/adapters/file-store.js";
import { KapiService } from "../src/application/workflow-service.js";
import { formatArtifactList } from "../src/presentation/messages.js";

test("service lists tracked artifacts for the active workflow", async () => {
const workspace = await mkdtemp(path.join(os.tmpdir(), "kapi-status-artifacts-"));
try {
const service = new KapiService(new FileKapiStore());
const service = new KapiService(new FileWorkflowStore());
await service.startWorkflow({ workspace, workflowId: "kapi-deep-interview", task: "Interview" });
await service.writeArtifact({ workspace, artifactName: "notes.md", content: "\n## Optional note\n" });
await service.writeArtifact({ workspace, artifactName: "notes.md", content: "\n## Required note\n", required: true });
Expand Down
4 changes: 2 additions & 2 deletions test/artifact-metadata.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ import { chmod, mkdir, mkdtemp, rm, symlink, writeFile } from "node:fs/promises"
import * as os from "node:os";
import * as path from "node:path";
import { test } from "node:test";
import { FileKapiStore } from "../src/adapters/file-store.js";
import { FileWorkflowStore } from "../src/adapters/file-store.js";
import { createWorkflowState } from "../src/domain/state-machine.js";

test("file store reports workflow artifact metadata without following symlinks", async () => {
const workspace = await mkdtemp(path.join(os.tmpdir(), "kapi-artifact-metadata-"));
try {
const store = new FileKapiStore();
const store = new FileWorkflowStore();
const state = createWorkflowState({ workspace, workflowId: "kapi-autoresearch", task: "Inspect artifact metadata", slug: "metadata", now: "2026-01-01T00:00:00.000Z" });
await mkdir(state.artifactRoot, { recursive: true });

Expand Down
6 changes: 3 additions & 3 deletions test/artifact-overwrite.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@ import { mkdtemp, rm } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { test } from "node:test";
import { FileKapiStore } from "../src/adapters/file-store.js";
import { FileWorkflowStore } from "../src/adapters/file-store.js";
import { KapiService } from "../src/application/workflow-service.js";
import { FixedClock } from "./support/fixtures.js";

test("artifact writes can explicitly overwrite or append workflow-scoped content", async () => {
const workspace = await mkdtemp(path.join(os.tmpdir(), "kapi-status-artifact-overwrite-"));
try {
const service = new KapiService(new FileKapiStore());
const service = new KapiService(new FileWorkflowStore());
await service.startWorkflow({ workspace, workflowId: "kapi-ralph", task: "Plan artifact overwrite behavior" });

await service.writeArtifact({ workspace, artifactName: "IMPLEMENTATION_PLAN.md", content: "first draft", append: false });
Expand Down Expand Up @@ -38,7 +38,7 @@ test("artifact writes can explicitly overwrite or append workflow-scoped content
test("terminal workflow completion syncs authored artifact headers without prepending decision logs", async () => {
const workspace = await mkdtemp(path.join(os.tmpdir(), "kapi-authored-artifact-sync-"));
try {
const service = new KapiService(new FileKapiStore(), new FixedClock("2026-05-09T00:00"));
const service = new KapiService(new FileWorkflowStore(), new FixedClock("2026-05-09T00:00"));
await service.startWorkflow({ workspace, workflowId: "kapi-deep-interview", task: "Clarify browser bridge" });
const handoffReadyBody = `# Decision Report

Expand Down
6 changes: 3 additions & 3 deletions test/artifact-path-safety-write.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@ import { mkdtemp, readFile, rm, symlink, writeFile } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { test } from "node:test";
import { FileKapiStore } from "../src/adapters/file-store.js";
import { FileWorkflowStore } from "../src/adapters/file-store.js";
import { KapiService } from "../src/application/workflow-service.js";

test("artifact operations reject symlinked files instead of following them outside the artifact root", async () => {
const workspace = await mkdtemp(path.join(os.tmpdir(), "kapi-artifact-symlink-safety-"));
const outside = path.join(workspace, "outside.md");
try {
const service = new KapiService(new FileKapiStore());
const service = new KapiService(new FileWorkflowStore());
const started = await service.startWorkflow({ workspace, workflowId: "kapi-ralph", task: "Artifact symlink safety", slug: "artifact-symlink-safety" });
const planPath = path.join(started.state.artifactRoot, "IMPLEMENTATION_PLAN.md");
const verifyPath = path.join(started.state.artifactRoot, "verify.md");
Expand All @@ -35,7 +35,7 @@ test("artifact operations reject symlinked files instead of following them outsi
test("artifact writes reject path separators and preserve active workflow state", async () => {
const workspace = await mkdtemp(path.join(os.tmpdir(), "kapi-status-artifact-write-safety-"));
try {
const service = new KapiService(new FileKapiStore());
const service = new KapiService(new FileWorkflowStore());
const started = await service.startWorkflow({ workspace, workflowId: "kapi-ralph", task: "Artifact path safety", slug: "artifact-safety" });

await assert.rejects(
Expand Down
4 changes: 2 additions & 2 deletions test/autoresearch-loop.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { mkdtemp, rm, writeFile } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import test from "node:test";
import { FileKapiStore } from "../src/adapters/file-store.js";
import { FileWorkflowStore } from "../src/adapters/file-store.js";
import { KapiService } from "../src/application/workflow-service.js";
import { parseAutoresearchContract, parseAutoresearchIdeas, runAutoresearchLoop, scanAutoresearchAntiGaming, serializeAutoresearchLedger, type AutoresearchCommandResult, type AutoresearchLoopEntry } from "../src/application/autoresearch-loop.js";
function runner(outputs: Record<string, AutoresearchCommandResult | AutoresearchCommandResult[]>) {
Expand Down Expand Up @@ -100,7 +100,7 @@ test("autoresearch anti-gaming scan forces discard before keep decisions", async
test("serialized loop output stays valid under Kapi autoresearch ledger validation", async () => {
const workspace = await mkdtemp(path.join(os.tmpdir(), "kapi-autoresearch-loop-ledger-"));
try {
const service = new KapiService(new FileKapiStore());
const service = new KapiService(new FileWorkflowStore());
const started = await service.startWorkflow({ workspace, workflowId: "kapi-autoresearch", task: "Validate loop ledger", slug: "loop-ledger" });
const benchmarkContent = "#!/usr/bin/env bash\nscore=1\nprintf 'METRIC score=%s\\n' \"$score\"\n";
const fx = runner({ "./benchmark.sh": [ok(5), ok(8)], "try-a": { exitCode: 0, stdout: "done" }, checks: { exitCode: 0, stdout: "ok" } });
Expand Down
4 changes: 2 additions & 2 deletions test/context-artifact-persistence.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { test } from "node:test";
import { FileKapiStore } from "../src/adapters/file-store.js";
import { FileWorkflowStore } from "../src/adapters/file-store.js";
import { FileProjectContextProvider } from "../src/adapters/project-context.js";
import { KapiService } from "../src/application/workflow-service.js";

Expand All @@ -15,7 +15,7 @@ test("context-aware durable modes include project context in the prompt and keep
await mkdir(path.join(workspace, "src"), { recursive: true });
await writeFile(path.join(workspace, "src", "feature.ts"), "export const feature = true;\n", "utf8");

const service = new KapiService(new FileKapiStore(), undefined, new FileProjectContextProvider());
const service = new KapiService(new FileWorkflowStore(), undefined, new FileProjectContextProvider());
const started = await service.startWorkflow({ workspace, workflowId: "kapi-ralph", task: "Plan with brownfield context", slug: "brownfield-plan" });
const verify = (await service.readArtifact({ workspace, artifactName: "verify.md" }))?.content ?? "";
assert.match(verify, /"mode": "kapi-ralph"/);
Expand Down
4 changes: 2 additions & 2 deletions test/corrupt-recorded-lane-skip.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { test } from "node:test";
import { FileKapiStore } from "../src/adapters/file-store.js";
import { FileWorkflowStore } from "../src/adapters/file-store.js";
import { KapiService } from "../src/application/workflow-service.js";

test("workflow history skips corrupt recorded workflow state without hiding valid workflows", async () => {
const workspace = await mkdtemp(path.join(os.tmpdir(), "kapi-corrupt-recorded-"));
try {
const service = new KapiService(new FileKapiStore());
const service = new KapiService(new FileWorkflowStore());
const valid = await service.startWorkflow({ workspace, workflowId: "kapi-ralph", task: "Valid recorded workflow" });
await service.clear(workspace, "keep valid workflow recorded");

Expand Down
10 changes: 5 additions & 5 deletions test/deep-interview-completion-gate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { mkdtemp, rm } from "node:fs/promises";
import * as os from "node:os";
import * as path from "node:path";
import { test } from "node:test";
import { FileKapiStore } from "../src/adapters/file-store.js";
import { FileWorkflowStore } from "../src/adapters/file-store.js";
import { KapiService } from "../src/application/workflow-service.js";
import type { DeepInterviewReviewer } from "../src/application/deep-interview/reviewer.js";
import { reviewDeepInterviewCompletion } from "../src/application/deep-interview/readiness-guard.js";
Expand Down Expand Up @@ -91,7 +91,7 @@ async function startDeepInterview(service: KapiService, workspace: string, conte
test("deep interview completion rejects self-approved incomplete handoff and keeps workflow active", async () => {
const workspace = await mkdtemp(path.join(os.tmpdir(), "kapi-deep-complete-block-"));
try {
const service = new KapiService(new FileKapiStore(), new FixedClock("2026-05-09T00:00"));
const service = new KapiService(new FileWorkflowStore(), new FixedClock("2026-05-09T00:00"));
await startDeepInterview(service, workspace, incompleteContext);

await assert.rejects(
Expand Down Expand Up @@ -129,7 +129,7 @@ test("deep interview completion rejects self-approved incomplete handoff and kee
test("deep interview completion succeeds only after judge approval from fresh complete snapshot", async () => {
const workspace = await mkdtemp(path.join(os.tmpdir(), "kapi-deep-complete-pass-"));
try {
const service = new KapiService(new FileKapiStore(), new FixedClock("2026-05-09T00:00"));
const service = new KapiService(new FileWorkflowStore(), new FixedClock("2026-05-09T00:00"));
await startDeepInterview(service, workspace, completeContext);

const completed = await service.completeWorkflow({ workspace, evidence: { kind: "review", role: "verifier", verdict: "approve", summary: "Main agent proposes handoff completion." } });
Expand All @@ -149,7 +149,7 @@ test("deep interview completion succeeds only after judge approval from fresh co
test("deep interview rejects direct status completion without readiness proposal path", async () => {
const workspace = await mkdtemp(path.join(os.tmpdir(), "kapi-deep-direct-complete-block-"));
try {
const service = new KapiService(new FileKapiStore(), new FixedClock("2026-05-09T00:00"));
const service = new KapiService(new FileWorkflowStore(), new FixedClock("2026-05-09T00:00"));
await startDeepInterview(service, workspace, completeContext);
await service.recordEvidence({ workspace, evidence: { kind: "manual", summary: "Some evidence exists." } });

Expand Down Expand Up @@ -178,7 +178,7 @@ test("deep interview rejects approved reviews with stale snapshot hashes", async
}),
};
try {
const service = new KapiService(new FileKapiStore(), new FixedClock("2026-05-09T00:00"), undefined, undefined, undefined, undefined, undefined, undefined, staleReviewer);
const service = new KapiService(new FileWorkflowStore(), new FixedClock("2026-05-09T00:00"), undefined, undefined, undefined, undefined, undefined, undefined, staleReviewer);
await startDeepInterview(service, workspace, completeContext);

await assert.rejects(
Expand Down
Loading
Loading