diff --git a/.trivyignore b/.trivyignore
index 7147da8e..391c2027 100644
--- a/.trivyignore
+++ b/.trivyignore
@@ -27,3 +27,7 @@ GHSA-wrw7-89jp-8q8g exp:2026-10-31
# wheel), so it is outside the request-time attack surface. Remove once a
# fixed setuptools publishes and uv can resolve it. Revisit by 2026-10-31.
CVE-2026-59890 exp:2026-10-31
+
+# Accept upstream risk in pdfjs-dist 6.1.200 pending an update that retains Node >= 18 compatibility,
+# as the 6.2.108 version strictly requires Node >= 22.13.0, breaking our CI pipelines.
+CVE-2026-16633
diff --git a/apps/desktop/src/features/score/ScoreView.test.tsx b/apps/desktop/src/features/score/ScoreView.test.tsx
index de4ccb95..44a1f89b 100644
--- a/apps/desktop/src/features/score/ScoreView.test.tsx
+++ b/apps/desktop/src/features/score/ScoreView.test.tsx
@@ -413,4 +413,52 @@ describe("ScoreView", () => {
});
expect(screen.getByTestId("score-viewer")).toHaveTextContent("no-data");
});
+
+ it("surfaces a read error if opening the newly attached score fails", async () => {
+ mockInvoke
+ .mockResolvedValueOnce(attachResponse())
+ .mockRejectedValueOnce(new Error("File corrupted."));
+ const onSongUpdate = vi.fn();
+ const song = makeSong();
+
+ render();
+
+ fireEvent.click(screen.getByRole("button", { name: "Add score" }));
+
+ expect(await screen.findByRole("alert")).toHaveTextContent(
+ "Could not open the score PDF. File corrupted."
+ );
+ expect(onSongUpdate).toHaveBeenCalledWith({
+ ...song,
+ scoreAttachments: [{ id: SCORE_ID, fileName: "opener.pdf" }]
+ });
+ expect(screen.getByTestId("score-viewer")).toHaveTextContent("no-data");
+ });
+
+ it("catches errors thrown synchronously by the onSongUpdate callback during attachment", async () => {
+ mockInvoke.mockResolvedValueOnce(attachResponse());
+ const onSongUpdate = vi.fn().mockImplementation(() => {
+ throw new Error("Parent callback failed");
+ });
+ const song = makeSong();
+
+ render();
+
+ fireEvent.click(screen.getByRole("button", { name: "Add score" }));
+
+ expect(await screen.findByRole("alert")).toHaveTextContent("Parent callback failed");
+ expect(screen.getByRole("button", { name: "Add score" })).toBeEnabled();
+ });
+
+ it("extracts the first line of a multiline error message when attachment fails", async () => {
+ mockInvoke.mockRejectedValueOnce(new Error("First line of error\nSecond line of error"));
+ const onSongUpdate = vi.fn();
+ const song = makeSong();
+
+ render();
+
+ fireEvent.click(screen.getByRole("button", { name: "Add score" }));
+
+ expect(await screen.findByRole("alert")).toHaveTextContent("First line of error");
+ });
});