This commit is contained in:
eric
2026-05-19 09:19:52 +00:00
parent 585bcd9500
commit b617555360
30 changed files with 477 additions and 207 deletions

View File

@@ -4,6 +4,7 @@ import base64
import importlib.util
import json
import os
import subprocess
import tempfile
import unittest
from pathlib import Path
@@ -87,7 +88,6 @@ class NekoConfigTests(unittest.TestCase):
self.assertIn("./external-extensions:/usr/share/chromium/extensions:ro", compose)
self.assertIn("./external-extensions:/usr/share/chromium-browser/extensions:ro", compose)
self.assertIn("./tampermonkey-seed:/etc/neko/tampermonkey-seed:ro", compose)
self.assertIn("./youtube-auto-dismiss-extension:/etc/neko/youtube-auto-dismiss-extension:ro", compose)
self.assertNotIn("./policies:/etc/chromium/policies/managed", compose)
self.assertNotIn("./policies:/etc/opt/chrome/policies/managed", compose)
self.assertIn("container_name: live-neko-1", compose)
@@ -114,6 +114,11 @@ class NekoConfigTests(unittest.TestCase):
{
"NEKO_IMAGE": "test-image",
"NEKO_PROFILE_HOST_DIR": f"{tmp}/profile",
"NEKO_HTTP_PORT_4": "9404",
"NEKO_WEBRTC_TCPMUX_4": "54304",
"NEKO_WEBRTC_UDPMUX_4": "54305",
"NEKO_WEBRTC_UDP_RANGE_4": "54400-54499",
"NEKO_PROFILE_HOST_DIR_4": f"{tmp}/custom-profile-4",
"NEKO_BROWSER_SUPERVISOR_CONF": "google-chrome.conf",
},
clear=False,
@@ -121,10 +126,11 @@ class NekoConfigTests(unittest.TestCase):
rendered = module.render(4)
self.assertIn("container_name: live-neko-4", rendered)
self.assertIn('"9203:8080"', rendered)
self.assertIn('"52303:52303/tcp"', rendered)
self.assertIn('"52400-52499:52400-52499/udp"', rendered)
self.assertIn(f'"{tmp}/profile-4:/home/neko/.config/google-chrome"', rendered)
self.assertIn('"9404:8080"', rendered)
self.assertIn('"54304:54304/tcp"', rendered)
self.assertIn('"54305:54305/udp"', rendered)
self.assertIn('"54400-54499:54400-54499/udp"', rendered)
self.assertIn(f'"{tmp}/custom-profile-4:/home/neko/.config/google-chrome"', rendered)
self.assertIn('NEKO_MANAGED_POLICIES: "0"', rendered)
self.assertIn('"./no-managed-policies:/etc/chromium/policies/managed:ro"', rendered)
self.assertIn('"./no-managed-policies:/etc/chromium-browser/policies/managed:ro"', rendered)
@@ -133,7 +139,6 @@ class NekoConfigTests(unittest.TestCase):
self.assertIn('"./external-extensions:/usr/share/chromium/extensions:ro"', rendered)
self.assertIn('"./external-extensions:/usr/share/chromium-browser/extensions:ro"', rendered)
self.assertIn('"./tampermonkey-seed:/etc/neko/tampermonkey-seed:ro"', rendered)
self.assertIn('"./youtube-auto-dismiss-extension:/etc/neko/youtube-auto-dismiss-extension:ro"', rendered)
self.assertNotIn('"./policies:/etc/chromium/policies/managed:ro"', rendered)
self.assertNotIn('"./policies:/etc/opt/chrome/policies/managed:ro"', rendered)
@@ -153,9 +158,14 @@ class NekoConfigTests(unittest.TestCase):
self.assertIn("compose_file=\"$(render_neko_compose_file)\"", script)
self.assertIn("NEKO_MAX_INSTANCE_COUNT:-64", script)
self.assertIn("NEKO_MANAGED_POLICIES", script)
self.assertIn("copy_neko_tampermonkey_seed_dir()", script)
self.assertIn("find_neko_tampermonkey_seed_profile()", script)
self.assertIn("services/neko/tampermonkey-seed", script)
self.assertIn("sync_neko_tampermonkey_preferences()", script)
self.assertIn("should_replace_current", script)
self.assertIn('value.get("from_webstore") is not True', script)
self.assertIn('path.startswith("/")', script)
self.assertIn('value.get("location") == 8', script)
self.assertIn("neko_managed_policies_enabled()", script)
def test_tampermonkey_external_extension_seed_is_available_without_managed_policy(self) -> None:
@@ -188,17 +198,14 @@ class NekoConfigTests(unittest.TestCase):
launch = (ROOT / "services" / "neko" / "browser-launch.sh").read_text(encoding="utf-8")
self.assertIn("ensure_tampermonkey_seed", launch)
self.assertIn("sync_tampermonkey_preferences", launch)
self.assertIn("find_tampermonkey_extension_dir", launch)
self.assertIn("find_youtube_auto_dismiss_extension_dir", launch)
self.assertIn("--load-extension=$load_extension_dirs", launch)
self.assertIn("should_replace_current", launch)
self.assertIn('value.get("from_webstore") is not True', launch)
self.assertIn('path.startswith("/")', launch)
self.assertIn('value.get("location") == 8', launch)
self.assertNotIn("--load-extension=", launch)
self.assertNotIn("youtube-auto-dismiss-extension", launch)
self.assertIn("/etc/neko/tampermonkey-seed", launch)
self.assertIn("rm -rf \"$profile_dir/Default/Managed Extension Settings/$ext_id\"", launch)
dismiss_manifest = ROOT / "services" / "neko" / "youtube-auto-dismiss-extension" / "manifest.json"
self.assertTrue(dismiss_manifest.is_file())
dismiss_script = (ROOT / "services" / "neko" / "youtube-auto-dismiss-extension" / "youtube-studio-auto-dismiss.js").read_text(
encoding="utf-8"
)
self.assertIn('text === "Dismiss"', dismiss_script)
def test_supervisor_uses_browser_launch_script(self) -> None:
chrome = (ROOT / "services" / "neko" / "google-chrome.conf").read_text(encoding="utf-8")
@@ -251,6 +258,46 @@ class NekoConfigTests(unittest.TestCase):
self.assertIn("Local Extension Settings/dhdgffkkebhmkfjojejmpbldmpobfkfo", common)
self.assertIn("NEKO_HTTP_PORT_2", common)
self.assertIn("NEKO_WEBRTC_UDP_RANGE_3", common)
self.assertIn('eval "override=\\"\\${NEKO_PROFILE_HOST_DIR_${idx}:-}\\""', common)
def test_neko_common_profile_helper_honors_instance_override_beyond_three(self) -> None:
script = (
f"ROOT_DIR={ROOT!s}; "
f"source {ROOT / 'scripts' / 'linux' / 'lib' / 'common.sh'}; "
"NEKO_PROFILE_HOST_DIR=/var/lib/live-neko/profile; "
"NEKO_PROFILE_HOST_DIR_4=/srv/live/neko/channel-four; "
"resolve_neko_profile_dir_for_instance 4"
)
proc = subprocess.run(["bash", "-lc", script], capture_output=True, text=True, check=False)
self.assertEqual(proc.returncode, 0, proc.stderr)
self.assertEqual(proc.stdout.strip(), "/srv/live/neko/channel-four")
def test_neko_common_arch_selection_covers_x86_and_arm(self) -> None:
script = (
f"ROOT_DIR={ROOT!s}; "
f"source {ROOT / 'scripts' / 'linux' / 'lib' / 'common.sh'}; "
"NEKO_IMAGE_X86=ghcr.io/m1k1o/neko/google-chrome:3.0.0; "
"NEKO_IMAGE_ARM64=ghcr.io/m1k1o/neko/chromium:3.0.0; "
"uname(){ printf '%s\\n' \"$FAKE_UNAME\"; }; "
"FAKE_UNAME=x86_64; resolve_neko_image; printf ' '; resolve_neko_supervisor_conf; printf '\\n'; "
"FAKE_UNAME=aarch64; resolve_neko_image; printf ' '; resolve_neko_supervisor_conf; printf '\\n'"
)
proc = subprocess.run(["bash", "-lc", script], capture_output=True, text=True, check=False)
self.assertEqual(proc.returncode, 0, proc.stderr)
self.assertEqual(
proc.stdout.splitlines(),
[
"ghcr.io/m1k1o/neko/google-chrome:3.0.0 google-chrome.conf",
"ghcr.io/m1k1o/neko/chromium:3.0.0 chromium.conf",
],
)
def test_neko_install_defaults_match_runtime_screen_profile(self) -> None:
common = (ROOT / "scripts" / "linux" / "lib" / "common.sh").read_text(encoding="utf-8")
service_ctl = (ROOT / "scripts" / "linux" / "service_ctl.sh").read_text(encoding="utf-8")
self.assertIn('NEKO_DESKTOP_SCREEN="${NEKO_DESKTOP_SCREEN:-1280x720@15}"', common)
self.assertIn('NEKO_DESKTOP_SCREEN="${NEKO_DESKTOP_SCREEN:-1280x720@15}"', service_ctl)
self.assertNotIn('NEKO_DESKTOP_SCREEN="${NEKO_DESKTOP_SCREEN:-1920x1080@30}"', common)
def test_neko_ip_watcher_is_installed_and_restarts_on_drift(self) -> None:
common = (ROOT / "scripts" / "linux" / "lib" / "common.sh").read_text(encoding="utf-8")

View File

@@ -89,12 +89,22 @@ class VerifyLocalInstallTests(unittest.TestCase):
def fake_docker_exec(container: str, command: str, *, timeout: float = 8.0):
if "supervisorctl status google-chrome" in command:
return 0, "google-chrome RUNNING pid 8", ""
if "NEKO member env invalid" in command:
return 0, "provider=multiuser admin_password=set user_password=set\n", ""
if "/proc/self/mountinfo" in command:
return 0, "/home/neko/.config/chromium\n/home/neko/.config/google-chrome\n", ""
if "Developer mode preference not found" in command:
return 0, "/home/neko/.config/google-chrome/Default/Preferences: developer_mode=true\n", ""
if "policies/managed" in command and "-name '*.json'" in command:
return 0, "", ""
if "supervisorctl status tampermonkey-provisioning" in command:
return 0, "tampermonkey-provisioning RUNNING pid 9", ""
if "tampermonkey-provisioning.json" in command:
return 0, "", ""
if "Tampermonkey preference not found" in command:
return 0, "/home/neko/.config/google-chrome/Default: state=1 disable_reasons=None\n", ""
if "no standalone youtube extension" in command:
return 0, "no standalone youtube extension\n", ""
if "grep -R -a -l" in command:
return 0, "/home/neko/.config/google-chrome/Default/Local Extension Settings", ""
self.fail(f"unexpected docker exec: {container} {command}")
@@ -128,8 +138,12 @@ class VerifyLocalInstallTests(unittest.TestCase):
"neko_ip_watch_service",
"youtube_pro_neko_capacity",
"neko_1_browser",
"neko_1_multiuser_admin",
"neko_1_persistent_profile",
"neko_1_developer_mode",
"neko_1_personal_policy_mode",
"neko_1_tm_payload",
"neko_1_tm_enabled",
"neko_1_tm_storage",
"android_devices",
"android_screenshot",
@@ -143,12 +157,22 @@ class VerifyLocalInstallTests(unittest.TestCase):
def fake_docker_exec(container: str, command: str, *, timeout: float = 8.0):
if "supervisorctl status google-chrome" in command:
return 0, "google-chrome RUNNING pid 8", ""
if "NEKO member env invalid" in command:
return 0, "provider=multiuser admin_password=set user_password=set\n", ""
if "/proc/self/mountinfo" in command:
return 0, "/home/neko/.config/chromium\n/home/neko/.config/google-chrome\n", ""
if "Developer mode preference not found" in command:
return 0, "/home/neko/.config/google-chrome/Default/Preferences: developer_mode=true\n", ""
if "policies/managed" in command and "-name '*.json'" in command:
return 0, "/etc/opt/chrome/policies/managed/policies.json\n", ""
if "supervisorctl status tampermonkey-provisioning" in command:
return 0, "tampermonkey-provisioning RUNNING pid 9", ""
if "tampermonkey-provisioning.json" in command:
return 0, "", ""
if "Tampermonkey preference not found" in command:
return 0, "/home/neko/.config/google-chrome/Default: state=1 disable_reasons=None\n", ""
if "no standalone youtube extension" in command:
return 0, "no standalone youtube extension\n", ""
if "grep -R -a -l" in command:
return 1, "", ""
self.fail(f"unexpected docker exec: {container} {command}")
@@ -174,12 +198,22 @@ class VerifyLocalInstallTests(unittest.TestCase):
def fake_docker_exec(container: str, command: str, *, timeout: float = 8.0):
if "supervisorctl status google-chrome" in command:
return 0, "google-chrome RUNNING pid 8", ""
if "NEKO member env invalid" in command:
return 0, "provider=multiuser admin_password=set user_password=set\n", ""
if "/proc/self/mountinfo" in command:
return 0, "/home/neko/.config/chromium\n/home/neko/.config/google-chrome\n", ""
if "Developer mode preference not found" in command:
return 0, "/home/neko/.config/google-chrome/Default/Preferences: developer_mode=true\n", ""
if "policies/managed" in command and "-name '*.json'" in command:
return 0, "/etc/opt/chrome/policies/managed/policies.json\n", ""
if "supervisorctl status tampermonkey-provisioning" in command:
return 0, "tampermonkey-provisioning RUNNING pid 9", ""
if "tampermonkey-provisioning.json" in command:
return 0, "", ""
if "Tampermonkey preference not found" in command:
return 0, "/home/neko/.config/google-chrome/Default: state=1 disable_reasons=None\n", ""
if "no standalone youtube extension" in command:
return 0, "no standalone youtube extension\n", ""
if "grep -R -a -l" in command:
return 1, "", ""
self.fail(f"unexpected docker exec: {container} {command}")
@@ -197,6 +231,51 @@ class VerifyLocalInstallTests(unittest.TestCase):
self.assertFalse(report["ready"])
self.assertEqual(report["failing_checks"][0]["id"], "neko_1_tm_storage")
def test_neko_tampermonkey_enabled_rejects_unpacked_profile_state(self) -> None:
checks: list[dict[str, object]] = []
def fake_docker_exec(container: str, command: str, *, timeout: float = 8.0):
if "supervisorctl status google-chrome" in command:
return 0, "google-chrome RUNNING pid 8", ""
if "NEKO member env invalid" in command:
return 0, "provider=multiuser admin_password=set user_password=set\n", ""
if "/proc/self/mountinfo" in command:
return 0, "/home/neko/.config/chromium\n/home/neko/.config/google-chrome\n", ""
if "Developer mode preference not found" in command:
return 0, "/home/neko/.config/google-chrome/Default/Preferences: developer_mode=true\n", ""
if "policies/managed" in command and "-name '*.json'" in command:
return 0, "", ""
if "supervisorctl status tampermonkey-provisioning" in command:
return 0, "tampermonkey-provisioning RUNNING pid 9", ""
if "tampermonkey-provisioning.json" in command:
return 0, "", ""
if "Tampermonkey preference not found" in command:
return (
1,
"/home/neko/.config/google-chrome/Default: "
"state=1 disable_reasons=None from_webstore=False "
"location=8 path=/etc/neko/tampermonkey-seed/Default/Extensions/dhdgffkkebhmkfjojejmpbldmpobfkfo/5.5.0_0\n",
"",
)
if "no standalone youtube extension" in command:
return 0, "no standalone youtube extension\n", ""
if "grep -R -a -l" in command:
return 0, "/home/neko/.config/google-chrome/Default/Local Extension Settings", ""
self.fail(f"unexpected docker exec: {container} {command}")
with patch.object(verify_local_install, "docker_ps_names", return_value=(True, ["live-neko-1", "live-neko-2"], "")):
with patch.object(verify_local_install, "systemd_active", return_value=(True, "active")):
with patch.object(verify_local_install, "docker_exec", side_effect=fake_docker_exec):
verify_local_install.verify_neko(
checks,
{"ENABLE_NEKO": "1", "NEKO_INSTANCE_COUNT": "2", "NEKO_MANAGED_POLICIES": "0"},
timeout=5.0,
)
report = verify_local_install.summarize(checks)
self.assertFalse(report["ready"])
self.assertEqual(report["failing_checks"][0]["id"], "neko_1_tm_enabled")
def test_android_endpoint_without_devices_is_not_a_required_install_failure(self) -> None:
checks: list[dict[str, object]] = []
with patch.object(

View File

@@ -43,17 +43,19 @@ class WebDynamicProcessesTests(unittest.TestCase):
def test_script_for_pm2_or_pro_accepts_custom_saved_pm2_name(self) -> None:
channels = [{"id": "lane3", "name": "Lane 3", "pm2Name": "custom-lane-three"}]
with patch("web.get_youtube_pro_channels", return_value=channels):
resolved = web.script_for_pm2_or_pro("custom-lane-three")
resolved = web.script_for_pm2_or_pro("youtube2__custom-lane-three")
self.assertEqual(resolved, "youtube.py")
def test_process_monitor_entries_keep_static_youtube_label_for_lane_one(self) -> None:
def test_process_monitor_entries_migrates_legacy_youtube_pm2_without_hiding_single_lane(self) -> None:
channels = [{"id": "lane1", "name": "Lane 1", "pm2Name": "youtube"}]
with patch("web.get_youtube_pro_channels", return_value=channels):
entries = web.process_monitor_entries()
youtube_row = next(row for row in entries if row["pm2"] == "youtube")
self.assertEqual(youtube_row["label"], "YouTube")
pro_row = next(row for row in entries if row["pm2"] == "youtube2__lane1")
self.assertEqual(pro_row["label"], "YouTube Pro · Lane 1")
def test_process_monitor_entries_do_not_duplicate_static_pm2_rows(self) -> None:
channels = [
@@ -64,6 +66,7 @@ class WebDynamicProcessesTests(unittest.TestCase):
entries = web.process_monitor_entries()
self.assertEqual(sum(1 for row in entries if row["pm2"] == "youtube"), 1)
self.assertEqual(sum(1 for row in entries if row["pm2"] == "youtube2__lane1"), 1)
self.assertEqual(sum(1 for row in entries if row["pm2"] == "youtube2__lane2"), 1)
def test_web_module_does_not_repeat_process_entry_function_defs(self) -> None:
@@ -183,6 +186,44 @@ class WebDynamicProcessesTests(unittest.TestCase):
self.assertEqual(payload.headers.get("cache-control"), "no-store, max-age=0, must-revalidate")
def test_save_url_config_updates_matching_youtube_pro_channel_store(self) -> None:
captured: list[list[dict[str, object]]] = []
channels = [{"id": "lane1", "name": "Lane 1", "pm2Name": "youtube2__lane1", "urlLines": "old\n"}]
with patch("web.allows_url_config", return_value=True):
with patch("web.managed_url_config_relpath", return_value="URL_config.youtube2__lane1.ini"):
with patch("web.governed_save_managed_file"):
with patch("web.get_youtube_pro_channels", return_value=channels):
with patch("web.set_youtube_pro_channels", side_effect=lambda rows: captured.append(rows)):
payload = asyncio.run(
web.save_url_config(
_JsonRequest({"content": "https://live.douyin.com/fresh\n"}),
process="youtube2__lane1",
)
)
self.assertEqual(_json_body(payload)["message"], "配置保存成功")
self.assertEqual(captured[-1][0]["urlLines"], "https://live.douyin.com/fresh\n")
def test_save_config_updates_matching_youtube_pro_channel_stream_key(self) -> None:
captured: list[list[dict[str, object]]] = []
channels = [{"id": "lane1", "name": "Lane 1", "pm2Name": "youtube2__lane1", "streamKey": "old-key"}]
content = "[youtube]\nkey = new-key-123\nrtmps = 是\n"
with patch("web.managed_youtube_ini_relpath", return_value="youtube.youtube2__lane1.ini"):
with patch("web.governed_save_managed_file"):
with patch("web.get_youtube_pro_channels", return_value=channels):
with patch("web.set_youtube_pro_channels", side_effect=lambda rows: captured.append(rows)):
payload = asyncio.run(
web.save_config(
_JsonRequest({"content": content}),
process="youtube2__lane1",
)
)
self.assertEqual(_json_body(payload)["message"], "配置保存成功")
self.assertEqual(captured[-1][0]["streamKey"], "new-key-123")
def test_single_youtube_save_does_not_mutate_legacy_pro_channel_named_youtube(self) -> None:
captured: list[list[dict[str, object]]] = []
channels = [{"id": "lane1", "name": "Lane 1", "pm2Name": "youtube", "urlLines": "old\n"}]
@@ -193,32 +234,13 @@ class WebDynamicProcessesTests(unittest.TestCase):
with patch("web.set_youtube_pro_channels", side_effect=lambda rows: captured.append(rows)):
payload = asyncio.run(
web.save_url_config(
_JsonRequest({"content": "https://live.douyin.com/fresh\n"}),
_JsonRequest({"content": "https://live.douyin.com/single\n"}),
process="youtube",
)
)
self.assertEqual(_json_body(payload)["message"], "配置保存成功")
self.assertEqual(captured[-1][0]["urlLines"], "https://live.douyin.com/fresh\n")
def test_save_config_updates_matching_youtube_pro_channel_stream_key(self) -> None:
captured: list[list[dict[str, object]]] = []
channels = [{"id": "lane1", "name": "Lane 1", "pm2Name": "youtube", "streamKey": "old-key"}]
content = "[youtube]\nkey = new-key-123\nrtmps = 是\n"
with patch("web.managed_youtube_ini_relpath", return_value="youtube.youtube.ini"):
with patch("web.governed_save_managed_file"):
with patch("web.get_youtube_pro_channels", return_value=channels):
with patch("web.set_youtube_pro_channels", side_effect=lambda rows: captured.append(rows)):
payload = asyncio.run(
web.save_config(
_JsonRequest({"content": content}),
process="youtube",
)
)
self.assertEqual(_json_body(payload)["message"], "配置保存成功")
self.assertEqual(captured[-1][0]["streamKey"], "new-key-123")
self.assertEqual(captured, [])
if __name__ == "__main__":

View File

@@ -17,6 +17,10 @@ class YoutubeProRuntimeTests(unittest.TestCase):
def test_normalize_channels_assigns_default_pm2_and_rejects_duplicate_names(self) -> None:
rows = normalize_youtube_pro_channels([{"id": "lane2", "name": "Lane 2"}])
self.assertEqual(rows[0]["pm2Name"], "youtube2__lane2")
rows = normalize_youtube_pro_channels([{"id": "lane1", "pm2Name": "youtube"}])
self.assertEqual(rows[0]["pm2Name"], "youtube2__lane1")
rows = normalize_youtube_pro_channels([{"id": "lane3", "pm2Name": "custom"}])
self.assertEqual(rows[0]["pm2Name"], "youtube2__custom")
with self.assertRaises(ValueError):
normalize_youtube_pro_channels(