Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
c9a4a80
test(mv3): require real bookmark mutation lifecycle
seonghobae Aug 10, 2026
50111a8
test(mv3): exercise bounded bookmark mutation
seonghobae Aug 10, 2026
e1099e3
test(mv3): align bookmarks compatibility contract with mutation lifec…
seonghobae Aug 10, 2026
495ea67
merge: align MV3 bookmark mutation with current prerequisite
seonghobae Aug 15, 2026
5d93647
merge: align MV3 bookmark mutation with current prerequisite
seonghobae Aug 15, 2026
49cfb55
test(mv3): preserve restart-safe download prerequisite
seonghobae Aug 15, 2026
18ebb7f
fix(mv3): preserve restart-safe download behavior
seonghobae Aug 15, 2026
83df60f
merge: align bookmark mutation with current MV3 download prerequisite
seonghobae Aug 15, 2026
857134a
merge: realign bookmark mutation with cleanup-safe MV3 root
seonghobae Aug 16, 2026
369cb5d
test(mv3): inherit cleanup import normalization
seonghobae Aug 16, 2026
6a98045
merge: realign bookmark mutation to current downloads prerequisite
seonghobae Aug 16, 2026
82403ec
chore(mv3): align bookmark mutation with current downloads prerequisite
seonghobae Aug 16, 2026
cea76b9
chore(mv3): align bookmark mutation with transport fix
seonghobae Aug 16, 2026
71420d0
test(mv3): realign bookmark mutation to hardened downloads root
seonghobae Aug 16, 2026
2e5984a
merge: align bookmark mutation with current MV3 prerequisite
seonghobae Aug 16, 2026
22bf829
chore(mv3): realign bookmark mutation with current downloads head
seonghobae Aug 16, 2026
2ede65b
chore(stack): align bookmark mutation with current downloads head
seonghobae Aug 19, 2026
70c2c36
chore(mv3): realign bookmark mutation with live downloads prerequisite
seonghobae Aug 20, 2026
7042b25
chore(mv3): align bookmark mutation with latest downloads prerequisite
seonghobae Aug 20, 2026
779c3c9
chore(mv3): merge live downloads prerequisite
seonghobae Aug 21, 2026
3536718
chore(mv3): realign bookmark stack to live downloads head
seonghobae Aug 21, 2026
8dc607c
test(mv3): require bounded bookmark diagnostics
seonghobae Aug 22, 2026
bf17be1
fix(mv3): classify bookmark mutation stages
seonghobae Aug 22, 2026
450509b
fix(mv3): propagate bookmark diagnostics
seonghobae Aug 22, 2026
a850467
fix(mv3): sanitize bookmark compatibility evidence
seonghobae Aug 22, 2026
03f8e37
merge(mv3): reconcile bookmark mutation onto downloads parent
seonghobae Aug 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions scripts/ci/run_mv3_compatibility.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
"commands",
"sidePanel",
"bookmarks",
"bookmarksDiagnostic",
"history",
"downloads",
"downloadsDiagnostic",
Expand All @@ -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",
Expand Down Expand Up @@ -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"


Expand Down Expand Up @@ -330,6 +346,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:
Expand All @@ -350,6 +368,7 @@ def _wait_for_extension_evidence(
"commands": "ready",
"sidePanel": "ready",
"bookmarks": "ready",
"bookmarksDiagnostic": "bookmark-complete-ready",
"history": "ready",
"downloads": "ready",
"downloadsDiagnostic": "download-complete-ready",
Expand Down
2 changes: 2 additions & 0 deletions tests/fixtures/mv3_basic/content_script.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down
71 changes: 69 additions & 2 deletions tests/fixtures/mv3_basic/service_worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,71 @@ async function exerciseDownload(sender) {
return waitForDownload(downloadId, url);
}

async function exerciseBookmarkMutation(sender) {
const sourceUrl = sender?.tab?.url;
if (typeof sourceUrl !== "string") {
return { ready: false, diagnostic: "bookmark-source-rejected" };
}

let parsed;
try {
parsed = new URL(sourceUrl);
} catch (_error) {
return { ready: false, diagnostic: "bookmark-source-rejected" };
}
if (
parsed.protocol !== "http:" ||
parsed.hostname !== "127.0.0.1" ||
parsed.pathname !== "/page.html" ||
parsed.username !== "" ||
parsed.password !== ""
) {
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 });
const createdId = created?.id;
if (typeof createdId !== "string" || createdId.length === 0) {
return { ready: false, diagnostic: "bookmark-create-rejected" };
}
bookmarkId = createdId;
} catch (_error) {
return { ready: false, diagnostic: "bookmark-create-rejected" };
}

let diagnostic = "bookmark-get-missing";
let bookmarkMutationReady = false;
try {
const nodes = await chrome.bookmarks.get(bookmarkId);
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 { ready: bookmarkMutationReady, diagnostic };
}

async function exerciseCoreApis(sender) {
const tabId = sender?.tab?.id;
if (!Number.isInteger(tabId)) {
Expand Down Expand Up @@ -124,8 +189,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 bookmarkResult = await exerciseBookmarkMutation(sender);
const bookmarksReady = bookmarkResult.ready;

const historyItems = await chrome.history.search({
text: "",
Expand All @@ -144,6 +209,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,
Expand Down Expand Up @@ -173,6 +239,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",
Expand Down
125 changes: 125 additions & 0 deletions tests/test_mv3_bookmark_mutation_contract.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
"""Fail-first contract for real Manifest V3 bookmark mutation compatibility."""

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):
"""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)

Comment thread
seonghobae marked this conversation as resolved.
Comment thread
seonghobae marked this conversation as resolved.
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)

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()
15 changes: 10 additions & 5 deletions tests/test_mv3_bookmarks_history_contract.py
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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."""
Expand All @@ -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)

Expand Down
Loading