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
1 change: 1 addition & 0 deletions scripts/run-tests.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -870,6 +870,7 @@ export const SUITES = {
"src/lib/harness-adapters.test.ts",
"src/lib/copilot-stream.test.ts",
"src/app/api/board/enrich-steps/route.test.ts",
"src/app/api/board/route.test.ts",
"src/app/api/board/[id]/chat/route.test.ts",
"src/app/api/sessions/route.test.ts",
"src/app/api/chat/send/harness-routing.test.ts",
Expand Down
40 changes: 40 additions & 0 deletions src/app/api/board/route.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";

const source = readFileSync(fileURLToPath(new URL("./route.ts", import.meta.url)), "utf8");

assert.match(source, /STATUSES/, "board create must import STATUSES for validation");
assert.match(source, /PRIORITIES/, "board create must import PRIORITIES for validation");
assert.match(
source,
/const STATUS_VALUES = new Set<CardStatus>\(STATUSES\)/,
"board create must build a CardStatus allowlist from STATUSES",
);
assert.match(
source,
/const PRIORITY_VALUES = new Set<CardPriority>\(PRIORITIES\)/,
"board create must build a CardPriority allowlist from PRIORITIES",
);
assert.match(
source,
/body\.status !== undefined && !STATUS_VALUES\.has\(body\.status\)/,
"POST must reject status values outside the board enum",
);
assert.match(
source,
/body\.priority !== undefined && !PRIORITY_VALUES\.has\(body\.priority\)/,
"POST must reject priority values outside the board enum",
);
assert.match(
source,
/error: "invalid status"/,
"invalid status responses must use a stable error string",
);
assert.match(
source,
/error: "invalid priority"/,
"invalid priority responses must use a stable error string",
);

console.log("board/route.test.ts: ok");
13 changes: 13 additions & 0 deletions src/app/api/board/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { NextResponse } from "next/server";
import {
createCard,
loadBoard,
PRIORITIES,
STATUSES,
type CardPriority,
type CardStatus,
} from "@/lib/cave-board";
Expand All @@ -11,6 +13,9 @@ import { trustedProjectCwd } from "@/lib/cave-projects";

export const dynamic = "force-dynamic";

const STATUS_VALUES = new Set<CardStatus>(STATUSES);
const PRIORITY_VALUES = new Set<CardPriority>(PRIORITIES);

export async function GET() {
const board = await loadBoard();
return NextResponse.json({ ok: true, cards: board.cards });
Expand Down Expand Up @@ -44,6 +49,14 @@ export async function POST(req: Request) {
if (!body.title || !body.title.trim()) {
return NextResponse.json({ ok: false, error: "title required" }, { status: 400 });
}
// Reject unknown column/priority values so hand-edited or agent payloads cannot
// plant strings that break Board filters and lifecycle inference.
if (body.status !== undefined && !STATUS_VALUES.has(body.status)) {
return NextResponse.json({ ok: false, error: "invalid status" }, { status: 400 });
}
if (body.priority !== undefined && !PRIORITY_VALUES.has(body.priority)) {
return NextResponse.json({ ok: false, error: "invalid priority" }, { status: 400 });
}
// A card assigned to a project derives its cwd from that project server-side —
// never from the client body, which could contradict the project (cave-pw83).
let cwd = body.cwd ?? null;
Expand Down
Loading