- click/click_ref: pre-click allowlist gate on resolvable anchor href
(same check as navigate; miss refuses before the click executes);
click/click_ref/click_at: post-click landing URL check.
click_at has no pre-click gate by design (coordinates name no
element); JS-driven navigation, evaluate, meta refresh and popups
are documented as unintercepable with the Tinyproxy 403 backstop.
- Tenant script upgrade is now safe: only proven-unmodified scripts
(manifest match, 2a569ac known-shipped SHA-256, missing, or
current) auto-merge; tenant-modified scripts are never overwritten,
are reported as skipped, and get one deduped audit record. The
manifest never learns a tenant hash (else the next upgrade would
mistake a modified file for untouched). Idempotent reruns.
- Activation candidate = deployed Tinyproxy programming set encoded
with leading-dot entries (plain entries would regress
docs.python.org); npmjs.org per the deployed filter, not npmjs.com.
Host activation itself is NOT performed this round: only the plan,
validation tests and rollback are delivered.
Tests: 28 new (click 13, upgrade 8, activation 7), all fake/no-network;
98 browser-related tests pass including prior suites.
(cherry picked from commit e4a93f5748ec815f163d0bc096ff4dc4b8c651f5)
217 lines
9.3 KiB
Python
217 lines
9.3 KiB
Python
"""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
|