From 1330585ccc604eb2509893aadfb2da02a5fad0f1 Mon Sep 17 00:00:00 2001 From: Rogee Date: Thu, 20 Aug 2026 22:53:26 +0800 Subject: [PATCH] H-385: address CDP spike review blockers Co-authored-by: multica-agent --- artifacts/m0-events.sample.jsonl | 83 +++++++++++--------- artifacts/m0-report.json | 11 +-- docs/m0-spike.md | 15 ++-- scripts/m0-spike.mjs | 129 ++++++++++++++++++++----------- test/m0-spike.test.mjs | 10 ++- 5 files changed, 150 insertions(+), 98 deletions(-) diff --git a/artifacts/m0-events.sample.jsonl b/artifacts/m0-events.sample.jsonl index fb58b6c..2f410d1 100644 --- a/artifacts/m0-events.sample.jsonl +++ b/artifacts/m0-events.sample.jsonl @@ -1,38 +1,45 @@ -{"schema_version":"1.0","session_id":"m0-2a6397b2-f63b-4f1a-9a75-4bad92a3dc94","target_id":"24707FCF8DFB0EA57823EF42C34ADD3B","kind":"http_request","request_id":"2352456.3","at":1787234901.120407,"request":{"method":"GET","url":"http://127.0.0.1:40745/ok","headers":{"accept-language":"en-US,en;q=0.9","authorization":"[REDACTED]","cookie":"[REDACTED]","referer":"[REDACTED]","user-agent":"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/148.0.0.0 Safari/537.36"}}} -{"schema_version":"1.0","session_id":"m0-2a6397b2-f63b-4f1a-9a75-4bad92a3dc94","target_id":"24707FCF8DFB0EA57823EF42C34ADD3B","kind":"http_request_headers","request_id":"2352456.3","headers":{"accept":"*/*","accept-encoding":"gzip, deflate, br, zstd","accept-language":"en-US,en;q=0.9","authorization":"[REDACTED]","connection":"keep-alive","cookie":"[REDACTED]","host":"127.0.0.1:40745","sec-fetch-dest":"empty","sec-fetch-mode":"cors","sec-fetch-site":"same-origin","user-agent":"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/148.0.0.0 Safari/537.36"}} -{"schema_version":"1.0","session_id":"m0-2a6397b2-f63b-4f1a-9a75-4bad92a3dc94","target_id":"24707FCF8DFB0EA57823EF42C34ADD3B","kind":"http_response","request_id":"2352456.2","response":{"url":"http://127.0.0.1:40745/favicon.ico","status":200,"protocol":"http/1.1","mime_type":"text/html","headers":{"connection":"close","content-length":"38","content-type":"text/html","date":"Thu, 20 Aug 2026 14:08:21 GMT"}}} -{"schema_version":"1.0","session_id":"m0-2a6397b2-f63b-4f1a-9a75-4bad92a3dc94","target_id":"24707FCF8DFB0EA57823EF42C34ADD3B","kind":"http_finished","request_id":"2352456.2","encoded_bytes":158} -{"schema_version":"1.0","session_id":"m0-2a6397b2-f63b-4f1a-9a75-4bad92a3dc94","target_id":"24707FCF8DFB0EA57823EF42C34ADD3B","kind":"http_response_headers","request_id":"2352456.3","status":200,"headers":{"access-control-allow-headers":"authorization","access-control-allow-origin":"*","connection":"close","content-length":"11","content-type":"application/json","date":"Thu, 20 Aug 2026 14:08:21 GMT","set-cookie":"[REDACTED]"}} -{"schema_version":"1.0","session_id":"m0-2a6397b2-f63b-4f1a-9a75-4bad92a3dc94","target_id":"24707FCF8DFB0EA57823EF42C34ADD3B","kind":"http_response","request_id":"2352456.3","response":{"url":"http://127.0.0.1:40745/ok","status":200,"protocol":"http/1.1","mime_type":"application/json","headers":{"access-control-allow-headers":"authorization","access-control-allow-origin":"*","connection":"close","content-length":"11","content-type":"application/json","date":"Thu, 20 Aug 2026 14:08:21 GMT"}}} -{"schema_version":"1.0","session_id":"m0-2a6397b2-f63b-4f1a-9a75-4bad92a3dc94","target_id":"24707FCF8DFB0EA57823EF42C34ADD3B","kind":"http_finished","request_id":"2352456.3","encoded_bytes":291} -{"schema_version":"1.0","session_id":"m0-2a6397b2-f63b-4f1a-9a75-4bad92a3dc94","target_id":"24707FCF8DFB0EA57823EF42C34ADD3B","kind":"http_request","request_id":"2352456.4","at":1787234901.124618,"request":{"method":"GET","url":"http://127.0.0.1:40745/large","headers":{"accept-language":"en-US,en;q=0.9","authorization":"[REDACTED]","cookie":"[REDACTED]","referer":"[REDACTED]","user-agent":"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/148.0.0.0 Safari/537.36"}}} -{"schema_version":"1.0","session_id":"m0-2a6397b2-f63b-4f1a-9a75-4bad92a3dc94","target_id":"24707FCF8DFB0EA57823EF42C34ADD3B","kind":"http_request_headers","request_id":"2352456.4","headers":{"accept":"*/*","accept-encoding":"gzip, deflate, br, zstd","accept-language":"en-US,en;q=0.9","authorization":"[REDACTED]","connection":"keep-alive","cookie":"[REDACTED]","host":"127.0.0.1:40745","sec-fetch-dest":"empty","sec-fetch-mode":"cors","sec-fetch-site":"same-origin","user-agent":"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/148.0.0.0 Safari/537.36"}} -{"schema_version":"1.0","session_id":"m0-2a6397b2-f63b-4f1a-9a75-4bad92a3dc94","target_id":"24707FCF8DFB0EA57823EF42C34ADD3B","kind":"http_response_headers","request_id":"2352456.4","status":200,"headers":{"access-control-allow-headers":"authorization","access-control-allow-origin":"*","connection":"close","content-length":"2048","content-type":"text/plain","date":"Thu, 20 Aug 2026 14:08:21 GMT"}} -{"schema_version":"1.0","session_id":"m0-2a6397b2-f63b-4f1a-9a75-4bad92a3dc94","target_id":"24707FCF8DFB0EA57823EF42C34ADD3B","kind":"http_response","request_id":"2352456.4","response":{"url":"http://127.0.0.1:40745/large","status":200,"protocol":"http/1.1","mime_type":"text/plain","headers":{"access-control-allow-headers":"authorization","access-control-allow-origin":"*","connection":"close","content-length":"2048","content-type":"text/plain","date":"Thu, 20 Aug 2026 14:08:21 GMT"}}} -{"schema_version":"1.0","session_id":"m0-2a6397b2-f63b-4f1a-9a75-4bad92a3dc94","target_id":"24707FCF8DFB0EA57823EF42C34ADD3B","kind":"http_finished","request_id":"2352456.4","encoded_bytes":2248} -{"schema_version":"1.0","session_id":"m0-2a6397b2-f63b-4f1a-9a75-4bad92a3dc94","target_id":"24707FCF8DFB0EA57823EF42C34ADD3B","kind":"http_request","request_id":"2352456.5","at":1787234901.128406,"request":{"method":"GET","url":"https://127.0.0.1:38819/secure","headers":{"accept-language":"en-US,en;q=0.9","authorization":"[REDACTED]","cookie":"[REDACTED]","referer":"[REDACTED]","user-agent":"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/148.0.0.0 Safari/537.36"}}} -{"schema_version":"1.0","session_id":"m0-2a6397b2-f63b-4f1a-9a75-4bad92a3dc94","target_id":"24707FCF8DFB0EA57823EF42C34ADD3B","kind":"http_request","request_id":"A1C10D76513CFFAB16DEFD007045EF58","at":1787234901.129414,"request":{"method":"OPTIONS","url":"https://127.0.0.1:38819/secure","headers":{"accept":"*/*","access-control-request-headers":"authorization","access-control-request-method":"GET","origin":"http://127.0.0.1:40745","sec-fetch-mode":"cors","user-agent":"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/148.0.0.0 Safari/537.36"}}} -{"schema_version":"1.0","session_id":"m0-2a6397b2-f63b-4f1a-9a75-4bad92a3dc94","target_id":"24707FCF8DFB0EA57823EF42C34ADD3B","kind":"http_request_headers","request_id":"A1C10D76513CFFAB16DEFD007045EF58","headers":{"accept":"*/*","accept-encoding":"gzip, deflate, br, zstd","accept-language":"en-US,en;q=0.9","access-control-request-headers":"authorization","access-control-request-method":"GET","connection":"keep-alive","host":"127.0.0.1:38819","origin":"http://127.0.0.1:40745","sec-fetch-dest":"empty","sec-fetch-mode":"cors","sec-fetch-site":"cross-site","user-agent":"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/148.0.0.0 Safari/537.36"}} -{"schema_version":"1.0","session_id":"m0-2a6397b2-f63b-4f1a-9a75-4bad92a3dc94","target_id":"24707FCF8DFB0EA57823EF42C34ADD3B","kind":"http_response_headers","request_id":"A1C10D76513CFFAB16DEFD007045EF58","status":200,"headers":{"access-control-allow-headers":"authorization","access-control-allow-origin":"*","connection":"close","content-length":"15","content-type":"application/json","date":"Thu, 20 Aug 2026 14:08:21 GMT"}} -{"schema_version":"1.0","session_id":"m0-2a6397b2-f63b-4f1a-9a75-4bad92a3dc94","target_id":"24707FCF8DFB0EA57823EF42C34ADD3B","kind":"http_response","request_id":"A1C10D76513CFFAB16DEFD007045EF58","response":{"url":"https://127.0.0.1:38819/secure","status":200,"protocol":"http/1.1","mime_type":"application/json","headers":{"access-control-allow-headers":"authorization","access-control-allow-origin":"*","connection":"close","content-length":"15","content-type":"application/json","date":"Thu, 20 Aug 2026 14:08:21 GMT"}}} -{"schema_version":"1.0","session_id":"m0-2a6397b2-f63b-4f1a-9a75-4bad92a3dc94","target_id":"24707FCF8DFB0EA57823EF42C34ADD3B","kind":"http_finished","request_id":"A1C10D76513CFFAB16DEFD007045EF58","encoded_bytes":0} -{"schema_version":"1.0","session_id":"m0-2a6397b2-f63b-4f1a-9a75-4bad92a3dc94","target_id":"24707FCF8DFB0EA57823EF42C34ADD3B","kind":"http_request_headers","request_id":"2352456.5","headers":{"accept":"*/*","accept-encoding":"gzip, deflate, br, zstd","accept-language":"en-US,en;q=0.9","authorization":"[REDACTED]","connection":"keep-alive","cookie":"[REDACTED]","host":"127.0.0.1:38819","origin":"http://127.0.0.1:40745","sec-fetch-dest":"empty","sec-fetch-mode":"cors","sec-fetch-site":"cross-site","user-agent":"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/148.0.0.0 Safari/537.36"}} -{"schema_version":"1.0","session_id":"m0-2a6397b2-f63b-4f1a-9a75-4bad92a3dc94","target_id":"24707FCF8DFB0EA57823EF42C34ADD3B","kind":"http_response_headers","request_id":"2352456.5","status":200,"headers":{"access-control-allow-headers":"authorization","access-control-allow-origin":"*","connection":"close","content-length":"15","content-type":"application/json","date":"Thu, 20 Aug 2026 14:08:21 GMT"}} -{"schema_version":"1.0","session_id":"m0-2a6397b2-f63b-4f1a-9a75-4bad92a3dc94","target_id":"24707FCF8DFB0EA57823EF42C34ADD3B","kind":"http_response","request_id":"2352456.5","response":{"url":"https://127.0.0.1:38819/secure","status":200,"protocol":"http/1.1","mime_type":"application/json","headers":{"access-control-allow-headers":"authorization","access-control-allow-origin":"*","connection":"close","content-length":"15","content-type":"application/json","date":"Thu, 20 Aug 2026 14:08:21 GMT"}}} -{"schema_version":"1.0","session_id":"m0-2a6397b2-f63b-4f1a-9a75-4bad92a3dc94","target_id":"24707FCF8DFB0EA57823EF42C34ADD3B","kind":"http_finished","request_id":"2352456.5","encoded_bytes":219} -{"schema_version":"1.0","session_id":"m0-2a6397b2-f63b-4f1a-9a75-4bad92a3dc94","target_id":"24707FCF8DFB0EA57823EF42C34ADD3B","kind":"websocket_created","connection_id":"2352456.6","url":"ws://127.0.0.1:40745/ws"} -{"schema_version":"1.0","session_id":"m0-2a6397b2-f63b-4f1a-9a75-4bad92a3dc94","target_id":"24707FCF8DFB0EA57823EF42C34ADD3B","kind":"websocket_handshake_request","connection_id":"2352456.6","headers":{"accept-encoding":"gzip, deflate, br, zstd","accept-language":"en-US,en;q=0.9","cache-control":"no-cache","connection":"Upgrade","cookie":"[REDACTED]","host":"127.0.0.1:40745","origin":"http://127.0.0.1:40745","pragma":"no-cache","sec-websocket-extensions":"permessage-deflate; client_max_window_bits","sec-websocket-key":"[REDACTED]","sec-websocket-version":"13","upgrade":"websocket","user-agent":"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/148.0.0.0 Safari/537.36"}} -{"schema_version":"1.0","session_id":"m0-2a6397b2-f63b-4f1a-9a75-4bad92a3dc94","target_id":"24707FCF8DFB0EA57823EF42C34ADD3B","kind":"websocket_handshake_response","connection_id":"2352456.6","status":101,"headers":{"connection":"Upgrade","sec-websocket-accept":"[REDACTED]","upgrade":"websocket"}} -{"schema_version":"1.0","session_id":"m0-2a6397b2-f63b-4f1a-9a75-4bad92a3dc94","target_id":"24707FCF8DFB0EA57823EF42C34ADD3B","kind":"websocket_frame","connection_id":"2352456.6","direction":"sent","opcode":1,"payload_bytes":14,"payload":{"state":"omitted","reason":"frame_policy"}} -{"schema_version":"1.0","session_id":"m0-2a6397b2-f63b-4f1a-9a75-4bad92a3dc94","target_id":"24707FCF8DFB0EA57823EF42C34ADD3B","kind":"websocket_frame","connection_id":"2352456.6","direction":"received","opcode":1,"payload_bytes":14,"payload":{"state":"omitted","reason":"frame_policy"}} -{"schema_version":"1.0","session_id":"m0-2a6397b2-f63b-4f1a-9a75-4bad92a3dc94","target_id":"24707FCF8DFB0EA57823EF42C34ADD3B","kind":"websocket_closed","connection_id":"2352456.6"} -{"schema_version":"1.0","session_id":"m0-2a6397b2-f63b-4f1a-9a75-4bad92a3dc94","target_id":"24707FCF8DFB0EA57823EF42C34ADD3B","kind":"http_request","request_id":"2352456.7","at":1787234901.156918,"request":{"method":"GET","url":"http://127.0.0.1:34677/missing","headers":{"accept-language":"en-US,en;q=0.9","authorization":"[REDACTED]","cookie":"[REDACTED]","referer":"[REDACTED]","user-agent":"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/148.0.0.0 Safari/537.36"}}} -{"schema_version":"1.0","session_id":"m0-2a6397b2-f63b-4f1a-9a75-4bad92a3dc94","target_id":"24707FCF8DFB0EA57823EF42C34ADD3B","kind":"http_request","request_id":"A8170B1659C31EA09293E5B60A446454","at":1787234901.15755,"request":{"method":"OPTIONS","url":"http://127.0.0.1:34677/missing","headers":{"accept":"*/*","access-control-request-headers":"authorization","access-control-request-method":"GET","origin":"http://127.0.0.1:40745","sec-fetch-mode":"cors","user-agent":"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/148.0.0.0 Safari/537.36"}}} -{"schema_version":"1.0","session_id":"m0-2a6397b2-f63b-4f1a-9a75-4bad92a3dc94","target_id":"24707FCF8DFB0EA57823EF42C34ADD3B","kind":"http_failed","request_id":"A8170B1659C31EA09293E5B60A446454","error":{"text":"net::ERR_CONNECTION_REFUSED","canceled":false}} -{"schema_version":"1.0","session_id":"m0-2a6397b2-f63b-4f1a-9a75-4bad92a3dc94","target_id":"24707FCF8DFB0EA57823EF42C34ADD3B","kind":"http_failed","request_id":"2352456.7","error":{"text":"net::ERR_CONNECTION_REFUSED","canceled":false}} -{"schema_version":"1.0","session_id":"m0-2a6397b2-f63b-4f1a-9a75-4bad92a3dc94","target_id":"24707FCF8DFB0EA57823EF42C34ADD3B","kind":"http_body","request_id":"2352456.3","body":{"state":"stored","bytes":11,"encoding":"utf8","text":"{\"ok\":true}"}} -{"schema_version":"1.0","session_id":"m0-2a6397b2-f63b-4f1a-9a75-4bad92a3dc94","target_id":"24707FCF8DFB0EA57823EF42C34ADD3B","kind":"http_body","request_id":"2352456.4","body":{"state":"omitted","reason":"size_limit","bytes":2048}} -{"schema_version":"1.0","session_id":"m0-2a6397b2-f63b-4f1a-9a75-4bad92a3dc94","target_id":"24707FCF8DFB0EA57823EF42C34ADD3B","kind":"http_body","request_id":"2352456.5","body":{"state":"stored","bytes":15,"encoding":"utf8","text":"{\"secure\":true}"}} -{"schema_version":"1.0","session_id":"m0-2a6397b2-f63b-4f1a-9a75-4bad92a3dc94","target_id":"24707FCF8DFB0EA57823EF42C34ADD3B","kind":"http_body","request_id":"2352456.7","body":{"state":"unavailable","reason":"cdp_error","error":"-32000: No data found for resource with given identifier"}} -{"schema_version":"1.0","session_id":"m0-2a6397b2-f63b-4f1a-9a75-4bad92a3dc94","target_id":"24707FCF8DFB0EA57823EF42C34ADD3B","kind":"target_closed","reason":"Target.closeTarget"} -{"schema_version":"1.0","session_id":"m0-2a6397b2-f63b-4f1a-9a75-4bad92a3dc94","target_id":"24707FCF8DFB0EA57823EF42C34ADD3B","kind":"cdp_disconnected","reason":"client_closed"} +{"schema_version":"1.0","session_id":"m0-7bb3bf9e-f607-4f75-bfbb-ebd44c149245","target_id":"4BC8ED5ED08A27BBC10AEEAB38A304D2","kind":"http_response","request_id":"2473485.2","response":{"url":"http://127.0.0.1:40745/favicon.ico","status":200,"protocol":"http/1.1","mime_type":"text/html","headers":{"connection":"close","content-length":"38","content-type":"text/html","date":"Thu, 20 Aug 2026 14:51:52 GMT"}}} +{"schema_version":"1.0","session_id":"m0-7bb3bf9e-f607-4f75-bfbb-ebd44c149245","target_id":"4BC8ED5ED08A27BBC10AEEAB38A304D2","kind":"http_finished","request_id":"2473485.2","encoded_bytes":158} +{"schema_version":"1.0","session_id":"m0-7bb3bf9e-f607-4f75-bfbb-ebd44c149245","target_id":"4BC8ED5ED08A27BBC10AEEAB38A304D2","kind":"http_request","request_id":"2473485.3","at":1787237512.679264,"request":{"method":"GET","url":"http://127.0.0.1:40745/ok","headers":{"accept-language":"en-US,en;q=0.9","authorization":"[REDACTED]","cookie":"[REDACTED]","referer":"[REDACTED]","user-agent":"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/148.0.0.0 Safari/537.36"}}} +{"schema_version":"1.0","session_id":"m0-7bb3bf9e-f607-4f75-bfbb-ebd44c149245","target_id":"4BC8ED5ED08A27BBC10AEEAB38A304D2","kind":"http_request_headers","request_id":"2473485.3","headers":{"accept":"*/*","accept-encoding":"gzip, deflate, br, zstd","accept-language":"en-US,en;q=0.9","authorization":"[REDACTED]","connection":"keep-alive","cookie":"[REDACTED]","host":"127.0.0.1:40745","sec-fetch-dest":"empty","sec-fetch-mode":"cors","sec-fetch-site":"same-origin","user-agent":"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/148.0.0.0 Safari/537.36"}} +{"schema_version":"1.0","session_id":"m0-7bb3bf9e-f607-4f75-bfbb-ebd44c149245","target_id":"4BC8ED5ED08A27BBC10AEEAB38A304D2","kind":"http_response_headers","request_id":"2473485.3","status":200,"headers":{"access-control-allow-headers":"authorization","access-control-allow-origin":"*","connection":"close","content-length":"11","content-type":"application/json","date":"Thu, 20 Aug 2026 14:51:52 GMT","set-cookie":"[REDACTED]"}} +{"schema_version":"1.0","session_id":"m0-7bb3bf9e-f607-4f75-bfbb-ebd44c149245","target_id":"4BC8ED5ED08A27BBC10AEEAB38A304D2","kind":"http_response","request_id":"2473485.3","response":{"url":"http://127.0.0.1:40745/ok","status":200,"protocol":"http/1.1","mime_type":"application/json","headers":{"access-control-allow-headers":"authorization","access-control-allow-origin":"*","connection":"close","content-length":"11","content-type":"application/json","date":"Thu, 20 Aug 2026 14:51:52 GMT"}}} +{"schema_version":"1.0","session_id":"m0-7bb3bf9e-f607-4f75-bfbb-ebd44c149245","target_id":"4BC8ED5ED08A27BBC10AEEAB38A304D2","kind":"http_finished","request_id":"2473485.3","encoded_bytes":291} +{"schema_version":"1.0","session_id":"m0-7bb3bf9e-f607-4f75-bfbb-ebd44c149245","target_id":"4BC8ED5ED08A27BBC10AEEAB38A304D2","kind":"http_request","request_id":"2473485.4","at":1787237512.684393,"request":{"method":"GET","url":"http://127.0.0.1:40745/secret","headers":{"accept-language":"en-US,en;q=0.9","authorization":"[REDACTED]","cookie":"[REDACTED]","referer":"[REDACTED]","user-agent":"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/148.0.0.0 Safari/537.36"}}} +{"schema_version":"1.0","session_id":"m0-7bb3bf9e-f607-4f75-bfbb-ebd44c149245","target_id":"4BC8ED5ED08A27BBC10AEEAB38A304D2","kind":"http_request_headers","request_id":"2473485.4","headers":{"accept":"*/*","accept-encoding":"gzip, deflate, br, zstd","accept-language":"en-US,en;q=0.9","authorization":"[REDACTED]","connection":"keep-alive","cookie":"[REDACTED]","host":"127.0.0.1:40745","sec-fetch-dest":"empty","sec-fetch-mode":"cors","sec-fetch-site":"same-origin","user-agent":"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/148.0.0.0 Safari/537.36"}} +{"schema_version":"1.0","session_id":"m0-7bb3bf9e-f607-4f75-bfbb-ebd44c149245","target_id":"4BC8ED5ED08A27BBC10AEEAB38A304D2","kind":"http_response_headers","request_id":"2473485.4","status":200,"headers":{"access-control-allow-headers":"authorization","access-control-allow-origin":"*","connection":"close","content-length":"49","content-type":"application/json","date":"Thu, 20 Aug 2026 14:51:52 GMT"}} +{"schema_version":"1.0","session_id":"m0-7bb3bf9e-f607-4f75-bfbb-ebd44c149245","target_id":"4BC8ED5ED08A27BBC10AEEAB38A304D2","kind":"http_response","request_id":"2473485.4","response":{"url":"http://127.0.0.1:40745/secret","status":200,"protocol":"http/1.1","mime_type":"application/json","headers":{"access-control-allow-headers":"authorization","access-control-allow-origin":"*","connection":"close","content-length":"49","content-type":"application/json","date":"Thu, 20 Aug 2026 14:51:52 GMT"}}} +{"schema_version":"1.0","session_id":"m0-7bb3bf9e-f607-4f75-bfbb-ebd44c149245","target_id":"4BC8ED5ED08A27BBC10AEEAB38A304D2","kind":"http_finished","request_id":"2473485.4","encoded_bytes":253} +{"schema_version":"1.0","session_id":"m0-7bb3bf9e-f607-4f75-bfbb-ebd44c149245","target_id":"4BC8ED5ED08A27BBC10AEEAB38A304D2","kind":"http_request","request_id":"2473485.5","at":1787237512.688362,"request":{"method":"GET","url":"http://127.0.0.1:40745/large","headers":{"accept-language":"en-US,en;q=0.9","authorization":"[REDACTED]","cookie":"[REDACTED]","referer":"[REDACTED]","user-agent":"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/148.0.0.0 Safari/537.36"}}} +{"schema_version":"1.0","session_id":"m0-7bb3bf9e-f607-4f75-bfbb-ebd44c149245","target_id":"4BC8ED5ED08A27BBC10AEEAB38A304D2","kind":"http_request_headers","request_id":"2473485.5","headers":{"accept":"*/*","accept-encoding":"gzip, deflate, br, zstd","accept-language":"en-US,en;q=0.9","authorization":"[REDACTED]","connection":"keep-alive","cookie":"[REDACTED]","host":"127.0.0.1:40745","sec-fetch-dest":"empty","sec-fetch-mode":"cors","sec-fetch-site":"same-origin","user-agent":"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/148.0.0.0 Safari/537.36"}} +{"schema_version":"1.0","session_id":"m0-7bb3bf9e-f607-4f75-bfbb-ebd44c149245","target_id":"4BC8ED5ED08A27BBC10AEEAB38A304D2","kind":"http_response_headers","request_id":"2473485.5","status":200,"headers":{"access-control-allow-headers":"authorization","access-control-allow-origin":"*","connection":"close","content-length":"2048","content-type":"text/plain","date":"Thu, 20 Aug 2026 14:51:52 GMT"}} +{"schema_version":"1.0","session_id":"m0-7bb3bf9e-f607-4f75-bfbb-ebd44c149245","target_id":"4BC8ED5ED08A27BBC10AEEAB38A304D2","kind":"http_response","request_id":"2473485.5","response":{"url":"http://127.0.0.1:40745/large","status":200,"protocol":"http/1.1","mime_type":"text/plain","headers":{"access-control-allow-headers":"authorization","access-control-allow-origin":"*","connection":"close","content-length":"2048","content-type":"text/plain","date":"Thu, 20 Aug 2026 14:51:52 GMT"}}} +{"schema_version":"1.0","session_id":"m0-7bb3bf9e-f607-4f75-bfbb-ebd44c149245","target_id":"4BC8ED5ED08A27BBC10AEEAB38A304D2","kind":"http_finished","request_id":"2473485.5","encoded_bytes":2248} +{"schema_version":"1.0","session_id":"m0-7bb3bf9e-f607-4f75-bfbb-ebd44c149245","target_id":"4BC8ED5ED08A27BBC10AEEAB38A304D2","kind":"http_request","request_id":"2473485.6","at":1787237512.692057,"request":{"method":"GET","url":"https://127.0.0.1:35233/secure","headers":{"accept-language":"en-US,en;q=0.9","authorization":"[REDACTED]","cookie":"[REDACTED]","referer":"[REDACTED]","user-agent":"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/148.0.0.0 Safari/537.36"}}} +{"schema_version":"1.0","session_id":"m0-7bb3bf9e-f607-4f75-bfbb-ebd44c149245","target_id":"4BC8ED5ED08A27BBC10AEEAB38A304D2","kind":"http_request","request_id":"C5A175B024C8583D1FE54757455E50C2","at":1787237512.692597,"request":{"method":"OPTIONS","url":"https://127.0.0.1:35233/secure","headers":{"accept":"*/*","access-control-request-headers":"authorization","access-control-request-method":"GET","origin":"http://127.0.0.1:40745","sec-fetch-mode":"cors","user-agent":"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/148.0.0.0 Safari/537.36"}}} +{"schema_version":"1.0","session_id":"m0-7bb3bf9e-f607-4f75-bfbb-ebd44c149245","target_id":"4BC8ED5ED08A27BBC10AEEAB38A304D2","kind":"http_request_headers","request_id":"C5A175B024C8583D1FE54757455E50C2","headers":{"accept":"*/*","accept-encoding":"gzip, deflate, br, zstd","accept-language":"en-US,en;q=0.9","access-control-request-headers":"authorization","access-control-request-method":"GET","connection":"keep-alive","host":"127.0.0.1:35233","origin":"http://127.0.0.1:40745","sec-fetch-dest":"empty","sec-fetch-mode":"cors","sec-fetch-site":"cross-site","user-agent":"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/148.0.0.0 Safari/537.36"}} +{"schema_version":"1.0","session_id":"m0-7bb3bf9e-f607-4f75-bfbb-ebd44c149245","target_id":"4BC8ED5ED08A27BBC10AEEAB38A304D2","kind":"http_response_headers","request_id":"C5A175B024C8583D1FE54757455E50C2","status":200,"headers":{"access-control-allow-headers":"authorization","access-control-allow-origin":"*","connection":"close","content-length":"15","content-type":"application/json","date":"Thu, 20 Aug 2026 14:51:52 GMT"}} +{"schema_version":"1.0","session_id":"m0-7bb3bf9e-f607-4f75-bfbb-ebd44c149245","target_id":"4BC8ED5ED08A27BBC10AEEAB38A304D2","kind":"http_response","request_id":"C5A175B024C8583D1FE54757455E50C2","response":{"url":"https://127.0.0.1:35233/secure","status":200,"protocol":"http/1.1","mime_type":"application/json","headers":{"access-control-allow-headers":"authorization","access-control-allow-origin":"*","connection":"close","content-length":"15","content-type":"application/json","date":"Thu, 20 Aug 2026 14:51:52 GMT"}}} +{"schema_version":"1.0","session_id":"m0-7bb3bf9e-f607-4f75-bfbb-ebd44c149245","target_id":"4BC8ED5ED08A27BBC10AEEAB38A304D2","kind":"http_finished","request_id":"C5A175B024C8583D1FE54757455E50C2","encoded_bytes":0} +{"schema_version":"1.0","session_id":"m0-7bb3bf9e-f607-4f75-bfbb-ebd44c149245","target_id":"4BC8ED5ED08A27BBC10AEEAB38A304D2","kind":"http_request_headers","request_id":"2473485.6","headers":{"accept":"*/*","accept-encoding":"gzip, deflate, br, zstd","accept-language":"en-US,en;q=0.9","authorization":"[REDACTED]","connection":"keep-alive","cookie":"[REDACTED]","host":"127.0.0.1:35233","origin":"http://127.0.0.1:40745","sec-fetch-dest":"empty","sec-fetch-mode":"cors","sec-fetch-site":"cross-site","user-agent":"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/148.0.0.0 Safari/537.36"}} +{"schema_version":"1.0","session_id":"m0-7bb3bf9e-f607-4f75-bfbb-ebd44c149245","target_id":"4BC8ED5ED08A27BBC10AEEAB38A304D2","kind":"http_response_headers","request_id":"2473485.6","status":200,"headers":{"access-control-allow-headers":"authorization","access-control-allow-origin":"*","connection":"close","content-length":"15","content-type":"application/json","date":"Thu, 20 Aug 2026 14:51:52 GMT"}} +{"schema_version":"1.0","session_id":"m0-7bb3bf9e-f607-4f75-bfbb-ebd44c149245","target_id":"4BC8ED5ED08A27BBC10AEEAB38A304D2","kind":"http_response","request_id":"2473485.6","response":{"url":"https://127.0.0.1:35233/secure","status":200,"protocol":"http/1.1","mime_type":"application/json","headers":{"access-control-allow-headers":"authorization","access-control-allow-origin":"*","connection":"close","content-length":"15","content-type":"application/json","date":"Thu, 20 Aug 2026 14:51:52 GMT"}}} +{"schema_version":"1.0","session_id":"m0-7bb3bf9e-f607-4f75-bfbb-ebd44c149245","target_id":"4BC8ED5ED08A27BBC10AEEAB38A304D2","kind":"http_finished","request_id":"2473485.6","encoded_bytes":219} +{"schema_version":"1.0","session_id":"m0-7bb3bf9e-f607-4f75-bfbb-ebd44c149245","target_id":"4BC8ED5ED08A27BBC10AEEAB38A304D2","kind":"websocket_created","connection_id":"2473485.7","url":"ws://127.0.0.1:40745/ws"} +{"schema_version":"1.0","session_id":"m0-7bb3bf9e-f607-4f75-bfbb-ebd44c149245","target_id":"4BC8ED5ED08A27BBC10AEEAB38A304D2","kind":"websocket_handshake_request","connection_id":"2473485.7","headers":{"accept-encoding":"gzip, deflate, br, zstd","accept-language":"en-US,en;q=0.9","cache-control":"no-cache","connection":"Upgrade","cookie":"[REDACTED]","host":"127.0.0.1:40745","origin":"http://127.0.0.1:40745","pragma":"no-cache","sec-websocket-extensions":"permessage-deflate; client_max_window_bits","sec-websocket-key":"[REDACTED]","sec-websocket-version":"13","upgrade":"websocket","user-agent":"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/148.0.0.0 Safari/537.36"}} +{"schema_version":"1.0","session_id":"m0-7bb3bf9e-f607-4f75-bfbb-ebd44c149245","target_id":"4BC8ED5ED08A27BBC10AEEAB38A304D2","kind":"websocket_handshake_response","connection_id":"2473485.7","status":101,"headers":{"connection":"Upgrade","sec-websocket-accept":"[REDACTED]","upgrade":"websocket"}} +{"schema_version":"1.0","session_id":"m0-7bb3bf9e-f607-4f75-bfbb-ebd44c149245","target_id":"4BC8ED5ED08A27BBC10AEEAB38A304D2","kind":"websocket_frame","connection_id":"2473485.7","direction":"sent","opcode":1,"payload_bytes":14,"payload":{"state":"omitted","reason":"frame_policy"}} +{"schema_version":"1.0","session_id":"m0-7bb3bf9e-f607-4f75-bfbb-ebd44c149245","target_id":"4BC8ED5ED08A27BBC10AEEAB38A304D2","kind":"websocket_frame","connection_id":"2473485.7","direction":"received","opcode":1,"payload_bytes":14,"payload":{"state":"omitted","reason":"frame_policy"}} +{"schema_version":"1.0","session_id":"m0-7bb3bf9e-f607-4f75-bfbb-ebd44c149245","target_id":"4BC8ED5ED08A27BBC10AEEAB38A304D2","kind":"websocket_closed","connection_id":"2473485.7"} +{"schema_version":"1.0","session_id":"m0-7bb3bf9e-f607-4f75-bfbb-ebd44c149245","target_id":"4BC8ED5ED08A27BBC10AEEAB38A304D2","kind":"http_request","request_id":"2473485.8","at":1787237512.717744,"request":{"method":"GET","url":"http://127.0.0.1:40479/missing","headers":{"accept-language":"en-US,en;q=0.9","authorization":"[REDACTED]","cookie":"[REDACTED]","referer":"[REDACTED]","user-agent":"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/148.0.0.0 Safari/537.36"}}} +{"schema_version":"1.0","session_id":"m0-7bb3bf9e-f607-4f75-bfbb-ebd44c149245","target_id":"4BC8ED5ED08A27BBC10AEEAB38A304D2","kind":"http_request","request_id":"0A5A1C88A693E98CBD1A4317FB1311EF","at":1787237512.718296,"request":{"method":"OPTIONS","url":"http://127.0.0.1:40479/missing","headers":{"accept":"*/*","access-control-request-headers":"authorization","access-control-request-method":"GET","origin":"http://127.0.0.1:40745","sec-fetch-mode":"cors","user-agent":"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/148.0.0.0 Safari/537.36"}}} +{"schema_version":"1.0","session_id":"m0-7bb3bf9e-f607-4f75-bfbb-ebd44c149245","target_id":"4BC8ED5ED08A27BBC10AEEAB38A304D2","kind":"http_failed","request_id":"0A5A1C88A693E98CBD1A4317FB1311EF","error":{"text":"net::ERR_CONNECTION_REFUSED","canceled":false}} +{"schema_version":"1.0","session_id":"m0-7bb3bf9e-f607-4f75-bfbb-ebd44c149245","target_id":"4BC8ED5ED08A27BBC10AEEAB38A304D2","kind":"http_failed","request_id":"2473485.8","error":{"text":"net::ERR_CONNECTION_REFUSED","canceled":false}} +{"schema_version":"1.0","session_id":"m0-7bb3bf9e-f607-4f75-bfbb-ebd44c149245","target_id":"4BC8ED5ED08A27BBC10AEEAB38A304D2","kind":"http_body","request_id":"2473485.3","body":{"state":"fetched","storage":"omitted","reason":"default_body_policy","bytes":11}} +{"schema_version":"1.0","session_id":"m0-7bb3bf9e-f607-4f75-bfbb-ebd44c149245","target_id":"4BC8ED5ED08A27BBC10AEEAB38A304D2","kind":"http_body","request_id":"2473485.4","body":{"state":"fetched","storage":"omitted","reason":"default_body_policy","bytes":49}} +{"schema_version":"1.0","session_id":"m0-7bb3bf9e-f607-4f75-bfbb-ebd44c149245","target_id":"4BC8ED5ED08A27BBC10AEEAB38A304D2","kind":"http_body","request_id":"2473485.5","body":{"state":"size_limit","storage":"omitted","bytes":2048}} +{"schema_version":"1.0","session_id":"m0-7bb3bf9e-f607-4f75-bfbb-ebd44c149245","target_id":"4BC8ED5ED08A27BBC10AEEAB38A304D2","kind":"http_body","request_id":"2473485.6","body":{"state":"fetched","storage":"omitted","reason":"default_body_policy","bytes":15}} +{"schema_version":"1.0","session_id":"m0-7bb3bf9e-f607-4f75-bfbb-ebd44c149245","target_id":"4BC8ED5ED08A27BBC10AEEAB38A304D2","kind":"http_body","request_id":"2473485.8","body":{"state":"unavailable","storage":"omitted","reason":"cdp_error","error":"-32000: No data found for resource with given identifier"}} +{"schema_version":"1.0","session_id":"m0-7bb3bf9e-f607-4f75-bfbb-ebd44c149245","target_id":"4BC8ED5ED08A27BBC10AEEAB38A304D2","kind":"cdp_disconnected","source":"socket","code":1006,"reason":"socket_closed","was_clean":false} +{"schema_version":"1.0","session_id":"m0-7bb3bf9e-f607-4f75-bfbb-ebd44c149245","target_id":"4BC8ED5ED08A27BBC10AEEAB38A304D2","kind":"target_closed","reason":"Target.closeTarget"} +{"schema_version":"1.0","session_id":"m0-7bb3bf9e-f607-4f75-bfbb-ebd44c149245","target_id":"AC8BC0503D429618D7033F5AE0731A3C","kind":"cdp_reconnected","verified":true} diff --git a/artifacts/m0-report.json b/artifacts/m0-report.json index 7888de8..33d65bb 100644 --- a/artifacts/m0-report.json +++ b/artifacts/m0-report.json @@ -15,15 +15,16 @@ "response_body_size_limit": "pass", "response_body_unavailable": "pass", "target_close_survives": "pass", - "cdp_disconnect_survives": "pass", - "sensitive_header_redaction": "pass" + "cdp_disconnect_reconnect": "pass", + "sensitive_header_redaction": "pass", + "sensitive_body_redaction": "pass" }, "resources": { "process_count": 9, - "rss_kib": 792008, - "cpu_percent_snapshot": 12.2 + "rss_kib": 803048, + "cpu_percent_snapshot": 10.3 }, - "event_count": 38, + "event_count": 45, "max_body_bytes": 1024, "attempt_failures": [] } diff --git a/docs/m0-spike.md b/docs/m0-spike.md index c96c911..9271c98 100644 --- a/docs/m0-spike.md +++ b/docs/m0-spike.md @@ -17,9 +17,9 @@ The spike starts only loopback listeners and uses generated, one-day self-signed ### Connection and schema -Clark is launched directly with `--remote-debugging-address=127.0.0.1`. The runner waits on `/json/version`, discovers the ready startup page through `/json/list`, connects to that target's `webSocketDebuggerUrl`, and navigates to a controlled loopback fixture. The fixture runs in a separate process and waits for a CDP trigger before generating test traffic. This avoids `clarkserve`'s current `0.0.0.0` default; a protected gateway and short-lived credentials remain required outside a local Spike. +Clark is launched directly with `--remote-debugging-address=127.0.0.1`. The runner waits on `/json/version`, discovers the startup page through `/json/list`, connects to its `webSocketDebuggerUrl`, and navigates to the already-listening loopback fixture before triggering test traffic. This avoids `clarkserve`'s current `0.0.0.0` default; a protected gateway and short-lived credentials remain required outside a local Spike. -Every JSONL line has `schema_version`, `session_id`, `target_id`, and `kind`. HTTP records use `request_id`; WebSocket records use the CDP `requestId` as `connection_id`. Kind-specific fields are nested under `request`, `response`, `headers`, `body`, or `error`. Query strings are removed, sensitive headers are replaced by `[REDACTED]`, WebSocket payloads are never persisted, and bodies over the configured limit are represented by `{state:"omitted",reason:"size_limit",bytes:N}`. +Every JSONL line has `schema_version`, `session_id`, `target_id`, and `kind`. HTTP records use `request_id`; WebSocket records use the CDP `requestId` as `connection_id`. Kind-specific fields are nested under `request`, `response`, `headers`, `body`, or `error`. Query strings are removed, sensitive headers are replaced by `[REDACTED]`, and WebSocket payloads are never persisted. Response bodies are safe by default: a fetched body is recorded as `{state:"fetched",storage:"omitted",reason:"default_body_policy",bytes:N}` with no content; oversize and unavailable bodies retain distinct structured states. ### Coverage and limits @@ -29,17 +29,18 @@ Every JSONL line has `schema_version`, `session_id`, `target_id`, and `kind`. HT | Failed HTTP request | Pass | Unused loopback port produces `loadingFailed`; error text retained. | | HTTPS without MITM | Pass | Direct browser connection to local TLS fixture; no network intermediary. | | WebSocket lifecycle and frames | Pass | Created, handshake, sent, received, and closed share one `connection_id`; payload omitted. | -| `getResponseBody` success | Pass | Called after `loadingFinished` for controlled responses. | -| Body size limit | Pass | 2 KiB fixture is not persisted with the 1 KiB default. | +| `getResponseBody` success | Pass | Called after `loadingFinished`; fetched bytes are recorded but content is omitted by default. | +| Sensitive response body | Pass | Random secret fixture is fetched; serialized JSONL is asserted not to contain its value. | +| Body size limit | Pass | 2 KiB fixture is reported as `size_limit` and not persisted with the 1 KiB default. | | Body unavailable | Pass | Calling on the failed request returns the original CDP error as structured `cdp_error`. | | Target close | Pass | Target connection closes; browser process remains alive. | -| Client disconnect | Pass | A second CDP client disconnects; browser process remains alive. | +| CDP disconnect/reconnect | Pass | Browser-side target close triggers the real socket `close` callback; the browser stays alive and a new target accepts a verified CDP command. | | Cookie / Authorization / Set-Cookie | Pass | `Network.*ExtraInfo` values are redacted before JSONL serialization. | | Cache-specific body availability | Not covered | Cache behavior varies by response/cache mode; retain optional-body semantics in M1. | | Sustained high-rate WebSocket | Not covered | M0 stores metadata only; add frame/event caps before M1 load testing. | | Multi-target/session isolation | Not covered | Explicitly M1 scope. | -| Top-level loopback navigation | Intermittent | This build can report `Inspector.detached: Render process gone` before the request reaches the fixture; rerun preserves the same local reproduction condition. | +| Startup `Page.navigate` | Intermittent | This build can report `Inspector.detached: Render process gone`; the exact startup failure is bounded and recorded. | The resource numbers are a one-time `ps` snapshot of the browser process tree, not a benchmark. Repeat under the intended container limits before setting capacity targets. -The runner restarts Clark at most three times only for the observed `Inspector.detached` startup failure. Every failed attempt is retained in `attempt_failures`; other errors fail immediately. +The runner restarts Clark at most three times only from the local startup navigation/readiness block, and only after CDP reports the exact detached reason `Render process gone.`. Every failed attempt is retained in `attempt_failures`; a timeout without that reason and every post-startup error fail immediately with the original message. diff --git a/scripts/m0-spike.mjs b/scripts/m0-spike.mjs index 753f629..196bbe7 100644 --- a/scripts/m0-spike.mjs +++ b/scripts/m0-spike.mjs @@ -48,23 +48,35 @@ export function safeUrl(raw) { export function boundedBody(body, base64Encoded, maxBytes) { const bytes = Buffer.from(body, base64Encoded ? "base64" : "utf8"); if (bytes.length > maxBytes) { - return { state: "omitted", reason: "size_limit", bytes: bytes.length }; + return { state: "size_limit", storage: "omitted", bytes: bytes.length }; } return { - state: "stored", + state: "fetched", + storage: "omitted", + reason: "default_body_policy", bytes: bytes.length, - encoding: "utf8", - text: bytes.toString("utf8"), }; } +export function shouldRetryStartup(detachedReason) { + return detachedReason === "Render process gone."; +} + +class StartupNavigateFailure extends Error { + constructor(cause) { + super(cause.message, { cause }); + } +} + class CDP { constructor(url) { this.url = url; this.nextId = 1; this.pending = new Map(); this.listeners = []; + this.closeListeners = []; this.closed = new Promise((resolve) => (this.resolveClosed = resolve)); + this.detached = new Promise((resolve) => (this.resolveDetached = resolve)); } async connect() { @@ -74,10 +86,12 @@ class CDP { this.ws.onerror = () => reject(new Error("CDP WebSocket connection failed")); }); this.ws.onmessage = ({ data }) => this.onMessage(JSON.parse(data)); - this.ws.onclose = () => { - for (const { reject } of this.pending.values()) reject(new Error("CDP WebSocket closed")); + this.ws.onclose = ({ code, reason, wasClean }) => { + const details = { code, reason: reason || "socket_closed", was_clean: wasClean }; + for (const { reject } of this.pending.values()) reject(new Error(`CDP WebSocket closed: ${code} ${details.reason}`)); this.pending.clear(); - this.resolveClosed(); + for (const listener of this.closeListeners) listener(details); + this.resolveClosed(details); }; return this; } @@ -86,6 +100,17 @@ class CDP { this.listeners.push(listener); } + onClose(listener) { + this.closeListeners.push(listener); + } + + waitForDetached(timeoutMs) { + return Promise.race([ + this.detached, + new Promise((resolve) => setTimeout(resolve, timeoutMs)), + ]); + } + onMessage(message) { if (DEBUG) console.error( "CDP", @@ -102,6 +127,8 @@ class CDP { return; } if (message.method === "Inspector.detached") { + this.detachedReason = message.params.reason; + this.resolveDetached(message.params.reason); const error = new Error(`Inspector.detached: ${message.params.reason}`); for (const { reject } of this.pending.values()) reject(error); this.pending.clear(); @@ -109,13 +136,13 @@ class CDP { for (const listener of this.listeners) listener(message.method, message.params ?? {}); } - call(method, params = {}) { + call(method, params = {}, timeoutMs = 20_000) { const id = this.nextId++; return new Promise((resolve, reject) => { const timer = setTimeout(() => { this.pending.delete(id); reject(new Error(`CDP command timed out: ${method}`)); - }, 20_000); + }, timeoutMs); this.pending.set(id, { resolve: (value) => { clearTimeout(timer); resolve(value); }, reject: (error) => { clearTimeout(timer); reject(error); }, @@ -192,6 +219,7 @@ function acceptWebsocket(request, socket) { async function startFixtures(tempDir) { const secrets = { authorization: randomUUID(), + body: randomUUID(), cookie: randomUUID(), setCookie: randomUUID(), }; @@ -248,6 +276,10 @@ async function startFixtures(tempDir) { send(response, { ...cors, "content-type": "text/plain" }, "x".repeat(2_048)); return; } + if (request.url === "/secret") { + send(response, { ...cors, "content-type": "application/json" }, JSON.stringify({ secret: secrets.body })); + return; + } send(response, { "content-type": "text/html" }, pageHtml); }); httpServer.on("upgrade", (request, socket) => { @@ -261,6 +293,7 @@ async function startFixtures(tempDir) { document.title = "M0_RUNNING"; try { await (await fetch("http://127.0.0.1:${httpPort}/ok")).text(); + await (await fetch("http://127.0.0.1:${httpPort}/secret")).text(); await (await fetch("http://127.0.0.1:${httpPort}/large")).text(); await (await fetch("https://127.0.0.1:${httpsPort}/secure")).text(); await new Promise((resolve, reject) => { @@ -367,15 +400,6 @@ function processSnapshot(rootPid) { }; } -function browserPidForPort(port) { - const result = spawnSync("ps", ["-eo", "pid=,args="], { encoding: "utf8" }); - assert.equal(result.status, 0, "ps must locate the browser process"); - const marker = `--remote-debugging-port=${port}`; - const line = result.stdout.split("\n").find((row) => row.includes(marker) && !row.includes("scripts/m0-spike.mjs")); - assert(line, `browser process with ${marker} must exist`); - return Number(line.trim().split(/\s+/, 1)[0]); -} - function isRunning(pid) { try { process.kill(pid, 0); @@ -486,14 +510,14 @@ async function collectBody(cdp, collector, requestId, maxBodyBytes) { } catch (error) { collector.push("http_body", { request_id: requestId, - body: { state: "unavailable", reason: "cdp_error", error: error.message }, + body: { state: "unavailable", storage: "omitted", reason: "cdp_error", error: error.message }, }); } } function assertCoverage(collector, secrets) { const { records, requestState } = collector; - for (const pathname of ["/ok", "/large", "/secure"]) { + for (const pathname of ["/ok", "/secret", "/large", "/secure"]) { const id = findRequest(records, pathname); assert(id, `request observed for ${pathname}`); assert.deepEqual([...requestState.get(id)].filter((value) => ["request", "response", "finished"].includes(value)).sort(), ["finished", "request", "response"]); @@ -509,8 +533,12 @@ function assertCoverage(collector, secrets) { assert(records.some((record) => record.kind === "http_request_headers" && record.headers.cookie === REDACTED), "Cookie is redacted"); assert(records.some((record) => record.kind === "http_request_headers" && record.headers.authorization === REDACTED), "Authorization is redacted"); assert(records.some((record) => record.kind === "http_response_headers" && record.headers["set-cookie"] === REDACTED), "Set-Cookie is redacted"); - assert(records.some((record) => record.kind === "http_body" && record.body.reason === "size_limit"), "oversize body is omitted"); + const secretBody = records.find((record) => record.kind === "http_body" && record.request_id === findRequest(records, "/secret")); + assert.deepEqual(secretBody?.body, { state: "fetched", storage: "omitted", reason: "default_body_policy", bytes: Buffer.byteLength(JSON.stringify({ secret: secrets.body })) }); + assert(records.some((record) => record.kind === "http_body" && record.body.state === "size_limit" && record.body.storage === "omitted"), "oversize body is omitted"); assert(records.some((record) => record.kind === "http_body" && record.body.state === "unavailable"), "unavailable body is structured"); + assert(records.some((record) => record.kind === "cdp_disconnected" && record.source === "socket"), "real CDP socket close is recorded"); + assert(records.some((record) => record.kind === "cdp_reconnected" && record.verified), "CDP reconnect is verified"); } async function main(attemptFailures = []) { @@ -525,7 +553,7 @@ async function main(attemptFailures = []) { const cdpPort = Number(process.env.M0_CDP_PORT ?? 0) || await unusedPort(); const fixtures = await startFixtureProcess(tempDir); assert.equal((await fetch(fixtures.url)).status, 200, "fixture process must be reachable before Clark starts"); - spawn("setsid", ["--fork", binary, + const browser = spawn(binary, [ "--headless=new", "--no-sandbox", "--use-mock-keychain", @@ -537,32 +565,40 @@ async function main(attemptFailures = []) { "--disable-gpu", "--disable-features=WebGPU", "about:blank", - ], { stdio: ["ignore", "ignore", DEBUG ? "inherit" : "ignore"] }); + ], { stdio: ["ignore", "ignore", "pipe"] }); + browser.stderr.pipe(process.stderr); const sessionId = `m0-${randomUUID()}`; - let browserPid; + const browserPid = browser.pid; let page; let browserClient; + let reconnectClient; try { const version = await waitFor( () => getJson(`http://127.0.0.1:${cdpPort}/json/version`).catch(() => null), "clark CDP endpoint", 20_000, ); - browserPid = browserPidForPort(cdpPort); const targets = await getJson(`http://127.0.0.1:${cdpPort}/json/list`); page = targets.find((target) => target.type === "page" && target.url === "about:blank"); assert(page, "clark must expose its startup page through /json/list"); const cdp = await new CDP(page.webSocketDebuggerUrl).connect(); const collector = eventCollector(sessionId, page.id); + cdp.onClose((details) => collector.push("cdp_disconnected", { source: "socket", ...details })); await cdp.call("Page.enable"); await cdp.call("Runtime.enable"); - await new Promise((resolve) => setTimeout(resolve, 500)); - await cdp.call("Page.navigate", { url: fixtures.url }); - await waitFor(async () => { - const { result } = await cdp.call("Runtime.evaluate", { expression: "document.title", returnByValue: true }); - return result.value === "M0_READY"; - }, "fixture page readiness"); + try { + await cdp.call("Page.navigate", { url: fixtures.url }, 60_000); + await waitFor(async () => { + const { result } = await cdp.call("Runtime.evaluate", { expression: "document.title", returnByValue: true }); + return result.value === "M0_READY"; + }, "fixture page readiness"); + } catch (error) { + const detachedReason = cdp.detachedReason ?? await cdp.waitForDetached(1_000); + if (DEBUG) console.error("startup failure", { detached_reason: detachedReason, error: error.message }); + if (shouldRetryStartup(detachedReason)) throw new StartupNavigateFailure(error); + throw error; + } cdp.onEvent(collector.listener); await cdp.call("Network.enable", { maxTotalBufferSize: 10_000_000, maxResourceBufferSize: 2_000_000 }); await cdp.call("Network.setExtraHTTPHeaders", { headers: { @@ -577,7 +613,7 @@ async function main(attemptFailures = []) { }, "fixture completion"); await waitFor(() => [...collector.requestState.values()].some((state) => state.has("ws_closed")), "WebSocket closure"); - for (const pathname of ["/ok", "/large", "/secure"]) { + for (const pathname of ["/ok", "/secret", "/large", "/secure"]) { await collectBody(cdp, collector, findRequest(collector.records, pathname), maxBodyBytes); } const failedId = [...collector.requestState.entries()].find(([, state]) => state.has("failed"))?.[0]; @@ -590,11 +626,13 @@ async function main(attemptFailures = []) { collector.push("target_closed", { reason: "Target.closeTarget" }); assert(isRunning(browserPid), "closing the page does not exit clark-browser"); - const disconnectTarget = await getJson(`http://127.0.0.1:${cdpPort}/json/new?about:blank`, { method: "PUT" }); - const disconnectClient = await new CDP(disconnectTarget.webSocketDebuggerUrl).connect(); - await disconnectClient.close(); - collector.push("cdp_disconnected", { reason: "client_closed" }); - assert(isRunning(browserPid), "disconnecting CDP does not exit clark-browser"); + assert(collector.records.some((record) => record.kind === "cdp_disconnected" && record.source === "socket"), "target closure emits a socket disconnect event"); + const reconnectTarget = await getJson(`http://127.0.0.1:${cdpPort}/json/new?about:blank`, { method: "PUT" }); + reconnectClient = await new CDP(reconnectTarget.webSocketDebuggerUrl).connect(); + const { result: reconnectResult } = await reconnectClient.call("Runtime.evaluate", { expression: "1 + 1", returnByValue: true }); + assert.equal(reconnectResult.value, 2, "CDP reconnect can execute a command"); + collector.push("cdp_reconnected", { target_id: reconnectTarget.id, verified: true }); + assert(isRunning(browserPid), "disconnecting and reconnecting CDP does not exit clark-browser"); assertCoverage(collector, fixtures.secrets); await mkdir(outputDir, { recursive: true }); @@ -616,8 +654,9 @@ async function main(attemptFailures = []) { response_body_size_limit: "pass", response_body_unavailable: "pass", target_close_survives: "pass", - cdp_disconnect_survives: "pass", + cdp_disconnect_reconnect: "pass", sensitive_header_redaction: "pass", + sensitive_body_redaction: "pass", }, resources, event_count: collector.records.length, @@ -626,9 +665,10 @@ async function main(attemptFailures = []) { }, null, 2)}\n`); console.log(`M0 PASS: ${collector.records.length} sanitized events; RSS snapshot ${resources.rss_kib} KiB`); } finally { + if (reconnectClient) await reconnectClient.close().catch(() => {}); if (browserClient) await browserClient.close().catch(() => {}); - if (browserPid && isRunning(browserPid)) process.kill(browserPid, "SIGTERM"); - if (browserPid) await waitFor(() => !isRunning(browserPid), "browser shutdown").catch(() => {}); + if (browser.exitCode === null) browser.kill("SIGTERM"); + if (browser.exitCode === null) await new Promise((resolve) => browser.once("exit", resolve)); await fixtures.close(); await rm(tempDir, { recursive: true, force: true }); } @@ -641,11 +681,10 @@ async function runSpike() { await main(failures); return; } catch (error) { - failures.push(error.message); - const retryable = error.message.startsWith("Inspector.detached:") || - error.message === "CDP command timed out: Page.navigate"; - if (!retryable || attempt === 3) throw error; - console.error(`M0 retry ${attempt}/3: ${error.message}`); + const original = error.cause ?? error; + failures.push(original.message); + if (!(error instanceof StartupNavigateFailure) || attempt === 3) throw original; + console.error(`M0 retry ${attempt}/3: ${original.message}`); } } } diff --git a/test/m0-spike.test.mjs b/test/m0-spike.test.mjs index d9ab3a6..25cd17d 100644 --- a/test/m0-spike.test.mjs +++ b/test/m0-spike.test.mjs @@ -2,7 +2,7 @@ import assert from "node:assert/strict"; import { randomUUID } from "node:crypto"; import test from "node:test"; -import { boundedBody, redactHeaders, safeUrl } from "../scripts/m0-spike.mjs"; +import { boundedBody, redactHeaders, safeUrl, shouldRetryStartup } from "../scripts/m0-spike.mjs"; test("export policy redacts credentials and bounds bodies", () => { const sensitive = randomUUID(); @@ -14,6 +14,10 @@ test("export policy redacts credentials and bounds bodies", () => { "set-cookie": "[REDACTED]", }); assert.equal(safeUrl("https://example.test/path?token=secret"), "https://example.test/path?"); - assert.deepEqual(boundedBody("hello", false, 5), { state: "stored", bytes: 5, encoding: "utf8", text: "hello" }); - assert.deepEqual(boundedBody("hello!", false, 5), { state: "omitted", reason: "size_limit", bytes: 6 }); + assert.deepEqual(boundedBody(sensitive, false, 100), { state: "fetched", storage: "omitted", reason: "default_body_policy", bytes: Buffer.byteLength(sensitive) }); + assert(!JSON.stringify(boundedBody(sensitive, false, 100)).includes(sensitive)); + assert.deepEqual(boundedBody("hello!", false, 5), { state: "size_limit", storage: "omitted", bytes: 6 }); + assert(shouldRetryStartup("Render process gone.")); + assert(!shouldRetryStartup("Target closed.")); + assert(!shouldRetryStartup(undefined)); });