From a60875f70f8412db27ff1025b75d7ad4b8ddc38e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 06:19:12 +0900 Subject: [PATCH 1/5] test(mv3): require update migration evidence --- tests/test_mv3_update_migration_contract.py | 60 +++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 tests/test_mv3_update_migration_contract.py diff --git a/tests/test_mv3_update_migration_contract.py b/tests/test_mv3_update_migration_contract.py new file mode 100644 index 00000000..94600dcb --- /dev/null +++ b/tests/test_mv3_update_migration_contract.py @@ -0,0 +1,60 @@ +"""Fail-first contract for pinned-Chromium extension update migration evidence.""" + +from __future__ import annotations + +import pathlib +import unittest + +ROOT = pathlib.Path(__file__).resolve().parents[1] +RUNNER = ROOT / "scripts" / "ci" / "run_mv3_compatibility.py" +FIXTURE = ROOT / "tests" / "fixtures" / "mv3_basic" + + +class ManifestV3UpdateMigrationContractTests(unittest.TestCase): + """Require a real restart across a controlled unpacked-extension version update.""" + + def test_runner_uses_trial_local_extension_copy_and_version_update(self) -> None: + """Update evidence must not rewrite the checked-in fixture or reuse global state.""" + + runner = RUNNER.read_text(encoding="utf-8") + for expected in ( + "shutil.copytree", + "extension_dir", + "INITIAL_EXTENSION_VERSION", + "UPDATED_EXTENSION_VERSION", + "_set_fixture_version", + "update-migration", + ): + with self.subTest(expected=expected): + self.assertIn(expected, runner) + + def test_service_worker_migrates_versioned_storage_state(self) -> None: + """The fixture must expose deterministic version-state migration, not update inference.""" + + worker = (FIXTURE / "service_worker.js").read_text(encoding="utf-8") + content = (FIXTURE / "content_script.js").read_text(encoding="utf-8") + for expected in ( + "chrome.runtime.getManifest().version", + "originweave_fixture_schema_version", + "storageMigration", + ): + with self.subTest(expected=expected): + self.assertIn(expected, worker) + self.assertIn("originweaveStorageMigration", content) + + def test_runner_requires_updated_version_and_migrated_state(self) -> None: + """A restart alone must not satisfy update/version-migration compatibility.""" + + runner = RUNNER.read_text(encoding="utf-8") + for expected in ( + '"extension_version"', + '"storage_migration"', + '"update-migration":', + "UPDATED_EXTENSION_VERSION", + ): + with self.subTest(expected=expected): + self.assertIn(expected, runner) + + +if __name__ == "__main__": + unittest.main() From 39285d33e40fa5f20ad431da5b1203ddcca8051d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 06:24:20 +0900 Subject: [PATCH 2/5] feat(mv3): expose versioned storage migration state --- tests/fixtures/mv3_basic/service_worker.js | 57 ++++++++++++++++++++-- 1 file changed, 54 insertions(+), 3 deletions(-) diff --git a/tests/fixtures/mv3_basic/service_worker.js b/tests/fixtures/mv3_basic/service_worker.js index 3b61c3d5..ecaaa1bf 100644 --- a/tests/fixtures/mv3_basic/service_worker.js +++ b/tests/fixtures/mv3_basic/service_worker.js @@ -3,6 +3,10 @@ const DOWNLOAD_PAYLOAD = "OriginWeave deterministic MV3 download fixture.\n"; const DOWNLOAD_POLL_ATTEMPTS = 100; const DOWNLOAD_POLL_INTERVAL_MS = 50; +const INITIAL_FIXTURE_VERSION = "1.0.0"; +const UPDATED_FIXTURE_VERSION = "1.0.1"; +const INITIAL_SCHEMA_VERSION = 1; +const UPDATED_SCHEMA_VERSION = 2; const workerStartPromise = (async () => { const values = await chrome.storage.local.get("originweave_worker_start_count"); @@ -20,6 +24,39 @@ async function ensureWorkerState() { return "installed"; } +async function ensureStorageMigrationState() { + const extensionVersion = chrome.runtime.getManifest().version; + const values = await chrome.storage.local.get("originweave_fixture_schema_version"); + const currentSchemaVersion = values.originweave_fixture_schema_version; + + if (extensionVersion === INITIAL_FIXTURE_VERSION) { + if (currentSchemaVersion === undefined) { + await chrome.storage.local.set({ + originweave_fixture_schema_version: INITIAL_SCHEMA_VERSION, + }); + return { extensionVersion, storageMigration: "initialized" }; + } + if (currentSchemaVersion === INITIAL_SCHEMA_VERSION) { + return { extensionVersion, storageMigration: "current" }; + } + return { extensionVersion, storageMigration: "invalid" }; + } + + if (extensionVersion === UPDATED_FIXTURE_VERSION) { + if (currentSchemaVersion === INITIAL_SCHEMA_VERSION) { + await chrome.storage.local.set({ + originweave_fixture_schema_version: UPDATED_SCHEMA_VERSION, + }); + return { extensionVersion, storageMigration: "migrated" }; + } + if (currentSchemaVersion === UPDATED_SCHEMA_VERSION) { + return { extensionVersion, storageMigration: "migrated" }; + } + } + + return { extensionVersion, storageMigration: "invalid" }; +} + async function waitForDownload(downloadId, expectedUrl) { const expectedBytes = new TextEncoder().encode(DOWNLOAD_PAYLOAD).byteLength; let observedDownload = false; @@ -270,15 +307,29 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { if (message !== "originweave-ping") { return false; } - Promise.all([ensureWorkerState(), workerStartPromise, exerciseCoreApis(sender)]).then( - ([worker, workerStartCount, coreApis]) => { - sendResponse({ reply: "pong", worker, workerStartCount, ...coreApis }); + Promise.all([ + ensureWorkerState(), + workerStartPromise, + ensureStorageMigrationState(), + exerciseCoreApis(sender), + ]).then( + ([worker, workerStartCount, migrationState, coreApis]) => { + sendResponse({ + reply: "pong", + worker, + workerStartCount, + extensionVersion: migrationState.extensionVersion, + storageMigration: migrationState.storageMigration, + ...coreApis, + }); }, () => { sendResponse({ reply: "pong", worker: "installed", workerStartCount: 0, + extensionVersion: "missing", + storageMigration: "invalid", tabs: "missing", windows: "missing", scripting: "missing", From 371194f8ecca6c7efbbefd04944ad6f4c952e1f7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 06:24:36 +0900 Subject: [PATCH 3/5] feat(mv3): surface update migration evidence --- tests/fixtures/mv3_basic/content_script.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/fixtures/mv3_basic/content_script.js b/tests/fixtures/mv3_basic/content_script.js index b70d1a27..1056d369 100644 --- a/tests/fixtures/mv3_basic/content_script.js +++ b/tests/fixtures/mv3_basic/content_script.js @@ -20,6 +20,10 @@ document.documentElement.dataset.originweaveWorkerStartCount = String( response?.workerStartCount ?? "missing" ); + document.documentElement.dataset.originweaveExtensionVersion = + response?.extensionVersion ?? "missing"; + document.documentElement.dataset.originweaveStorageMigration = + response?.storageMigration ?? "missing"; document.documentElement.dataset.originweaveTabs = response?.tabs ?? "missing"; document.documentElement.dataset.originweaveWindows = response?.windows ?? "missing"; document.documentElement.dataset.originweaveScripting = response?.scripting ?? "missing"; From 722de054ce8295399e8b5c5f8b908467f8ae4508 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 06:27:13 +0900 Subject: [PATCH 4/5] feat(mv3): prove trial-local update migration --- scripts/ci/run_mv3_compatibility.py | 138 +++++++++++++++++++++++++--- 1 file changed, 124 insertions(+), 14 deletions(-) diff --git a/scripts/ci/run_mv3_compatibility.py b/scripts/ci/run_mv3_compatibility.py index 4cb3c732..4ae24fec 100755 --- a/scripts/ci/run_mv3_compatibility.py +++ b/scripts/ci/run_mv3_compatibility.py @@ -5,8 +5,8 @@ W3C WebDriver HTTP protocol only to prove that a real Chrome for Testing build can load the controlled MV3 fixture and repeatedly exercise service-worker, content-script, storage, declarative-net-request, tabs, windows, scripting, -commands, side-panel, bookmarks, history, downloads, real browser-click, and -restart-persistence behavior. +commands, side-panel, bookmarks, history, downloads, real browser-click, +restart-persistence, and controlled extension update-migration behavior. """ from __future__ import annotations @@ -17,6 +17,7 @@ import json import os import pathlib +import shutil import socket import string import subprocess @@ -29,6 +30,8 @@ FIXTURE = ROOT / "tests" / "fixtures" / "mv3_basic" PINNED_CHROME_VERSION = "150.0.7871.129" PINNED_CHROME_REVISION = "r1639810" +INITIAL_EXTENSION_VERSION = "1.0.0" +UPDATED_EXTENSION_VERSION = "1.0.1" REPEATABILITY_TRIALS = 3 REQUEST_TIMEOUT_SECONDS = 5.0 STARTUP_TIMEOUT_SECONDS = 20.0 @@ -43,6 +46,8 @@ "workerReply", "workerState", "workerStartCount", + "extensionVersion", + "storageMigration", "dnr", "tabs", "windows", @@ -56,7 +61,18 @@ "downloadsDiagnostic", ) SURFACE_EVIDENCE_VALUES = frozenset( - {"ready", "missing", "initialized", "persisted", "pong", "installed", "blocked"} + { + "ready", + "missing", + "initialized", + "persisted", + "current", + "migrated", + "invalid", + "pong", + "installed", + "blocked", + } ) DOWNLOAD_DIAGNOSTIC_VALUES = frozenset( { @@ -72,6 +88,9 @@ "download-not-evaluated", } ) +EXTENSION_VERSION_EVIDENCE_VALUES = frozenset( + {INITIAL_EXTENSION_VERSION, UPDATED_EXTENSION_VERSION, "missing"} +) class CompatibilitySurfaceError(RuntimeError): @@ -100,6 +119,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 == "extensionVersion": + return value if value in EXTENSION_VERSION_EVIDENCE_VALUES else "unexpected" return value if value in SURFACE_EVIDENCE_VALUES else "unexpected" @@ -226,11 +247,20 @@ def _wait_for_extension_evidence( driver_port: int, session_id: str, expected_storage_persistence: str, + expected_extension_version: str, + expected_storage_migration: str, ) -> dict[str, str]: """Wait until every controlled MV3 fixture surface reports its expected result.""" if expected_storage_persistence not in {"initialized", "persisted"}: raise ValueError("invalid storage persistence expectation") + if expected_extension_version not in { + INITIAL_EXTENSION_VERSION, + UPDATED_EXTENSION_VERSION, + }: + raise ValueError("invalid extension version expectation") + if expected_storage_migration not in {"initialized", "current", "migrated"}: + raise ValueError("invalid storage migration expectation") script = """ return { content: document.documentElement.dataset.originweaveContentScript || "missing", @@ -241,6 +271,10 @@ def _wait_for_extension_evidence( workerState: document.documentElement.dataset.originweaveWorkerState || "missing", workerStartCount: document.documentElement.dataset.originweaveWorkerStartCount || "missing", + extensionVersion: + document.documentElement.dataset.originweaveExtensionVersion || "missing", + storageMigration: + document.documentElement.dataset.originweaveStorageMigration || "missing", dnr: document.documentElement.dataset.originweaveDnr || "missing", tabs: document.documentElement.dataset.originweaveTabs || "missing", windows: document.documentElement.dataset.originweaveWindows || "missing", @@ -262,6 +296,8 @@ def _wait_for_extension_evidence( "storagePersistence": expected_storage_persistence, "workerReply": "pong", "workerState": "installed", + "extensionVersion": expected_extension_version, + "storageMigration": expected_storage_migration, "dnr": "blocked", "tabs": "ready", "windows": "ready", @@ -332,18 +368,47 @@ def _exercise_real_click(driver_port: int, session_id: str) -> str: return str(text) +def _set_fixture_version(extension_dir: pathlib.Path, version: str) -> None: + """Set one controlled version only inside a trial-local extension copy.""" + + if version not in {INITIAL_EXTENSION_VERSION, UPDATED_EXTENSION_VERSION}: + raise ValueError("unsupported fixture extension version") + resolved_extension_dir = extension_dir.resolve() + if resolved_extension_dir == FIXTURE.resolve(): + raise RuntimeError("refusing to mutate the checked-in MV3 fixture") + manifest_path = resolved_extension_dir / "manifest.json" + manifest = json.loads(manifest_path.read_text(encoding="utf-8")) + if not isinstance(manifest, dict): + raise RuntimeError("MV3 fixture manifest must be a JSON object") + current_version = manifest.get("version") + if current_version not in {INITIAL_EXTENSION_VERSION, UPDATED_EXTENSION_VERSION}: + raise RuntimeError("MV3 fixture manifest has an unexpected version") + if version == INITIAL_EXTENSION_VERSION and current_version != INITIAL_EXTENSION_VERSION: + raise RuntimeError("cannot rewind the trial-local extension version") + if version == UPDATED_EXTENSION_VERSION and current_version != INITIAL_EXTENSION_VERSION: + raise RuntimeError("extension update must start from the initial version") + manifest["version"] = version + manifest_path.write_text( + json.dumps(manifest, indent=2, ensure_ascii=False) + "\n", + encoding="utf-8", + ) + + def _run_browser_pass( chrome_bin: pathlib.Path, chromedriver_bin: pathlib.Path, fixture_url: str, - profile_dir: str, + profile_dir: pathlib.Path, + extension_dir: pathlib.Path, expected_storage_persistence: str, + expected_extension_version: str, + expected_storage_migration: str, ) -> dict[str, Any]: """Run one fresh browser process against a shared bounded compatibility profile.""" driver_port = _free_loopback_port() session_id: str | None = None - download_dir = pathlib.Path(profile_dir) / "downloads" + download_dir = profile_dir / "downloads" download_dir.mkdir(mode=0o700, parents=True, exist_ok=True) driver = subprocess.Popen( [str(chromedriver_bin), f"--port={driver_port}", "--allowed-ips=127.0.0.1"], @@ -372,8 +437,8 @@ def _run_browser_pass( "--disable-dev-shm-usage", "--no-sandbox", f"--user-data-dir={profile_dir}", - f"--disable-extensions-except={FIXTURE}", - f"--load-extension={FIXTURE}", + f"--disable-extensions-except={extension_dir}", + f"--load-extension={extension_dir}", ], "prefs": { "download.default_directory": str(download_dir), @@ -411,6 +476,8 @@ def _run_browser_pass( driver_port, session_id, expected_storage_persistence, + expected_extension_version, + expected_storage_migration, ) click_result = _exercise_real_click(driver_port, session_id) worker_start_count = int(surfaces["workerStartCount"]) @@ -418,6 +485,8 @@ def _run_browser_pass( "browser_version": browser_version, "worker_start_count": worker_start_count, "storage_persistence": surfaces["storagePersistence"], + "extension_version": surfaces["extensionVersion"], + "storage_migration": surfaces["storageMigration"], "surfaces": { "service-worker": surfaces["workerReply"] == "pong", "content-script": surfaces["content"] == "ready", @@ -458,17 +527,26 @@ def _run_restart_trial( fixture_url: str, trial_number: int, ) -> dict[str, Any]: - """Run one independent initial/restart pair and return credential-free evidence.""" + """Run one independent initial/restart/update-migration trial.""" trial_started = time.monotonic() with tempfile.TemporaryDirectory( prefix=f"originweave-mv3-trial-{trial_number}-" - ) as profile_dir: + ) as trial_root: + trial_dir = pathlib.Path(trial_root) + profile_dir = trial_dir / "profile" + extension_dir = trial_dir / "extension" + shutil.copytree(FIXTURE, extension_dir) + _set_fixture_version(extension_dir, INITIAL_EXTENSION_VERSION) + initial = _run_browser_pass( chrome_bin, chromedriver_bin, fixture_url, profile_dir, + extension_dir, + "initialized", + INITIAL_EXTENSION_VERSION, "initialized", ) restarted = _run_browser_pass( @@ -476,20 +554,41 @@ def _run_restart_trial( chromedriver_bin, fixture_url, profile_dir, + extension_dir, "persisted", + INITIAL_EXTENSION_VERSION, + "current", + ) + + _set_fixture_version(extension_dir, UPDATED_EXTENSION_VERSION) + updated = _run_browser_pass( + chrome_bin, + chromedriver_bin, + fixture_url, + profile_dir, + extension_dir, + "persisted", + UPDATED_EXTENSION_VERSION, + "migrated", ) initial_count = int(initial["worker_start_count"]) restarted_count = int(restarted["worker_start_count"]) + updated_count = int(updated["worker_start_count"]) surfaces = { - name: bool(initial["surfaces"][name]) and bool(restarted["surfaces"][name]) + name: bool(initial["surfaces"][name]) + and bool(restarted["surfaces"][name]) + and bool(updated["surfaces"][name]) for name in initial["surfaces"] } surfaces.update( { "restart-persistence": restarted["storage_persistence"] == "persisted", - "worker-start-count": restarted_count > initial_count, - "storage-persistence": restarted["storage_persistence"] == "persisted", + "worker-start-count": restarted_count > initial_count + and updated_count > restarted_count, + "storage-persistence": updated["storage_persistence"] == "persisted", + "update-migration": updated["extension_version"] == UPDATED_EXTENSION_VERSION + and updated["storage_migration"] == "migrated", } ) if not all(surfaces.values()): @@ -498,18 +597,29 @@ def _run_restart_trial( return { "trial_number": trial_number, "passed": True, - "browser_version": restarted["browser_version"], + "browser_version": updated["browser_version"], "surfaces": surfaces, "browser_passes": [ { "phase": "initial", "worker_start_count": initial_count, "storage_persistence": initial["storage_persistence"], + "extension_version": initial["extension_version"], + "storage_migration": initial["storage_migration"], }, { "phase": "restart", "worker_start_count": restarted_count, "storage_persistence": restarted["storage_persistence"], + "extension_version": restarted["extension_version"], + "storage_migration": restarted["storage_migration"], + }, + { + "phase": "update-migration", + "worker_start_count": updated_count, + "storage_persistence": updated["storage_persistence"], + "extension_version": updated["extension_version"], + "storage_migration": updated["storage_migration"], }, ], "duration_ms": round((time.monotonic() - trial_started) * 1000), @@ -517,7 +627,7 @@ def _run_restart_trial( def main() -> int: - """Run three independent restart trials and emit bounded repeatability evidence.""" + """Run three independent restart/update trials and emit bounded repeatability evidence.""" chrome_bin = pathlib.Path(os.environ.get("CHROME_BIN", "")) chromedriver_bin = pathlib.Path(os.environ.get("CHROMEDRIVER_BIN", "")) From e696e19c9eaf3dedb104a5de4bdbd7970abf90d4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 06:27:50 +0900 Subject: [PATCH 5/5] docs(changelog): record update migration compatibility --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 039b5e62..119e20f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ All notable changes to OriginWeave are documented in this file. The format follo - Purpose-bound data-governance and privacy baseline that rejects both blanket masking and ambient raw-value propagation, defines field-scoped just-in-time disclosure, opaque-handle/trusted-broker boundaries, model/provider/region policy, retention/deletion/residency/break-glass controls, truthful CSAP/SOC 2 readiness language, and machine-checkable documentation contracts without inventing an OriginWeave-owned production database. - Proposed product-wide target-architecture ADRs for the Rust control plane, isolated execution modes, typed actions, semantic observation/stale-node authority, prompt-injection and secret separation, resource-governor priority, provenance evidence, browser/protocol adapters, crawler policy, and hourly automation operational closure; these remain Proposed rather than shipped claims until protected review and merge. - Pinned real-Chromium Manifest V3 compatibility evidence for a controlled history add/read/delete lifecycle confined to the ephemeral loopback fixture profile, with cleanup verification and no OriginWeave Agent history-authority claim. +- Pinned real-Chromium Manifest V3 update-migration evidence using a trial-local unpacked-extension copy, a controlled `1.0.0` to `1.0.1` transition, persisted profile state, and explicit schema migration without modifying the checked-in fixture or granting Agent authority. ### Changed