forked from bai/curriculum-project-hub
e60aa43731
三层 integration,对齐 unit 层: - trigger.test.ts: @bot→run→lock→release→card 全链路;权限拒绝(READ 不能 triggerAgent ADR-0004);竞态 busy(ADR-0002);未绑定 chat 忽略 (ADR-0001);无 @bot 忽略 —— 5 tests - lock.test.ts: acquire/release 回环;排他性(同 project 二次 acquire 抛); WellFormed(终止 run 持锁即抛);release 幂等;isTerminal 对齐 spec —— 5 tests - cph.test.ts: 真 cph binary 跑 TH-141 examples 的 check(exit 0)+ build student PDF(exit 0)+ 空目录 check(非 0)—— 3 tests helpers: resetDb(truncate CASCADE)、mockFeishuRuntime(记录 sentTexts/ sentCards)、MockProvider(fixed stop 响应)、seedProject(project→user→grant) prisma migrate dev init 生成初始 migration 并应用到 cph_hub_test 库。 vitest config 改 fileParallelism=false(integration 共享一 DB,串行避免 truncate/insert 竞态)。 vitest run 35/35(22 unit + 13 integration)通过;tsc rc=0。
86 lines
3.5 KiB
TypeScript
86 lines
3.5 KiB
TypeScript
import { describe, it, expect, beforeEach, afterAll } from "vitest";
|
|
import { prisma, resetDb } from "./helpers.js";
|
|
import { acquireLock, releaseLock, currentLockRunId, isTerminal } from "../../src/lock.js";
|
|
|
|
describe("ProjectAgentLock WellFormed (integration, ADR-0002)", () => {
|
|
beforeEach(async () => {
|
|
await resetDb();
|
|
});
|
|
|
|
it("acquireLock + currentLockRunId round-trip", async () => {
|
|
const project = await prisma.project.create({
|
|
data: { id: "p-lock-1", name: "Test", workspaceDir: "/tmp/x" },
|
|
});
|
|
const run = await prisma.agentRun.create({
|
|
data: { projectId: project.id, entrypoint: "FEISHU", status: "ACTIVE", prompt: "x", model: "m", provider: "mock", metadata: {} },
|
|
});
|
|
|
|
await acquireLock(prisma, project.id, run.id, null);
|
|
const holder = await currentLockRunId(prisma, project.id);
|
|
expect(holder).toBe(run.id);
|
|
|
|
await releaseLock(prisma, run.id);
|
|
const after = await currentLockRunId(prisma, project.id);
|
|
expect(after).toBeNull();
|
|
});
|
|
|
|
it("acquireLock fails when project already locked (exclusivity)", async () => {
|
|
const project = await prisma.project.create({
|
|
data: { id: "p-lock-2", name: "Test", workspaceDir: "/tmp/x" },
|
|
});
|
|
const run1 = await prisma.agentRun.create({
|
|
data: { projectId: project.id, entrypoint: "FEISHU", status: "ACTIVE", prompt: "x", model: "m", provider: "mock", metadata: {} },
|
|
});
|
|
const run2 = await prisma.agentRun.create({
|
|
data: { projectId: project.id, entrypoint: "FEISHU", status: "ACTIVE", prompt: "y", model: "m", provider: "mock", metadata: {} },
|
|
});
|
|
|
|
await acquireLock(prisma, project.id, run1.id, null);
|
|
await expect(acquireLock(prisma, project.id, run2.id, null)).rejects.toThrow();
|
|
});
|
|
|
|
it("currentLockRunId throws when a terminal run holds the lock (WellFormed)", async () => {
|
|
const project = await prisma.project.create({
|
|
data: { id: "p-lock-3", name: "Test", workspaceDir: "/tmp/x" },
|
|
});
|
|
// Create a run that is already COMPLETED (terminal), then manually insert a lock.
|
|
const run = await prisma.agentRun.create({
|
|
data: { projectId: project.id, entrypoint: "FEISHU", status: "COMPLETED", prompt: "x", model: "m", provider: "mock", metadata: {}, finishedAt: new Date() },
|
|
});
|
|
await prisma.projectAgentLock.create({
|
|
data: { projectId: project.id, runId: run.id },
|
|
});
|
|
|
|
// WellFormed invariant: reading a lock held by a terminal run must throw.
|
|
await expect(currentLockRunId(prisma, project.id)).rejects.toThrow(/lock invariant violated/);
|
|
});
|
|
|
|
it("releaseLock is idempotent", async () => {
|
|
const project = await prisma.project.create({
|
|
data: { id: "p-lock-4", name: "Test", workspaceDir: "/tmp/x" },
|
|
});
|
|
const run = await prisma.agentRun.create({
|
|
data: { projectId: project.id, entrypoint: "FEISHU", status: "ACTIVE", prompt: "x", model: "m", provider: "mock", metadata: {} },
|
|
});
|
|
|
|
await acquireLock(prisma, project.id, run.id, null);
|
|
await releaseLock(prisma, run.id);
|
|
// Second release is a no-op.
|
|
await releaseLock(prisma, run.id);
|
|
expect(await currentLockRunId(prisma, project.id)).toBeNull();
|
|
});
|
|
|
|
it("isTerminal matches spec RunState.Terminal", () => {
|
|
expect(isTerminal("ACTIVE")).toBe(false);
|
|
expect(isTerminal("WAITING_FOR_USER")).toBe(false);
|
|
expect(isTerminal("COMPLETED")).toBe(true);
|
|
expect(isTerminal("FAILED")).toBe(true);
|
|
expect(isTerminal("TIMED_OUT")).toBe(true);
|
|
expect(isTerminal("CANCELED")).toBe(true);
|
|
});
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await prisma.$disconnect();
|
|
});
|