90 lines
4.4 KiB
JavaScript
90 lines
4.4 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
|
const read = (relative) => fs.readFileSync(path.join(root, relative), "utf8");
|
|
const json = (relative) => JSON.parse(read(relative));
|
|
const planPath = "docs/BLENDER_WEB_PARITY_TASK_PLAN_V2.json";
|
|
const plan = json(planPath);
|
|
const checkpoint = plan.correctiveCheckpoint;
|
|
const handoff = json(checkpoint.handoff);
|
|
const queue = read("docs/EXECUTION_QUEUE.md");
|
|
const queueTask = queue.match(/\|\s*当前任务\s*\|\s*`([^`]+)`/u)?.[1];
|
|
const parentPath = queue.match(/\|\s*parent manifest\s*\|\s*`([^`]+)`/iu)?.[1];
|
|
assert.ok(queueTask && parentPath, "legacy queue must expose current task and parent manifest");
|
|
const parent = json(parentPath);
|
|
|
|
function isCheckpointAncestor(checkpointTask) {
|
|
if (checkpointTask === queueTask) return true;
|
|
let cursor = parent;
|
|
const visited = new Set();
|
|
while (cursor?.task && !visited.has(cursor.task)) {
|
|
visited.add(cursor.task);
|
|
if (cursor.task === checkpointTask) return true;
|
|
if (!cursor.parentTask) return false;
|
|
const manifestPath = path.join(root, "tests/golden", cursor.parentTask, "manifest.json");
|
|
if (!fs.existsSync(manifestPath)) return false;
|
|
cursor = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
|
|
}
|
|
return false;
|
|
}
|
|
|
|
assert.equal(plan.schemaVersion, 1, "V2 plan schema must be 1");
|
|
assert.equal(plan.planId, "BLENDER_WEB_PARITY_V2");
|
|
assert.equal(plan.status, "ACTIVE_CAPABILITY_QUEUE");
|
|
assert.equal(plan.authority, "queue_reactivated_after_owner_review");
|
|
assert.equal(plan.executionPointer, "docs/EXECUTION_QUEUE.md");
|
|
assert.equal(checkpoint.task, "C6-003");
|
|
assert.equal(checkpoint.status, "active");
|
|
assert.equal(checkpoint.queueMutation, true);
|
|
assert.equal(checkpoint.blocker, null);
|
|
assert.ok(isCheckpointAncestor(checkpoint.legacyTask), "V2 checkpoint must be current or an ancestor of the legacy queue");
|
|
assert.ok(isCheckpointAncestor(checkpoint.legacyParentNextTask), "V2 parent checkpoint must be current or an ancestor of the legacy queue");
|
|
assert.equal(parent.nextTask, queueTask);
|
|
assert.equal(handoff.task, checkpoint.task);
|
|
assert.equal(handoff.status, "in_progress");
|
|
assert.equal(handoff.queueMutation, true);
|
|
assert.equal(handoff.nextCorrectiveTask, checkpoint.task);
|
|
|
|
const readyTasks = plan.tasks.filter((task) => task.ready === true);
|
|
assert.equal(readyTasks.length, 1, "V2 plan must have exactly one ready task");
|
|
assert.equal(readyTasks[0].id, queueTask, "V2 ready task must match the execution queue");
|
|
|
|
const taskIds = new Set();
|
|
const tasksById = new Map();
|
|
for (const task of plan.tasks) {
|
|
assert.match(task.id, /^WBV2-P[0-7]-\d{3}$/u, `${task.id} has invalid ID`);
|
|
assert.equal(taskIds.has(task.id), false, `${task.id} is duplicated`);
|
|
taskIds.add(task.id);
|
|
tasksById.set(task.id, task);
|
|
assert.ok(plan.gates.some((gate) => gate.id === task.gate), `${task.id} gate is missing`);
|
|
if (task.ready) assert.equal(task.status, "in_progress", `${task.id} ready task must be in_progress`);
|
|
assert.equal(task.queueMutationAllowed, false, `${task.id} cannot mutate queue`);
|
|
assert.ok(task.behavior && task.acceptance?.length && task.outputs?.length, `${task.id} contract is incomplete`);
|
|
for (const dependency of task.dependsOn ?? []) assert.ok(taskIds.has(dependency) || plan.tasks.some((candidate) => candidate.id === dependency), `${task.id} dependency is missing: ${dependency}`);
|
|
}
|
|
for (const wave of plan.waves) {
|
|
assert.ok(plan.gates.some((gate) => gate.id === wave.gate), `${wave.id} gate is missing`);
|
|
for (const taskId of wave.tasks) {
|
|
const task = tasksById.get(taskId);
|
|
assert.ok(task, `${wave.id} references unknown task ${taskId}`);
|
|
assert.equal(task.phase, wave.id, `${taskId} is assigned to the wrong wave`);
|
|
}
|
|
}
|
|
|
|
const visiting = new Set();
|
|
const visited = new Set();
|
|
function visit(taskId) {
|
|
if (visiting.has(taskId)) throw new Error(`dependency cycle includes ${taskId}`);
|
|
if (visited.has(taskId)) return;
|
|
visiting.add(taskId);
|
|
for (const dependency of tasksById.get(taskId).dependsOn ?? []) visit(dependency);
|
|
visiting.delete(taskId);
|
|
visited.add(taskId);
|
|
}
|
|
for (const taskId of taskIds) visit(taskId);
|
|
|
|
process.stdout.write(`web-parity-v2-plan-ok status=${plan.status} tasks=${plan.tasks.length} waves=${plan.waves.length} checkpoint=${checkpoint.task} queue=${queueTask} queueMutation=${checkpoint.queueMutation}\n`);
|