diff --git a/src/application/analysisView/AnalysisViewService.test.ts b/src/application/analysisView/AnalysisViewService.test.ts index d7b3a422da..5ceb730528 100644 --- a/src/application/analysisView/AnalysisViewService.test.ts +++ b/src/application/analysisView/AnalysisViewService.test.ts @@ -1478,6 +1478,53 @@ it.each(["file", "bundle"] as const)( }, ); +it.each(["file", "bundle"] as const)( + "preserves authenticated %s parent Evidence when cancellation arrives before projection", + async (kind) => { + const parent = analysisViewCaptureEvidence(); + const path = join( + await createTestTempDirectory("rea-view-admission-"), + "source.json", + ); + await writeFile( + path, + JSON.stringify(kind === "file" ? parent : createEvidenceBundle([parent])), + ); + const controller = new AbortController(); + const dispatch = queuedInterrupts.dispatchQueuedInterrupts; + const interrupt = vi + .spyOn(queuedInterrupts, "dispatchQueuedInterrupts") + .mockImplementation(async () => { + await dispatch(); + controller.abort("after-source-admission"); + }); + try { + const result = await inspectAnalysisView( + { + source: + kind === "file" + ? { kind: "evidence-file", path } + : { kind: "retained-evidence", evidence_id: parent.evidence_id }, + view: manifestView, + }, + undefined, + { + signal: controller.signal, + ...(kind === "bundle" ? { evidenceBundlePath: path } : {}), + }, + ); + if (result.ok) throw new Error("Expected cancellation before projection"); + expect(result.error).toMatchObject({ + _tag: "AnalysisCancelledError", + cause: "after-source-admission", + partialObservation: parent, + }); + } finally { + interrupt.mockRestore(); + } + }, +); + it.each(["file", "bundle"] as const)( "preserves completed %s view Evidence when cancellation arrives after projection", async (kind) => { diff --git a/src/application/analysisView/AnalysisViewService.ts b/src/application/analysisView/AnalysisViewService.ts index e24921b7ee..e0576ac98e 100644 --- a/src/application/analysisView/AnalysisViewService.ts +++ b/src/application/analysisView/AnalysisViewService.ts @@ -101,7 +101,11 @@ const inspectFileView = async ( // the signal notifications they enqueue before accepting either result. await dispatchQueuedInterrupts(); // A request cancelled during authentication reports cancellation, even when authentication failed. - const admitted = cancelledFileViewResult(loaded, options.signal); + const admitted = cancelledFileViewResult( + loaded, + options.signal, + loaded.ok ? loaded.value : undefined, + ); if (!admitted.ok) return admitted; const id = admitted.value.evidence_id; const projected = inspectAnalysisViewValidated( @@ -140,7 +144,14 @@ const inspectBundleView = async ( await dispatchQueuedInterrupts(); // Preserve an already-produced bundle failure before considering a later abort. if (!loaded.ok) return loaded; - const admitted = cancelledFileViewResult(loaded, signal); + const parentId = input.source.evidence_id; + const admitted = cancelledFileViewResult( + loaded, + signal, + signal?.aborted + ? loaded.value.records.find((record) => record.evidence_id === parentId) + : undefined, + ); if (!admitted.ok) return admitted; const projected = inspectAnalysisViewValidated( { source: input.source, view: input.view },