From c9a4a80a3fe3ded85b646b358a193a9d2fb2dce3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 04:04:24 +0900 Subject: [PATCH 1/3] 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 00000000..4c121b19 --- /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 2/3] 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 37e8129d..55ded0c4 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 3/3] 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 1c7c2f7f..164105ad 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)