134 lines
3.6 KiB
TypeScript
134 lines
3.6 KiB
TypeScript
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<string, Project>();
|
|
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<string, Conversation>();
|
|
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<Conversation> {
|
|
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 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" },
|
|
});
|
|
}
|