From c9a4a80a3fe3ded85b646b358a193a9d2fb2dce3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 04:04:24 +0900 Subject: [PATCH 01/10] test(mv3): require real bookmark mutation lifecycle --- tests/test_mv3_bookmark_mutation_contract.py | 55 ++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 tests/test_mv3_bookmark_mutation_contract.py diff --git a/tests/test_mv3_bookmark_mutation_contract.py b/tests/test_mv3_bookmark_mutation_contract.py new file mode 100644 index 000000000..4c121b19f --- /dev/null +++ b/tests/test_mv3_bookmark_mutation_contract.py @@ -0,0 +1,55 @@ +"""Fail-first contract for real Manifest V3 bookmark mutation compatibility.""" + +from __future__ import annotations + +import json +import pathlib +import unittest + +ROOT = pathlib.Path(__file__).resolve().parents[1] +FIXTURE = ROOT / "tests" / "fixtures" / "mv3_basic" + + +class ManifestV3BookmarkMutationContractTests(unittest.TestCase): + """Require one bounded create/read/delete bookmark lifecycle in real Chromium.""" + + def test_fixture_declares_bookmarks_permission(self) -> None: + """The controlled extension must explicitly request bookmark authority.""" + + manifest = json.loads((FIXTURE / "manifest.json").read_text(encoding="utf-8")) + self.assertIn("bookmarks", manifest["permissions"]) + + def test_service_worker_executes_bounded_bookmark_mutation_lifecycle(self) -> None: + """Compatibility evidence must require create/read/delete, not only tree reads.""" + + worker = (FIXTURE / "service_worker.js").read_text(encoding="utf-8") + for expected in ( + "exerciseBookmarkMutation", + "chrome.bookmarks.create", + "chrome.bookmarks.get", + "chrome.bookmarks.remove", + '"OriginWeave MV3 compatibility bookmark"', + "bookmarkMutationReady", + ): + with self.subTest(expected=expected): + self.assertIn(expected, worker) + + def test_bookmark_mutation_is_bound_to_controlled_fixture_url_and_cleanup(self) -> None: + """The fixture must not mutate bookmarks for an arbitrary sender or leave residue.""" + + worker = (FIXTURE / "service_worker.js").read_text(encoding="utf-8") + for expected in ( + 'parsed.protocol !== "http:"', + 'parsed.hostname !== "127.0.0.1"', + 'parsed.pathname !== "/page.html"', + "finally", + "chrome.bookmarks.remove", + ): + with self.subTest(expected=expected): + self.assertIn(expected, worker) + self.assertNotIn("_error.message", worker) + self.assertNotIn("String(_error)", worker) + + +if __name__ == "__main__": + unittest.main() From 50111a845927bd6e657063b85ce76da45c13436e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 04:06:49 +0900 Subject: [PATCH 02/10] test(mv3): exercise bounded bookmark mutation --- tests/fixtures/mv3_basic/service_worker.js | 59 +++++++++++++++++++++- 1 file changed, 57 insertions(+), 2 deletions(-) diff --git a/tests/fixtures/mv3_basic/service_worker.js b/tests/fixtures/mv3_basic/service_worker.js index 37e8129d5..55ded0c42 100644 --- a/tests/fixtures/mv3_basic/service_worker.js +++ b/tests/fixtures/mv3_basic/service_worker.js @@ -99,6 +99,61 @@ async function exerciseDownload(sender) { return waitForDownload(downloadId, url); } +async function exerciseBookmarkMutation(sender) { + const sourceUrl = sender?.tab?.url; + if (typeof sourceUrl !== "string") { + return false; + } + + let parsed; + try { + parsed = new URL(sourceUrl); + } catch (_error) { + return false; + } + if ( + parsed.protocol !== "http:" || + parsed.hostname !== "127.0.0.1" || + parsed.pathname !== "/page.html" || + parsed.username !== "" || + parsed.password !== "" + ) { + return false; + } + + const title = "OriginWeave MV3 compatibility bookmark"; + let bookmarkId; + try { + const created = await chrome.bookmarks.create({ title, url: sourceUrl }); + if (typeof created?.id !== "string" || created.id.length === 0) { + return false; + } + bookmarkId = created.id; + } catch (_error) { + return false; + } + + let bookmarkMutationReady = false; + try { + const nodes = await chrome.bookmarks.get(bookmarkId); + bookmarkMutationReady = + Array.isArray(nodes) && + nodes.length === 1 && + nodes[0]?.id === bookmarkId && + nodes[0]?.title === title && + nodes[0]?.url === sourceUrl; + } catch (_error) { + bookmarkMutationReady = false; + } finally { + try { + await chrome.bookmarks.remove(bookmarkId); + } catch (_error) { + bookmarkMutationReady = false; + } + } + return bookmarkMutationReady; +} + async function exerciseCoreApis(sender) { const tabId = sender?.tab?.id; if (!Number.isInteger(tabId)) { @@ -129,8 +184,8 @@ async function exerciseCoreApis(sender) { const sidePanelOptions = await chrome.sidePanel.getOptions({ tabId }); const sidePanelReady = sidePanelOptions?.path === "side_panel.html"; - const bookmarkTree = await chrome.bookmarks.getTree(); - const bookmarksReady = Array.isArray(bookmarkTree) && bookmarkTree.length > 0; + const bookmarkMutationReady = await exerciseBookmarkMutation(sender); + const bookmarksReady = bookmarkMutationReady; const historyItems = await chrome.history.search({ text: "", From e1099e35ac000c7bf87ea75666cfdd928a386370 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 05:04:13 +0900 Subject: [PATCH 03/10] test(mv3): align bookmarks compatibility contract with mutation lifecycle --- tests/test_mv3_bookmarks_history_contract.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/tests/test_mv3_bookmarks_history_contract.py b/tests/test_mv3_bookmarks_history_contract.py index 1c7c2f7fa..164105adc 100644 --- a/tests/test_mv3_bookmarks_history_contract.py +++ b/tests/test_mv3_bookmarks_history_contract.py @@ -1,4 +1,4 @@ -"""Fail-first contract for real Manifest V3 bookmarks and history compatibility.""" +"""Compatibility contract for real Manifest V3 bookmarks and history surfaces.""" from __future__ import annotations @@ -12,7 +12,7 @@ class ManifestV3BookmarksHistoryContractTests(unittest.TestCase): - """Require two additional read-only Chrome API surfaces in the real browser lane.""" + """Require bounded bookmarks mutation plus read-only history compatibility evidence.""" def test_fixture_declares_bookmarks_and_history_permissions(self) -> None: """The controlled fixture must request the APIs it exercises.""" @@ -22,11 +22,16 @@ def test_fixture_declares_bookmarks_and_history_permissions(self) -> None: with self.subTest(permission=permission): self.assertIn(permission, manifest["permissions"]) - def test_service_worker_exercises_read_only_bookmarks_and_history_apis(self) -> None: - """Compatibility evidence must come from executing the real extension APIs.""" + def test_service_worker_exercises_bookmark_lifecycle_and_history_api(self) -> None: + """Compatibility evidence must execute bounded bookmark and history operations.""" worker = (FIXTURE / "service_worker.js").read_text(encoding="utf-8") - for expected in ("chrome.bookmarks.getTree", "chrome.history.search"): + for expected in ( + "chrome.bookmarks.create", + "chrome.bookmarks.get", + "chrome.bookmarks.remove", + "chrome.history.search", + ): with self.subTest(expected=expected): self.assertIn(expected, worker) From 49cfb5574b8a3b3d2d6f7015ede3a6b4ea2ef594 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 16 Aug 2026 08:18:44 +0900 Subject: [PATCH 04/10] test(mv3): preserve restart-safe download prerequisite --- tests/test_mv3_downloads_contract.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/test_mv3_downloads_contract.py b/tests/test_mv3_downloads_contract.py index 94696376c..53218b965 100644 --- a/tests/test_mv3_downloads_contract.py +++ b/tests/test_mv3_downloads_contract.py @@ -50,6 +50,13 @@ def test_service_worker_executes_and_verifies_a_real_loopback_download(self) -> self.assertIn(expected, worker) self.assertNotIn('chrome.runtime.getURL("download.txt")', worker) + def test_restart_pair_never_overwrites_the_previous_controlled_download(self) -> None: + """Restart evidence must not race Chrome while replacing the first pass's file.""" + + worker = (FIXTURE / "service_worker.js").read_text(encoding="utf-8") + self.assertIn('conflictAction: "uniquify"', worker) + self.assertNotIn('conflictAction: "overwrite"', worker) + def test_download_failures_emit_only_bounded_stage_diagnostics(self) -> None: """Fixture diagnostics must name a reviewed stage without retaining raw browser errors.""" From 18ebb7f7ccae1324696aeeae27c82d623ff98010 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 16 Aug 2026 08:20:30 +0900 Subject: [PATCH 05/10] fix(mv3): preserve restart-safe download behavior --- tests/fixtures/mv3_basic/service_worker.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/fixtures/mv3_basic/service_worker.js b/tests/fixtures/mv3_basic/service_worker.js index 55ded0c42..a8cec9575 100644 --- a/tests/fixtures/mv3_basic/service_worker.js +++ b/tests/fixtures/mv3_basic/service_worker.js @@ -87,7 +87,7 @@ async function exerciseDownload(sender) { downloadId = await chrome.downloads.download({ url, filename: "originweave-mv3/download.txt", - conflictAction: "overwrite", + conflictAction: "uniquify", saveAs: false, }); } catch (_error) { From 369cb5d10c878c92b63583d1d37c3741aeb0fb8d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 16 Aug 2026 22:13:51 +0900 Subject: [PATCH 06/10] test(mv3): inherit cleanup import normalization --- tests/test_mv3_session_cleanup_exception_contract.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/test_mv3_session_cleanup_exception_contract.py b/tests/test_mv3_session_cleanup_exception_contract.py index b1037696a..d9afb366b 100644 --- a/tests/test_mv3_session_cleanup_exception_contract.py +++ b/tests/test_mv3_session_cleanup_exception_contract.py @@ -6,7 +6,7 @@ import runpy import tempfile import unittest -from unittest import mock +import unittest.mock ROOT = pathlib.Path(__file__).resolve().parents[1] RUNNER = ROOT / "scripts" / "ci" / "run_mv3_compatibility.py" @@ -97,8 +97,10 @@ def fake_json_request( with tempfile.TemporaryDirectory(prefix="originweave-cleanup-contract-") as profile_dir: with ( - mock.patch.object(globals_["subprocess"], "Popen", return_value=fake_driver), - mock.patch.dict( + unittest.mock.patch.object( + globals_["subprocess"], "Popen", return_value=fake_driver + ), + unittest.mock.patch.dict( globals_, { "_free_loopback_port": lambda: 43123, From 8dc607c7432454445609118e562056d2f3ada7f8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 15:44:39 -0700 Subject: [PATCH 07/10] test(mv3): require bounded bookmark diagnostics --- tests/test_mv3_bookmark_mutation_contract.py | 70 ++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/tests/test_mv3_bookmark_mutation_contract.py b/tests/test_mv3_bookmark_mutation_contract.py index 4c121b19f..f5f3e2feb 100644 --- a/tests/test_mv3_bookmark_mutation_contract.py +++ b/tests/test_mv3_bookmark_mutation_contract.py @@ -2,12 +2,25 @@ from __future__ import annotations +import importlib.util import json import pathlib import unittest ROOT = pathlib.Path(__file__).resolve().parents[1] FIXTURE = ROOT / "tests" / "fixtures" / "mv3_basic" +RUNNER = ROOT / "scripts" / "ci" / "run_mv3_compatibility.py" + + +def _load_runner_module(): + """Load the compatibility runner without invoking its command-line entry point.""" + + spec = importlib.util.spec_from_file_location("originweave_mv3_runner", RUNNER) + if spec is None or spec.loader is None: + raise AssertionError("unable to load the MV3 compatibility runner") + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module class ManifestV3BookmarkMutationContractTests(unittest.TestCase): @@ -50,6 +63,63 @@ def test_bookmark_mutation_is_bound_to_controlled_fixture_url_and_cleanup(self) self.assertNotIn("_error.message", worker) self.assertNotIn("String(_error)", worker) + def test_bookmark_failures_emit_only_bounded_stage_diagnostics(self) -> None: + """Every bookmark stage must return a reviewed token without raw browser values.""" + + worker = (FIXTURE / "service_worker.js").read_text(encoding="utf-8") + for expected in ( + "bookmark-source-rejected", + "bookmark-create-rejected", + "bookmark-get-missing", + "bookmark-id-mismatch", + "bookmark-title-mismatch", + "bookmark-url-mismatch", + "bookmark-remove-rejected", + "bookmark-complete-ready", + "bookmarksDiagnostic", + ): + with self.subTest(expected=expected): + self.assertIn(expected, worker) + self.assertNotIn("_error.message", worker) + self.assertNotIn("String(_error)", worker) + + def test_runner_preserves_only_reviewed_bookmark_diagnostic_tokens(self) -> None: + """Trial evidence must reduce arbitrary bookmark diagnostics to `unexpected`.""" + + runner = _load_runner_module() + approved = { + "bookmark-source-rejected", + "bookmark-create-rejected", + "bookmark-get-missing", + "bookmark-id-mismatch", + "bookmark-title-mismatch", + "bookmark-url-mismatch", + "bookmark-remove-rejected", + "bookmark-complete-ready", + "bookmark-not-evaluated", + } + self.assertIn("bookmarksDiagnostic", runner.SURFACE_EVIDENCE_KEYS) + self.assertEqual(runner.BOOKMARK_DIAGNOSTIC_VALUES, frozenset(approved)) + for token in approved: + with self.subTest(token=token): + self.assertEqual( + runner._safe_surface_value("bookmarksDiagnostic", token), token + ) + + for raw in ( + "OriginWeave MV3 compatibility bookmark", + "Error: secret bookmark failure", + ): + with self.subTest(raw=raw): + error = runner.CompatibilitySurfaceError( + {"bookmarks": "missing", "bookmarksDiagnostic": raw} + ) + evidence = runner._failure_evidence(error) + self.assertEqual( + evidence["observed"]["bookmarksDiagnostic"], "unexpected" + ) + self.assertNotIn(raw, repr(evidence)) + if __name__ == "__main__": unittest.main() From bf17be1bcb1d337c2af68f85ab5ff8cf30ebb0ad Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 15:45:55 -0700 Subject: [PATCH 08/10] fix(mv3): classify bookmark mutation stages --- tests/fixtures/mv3_basic/service_worker.js | 44 ++++++++++++++-------- 1 file changed, 28 insertions(+), 16 deletions(-) diff --git a/tests/fixtures/mv3_basic/service_worker.js b/tests/fixtures/mv3_basic/service_worker.js index a8cec9575..d20e40168 100644 --- a/tests/fixtures/mv3_basic/service_worker.js +++ b/tests/fixtures/mv3_basic/service_worker.js @@ -102,14 +102,14 @@ async function exerciseDownload(sender) { async function exerciseBookmarkMutation(sender) { const sourceUrl = sender?.tab?.url; if (typeof sourceUrl !== "string") { - return false; + return { ready: false, diagnostic: "bookmark-source-rejected" }; } let parsed; try { parsed = new URL(sourceUrl); } catch (_error) { - return false; + return { ready: false, diagnostic: "bookmark-source-rejected" }; } if ( parsed.protocol !== "http:" || @@ -118,40 +118,50 @@ async function exerciseBookmarkMutation(sender) { parsed.username !== "" || parsed.password !== "" ) { - return false; + return { ready: false, diagnostic: "bookmark-source-rejected" }; } const title = "OriginWeave MV3 compatibility bookmark"; let bookmarkId; try { const created = await chrome.bookmarks.create({ title, url: sourceUrl }); - if (typeof created?.id !== "string" || created.id.length === 0) { - return false; + const createdId = created?.id; + if (typeof createdId !== "string" || createdId.length === 0) { + return { ready: false, diagnostic: "bookmark-create-rejected" }; } - bookmarkId = created.id; + bookmarkId = createdId; } catch (_error) { - return false; + return { ready: false, diagnostic: "bookmark-create-rejected" }; } + let diagnostic = "bookmark-get-missing"; let bookmarkMutationReady = false; try { const nodes = await chrome.bookmarks.get(bookmarkId); - bookmarkMutationReady = - Array.isArray(nodes) && - nodes.length === 1 && - nodes[0]?.id === bookmarkId && - nodes[0]?.title === title && - nodes[0]?.url === sourceUrl; + if (!Array.isArray(nodes) || nodes.length !== 1) { + diagnostic = "bookmark-get-missing"; + } else if (nodes[0]?.id !== bookmarkId) { + diagnostic = "bookmark-id-mismatch"; + } else if (nodes[0]?.title !== title) { + diagnostic = "bookmark-title-mismatch"; + } else if (nodes[0]?.url !== sourceUrl) { + diagnostic = "bookmark-url-mismatch"; + } else { + diagnostic = "bookmark-complete-ready"; + bookmarkMutationReady = true; + } } catch (_error) { + diagnostic = "bookmark-get-missing"; bookmarkMutationReady = false; } finally { try { await chrome.bookmarks.remove(bookmarkId); } catch (_error) { + diagnostic = "bookmark-remove-rejected"; bookmarkMutationReady = false; } } - return bookmarkMutationReady; + return { ready: bookmarkMutationReady, diagnostic }; } async function exerciseCoreApis(sender) { @@ -184,8 +194,8 @@ async function exerciseCoreApis(sender) { const sidePanelOptions = await chrome.sidePanel.getOptions({ tabId }); const sidePanelReady = sidePanelOptions?.path === "side_panel.html"; - const bookmarkMutationReady = await exerciseBookmarkMutation(sender); - const bookmarksReady = bookmarkMutationReady; + const bookmarkResult = await exerciseBookmarkMutation(sender); + const bookmarksReady = bookmarkResult.ready; const historyItems = await chrome.history.search({ text: "", @@ -204,6 +214,7 @@ async function exerciseCoreApis(sender) { commands: commandsReady ? "ready" : "missing", sidePanel: sidePanelReady ? "ready" : "missing", bookmarks: bookmarksReady ? "ready" : "missing", + bookmarksDiagnostic: bookmarkResult.diagnostic, history: historyReady ? "ready" : "missing", downloads: downloadsReady ? "ready" : "missing", downloadsDiagnostic: downloadResult.diagnostic, @@ -233,6 +244,7 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { commands: "missing", sidePanel: "missing", bookmarks: "missing", + bookmarksDiagnostic: "bookmark-not-evaluated", history: "missing", downloads: "missing", downloadsDiagnostic: "download-not-evaluated", From 450509bfc0272841694df23725cb6afa36061793 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 15:46:15 -0700 Subject: [PATCH 09/10] fix(mv3): propagate bookmark diagnostics --- tests/fixtures/mv3_basic/content_script.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/fixtures/mv3_basic/content_script.js b/tests/fixtures/mv3_basic/content_script.js index b70d1a27f..352dc6393 100644 --- a/tests/fixtures/mv3_basic/content_script.js +++ b/tests/fixtures/mv3_basic/content_script.js @@ -26,6 +26,8 @@ document.documentElement.dataset.originweaveCommands = response?.commands ?? "missing"; document.documentElement.dataset.originweaveSidePanel = response?.sidePanel ?? "missing"; document.documentElement.dataset.originweaveBookmarks = response?.bookmarks ?? "missing"; + document.documentElement.dataset.originweaveBookmarksDiagnostic = + response?.bookmarksDiagnostic ?? "bookmark-not-evaluated"; document.documentElement.dataset.originweaveHistory = response?.history ?? "missing"; document.documentElement.dataset.originweaveDownloads = response?.downloads ?? "missing"; document.documentElement.dataset.originweaveDownloadsDiagnostic = From a8504673ba4462fc01f3c42f1f5d8144d7c5a5e4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 15:49:41 -0700 Subject: [PATCH 10/10] fix(mv3): sanitize bookmark compatibility evidence --- scripts/ci/run_mv3_compatibility.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/scripts/ci/run_mv3_compatibility.py b/scripts/ci/run_mv3_compatibility.py index a0d60f397..49cdf1241 100644 --- a/scripts/ci/run_mv3_compatibility.py +++ b/scripts/ci/run_mv3_compatibility.py @@ -58,6 +58,7 @@ "commands", "sidePanel", "bookmarks", + "bookmarksDiagnostic", "history", "downloads", "downloadsDiagnostic", @@ -79,6 +80,19 @@ "download-not-evaluated", } ) +BOOKMARK_DIAGNOSTIC_VALUES = frozenset( + { + "bookmark-source-rejected", + "bookmark-create-rejected", + "bookmark-get-missing", + "bookmark-id-mismatch", + "bookmark-title-mismatch", + "bookmark-url-mismatch", + "bookmark-remove-rejected", + "bookmark-complete-ready", + "bookmark-not-evaluated", + } +) WEBDRIVER_ERROR_CODES = frozenset( { "invalid argument", @@ -129,6 +143,8 @@ def _safe_surface_value(key: str, value: str) -> str: return value if value.isdecimal() and len(value) <= 20 else "invalid" if key == "downloadsDiagnostic": return value if value in DOWNLOAD_DIAGNOSTIC_VALUES else "unexpected" + if key == "bookmarksDiagnostic": + return value if value in BOOKMARK_DIAGNOSTIC_VALUES else "unexpected" return value if value in SURFACE_EVIDENCE_VALUES else "unexpected" @@ -314,6 +330,8 @@ def _wait_for_extension_evidence( commands: document.documentElement.dataset.originweaveCommands || "missing", sidePanel: document.documentElement.dataset.originweaveSidePanel || "missing", bookmarks: document.documentElement.dataset.originweaveBookmarks || "missing", + bookmarksDiagnostic: + document.documentElement.dataset.originweaveBookmarksDiagnostic || "bookmark-not-evaluated", history: document.documentElement.dataset.originweaveHistory || "missing", downloads: document.documentElement.dataset.originweaveDownloads || "missing", downloadsDiagnostic: @@ -334,6 +352,7 @@ def _wait_for_extension_evidence( "commands": "ready", "sidePanel": "ready", "bookmarks": "ready", + "bookmarksDiagnostic": "bookmark-complete-ready", "history": "ready", "downloads": "ready", "downloadsDiagnostic": "download-complete-ready",