AkurAI Build
Menu

AkurAI-Build

public

Latest 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/);
});