Files
devclaw-gitea/lib/projects.test.ts
Lauren ten Hoor a359ffed34 feat(migration): implement workspace layout migration and testing
- Added `migrate-layout.ts` to handle migration from old workspace layouts to the new `devclaw/` structure.
- Introduced `migrate-layout.test.ts` for comprehensive tests covering various migration scenarios.
- Updated `workspace.ts` to ensure default files are created post-migration, including `workflow.yaml` and role-specific prompts.
- Refactored role instruction handling to accommodate new directory structure.
- Enhanced project registration to scaffold prompt files in the new `devclaw/projects/<project>/prompts/` directory.
- Adjusted setup tool descriptions and logic to reflect changes in file structure.
- Updated templates to align with the new workflow configuration and role instructions.
2026-02-15 20:19:09 +08:00

249 lines
9.0 KiB
TypeScript

/**
* Tests for projects.ts — worker state, migration, and accessors.
* Run with: npx tsx --test lib/projects.test.ts
*/
import { describe, it } from "node:test";
import assert from "node:assert";
import fs from "node:fs/promises";
import path from "node:path";
import os from "node:os";
import { readProjects, getWorker, emptyWorkerState, writeProjects, type ProjectsData } from "./projects.js";
describe("readProjects migration", () => {
it("should migrate old format (dev/qa/architect fields) to workers map", async () => {
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "devclaw-proj-"));
const projDir = path.join(tmpDir, "projects");
await fs.mkdir(projDir, { recursive: true });
// Old format: hardcoded dev/qa/architect fields
const oldFormat = {
projects: {
"group-1": {
name: "test-project",
repo: "~/git/test",
groupName: "Test",
deployUrl: "",
baseBranch: "main",
deployBranch: "main",
dev: { active: true, issueId: "42", startTime: null, level: "mid", sessions: { mid: "key-1" } },
qa: { active: false, issueId: null, startTime: null, level: null, sessions: {} },
architect: { active: false, issueId: null, startTime: null, level: null, sessions: {} },
},
},
};
await fs.writeFile(path.join(projDir, "projects.json"), JSON.stringify(oldFormat), "utf-8");
const data = await readProjects(tmpDir);
const project = data.projects["group-1"];
// Should have workers map with migrated role keys
assert.ok(project.workers, "should have workers map");
assert.ok(project.workers.developer, "should have developer worker (migrated from dev)");
assert.ok(project.workers.tester, "should have tester worker (migrated from qa)");
assert.ok(project.workers.architect, "should have architect worker");
// Developer worker should be active with migrated level
assert.strictEqual(project.workers.developer.active, true);
assert.strictEqual(project.workers.developer.issueId, "42");
assert.strictEqual(project.workers.developer.level, "medior");
// Old fields should not exist on the object
assert.strictEqual((project as any).dev, undefined);
assert.strictEqual((project as any).qa, undefined);
assert.strictEqual((project as any).architect, undefined);
await fs.rm(tmpDir, { recursive: true });
});
it("should migrate old level names in old format", async () => {
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "devclaw-proj-"));
const projDir = path.join(tmpDir, "projects");
await fs.mkdir(projDir, { recursive: true });
const oldFormat = {
projects: {
"group-1": {
name: "legacy",
repo: "~/git/legacy",
groupName: "Legacy",
deployUrl: "",
baseBranch: "main",
deployBranch: "main",
dev: { active: false, issueId: null, startTime: null, level: "medior", sessions: { medior: "key-1" } },
qa: { active: false, issueId: null, startTime: null, level: "reviewer", sessions: { reviewer: "key-2" } },
architect: { active: false, issueId: null, startTime: null, level: "opus", sessions: { opus: "key-3" } },
},
},
};
await fs.writeFile(path.join(projDir, "projects.json"), JSON.stringify(oldFormat), "utf-8");
const data = await readProjects(tmpDir);
const project = data.projects["group-1"];
// Level names should be migrated (dev→developer, qa→tester, medior→medior, reviewer→medior)
assert.strictEqual(project.workers.developer.level, "medior");
assert.strictEqual(project.workers.tester.level, "medior");
assert.strictEqual(project.workers.architect.level, "senior");
// Session keys should be migrated
assert.strictEqual(project.workers.developer.sessions.medior, "key-1");
assert.strictEqual(project.workers.tester.sessions.medior, "key-2");
assert.strictEqual(project.workers.architect.sessions.senior, "key-3");
await fs.rm(tmpDir, { recursive: true });
});
it("should read new format (workers map) correctly", async () => {
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "devclaw-proj-"));
const dataDir = path.join(tmpDir, "devclaw");
await fs.mkdir(dataDir, { recursive: true });
const newFormat = {
projects: {
"group-1": {
name: "modern",
repo: "~/git/modern",
groupName: "Modern",
deployUrl: "",
baseBranch: "main",
deployBranch: "main",
workers: {
developer: { active: true, issueId: "10", startTime: null, level: "senior", sessions: { senior: "key-s" } },
tester: { active: false, issueId: null, startTime: null, level: null, sessions: {} },
},
},
},
};
await fs.writeFile(path.join(dataDir, "projects.json"), JSON.stringify(newFormat), "utf-8");
const data = await readProjects(tmpDir);
const project = data.projects["group-1"];
assert.ok(project.workers.developer);
assert.strictEqual(project.workers.developer.active, true);
assert.strictEqual(project.workers.developer.level, "senior");
assert.ok(project.workers.tester);
await fs.rm(tmpDir, { recursive: true });
});
it("should migrate old worker keys in new format", async () => {
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "devclaw-proj-"));
const dataDir = path.join(tmpDir, "devclaw");
await fs.mkdir(dataDir, { recursive: true });
// Workers map but with old role keys
const mixedFormat = {
projects: {
"group-1": {
name: "mixed",
repo: "~/git/mixed",
groupName: "Mixed",
deployUrl: "",
baseBranch: "main",
deployBranch: "main",
workers: {
dev: { active: true, issueId: "10", startTime: null, level: "mid", sessions: { mid: "key-m" } },
qa: { active: false, issueId: null, startTime: null, level: null, sessions: {} },
},
},
},
};
await fs.writeFile(path.join(dataDir, "projects.json"), JSON.stringify(mixedFormat), "utf-8");
const data = await readProjects(tmpDir);
const project = data.projects["group-1"];
// Old keys should be migrated
assert.ok(project.workers.developer, "dev should be migrated to developer");
assert.ok(project.workers.tester, "qa should be migrated to tester");
assert.strictEqual(project.workers.developer.level, "medior");
assert.strictEqual(project.workers.developer.sessions.medior, "key-m");
await fs.rm(tmpDir, { recursive: true });
});
});
describe("getWorker", () => {
it("should return worker from workers map", () => {
const data: ProjectsData = {
projects: {
"g1": {
name: "test",
repo: "~/git/test",
groupName: "Test",
deployUrl: "",
baseBranch: "main",
deployBranch: "main",
workers: {
developer: { active: true, issueId: "5", startTime: null, level: "medior", sessions: {} },
},
},
},
};
const worker = getWorker(data.projects["g1"], "developer");
assert.strictEqual(worker.active, true);
assert.strictEqual(worker.issueId, "5");
});
it("should return empty worker for unknown role", () => {
const data: ProjectsData = {
projects: {
"g1": {
name: "test",
repo: "~/git/test",
groupName: "Test",
deployUrl: "",
baseBranch: "main",
deployBranch: "main",
workers: {},
},
},
};
const worker = getWorker(data.projects["g1"], "nonexistent");
assert.strictEqual(worker.active, false);
assert.strictEqual(worker.issueId, null);
});
});
describe("writeProjects round-trip", () => {
it("should preserve workers map through write/read cycle", async () => {
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "devclaw-proj-"));
const dataDir = path.join(tmpDir, "devclaw");
await fs.mkdir(dataDir, { recursive: true });
const data: ProjectsData = {
projects: {
"g1": {
name: "roundtrip",
repo: "~/git/rt",
groupName: "RT",
deployUrl: "",
baseBranch: "main",
deployBranch: "main",
workers: {
developer: emptyWorkerState(["junior", "medior", "senior"]),
tester: emptyWorkerState(["junior", "medior", "senior"]),
architect: emptyWorkerState(["junior", "senior"]),
},
},
},
};
await writeProjects(tmpDir, data);
const loaded = await readProjects(tmpDir);
const project = loaded.projects["g1"];
assert.ok(project.workers.developer);
assert.ok(project.workers.tester);
assert.ok(project.workers.architect);
assert.strictEqual(project.workers.developer.sessions.junior, null);
assert.strictEqual(project.workers.developer.sessions.medior, null);
assert.strictEqual(project.workers.developer.sessions.senior, null);
await fs.rm(tmpDir, { recursive: true });
});
});