import assert from 'node:assert/strict'; import type { AddressInfo } from 'node:net'; import test from 'node:test'; import express, { type ErrorRequestHandler } from 'express'; import { ZodError } from 'zod'; import { ApiError } from '../src/http/api-error.js'; import { createLocalDemoApp } from '../src/local-app.js'; import { LocalSyncJobStore } from '../src/modules/domestic-voc/local/local-sync-job.store.js'; import { createAuthenticationMiddleware, type RequestAuthenticator, WorkspaceAccessService, } from '../src/modules/saas-platform/auth.js'; import { LocalPlatformRepository } from '../src/modules/saas-platform/local-platform.repository.js'; import { createSaasPlatformRouter } from '../src/modules/saas-platform/routes.js'; import type { DomesticDataset, DomesticMetricSummary, DomesticProduct } from '../src/types/domestic-dataset.js'; const emptyMetrics: DomesticMetricSummary = { gmv: 0, soldUnits: 0, transactionOrders: 0, transactionCustomers: 0, impressions: 0, clicks: 0, views: 0, visitors: 0, cartUnits: 0, orderAmount: 0, orderUnits: 0, orderCount: 0, refundAmount: 0, refundUnits: 0, refundOrders: 0, conversionRate: 0, clickThroughRate: 0, averageUnitPrice: 0, refundToGmvRate: 0, }; function product(productId: string, role: DomesticProduct['role']): DomesticProduct { return { platform: 'jd', productId, productKey: `jd:${productId}`, asin: productId, role, brand: role === 'own' ? 'Demashi' : 'Competitor', title: `Product ${productId}`, model: `M-${productId}`, category1: 'Commercial appliance', category2: 'Kitchen equipment', category3: 'Oven', source: 'test', relationCount: role === 'own' ? 2 : 0, summary: emptyMetrics, trend: [], }; } const dataset: DomesticDataset = { schemaVersion: 1, generatedAt: '2026-07-23T00:00:00.000Z', caseName: 'Demashi', platform: 'jd', source: { sourceFile: 'demashi-summary.json', sourceHash: 'test', sheets: [], dateRange: { start: '2026-07-01', end: '2026-07-23' }, }, summary: { metricRows: 0, metricProducts: 3, mappingRows: 2, relations: 2, uniqueCompetitorProducts: 2, category2Count: 1, category3Count: 1, reviewCount: 0, }, dailyTotals: [], products: [product('1001', 'own'), product('1002', 'competitor'), product('1003', 'competitor')], mappingGroups: [], relations: [ { relationKey: 'jd:1001:1002', ownProductKey: 'jd:1001', ownProductId: '1001', competitorProductKey: 'jd:1002', competitorProductId: '1002', competitorBrand: 'Competitor', category: 'Oven', }, { relationKey: 'jd:1001:1003', ownProductKey: 'jd:1001', ownProductId: '1001', competitorProductKey: 'jd:1003', competitorProductId: '1003', competitorBrand: 'Competitor', category: 'Oven', }, ], reviews: [], quality: { orphanMappings: [], mappingsWithoutCompetitor: [], brandWithoutProductId: [] }, }; async function listen(app: ReturnType) { const server = await new Promise>((resolve) => { const listening = app.listen(0, '127.0.0.1', () => resolve(listening)); }); const address = server.address() as AddressInfo; return { baseUrl: `http://127.0.0.1:${address.port}`, close: () => new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve())), }; } async function json(response: Response): Promise> { return await response.json() as Record; } test('local SaaS APIs cover context, cursor catalogs, workflows, and audit history', async () => { const app = createLocalDemoApp({ dataset, corsOrigins: ['http://localhost:4200'] }); const server = await listen(app); try { const contextResponse = await fetch(`${server.baseUrl}/api/saas/context`); assert.equal(contextResponse.status, 200); const context = await json(contextResponse); assert.equal(context.principal.userId, 'local-admin'); assert.equal(context.workspaces[0].role, 'owner'); const firstProductsResponse = await fetch(`${server.baseUrl}/api/domestic-voc/products?limit=2`); assert.equal(firstProductsResponse.status, 200); const firstProducts = await json(firstProductsResponse); assert.equal(firstProducts.items.length, 2); assert.equal(typeof firstProducts.nextCursor, 'string'); const secondProductsResponse = await fetch( `${server.baseUrl}/api/domestic-voc/products?limit=2&cursor=${encodeURIComponent(firstProducts.nextCursor)}`, ); assert.equal(secondProductsResponse.status, 200); const secondProducts = await json(secondProductsResponse); assert.equal(secondProducts.items.length, 1); assert.equal(secondProducts.nextCursor, null); const invalidCursor = await fetch(`${server.baseUrl}/api/domestic-voc/products?cursor=not-a-cursor`); assert.equal(invalidCursor.status, 400); assert.equal((await json(invalidCursor)).error, 'invalid_cursor'); const invalidActionStatus = await fetch(`${server.baseUrl}/api/saas/workspaces/demashi/actions?status=unknown`); assert.equal(invalidActionStatus.status, 400); const missingAnalysisTarget = await fetch(`${server.baseUrl}/api/saas/workspaces/demashi/analyses`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ analysisType: 'voice', targetKind: 'product' }), }); assert.equal(missingAnalysisTarget.status, 400); const firstRelations = await json(await fetch(`${server.baseUrl}/api/domestic-voc/relations?limit=1`)); assert.equal(firstRelations.items.length, 1); assert.equal(typeof firstRelations.nextCursor, 'string'); const memberResponse = await fetch(`${server.baseUrl}/api/saas/workspaces/demashi/members/viewer-user`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email: 'viewer@example.test', displayName: 'Viewer', role: 'viewer', status: 'active', }), }); assert.equal(memberResponse.status, 200); assert.equal((await json(memberResponse)).member.role, 'viewer'); const analysisResponse = await fetch(`${server.baseUrl}/api/saas/workspaces/demashi/analyses`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ analysisType: 'voc_insight', targetKind: 'workspace', input: { platform: 'jd' } }), }); assert.equal(analysisResponse.status, 202); const createdAnalysis = (await json(analysisResponse)).analysis as { id: string; status: string }; assert.equal(createdAnalysis.status, 'pending'); const processingResponse = await fetch(`${server.baseUrl}/api/saas/workspaces/demashi/analyses/${createdAnalysis.id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ status: 'processing', startedAt: '2026-07-23T10:00:00.000Z', evidenceCount: 3 }), }); assert.equal(processingResponse.status, 200); const processingAnalysis = (await json(processingResponse)).analysis as { status: string; startedAt: string | null; evidenceCount: number; }; assert.equal(processingAnalysis.status, 'processing'); assert.equal(processingAnalysis.startedAt, '2026-07-23T10:00:00.000Z'); assert.equal(processingAnalysis.evidenceCount, 3); const completedResponse = await fetch(`${server.baseUrl}/api/saas/workspaces/demashi/analyses/${createdAnalysis.id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ status: 'completed', result: { findings: ['Need clearer VOC evidence links'] }, evidenceCount: 5, completedAt: '2026-07-23T10:05:00.000Z', }), }); assert.equal(completedResponse.status, 200); const completedAnalysis = (await json(completedResponse)).analysis as { status: string; result: Record | null; completedAt: string | null; }; assert.equal(completedAnalysis.status, 'completed'); assert.deepEqual(completedAnalysis.result, { findings: ['Need clearer VOC evidence links'] }); assert.equal(completedAnalysis.completedAt, '2026-07-23T10:05:00.000Z'); const terminalAnalysisResponse = await fetch(`${server.baseUrl}/api/saas/workspaces/demashi/analyses/${createdAnalysis.id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ status: 'cancelled' }), }); assert.equal(terminalAnalysisResponse.status, 409); assert.equal((await json(terminalAnalysisResponse)).error, 'analysis_terminal'); const analyses = await json(await fetch(`${server.baseUrl}/api/saas/workspaces/demashi/analyses`)); assert.equal(analyses.items.length, 1); const actionResponse = await fetch(`${server.baseUrl}/api/saas/workspaces/demashi/actions`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ title: 'Verify review source', priority: 'high' }), }); assert.equal(actionResponse.status, 201); const action = (await json(actionResponse)).action; const completedActionResponse = await fetch(`${server.baseUrl}/api/saas/workspaces/demashi/actions/${action.id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ status: 'completed' }), }); assert.equal(completedActionResponse.status, 200); assert.equal((await json(completedActionResponse)).action.status, 'completed'); const completedActions = await json(await fetch(`${server.baseUrl}/api/saas/workspaces/demashi/actions?status=completed`)); assert.equal(completedActions.items.length, 1); assert.equal(typeof completedActions.items[0].completedAt, 'string'); const alertResponse = await fetch(`${server.baseUrl}/api/saas/workspaces/demashi/alerts`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ title: 'Review evidence unavailable', alertType: 'data_quality', severity: 'high' }), }); assert.equal(alertResponse.status, 201); const alert = (await json(alertResponse)).alert; const resolvedAlertResponse = await fetch(`${server.baseUrl}/api/saas/workspaces/demashi/alerts/${alert.id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ status: 'resolved' }), }); assert.equal(resolvedAlertResponse.status, 200); assert.equal(typeof (await json(resolvedAlertResponse)).alert.resolvedAt, 'string'); const syncResponse = await fetch(`${server.baseUrl}/api/domestic-voc/sync`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Idempotency-Key': 'platform-audit-sync-1001' }, body: JSON.stringify({ productIds: ['1001'] }), }); assert.equal(syncResponse.status, 202); const auditResponse = await fetch(`${server.baseUrl}/api/saas/workspaces/demashi/audit?limit=100`); assert.equal(auditResponse.status, 200); const auditActions = (await json(auditResponse)).items.map((item: { action: string }) => item.action); assert.ok(auditActions.includes('member.upserted')); assert.ok(auditActions.includes('analysis.created')); assert.ok(auditActions.includes('analysis.updated')); assert.ok(auditActions.includes('action.updated')); assert.ok(auditActions.includes('alert.updated')); assert.ok(auditActions.includes('sync.requested')); } finally { await server.close(); } }); test('action items retain validated VOC insight provenance and deduplicated evidence', async () => { const app = createLocalDemoApp({ dataset, corsOrigins: ['http://localhost:4200'] }); const server = await listen(app); const headers = { 'Content-Type': 'application/json' }; async function createAnalysis(analysisType: 'voice' | 'voc_insight') { const response = await fetch(`${server.baseUrl}/api/saas/workspaces/demashi/analyses`, { method: 'POST', headers, body: JSON.stringify({ analysisType, targetKind: 'workspace', input: { source: 'action-test' } }), }); assert.equal(response.status, 202); return (await json(response)).analysis as { id: string }; } async function finishAnalysis(id: string, status: 'completed' | 'partial') { const processing = await fetch(`${server.baseUrl}/api/saas/workspaces/demashi/analyses/${id}`, { method: 'PATCH', headers, body: JSON.stringify({ status: 'processing' }), }); assert.equal(processing.status, 200); const terminal = await fetch(`${server.baseUrl}/api/saas/workspaces/demashi/analyses/${id}`, { method: 'PATCH', headers, body: JSON.stringify({ status, result: { insights: [ { id: 'insight-1', evidenceIds: ['review-1', 'review-2'] }, { id: 'insight-2', evidenceIds: ['review-3'] }, ], }, evidenceCount: 3, }), }); assert.equal(terminal.status, 200); } async function createDecision( analysisId: string, insightId: string, reviewedEvidenceIds: string[], ): Promise<{ id: string }> { const response = await fetch(`${server.baseUrl}/api/saas/workspaces/demashi/insight-decisions`, { method: 'POST', headers, body: JSON.stringify({ sourceAnalysisId: analysisId, sourceInsightId: insightId, decision: 'confirmed', reviewedEvidenceIds, comment: 'Evidence reviewed for action creation.', }), }); assert.equal(response.status, 201); return (await json(response)).decision as { id: string }; } try { const ordinaryResponse = await fetch(`${server.baseUrl}/api/saas/workspaces/demashi/actions`, { method: 'POST', headers, body: JSON.stringify({ title: 'Ordinary action' }), }); assert.equal(ordinaryResponse.status, 201); const ordinary = (await json(ordinaryResponse)).action; assert.equal(ordinary.sourceAnalysisId, null); assert.equal(ordinary.sourceInsightId, null); assert.deepEqual(ordinary.evidenceIds, []); assert.equal(ordinary.validationMetric, ''); const completedInsight = await createAnalysis('voc_insight'); const pendingSource = await fetch(`${server.baseUrl}/api/saas/workspaces/demashi/actions`, { method: 'POST', headers, body: JSON.stringify({ title: 'Pending source', sourceAnalysisId: completedInsight.id }), }); assert.equal(pendingSource.status, 400); assert.equal((await json(pendingSource)).error, 'source_analysis_not_ready'); await finishAnalysis(completedInsight.id, 'completed'); const missingInsight = await fetch(`${server.baseUrl}/api/saas/workspaces/demashi/actions`, { method: 'POST', headers, body: JSON.stringify({ title: 'Missing insight', sourceAnalysisId: completedInsight.id }), }); assert.equal(missingInsight.status, 400); assert.equal((await json(missingInsight)).error, 'source_insight_required'); const unknownInsight = await fetch(`${server.baseUrl}/api/saas/workspaces/demashi/actions`, { method: 'POST', headers, body: JSON.stringify({ title: 'Unknown insight', sourceAnalysisId: completedInsight.id, sourceInsightId: 'insight-unknown', evidenceIds: ['review-1'], }), }); assert.equal(unknownInsight.status, 400); assert.equal((await json(unknownInsight)).error, 'source_insight_not_found'); const missingEvidence = await fetch(`${server.baseUrl}/api/saas/workspaces/demashi/actions`, { method: 'POST', headers, body: JSON.stringify({ title: 'Missing evidence', sourceAnalysisId: completedInsight.id, sourceInsightId: 'insight-1', }), }); assert.equal(missingEvidence.status, 400); assert.equal((await json(missingEvidence)).error, 'source_evidence_required'); const unknownEvidence = await fetch(`${server.baseUrl}/api/saas/workspaces/demashi/actions`, { method: 'POST', headers, body: JSON.stringify({ title: 'Unknown evidence', sourceAnalysisId: completedInsight.id, sourceInsightId: 'insight-1', evidenceIds: ['review-unknown'], }), }); assert.equal(unknownEvidence.status, 400); assert.equal((await json(unknownEvidence)).error, 'source_evidence_not_in_insight'); const crossInsightEvidence = await fetch(`${server.baseUrl}/api/saas/workspaces/demashi/actions`, { method: 'POST', headers, body: JSON.stringify({ title: 'Cross insight evidence', sourceAnalysisId: completedInsight.id, sourceInsightId: 'insight-1', evidenceIds: ['review-3'], }), }); assert.equal(crossInsightEvidence.status, 400); assert.equal((await json(crossInsightEvidence)).error, 'source_evidence_not_in_insight'); const orphanInsight = await fetch(`${server.baseUrl}/api/saas/workspaces/demashi/actions`, { method: 'POST', headers, body: JSON.stringify({ title: 'Orphan insight', sourceInsightId: 'insight-1' }), }); assert.equal(orphanInsight.status, 400); assert.equal((await json(orphanInsight)).error, 'source_insight_orphan'); const unconfirmedAction = await fetch(`${server.baseUrl}/api/saas/workspaces/demashi/actions`, { method: 'POST', headers, body: JSON.stringify({ title: 'Improve evidence traceability', sourceAnalysisId: completedInsight.id, sourceInsightId: 'insight-1', evidenceIds: ['review-1', 'review-1', 'review-2'], validationMetric: 'Evidence coverage >= 80%', }), }); assert.equal(unconfirmedAction.status, 400); assert.equal((await json(unconfirmedAction)).error, 'source_decision_required'); const completedDecision = await createDecision( completedInsight.id, 'insight-1', ['review-1', 'review-2'], ); const linkedPayload = { title: 'Improve evidence traceability', sourceAnalysisId: completedInsight.id, sourceInsightId: 'insight-1', sourceDecisionId: completedDecision.id, sourceKind: 'insight', evidenceIds: ['review-1', 'review-1', 'review-2'], validationMetric: 'Evidence coverage >= 80%', }; const linkedResponse = await fetch(`${server.baseUrl}/api/saas/workspaces/demashi/actions`, { method: 'POST', headers, body: JSON.stringify(linkedPayload), }); assert.equal(linkedResponse.status, 201); const linked = (await json(linkedResponse)).action; assert.equal(linked.sourceAnalysisId, completedInsight.id); assert.equal(linked.sourceInsightId, 'insight-1'); assert.equal(linked.sourceDecisionId, completedDecision.id); assert.equal(linked.sourceKind, 'insight'); assert.deepEqual(linked.evidenceIds, ['review-1', 'review-2']); assert.equal(linked.validationMetric, 'Evidence coverage >= 80%'); const duplicateResponse = await fetch(`${server.baseUrl}/api/saas/workspaces/demashi/actions`, { method: 'POST', headers, body: JSON.stringify(linkedPayload), }); assert.equal(duplicateResponse.status, 200); const duplicateBody = await json(duplicateResponse); assert.equal(duplicateBody.idempotent, true); assert.equal(duplicateBody.action.id, linked.id); const partialInsight = await createAnalysis('voc_insight'); await finishAnalysis(partialInsight.id, 'partial'); const partialDecision = await createDecision(partialInsight.id, 'insight-2', ['review-3']); const partialSource = await fetch(`${server.baseUrl}/api/saas/workspaces/demashi/actions`, { method: 'POST', headers, body: JSON.stringify({ title: 'Validate partial insight', sourceAnalysisId: partialInsight.id, sourceInsightId: 'insight-2', sourceDecisionId: partialDecision.id, sourceKind: 'insight', evidenceIds: ['review-3'], }), }); assert.equal(partialSource.status, 201); const voiceAnalysis = await createAnalysis('voice'); await finishAnalysis(voiceAnalysis.id, 'completed'); const wrongType = await fetch(`${server.baseUrl}/api/saas/workspaces/demashi/actions`, { method: 'POST', headers, body: JSON.stringify({ title: 'Wrong source type', sourceAnalysisId: voiceAnalysis.id }), }); assert.equal(wrongType.status, 400); assert.equal((await json(wrongType)).error, 'source_analysis_not_voc_insight'); const missingSource = await fetch(`${server.baseUrl}/api/saas/workspaces/demashi/actions`, { method: 'POST', headers, body: JSON.stringify({ title: 'Missing source', sourceAnalysisId: 'other-workspace-analysis' }), }); assert.equal(missingSource.status, 400); assert.equal((await json(missingSource)).error, 'source_analysis_not_found'); const tooManyEvidence = await fetch(`${server.baseUrl}/api/saas/workspaces/demashi/actions`, { method: 'POST', headers, body: JSON.stringify({ title: 'Too much evidence', evidenceIds: Array.from({ length: 101 }, (_, index) => `review-${index}`) }), }); assert.equal(tooManyEvidence.status, 400); assert.equal((await json(tooManyEvidence)).error, 'invalid_request'); const actions = await json(await fetch(`${server.baseUrl}/api/saas/workspaces/demashi/actions?limit=100`)); const persisted = actions.items.find((item: { id: string }) => item.id === linked.id); assert.equal(persisted.sourceAnalysisId, completedInsight.id); assert.deepEqual(persisted.evidenceIds, ['review-1', 'review-2']); const audit = await json(await fetch(`${server.baseUrl}/api/saas/workspaces/demashi/audit?limit=100`)); const entry = audit.items.find((item: { action: string; entityId: string }) => ( item.action === 'action.created' && item.entityId === linked.id )); assert.equal(entry.metadata.sourceAnalysisId, completedInsight.id); assert.equal(entry.metadata.sourceInsightId, 'insight-1'); assert.equal(entry.metadata.sourceDecisionId, completedDecision.id); assert.equal(entry.metadata.sourceKind, 'insight'); assert.deepEqual(entry.metadata.evidenceIds, ['review-1', 'review-2']); assert.equal(entry.metadata.evidenceCount, 2); assert.equal(entry.metadata.validationMetric, 'Evidence coverage >= 80%'); } finally { await server.close(); } }); test('viewer membership can read but cannot call write or member-management routes', async () => { const jobs = new LocalSyncJobStore(dataset); const repository = new LocalPlatformRepository(dataset, jobs, { userId: 'local-admin', email: 'local-admin@localhost', displayName: 'Local Admin', }); await repository.upsertMember({ workspaceId: 'demashi', userId: 'viewer-user', email: 'viewer@example.test', displayName: 'Viewer', role: 'viewer', status: 'active', }); const authenticator: RequestAuthenticator = { async authenticate(request) { const userId = request.header('X-Test-User') || 'local-admin'; return { userId, email: `${userId}@example.test`, displayName: userId, authMode: 'disabled' }; }, }; const app = express(); app.use(express.json()); app.use('/api', createAuthenticationMiddleware(authenticator)); app.use('/api/saas', createSaasPlatformRouter({ repository, access: new WorkspaceAccessService(repository), })); const errorHandler: ErrorRequestHandler = (error, _request, response, _next) => { if (error instanceof ApiError) { response.status(error.status).json({ error: error.code }); return; } if (error instanceof ZodError) { response.status(400).json({ error: 'invalid_request' }); return; } response.status(500).json({ error: 'internal_error' }); }; app.use(errorHandler); const server = await listen(app); const viewerHeaders = { 'Content-Type': 'application/json', 'X-Test-User': 'viewer-user' }; const ownerHeaders = { 'Content-Type': 'application/json', 'X-Test-User': 'local-admin' }; try { const readable = await fetch(`${server.baseUrl}/api/saas/workspaces/demashi/actions`, { headers: viewerHeaders }); assert.equal(readable.status, 200); const writes = [ fetch(`${server.baseUrl}/api/saas/workspaces/demashi/analyses`, { method: 'POST', headers: viewerHeaders, body: JSON.stringify({ analysisType: 'voice' }), }), fetch(`${server.baseUrl}/api/saas/workspaces/demashi/actions`, { method: 'POST', headers: viewerHeaders, body: JSON.stringify({ title: 'Forbidden' }), }), fetch(`${server.baseUrl}/api/saas/workspaces/demashi/alerts`, { method: 'POST', headers: viewerHeaders, body: JSON.stringify({ title: 'Forbidden' }), }), fetch(`${server.baseUrl}/api/saas/workspaces/demashi/members/new-user`, { method: 'PUT', headers: viewerHeaders, body: JSON.stringify({ role: 'viewer', status: 'active' }), }), ]; for (const response of await Promise.all(writes)) { assert.equal(response.status, 403); assert.equal((await json(response)).error, 'workspace_permission_denied'); } const lastOwnerRemoval = await fetch(`${server.baseUrl}/api/saas/workspaces/demashi/members/local-admin`, { method: 'PUT', headers: ownerHeaders, body: JSON.stringify({ role: 'analyst', status: 'active' }), }); assert.equal(lastOwnerRemoval.status, 409); assert.equal((await json(lastOwnerRemoval)).error, 'workspace_requires_active_owner'); const invalidAssignee = await fetch(`${server.baseUrl}/api/saas/workspaces/demashi/actions`, { method: 'POST', headers: ownerHeaders, body: JSON.stringify({ title: 'Invalid assignment', assigneeUserId: 'outside-user' }), }); assert.equal(invalidAssignee.status, 400); assert.equal((await json(invalidAssignee)).error, 'assignee_not_workspace_member'); const ownerAnalysis = await fetch(`${server.baseUrl}/api/saas/workspaces/demashi/analyses`, { method: 'POST', headers: ownerHeaders, body: JSON.stringify({ analysisType: 'voc_insight', targetKind: 'workspace', input: { source: 'viewer-test' } }), }); assert.equal(ownerAnalysis.status, 202); const ownerAnalysisId = (await json(ownerAnalysis)).analysis.id as string; const viewerPatch = await fetch(`${server.baseUrl}/api/saas/workspaces/demashi/analyses/${ownerAnalysisId}`, { method: 'PATCH', headers: viewerHeaders, body: JSON.stringify({ status: 'processing' }), }); assert.equal(viewerPatch.status, 403); assert.equal((await json(viewerPatch)).error, 'workspace_permission_denied'); const adminMember = await fetch(`${server.baseUrl}/api/saas/workspaces/demashi/members/admin-user`, { method: 'PUT', headers: ownerHeaders, body: JSON.stringify({ role: 'admin', status: 'active' }), }); assert.equal(adminMember.status, 200); const adminOwnerChange = await fetch(`${server.baseUrl}/api/saas/workspaces/demashi/members/viewer-user`, { method: 'PUT', headers: { 'Content-Type': 'application/json', 'X-Test-User': 'admin-user' }, body: JSON.stringify({ role: 'owner', status: 'active' }), }); assert.equal(adminOwnerChange.status, 403); assert.equal((await json(adminOwnerChange)).error, 'workspace_owner_required'); const secondOwner = await fetch(`${server.baseUrl}/api/saas/workspaces/demashi/members/second-owner`, { method: 'PUT', headers: ownerHeaders, body: JSON.stringify({ role: 'owner', status: 'active' }), }); assert.equal(secondOwner.status, 200); const allowedOwnerRemoval = await fetch(`${server.baseUrl}/api/saas/workspaces/demashi/members/local-admin`, { method: 'PUT', headers: ownerHeaders, body: JSON.stringify({ role: 'analyst', status: 'active' }), }); assert.equal(allowedOwnerRemoval.status, 200); } finally { await server.close(); } });