import assert from 'node:assert/strict'; import test from 'node:test'; import { __resetApiAvailabilityForTests, resolveApiBase } from './api'; import { loadV22RequirementsPage, loadV22VersionDetailData, loadV22WorkspaceData, loadV22XiaobaoWarnings, } from './v22-api'; test('V2.2 version detail API maps relation rows to existing frontend scope types', async () => { const originalFetch = globalThis.fetch; const apiBase = resolveApiBase(); const calls: Array<{ url: string; method: string }> = []; globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => { const url = String(input); calls.push({ url, method: init?.method ?? 'GET' }); if (url === `${apiBase}/config/ai`) { return jsonResponse({}); } if (url === `${apiBase}/v2.2/versions/version-1/detail-data`) { return jsonResponse({ version: { id: 'version-1', productId: 'product-1', projectId: 'project-1', name: 'Project A V1.0', releaseDate: '2026-01-20T00:00:00.000Z', createdAt: '2026-01-01T00:00:00.000Z', }, requirements: [{ id: 'req-1', productId: 'product-1', projectId: 'project-1', versionId: 'version-1', code: 'REQ-001', title: 'Login', description: 'Password login', status: 'adopted', priority: 1, type: 'feature', sourceType: 'customer', sourceTarget: 'ACME', platform: 'web, ios', creatorId: 'member-1', creator: { id: 'member-1', name: 'Product manager' }, createdAt: '2026-01-01T00:00:00.000Z', }], versionPlans: [{ id: 'plan-1', versionId: 'version-1', type: 'research', title: 'Research login', status: 'pending', ownerId: 'member-1', expectedStartAt: '2026-01-01', expectedEndAt: '2026-01-05T18:00:00.000Z', requirementCoverage: [{ requirementId: 'req-1', status: 'completed', updatedAt: '2026-01-02T00:00:00.000Z', updatedBy: 'member-1' }], logs: [], createdAt: '2026-01-01T00:00:00.000Z', }], devTasks: [{ id: 'dev-1', versionId: 'version-1', requirementId: 'req-1', categoryId: 'cat-dev', code: 'DEV-001', title: 'Build login', description: 'Implement login', status: 'in_progress', priority: 2, assigneeId: 'member-2', creatorId: 'member-1', isBlocked: false, expectedStartAt: '2026-01-02T09:00:00.000Z', expectedEndAt: '2026-01-03T18:00:00.000Z', startDate: '2026-01-02T09:00:00.000Z', completedAt: null, estimateHours: 8, references: [{ type: 'requirement', id: 'req-1', label: 'REQ-001' }], aiDraft: false, createdAt: '2026-01-02T08:00:00.000Z', updatedAt: '2026-01-02T09:00:00.000Z', }], testCases: [{ id: 'tc-1', versionId: 'version-1', requirementId: 'req-1', categoryId: 'cat-test', code: 'TC-001', title: 'Verify login', description: 'Check password login', status: 'pending', roundNo: 1, priority: 3, assigneeId: 'member-3', creatorId: 'member-1', references: [], aiDraft: false, createdAt: '2026-01-03T08:00:00.000Z', updatedAt: '2026-01-03T08:00:00.000Z', }], bugs: [{ id: 'bug-1', versionId: 'version-1', testCaseId: 'tc-1', code: 'BUG-001', title: 'Login error', description: 'Wrong prompt', status: 'open', severity: 'major', priority: 0, assigneeId: 'member-2', reporterId: 'member-3', createdAt: '2026-01-04T08:00:00.000Z', updatedAt: '2026-01-04T08:00:00.000Z', }], }); } throw new Error(`Unexpected fetch ${url}`); }) as typeof fetch; try { __resetApiAvailabilityForTests(); const result = await loadV22VersionDetailData('version-1'); assert.equal(calls[1].url, `${apiBase}/v2.2/versions/version-1/detail-data`); assert.equal(result.scope.requirements[0].typeId, 'feature'); assert.deepEqual(result.scope.requirements[0].platforms, ['web', 'ios']); assert.equal(result.scope.requirements[0].priority, 'P1'); assert.equal(result.scope.requirements[0].creator, 'Product manager'); assert.equal(result.scope.devTasks[0].taskNo, 'DEV-001'); assert.equal(result.scope.devTasks[0].actualStartAt, '2026-01-02T09:00:00.000Z'); assert.equal(result.scope.devTasks[0].actualEndAt, undefined); assert.equal(result.scope.devTasks[0].createdBy, 'member-1'); assert.equal(result.scope.testCases[0].caseNo, 'TC-001'); assert.equal(result.scope.testCases[0].createdBy, 'member-1'); assert.equal(result.scope.bugs[0].bugNo, 'BUG-001'); assert.equal(result.scope.bugs[0].reportedBy, 'member-3'); assert.equal(result.scope.plans[0].owner, 'member-1'); assert.equal(result.scope.plans[0].startTime, '2026-01-01T00:00:00.000Z'); assert.equal(result.scope.plansByType.research[0].id, 'plan-1'); assert.equal(result.scope.requirementIdSet.has('req-1'), true); } finally { globalThis.fetch = originalFetch; } }); test('V2.2 list APIs build scoped query strings without full-table reads', async () => { const originalFetch = globalThis.fetch; const apiBase = resolveApiBase(); const urls: string[] = []; globalThis.fetch = (async (input: RequestInfo | URL) => { const url = String(input); urls.push(url); if (url === `${apiBase}/config/ai`) return jsonResponse({}); if (url.startsWith(`${apiBase}/v2.2/requirements?`)) return jsonResponse({ items: [], nextCursor: 'req-2' }); if (url.startsWith(`${apiBase}/v2.2/workspace?`)) return jsonResponse({ versionPlans: [], devTasks: [], testCases: [], bugs: [] }); if (url.startsWith(`${apiBase}/v2.2/xiaobao-warning?`)) return jsonResponse([]); throw new Error(`Unexpected fetch ${url}`); }) as typeof fetch; try { __resetApiAvailabilityForTests(); await loadV22RequirementsPage({ productId: 'product-1', projectId: 'project-1', status: 'adopted', priority: 'P1', type: 'feature', q: '登录', sort: 'created_at_asc', cursor: 'req-1', limit: 50, }); await loadV22WorkspaceData('member-1'); await loadV22XiaobaoWarnings({ userId: 'member-1' }); const requirementUrl = new URL(urls[1]); assert.equal(requirementUrl.pathname.endsWith('/v2.2/requirements'), true); assert.equal(requirementUrl.searchParams.get('productId'), 'product-1'); assert.equal(requirementUrl.searchParams.get('projectId'), 'project-1'); assert.equal(requirementUrl.searchParams.get('status'), 'adopted'); assert.equal(requirementUrl.searchParams.get('priority'), 'P1'); assert.equal(requirementUrl.searchParams.get('type'), 'feature'); assert.equal(requirementUrl.searchParams.get('q'), '登录'); assert.equal(requirementUrl.searchParams.get('sort'), 'created_at_asc'); assert.equal(requirementUrl.searchParams.get('cursor'), 'req-1'); assert.equal(requirementUrl.searchParams.get('limit'), '50'); assert.equal(new URL(urls[2]).searchParams.get('userId'), 'member-1'); assert.equal(new URL(urls[3]).searchParams.get('userId'), 'member-1'); } finally { globalThis.fetch = originalFetch; } }); function jsonResponse(body: unknown): Response { return new Response(JSON.stringify(body), { status: 200, headers: { 'Content-Type': 'application/json' }, }); }