refactor: migrate role handling from tiers to roles module

- Removed the deprecated tiers.ts file and migrated all related functionality to roles/index.js.
- Updated tests and tools to reflect the new role structure, replacing references to "dev", "qa", and "architect" with "developer", "tester", and "architect".
- Adjusted workflow configurations and state management to accommodate the new role naming conventions.
- Enhanced project registration and health check tools to support dynamic role handling.
- Updated task creation, update, and completion processes to align with the new role definitions.
- Improved documentation and comments to clarify role responsibilities and usage.
This commit is contained in:
Lauren ten Hoor
2026-02-15 18:32:10 +08:00
parent 6a99752e5f
commit 0e24a68882
44 changed files with 1162 additions and 762 deletions

254
lib/projects.test.ts Normal file
View File

@@ -0,0 +1,254 @@
/**
* 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 projDir = path.join(tmpDir, "projects");
await fs.mkdir(projDir, { 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(projDir, "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 projDir = path.join(tmpDir, "projects");
await fs.mkdir(projDir, { 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(projDir, "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", 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 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");
await fs.rm(tmpDir, { recursive: true });
});
it("should return empty worker for unknown role", async () => {
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 projDir = path.join(tmpDir, "projects");
await fs.mkdir(projDir, { 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 });
});
});