import { describe, expect, test } from "bun:test"; import type { AgentRun, Conversation, ConversationRepository, Project, ProjectRepository, RunEvent, RunRepository, } from ".."; import { ProjectService } from ".."; class Projects implements ProjectRepository { values = new Map(); async list() { return [...this.values.values()]; } async getById(id: string) { return this.values.get(id) ?? null; } async create(project: Project) { this.values.set(project.id, project); } async update(project: Project) { this.values.set(project.id, project); } async delete(id: string) { this.values.delete(id); } } class Conversations implements ConversationRepository { values = new Map(); async listRecent() { return [...this.values.values()].filter((value) => !value.projectId); } async listByProject(projectId: string) { return [...this.values.values()].filter( (value) => value.projectId === projectId, ); } async getById(id: string) { return this.values.get(id) ?? null; } async create(value: Conversation) { this.values.set(value.id, value); } async appendMessage( _id: string, _message: Conversation["messages"][number], ): Promise { throw new Error("unused"); } async deleteByProject(projectId: string) { for (const [id, value] of this.values) if (value.projectId === projectId) this.values.delete(id); } } class Runs implements RunRepository { active = false; async create(_run: AgentRun) {} async update(_run: AgentRun) {} async getById(_id: string) { return null; } async findActive() { return null; } async appendEvent(_event: RunEvent) {} async listEvents(_id: string) { return []; } async hasActiveForConversations() { return this.active; } async deleteByConversations() {} } describe("ProjectService", () => { test("创建项目时固化真实目录,重命名后仍保留目录", async () => { const projects = new Projects(); const service = createService(projects, new Conversations(), new Runs()); const created = await service.create(" 我的项目 ", "/tmp/link"); expect(created).toMatchObject({ name: "我的项目", workspaceRoot: "/tmp/real", conversationCount: 0, workspaceAvailable: true, }); const renamed = await service.rename(created.id, "新名字"); expect(renamed.name).toBe("新名字"); expect(renamed.workspaceRoot).toBe("/tmp/real"); }); test("有运行中的项目任务时拒绝删除", async () => { const projects = new Projects(); const conversations = new Conversations(); const runs = new Runs(); const service = createService(projects, conversations, runs); const created = await service.create("项目", "/tmp/real"); conversations.values.set("conversation", { id: "conversation", projectId: created.id, title: "对话", messages: [], createdAt: "now", updatedAt: "now", }); runs.active = true; expect(service.delete(created.id)).rejects.toMatchObject({ code: "PROJECT_RUN_ACTIVE", }); expect(await projects.getById(created.id)).not.toBeNull(); }); }); function createService( projects: Projects, conversations: Conversations, runs: Runs, ) { return new ProjectService({ projects, conversations, runs, workspaces: { async resolveForCreation() { return "/tmp/real"; }, async isAvailable() { return true; }, }, clock: { now: () => new Date("2026-08-13T00:00:00Z") }, ids: { create: () => "project_1" }, }); }