diff --git a/docs/plans/lab-workflow-reliability/reports/access-browser-ci-20261020/acceptance.md b/docs/plans/lab-workflow-reliability/reports/access-browser-ci-20261020/acceptance.md new file mode 100644 index 00000000..709f8b4b --- /dev/null +++ b/docs/plans/lab-workflow-reliability/reports/access-browser-ci-20261020/acceptance.md @@ -0,0 +1,68 @@ +# Access browser navigation and shutdown qualification + +The local repair descends from PR112 at +`0dedbc953902ac89389dca0693006aaa02a0a87c` on +`fix/access-browser-ci-qualification-20261020`. Root owns subsequent serial +integration. This branch is unpublished and retained for review; no remote PR, +workflow, branch or hosting state changed. + +The implementation changes three test files only. Both the initial workbench +navigation and scoped-agent reload capture the previous main frame/loader and +wait for a changed main loader with its matching load event before reading DOM. +The helper reuses the existing CDP session and load tracker. Request, connection +and document-load waits are bounded; failure context contains phase, method and +bounded frame/loader identities without command parameters. All 14 original +assertions remain, with one added assertion selecting the explicitly launched +blank page. No authentication or product behavior changes. + +Shutdown now awaits the existing supplied-child helper before bounded removal of +the exact test profile. Primary access failures and secondary cleanup failures +retain separate labels and a nonzero outcome. Main-handle termination does not +establish an unconditional Chromium-descendant shutdown guarantee. + +## Evidence + +- [Controlled regression](../../../../../tests/test_browser_access_navigation.cjs) + reproduces the old navigation/reload acknowledgement ordering with the exact + `-32000` error. The corrected sequence issues no DOM read before the changed + main loader's load; old-loader, subframe and DOMContentLoaded events cannot + release the gate. Target closure and request/load timeout cases remain failures. + This is a controlled protocol reproduction, not the original hosted trace. +- Three syntax checks, the new regression, the existing page-load regression, + owned-child delayed-write/ignored-TERM regression and diff check passed. +- The unchanged default command `node tests/browser_access.cjs` passed against + actual existing PostgreSQL17.6, Python3.12.14, Node24.19.0 and Chromium151 in + 8.605350 seconds. It includes the new regression before its real browser journey. + [Raw log](actual-access.log), [actual receipt](receipt.json), [runner](runner-receipt.json) + and desktop/narrow screenshots retain the result. +- The initial owner login still uses an actual one-time CLI code and interactive + browser form, not a preinstalled fixture cookie. Anonymous denials, HttpOnly/code + clearing, retained drafts, no mutation replay, deliberate commit and final + scoped-agent review denial all pass. The final scoped-agent switch installs its + separately issued real credential as in the original test. +- [Cleanup](post-run-cleanup.json) records the exact preregistered schema queried + absent after disposal, zero workspace Runs, terminal supplied app/browser + handles, absent exact profile and no matching task process at observation. + Only the disposable fixture was removed; no service or package was installed + or reconfigured. Reviewer inspected these receipts, not a second PG/browser run. +- [Independent review](independent-review.json) clears source, focused checks and + actual evidence. It rechecks all three tested source hashes. The actual receipt + records precommit HEAD0ded with dirty source; file hashes bind the tested repair + bytes and must match the eventual local commit. It does not claim 0ded already + contained the repair. + +## Original failure and remaining gates + +Original run39370107550/job118455819881 remains failed at access step41. +Its [excerpt](original-failure-excerpt.log), full decoded-log SHA and source +identity are retained in [qualification](qualification.json). The log provides +no failing command/phase trace, so exact navigation versus target closure/crash +remains unproven. Cleanup ENOTEMPTY followed the primary error and is separate. +The artifact ZIP was proxy403 blocked and its contents remain uninspected. + +The running sibling CI was untouched. No cancellation, rerun, source publication, +merge or branch/worktree cleanup occurred. This local access qualification is +not a passing full repaired-head lifecycle; fresh hosted gates and independent +integration review remain necessary when publication is authorized. PUB-1 original +diagnostic evidence and SCI-1 owned criterion/compatible real lab pair remain +missing. No training, scientific, CUDA or model-quality acceptance is inferred. diff --git a/docs/plans/lab-workflow-reliability/reports/access-browser-ci-20261020/actual-access.log b/docs/plans/lab-workflow-reliability/reports/access-browser-ci-20261020/actual-access.log new file mode 100644 index 00000000..0515b4c7 --- /dev/null +++ b/docs/plans/lab-workflow-reliability/reports/access-browser-ci-20261020/actual-access.log @@ -0,0 +1,3 @@ +PASS access navigation/reload reproduction: old acknowledgement permits failing DOM read; changed main loader plus its load gates the original assertion +PASS access phase-specific timeout/target-close failures are bounded; diagnostics omit command parameters +PASS access browser /tmp/tuldok-access-browser-ci-repair-20261020/qa/access/run-vVR332 diff --git a/docs/plans/lab-workflow-reliability/reports/access-browser-ci-20261020/cleanup.json b/docs/plans/lab-workflow-reliability/reports/access-browser-ci-20261020/cleanup.json new file mode 100644 index 00000000..ddf102bc --- /dev/null +++ b/docs/plans/lab-workflow-reliability/reports/access-browser-ci-20261020/cleanup.json @@ -0,0 +1,17 @@ +{ + "temporary": "/tmp/tuldok-access-browser-7VR9Z3", + "removed": true, + "children": [ + { + "pid": 119512, + "exit_code": null, + "signal": "SIGTERM" + }, + { + "pid": 119521, + "exit_code": 0, + "signal": null + } + ], + "scope": "Supplied test-owned main handles exited before bounded profile removal" +} diff --git a/docs/plans/lab-workflow-reliability/reports/access-browser-ci-20261020/independent-review.json b/docs/plans/lab-workflow-reliability/reports/access-browser-ci-20261020/independent-review.json new file mode 100644 index 00000000..e6112e9e --- /dev/null +++ b/docs/plans/lab-workflow-reliability/reports/access-browser-ci-20261020/independent-review.json @@ -0,0 +1,171 @@ +{ + "actual_qualification_review": { + "auth_scope": "Actual default entrypoint uses ordinary CLI one-time owner login, verifies replay denial, HttpOnly/code clearance, preserved unsaved drafts, no mutation replay and explicit successful retry. Existing final scoped-agent branch deliberately installs a real issued agent credential cookie and verifies human-review controls disabled; no fixture owner-cookie bootstrap is substituted.", + "checks": { + "actual_exit_zero": true, + "all3_original_auth_checks_complete": true, + "both_new_main_loads_recorded": true, + "exact_schema_absence_recorded": true, + "exact_schema_and_path_bound": true, + "exact_temporary_absence_and_no_matching_processes_recorded": true, + "three_source_hashes_exact_review_actual_and_current": true, + "two_supplied_main_handles_terminal_and_match": true, + "zero_workspace_runs": true + }, + "cleanup_limit": "Receipts support exact preregistered schema absent,0workspaceRuns, two supplied main handles terminal before rm, exact profile absent/no taskprocess matches at recorded observation. No blanket all-Chrome-descendant guarantee.", + "command": "node tests/browser_access.cjs", + "environment": "Root reports existing Python3.12.14, PostgreSQL17.6 and Chromium151; no installation/service/config change. Version commands not independently rerun by reviewer.", + "evidence": [ + { + "bytes": 763, + "path": "/tmp/tuldok-access-browser-ci-repair-20261020/runner-receipt.json", + "sha256": "b247a3d1147a47f0304649ae2a110969c2da7c1d6e4cd5a0abe8d82430753ef9" + }, + { + "bytes": 346, + "path": "/tmp/tuldok-access-browser-ci-repair-20261020/actual-access.log", + "sha256": "ae63a93687389ad38ed33d1ecbe7fe8916db65afff8222c4a9acfe8b07f4b2f4" + }, + { + "bytes": 1433, + "path": "/tmp/tuldok-access-browser-ci-repair-20261020/post-run-cleanup.json", + "sha256": "3fefda92278099fc5e93f1c05f363277b72469535a8dbd9cd83f5832023a6fc1" + }, + { + "bytes": 1693, + "path": "/tmp/tuldok-access-browser-ci-repair-20261020/qa/access/run-vVR332/receipt.json", + "sha256": "57b652807e1ac0e89d9f422c0c551051e33782ec779a016f0046c3ed48c6268d" + }, + { + "bytes": 341, + "path": "/tmp/tuldok-access-browser-ci-repair-20261020/qa/access/run-vVR332/cleanup.json", + "sha256": "06b2c257bca7797d663ad809335504a2a2d56a7b41eae4edf77b2049b110538e" + }, + { + "bytes": 92512, + "path": "/tmp/tuldok-access-browser-ci-repair-20261020/qa/access/run-vVR332/session-1280.jpg", + "sha256": "ae5ecddb60cd751033a62eb24ee383daa4d56da85439115b080178b18b5c5580" + }, + { + "bytes": 46763, + "path": "/tmp/tuldok-access-browser-ci-repair-20261020/qa/access/run-vVR332/session-390.jpg", + "sha256": "71f975c51592a0a92669c3d5213b21561acbc83cd4450a532b9e1a2cb7907519" + } + ], + "execution_owner": "Root executed actual PostgreSQL/HTTP/Chromium journey; this reviewer inspected retained bytes/receipts, did not repeat browser/server/PG.", + "exit": 0, + "finished_utc": "2026-10-20T06:56:00.320089+00:00", + "historical_failure_limit": "No new host event trace: controlled old-red/new-green and actual successful repair do not retroactively prove original failed-host cause.", + "loader_events": [ + { + "event": "load", + "frame": "429B7ADFD788931A1DAC0F8F1B79610A", + "loader": "C704421B4745793C1F166C7CC67C83F8" + }, + { + "event": "main-frame", + "frame": "429B7ADFD788931A1DAC0F8F1B79610A", + "loader": "7534733FE54346FBED7374DBFE2EEB98" + }, + { + "event": "load", + "frame": "429B7ADFD788931A1DAC0F8F1B79610A", + "loader": "7534733FE54346FBED7374DBFE2EEB98" + }, + { + "event": "main-frame", + "frame": "429B7ADFD788931A1DAC0F8F1B79610A", + "loader": "7B0CA169AEB6A2150994309812BCD5B7" + }, + { + "event": "load", + "frame": "429B7ADFD788931A1DAC0F8F1B79610A", + "loader": "7B0CA169AEB6A2150994309812BCD5B7" + } + ], + "reviewed_utc": "2026-10-20T06:57:54.185655+00:00", + "seconds": 8.60535, + "source_attribution": "Actual run used base0ded checkout with reviewed modified file bytes. Three file hashes agree in source-review/currentfiles/browserreceipt/postcleanup; do not claim base0ded includes repair or a later commit was literally executed.", + "source_dirty_at_execution": true, + "source_head_at_execution": "0dedbc953902ac89389dca0693006aaa02a0a87c", + "started_utc": "2026-10-20T06:55:51.714739+00:00" + }, + "base": "0dedbc953902ac89389dca0693006aaa02a0a87c", + "blocking_findings": [], + "cleanup_review": { + "limit": "Existing helper confirms supplied main child handles only, not an unconditional Chrome-descendant extinction guarantee; actual fixture run and final cleanup observation pending.", + "order": "Dispose session/socket; await existing stopOwnedChildren only on supplied children; bounded recursive removal maxRetries10/retryDelay100; write terminal-main-handle receipt.", + "primary_preservation": "Original failure printed as Primary access failure and recorded phase/protocol metadata; exitCode1 set before finally. Cleanup failure separately prints Secondary access cleanup failure and retains nonzero exit." + }, + "disposition": "CLEAR within reviewed three-file test-harness source plus actual default access qualification scope; external publication/CI acceptance remain parent-owned", + "historical_failure": { + "cause_limit": "Source race is demonstrated in controlled mock protocol scheduling; original hosted execution had no phase/pointer/context trace. Target closure/crash remains unexcluded. Original failed run is not reclassified.", + "head": "0dedbc953902ac89389dca0693006aaa02a0a87c", + "job": 118455819881, + "primary": "Inspected target navigated or closed", + "run": 39370107550, + "secondary": "ENOTEMPTY after unawaited TERM/removal" + }, + "independent_tests": { + "command": "node --check tests/browser_access.cjs && node --check tests/browser_access_session.cjs && node --check tests/test_browser_access_navigation.cjs && node tests/test_browser_access_navigation.cjs && node tests/test_browser_page_load.cjs", + "exit": 0, + "not_run_by_reviewer": [ + "Actual browser/HTTP/PostgreSQL", + "Owned-child regression(no need duplicate existingroot pass)", + "Heavy suites/CI" + ], + "results": [ + "Controlled old navigation/reload ACK\u2192departing-DOM read produces -32000", + "New helper issues zero DOM reads until changed main loader matchingload; oldloader/subframe/DOMContentLoaded insufficient", + "Navigation load timeout and targetclose fail within configured bounds", + "Request timeout and snapshots omit MUST_NOT_APPEAR command token", + "Existing fresh-document helper regression passes" + ] + }, + "limits": [ + "No product/API/domain changes; exact source diff limited to three test files.", + "No remote mutation/CI polling/source edit or server/browser execution by reviewer.", + "PUB-1 and SCI-1 unchanged.", + "Actual evidence inspected, not independently rerun; no performance claim from8.605350s descriptive single-run wall time. Parent prohibits external publication, merging, branch cleanup or environment changes." + ], + "navigation_review": { + "both_transitions": [ + "Initial Page.navigate to workbench", + "Scoped-agent Page.reload after exact cookie replacement" + ], + "diagnostics": "Bounded last12main-frame/load identities plus phase and last method; command parameters absent. contextual errors preserve underlying code/cause.", + "exact_target": "Initial page target must type page and url about:blank; connection deadline10000ms.", + "failure_boundaries": "Default CDP requests15000ms; new-load wait20000ms with25ms polling; explicit errorText/target close/error refuse. No blanket navigation-error suppression or mutation retry.", + "fresh_dom_checks": "Original session dialog/identity/Collection-ready checks remain after fresh-document gate.", + "owner": "accessSession captures Page.getFrameTree previous main id+loader before command; pageLoadTracker observes Page.frameNavigated and lifecycle load. Only changed main loader with matching load releases DOM checks." + }, + "original_assertions": { + "all14_original_calls_present_verbatim": true, + "base_calls": 14, + "current_calls": 15, + "extra": "Explicitly launched about:blank target assertion", + "preserved_semantics": [ + "Anonymous legacy/current denied writes", + "One-time CLI login consumed/code cleared/HttpOnly cookie", + "Signout and rejected mutation preserve unsaved draft", + "Reauthentication never replays original mutation", + "Only deliberate retry changes revision", + "Real scoped agent cannot use owner human-review controls", + "No runtime exceptions or application stderr" + ] + }, + "reviewed_utc": "2026-10-20T06:54:04.651464+00:00", + "source_only_review_history": { + "old_disposition": "CLEAR within test-harness source/focused-test scope; actual default PostgreSQL/Chromium access qualification remains pending", + "path": "/tmp/tuldok-access-browser-ci-independent-review-source-only.json", + "sha256": "3b8d598bd31f575c0d8b20780a275abd9b0df10d7ee8a589b21a4968eccf56e3" + }, + "source_sha256": { + "tests/browser_access.cjs": "e3c1bcd19d73981a39a3adcc0d32d7e67029907e0cfd689e2739348efbf0e56f", + "tests/browser_access_session.cjs": "d61b7cf0c366134050d6bba3542fd209005c5ad22f10a3659afe0ca8f948e1a5", + "tests/test_browser_access_navigation.cjs": "56ae9b76f1aaa3803a90f03468129f243d0b37f6a50110bcf7c13840da18f80e" + }, + "source_state": "Uncommitted exact three test-file repair; root owns final commit/publication.", + "status_porcelain": " M tests/browser_access.cjs\n?? tests/browser_access_session.cjs\n?? tests/test_browser_access_navigation.cjs\n", + "worktree": "/workspace/Tuldok-access-browser-ci-repair-20261020" +} diff --git a/docs/plans/lab-workflow-reliability/reports/access-browser-ci-20261020/independent-source-review.json b/docs/plans/lab-workflow-reliability/reports/access-browser-ci-20261020/independent-source-review.json new file mode 100644 index 00000000..9aa653c4 --- /dev/null +++ b/docs/plans/lab-workflow-reliability/reports/access-browser-ci-20261020/independent-source-review.json @@ -0,0 +1,75 @@ +{ + "base": "0dedbc953902ac89389dca0693006aaa02a0a87c", + "blocking_findings": [], + "cleanup_review": { + "limit": "Existing helper confirms supplied main child handles only, not an unconditional Chrome-descendant extinction guarantee; actual fixture run and final cleanup observation pending.", + "order": "Dispose session/socket; await existing stopOwnedChildren only on supplied children; bounded recursive removal maxRetries10/retryDelay100; write terminal-main-handle receipt.", + "primary_preservation": "Original failure printed as Primary access failure and recorded phase/protocol metadata; exitCode1 set before finally. Cleanup failure separately prints Secondary access cleanup failure and retains nonzero exit." + }, + "disposition": "CLEAR within test-harness source/focused-test scope; actual default PostgreSQL/Chromium access qualification remains pending", + "historical_failure": { + "cause_limit": "Source race is demonstrated in controlled mock protocol scheduling; original hosted execution had no phase/pointer/context trace. Target closure/crash remains unexcluded. Original failed run is not reclassified.", + "head": "0dedbc953902ac89389dca0693006aaa02a0a87c", + "job": 118455819881, + "primary": "Inspected target navigated or closed", + "run": 39370107550, + "secondary": "ENOTEMPTY after unawaited TERM/removal" + }, + "independent_tests": { + "command": "node --check tests/browser_access.cjs && node --check tests/browser_access_session.cjs && node --check tests/test_browser_access_navigation.cjs && node tests/test_browser_access_navigation.cjs && node tests/test_browser_page_load.cjs", + "exit": 0, + "not_run_by_reviewer": [ + "Actual browser/HTTP/PostgreSQL", + "Owned-child regression(no need duplicate existingroot pass)", + "Heavy suites/CI" + ], + "results": [ + "Controlled old navigation/reload ACK\u2192departing-DOM read produces -32000", + "New helper issues zero DOM reads until changed main loader matchingload; oldloader/subframe/DOMContentLoaded insufficient", + "Navigation load timeout and targetclose fail within configured bounds", + "Request timeout and snapshots omit MUST_NOT_APPEAR command token", + "Existing fresh-document helper regression passes" + ] + }, + "limits": [ + "No product/API/domain changes; exact source diff limited to three test files.", + "Root must execute and retain actual default access journey against these exact source hashes before broader acceptance.", + "No remote mutation/CI polling/source edit or server/browser execution by reviewer.", + "PUB-1 and SCI-1 unchanged." + ], + "navigation_review": { + "both_transitions": [ + "Initial Page.navigate to workbench", + "Scoped-agent Page.reload after exact cookie replacement" + ], + "diagnostics": "Bounded last12main-frame/load identities plus phase and last method; command parameters absent. contextual errors preserve underlying code/cause.", + "exact_target": "Initial page target must type page and url about:blank; connection deadline10000ms.", + "failure_boundaries": "Default CDP requests15000ms; new-load wait20000ms with25ms polling; explicit errorText/target close/error refuse. No blanket navigation-error suppression or mutation retry.", + "fresh_dom_checks": "Original session dialog/identity/Collection-ready checks remain after fresh-document gate.", + "owner": "accessSession captures Page.getFrameTree previous main id+loader before command; pageLoadTracker observes Page.frameNavigated and lifecycle load. Only changed main loader with matching load releases DOM checks." + }, + "original_assertions": { + "all14_original_calls_present_verbatim": true, + "base_calls": 14, + "current_calls": 15, + "extra": "Explicitly launched about:blank target assertion", + "preserved_semantics": [ + "Anonymous legacy/current denied writes", + "One-time CLI login consumed/code cleared/HttpOnly cookie", + "Signout and rejected mutation preserve unsaved draft", + "Reauthentication never replays original mutation", + "Only deliberate retry changes revision", + "Real scoped agent cannot use owner human-review controls", + "No runtime exceptions or application stderr" + ] + }, + "reviewed_utc": "2026-10-20T06:54:04.651464+00:00", + "source_sha256": { + "tests/browser_access.cjs": "e3c1bcd19d73981a39a3adcc0d32d7e67029907e0cfd689e2739348efbf0e56f", + "tests/browser_access_session.cjs": "d61b7cf0c366134050d6bba3542fd209005c5ad22f10a3659afe0ca8f948e1a5", + "tests/test_browser_access_navigation.cjs": "56ae9b76f1aaa3803a90f03468129f243d0b37f6a50110bcf7c13840da18f80e" + }, + "source_state": "Uncommitted exact three test-file repair; root owns final commit/publication.", + "status_porcelain": " M tests/browser_access.cjs\n?? tests/browser_access_session.cjs\n?? tests/test_browser_access_navigation.cjs\n", + "worktree": "/workspace/Tuldok-access-browser-ci-repair-20261020" +} diff --git a/docs/plans/lab-workflow-reliability/reports/access-browser-ci-20261020/original-failure-disposition.json b/docs/plans/lab-workflow-reliability/reports/access-browser-ci-20261020/original-failure-disposition.json new file mode 100644 index 00000000..2858438f --- /dev/null +++ b/docs/plans/lab-workflow-reliability/reports/access-browser-ci-20261020/original-failure-disposition.json @@ -0,0 +1,113 @@ +{ + "observed_utc": "2026-10-20T06:41:53.697361+00:00", + "scope": "Read-only source/log diagnosis; local evidence files only. No browser/PG reproduction, tests, source edits or external mutations.", + "pr": 112, + "head": "0dedbc953902ac89389dca0693006aaa02a0a87c", + "tested_checkout": "fb6c6c6c55b6ece1d6eab0eae20bef630b9fdf46", + "tested_source_attribution": "Hosted checkout merge message binds0ded into29011; remote comparison from0ded tofb6 has no changed files.", + "run": 39370107550, + "job": 118455819881, + "conclusion": "failure", + "successful_steps": 45, + "skipped_steps": 127, + "failed_steps": [ + { + "name": "Run node tests/browser_access.cjs", + "status": "completed", + "conclusion": "failure", + "number": 41 + } + ], + "prior_full_unittest": { + "tests": 1771, + "skips": 83, + "non_skipped": 1688, + "seconds": 2989.096, + "result": "OK" + }, + "log": { + "path": "/tmp/tuldok-pr112-0ded-access-failed-job-118455819881.log", + "bytes": 251955, + "sha256": "dce0386ccf600104e93c6129a28bbd4844ee5d73921b64567d3a2234a9748f6e" + }, + "source": { + "path": "tests/browser_access.cjs", + "git_blob": "bae9bae294fd3245d7f099e6185924308c7233bc", + "sha256": "b19ba3559cc8321c3a4d932f0c14a08c3763ca41ac8640fd965d346f95213159", + "exact_local_committed_bytes_match_remote": true + }, + "primary": { + "time": "2026-10-20T06:37:39.7616902Z", + "code": -32000, + "message": "Inspected target navigated or closed", + "command_and_phase": "Unknown: raw CDP error object has no method/phase/call stack.", + "source_findings": [ + "line31:Page.navigate acknowledgement immediately followed by Runtime.evaluate polling", + "line49:Page.reload acknowledgement immediately followed by Runtime.evaluate polling", + "No pageLoadTracker, no lifecycle subscription, no changed-main-loader barrier", + "line22 selects first page target without explicit about:blank identity", + "line23 rejects plain protocol object, losing method/phase/stack; requests have no timer or socket-close drainage" + ], + "causal_status": "The source has an exposed departing-document window compatible with the error. Exact hosted navigation versus target crash/closure and failing command remain unproven." + }, + "secondary": { + "time": "2026-10-20T06:37:39.7890264Z", + "error": "ENOTEMPTY", + "location": "browser_access.cjs53:141", + "path": "/tmp/tuldok-access-browser-aOJaTf", + "source_findings": [ + "finally sends SIGTERM to supplied ChildProcess handles and synchronously removes profile immediately", + "It does not await exit; a still-writing process can race removal", + "Thrown cleanup error obscures final primary diagnostic; no exited-process evidence is retained" + ], + "causal_status": "Missing shutdown-before-remove ordering is established in source; exact descendant write responsible for hosted ENOTEMPTY was not observed." + }, + "artifact": { + "id": 12245483273, + "metadata_digest": "sha256:0b60815d2c44a77b1b082d38280acd4d185907c6d11729a3735c0a175481cba6", + "metadata_bytes": 110805, + "reference_returned": true, + "contents_inspected": false, + "reason": "One local read failed: Tunnel connection failed:403 Forbidden. No retry or bypass. Screenshot/progress contents are unknown." + }, + "independent_reviewer": "/root/mcp_contract_ci_review", + "independent_findings": "Agrees navigation barrier absent and cleanup separate; exact hostcause unproven. No tests or changes.", + "proposed_smallest_repair": [ + "In access harness only: observe accepted pageLoadTracker events, enable Page lifecycle events, capture previous main frame/loader before both initial navigate and scoped-agent reload, and await changed MAIN loader plus matching load before DOM assertions. Keep every existing auth and mutation assertion.", + "Select explicitly launched about:blank page; reuse existing bounded cdpSession or equivalent method-tagged request timeouts/close drainage so target loss remains a finite failure. No blanket navigation-error catch/retry, no privileged-action replay.", + "Await existing stopOwnedChildren on supplied handles before bounded exact-owned-path removal. Retain directory when shutdown cannot be confirmed. Report primary and secondary failures separately, retaining exit1 and bounded phase/loader/method diagnostics without credentials." + ], + "verification_required_before_causal_claim": [ + "Existing pure page-load cases must continue refusing old-document, subframe, wrong-loader and DOMContentLoaded-only readiness.", + "Controlled delayed actual navigation/reload reproduction should demonstrate original failure and fixed absence of DOM reads until the exact new loader load; force target close to remain a bounded failure.", + "Owned-child delayed-final-write and ignored-TERM tests; ensure cleanup diagnostics cannot replace the recorded primary failure.", + "Once separately authorized for implementation/runtime execution: full unchanged access entrypoint on actual app/PostgreSQL/Chromium: real CLI one-time login, HttpOnly cookie, no mutation replay, preserved drafts, explicit commit, scoped-agent review denial,1280/390 captures, exact resource/schema cleanup.", + "Fresh exact repaired-head lifecycle and independent review; sibling success never erases this exact-head failure." + ], + "sibling_untouched": { + "run": 39370108074, + "job": 118455821471, + "status": "in_progress", + "active_steps": [ + { + "name": "Run python -m unittest discover -s tests", + "status": "in_progress", + "conclusion": null, + "number": 37 + } + ], + "cancelled_or_rerun": false, + "observed_utc": "2026-10-20 06:42:19 UTC", + "conclusion": null + }, + "PUB_1": "Original failed diagnostic bytes/path/profile/caps/receipt still missing", + "SCI_1": "Owned qualified criterion and compatible real lab pair still missing", + "external_mutations": [], + "system_integration_block": "Remains in force; no denied operation retried", + "same_as_predecessor_29011_sources": [ + "tests/browser_access.cjs", + "tests/browser_page_load.cjs", + "tests/browser_owned_cleanup.cjs", + "static/auth.js" + ] +} diff --git a/docs/plans/lab-workflow-reliability/reports/access-browser-ci-20261020/original-failure-excerpt.log b/docs/plans/lab-workflow-reliability/reports/access-browser-ci-20261020/original-failure-excerpt.log new file mode 100644 index 00000000..777038ac --- /dev/null +++ b/docs/plans/lab-workflow-reliability/reports/access-browser-ci-20261020/original-failure-excerpt.log @@ -0,0 +1,32 @@ +2026-10-20T06:37:15.2772080Z ##[group]Run node tests/browser_access.cjs +2026-10-20T06:37:15.2772426Z node tests/browser_access.cjs +2026-10-20T06:37:15.2825753Z shell: /usr/bin/bash -e {0} +2026-10-20T06:37:15.2826003Z env: +2026-10-20T06:37:15.2826392Z TULDOK_TRAINING_PYTHON: /home/runner/work/Tuldok/Tuldok/build/workspace-training/bin/python +2026-10-20T06:37:15.2827074Z TULDOK_NATIVE_READER_PYTHON: /home/runner/work/Tuldok/Tuldok/build/workspace-training/bin/python +2026-10-20T06:37:15.2827855Z TULDOK_DATABASE_URL: ***127.0.0.1:5432/tuldok_test +2026-10-20T06:37:15.2828258Z pythonLocation: /opt/hostedtoolcache/Python/3.12.15/x64 +2026-10-20T06:37:15.2828711Z PKG_CONFIG_PATH: /opt/hostedtoolcache/Python/3.12.15/x64/lib/pkgconfig +2026-10-20T06:37:15.2829143Z Python_ROOT_DIR: /opt/hostedtoolcache/Python/3.12.15/x64 +2026-10-20T06:37:15.2829525Z Python2_ROOT_DIR: /opt/hostedtoolcache/Python/3.12.15/x64 +2026-10-20T06:37:15.2829903Z Python3_ROOT_DIR: /opt/hostedtoolcache/Python/3.12.15/x64 +2026-10-20T06:37:15.2830328Z LD_LIBRARY_PATH: /opt/hostedtoolcache/Python/3.12.15/x64/lib +2026-10-20T06:37:15.2830686Z BROWSER: /usr/bin/google-chrome +2026-10-20T06:37:15.2830939Z ##[endgroup] +2026-10-20T06:37:39.7616902Z { code: -32000, message: 'Inspected target navigated or closed' } +2026-10-20T06:37:39.7886304Z node:fs:1282 +2026-10-20T06:37:39.7887476Z return binding.rmSync(getValidatedPath(path), opts.maxRetries, opts.recursive, opts.retryDelay); +2026-10-20T06:37:39.7889224Z ^ +2026-10-20T06:37:39.7889560Z +2026-10-20T06:37:39.7890264Z Error: ENOTEMPTY, Directory not empty: '/tmp/tuldok-access-browser-aOJaTf' +2026-10-20T06:37:39.7891130Z at Object.rmSync (node:fs:1282:18) +2026-10-20T06:37:39.7892007Z at /home/runner/work/Tuldok/Tuldok/tests/browser_access.cjs:53:141 +2026-10-20T06:37:39.7894084Z at { +2026-10-20T06:37:39.7894665Z errno: 39, +2026-10-20T06:37:39.7895114Z code: 'ENOTEMPTY', +2026-10-20T06:37:39.7895754Z path: '/tmp/tuldok-access-browser-aOJaTf', +2026-10-20T06:37:39.7896347Z syscall: 'rm' +2026-10-20T06:37:39.7896775Z } +2026-10-20T06:37:39.7897797Z +2026-10-20T06:37:39.7898074Z Node.js v24.22.0 +2026-10-20T06:37:39.8035099Z ##[error]Process completed with exit code 1. diff --git a/docs/plans/lab-workflow-reliability/reports/access-browser-ci-20261020/post-run-cleanup.json b/docs/plans/lab-workflow-reliability/reports/access-browser-ci-20261020/post-run-cleanup.json new file mode 100644 index 00000000..00a8746d --- /dev/null +++ b/docs/plans/lab-workflow-reliability/reports/access-browser-ci-20261020/post-run-cleanup.json @@ -0,0 +1,35 @@ +{ + "observed_utc": "2026-10-20T06:56:29.403793+00:00", + "schema": "catalog_82698c551bbd413a878d83de58438931", + "counts_before_disposal": { + "workspace_runs": 0 + }, + "schema_absent": true, + "temporary": "/tmp/tuldok-access-browser-7VR9Z3", + "temporary_absent": true, + "matching_task_processes": [], + "owned_main_handles": [ + { + "pid": 119512, + "exit_code": null, + "signal": "SIGTERM" + }, + { + "pid": 119521, + "exit_code": 0, + "signal": null + } + ], + "source_hashes": { + "tests/browser_access.cjs": "e3c1bcd19d73981a39a3adcc0d32d7e67029907e0cfd689e2739348efbf0e56f", + "tests/browser_access_session.cjs": "d61b7cf0c366134050d6bba3542fd209005c5ad22f10a3659afe0ca8f948e1a5", + "tests/test_browser_access_navigation.cjs": "56ae9b76f1aaa3803a90f03468129f243d0b37f6a50110bcf7c13840da18f80e" + }, + "checks": [ + "Anonymous legacy/current APIs denied; real one-time login consumed; cookie is HttpOnly and code cleared", + "Sign out and rejected mutation preserve draft; reauthentication does not replay mutation; deliberate retry commits", + "Real scoped agent identity cannot use owner human-review controls" + ], + "report": "/tmp/tuldok-access-browser-ci-repair-20261020/qa/access/run-vVR332", + "scope": "Exact preregistered test schema only; supplied main handles terminal and exact profile absent at post-exit observation; no blanket descendant shutdown guarantee." +} diff --git a/docs/plans/lab-workflow-reliability/reports/access-browser-ci-20261020/qualification.json b/docs/plans/lab-workflow-reliability/reports/access-browser-ci-20261020/qualification.json new file mode 100644 index 00000000..d371e0bd --- /dev/null +++ b/docs/plans/lab-workflow-reliability/reports/access-browser-ci-20261020/qualification.json @@ -0,0 +1,114 @@ +{ + "base": "0dedbc953902ac89389dca0693006aaa02a0a87c", + "branch": "fix/access-browser-ci-qualification-20261020", + "worktree": "/workspace/Tuldok-access-browser-ci-repair-20261020", + "retention": "retained-protected; unpublished local repair pending serial integration authority; no cleanup authorized", + "source_sha256": { + "tests/browser_access.cjs": "e3c1bcd19d73981a39a3adcc0d32d7e67029907e0cfd689e2739348efbf0e56f", + "tests/browser_access_session.cjs": "d61b7cf0c366134050d6bba3542fd209005c5ad22f10a3659afe0ca8f948e1a5", + "tests/test_browser_access_navigation.cjs": "56ae9b76f1aaa3803a90f03468129f243d0b37f6a50110bcf7c13840da18f80e" + }, + "companion_source_sha256": { + "tests/browser_cdp_session.cjs": "ecfac048e343e0be85a02d22d284ea2c731617ec80807762a0691276f2bec40f", + "tests/browser_page_load.cjs": "bb74bd6ad97e0ff7c4545c747df698d0466d143764b0ccb52ed26b2051a13318", + "tests/browser_owned_cleanup.cjs": "1cddb8bfe88999c89193b2c3f11f57ce842cb04e25dcdd99ba4f2f9ff28b3988" + }, + "actual_command": "node tests/browser_access.cjs", + "actual_exit": 0, + "actual_seconds": 8.60535, + "actual_attribution": "Precommit HEAD0ded plus three modified/new file hashes; actual receipt is not a literal repaired-commit execution.", + "original_failure": { + "run": 39370107550, + "job": 118455819881, + "decoded_log": "/tmp/tuldok-pr112-0ded-access-failed-job-118455819881.log", + "decoded_log_sha256": "dce0386ccf600104e93c6129a28bbd4844ee5d73921b64567d3a2234a9748f6e", + "primary": "Inspected target navigated or closed", + "secondary": "ENOTEMPTY", + "original_cause_proven": false, + "artifact_contents_inspected": false + }, + "focused_checks": { + "syntax": 3, + "new_controlled_navigation_reload_regression": "passed; mock CDP ordering only", + "existing_page_load_regression": "passed", + "owned_child_delayed_write_ignored_TERM_regression": "passed", + "diff_check": "passed" + }, + "preservation": { + "original_refs": 82, + "original_worktree_records": 96, + "unchanged": true, + "running_sibling_untouched": true + }, + "external_mutations": [], + "hosted_repaired_head_gate": "not run; publication outside current scope", + "PUB_1": "missing original evidence", + "SCI_1": "missing owned criterion and real lab pair", + "evidence": [ + { + "path": "acceptance.md", + "bytes": 4413, + "sha256": "ee8529aecfc71963e768cfc98c72a847680fe10de8b8930d21b4d9b394f85c74" + }, + { + "path": "actual-access.log", + "bytes": 346, + "sha256": "ae63a93687389ad38ed33d1ecbe7fe8916db65afff8222c4a9acfe8b07f4b2f4" + }, + { + "path": "cleanup.json", + "bytes": 341, + "sha256": "06b2c257bca7797d663ad809335504a2a2d56a7b41eae4edf77b2049b110538e" + }, + { + "path": "independent-review.json", + "bytes": 10086, + "sha256": "ce616c44d452ef18a96df683fd9ce6b524ac6eb97a40c497c91164ec480182c5" + }, + { + "path": "independent-source-review.json", + "bytes": 4965, + "sha256": "3b8d598bd31f575c0d8b20780a275abd9b0df10d7ee8a589b21a4968eccf56e3" + }, + { + "path": "original-failure-disposition.json", + "bytes": 6212, + "sha256": "3abe25f69e93b32459308ec15dae8e32e35c504447414faa0e657c1a0d9f5696" + }, + { + "path": "original-failure-excerpt.log", + "bytes": 2275, + "sha256": "7a92c80edc4a25e16424b63efcbad9b04cb2b53e45a9559733aa8d1724b72c87" + }, + { + "path": "post-run-cleanup.json", + "bytes": 1433, + "sha256": "3fefda92278099fc5e93f1c05f363277b72469535a8dbd9cd83f5832023a6fc1" + }, + { + "path": "receipt.json", + "bytes": 1693, + "sha256": "57b652807e1ac0e89d9f422c0c551051e33782ec779a016f0046c3ed48c6268d" + }, + { + "path": "runner-receipt.json", + "bytes": 763, + "sha256": "b247a3d1147a47f0304649ae2a110969c2da7c1d6e4cd5a0abe8d82430753ef9" + }, + { + "path": "schema-reservation.json", + "bytes": 234, + "sha256": "20bc035c94c464cfd13179a32bfdf069e49302f7d6f1369e8f7c2081ba15e5a5" + }, + { + "path": "session-1280.jpg", + "bytes": 92512, + "sha256": "ae5ecddb60cd751033a62eb24ee383daa4d56da85439115b080178b18b5c5580" + }, + { + "path": "session-390.jpg", + "bytes": 46763, + "sha256": "71f975c51592a0a92669c3d5213b21561acbc83cd4450a532b9e1a2cb7907519" + } + ] +} diff --git a/docs/plans/lab-workflow-reliability/reports/access-browser-ci-20261020/receipt.json b/docs/plans/lab-workflow-reliability/reports/access-browser-ci-20261020/receipt.json new file mode 100644 index 00000000..e2b89d92 --- /dev/null +++ b/docs/plans/lab-workflow-reliability/reports/access-browser-ci-20261020/receipt.json @@ -0,0 +1,47 @@ +{ + "checks": [ + "Anonymous legacy/current APIs denied; real one-time login consumed; cookie is HttpOnly and code cleared", + "Sign out and rejected mutation preserve draft; reauthentication does not replay mutation; deliberate retry commits", + "Real scoped agent identity cannot use owner human-review controls" + ], + "anonymous_denied": true, + "real_cli_login": true, + "no_mutation_replay": true, + "source_commit": "0dedbc953902ac89389dca0693006aaa02a0a87c", + "source_sha256": { + "tests/browser_access.cjs": "e3c1bcd19d73981a39a3adcc0d32d7e67029907e0cfd689e2739348efbf0e56f", + "tests/browser_access_session.cjs": "d61b7cf0c366134050d6bba3542fd209005c5ad22f10a3659afe0ca8f948e1a5", + "tests/test_browser_access_navigation.cjs": "56ae9b76f1aaa3803a90f03468129f243d0b37f6a50110bcf7c13840da18f80e" + }, + "navigation": { + "phase": "scoped-agent-reload", + "last_method": "Runtime.evaluate", + "events": [ + { + "event": "load", + "frame": "429B7ADFD788931A1DAC0F8F1B79610A", + "loader": "C704421B4745793C1F166C7CC67C83F8" + }, + { + "event": "main-frame", + "frame": "429B7ADFD788931A1DAC0F8F1B79610A", + "loader": "7534733FE54346FBED7374DBFE2EEB98" + }, + { + "event": "load", + "frame": "429B7ADFD788931A1DAC0F8F1B79610A", + "loader": "7534733FE54346FBED7374DBFE2EEB98" + }, + { + "event": "main-frame", + "frame": "429B7ADFD788931A1DAC0F8F1B79610A", + "loader": "7B0CA169AEB6A2150994309812BCD5B7" + }, + { + "event": "load", + "frame": "429B7ADFD788931A1DAC0F8F1B79610A", + "loader": "7B0CA169AEB6A2150994309812BCD5B7" + } + ] + } +} \ No newline at end of file diff --git a/docs/plans/lab-workflow-reliability/reports/access-browser-ci-20261020/runner-receipt.json b/docs/plans/lab-workflow-reliability/reports/access-browser-ci-20261020/runner-receipt.json new file mode 100644 index 00000000..08de421c --- /dev/null +++ b/docs/plans/lab-workflow-reliability/reports/access-browser-ci-20261020/runner-receipt.json @@ -0,0 +1,17 @@ +{ + "command": "node tests/browser_access.cjs", + "exit_code": 0, + "started_utc": "2026-10-20T06:55:51.714739+00:00", + "finished_utc": "2026-10-20T06:56:00.320089+00:00", + "source_head_at_run": "0dedbc953902ac89389dca0693006aaa02a0a87c", + "source_dirty_at_run": true, + "log": "/tmp/tuldok-access-browser-ci-repair-20261020/actual-access.log", + "schema_reservation": { + "dataset_id": "82698c551bbd413a878d83de58438931", + "schema": "catalog_82698c551bbd413a878d83de58438931", + "state": "created", + "owner": "access-browser-ci-20261020", + "catalog_path": "/tmp/tuldok-access-browser-7VR9Z3/data" + }, + "environment_changes": "None: existing runtime/Chromium/PG17 only; child environment and preregistered disposable fixture are task-scoped." +} diff --git a/docs/plans/lab-workflow-reliability/reports/access-browser-ci-20261020/schema-reservation.json b/docs/plans/lab-workflow-reliability/reports/access-browser-ci-20261020/schema-reservation.json new file mode 100644 index 00000000..98671362 --- /dev/null +++ b/docs/plans/lab-workflow-reliability/reports/access-browser-ci-20261020/schema-reservation.json @@ -0,0 +1,7 @@ +{ + "dataset_id": "82698c551bbd413a878d83de58438931", + "schema": "catalog_82698c551bbd413a878d83de58438931", + "state": "created", + "owner": "access-browser-ci-20261020", + "catalog_path": "/tmp/tuldok-access-browser-7VR9Z3/data" +} diff --git a/docs/plans/lab-workflow-reliability/reports/access-browser-ci-20261020/session-1280.jpg b/docs/plans/lab-workflow-reliability/reports/access-browser-ci-20261020/session-1280.jpg new file mode 100644 index 00000000..2fb51729 Binary files /dev/null and b/docs/plans/lab-workflow-reliability/reports/access-browser-ci-20261020/session-1280.jpg differ diff --git a/docs/plans/lab-workflow-reliability/reports/access-browser-ci-20261020/session-390.jpg b/docs/plans/lab-workflow-reliability/reports/access-browser-ci-20261020/session-390.jpg new file mode 100644 index 00000000..2e9d449c Binary files /dev/null and b/docs/plans/lab-workflow-reliability/reports/access-browser-ci-20261020/session-390.jpg differ diff --git a/tests/browser_access.cjs b/tests/browser_access.cjs index bae9bae2..eba38f48 100644 --- a/tests/browser_access.cjs +++ b/tests/browser_access.cjs @@ -4,14 +4,19 @@ const assert=require('node:assert/strict'),fs=require('node:fs'),path=require('n const {spawn,execFileSync}=require('node:child_process'); const {authenticatedFixture}=require('./auth_helpers.cjs'); const {waitForDebugger}=require('./browser_startup.cjs'); +const {accessSession}=require('./browser_access_session.cjs'); +const {stopOwnedChildren}=require('./browser_owned_cleanup.cjs'); const {qaDirectory,screenshotOptions}=require('./qa_artifacts.cjs'); const root=path.resolve(__dirname,'..'),temporary=fs.mkdtempSync(path.join(os.tmpdir(),'tuldok-access-browser-')),data=path.join(temporary,'data'); const report=qaDirectory(root,'access'),children=[],errors=[],checks=[],fixture=authenticatedFixture(); -const pause=ms=>new Promise(resolve=>setTimeout(resolve,ms));let ws,send,evaluate; +const pause=ms=>new Promise(resolve=>setTimeout(resolve,ms));let ws,session,send,evaluate,phase='startup'; +function setPhase(value){phase=value;session?.setPhase(value);} async function until(fn){for(let n=0;n<200;n++){const value=await fn();if(value)return value;await pause(100);}throw Error('Access browser wait timed out');} function launch(command,args,options){const child=spawn(command,args,options);children.push(child);return child;} function code(){const file=path.join(temporary,'login-'+Date.now());execFileSync('python3',['access.py','login','--data',data,'--out',file],{cwd:root});const value=fs.readFileSync(file,'utf8').trim();fs.unlinkSync(file);return value;} (async()=>{ + await require('./test_browser_access_navigation.cjs').verifyAccessNavigation(); + setPhase('application-startup'); const server=launch('python3',['-u','app.py','--port','0','--data',data],{cwd:root,stdio:['ignore','pipe','pipe']});let output='',serverErrors='';server.stdout.on('data',chunk=>output+=chunk);server.stderr.on('data',chunk=>serverErrors+=chunk); const port=await until(()=>output.match(/127\.0\.0\.1:(\d+)/)?.[1]),base='http://127.0.0.1:'+port; assert.equal((await fetch(base+'/api/workbench/records')).status,401); @@ -19,19 +24,22 @@ function code(){const file=path.join(temporary,'login-'+Date.now());execFileSync await fixture.register(base,data,root); const response=await fixture.fetch(base+'/api/workbench/import',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({kind:'text',name:'Retained owner draft',text:'Original immutable source',groups:['auth-fixture'],rights:'Authored test fixture'})});assert.equal(response.status,201);const record=await response.json(); const browser=launch(process.env.BROWSER||'/usr/bin/chromium',['--headless','--no-sandbox','--disable-gpu','--no-first-run','--remote-debugging-port=0','--window-size=1280,900','--user-data-dir='+path.join(temporary,'browser'),'about:blank'],{stdio:'ignore'}); - const debug=await waitForDebugger(browser,path.join(temporary,'browser','DevToolsActivePort')),tabs=await(await fetch('http://127.0.0.1:'+debug+'/json')).json();ws=new WebSocket(tabs.find(tab=>tab.type==='page').webSocketDebuggerUrl);await new Promise((resolve,reject)=>{ws.onopen=resolve;ws.onerror=reject;}); - let counter=0;const pending=new Map();ws.onmessage=event=>{const value=JSON.parse(event.data);if(value.id){const task=pending.get(value.id);pending.delete(value.id);value.error?task.reject(value.error):task.resolve(value.result);}else if(value.method==='Runtime.exceptionThrown')errors.push(value.params.exceptionDetails.text);}; - send=(method,params={})=>new Promise((resolve,reject)=>{pending.set(++counter,{resolve,reject});ws.send(JSON.stringify({id:counter,method,params}));}); + const debug=await waitForDebugger(browser,path.join(temporary,'browser','DevToolsActivePort')),tabs=await(await fetch('http://127.0.0.1:'+debug+'/json')).json(); + const target=tabs.find(tab=>tab.type==='page'&&tab.url==='about:blank');assert.ok(target,'Expected the explicitly launched blank page'); + ws=new WebSocket(target.webSocketDebuggerUrl);await new Promise((resolve,reject)=>{const timer=setTimeout(()=>reject(Error('Access CDP connection timed out')),10000);ws.onopen=()=>{clearTimeout(timer);resolve();};ws.onerror=()=>{clearTimeout(timer);reject(Error('Access CDP connection failed'));};}); + session=accessSession(ws,{onEvent:value=>{if(value.method==='Runtime.exceptionThrown')errors.push(value.params.exceptionDetails.text);}});session.setPhase(phase);send=session.send; evaluate=async expression=>{const value=await send('Runtime.evaluate',{expression,awaitPromise:true,returnByValue:true});if(value.exceptionDetails)throw Error(JSON.stringify(value.exceptionDetails));return value.result.value;}; async function click(selector){const box=await evaluate(`(()=>{const el=document.querySelector(${JSON.stringify(selector)});el.scrollIntoView({block:'center'});const r=el.getBoundingClientRect();return {x:r.x+r.width/2,y:r.y+r.height/2}})()`);await send('Input.dispatchMouseEvent',{type:'mousePressed',button:'left',clickCount:1,...box});await send('Input.dispatchMouseEvent',{type:'mouseReleased',button:'left',clickCount:1,...box});} async function fill(selector,value){await click(selector);await evaluate(`document.querySelector(${JSON.stringify(selector)}).select()`);await send('Input.insertText',{text:value});} async function login(){const token=code();await fill('#session-token',token);await click('#session-submit');await until(()=>evaluate('!document.getElementById("session-dialog").open'));assert.equal(await evaluate('document.getElementById("session-token").value'),'');assert.equal(await evaluate('document.cookie.includes("tuldok_session")'),false);return token;} - await send('Page.enable');await send('Runtime.enable');await send('Network.enable'); + await send('Page.enable');await send('Page.setLifecycleEventsEnabled',{enabled:true});await send('Runtime.enable');await send('Network.enable'); // No fixture cookie is installed: this suite exercises actual interactive login. - await send('Page.navigate',{url:base+'/workbench'});await until(()=>evaluate('document.getElementById("session-dialog")?.open')); + setPhase('initial-navigation');await session.navigate('Page.navigate',{url:base+'/workbench'});await until(()=>evaluate('document.getElementById("session-dialog")?.open')); + setPhase('one-time-owner-login'); const oneTime=await login();await until(()=>evaluate('document.getElementById("notice").textContent==="Collection ready."')); assert.equal((await fetch(base+'/api/access/session',{method:'POST',headers:{Origin:base,'Content-Type':'application/json'},body:JSON.stringify({token:oneTime})})).status,401); checks.push('Anonymous legacy/current APIs denied; real one-time login consumed; cookie is HttpOnly and code cleared'); + setPhase('draft-and-reauthentication'); await click('#records .record button');await until(()=>evaluate('!document.getElementById("editor").hidden'));await fill('#label','retained-draft'); const before=(await(await fixture.fetch(base+'/api/workbench/records/'+record.id)).json()).revision; await click('#session-logout');await until(()=>evaluate('document.getElementById("session-dialog").open'));await click('#session-close'); @@ -42,12 +50,23 @@ function code(){const file=path.join(temporary,'login-'+Date.now());execFileSync assert.equal(await evaluate('document.getElementById("label").value'),'retained-draft'); await click('#save');await until(async()=>((await(await fixture.fetch(base+'/api/workbench/records/'+record.id)).json()).revision)>before); checks.push('Sign out and rejected mutation preserve draft; reauthentication does not replay mutation; deliberate retry commits'); + setPhase('session-layout-captures'); for(const [width,height] of [[1280,900],[390,844]]){await send('Emulation.setDeviceMetricsOverride',{width,height,deviceScaleFactor:1,mobile:false});await click('#session-open');await pause(100);const shot=await send('Page.captureScreenshot',screenshotOptions);fs.writeFileSync(path.join(report,'session-'+width+'.jpg'),Buffer.from(shot.data,'base64'));await click('#session-close');} const issued=await fixture.fetch(base+'/api/access/credentials',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({label:'Scoped browser agent',scopes:['inspect','edit']})});assert.equal(issued.status,201);const agent=await issued.json(); const cookies=await send('Network.getCookies',{urls:[base]});const sessionCookie=cookies.cookies.find(cookie=>cookie.httpOnly&&cookie.name==='tuldok_session');assert.ok(sessionCookie); await send('Network.setCookie',{name:sessionCookie.name,value:agent.token,url:base+'/',httpOnly:true,sameSite:'Strict',path:'/'}); - await send('Page.reload');await until(()=>evaluate('document.getElementById("session-identity")?.textContent.includes("Scoped browser agent")'));await until(()=>evaluate('document.getElementById("notice").textContent==="Collection ready."'));await click('#records .record button');await until(()=>evaluate('!document.getElementById("editor").hidden')); + setPhase('scoped-agent-reload');await session.navigate('Page.reload');await until(()=>evaluate('document.getElementById("session-identity")?.textContent.includes("Scoped browser agent")'));await until(()=>evaluate('document.getElementById("notice").textContent==="Collection ready."'));await click('#records .record button');await until(()=>evaluate('!document.getElementById("editor").hidden')); assert.equal(await evaluate('document.getElementById("record-review").disabled'),true); checks.push('Real scoped agent identity cannot use owner human-review controls'); - assert.deepEqual(errors,[]);assert.equal(serverErrors,'');fs.writeFileSync(path.join(report,'receipt.json'),JSON.stringify({checks,anonymous_denied:true,real_cli_login:true,no_mutation_replay:true},null,2));console.log('PASS access browser '+report); -})().catch(error=>{console.error(error);process.exitCode=1;}).finally(()=>{ws?.close();for(const child of children)child.kill('SIGTERM');fs.rmSync(temporary,{recursive:true,force:true});}); + assert.deepEqual(errors,[]);assert.equal(serverErrors,'');fs.writeFileSync(path.join(report,'receipt.json'),JSON.stringify({checks,anonymous_denied:true,real_cli_login:true,no_mutation_replay:true,source_commit:execFileSync('git',['rev-parse','HEAD'],{cwd:root,encoding:'utf8'}).trim(),source_sha256:Object.fromEntries(['tests/browser_access.cjs','tests/browser_access_session.cjs','tests/test_browser_access_navigation.cjs'].map(file=>[file,require('node:crypto').createHash('sha256').update(fs.readFileSync(path.join(root,file))).digest('hex')])),navigation:session.snapshot()},null,2));console.log('PASS access browser '+report); +})().catch(error=>{ + console.error('Primary access failure:',error);process.exitCode=1; + const failure={phase,protocol:session?.snapshot(),error:{name:error.name,message:error.message,code:error.code}}; + console.error('Access failure context:',JSON.stringify(failure)); + fs.writeFileSync(path.join(report,'failure.json'),JSON.stringify(failure,null,2)+'\n'); +}).finally(async()=>{ + if(session)session.close();else ws?.close(); + await stopOwnedChildren(children); + fs.rmSync(temporary,{recursive:true,force:true,maxRetries:10,retryDelay:100}); + fs.writeFileSync(path.join(report,'cleanup.json'),JSON.stringify({temporary,removed:!fs.existsSync(temporary),children:children.map(child=>({pid:child.pid,exit_code:child.exitCode,signal:child.signalCode})),scope:'Supplied test-owned main handles exited before bounded profile removal'},null,2)+'\n'); +}).catch(error=>{console.error('Secondary access cleanup failure:',error);process.exitCode=1;}); diff --git a/tests/browser_access_session.cjs b/tests/browser_access_session.cjs new file mode 100644 index 00000000..3c7bbaa2 --- /dev/null +++ b/tests/browser_access_session.cjs @@ -0,0 +1,46 @@ +'use strict'; +const {cdpSession}=require('./browser_cdp_session.cjs'); +const {pageLoadTracker}=require('./browser_page_load.cjs'); + +// Access-test diagnostics contain phase/method/loader identity, never command parameters. +function accessSession(socket,{timeoutMs=15000,loadTimeoutMs=20000,pollMs=25,onEvent=()=>{}}={}){ + const loads=pageLoadTracker(),events=[]; + let phase='startup',lastMethod=null,terminal=null; + const session=cdpSession(socket,{timeoutMs,onEvent:message=>{ + loads.observe(message); + if(message.method==='Page.frameNavigated'&&!message.params.frame.parentId){ + const {id,loaderId}=message.params.frame;events.push({event:'main-frame',frame:id,loader:loaderId}); + } + if(message.method==='Page.lifecycleEvent'&&message.params.name==='load'){ + events.push({event:'load',frame:message.params.frameId,loader:message.params.loaderId}); + } + if(events.length>12)events.splice(0,events.length-12); + onEvent(message); + }}); + socket.addEventListener('close',()=>{terminal='CDP socket closed';}); + socket.addEventListener('error',()=>{terminal='CDP socket failed';}); + function contextual(error,owner,method){ + return Object.assign(new Error(`Access browser [${owner}] ${method}: ${error.message}`,{cause:error}),{code:error.code}); + } + async function send(method,params={},budgetMs){ + const owner=phase;lastMethod=method; + try{return await session.send(method,params,budgetMs);} + catch(error){throw contextual(error,owner,method);} + } + async function navigate(method,params={}){ + if(!['Page.navigate','Page.reload'].includes(method))throw Error('Unsupported access navigation'); + const owner=phase,previous=(await send('Page.getFrameTree')).frameTree.frame; + if(!previous.id||!previous.loaderId)throw Error(`Access browser [${owner}] missing previous main loader`); + const result=await send(method,params); + if(result.errorText)throw Error(`Access browser [${owner}] ${method}: ${result.errorText}`); + const deadline=performance.now()+loadTimeoutMs; + // Navigation acknowledgement can precede document replacement. Never poll that old DOM. + while(!loads.reloaded(previous)){ + if(terminal)throw contextual(Error(terminal),owner,method); + if(performance.now()>=deadline)throw Error(`Access browser [${owner}] ${method}: new main document load timed out`); + await new Promise(resolve=>setTimeout(resolve,pollMs)); + } + } + return {send,navigate,setPhase(value){phase=value;},snapshot(){return {phase,last_method:lastMethod,events:[...events]};},close(){terminal='CDP session disposed';session.close();}}; +} +module.exports={accessSession}; diff --git a/tests/test_browser_access_navigation.cjs b/tests/test_browser_access_navigation.cjs new file mode 100644 index 00000000..a097d5bb --- /dev/null +++ b/tests/test_browser_access_navigation.cjs @@ -0,0 +1,61 @@ +'use strict'; +// Controlled CDP scheduling reproduces the departing-document window; not a hosted trace. +const assert=require('node:assert/strict'); +const {cdpSession}=require('./browser_cdp_session.cjs'); +const {accessSession}=require('./browser_access_session.cjs'); +const tick=()=>new Promise(resolve=>setImmediate(resolve)); +class Socket extends EventTarget{ + constructor(){super();this.reads=0;this.loaded=false;this.requested=new Promise(resolve=>{this.acknowledged=resolve;});} + receive(value){this.dispatchEvent(new MessageEvent('message',{data:JSON.stringify(value)}));} + send(raw){ + const {id,method}=JSON.parse(raw); + if(method==='Page.getFrameTree'){this.receive({id,result:{frameTree:{frame:{id:'main',loaderId:'old'}}}});return;} + if(method==='Page.navigate'||method==='Page.reload'){this.receive({id,result:{}});this.acknowledged();return;} + if(method==='Runtime.never')return; + if(method==='Runtime.evaluate'){ + this.reads++; + if(!this.loaded){this.receive({id,error:{code:-32000,message:'Inspected target navigated or closed'}});return;} + } + this.receive({id,result:{result:{value:true}}}); + } + frame(id,loaderId,parentId){this.receive({method:'Page.frameNavigated',params:{frame:{id,loaderId,parentId}}});} + load(frameId,loaderId,name='load'){this.receive({method:'Page.lifecycleEvent',params:{frameId,loaderId,name}});} + close(){this.dispatchEvent(new Event('close'));} +} +async function verifyAccessNavigation(){ + for(const method of ['Page.navigate','Page.reload']){ + const oldSocket=new Socket(),old=cdpSession(oldSocket); + try{ + await old.send(method); + await assert.rejects(old.send('Runtime.evaluate',{expression:'original access assertion'}),error=>error.code===-32000&&error.message==='Inspected target navigated or closed'); + }finally{old.close();} + const socket=new Socket(),current=accessSession(socket,{pollMs:1,loadTimeoutMs:1000}); + current.setPhase(method==='Page.reload'?'scoped-agent-reload':'initial-navigation'); + try{ + let finished=false; + const action=(async()=>{await current.navigate(method);await current.send('Runtime.evaluate',{expression:'original access assertion'});finished=true;})(); + await socket.requested;await tick();assert.equal(socket.reads,0);assert.equal(finished,false); + socket.frame('child','child-new','main');socket.load('child','child-new'); + socket.frame('main','new');socket.load('main','old');socket.load('main','new','DOMContentLoaded'); + await tick();assert.equal(socket.reads,0);assert.equal(finished,false,'Old/subframe/DOMContentLoaded events never permit access DOM reads'); + socket.loaded=true;socket.load('main','new');await action;assert.equal(socket.reads,1);assert.equal(finished,true); + }finally{current.close();} + } + console.log('PASS access navigation/reload reproduction: old acknowledgement permits failing DOM read; changed main loader plus its load gates the original assertion'); + for(const kind of ['timeout','close']){ + const socket=new Socket(),current=accessSession(socket,{pollMs:1,loadTimeoutMs:25});current.setPhase('scoped-agent-reload'); + try{ + const navigation=current.navigate('Page.reload'); + const rejected=assert.rejects(navigation,kind==='close'?/\[scoped-agent-reload\] Page.reload: CDP socket closed/:/\[scoped-agent-reload\] Page.reload: new main document load timed out/); + await socket.requested;if(kind==='close')socket.close();await rejected;assert.equal(socket.reads,0); + }finally{current.close();} + } + const socket=new Socket(),current=accessSession(socket,{timeoutMs:20});current.setPhase('private-operation'); + try{ + await assert.rejects(current.send('Runtime.never',{token:'MUST_NOT_APPEAR'}),error=>error.message==='Access browser [private-operation] Runtime.never: CDP timeout: Runtime.never'&&!error.message.includes('MUST_NOT_APPEAR')); + assert.ok(!JSON.stringify(current.snapshot()).includes('MUST_NOT_APPEAR')); + }finally{current.close();} + console.log('PASS access phase-specific timeout/target-close failures are bounded; diagnostics omit command parameters'); +} +module.exports={verifyAccessNavigation}; +if(require.main===module)verifyAccessNavigation().catch(error=>{console.error(error);process.exitCode=1;});