Menu
AkurAI-Build
publicLatest change 30e6b8c347048d64c2c048dfd05a8ded219d37f6 - Host authenticated Git repositories over Smart HTTP with repo host/sync CLI by Ólafur Búi Ólafsson
import assert from "node:assert/strict";
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { dirname, join } from "node:path";
import { spawnSync } from "node:child_process";
import test from "node:test";
import { fileURLToPath } from "node:url";
const KANBAN = join(dirname(fileURLToPath(import.meta.url)), "..");
const CLI = join(KANBAN, "bin", "kanban.mjs");
function parseJson(value, context) {
try {
return JSON.parse(value);
} catch (error) {
throw new Error(`${context}: invalid JSON (${error.message})`);
}
}
async function fixture(inProgressWip = 3) {
const root = await mkdtemp(join(tmpdir(), "project-kanban-"));
const board = parseJson(
await readFile(join(KANBAN, "board.json"), "utf8"),
"board.json",
);
board.columns.find((column) => column.id === "in-progress").wipLimit =
inProgressWip;
await writeFile(join(root, "board.json"), JSON.stringify(board));
for (const column of board.columns) {
await mkdir(join(root, "columns", column.directory), { recursive: true });
}
await mkdir(join(root, "state"), { recursive: true });
await mkdir(join(root, "handoffs"), { recursive: true });
await writeFile(join(root, "state", "events.jsonl"), "");
return root;
}
function run(root, ...args) {
return spawnSync(process.execPath, [CLI, ...args], {
env: { ...process.env, KANBAN_ROOT: root },
encoding: "utf8",
});
}
function succeeds(result) {
assert.equal(result.status, 0, result.stderr || result.stdout);
return result.stdout.trim();
}
async function completeHandoff(root, handoff) {
const path = join(root, handoff);
const document = await readFile(path, "utf8");
await writeFile(
path,
document
.replace(
"Describe what is complete and the task's present status.",
"Implementation complete; review is next.",
)
.replace(
"List changed paths, branches, commits, or external artifacts.",
"Changed the task-scoped fixture only.",
)
.replace(
"List commands and results already obtained. Never claim checks that were not run.",
"node --test: pass",
)
.replace(
"List bounded next steps in execution order.",
"Review, verify, and prepare release evidence.",
)
.replace(
"Record blockers, assumptions, rejected alternatives, and approvals still required.",
"No blocker; release approval remains required.",
)
.replace(
"Give the receiving agent one deterministic first action.",
"Read the task and inspect current evidence.",
)
.replace(
"State when the receiver must stop for human input or external action.",
"Stop before release without human approval.",
),
);
}
async function createReady(root, title) {
const id = succeeds(run(root, "new", "--title", title));
succeeds(run(root, "move", id, "triage", "--agent", "planner"));
await refine(root, "10-triage", id);
succeeds(run(root, "move", id, "ready", "--agent", "planner"));
return id;
}
async function refine(root, directory, id) {
const path = join(root, "columns", directory, `${id}.md`);
const card = await readFile(path, "utf8");
await writeFile(
path,
card
.replace(
"Describe the user or project outcome and why it matters.",
"Deliver a tested project outcome for the intended user.",
)
.replace(
"Link the canonical issue, specification, decision, or incident. Record constraints and non-goals.",
"Canonical local task; no external issue. Non-goal: unrelated refactors.",
)
.replace(
"- [ ] State observable, testable outcomes.",
"- [ ] The requested behavior is implemented and verified.",
)
.replace(
"State always-do, ask-first, and never-do boundaries for this task.",
"Always run tests. Ask before dependencies. Never expose secrets.",
)
.replace(
"List exact commands, checks, URLs, screenshots, or artifacts required for completion.",
"Run node --test and record the exact result.",
),
);
}
test("runs a task through claim, handoff, gates, and metrics", async (t) => {
const root = await fixture();
t.after(() => rm(root, { recursive: true, force: true }));
assert.match(succeeds(run(root, "validate")), /^OK:/);
const id = succeeds(
run(
root,
"new",
"--title",
"Ship a bounded change",
"--type",
"feature",
"--priority",
"P1",
),
);
for (const status of ["triage", "discovery"]) {
succeeds(run(root, "move", id, status, "--agent", "planner"));
}
await refine(root, "20-discovery", id);
succeeds(run(root, "move", id, "ready", "--agent", "planner"));
const next = parseJson(succeeds(run(root, "next", "--json")), "next output");
assert.equal(next[0].id, id);
succeeds(run(root, "claim", id, "--agent", "developer"));
succeeds(
run(
root,
"evidence",
id,
"--stage",
"implementation",
"--result",
"node --test: pass",
"--agent",
"developer",
),
);
const intruder = run(
root,
"set",
id,
"--field",
"priority",
"--value",
"P2",
"--agent",
"intruder",
);
assert.notEqual(intruder.status, 0);
assert.match(intruder.stderr, /use a handoff/);
const handoff = succeeds(
run(
root,
"handoff",
id,
"--from",
"developer",
"--to",
"reviewer",
"--summary",
"Review the bounded implementation",
),
);
assert.match(handoff, /^handoffs\//);
const incompleteHandoff = run(root, "accept", id, "--agent", "reviewer");
assert.notEqual(incompleteHandoff.status, 0);
assert.match(incompleteHandoff.stderr, /handoff requires/);
await completeHandoff(root, handoff);
const handoffPath = join(root, handoff);
const validHandoff = await readFile(handoffPath, "utf8");
await writeFile(
handoffPath,
validHandoff.replace(
"node --test: pass",
"List commands and results already obtained. Never claim checks that were not run.",
),
);
const missingHandoffEvidence = run(root, "accept", id, "--agent", "reviewer");
assert.notEqual(missingHandoffEvidence.status, 0);
assert.match(missingHandoffEvidence.stderr, /Verification evidence/);
await writeFile(handoffPath, validHandoff);
await writeFile(
handoffPath,
validHandoff.replace(
`"taskId": "${id}"`,
'"taskId": "KB-00000000000000-0000"',
),
);
const mismatchedHandoff = run(root, "accept", id, "--agent", "reviewer");
assert.notEqual(mismatchedHandoff.status, 0);
assert.match(mismatchedHandoff.stderr, /taskId does not match/);
await writeFile(handoffPath, validHandoff);
succeeds(run(root, "accept", id, "--agent", "reviewer"));
succeeds(run(root, "move", id, "review", "--agent", "reviewer"));
const staleReview = run(
root,
"move",
id,
"verification",
"--agent",
"reviewer",
);
assert.notEqual(staleReview.status, 0);
assert.match(staleReview.stderr, /requires review evidence/);
succeeds(
run(
root,
"evidence",
id,
"--stage",
"review",
"--result",
"independent review: clean",
"--agent",
"reviewer",
),
);
succeeds(run(root, "move", id, "verification", "--agent", "reviewer"));
const staleVerification = run(
root,
"move",
id,
"release-ready",
"--agent",
"reviewer",
);
assert.notEqual(staleVerification.status, 0);
assert.match(staleVerification.stderr, /requires verification evidence/);
succeeds(
run(
root,
"evidence",
id,
"--stage",
"verification",
"--result",
"acceptance checks: pass",
"--agent",
"reviewer",
),
);
succeeds(run(root, "move", id, "release-ready", "--agent", "reviewer"));
succeeds(run(root, "move", id, "verification", "--agent", "reviewer"));
const staleAfterReturn = run(
root,
"move",
id,
"release-ready",
"--agent",
"reviewer",
);
assert.notEqual(staleAfterReturn.status, 0);
assert.match(staleAfterReturn.stderr, /requires verification evidence/);
succeeds(
run(
root,
"evidence",
id,
"--stage",
"verification",
"--result",
"reverification: pass",
"--agent",
"reviewer",
),
);
succeeds(run(root, "move", id, "release-ready", "--agent", "reviewer"));
succeeds(
run(
root,
"set",
id,
"--field",
"resolution",
"--value",
"delivered",
"--agent",
"reviewer",
),
);
const unapproved = run(root, "move", id, "done", "--agent", "reviewer");
assert.notEqual(unapproved.status, 0);
assert.match(unapproved.stderr, /requires --approval/);
succeeds(
run(
root,
"move",
id,
"done",
"--agent",
"reviewer",
"--approval",
"human-owner",
),
);
const validation = parseJson(
succeeds(run(root, "validate", "--json")),
"validate output",
);
assert.equal(validation.ok, true);
const acceptedHandoff = await readFile(handoffPath, "utf8");
const acceptedAt = acceptedHandoff.match(/"acceptedAt": "([^"]+)"/)?.[1];
assert.ok(acceptedAt);
const shiftedAcceptedAt = new Date(
Date.parse(acceptedAt) + 1000,
).toISOString();
await writeFile(
handoffPath,
acceptedHandoff.replace(acceptedAt, shiftedAcceptedAt),
);
const mismatchedHandoffTime = run(root, "validate", "--json");
assert.notEqual(mismatchedHandoffTime.status, 0);
assert.match(mismatchedHandoffTime.stdout, /missing handoff-accepted event/);
await writeFile(handoffPath, acceptedHandoff);
const metrics = parseJson(succeeds(run(root, "metrics")), "metrics output");
assert.equal(metrics.wip.done, 1);
assert.equal(metrics.throughput7Days, 1);
assert.equal(metrics.cycleTimeDays.count, 1);
const eventText = await readFile(join(root, "state", "events.jsonl"), "utf8");
const events = eventText
.trim()
.split("\n")
.map((line) => parseJson(line, "event"));
assert.ok(events.some((event) => event.action === "handoff-accepted"));
const tamperedEvents = events.map((event) =>
event.action === "moved" && event.to === "done"
? { ...event, approval: null }
: event,
);
await writeFile(
join(root, "state", "events.jsonl"),
tamperedEvents.map((event) => JSON.stringify(event)).join("\n") + "\n",
);
const missingApprovalEvent = run(root, "validate", "--json");
assert.notEqual(missingApprovalEvent.status, 0);
assert.match(missingApprovalEvent.stdout, /lacks Release Ready approval/);
await writeFile(join(root, "state", "events.jsonl"), eventText);
});
test("requires every Ready section independently", async (t) => {
const root = await fixture();
t.after(() => rm(root, { recursive: true, force: true }));
const id = succeeds(run(root, "new", "--title", "Refine every section"));
succeeds(run(root, "move", id, "triage", "--agent", "planner"));
await refine(root, "10-triage", id);
const path = join(root, "columns", "10-triage", `${id}.md`);
const valid = await readFile(path, "utf8");
const cases = [
[
"Deliver a tested project outcome for the intended user.",
"Describe the user or project outcome and why it matters.",
/Objective/,
],
[
"Canonical local task; no external issue. Non-goal: unrelated refactors.",
"Link the canonical issue, specification, decision, or incident. Record constraints and non-goals.",
/Context/,
],
[
"- [ ] The requested behavior is implemented and verified.",
"- [ ] State observable, testable outcomes.",
/acceptance criteria/,
],
[
"Always run tests. Ask before dependencies. Never expose secrets.",
"State always-do, ask-first, and never-do boundaries for this task.",
/Boundaries/,
],
[
"Run node --test and record the exact result.",
"List exact commands, checks, URLs, screenshots, or artifacts required for completion.",
/Verification/,
],
];
for (const [completed, placeholder, message] of cases) {
await writeFile(path, valid.replace(completed, placeholder));
const result = run(root, "move", id, "ready", "--agent", "planner");
assert.notEqual(result.status, 0);
assert.match(result.stderr, message);
}
await writeFile(path, valid);
succeeds(run(root, "move", id, "ready", "--agent", "planner"));
});
test("keeps blocked work in place and refuses claim", async (t) => {
const root = await fixture();
t.after(() => rm(root, { recursive: true, force: true }));
const id = succeeds(run(root, "new", "--title", "Wait for dependency"));
succeeds(run(root, "move", id, "triage", "--agent", "planner"));
const unrefined = run(root, "move", id, "ready", "--agent", "planner");
assert.notEqual(unrefined.status, 0);
assert.match(unrefined.stderr, /Ready requires/);
await refine(root, "10-triage", id);
const triagePath = join(root, "columns", "10-triage", `${id}.md`);
const refined = await readFile(triagePath, "utf8");
await writeFile(
triagePath,
refined.replace(
"Always run tests. Ask before dependencies. Never expose secrets.",
"State always-do, ask-first, and never-do boundaries for this task.",
),
);
const missingBoundaries = run(
root,
"move",
id,
"ready",
"--agent",
"planner",
);
assert.notEqual(missingBoundaries.status, 0);
assert.match(missingBoundaries.stderr, /Boundaries/);
await writeFile(triagePath, refined);
succeeds(run(root, "move", id, "ready", "--agent", "planner"));
succeeds(
run(
root,
"block",
id,
"--reason",
"external approval",
"--agent",
"planner",
),
);
const claim = run(root, "claim", id, "--agent", "developer");
assert.notEqual(claim.status, 0);
assert.match(claim.stderr, /blocked task cannot be claimed/);
const card = parseJson(
succeeds(run(root, "show", id, "--json")),
"show output",
);
assert.equal(card.status, "ready");
assert.equal(card.blocked, true);
succeeds(run(root, "unblock", id, "--agent", "planner"));
const directMove = run(
root,
"move",
id,
"in-progress",
"--agent",
"developer",
);
assert.notEqual(directMove.status, 0);
assert.match(directMove.stderr, /use claim/);
succeeds(run(root, "claim", id, "--agent", "developer"));
});
test("refuses to reopen a dependency used by committed work", async (t) => {
const root = await fixture();
t.after(() => rm(root, { recursive: true, force: true }));
const dependency = succeeds(
run(root, "new", "--title", "Completed dependency"),
);
succeeds(run(root, "move", dependency, "triage", "--agent", "planner"));
succeeds(
run(
root,
"evidence",
dependency,
"--stage",
"resolution",
"--result",
"existing behavior verified",
"--agent",
"planner",
),
);
succeeds(
run(
root,
"set",
dependency,
"--field",
"resolution",
"--value",
"verified-existing",
"--agent",
"planner",
),
);
succeeds(run(root, "move", dependency, "discovery", "--agent", "planner"));
const staleResolution = run(
root,
"move",
dependency,
"done",
"--agent",
"planner",
);
assert.notEqual(staleResolution.status, 0);
assert.match(staleResolution.stderr, /requires resolution evidence/);
succeeds(
run(
root,
"evidence",
dependency,
"--stage",
"resolution",
"--result",
"discovery resolution verified",
"--agent",
"planner",
),
);
succeeds(run(root, "move", dependency, "done", "--agent", "planner"));
const dependent = succeeds(
run(root, "new", "--title", "Committed dependent"),
);
succeeds(run(root, "move", dependent, "triage", "--agent", "planner"));
await refine(root, "10-triage", dependent);
succeeds(
run(
root,
"set",
dependent,
"--field",
"dependencies",
"--value",
JSON.stringify([dependency]),
"--agent",
"planner",
),
);
succeeds(run(root, "move", dependent, "ready", "--agent", "planner"));
succeeds(run(root, "claim", dependent, "--agent", "developer"));
const reopen = run(root, "move", dependency, "triage", "--agent", "planner");
assert.notEqual(reopen.status, 0);
assert.match(reopen.stderr, /cannot reopen/);
assert.match(succeeds(run(root, "validate")), /^WARN:|^OK:/);
});
test("next and validation reject cancelled dependencies and incomplete card schema", async (t) => {
const root = await fixture();
t.after(() => rm(root, { recursive: true, force: true }));
const dependency = succeeds(
run(root, "new", "--title", "Satisfied dependency"),
);
succeeds(run(root, "move", dependency, "triage", "--agent", "planner"));
succeeds(
run(
root,
"evidence",
dependency,
"--stage",
"resolution",
"--result",
"existing outcome verified",
"--agent",
"planner",
),
);
succeeds(
run(
root,
"set",
dependency,
"--field",
"resolution",
"--value",
"verified-existing",
"--agent",
"planner",
),
);
succeeds(run(root, "move", dependency, "done", "--agent", "planner"));
const dependent = succeeds(run(root, "new", "--title", "Ready dependent"));
succeeds(run(root, "move", dependent, "triage", "--agent", "planner"));
await refine(root, "10-triage", dependent);
succeeds(
run(
root,
"set",
dependent,
"--field",
"dependencies",
"--value",
JSON.stringify([dependency]),
"--agent",
"planner",
),
);
succeeds(run(root, "move", dependent, "ready", "--agent", "planner"));
const dependencyPath = join(root, "columns", "80-done", `${dependency}.md`);
const validDependency = await readFile(dependencyPath, "utf8");
await writeFile(
dependencyPath,
validDependency.replace(
'"resolution": "verified-existing"',
'"resolution": "cancelled"',
),
);
assert.equal(succeeds(run(root, "next", "--json")), "null");
const unsatisfied = run(root, "validate", "--json");
assert.notEqual(unsatisfied.status, 0);
assert.match(unsatisfied.stdout, /dependency is not satisfied/);
await writeFile(dependencyPath, validDependency);
const dependentPath = join(root, "columns", "30-ready", `${dependent}.md`);
const validDependent = await readFile(dependentPath, "utf8");
await writeFile(
dependentPath,
validDependent.replace(' "spec": null,\n', ""),
);
const incompleteSchema = run(root, "validate", "--json");
assert.notEqual(incompleteSchema.status, 0);
assert.match(incompleteSchema.stdout, /missing spec/);
await writeFile(dependentPath, validDependent);
const createdAt = validDependent.match(/"createdAt": "([^"]+)"/)?.[1];
assert.ok(createdAt);
await writeFile(
dependentPath,
validDependent.replace(
/"statusEnteredAt": "[^"]+"/,
`"statusEnteredAt": "${createdAt}"`,
),
);
const mismatchedStatusTime = run(root, "validate", "--json");
assert.notEqual(mismatchedStatusTime.status, 0);
assert.match(mismatchedStatusTime.stdout, /statusEnteredAt does not match/);
await writeFile(dependentPath, validDependent);
await writeFile(
dependentPath,
validDependent.replace(
' "resolution": null\n',
' "resolution": null,\n "extra": true\n',
),
);
const unknownField = run(root, "validate", "--json");
assert.notEqual(unknownField.status, 0);
assert.match(unknownField.stdout, /unknown field extra/);
await writeFile(dependentPath, validDependent);
const eventsPath = join(root, "state", "events.jsonl");
const validEvents = await readFile(eventsPath, "utf8");
const eventLines = validEvents
.trim()
.split("\n")
.map((line) => parseJson(line, "event"));
eventLines[0].at = "July 12, 2026";
await writeFile(
eventsPath,
eventLines.map((event) => JSON.stringify(event)).join("\n") + "\n",
);
const nonCanonicalTime = run(root, "validate", "--json");
assert.notEqual(nonCanonicalTime.status, 0);
assert.match(nonCanonicalTime.stdout, /invalid event fields/);
await writeFile(eventsPath, validEvents);
const boardPath = join(root, "board.json");
const validBoard = await readFile(boardPath, "utf8");
const invalidBoardConfig = parseJson(validBoard, "board");
invalidBoardConfig.transitions.inbox = [];
await writeFile(boardPath, JSON.stringify(invalidBoardConfig));
const invalidGraph = run(root, "validate", "--json");
assert.notEqual(invalidGraph.status, 0);
assert.match(invalidGraph.stderr, /invalid transition policy for inbox/);
await writeFile(boardPath, validBoard);
const extraDirectory = join(root, "columns", "99-extra");
await mkdir(extraDirectory);
const invalidDirectories = run(root, "validate", "--json");
assert.notEqual(invalidDirectories.status, 0);
assert.match(
invalidDirectories.stdout,
/columns directory must contain exactly/,
);
await rm(extraDirectory, { recursive: true, force: true });
assert.match(succeeds(run(root, "validate")), /^OK:/);
});
test("enforces dependency, WIP, and forced-action policies", async (t) => {
const root = await fixture(1);
t.after(() => rm(root, { recursive: true, force: true }));
const dependency = succeeds(run(root, "new", "--title", "Dependency"));
const dependent = succeeds(run(root, "new", "--title", "Dependent task"));
succeeds(run(root, "move", dependent, "triage", "--agent", "planner"));
await refine(root, "10-triage", dependent);
succeeds(
run(
root,
"set",
dependent,
"--field",
"dependencies",
"--value",
JSON.stringify([dependency]),
"--agent",
"planner",
),
);
const dependencyGate = run(
root,
"move",
dependent,
"ready",
"--agent",
"planner",
);
assert.notEqual(dependencyGate.status, 0);
assert.match(dependencyGate.stderr, /dependency is not satisfied/);
const first = await createReady(root, "First WIP item");
const second = await createReady(root, "Second WIP item");
const invalidReadyUpdate = run(
root,
"set",
second,
"--field",
"dependencies",
"--value",
JSON.stringify([dependency]),
"--agent",
"planner",
);
assert.notEqual(invalidReadyUpdate.status, 0);
assert.match(invalidReadyUpdate.stderr, /dependency is not satisfied/);
const secondPath = join(root, "columns", "30-ready", `${second}.md`);
const validSecond = await readFile(secondPath, "utf8");
await writeFile(
secondPath,
validSecond.replace(
'"dependencies": []',
`"dependencies": ["${dependency}"]`,
),
);
const invalidBoard = run(root, "validate", "--json");
assert.notEqual(invalidBoard.status, 0);
assert.match(invalidBoard.stdout, /dependency is not satisfied/);
await writeFile(secondPath, validSecond);
succeeds(run(root, "claim", first, "--agent", "developer-a"));
const full = run(root, "claim", second, "--agent", "developer-b");
assert.notEqual(full.status, 0);
assert.match(full.stderr, /WIP limit 1 reached/);
const forcedWithoutReason = run(
root,
"move",
second,
"triage",
"--agent",
"planner",
"--force",
);
assert.notEqual(forcedWithoutReason.status, 0);
assert.match(forcedWithoutReason.stderr, /--force requires --reason/);
const forcedNonP0 = run(
root,
"move",
second,
"triage",
"--agent",
"planner",
"--force",
"--reason",
"expedite",
);
assert.notEqual(forcedNonP0.status, 0);
assert.match(forcedNonP0.stderr, /limited to P0/);
});