diff --git a/internal/traceimpact/service_empty_lists_test.go b/internal/traceimpact/service_empty_lists_test.go new file mode 100644 index 00000000..668ffe75 --- /dev/null +++ b/internal/traceimpact/service_empty_lists_test.go @@ -0,0 +1,38 @@ +package traceimpact + +import ( + "context" + "encoding/json" + "strings" + "testing" +) + +func TestListMethodsReturnEmptySlicesNotNil(t *testing.T) { + svc := NewService(nil) + ctx := context.Background() + + items, err := svc.ListItems(ctx, 42) + if err != nil || items == nil { + t.Fatalf("ListItems items=%v err=%v", items, err) + } + index, err := svc.ListIndex(ctx, 42) + if err != nil || index == nil { + t.Fatalf("ListIndex index=%v err=%v", index, err) + } + marks, err := svc.ListOpenMarks(ctx, 42, "") + if err != nil || marks == nil { + t.Fatalf("ListOpenMarks marks=%v err=%v", marks, err) + } + + raw, err := json.Marshal(map[string]any{ + "items": items, + "index": index, + "recheck_marks": marks, + }) + if err != nil { + t.Fatal(err) + } + if strings.Contains(string(raw), "null") { + t.Fatalf("empty lists encoded as JSON null: %s", raw) + } +} diff --git a/web/src/api/projectTrace.test.ts b/web/src/api/projectTrace.test.ts new file mode 100644 index 00000000..8a1e3b94 --- /dev/null +++ b/web/src/api/projectTrace.test.ts @@ -0,0 +1,55 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { fetchProjectTrace } from './projectTrace'; + +test('fetchProjectTrace turns null arrays into empty arrays', async () => { + const originalFetch = globalThis.fetch; + globalThis.fetch = async () => + new Response( + JSON.stringify({ + items: null, + hidden_count: 2, + index: null, + recheck_marks: null, + }), + { status: 200, headers: { 'Content-Type': 'application/json' } }, + ); + try { + const view = await fetchProjectTrace(7); + assert.deepEqual(view.items, []); + assert.equal(view.hidden_count, 2); + assert.deepEqual(view.index, []); + assert.deepEqual(view.recheck_marks, []); + } finally { + globalThis.fetch = originalFetch; + } +}); + +test('fetchProjectTrace keeps real arrays and rejects failed responses', async () => { + const originalFetch = globalThis.fetch; + globalThis.fetch = async () => + new Response( + JSON.stringify({ + items: [{ id: 1, business_key: 'UR-1', kind: 'requirement' }], + hidden_count: 0, + index: [{ doc_id: 'doc-1', status: 'ready', item_count: 1 }], + recheck_marks: [], + }), + { status: 200, headers: { 'Content-Type': 'application/json' } }, + ); + try { + const view = await fetchProjectTrace(7); + assert.equal(view.items.length, 1); + assert.equal(view.index[0]?.doc_id, 'doc-1'); + assert.deepEqual(view.recheck_marks, []); + } finally { + globalThis.fetch = originalFetch; + } + + globalThis.fetch = async () => new Response('nope', { status: 500 }); + try { + await assert.rejects(() => fetchProjectTrace(7), /需求与影响加载失败: 500/); + } finally { + globalThis.fetch = originalFetch; + } +});