G7 증명과 G8 clean-head 승격 준비
This commit is contained in:
parent
94c681d450
commit
5221f79e3f
52 changed files with 6876 additions and 506 deletions
|
|
@ -18,20 +18,38 @@ SPEC.loader.exec_module(MODULE)
|
|||
def args(**overrides):
|
||||
base = {
|
||||
"wss_url": "wss://api.example.test/voice/ws",
|
||||
"origin": "https://api.example.test",
|
||||
"origin": "https://web.example.test",
|
||||
"allowed_browser_origins": ["https://web.example.test"],
|
||||
"admin_runtime_url": "https://api.example.test/admin/voice-runtime",
|
||||
"topology_mode": "linux-compose",
|
||||
"compose_project": "vignette",
|
||||
"api_container": "vignette-api-1",
|
||||
"api_image_digest": "sha256:" + "a" * 64,
|
||||
"caddy_container": "vignette-proxy-1",
|
||||
"caddy_image_digest": "sha256:" + "b" * 64,
|
||||
"repo_root": None,
|
||||
"git_sha": None,
|
||||
"git_tree_sha": None,
|
||||
"runner_script_sha256": None,
|
||||
"collector_script_sha256": None,
|
||||
"checker_script_sha256": None,
|
||||
"psutil_version": None,
|
||||
"api_pid": None,
|
||||
"api_executable_name": None,
|
||||
"api_executable_sha256": None,
|
||||
"api_cwd": None,
|
||||
"api_listen_port": None,
|
||||
"cloudflared_pid": None,
|
||||
"cloudflared_executable_name": None,
|
||||
"cloudflared_executable_sha256": None,
|
||||
"cloudflared_cwd": None,
|
||||
"expected_stt_provider": "local_whisper",
|
||||
"expected_stt_model": "large-v3",
|
||||
"expected_stt_model": "small",
|
||||
"expected_tts_provider": "melotts",
|
||||
"expected_tts_model": "melotts-korean",
|
||||
"microphone_device": "",
|
||||
"confirm_physical_capture": False,
|
||||
"duration_seconds": 3_000.0,
|
||||
"duration_seconds": 3_120.0,
|
||||
"runtime_interval_seconds": 100.0,
|
||||
"topology_interval_seconds": 100.0,
|
||||
"human_voice_gain": Path("pack.json"),
|
||||
|
|
@ -50,33 +68,110 @@ class FakeCompleted:
|
|||
|
||||
|
||||
class HostAlignmentTest(unittest.TestCase):
|
||||
def test_matching_hosts_are_accepted(self) -> None:
|
||||
def test_matching_transport_hosts_are_accepted(self) -> None:
|
||||
host = MODULE.assert_single_host(
|
||||
"wss://api.example.test/voice/ws",
|
||||
"https://api.example.test",
|
||||
"https://api.example.test/admin/voice-runtime",
|
||||
)
|
||||
self.assertEqual(host, "api.example.test")
|
||||
|
||||
def test_mismatched_hosts_fail_closed(self) -> None:
|
||||
with self.assertRaises(MODULE.WindowError):
|
||||
def test_mismatched_transport_hosts_fail_closed_without_echoing_hosts(self) -> None:
|
||||
with self.assertRaises(MODULE.WindowError) as caught:
|
||||
MODULE.assert_single_host(
|
||||
"wss://api.example.test/voice/ws", "https://other.example.test"
|
||||
"wss://api.example.test/voice/ws",
|
||||
"https://private-tenant.example.test/admin/voice-runtime",
|
||||
)
|
||||
self.assertEqual("transport_hosts_must_match", str(caught.exception))
|
||||
self.assertNotIn("private-tenant", str(caught.exception))
|
||||
|
||||
def test_plan_rejects_a_split_window(self) -> None:
|
||||
with self.assertRaises(MODULE.WindowError):
|
||||
def test_browser_origin_is_independent_from_transport_host(self) -> None:
|
||||
legs = MODULE.plan_legs(
|
||||
args(microphone_device="mic", confirm_physical_capture=True),
|
||||
Path("out"),
|
||||
admin_probe=lambda origin: ["/admin/voice-runtime"],
|
||||
)
|
||||
self.assertEqual(legs[0].argv[legs[0].argv.index("--origin") + 1],
|
||||
"https://web.example.test")
|
||||
|
||||
def test_plan_rejects_a_split_transport_window(self) -> None:
|
||||
with self.assertRaises(MODULE.WindowError) as caught:
|
||||
MODULE.plan_legs(
|
||||
args(admin_runtime_url="https://elsewhere.test/admin/voice-runtime",
|
||||
microphone_device="mic", confirm_physical_capture=True),
|
||||
Path("out"),
|
||||
admin_probe=lambda origin: ["/admin/voice-runtime"],
|
||||
)
|
||||
self.assertEqual("transport_hosts_must_match", str(caught.exception))
|
||||
|
||||
def test_browser_origin_requires_an_explicit_allowlist_match(self) -> None:
|
||||
with self.assertRaises(MODULE.WindowError) as caught:
|
||||
MODULE.plan_legs(
|
||||
args(origin="https://unlisted.example.test"),
|
||||
Path("out"),
|
||||
admin_probe=lambda origin: ["/admin/voice-runtime"],
|
||||
)
|
||||
self.assertEqual("browser_origin_not_allowed", str(caught.exception))
|
||||
|
||||
def test_added_origin_does_not_remove_the_public_default(self) -> None:
|
||||
normalized = MODULE.assert_browser_origin_allowed(
|
||||
MODULE.DEFAULT_BROWSER_ORIGIN, ["https://preview.example.test"]
|
||||
)
|
||||
self.assertEqual(MODULE.DEFAULT_BROWSER_ORIGIN, normalized)
|
||||
|
||||
def test_transport_schemes_fail_closed_without_reflecting_urls(self) -> None:
|
||||
cases = (
|
||||
(
|
||||
"ws://private-wss.example.test/voice/ws?token=secret",
|
||||
"https://api.example.test/admin/voice-runtime",
|
||||
"wss_url_scheme_invalid",
|
||||
),
|
||||
(
|
||||
"wss://api.example.test/voice/ws",
|
||||
"http://private-admin.example.test/admin/voice-runtime?email=secret",
|
||||
"admin_runtime_url_scheme_invalid",
|
||||
),
|
||||
)
|
||||
for wss_url, admin_url, expected in cases:
|
||||
with self.subTest(expected=expected):
|
||||
with self.assertRaises(MODULE.WindowError) as caught:
|
||||
MODULE.assert_transport_schemes(wss_url, admin_url)
|
||||
self.assertEqual(expected, str(caught.exception))
|
||||
self.assertNotIn("secret", str(caught.exception))
|
||||
|
||||
def test_browser_origin_query_is_rejected_without_echoing_it(self) -> None:
|
||||
secret_query = "https://web.example.test?email=person@example.test"
|
||||
with self.assertRaises(MODULE.WindowError) as caught:
|
||||
MODULE.assert_browser_origin_allowed(
|
||||
secret_query, ["https://web.example.test"]
|
||||
)
|
||||
self.assertEqual("browser_origin_invalid", str(caught.exception))
|
||||
self.assertNotIn("person@example.test", str(caught.exception))
|
||||
|
||||
def test_public_parser_defaults_match_browser_and_transport_contract(self) -> None:
|
||||
parser = MODULE.build_parser()
|
||||
parsed = parser.parse_args(["--out-dir", "out"])
|
||||
self.assertEqual(MODULE.DEFAULT_WSS_URL, parsed.wss_url)
|
||||
self.assertEqual(MODULE.DEFAULT_BROWSER_ORIGIN, parsed.origin)
|
||||
self.assertEqual(MODULE.DEFAULT_ADMIN_RUNTIME_URL, parsed.admin_runtime_url)
|
||||
self.assertEqual(3_120.0, parsed.duration_seconds)
|
||||
self.assertIsNone(parsed.allowed_browser_origins)
|
||||
help_text = parser.format_help()
|
||||
for expected in (
|
||||
"https://vignette.chanpaca.net",
|
||||
"wss://api-vignette.chanpaca.net/voice/ws",
|
||||
"stt_provider=local_whisper stt_model=small",
|
||||
"tts_provider=melotts tts_model=melotts-korean",
|
||||
"3120초",
|
||||
):
|
||||
self.assertIn(expected, help_text)
|
||||
|
||||
|
||||
class SamplePlanTest(unittest.TestCase):
|
||||
def test_samples_cover_the_whole_window(self) -> None:
|
||||
self.assertEqual(MODULE.sample_plan(3_000.0, 100.0), 31)
|
||||
self.assertEqual(MODULE.sample_plan(3_120.0, 100.0), 33)
|
||||
|
||||
def test_fractional_interval_always_gets_a_terminal_sample(self) -> None:
|
||||
self.assertEqual(MODULE.sample_plan(3_121.0, 100.0), 33)
|
||||
|
||||
def test_invalid_plan_fails_closed(self) -> None:
|
||||
for duration, interval in ((0, 100.0), (3_000.0, 0)):
|
||||
|
|
@ -115,11 +210,14 @@ class ConsentGateTest(unittest.TestCase):
|
|||
self.assertNotIn("--confirm-physical-capture", leg.argv)
|
||||
self.assertNotIn("--microphone-device", leg.argv)
|
||||
|
||||
def test_production_window_shorter_than_fifty_minutes_is_rejected(self) -> None:
|
||||
def test_three_thousand_second_capture_is_rejected_without_skew_margin(self) -> None:
|
||||
with self.assertRaises(MODULE.WindowError) as ctx:
|
||||
MODULE.validate(args(duration_seconds=600.0))
|
||||
MODULE.validate(args(duration_seconds=3_000.0))
|
||||
self.assertEqual(str(ctx.exception), "production_window_too_short")
|
||||
|
||||
def test_fifty_two_minute_capture_is_accepted(self) -> None:
|
||||
MODULE.validate(args(duration_seconds=3_120.0))
|
||||
|
||||
def test_production_run_requires_the_human_pack(self) -> None:
|
||||
with self.assertRaises(MODULE.WindowError) as ctx:
|
||||
MODULE.validate(args(human_voice_gain=None))
|
||||
|
|
@ -145,16 +243,88 @@ class LegCompositionTest(unittest.TestCase):
|
|||
leg.argv[leg.argv.index("--public-host") + 1], "api.example.test"
|
||||
)
|
||||
|
||||
def test_all_three_legs_share_one_window_length(self) -> None:
|
||||
def test_windows_topology_leg_passes_exact_process_pins(self) -> None:
|
||||
windows_args = args(
|
||||
topology_mode="windows-host",
|
||||
compose_project=None,
|
||||
api_container=None,
|
||||
api_image_digest=None,
|
||||
caddy_container=None,
|
||||
caddy_image_digest=None,
|
||||
repo_root=r"D:\workspace\vignette",
|
||||
git_sha="5" * 40,
|
||||
git_tree_sha="6" * 40,
|
||||
runner_script_sha256="7" * 64,
|
||||
collector_script_sha256="8" * 64,
|
||||
checker_script_sha256="9" * 64,
|
||||
psutil_version="7.0.0",
|
||||
api_pid=301,
|
||||
api_executable_name="python.exe",
|
||||
api_executable_sha256="3" * 64,
|
||||
api_cwd=r"D:\workspace\vignette\apps\api",
|
||||
api_listen_port=8001,
|
||||
cloudflared_pid=302,
|
||||
cloudflared_executable_name="cloudflared.exe",
|
||||
cloudflared_executable_sha256="4" * 64,
|
||||
cloudflared_cwd=r"D:\workspace\vignette",
|
||||
)
|
||||
MODULE.validate(windows_args)
|
||||
leg = MODULE.build_topology_leg(windows_args, Path("topology.json"))
|
||||
|
||||
self.assertEqual("windows-host", leg.argv[leg.argv.index("--topology-mode") + 1])
|
||||
self.assertEqual("301", leg.argv[leg.argv.index("--api-pid") + 1])
|
||||
self.assertEqual(
|
||||
"6" * 40, leg.argv[leg.argv.index("--git-tree-sha") + 1]
|
||||
)
|
||||
self.assertEqual(
|
||||
"7" * 64, leg.argv[leg.argv.index("--runner-script-sha256") + 1]
|
||||
)
|
||||
self.assertEqual(
|
||||
"8" * 64,
|
||||
leg.argv[leg.argv.index("--collector-script-sha256") + 1],
|
||||
)
|
||||
self.assertEqual(
|
||||
"9" * 64, leg.argv[leg.argv.index("--checker-script-sha256") + 1]
|
||||
)
|
||||
self.assertEqual("7.0.0", leg.argv[leg.argv.index("--psutil-version") + 1])
|
||||
self.assertEqual(
|
||||
"4" * 64,
|
||||
leg.argv[leg.argv.index("--cloudflared-executable-sha256") + 1],
|
||||
)
|
||||
self.assertNotIn("--compose-project", leg.argv)
|
||||
|
||||
def test_each_topology_mode_requires_its_own_pins(self) -> None:
|
||||
cases = (
|
||||
(args(topology_mode="unknown"), "topology_mode_invalid"),
|
||||
(args(compose_project=None), "topology_argument_required:compose-project"),
|
||||
(
|
||||
args(
|
||||
topology_mode="windows-host",
|
||||
compose_project=None,
|
||||
api_container=None,
|
||||
api_image_digest=None,
|
||||
caddy_container=None,
|
||||
caddy_image_digest=None,
|
||||
),
|
||||
"topology_argument_required:repo-root",
|
||||
),
|
||||
)
|
||||
for invalid, failure in cases:
|
||||
with self.subTest(failure=failure):
|
||||
with self.assertRaises(MODULE.WindowError) as caught:
|
||||
MODULE.validate(invalid)
|
||||
self.assertEqual(failure, str(caught.exception))
|
||||
|
||||
def test_all_three_legs_share_the_margin_extended_window_length(self) -> None:
|
||||
legs = MODULE.plan_legs(
|
||||
args(rehearse=True, duration_seconds=3_000.0), Path("out"),
|
||||
args(rehearse=True, duration_seconds=3_120.0), Path("out"),
|
||||
admin_probe=lambda origin: ["/admin/voice-runtime"],
|
||||
)
|
||||
self.assertEqual([leg.name for leg in legs][1:], ["runtime", "topology"])
|
||||
for leg in legs[1:]:
|
||||
samples = int(leg.argv[leg.argv.index("--samples") + 1])
|
||||
interval = float(leg.argv[leg.argv.index("--interval-seconds") + 1])
|
||||
self.assertGreaterEqual(samples * interval, 3_000.0)
|
||||
self.assertGreaterEqual((samples - 1) * interval, 3_120.0)
|
||||
|
||||
|
||||
class WindowExecutionTest(unittest.TestCase):
|
||||
|
|
@ -256,6 +426,31 @@ class ReportTest(unittest.TestCase):
|
|||
self.assertTrue(closed["gate_closed"])
|
||||
self.assertFalse(open_gate["gate_closed"])
|
||||
|
||||
def test_production_process_exit_is_bound_to_checker_and_closed_gate(self) -> None:
|
||||
closed = MODULE.summarize(
|
||||
self._results(), rehearse=False, checker_returncode=0
|
||||
)
|
||||
checker_failed = MODULE.summarize(
|
||||
self._results(), rehearse=False, checker_returncode=1
|
||||
)
|
||||
leg_failed = MODULE.summarize(
|
||||
self._results((0, 1, 0)), rehearse=False, checker_returncode=0
|
||||
)
|
||||
self.assertEqual(0, MODULE.process_exit_code(closed))
|
||||
self.assertEqual(1, MODULE.process_exit_code(checker_failed))
|
||||
self.assertEqual(1, MODULE.process_exit_code(leg_failed))
|
||||
|
||||
def test_rehearse_exit_reports_leg_health_but_never_closes_gate(self) -> None:
|
||||
passed = MODULE.summarize(
|
||||
self._results(), rehearse=True, checker_returncode=None
|
||||
)
|
||||
failed = MODULE.summarize(
|
||||
self._results((0, 1, 0)), rehearse=True, checker_returncode=None
|
||||
)
|
||||
self.assertEqual(0, MODULE.process_exit_code(passed))
|
||||
self.assertEqual(1, MODULE.process_exit_code(failed))
|
||||
self.assertFalse(passed["gate_closed"])
|
||||
|
||||
def test_failed_leg_is_reported(self) -> None:
|
||||
report = MODULE.summarize(
|
||||
self._results((0, 1, 0)), rehearse=False, checker_returncode=None
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue