Skip to content

Commit f8d9d36

Browse files
committed
Test that Nuvio runtime instrumenters stay disabled
1 parent f179302 commit f8d9d36

1 file changed

Lines changed: 75 additions & 126 deletions

File tree

Lines changed: 75 additions & 126 deletions
Original file line numberDiff line numberDiff line change
@@ -1,146 +1,95 @@
11
#!/usr/bin/env python3
2+
"""Regression guard: native runtime instrumentation entry points stay non-mutating."""
23
from __future__ import annotations
34

4-
import importlib.util
5+
import json
56
import subprocess
67
import sys
78
import tempfile
89
from pathlib import Path
910

1011
ROOT = Path(__file__).resolve().parents[1]
11-
MODULE = ROOT / "scripts/instrument_native_repository_http_evidence.py"
12-
spec = importlib.util.spec_from_file_location("repository_http_evidence", MODULE)
13-
assert spec and spec.loader
14-
mod = importlib.util.module_from_spec(spec)
15-
spec.loader.exec_module(mod)
12+
POLICY = json.loads((ROOT / "automation/native-human-ux-policy.json").read_text(encoding="utf-8"))
13+
CLIENT = ROOT / "scripts/instrument_native_client_evidence.py"
14+
REPOSITORY = ROOT / "scripts/instrument_native_repository_http_evidence.py"
15+
DESKTOP = ROOT / "scripts/instrument_native_desktop_evidence.py"
1616

17+
assert "patch Nuvio source code for logging or instrumentation" in POLICY["forbidden_behaviors"]
18+
assert "patch Nuvio repository/network loaders to inject evidence interceptors" in POLICY["forbidden_behaviors"]
1719

18-
def write(root: Path, relative: str, content: str) -> Path:
19-
target = root / relative
20-
target.parent.mkdir(parents=True, exist_ok=True)
21-
target.write_text(content, encoding="utf-8")
22-
return target
20+
for path in (CLIENT, REPOSITORY, DESKTOP):
21+
text = path.read_text(encoding="utf-8")
22+
assert "disabled_by_human_ux_policy" in text, path.name
23+
assert "runtime_mutation=false" in text, path.name
24+
for forbidden in (
25+
"write_text(",
26+
"write_bytes(",
27+
"replace_once(",
28+
"addInterceptor",
29+
"PluginRuntime.kt",
30+
"FetchBridge.kt",
31+
"AndroidManifest.xml",
32+
"android.permission.INTERNET",
33+
"usesCleartextTraffic",
34+
"networkSecurityConfig",
35+
):
36+
assert forbidden not in text, f"{path.name}:{forbidden}"
2337

2438

