Files
pikasTech-HWLAB/internal/project-management/server.test.ts
T

313 lines
14 KiB
TypeScript

import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { expect, test } from "bun:test";
import { executeHwpodNodeOpsPlan } from "../../tools/src/hwpod-node-lib.ts";
import { createProjectManagementApp } from "./server.ts";
import { createProjectManagementStore } from "./store.ts";
test("project management app exposes MDTODO read model and Workbench link writes", async () => {
const root = await mkdtemp(join(tmpdir(), "hwlab-project-management-"));
try {
await writeFile(join(root, "MDTODO.md"), "# Demo\n\n## R1 Launch project nav\n\n## R2 Wire read model [completed]\n", "utf8");
const app = createProjectManagementApp({
env: {},
store: createProjectManagementStore({ kind: "memory" }),
sourceRoot: root,
sourceId: "test-mdtodo",
projectId: "project_test"
});
const ready = await app.fetch(new Request("http://service/health/ready"));
expect(ready.status).toBe(200);
const navigation = await json(app, "/v1/project-management/navigation");
expect(navigation.navigation.id).toBe("project-management");
const sources = await json(app, "/v1/project-management/mdtodo/sources");
expect(sources.sources[0].sourceId).toBe("test-mdtodo");
const files = await json(app, "/v1/project-management/mdtodo/files?sourceId=test-mdtodo");
expect(files.files[0].taskCount).toBe(2);
const tasks = await json(app, "/v1/project-management/mdtodo/tasks?sourceId=test-mdtodo");
expect(tasks.tasks.map((task) => task.taskId)).toEqual(["R1", "R2"]);
expect(tasks.tasks.map((task) => task.status)).toEqual(["open", "done"]);
expect(tasks.tasks.every((task) => task.projectId === "project_test")).toBe(true);
const links = await json(app, "/v1/project-management/workbench-links?projectId=project_test");
expect(links.links).toEqual([]);
const linkResponse = await app.fetch(new Request("http://service/v1/project-management/workbench-links", {
method: "POST",
headers: { "content-type": "application/json", "x-hwlab-actor-id": "usr_test" },
body: JSON.stringify({ projectId: "project_test", taskRef: tasks.tasks[0].taskRef, sessionId: "ses_project_test", role: "launch", link: { workbenchUrl: "/workbench/sessions/ses_project_test", launchContext: { projectId: "project_test", taskRef: tasks.tasks[0].taskRef, title: tasks.tasks[0].title } } })
}));
expect(linkResponse.status).toBe(201);
const createdLink = await linkResponse.json();
expect(createdLink.link.sessionId).toBe("ses_project_test");
const linkedTasks = await json(app, "/v1/project-management/mdtodo/tasks?sourceId=test-mdtodo");
expect(linkedTasks.tasks[0].linkCount).toBe(1);
const taskLinks = await json(app, `/v1/project-management/workbench-links?taskRef=${encodeURIComponent(tasks.tasks[0].taskRef)}`);
expect(taskLinks.links[0].sessionId).toBe("ses_project_test");
const rejected = await app.fetch(new Request("http://service/v1/project-management/mdtodo/tasks", { method: "PUT" }));
expect(rejected.status).toBe(405);
await app.close();
} finally {
await rm(root, { recursive: true, force: true });
}
});
test("project management app configures HWPOD source and writes MDTODO content", async () => {
const root = await mkdtemp(join(tmpdir(), "hwlab-project-management-hwpod-"));
const defaultRoot = join(root, "default");
const hwpodRoot = join(root, "hwpod-workspace");
const mdtodoDir = join(hwpodRoot, "docs", "MDTODO");
const samplePath = join(mdtodoDir, "hwlab-v03-mdtodo-web-sample.md");
const store = createProjectManagementStore({ kind: "memory" });
try {
await mkdir(defaultRoot, { recursive: true });
await mkdir(mdtodoDir, { recursive: true });
await writeFile(join(defaultRoot, "MDTODO.md"), "# Default\n\n## R1 Default\n", "utf8");
await writeFile(samplePath, "# Sample\n\n## R1 Root\n\n### R1.1 Child [in_progress]\n", "utf8");
const app = createProjectManagementApp({
env: {},
store,
sourceRoot: defaultRoot,
sourceId: "default-mdtodo",
projectId: "project_test",
hwpodNodeOpsHandler: (plan) => executeHwpodNodeOpsPlan(plan)
});
const createResponse = await app.fetch(jsonRequest("/v1/project-management/mdtodo/sources", {
method: "POST",
body: {
sourceId: "hwpod-mdtodo",
sourceKind: "hwpod-workspace",
displayName: "D601 F103 MDTODO",
projectId: "project_test",
hwpodId: "d601-f103-v2",
nodeId: "node-d601-f103-v2",
workspaceRootRef: hwpodRoot,
mdtodoRootRef: "docs/MDTODO/"
}
}));
expect(createResponse.status).toBe(201);
const created = await createResponse.json();
expect(created.source.sourceKind).toBe("hwpod-workspace");
const probe = await post(app, "/v1/project-management/mdtodo/sources/hwpod-mdtodo/probe", {});
expect(probe.probe.status).toBe("ok");
const reindex = await post(app, "/v1/project-management/mdtodo/sources/hwpod-mdtodo/reindex", {});
expect(reindex.projection.documentCount).toBe(1);
expect(reindex.projection.taskCount).toBe(2);
const files = await json(app, "/v1/project-management/mdtodo/files?sourceId=hwpod-mdtodo");
expect(files.files[0].relativePath).toBe("hwlab-v03-mdtodo-web-sample.md");
const fileRef = files.files[0].fileRef;
const content = await json(app, `/v1/project-management/mdtodo/files/${fileRef}/content?sourceId=hwpod-mdtodo`);
expect(content.file.content).toContain("## R1 Root");
const nextContent = "# Sample\n\n## R1 Edited root\n\n### R1.1 Child [completed]\n";
const write = await app.fetch(jsonRequest(`/v1/project-management/mdtodo/files/${fileRef}/content?sourceId=hwpod-mdtodo`, {
method: "PATCH",
body: { sourceId: "hwpod-mdtodo", expectedFingerprint: content.file.fingerprint, content: nextContent }
}));
expect(write.status).toBe(200);
const written = await write.json();
expect(written.file.fingerprint).not.toBe(content.file.fingerprint);
expect(await readFile(samplePath, "utf8")).toBe(nextContent);
const tasks = await json(app, "/v1/project-management/mdtodo/tasks?sourceId=hwpod-mdtodo");
expect(tasks.tasks.map((task) => [task.taskId, task.title, task.status])).toEqual([
["R1", "Edited root", "open"],
["R1.1", "Child", "done"]
]);
const rootTaskRef = `mdtodo:hwpod-mdtodo:${fileRef}:R1`;
const updateTask = await app.fetch(jsonRequest(`/v1/project-management/mdtodo/tasks/${encodeURIComponent(rootTaskRef)}`, {
method: "PATCH",
body: { expectedFingerprint: written.file.fingerprint, title: "Root from task API", status: "in_progress", body: "Task API body." }
}));
expect(updateTask.status).toBe(200);
const updatedTask = await updateTask.json();
expect(updatedTask.task.title).toBe("Root from task API");
expect(updatedTask.task.status).toBe("in_progress");
expect(await readFile(samplePath, "utf8")).toContain("## R1 Root from task API [in_progress]\n\nTask API body.");
const addSubtask = await app.fetch(jsonRequest("/v1/project-management/mdtodo/tasks", {
method: "POST",
body: { parentTaskRef: rootTaskRef, expectedFingerprint: updatedTask.file.fingerprint, title: "Added child", body: "Child body." }
}));
expect(addSubtask.status).toBe(201);
const added = await addSubtask.json();
expect(added.changedTaskId).toBe("R1.2");
expect(added.task.taskRef).toBe(`mdtodo:hwpod-mdtodo:${fileRef}:R1.2`);
const continueTask = await app.fetch(jsonRequest("/v1/project-management/mdtodo/tasks", {
method: "POST",
body: { afterTaskRef: `mdtodo:hwpod-mdtodo:${fileRef}:R1.1`, expectedFingerprint: added.file.fingerprint, title: "Continued child" }
}));
expect(continueTask.status).toBe(201);
const continued = await continueTask.json();
expect(continued.changedTaskId).toBe("R1.3");
const deleteTask = await app.fetch(jsonRequest(`/v1/project-management/mdtodo/tasks/${encodeURIComponent(added.task.taskRef)}`, {
method: "DELETE",
body: { expectedFingerprint: continued.file.fingerprint }
}));
expect(deleteTask.status).toBe(200);
const deleted = await deleteTask.json();
expect(deleted.affectedTaskIds).toEqual(["R1.2"]);
expect(await readFile(samplePath, "utf8")).not.toContain("R1.2 Added child");
const conflict = await app.fetch(jsonRequest(`/v1/project-management/mdtodo/files/${fileRef}/content?sourceId=hwpod-mdtodo`, {
method: "PATCH",
body: { sourceId: "hwpod-mdtodo", expectedFingerprint: content.file.fingerprint, content: nextContent }
}));
expect(conflict.status).toBe(409);
const rejected = await app.fetch(jsonRequest("/v1/project-management/mdtodo/sources", {
method: "POST",
body: { sourceKind: "hwpod-workspace", hwpodId: "d601-f103-v2", nodeId: "node-d601-f103-v2", workspaceRootRef: hwpodRoot, mdtodoRootRef: "../secret" }
}));
expect(rejected.status).toBe(400);
expect((await store.listAuditEvents()).some((event) => event.action === "mdtodo.file.write")).toBe(true);
await app.close();
} finally {
await rm(root, { recursive: true, force: true });
}
});
test("project management app bootstraps HWPOD sources from a mounted JSON file", async () => {
const root = await mkdtemp(join(tmpdir(), "hwlab-project-management-bootstrap-"));
const defaultRoot = join(root, "default");
const hwpodRoot = join(root, "hwpod-workspace");
const mdtodoDir = join(hwpodRoot, "docs", "MDTODO");
const configPath = join(root, "sources.json");
try {
await mkdir(defaultRoot, { recursive: true });
await mkdir(mdtodoDir, { recursive: true });
await writeFile(join(defaultRoot, "MDTODO.md"), "# Default\n\n## R1 Default\n", "utf8");
await writeFile(join(mdtodoDir, "bootstrap.md"), "# Bootstrap\n\n## R1 From mounted source\n", "utf8");
await writeFile(configPath, JSON.stringify({
sources: [{
sourceId: "constart-71freq-mdtodo",
sourceKind: "hwpod-workspace",
displayName: "71-FREQ MDTODO",
projectId: "project_constart_71freq",
hwpodId: "constart-71freq-c",
nodeId: "node-d601-f103-v2",
workspaceRootRef: hwpodRoot,
mdtodoRootRef: "docs/MDTODO"
}]
}), "utf8");
const app = createProjectManagementApp({
env: { HWLAB_PROJECT_MANAGEMENT_BOOTSTRAP_SOURCES_PATH: configPath },
store: createProjectManagementStore({ kind: "memory" }),
sourceRoot: defaultRoot,
sourceId: "default-mdtodo",
projectId: "project_test",
hwpodNodeOpsHandler: (plan) => executeHwpodNodeOpsPlan(plan)
});
const ready = await app.fetch(new Request("http://service/health/ready"));
expect(ready.status).toBe(200);
const sources = await json(app, "/v1/project-management/mdtodo/sources");
expect(sources.sources.map((source) => source.sourceId).sort()).toEqual(["constart-71freq-mdtodo", "default-mdtodo"]);
const files = await json(app, "/v1/project-management/mdtodo/files?sourceId=constart-71freq-mdtodo");
expect(files.files[0].relativePath).toBe("bootstrap.md");
await app.close();
} finally {
await rm(root, { recursive: true, force: true });
}
});
test("project management HWPOD source adapter authenticates node-ops requests from configured env", async () => {
const root = await mkdtemp(join(tmpdir(), "hwlab-project-management-hwpod-auth-"));
const defaultRoot = join(root, "default");
const seen = [];
try {
await mkdir(defaultRoot, { recursive: true });
await writeFile(join(defaultRoot, "MDTODO.md"), "# Default\n\n## R1 Default\n", "utf8");
const app = createProjectManagementApp({
env: {
HWLAB_PROJECT_MANAGEMENT_HWPOD_NODE_OPS_URL: "http://cloud-api.test/v1/hwpod-node-ops",
HWLAB_PROJECT_MANAGEMENT_HWPOD_NODE_OPS_API_KEY: "hwl_live_project_management_test.redacted"
},
store: createProjectManagementStore({ kind: "memory" }),
sourceRoot: defaultRoot,
sourceId: "default-mdtodo",
projectId: "project_test",
fetchImpl: async (url, init) => {
const headers = new Headers(init.headers);
seen.push({
url: String(url),
authorization: headers.get("authorization"),
sourceServiceId: headers.get("x-source-service-id")
});
return new Response(JSON.stringify({
ok: true,
status: "completed",
results: [{ opId: "op_01", op: "workspace.ls", ok: true, status: "completed", output: { entries: [] } }]
}), { status: 200, headers: { "content-type": "application/json" } });
}
});
const createResponse = await app.fetch(jsonRequest("/v1/project-management/mdtodo/sources", {
method: "POST",
body: {
sourceId: "hwpod-mdtodo",
sourceKind: "hwpod-workspace",
displayName: "D601 F103 MDTODO",
projectId: "project_test",
hwpodId: "d601-f103-v2",
nodeId: "node-d601-f103-v2",
workspaceRootRef: "F:/Work/ConStart",
mdtodoRootRef: "docs/MDTODO/"
}
}));
expect(createResponse.status).toBe(201);
const probe = await post(app, "/v1/project-management/mdtodo/sources/hwpod-mdtodo/probe", {});
expect(probe.probe.status).toBe("ok");
expect(seen[0]).toEqual({
url: "http://cloud-api.test/v1/hwpod-node-ops",
authorization: "Bearer hwl_live_project_management_test.redacted",
sourceServiceId: "hwlab-project-management"
});
await app.close();
} finally {
await rm(root, { recursive: true, force: true });
}
});
async function json(app, path) {
const response = await app.fetch(new Request(`http://service${path}`, { headers: { "x-hwlab-actor-id": "usr_test" } }));
expect(response.status).toBe(200);
return response.json();
}
async function post(app, path, body) {
const response = await app.fetch(jsonRequest(path, { method: "POST", body }));
expect(response.status).toBe(200);
return response.json();
}
function jsonRequest(path, options = {}) {
return new Request(`http://service${path}`, {
method: options.method ?? "GET",
headers: { "content-type": "application/json", "x-hwlab-actor-id": "usr_test" },
body: options.body === undefined ? undefined : JSON.stringify(options.body)
});
}