// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0 // Simulated ports prove module boundaries, not live service/database acceptance. import { actorRef, projectRef } from "@conusai-kit/business" import type { ProjectScope } from "@conusai-kit/core" import { describe, expect, it, vi } from "vitest" import type { ActivitiesService, TaskRecord } from "../core/ports.js" import { addComment, changeTask, createTask, listComments, listTasks, ownedTask, updateTask } from "../core/tasks.js" import { en } from "../i18n/en.js" import { lt } from "../i18n/lt.js" const bindingId = "01900000-0000-7000-8000-000000000001" const projectId = "01900000-0000-7000-8000-000000000002" const taskId = "act_01900000-0000-7000-8000-000000000003" const userId = "01900000-0000-7000-8000-000000000004" const requestId = "01900000-0000-7000-8000-000000000005" const scope: ProjectScope = { level: "project", orgId: bindingId, workspaceId: bindingId, projectId, actor: { userId: userId as never }, } function fixture() { const task: TaskRecord = { taskId, subject: "Synthetic task", body: "Fixture details", revision: "1", status: "open", targets: [projectRef(bindingId, projectId)], createdAt: "2026-11-03T00:00:00Z", } const outcomes = new Map() let commits = 0 const service: ActivitiesService = { bindingId, get: vi.fn(async () => ({ ...task })), list: vi.fn(async () => ({ rows: [{ ...task }] })), comments: vi.fn(async () => ({ rows: [] })), timeline: vi.fn(async () => ({ rows: [{ ...task }] })), create: vi.fn(async () => ({ taskId, revision: "1" })), update: vi.fn(async () => ({ taskId, revision: "2" })), comment: vi.fn(async () => ({ commentId: "cmt_01900000-0000-7000-8000-000000000003", revision: "2", })), change: vi.fn(async (input) => { const prior = outcomes.get(input.key) if (prior) return prior if (input.expectedVersion !== task.revision) throw new Error("fixture stale revision") task.status = input.state === "done" ? "done" : "open" task.revision = (BigInt(task.revision) + 1n).toString() const result = { taskId, revision: task.revision } outcomes.set(input.key, result) commits += 1 return result }), } return { task, service, commits: () => commits } } describe("project Activities module boundaries (simulated ports)", () => { it("derives project and participant references from resolved scope, with an operation key", async () => { const { service } = fixture() await createTask(service, scope, { subject: " Task ", requestId }) expect(service.create).toHaveBeenCalledWith({ subject: "Task", body: "", target: projectRef(bindingId, projectId), participant: actorRef(bindingId, userId), key: `activities:${projectId}:create:${requestId}`, }) await listTasks(service, scope, {}) expect(service.list).toHaveBeenCalledWith({ target: projectRef(bindingId, projectId), status: "open" }) }) it("refuses guessed task ids from another app project before writes or discussion reads", async () => { const { task, service } = fixture() task.targets = [projectRef(bindingId, "01900000-0000-7000-8000-000000000099")] for (const operation of [ () => ownedTask(service, scope, taskId), () => listComments(service, scope, { taskId }), () => addComment(service, scope, { taskId, body: "Synthetic", expectedVersion: "1", requestId }), () => changeTask(service, scope, { taskId, expectedVersion: "1", requestId }, "done"), ]) await expect(operation()).rejects.toMatchObject({ code: "not_found" }) expect(service.comment).not.toHaveBeenCalled() expect(service.change).not.toHaveBeenCalled() expect(service.comments).not.toHaveBeenCalled() }) it("refuses future revision prediction before external mutation", async () => { const { service } = fixture() await expect(changeTask(service, scope, { taskId, expectedVersion: "2", requestId }, "done")).rejects.toMatchObject( { code: "conflict" }, ) expect(service.change).not.toHaveBeenCalled() }) it("lets the service deduplicate a lost-response retry without a second domain commit", async () => { const f = fixture() const input = { taskId, expectedVersion: "1", requestId } const first = await changeTask(f.service, scope, input, "done") expect(await changeTask(f.service, scope, input, "done")).toEqual(first) expect(f.commits()).toBe(1) expect(await changeTask(f.service, scope, { ...input, expectedVersion: "2" }, "open")).toEqual({ taskId, revision: "3", }) expect(f.commits()).toBe(2) }) it("rechecks project ownership after discussion reads", async () => { const { service } = fixture() vi.mocked(service.comments).mockImplementation(async () => { vi.mocked(service.get).mockResolvedValue({ ...fixture().task, targets: [projectRef(bindingId, "01900000-0000-7000-8000-000000000099")], }) return { rows: [{ commentId: taskId, body: "Must not be returned", author: null, erased: false }] } }) await expect(listComments(service, scope, { taskId })).rejects.toMatchObject({ code: "not_found" }) }) it("passes the authorized target to an update so the adapter preserves other fields", async () => { const { service } = fixture() await updateTask(service, scope, { taskId, subject: "Changed", body: "Details", expectedVersion: "1", requestId }) expect(service.update).toHaveBeenCalledWith( expect.objectContaining({ target: projectRef(bindingId, projectId), expectedVersion: "1", subject: "Changed" }), ) }) it("has the same translated keys in both languages", () => expect(Object.keys(en).sort()).toEqual(Object.keys(lt).sort())) })