@@ -0,0 +1,128 @@
|
||||
// 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()))
|
||||
})
|
||||
Reference in New Issue
Block a user