"""Safe tenant-script upgrade tests (no Docker, no network). The rule under test: only a PROVEN-unmodified tenant script (manifest match, known-shipped-hash match, missing, or already current) is auto-merged. A tenant-modified script is NEVER overwritten: it stays byte-identical, is reported as skipped, and gets exactly one deduped audit record. Reruns are idempotent. No real credentials appear here. """ import hashlib import json import os import sys import tempfile import unittest sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) import playwright_browser as pb # noqa: E402 from playwright_browser import ensure_codex_home_playwright # noqa: E402 NAMES = pb.PLAYWRIGHT_SCRIPT_NAMES def _write_scripts(d, contents): for name in NAMES: with open(os.path.join(d, name), "w", encoding="utf-8") as fh: fh.write(contents[name]) def _read_scripts(d): out = {} for name in NAMES: with open(os.path.join(d, name), encoding="utf-8") as fh: out[name] = fh.read() return out def _v1(): return {name: "# v1 %s\nprint('v1')\n" % name for name in NAMES} def _v2(): return {name: "# v2 %s\nprint('v2')\n" % name for name in NAMES} class UpgradeTest(unittest.TestCase): def setUp(self): self._tmp = tempfile.TemporaryDirectory() self.src_v1 = os.path.join(self._tmp.name, "src-v1") self.src_v2 = os.path.join(self._tmp.name, "src-v2") self.home = os.path.join(self._tmp.name, "home") os.makedirs(self.src_v1) os.makedirs(self.src_v2) os.makedirs(self.home) _write_scripts(self.src_v1, _v1()) _write_scripts(self.src_v2, _v2()) def tearDown(self): self._tmp.cleanup() def _audit_records(self): path = os.path.join(self.home, "playwright_scripts_audit.jsonl") if not os.path.exists(path): return [] with open(path, encoding="utf-8") as fh: return [json.loads(line) for line in fh if line.strip()] def test_fresh_seed_and_idempotent_rerun(self): out = ensure_codex_home_playwright(self.home, script_dir=self.src_v2) self.assertEqual(_read_scripts(self.home), _v2()) self.assertFalse(out["script_skipped_modified"]) self.assertEqual(self._audit_records(), []) # Config section appended exactly once. with open(os.path.join(self.home, "config.toml"), encoding="utf-8") as fh: config = fh.read() self.assertEqual(config.count("[mcp_servers.playwright]"), 1) out2 = ensure_codex_home_playwright(self.home, script_dir=self.src_v2) self.assertFalse(out2["script_changed"]) self.assertFalse(out2["config_changed"]) self.assertFalse(out2["script_refreshed"]) self.assertEqual(_read_scripts(self.home), _v2()) with open(os.path.join(self.home, "config.toml"), encoding="utf-8") as fh: self.assertEqual( fh.read().count("[mcp_servers.playwright]"), 1) def test_unmodified_script_auto_merges(self): ensure_codex_home_playwright(self.home, script_dir=self.src_v1) out = ensure_codex_home_playwright(self.home, script_dir=self.src_v2) self.assertEqual(set(out["script_refreshed"]), set(NAMES)) self.assertFalse(out["script_skipped_modified"]) self.assertEqual(_read_scripts(self.home), _v2()) manifest_path = os.path.join( self.home, "playwright_scripts_manifest.json") with open(manifest_path, encoding="utf-8") as fh: manifest = json.load(fh) for name in NAMES: self.assertEqual( manifest[name], hashlib.sha256(_v2()[name].encode()).hexdigest()) def test_modified_script_untouched_with_audit(self): ensure_codex_home_playwright(self.home, script_dir=self.src_v1) tenant_path = os.path.join(self.home, NAMES[0]) with open(tenant_path, "a", encoding="utf-8") as fh: fh.write("# tenant tweak: custom timeout\n") with open(tenant_path, encoding="utf-8") as fh: tenant_bytes = fh.read() out = ensure_codex_home_playwright(self.home, script_dir=self.src_v2) self.assertEqual(out["script_skipped_modified"], [NAMES[0]]) self.assertEqual(out["script_differs"], [NAMES[0]]) # Modified file byte-identical; the other two auto-merged. with open(tenant_path, encoding="utf-8") as fh: self.assertEqual(fh.read(), tenant_bytes) rest = _read_scripts(self.home) for name in NAMES[1:]: self.assertEqual(rest[name], _v2()[name]) records = self._audit_records() self.assertEqual(len(records), 1) self.assertEqual(records[0]["action"], "skipped_modified") self.assertEqual(records[0]["script"], NAMES[0]) self.assertEqual( records[0]["dst_sha256"], hashlib.sha256(tenant_bytes.encode()).hexdigest()) def test_modified_rerun_does_not_rewrite_or_duplicate_audit(self): ensure_codex_home_playwright(self.home, script_dir=self.src_v1) tenant_path = os.path.join(self.home, NAMES[0]) with open(tenant_path, "a", encoding="utf-8") as fh: fh.write("# tenant tweak\n") with open(tenant_path, encoding="utf-8") as fh: tenant_bytes = fh.read() ensure_codex_home_playwright(self.home, script_dir=self.src_v2) out = ensure_codex_home_playwright(self.home, script_dir=self.src_v2) self.assertFalse(out["script_changed"]) self.assertEqual(out["script_skipped_modified"], [NAMES[0]]) self.assertEqual(len(self._audit_records()), 1) with open(tenant_path, encoding="utf-8") as fh: self.assertEqual(fh.read(), tenant_bytes) # The manifest must NOT have learned the tenant hash: # otherwise the next upgrade would mistake the modified # file for untouched and overwrite it. with open(os.path.join( self.home, "playwright_scripts_manifest.json"), encoding="utf-8") as fh: manifest = json.load(fh) self.assertNotEqual( manifest.get(NAMES[0]), hashlib.sha256(tenant_bytes.encode()).hexdigest()) def test_legacy_no_manifest_known_hash_merges(self): # Legacy tenant: files seeded before manifests existed, # content exactly a known shipped version -> proven # unmodified, safe to merge even with no manifest. old = {name: "# legacy %s\n" % name for name in NAMES} _write_scripts(self.home, old) patched = {name: hashlib.sha256(old[name].encode()).hexdigest() for name in NAMES} saved = pb.KNOWN_SHIPPED_SCRIPT_HASHES pb.KNOWN_SHIPPED_SCRIPT_HASHES = patched try: out = ensure_codex_home_playwright(self.home, script_dir=self.src_v2) finally: pb.KNOWN_SHIPPED_SCRIPT_HASHES = saved self.assertEqual(set(out["script_refreshed"]), set(NAMES)) self.assertEqual(_read_scripts(self.home), _v2()) def test_legacy_no_manifest_unknown_hash_skips(self): old = {name: "# tenant-custom %s\n" % name for name in NAMES} _write_scripts(self.home, old) saved = pb.KNOWN_SHIPPED_SCRIPT_HASHES pb.KNOWN_SHIPPED_SCRIPT_HASHES = {} try: out = ensure_codex_home_playwright(self.home, script_dir=self.src_v2) finally: pb.KNOWN_SHIPPED_SCRIPT_HASHES = saved self.assertEqual(set(out["script_skipped_modified"]), set(NAMES)) self.assertEqual(_read_scripts(self.home), old) self.assertEqual(len(self._audit_records()), 3) def test_upgrade_never_touches_tenant_data_or_config(self): notes = os.path.join(self.home, "notes.txt") with open(notes, "w", encoding="utf-8") as fh: fh.write("tenant notes, must survive\n") config = os.path.join(self.home, "config.toml") with open(config, "w", encoding="utf-8") as fh: fh.write('[user]\nname = "tenant"\n\n' '[mcp_servers.playwright.env]\n' 'FOO = "bar"\n') ensure_codex_home_playwright(self.home, script_dir=self.src_v2) ensure_codex_home_playwright(self.home, script_dir=self.src_v2) with open(notes, encoding="utf-8") as fh: self.assertEqual(fh.read(), "tenant notes, must survive\n") with open(config, encoding="utf-8") as fh: text = fh.read() # Existing sections intact, playwright section not # duplicated (it was absent, so appended exactly once). self.assertIn('[user]\nname = "tenant"', text) self.assertIn('FOO = "bar"', text) self.assertEqual(text.count("[mcp_servers.playwright]"), 1) def test_known_shipped_hashes_are_well_formed(self): self.assertEqual(set(pb.KNOWN_SHIPPED_SCRIPT_HASHES), set(NAMES)) for value in pb.KNOWN_SHIPPED_SCRIPT_HASHES.values(): self.assertEqual(len(value), 64) int(value, 16) # must be hex