25-
with tempfile.TemporaryDirectory() as tmp_raw:
26-
tmp = Path(tmp_raw)
27-
28-
tv = tmp / "tv"
29-
tv_path = write(
30-
tv,
31-
"app/src/full/java/com/nuvio/tv/core/plugin/PluginManager.kt",
32-
"private val httpClient = OkHttpClient.Builder()\n"
33-
" .dns(com.nuvio.tv.core.network.IPv4FirstDns())\n"
34-
" .connectTimeout(30, TimeUnit.SECONDS)\n"
35-
" .readTimeout(30, TimeUnit.SECONDS)\n"
36-
" .build()\n",
37-
)
38-
mod.instrument_tv(tv)
39-
tv_out = tv_path.read_text(encoding="utf-8")
40-
assert "FIELD_NATIVE_REPOSITORY_HTTP_REQUEST client=tv" in tv_out
41-
assert ".connectTimeout(30, TimeUnit.SECONDS)" in tv_out
42-
assert ".readTimeout(30, TimeUnit.SECONDS)" in tv_out
43-
44-
mobile = tmp / "mobile"
45-
mobile_path = write(
46-
mobile,
47-
"composeApp/src/androidMain/kotlin/com/nuvio/app/features/addons/AddonPlatform.android.kt",
48-
"private fun client() = OkHttpClient.Builder()\n"
49-
" .addInterceptor(SentryNetworkBreadcrumbInterceptor())\n"
50-
" .proxy(Proxy.NO_PROXY)\n"
51-
" .build()\n",
39+
def init_repo(path: Path) -> None:
40+
subprocess.run(["git", "init", "-q", str(path)], check=True)
41+
marker = path / "production-runtime.txt"
42+
marker.write_text("official runtime\n", encoding="utf-8")
43+
subprocess.run(["git", "-C", str(path), "add", "production-runtime.txt"], check=True)
44+
subprocess.run(
45+
[
46+
"git",
47+
"-C",
48+
str(path),
49+
"-c",
50+
"user.email=native-lab@example.invalid",
51+
"-c",
52+
"user.name=Native Lab",
53+
"commit",
54+
"-qm",
55+
"baseline",
56+
],
57+
check=True,
5258
)
53-
mod.instrument_mobile(mobile)
54-
mobile_out = mobile_path.read_text(encoding="utf-8")
55-
assert "FIELD_NATIVE_REPOSITORY_HTTP_REQUEST client=mobile" in mobile_out
56-
assert ".proxy(Proxy.NO_PROXY)" in mobile_out
5759

58-
desktop = tmp / "desktop"
59-
desktop_path = write(
60-
desktop,
61-
"composeApp/src/desktopMain/kotlin/com/nuvio/app/features/addons/AddonPlatform.desktop.kt",
62-
"private val desktopHttpClient = OkHttpClient.Builder()\n"
63-
" .followRedirects(true)\n"
64-
" .followSslRedirects(true)\n"
65-
" .build()\n",
66-
)
67-
mod.instrument_desktop(desktop)
68-
desktop_out = desktop_path.read_text(encoding="utf-8")
69-
assert "FIELD_NATIVE_REPOSITORY_HTTP_REQUEST client=desktop" in desktop_out
70-
assert "desktop-native-http-evidence.log" in desktop_out
71-
assert "println(requestMessage)" not in desktop_out
72-
assert "println(responseMessage)" not in desktop_out
73-
assert "println(errorMessage)" not in desktop_out
74-
75-
for client_out in (tv_out, mobile_out, desktop_out):
76-
for required in (
77-
"rawGithubEvidence",
78-
"loopbackEvidence",
79-
'setOf("127.0.0.1", "localhost", "10.0.2.2")',
80-
'Regex("/candidate-[0-9a-f]{32}/")',
81-
"FIELD_NATIVE_REPOSITORY_HTTP_RESPONSE",
82-
"FIELD_NATIVE_REPOSITORY_HTTP_ERROR",
83-
"request_header_names=$requestHeaderNames",
84-
"response_header_names=$responseHeaderNames",
85-
"source=$cacheSource",
86-
):
87-
assert required in client_out, required
88-
# Evidence must not persist query strings, request/response bodies or
89-
# credential/header values. Only endpoint sans query and header names exist.
90-
assert "authorization=" not in client_out.lower()
91-
assert "cookie=" not in client_out.lower()
92-
assert "query=" not in client_out.lower()
9360

94-
# Execute the provider-runtime Desktop code generator against its exact anchors.
95-
# This catches Python/Kotlin escaping regressions before macOS/Windows runners.
96-
desktop_runtime = tmp / "desktop-runtime"
97-
runtime_path = write(
98-
desktop_runtime,
99-
"composeApp/src/fullCommonMain/kotlin/com/nuvio/app/features/plugins/runtime/PluginRuntime.kt",
100-
"fun install() { addModule(FetchBridge()) }\n",
101-
)
102-
bridge_path = write(
103-
desktop_runtime,
104-
"composeApp/src/fullCommonMain/kotlin/com/nuvio/app/features/plugins/runtime/network/FetchBridge.kt",
105-
'''internal class FetchBridge : HostModule {\n'''
106-
''' fun fetch() {\n'''
107-
''' } catch (t: Throwable) {\n'''
108-
''' log.e(t) { "Fetch bridge error for $method $url" }\n'''
109-
''' val headers = parseHeaders(headersJson).toMutableMap()\n'''
110-
''' if (!headers.containsKey("User-Agent")) {\n'''
111-
''' headers["User-Agent"] = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"\n'''
112-
''' }\n\n'''
113-
''' val response = httpRequestRaw(\n'''
114-
''' val responseHeaders = response.headers.mapKeys { (key, _) -> key.lowercase() }\n'''
115-
''' .mapValues { (_, value) -> truncateString(value, MAX_FETCH_HEADER_VALUE_CHARS) }\n'''
116-
''' val result = JsonObject(\n'''
117-
''' }\n}\n''',
118-
)
119-
generated = subprocess.run(
120-
[sys.executable, str(ROOT / "scripts/instrument_native_desktop_evidence.py"), str(desktop_runtime)],
121-
cwd=ROOT,
61+
def assert_clean(path: Path) -> None:
62+
status = subprocess.run(
63+
["git", "-C", str(path), "status", "--porcelain=v1", "--untracked-files=all"],
64+
check=True,
12265
text=True,
12366
capture_output=True,
124-
)
125-
assert generated.returncode == 0, generated.stdout + generated.stderr
126-
runtime_out = runtime_path.read_text(encoding="utf-8")
127-
bridge_out = bridge_path.read_text(encoding="utf-8")
128-
assert "FetchBridge(scraperId, mediaType)" in runtime_out
129-
assert "FIELD_NATIVE_HTTP_REQUEST client=desktop" in bridge_out
130-
assert "FIELD_NATIVE_HTTP_RESPONSE client=desktop" in bridge_out
131-
assert "FIELD_NATIVE_HTTP_ERROR client=desktop" in bridge_out
132-
assert bridge_out.count("desktop-native-http-evidence.log") == 3
133-
assert 'log.i { "FIELD_NATIVE_HTTP_' not in bridge_out
67+
).stdout
68+
assert status.strip() == "", status
69+
13470

135-
# Provider-runtime Desktop evidence uses the same dedicated sanitized channel;
136-
# never depend on Gradle's captured test stdout for Brain input.
137-
desktop_provider_instrumenter = (ROOT / "scripts/instrument_native_desktop_evidence.py").read_text(encoding="utf-8")
138-
desktop_suite = (ROOT / "scripts/run_native_corpus_desktop_suite.sh").read_text(encoding="utf-8")
139-
assert "desktop-native-http-evidence.log" in desktop_provider_instrumenter
140-
assert 'log.i { "FIELD_NATIVE_HTTP_' not in desktop_provider_instrumenter
141-
assert 'HTTP_LOG="${WORKSPACE}/desktop-native-http-evidence.log"' in desktop_suite
142-
assert 'cat "$HTTP_LOG" >> "$LOG"' in desktop_suite
143-
assert 'rm -f "$HTTP_LOG" "$GRADLE_LOG"' in desktop_suite
144-
assert "grep -E 'FIELD_NATIVE_(REPOSITORY_)?HTTP_" not in desktop_suite
71+
with tempfile.TemporaryDirectory() as raw:
72+
root = Path(raw)
73+
tv = root / "tv"
74+
mobile = root / "mobile"
75+
desktop = root / "desktop"
76+
for repo in (tv, mobile, desktop):
77+
repo.mkdir()
78+
init_repo(repo)
79+
80+
calls = (
81+
([sys.executable, str(CLIENT), "tv", str(tv)], tv, "client=tv"),
82+
([sys.executable, str(CLIENT), "mobile", str(mobile)], mobile, "client=mobile"),
83+
([sys.executable, str(REPOSITORY), "tv", str(tv)], tv, "client=tv"),
84+
([sys.executable, str(REPOSITORY), "mobile", str(mobile)], mobile, "client=mobile"),
85+
([sys.executable, str(REPOSITORY), "desktop", str(desktop)], desktop, "client=desktop"),
86+
([sys.executable, str(DESKTOP), str(desktop)], desktop, "client=desktop"),
87+
)
88+
for command, repo, marker in calls:
89+
completed = subprocess.run(command, cwd=ROOT, text=True, capture_output=True)
90+
assert completed.returncode == 0, completed.stdout + completed.stderr
91+
assert "disabled_by_human_ux_policy" in completed.stdout
92+
assert marker in completed.stdout
93+
assert_clean(repo)
14594

146-
print("native repository HTTP instrumentation tests passed")
95+
print("native runtime instrumentation shims are policy-locked and non-mutating")

0 commit comments

Comments
 (0)