diff --git a/src/source/pdf/viewer-url.test.ts b/src/source/pdf/viewer-url.test.ts
index 2e553ba..ce6b602 100644
--- a/src/source/pdf/viewer-url.test.ts
+++ b/src/source/pdf/viewer-url.test.ts
@@ -38,11 +38,16 @@ describe("resolvePdfViewerUrl", () => {
);
});
- it("falls back to fixture path when only a stale blob uri is stored", () => {
+ it("returns null for stale blob uri without live bytes (no fixture guess)", () => {
const store = createPdfByteStore();
expect(
resolvePdfViewerUrl(doc({ uri: "blob:revoked", title: "a.pdf" }), store),
- ).toBe("/fixtures/pdfs/a.pdf");
+ ).toBeNull();
+ });
+
+ it("returns null when uri was stripped on persist and bytes are gone", () => {
+ const store = createPdfByteStore();
+ expect(resolvePdfViewerUrl(doc({ uri: undefined, title: "upload.pdf" }), store)).toBeNull();
});
});
@@ -51,4 +56,4 @@ describe("isEphemeralBlobUri", () => {
expect(isEphemeralBlobUri("blob:http://localhost/x")).toBe(true);
expect(isEphemeralBlobUri("/fixtures/pdfs/x.pdf")).toBe(false);
});
-});
\ No newline at end of file
+});
diff --git a/src/source/pdf/viewer-url.ts b/src/source/pdf/viewer-url.ts
index 4768b0f..da36819 100644
--- a/src/source/pdf/viewer-url.ts
+++ b/src/source/pdf/viewer-url.ts
@@ -4,6 +4,10 @@
* Uploaded PDFs are served via ephemeral `blob:` URLs owned by
* `PdfByteStore`. Those URLs must not be persisted (see persistence
* sanitization) and must be re-resolved from live bytes on every render.
+ *
+ * After a reload, bytes come back via IndexedDB hydrate into the byte store.
+ * Never invent a `/fixtures/pdfs/
` URL for arbitrary uploads — that
+ * path 404s (or returns HTML) and PDF.js reports "Invalid PDF structure".
*/
import type { Document } from "@shared/document";
@@ -20,7 +24,8 @@ export function isEphemeralBlobUri(uri: string | undefined): boolean {
/**
* Prefer the byte store's live blob URL for uploaded documents; fall back
- * to a stable HTTP fixture path or a non-blob `document.uri`.
+ * to a stable non-blob `document.uri` (e.g. fixture HTTP path). Returns
+ * `null` when bytes are not available — ViewerShell shows re-upload copy.
*/
export function resolvePdfViewerUrl(
document: Document,
@@ -33,8 +38,8 @@ export function resolvePdfViewerUrl(
return document.uri;
}
- const titleOrId = document.title ?? document.id;
- return `/fixtures/pdfs/${encodeURIComponent(titleOrId)}`;
+ // Stale blob URI and no in-memory/IDB bytes: do not guess a fixture path.
+ return null;
}
/** True when bytes exist in the store but the document record lacks a URI. */
@@ -43,4 +48,4 @@ export function documentHasUploadedBytes(
byteStore: PdfByteStore,
): boolean {
return byteStore.has(documentId);
-}
\ No newline at end of file
+}
diff --git a/tests/integration/helpers/seed-session.ts b/tests/integration/helpers/seed-session.ts
index a7aa2e9..c2e8848 100644
--- a/tests/integration/helpers/seed-session.ts
+++ b/tests/integration/helpers/seed-session.ts
@@ -49,10 +49,18 @@ export function seedSessionWithDoc(opts: SeedOptions): SeedResult {
lastOpenedAt: now,
};
+ // Stable HTTP fixture URI so the viewer mounts without IndexedDB bytes.
+ // (resolvePdfViewerUrl no longer invents /fixtures paths from title alone —
+ // that produced "Invalid PDF structure" for arbitrary uploads after reload.)
const document: Document = {
id: documentId,
mediaType: "application/pdf",
- ...(opts.documentTitle ? { title: opts.documentTitle } : {}),
+ ...(opts.documentTitle
+ ? {
+ title: opts.documentTitle,
+ uri: `/fixtures/pdfs/${encodeURIComponent(opts.documentTitle)}`,
+ }
+ : {}),
fingerprint: "seed-fingerprint",
createdAt: now,
updatedAt: now,