import assert from "node:assert/strict"; const api = process.env.AI_DRAMA_API_BASE || "http://127.0.0.1:8787"; const studioScope = { "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, headers = {}, options = {}) { const response = await fetch(`${api}${path}`, { ...options, headers: { "content-type": "application/json", ...headers, ...(options.headers || {}) } }); const payload = await response.json().catch(() => ({})); return { response, payload }; } async function requestText(path, headers = {}, options = {}) { const response = await fetch(`${api}${path}`, { ...options, headers: { ...headers, ...(options.headers || {}) } }); return { response, body: await response.text() }; } function assertOk(result, label) { assert.equal(result.response.ok, true, `${label}: ${result.response.status} ${result.payload.detail || result.payload.error || ""}`); return result.payload; } function billingSnapshot(billing) { return { planName: billing.plan_name, seatLimit: Number(billing.seat_limit), storageGb: Number(billing.storage_gb), monthlyClipQuota: Number(billing.monthly_clip_quota), localRunnerOnly: Boolean(billing.local_runner_only), cloudConnectorsRequireApproval: Boolean(billing.cloud_connectors_require_approval) }; } function quotaSnapshots(quotas) { return (quotas || []).map((quota) => ({ id: quota.id, limitValue: Number(quota.limitValue ?? quota.limit_value) })); } function mutatedBilling(original, commercial) { const reserved = Number(commercial.seat?.reserved || 0); const clipUsed = Math.ceil(Math.max(...(commercial.quotas || []).filter((quota) => quota.metric === "clip").map((quota) => Number(quota.usedValue || 0)), 0)); const storageUsed = Math.ceil(Math.max(...(commercial.quotas || []).filter((quota) => quota.metric === "storage").map((quota) => Number(quota.usedValue || 0)), 0)); return { planName: `${original.planName} · Smoke`, seatLimit: Math.max(original.seatLimit + 1, reserved + 1), storageGb: Math.max(original.storageGb + 1, storageUsed + 1), monthlyClipQuota: Math.max(original.monthlyClipQuota + 1, clipUsed + 1), localRunnerOnly: !original.localRunnerOnly, cloudConnectorsRequireApproval: !original.cloudConnectorsRequireApproval }; } async function login(email) { const result = await request("/api/auth/login", {}, { method: "POST", body: JSON.stringify({ email, password: "Demo@123456" }) }); assertOk(result, `${email} 登录`); return { authorization: `Bearer ${result.payload.session.token}` }; } async function patchBilling(headers, organizationId, body) { return request(`/api/organizations/${encodeURIComponent(organizationId)}/billing`, headers, { method: "PATCH", body: JSON.stringify(body) }); } async function restoreOrganization(label, headers, organizationId, originalBilling, originalQuotas) { const failures = []; for (const quota of originalQuotas) { const result = await request(`/api/organizations/${encodeURIComponent(organizationId)}/quotas/${encodeURIComponent(quota.id)}`, headers, { method: "PATCH", body: JSON.stringify({ limitValue: quota.limitValue }) }); if (!result.response.ok) failures.push(`${label} quota ${quota.id}: ${result.response.status} ${result.payload.error || ""}`); } const billingResult = await patchBilling(headers, organizationId, originalBilling); if (!billingResult.response.ok) failures.push(`${label} billing: ${billingResult.response.status} ${billingResult.payload.error || ""}`); if (failures.length) throw new Error(`恢复 ${label} 配置失败:${failures.join("; ")}`); } const ownerAuth = await login("producer@local.test"); const writerAuth = await login("writer@local.test"); const orgAdminAuth = await login("producer2@local.test"); const ownerHeaders = { ...ownerAuth, ...studioScope }; const writerHeaders = { ...writerAuth, ...studioScope }; const orgAdminHeaders = { ...orgAdminAuth, ...northstarScope }; let studioOriginal = null; let studioOriginalQuotas = []; let northstarOriginal = null; let northstarOriginalQuotas = []; try { const studioBefore = assertOk(await request("/api/organizations/org-studio-lab/commercial", ownerHeaders), "系统管理员读取星河商业配置"); assert.ok(studioBefore.billing && studioBefore.seat && Array.isArray(studioBefore.quotas), "商业配置必须包含套餐、席位和工作区配额"); studioOriginal = billingSnapshot(studioBefore.billing); studioOriginalQuotas = quotaSnapshots(studioBefore.quotas); const writerCommercial = await request("/api/organizations/org-studio-lab/commercial", writerHeaders); assert.equal(writerCommercial.response.status, 403, "普通编剧不应读取组织商业配置"); const writerBilling = await patchBilling(writerHeaders, "org-studio-lab", studioOriginal); assert.equal(writerBilling.response.status, 403, "普通编剧不应修改组织套餐"); const usageBefore = assertOk(await request("/api/organizations/org-studio-lab/usage?from=2026-01-01&to=2026-12-31&page=1&pageSize=10", ownerHeaders), "系统管理员读取事件级用量"); assert.ok(Array.isArray(usageBefore.items), "用量明细必须返回事件列表"); assert.ok(usageBefore.pagination && Number.isInteger(usageBefore.pagination.total), "用量明细必须返回分页信息"); assert.ok(usageBefore.summary && Array.isArray(usageBefore.summary.byKind), "用量明细必须返回分类汇总"); assert.ok(usageBefore.facets && Array.isArray(usageBefore.facets.workspaces) && Array.isArray(usageBefore.facets.users), "用量明细必须返回筛选选项"); const workspaceUsage = assertOk(await request("/api/organizations/org-studio-lab/usage?workspaceId=ws-local-aidrama&from=2026-01-01&to=2026-12-31", ownerHeaders), "按工作区筛选用量"); assert.ok(workspaceUsage.items.every((item) => item.workspaceId === "ws-local-aidrama"), "工作区筛选不能返回其他工作区事件"); const costCenterUsage = assertOk(await request("/api/organizations/org-studio-lab/usage?costCenter=local-gpu&from=2026-01-01&to=2026-12-31", ownerHeaders), "按成本中心筛选用量"); assert.ok(costCenterUsage.items.every((item) => item.costCenter === "local-gpu"), "成本中心筛选不能返回其他成本中心事件"); const writerUsage = await request("/api/organizations/org-studio-lab/usage", writerHeaders); assert.equal(writerUsage.response.status, 403, "普通编剧不应读取组织用量明细"); const writerUsageExport = await request("/api/organizations/org-studio-lab/usage/export?format=json", writerHeaders); assert.equal(writerUsageExport.response.status, 403, "普通编剧不应导出组织用量明细"); const usageJsonExport = assertOk(await request("/api/organizations/org-studio-lab/usage/export?format=json&from=2026-01-01&to=2026-12-31", ownerHeaders), "导出用量 JSON"); assert.ok(Array.isArray(usageJsonExport.items), "JSON 用量导出必须包含事件列表"); const usageCsvExport = await requestText("/api/organizations/org-studio-lab/usage/export?format=csv&from=2026-01-01&to=2026-12-31", ownerHeaders); assert.equal(usageCsvExport.response.status, 200, "导出用量 CSV 应成功"); assert.match(usageCsvExport.response.headers.get("content-type") || "", /text\/csv/, "用量 CSV content-type 不正确"); assert.match(usageCsvExport.body, /"id","created_at","workspace_id"/, "用量 CSV 表头不完整"); const studioMutation = mutatedBilling(studioOriginal, studioBefore); const studioAfterUpdate = assertOk(await patchBilling(ownerHeaders, "org-studio-lab", studioMutation), "系统管理员更新星河套餐"); assert.equal(studioAfterUpdate.billing.plan_name, studioMutation.planName, "套餐名称未保存"); assert.equal(Number(studioAfterUpdate.billing.seat_limit), studioMutation.seatLimit, "席位上限未保存"); assert.equal(Boolean(studioAfterUpdate.billing.local_runner_only), studioMutation.localRunnerOnly, "本地 Runner 策略未保存"); assert.equal(Boolean(studioAfterUpdate.billing.cloud_connectors_require_approval), studioMutation.cloudConnectorsRequireApproval, "外部连接器审批策略未保存"); const studioClipQuota = studioBefore.quotas.find((quota) => quota.metric === "clip"); assert.ok(studioClipQuota, "星河组织缺少片段配额"); const nextClipLimit = Math.max(Number(studioClipQuota.usedValue || 0), Math.min(studioMutation.monthlyClipQuota, Number(studioClipQuota.limitValue || 0) + 1)); const clipQuotaUpdate = assertOk(await request(`/api/organizations/org-studio-lab/quotas/${encodeURIComponent(studioClipQuota.id)}`, ownerHeaders, { method: "PATCH", body: JSON.stringify({ limitValue: nextClipLimit }) }), "更新星河片段配额"); assert.equal(Number(clipQuotaUpdate.quotas.find((quota) => quota.id === studioClipQuota.id)?.limitValue), nextClipLimit, "片段配额未保存"); const overPlanQuota = await request(`/api/organizations/org-studio-lab/quotas/${encodeURIComponent(studioClipQuota.id)}`, ownerHeaders, { method: "PATCH", body: JSON.stringify({ limitValue: studioMutation.monthlyClipQuota + 1 }) }); assert.equal(overPlanQuota.response.status, 409, "工作区片段配额不能超过组织套餐"); assert.equal(overPlanQuota.payload.error, "quota_above_plan", "超套餐配额错误码不稳定"); if (Number(studioBefore.seat?.reserved || 0) > 1) { const belowReserved = await patchBilling(ownerHeaders, "org-studio-lab", { seatLimit: Number(studioBefore.seat.reserved) - 1 }); assert.equal(belowReserved.response.status, 409, "席位上限不能低于已占用和待处理邀请"); assert.equal(belowReserved.payload.error, "seat_limit_below_reserved", "席位冲突错误码不稳定"); } const northstarBefore = assertOk(await request("/api/organizations/org-northstar/commercial", orgAdminHeaders), "组织管理员读取北辰商业配置"); assert.ok(northstarBefore.billing && northstarBefore.seat, "组织管理员商业配置返回不完整"); northstarOriginal = billingSnapshot(northstarBefore.billing); northstarOriginalQuotas = quotaSnapshots(northstarBefore.quotas); assert.ok((await request("/api/context", orgAdminHeaders)).payload.context.permissions.includes("billing:manage"), "组织管理员缺少套餐管理权限"); const northstarMutation = mutatedBilling(northstarOriginal, northstarBefore); const northstarAfterUpdate = assertOk(await patchBilling(orgAdminHeaders, "org-northstar", northstarMutation), "组织管理员更新北辰套餐"); assert.equal(northstarAfterUpdate.billing.plan_name, northstarMutation.planName, "组织管理员套餐名称未保存"); assert.equal(Number(northstarAfterUpdate.billing.seat_limit), northstarMutation.seatLimit, "组织管理员席位上限未保存"); console.log(`commercial ops smoke passed: ${api}`); } finally { const failures = []; if (studioOriginal) { try { await restoreOrganization("星河组织", ownerHeaders, "org-studio-lab", studioOriginal, studioOriginalQuotas); } catch (error) { failures.push(error.message); } } if (northstarOriginal) { try { await restoreOrganization("北辰组织", orgAdminHeaders, "org-northstar", northstarOriginal, northstarOriginalQuotas); } catch (error) { failures.push(error.message); } } if (failures.length) throw new Error(failures.join("; ")); }