Files
template-app/modules/activities/__tests__/core.test.ts
T
2026-11-14 09:56:45 +00:00

129 lines
5.7 KiB
TypeScript

// 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<string, { taskId: string; revision: string }>()
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()))
})