/** * mock 任务看板服务器:内存实现,语义严格对齐真实 Host v3 API: * GET 全量文档;PUT 体内 revision 必须匹配(同时认 If-Match 头), * 冲突 409 并回带当前文档;成功 revision+1 返回新文档。 * 可强制前 N 次 PUT 返回 409(forceConflicts),用于验证客户端重试。 * 零依赖,仅供测试/演练。 */ import { createServer } from "node:http"; import { randomUUID } from "node:crypto"; export function makeTask(overrides = {}) { const now = Date.now(); return { id: overrides.id || `task-${now}-${Math.random().toString(36).slice(2, 7)}`, title: "示例任务", description: "", prompt: "在工作区创建 hello.txt", status: "todo", createdAt: now, updatedAt: now, executions: [], ...overrides, }; } function initialState(seedTasks = []) { return { schemaVersion: 3, revision: 1, updatedAt: Date.now(), projects: [], tasks: seedTasks, evidences: [], }; } export function startMockBoard({ port = 0, seedTasks = [], forceConflicts = 0 } = {}) { let state = initialState(seedTasks); let forcedLeft = forceConflicts; const conflictCount = { value: 0 }; const server = createServer((req, res) => { if (req.url.endsWith("/events")) { res.writeHead(200, { "content-type": "text/event-stream" }); res.write(`data: ${JSON.stringify({ type: "ready", revision: state.revision })}\n\n`); // 200ms 后主动断开,用于验证客户端 SSE 自动重连 const t = setTimeout(() => res.end(), 200); req.on("close", () => { clearTimeout(t); res.end(); }); return; } if (req.method === "GET") { res.writeHead(200, { "content-type": "application/json" }); res.end(JSON.stringify(state)); return; } if (req.method === "PUT") { const chunks = []; req.on("data", (c) => chunks.push(c)); req.on("end", () => { let body; try { body = JSON.parse(Buffer.concat(chunks).toString("utf8")); } catch { res.writeHead(400).end(); return; } const forced = forcedLeft > 0; if (forced) forcedLeft -= 1; if (forced || body.revision !== state.revision) { conflictCount.value += 1; res.writeHead(409, { "content-type": "application/json" }); res.end( JSON.stringify({ ok: false, code: "task-board-revision-conflict", document: state }), ); return; } state = { schemaVersion: 3, revision: state.revision + 1, updatedAt: Date.now(), projects: body.projects ?? [], tasks: body.tasks ?? [], evidences: body.evidences ?? [], }; res.writeHead(200, { "content-type": "application/json" }); res.end(JSON.stringify(state)); }); return; } res.writeHead(405).end(); }); return new Promise((resolve) => { server.listen(port, "127.0.0.1", () => { const addr = server.address(); resolve({ url: `http://127.0.0.1:${addr.port}/api/dsh-task-board/v3`, port: addr.port, conflictCount, getState: () => structuredClone(state), close: () => new Promise((r) => server.close(r)), }); }); }); } /** 可直接运行:node test/mock-board-server.js [port] */ if (import.meta.url === `file://${process.argv[1].replace(/\\/g, "/")}`) { const port = Number(process.argv[2]) || 32999; const seed = [ makeTask({ id: "demo-todo", title: "演示待办", status: "todo" }), makeTask({ id: "demo-backlog", title: "演示 backlog", status: "backlog" }), ]; const mock = await startMockBoard({ port, seedTasks: seed }); console.log(`mock board: ${mock.url}(Ctrl+C 退出)`); }