scheduler-gateway/test/mock-board-server.js
Liuxinyu176 bbb364cd69 feat: DSH 调度网关(互联网关)v6.0.0 — 四模型合并版 R1+R2+R3
- 零第三方依赖,Node >=20 原生 ESM
- 团队式编排引擎:拆解/路由/执行/审查/合并全真实 LLM
- 阶段心跳、单一权威清单守卫、all-keys-failed 如实上报
- H4 会话视图/amend/watchdog 有界重试/产物区 artifacts.json
- H5 零依赖三栏控制台
2026-10-09 23:20:27 +08:00

123 lines
3.9 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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 退出)`);
}