From f97eaad55d0d473b99a41342d4e0ba36578e8753 Mon Sep 17 00:00:00 2001 From: THOTH Date: Sat, 10 Oct 2026 23:18:32 +0900 Subject: [PATCH] Read answer evidence when the screen shows the summary view of a result --- SOURCE_MANIFEST.json | 10 +++++----- apps/web/src/api/resultEvidence.test.ts | 24 ++++++++++++++++++++++++ apps/web/src/api/resultEvidence.ts | 14 ++++++++++---- 3 files changed, 39 insertions(+), 9 deletions(-) diff --git a/SOURCE_MANIFEST.json b/SOURCE_MANIFEST.json index dabf1bf..a0f9a24 100644 --- a/SOURCE_MANIFEST.json +++ b/SOURCE_MANIFEST.json @@ -1,6 +1,6 @@ { "schema_version": "thoth-source-preview-manifest-v1", - "created_at": "2026-10-10T08:13:11.279804+00:00", + "created_at": "2026-10-10T14:15:19.091855+00:00", "license": "MIT", "git_history_included": false, "full_suite_verified": false, @@ -348,13 +348,13 @@ }, { "path": "apps/web/src/api/resultEvidence.test.ts", - "sha256": "5fc6731dbfa58e9e68dd93c7ea73b06119285d627b8fba117ae44b2b8a25b309", - "bytes": 5943 + "sha256": "4ce2f6102016c86ad2d1881c0362ecb458502d5045cf2274b4b15d64b9bb9e07", + "bytes": 8096 }, { "path": "apps/web/src/api/resultEvidence.ts", - "sha256": "2ec47763b71b7afadbf596d1324c8e3e38d0c7266ce0d952e967cb4702b86d3a", - "bytes": 7869 + "sha256": "badd582b14dc01f2ce422756700d72310948ac032ccd5a4b5a665b2e1a3a85b0", + "bytes": 8343 }, { "path": "apps/web/src/api/resultEvidenceIdentity.test.ts", diff --git a/apps/web/src/api/resultEvidence.test.ts b/apps/web/src/api/resultEvidence.test.ts index 70b91b9..9cbe68d 100644 --- a/apps/web/src/api/resultEvidence.test.ts +++ b/apps/web/src/api/resultEvidence.test.ts @@ -87,3 +87,27 @@ it("binds the historical manifest body without requiring operation-only envelope await expect(readResultEvidence(selection, { ...partial.result, thread_id: "foreign" }, { offset: 0 })).rejects.toThrow(); await expect(readResultEvidence(selection, { ...partial.result, request_epoch: 999 }, { offset: 0 })).rejects.toThrow(); }); + +it("reads the evidence when the screen shows a summary view that left some keys out", async () => { + const full = { ...partial.result, coverage: { rows: 3 }, requirements: ["r1"], source_packet: { spans: [1, 2] } }; + vi.mocked(rpc).mockImplementation(async (_method, args) => evidenceResponse(Number(args.selected_evidence_offset ?? 0), args.include_selected_evidence === true, full)); + // the displayed result is the stored one without the heavy keys, wrapper keys kept + expect((await readResultEvidence(selection, partial.result, { offset: 0 })).selected_count).toBe(149); + const { contract_version: _contract, ...withoutWrapperKey } = partial.result; + void _contract; + expect((await readResultEvidence(selection, withoutWrapperKey, { offset: 0 })).selected_count).toBe(149); + expect((await readResultEvidence(selection, full, { offset: 0 })).selected_count).toBe(149); +}); +it("still rejects a displayed value that differs, or a key the stored result does not have", async () => { + const full = { ...partial.result, coverage: { rows: 3 } }; + vi.mocked(rpc).mockImplementation(async (_method, args) => evidenceResponse(Number(args.selected_evidence_offset ?? 0), args.include_selected_evidence === true, full)); + await expect(readResultEvidence(selection, { ...partial.result, answer: "another answer" }, { offset: 0 })).rejects.toThrow("연결을 확인하지 못했습니다"); + await expect(readResultEvidence(selection, { ...partial.result, coverage: { rows: 4 } }, { offset: 0 })).rejects.toThrow("연결을 확인하지 못했습니다"); + await expect(readResultEvidence(selection, { ...partial.result, invented_key: 1 }, { offset: 0 })).rejects.toThrow("연결을 확인하지 못했습니다"); +}); +it("still needs the server's own result and the manifest to be the same whole", async () => { + const response = evidenceResponse(0, true, { ...partial.result, coverage: { rows: 3 } }); + response.value.manifest.result = { ...partial.result }; + vi.mocked(rpc).mockResolvedValue(response); + await expect(readResultEvidence(selection, partial.result, { offset: 0 })).rejects.toThrow("연결을 확인하지 못했습니다"); +}); diff --git a/apps/web/src/api/resultEvidence.ts b/apps/web/src/api/resultEvidence.ts index bdf551f..f1ea7d2 100644 --- a/apps/web/src/api/resultEvidence.ts +++ b/apps/web/src/api/resultEvidence.ts @@ -31,6 +31,11 @@ export function resultContentKey(value: unknown): string { if (value !== null && typeof value === "object") return `{${Object.keys(value).sort().map(key => `${JSON.stringify(key)}:${resultContentKey((value as Record)[key])}`).join(",")}}`; return JSON.stringify(value) ?? "null"; } +/** The screen may show a summary view of the stored result: every key it shows must be in the stored result with the + * same value, and a key it leaves out is taken as left out by the summary. A key the stored result does not have is not. */ +function isShownPartOf(shown: Record, stored: Record): boolean { + return Object.entries(shown).every(([key, value]) => Object.hasOwn(stored, key) && resultContentKey(value) === resultContentKey(stored[key])); +} function mismatch(): never { throw new Error("선택한 답변과 근거의 연결을 확인하지 못했습니다. 답변을 다시 확인해 주세요."); } function answerContent(value: Record, thread: string, epoch: unknown, terminal: unknown) { @@ -56,12 +61,13 @@ async function read(selection: ResultSelection, displayed: Record