import assert from "node:assert/strict"; import { dbGet, dbRun } from "../server/db.mjs"; const api = process.env.AI_DRAMA_API_BASE || "http://127.0.0.1:8787"; const localScope = { "x-organization-id": "org-studio-lab", "x-workspace-id": "ws-local-aidrama", "x-project-id": "thunder-mouth" }; const northstarScope = { "x-organization-id": "org-northstar", "x-workspace-id": "ws-northstar-main", "x-project-id": "northstar-pilot" }; async function request(path, options = {}) { const response = await fetch(`${api}${path}`, { ...options, headers: { "content-type": "application/json", ...(options.headers || {}) } }); const payload = await response.json().catch(() => ({})); return { response, payload }; } function headers(token, scope = localScope) { return { authorization: `Bearer ${token}`, ...scope }; } async function login(email) { const result = await request("/api/auth/login", { method: "POST", body: JSON.stringify({ email, password: "Demo@123456" }) }); assert.equal(result.response.ok, true, `${email} login failed: ${JSON.stringify(result.payload)}`); return result.payload.session.token; } const taskTitle = `collaboration-smoke-${Date.now()}`; const shotId = dbGet("SELECT id FROM shots WHERE episode_id = 'episode-thunder-mouth-01' ORDER BY shot_number LIMIT 1")?.id; assert.ok(shotId, "a seeded shot is required for task-link smoke"); let ownerToken = ""; let reviewerToken = ""; let taskId = ""; let commentId = ""; let linkId = ""; try { const anonymous = await request("/api/project-activity"); assert.equal(anonymous.response.status, 401, "anonymous activity access must be rejected"); ownerToken = await login("producer@local.test"); reviewerToken = await login("review@local.test"); const created = await request("/api/tasks", { method: "POST", headers: headers(ownerToken), body: JSON.stringify({ title: taskTitle, description: "验证商业协作详情、讨论、@通知、镜头关联和活动流", kind: "review", priority: "high", assigneeUserId: "u-review", targetTab: "qa" }) }); assert.equal(created.response.status, 201, JSON.stringify(created.payload)); taskId = created.payload.task.id; const detail = await request(`/api/tasks/${encodeURIComponent(taskId)}/detail`, { headers: headers(ownerToken) }); assert.equal(detail.response.ok, true, JSON.stringify(detail.payload)); assert.equal(detail.payload.comments.length, 0, "new task should have no comments"); const comment = await request(`/api/tasks/${encodeURIComponent(taskId)}/comments`, { method: "POST", headers: headers(ownerToken), body: JSON.stringify({ body: "请 @u-review 在审片中心确认实际末帧证据", mentionUserIds: ["u-review"] }) }); assert.equal(comment.response.status, 201, JSON.stringify(comment.payload)); commentId = comment.payload.comment.id; assert.equal(comment.payload.comments.length, 1, "comment must be returned in task detail"); assert.equal(comment.payload.comments[0].mentions[0].id, "u-review", "mention must be resolved to a valid project member"); const reviewerNotifications = await request("/api/notifications?limit=200", { headers: headers(reviewerToken) }); assert.equal(reviewerNotifications.response.ok, true, JSON.stringify(reviewerNotifications.payload)); assert.ok(reviewerNotifications.payload.notifications.some((item) => item.eventKey === "task.commented" && item.targetId === taskId), "task comment must notify assignee"); const linked = await request(`/api/tasks/${encodeURIComponent(taskId)}/links`, { method: "POST", headers: headers(ownerToken), body: JSON.stringify({ linkType: "shot", targetId: shotId }) }); assert.equal(linked.response.status, 201, JSON.stringify(linked.payload)); linkId = linked.payload.link.id; assert.equal(linked.payload.links.length, 1, "shot link must be returned in task detail"); assert.equal(linked.payload.links[0].targetId, shotId, "shot link target mismatch"); const activity = await request("/api/project-activity?limit=100", { headers: headers(ownerToken) }); assert.equal(activity.response.ok, true, JSON.stringify(activity.payload)); assert.ok(activity.payload.activities.some((item) => item.action === "project.task.comment.created" && item.targetId === commentId), "comment must appear in project activity"); assert.ok(activity.payload.activities.some((item) => item.action === "project.task.link.created" && item.targetId === linkId), "link must appear in project activity"); const crossTenant = await request(`/api/tasks/${encodeURIComponent(taskId)}/detail`, { headers: headers(ownerToken, northstarScope) }); assert.equal(crossTenant.response.status, 404, "cross-organization task detail must be isolated"); const removed = await request(`/api/tasks/${encodeURIComponent(taskId)}/links/${encodeURIComponent(linkId)}`, { method: "DELETE", headers: headers(ownerToken) }); assert.equal(removed.response.ok, true, JSON.stringify(removed.payload)); assert.equal(removed.payload.links.length, 0, "task link must be removable"); console.log(`task collaboration smoke passed: task=${taskId}, comment=${commentId}, shot=${shotId}`); } finally { if (linkId) dbRun("DELETE FROM task_links WHERE id = ?", [linkId]); if (commentId) dbRun("DELETE FROM task_comments WHERE id = ?", [commentId]); if (taskId) dbRun("DELETE FROM project_tasks WHERE id = ?", [taskId]); if (taskId) dbRun("DELETE FROM user_notifications WHERE target_id = ?", [taskId]); if (taskId) dbRun("DELETE FROM audit_logs WHERE target_id = ? OR metadata_json LIKE ?", [taskId, `%${taskId}%`]); if (ownerToken) await request("/api/auth/logout", { method: "POST", headers: { authorization: `Bearer ${ownerToken}` } }).catch(() => {}); if (reviewerToken) await request("/api/auth/logout", { method: "POST", headers: { authorization: `Bearer ${reviewerToken}` } }).catch(() => {}); }