diff --git a/docs/plans/lab-workflow-reliability/reports/caption-dialog-ci-20261020/acceptance.md b/docs/plans/lab-workflow-reliability/reports/caption-dialog-ci-20261020/acceptance.md new file mode 100644 index 00000000..449dd98e --- /dev/null +++ b/docs/plans/lab-workflow-reliability/reports/caption-dialog-ci-20261020/acceptance.md @@ -0,0 +1,63 @@ +# Caption browser dialog CI repair + +The cancelled obsolete PR105 duplicate run39349523584/job118390149863 reported +`No dialog is showing` at02:06:23, with no runtime exceptions. It produced no +completed page diagnostics before manual cancellation at03:22:40. The original +[observation](original-task-observation.json) and [failure excerpt](original-failure-excerpt.log) +retain its cancelled outcome and exact full-log/raw-response identities. A +same-head primary passed. This is a nondeterministic harness failure, with no +product-failure claim. A late modal blocking the old diagnostic evaluation is a +source-supported hypothesis; the hosted log contains no modal-event trace. + +The repair starts from published PR111 head +`0ba4db01f04a77c262eb4b397f88849e9688b5ab` on +`fix/captions-dialog-ci-qualification-20261020`. Its implementation write set is +`tests/browser_captions.cjs`, `tests/browser_cdp_session.cjs` and +`tests/test_browser_cdp_session.cjs`. Product code and all original assertions +are unchanged. The existing caption CI entrypoint awaits the focused regression +before launching the browser, so no workflow change is needed. + +Each modal expectation is armed before the triggering click. The helper waits +for the fresh exact confirm type/message, handles the selected decision once, +then awaits the original click command. Cancel retains the unsaved caption; +accept restores the saved caption; discard after a keyboard edit restores human +review. Unexpected and absent dialogs fail. No autoaccept or ignored protocol +error replaces these witnesses. + +CDP requests expire, timed-out replies are ignored, timers clear on terminal +outcomes, and socket closure/error drains pending requests. Click rejections +are observed immediately. Diagnostics skip a known open modal and otherwise +have a separate one-second deadline. The caption fixture reuses the accepted +browser startup and owned-child shutdown helpers, then removes only its own +temporary directory with bounded retries after all children exit. + +[Controlled protocol regression](cdp-regression.log) passed: a180ms delayed +opening reliably defeats the old100ms handling and yields the original-32602 +error; a blocked diagnostic expires. Event-owned cancel/accept/discard, +missing/wrong dialogs, trigger rejection, request timeout, late reply and +socket close/error cases pass. This controlled protocol reproduction does not +claim to reconstruct the original hosted browser scheduling. + +[Actual caption entrypoint](actual-captions.log), [source-bound receipt](receipt.json) +and [qualification](qualification.json) passed using PostgreSQL17.6, +Python3.12.14 and Chromium151. All original edit/draft/review/reopen/filter, +keyboard review reset, repeated-submit, cancel/discard, four-record exact ZIP +and390px assertions remain. The real browser observes modal decisions +`false,true,true`. The fixture uses the existing local controlled generation +provider; it is actual app/HTTP/PostgreSQL/Chromium evidence, with no live-model +or training claim. Desktop and narrow captures are retained beside the receipt. + +The existing [owned-child regression](owned-cleanup-regression.log), all three +Node syntax checks and `git diff --check` passed. [Child cleanup](cleanup.json) +confirms both owned handles exited before temporary removal. +[Schema cleanup](schema-cleanup.json) confirms the final preregistered fresh +schema was dropped and queried absent, with five records and zero workspace +Runs before disposal. The preliminary complete browser run used a different +preregistered schema, also dropped and queried absent. Its source predates the +CI-entrypoint regression call; only the final run is the source-bound acceptance. + +No heavy CPU suite, performance comparison, model-quality, scientific acceptance +or CUDA claim is made. Original failures and source attributions remain intact. +Root owns independent review, serial publication/integration after PR111 and +cleanup. The task worktree `/workspace/Tuldok-captions-dialog-ci-repair-20261020` +is retained-protected by its named branch until root accepts and disposes it. diff --git a/docs/plans/lab-workflow-reliability/reports/caption-dialog-ci-20261020/actual-captions.log b/docs/plans/lab-workflow-reliability/reports/caption-dialog-ci-20261020/actual-captions.log new file mode 100644 index 00000000..8fa4b2e7 --- /dev/null +++ b/docs/plans/lab-workflow-reliability/reports/caption-dialog-ci-20261020/actual-captions.log @@ -0,0 +1,35 @@ +PASS reproduced fixed100ms race: delayed180ms confirm rejects premature handling; blocked diagnostics time out +PASS fresh delayed cancel/accept/discard, absent dialog, trigger failure, bounded request, late reply and socket-close drainage +PASS unexpected dialog fails closed and socket error drains owned requests +[112014:112041:1020/032849.559242:ERROR:dbus/bus.cc:405] Failed to connect to the bus: Failed to connect to socket /run/dbus/system_bus_socket: No such file or directory +[112014:112041:1020/032849.565241:ERROR:dbus/bus.cc:405] Failed to connect to the bus: Could not parse server address: Unknown address type (examples of valid types are "tcp" and on UNIX "unix") +[112014:112041:1020/032849.565717:ERROR:dbus/bus.cc:405] Failed to connect to the bus: Failed to connect to socket /run/dbus/system_bus_socket: No such file or directory +[112014:112041:1020/032849.565746:ERROR:dbus/bus.cc:405] Failed to connect to the bus: Failed to connect to socket /run/dbus/system_bus_socket: No such file or directory +[112014:112041:1020/032849.565752:ERROR:dbus/bus.cc:405] Failed to connect to the bus: Could not parse server address: Unknown address type (examples of valid types are "tcp" and on UNIX "unix") +[112014:112053:1020/032849.581169:ERROR:content/browser/zygote_host/zygote_host_impl_linux.cc:279] Failed to adjust OOM score of renderer with pid 112055: Read-only file system (30) +[112014:112041:1020/032850.176012:ERROR:dbus/bus.cc:405] Failed to connect to the bus: Could not parse server address: Unknown address type (examples of valid types are "tcp" and on UNIX "unix") +[112014:112041:1020/032850.250225:ERROR:dbus/bus.cc:405] Failed to connect to the bus: Could not parse server address: Unknown address type (examples of valid types are "tcp" and on UNIX "unix") +[112014:112053:1020/032850.254782:ERROR:content/browser/zygote_host/zygote_host_impl_linux.cc:279] Failed to adjust OOM score of renderer with pid 112125: Read-only file system (30) +[112014:112014:1020/032850.278305:ERROR:dbus/object_proxy.cc:572] Failed to call method: org.freedesktop.DBus.NameHasOwner: object_path= /org/freedesktop/DBus: unknown error type: +[112014:112014:1020/032850.278374:ERROR:dbus/object_proxy.cc:572] Failed to call method: org.freedesktop.DBus.NameHasOwner: object_path= /org/freedesktop/DBus: unknown error type: + +DevTools listening on ws://127.0.0.1:46283/devtools/browser/5e6d27b3-ae4d-4450-985f-00473484f268 +[112014:112041:1020/032850.280676:ERROR:dbus/bus.cc:405] Failed to connect to the bus: Could not parse server address: Unknown address type (examples of valid types are "tcp" and on UNIX "unix") +[112014:112041:1020/032850.280695:ERROR:dbus/bus.cc:405] Failed to connect to the bus: Could not parse server address: Unknown address type (examples of valid types are "tcp" and on UNIX "unix") +[112014:112014:1020/032850.281416:ERROR:dbus/object_proxy.cc:572] Failed to call method: org.freedesktop.DBus.NameHasOwner: object_path= /org/freedesktop/DBus: unknown error type: +[112014:112014:1020/032850.285386:ERROR:dbus/object_proxy.cc:572] Failed to call method: org.freedesktop.DBus.NameHasOwner: object_path= /org/freedesktop/DBus: unknown error type: +[112014:112041:1020/032850.287808:ERROR:dbus/bus.cc:405] Failed to connect to the bus: Failed to connect to socket /run/dbus/system_bus_socket: No such file or directory +[112014:112041:1020/032850.287830:ERROR:dbus/bus.cc:405] Failed to connect to the bus: Failed to connect to socket /run/dbus/system_bus_socket: No such file or directory +[112014:112053:1020/032850.338429:ERROR:content/browser/zygote_host/zygote_host_impl_linux.cc:279] Failed to adjust OOM score of renderer with pid 112151: Read-only file system (30) +[112014:112053:1020/032850.379160:ERROR:content/browser/zygote_host/zygote_host_impl_linux.cc:279] Failed to adjust OOM score of renderer with pid 112160: Read-only file system (30) +[112014:112053:1020/032850.384271:ERROR:content/browser/zygote_host/zygote_host_impl_linux.cc:279] Failed to adjust OOM score of renderer with pid 112161: Read-only file system (30) +[112014:112014:1020/032850.405991:ERROR:dbus/object_proxy.cc:572] Failed to call method: org.freedesktop.DBus.NameHasOwner: object_path= /org/freedesktop/DBus: unknown error type: +[112014:112014:1020/032850.407079:ERROR:dbus/object_proxy.cc:572] Failed to call method: org.freedesktop.DBus.NameHasOwner: object_path= /org/freedesktop/DBus: unknown error type: +[112014:112041:1020/032850.407536:ERROR:dbus/bus.cc:405] Failed to connect to the bus: Could not parse server address: Unknown address type (examples of valid types are "tcp" and on UNIX "unix") +[112014:112014:1020/032850.415146:ERROR:dbus/object_proxy.cc:572] Failed to call method: org.freedesktop.DBus.Properties.GetAll: object_path= /org/freedesktop/UPower/devices/DisplayDevice: unknown error type: +[112014:112014:1020/032850.441651:ERROR:dbus/object_proxy.cc:572] Failed to call method: org.freedesktop.DBus.NameHasOwner: object_path= /org/freedesktop/DBus: unknown error type: +[112014:112041:1020/032850.441785:ERROR:dbus/bus.cc:405] Failed to connect to the bus: Could not parse server address: Unknown address type (examples of valid types are "tcp" and on UNIX "unix") +[112014:112014:1020/032850.465917:ERROR:dbus/object_proxy.cc:572] Failed to call method: org.freedesktop.DBus.NameHasOwner: object_path= /org/freedesktop/DBus: unknown error type: +[112014:112041:1020/032850.466040:ERROR:dbus/bus.cc:405] Failed to connect to the bus: Could not parse server address: Unknown address type (examples of valid types are "tcp" and on UNIX "unix") +[112014:112014:1020/032850.475312:ERROR:dbus/object_proxy.cc:572] Failed to call method: org.freedesktop.DBus.NameHasOwner: object_path= /org/freedesktop/DBus: unknown error type: +Caption Chromium edit/draft/review/reopen/filter/export, keyboard review reset, discard, repeated-submit and narrow layout passed. /tmp/tuldok-caption-dialog-repair/qa/image-caption-exports/run-Gj153o diff --git a/docs/plans/lab-workflow-reliability/reports/caption-dialog-ci-20261020/captions-desktop.jpg b/docs/plans/lab-workflow-reliability/reports/caption-dialog-ci-20261020/captions-desktop.jpg new file mode 100644 index 00000000..51d8745b Binary files /dev/null and b/docs/plans/lab-workflow-reliability/reports/caption-dialog-ci-20261020/captions-desktop.jpg differ diff --git a/docs/plans/lab-workflow-reliability/reports/caption-dialog-ci-20261020/captions-narrow.jpg b/docs/plans/lab-workflow-reliability/reports/caption-dialog-ci-20261020/captions-narrow.jpg new file mode 100644 index 00000000..28a1549e Binary files /dev/null and b/docs/plans/lab-workflow-reliability/reports/caption-dialog-ci-20261020/captions-narrow.jpg differ diff --git a/docs/plans/lab-workflow-reliability/reports/caption-dialog-ci-20261020/cdp-regression.log b/docs/plans/lab-workflow-reliability/reports/caption-dialog-ci-20261020/cdp-regression.log new file mode 100644 index 00000000..9e07bc66 --- /dev/null +++ b/docs/plans/lab-workflow-reliability/reports/caption-dialog-ci-20261020/cdp-regression.log @@ -0,0 +1,3 @@ +PASS reproduced fixed100ms race: delayed180ms confirm rejects premature handling; blocked diagnostics time out +PASS fresh delayed cancel/accept/discard, absent dialog, trigger failure, bounded request, late reply and socket-close drainage +PASS unexpected dialog fails closed and socket error drains owned requests diff --git a/docs/plans/lab-workflow-reliability/reports/caption-dialog-ci-20261020/cleanup.json b/docs/plans/lab-workflow-reliability/reports/caption-dialog-ci-20261020/cleanup.json new file mode 100644 index 00000000..764193e0 --- /dev/null +++ b/docs/plans/lab-workflow-reliability/reports/caption-dialog-ci-20261020/cleanup.json @@ -0,0 +1,17 @@ +{ + "temporary": "/tmp/tuldok-caption-browser-hAbzQw", + "removed": true, + "children": [ + { + "pid": 112007, + "exit_code": null, + "signal": "SIGTERM" + }, + { + "pid": 112014, + "exit_code": 0, + "signal": null + } + ], + "scope": "Only test-owned child handles; all exited before temporary removal" +} diff --git a/docs/plans/lab-workflow-reliability/reports/caption-dialog-ci-20261020/original-failure-excerpt.log b/docs/plans/lab-workflow-reliability/reports/caption-dialog-ci-20261020/original-failure-excerpt.log new file mode 100644 index 00000000..0ab50aa5 --- /dev/null +++ b/docs/plans/lab-workflow-reliability/reports/caption-dialog-ci-20261020/original-failure-excerpt.log @@ -0,0 +1,6 @@ +2026-10-20T02:06:23.5114153Z { code: -32602, message: 'No dialog is showing' } +2026-10-20T02:06:23.5114733Z Runtime errors: [] +2026-10-20T02:06:24.4591273Z [51779:51802:1020/020624.458874:ERROR:google_apis/gcm/engine/registration_request.cc:291] Registration response error message: PHONE_REGISTRATION_ERROR +2026-10-20T02:13:15.7950226Z [51779:51802:1020/021315.794793:ERROR:google_apis/gcm/engine/registration_request.cc:291] Registration response error message: DEPRECATED_ENDPOINT +2026-10-20T02:19:17.9209144Z [51779:51802:1020/021917.920698:ERROR:google_apis/gcm/engine/registration_request.cc:291] Registration response error message: DEPRECATED_ENDPOINT +2026-10-20T03:22:40.7751736Z ##[error]The operation was canceled. diff --git a/docs/plans/lab-workflow-reliability/reports/caption-dialog-ci-20261020/original-task-observation.json b/docs/plans/lab-workflow-reliability/reports/caption-dialog-ci-20261020/original-task-observation.json new file mode 100644 index 00000000..4344d8af --- /dev/null +++ b/docs/plans/lab-workflow-reliability/reports/caption-dialog-ci-20261020/original-task-observation.json @@ -0,0 +1,29 @@ +{ + "observed_utc": "2026-10-20T03:24:50.686741+00:00", + "run": 39349523584, + "job": 118390149863, + "conclusion": "cancelled", + "primary_acceptance_run": 39349557067, + "primary_conclusion": "success", + "observed_error": [ + "2026-10-20T02:06:23.5114153Z { code: -32602, message: 'No dialog is showing' }" + ], + "observed": "Captions step began02:06:19; CDP No dialog is showing reported02:06:23; no Page diagnostics completion before explicit cancellation03:22:40. Original cancelledrun is not success.", + "source_observation": "Three fixed100ms dialog pauses and unbounded CDP send/diagnostics in tests/browser_captions.cjs. A late modal blocking diagnostic Runtime.evaluate is a supported source-based hypothesis requiring deterministic repair verification, not a proven browser trace.", + "assigned_repair": { + "agent": "/root/pr111_ci_repair", + "base": "0ba4db01f04a77c262eb4b397f88849e9688b5ab", + "after_pr": 111, + "status": "implementation in progress", + "scope": "Caption browser harness and meaningful focused regression, actual fullPG/Chromium qualification, source-attributed reports. No product/assertion relaxation." + }, + "decoded_log": { + "path": "/tmp/tuldok-pr105-obsolete-duplicate-cancelled-job-118390149863.log", + "bytes": 367915, + "sha256": "abbea44875271000483136d48225c05bfe3be72352bcf608e69b6405f40f5dab" + }, + "raw_response": { + "path": "/tmp/tuldok-pr105-obsolete-duplicate-cancelled-job-118390149863-raw.json", + "sha256": "0b57354b2329b8c74b5ca90b9b83df31d31d295be5df061c6723b99f74b6f3c3" + } +} diff --git a/docs/plans/lab-workflow-reliability/reports/caption-dialog-ci-20261020/owned-cleanup-regression.log b/docs/plans/lab-workflow-reliability/reports/caption-dialog-ci-20261020/owned-cleanup-regression.log new file mode 100644 index 00000000..830e4c3f --- /dev/null +++ b/docs/plans/lab-workflow-reliability/reports/caption-dialog-ci-20261020/owned-cleanup-regression.log @@ -0,0 +1 @@ +PASS owned delayed final write completes before removal; ignored TERM is bounded and already-exited handles are harmless diff --git a/docs/plans/lab-workflow-reliability/reports/caption-dialog-ci-20261020/qualification.json b/docs/plans/lab-workflow-reliability/reports/caption-dialog-ci-20261020/qualification.json new file mode 100644 index 00000000..024f0f86 --- /dev/null +++ b/docs/plans/lab-workflow-reliability/reports/caption-dialog-ci-20261020/qualification.json @@ -0,0 +1,45 @@ +{ + "base_commit": "0ba4db01f04a77c262eb4b397f88849e9688b5ab", + "scope": "Test harness only; no product behavior, assertion relaxation, remote or inherited branch mutation", + "tested_before_commit_with_exact_file_hashes": { + "tests/browser_captions.cjs": "5cc3fd4b7e9dc299c8e9c22ad6d8f9100ddfa5b1c214e36f58e1b562d7349689", + "tests/browser_cdp_session.cjs": "ecfac048e343e0be85a02d22d284ea2c731617ec80807762a0691276f2bec40f", + "tests/test_browser_cdp_session.cjs": "1742a8b29c33b4ceb35ce76310df581cdb7e44b21aede4c1998526222e9eb7d2", + "tests/browser_owned_cleanup.cjs": "1cddb8bfe88999c89193b2c3f11f57ce842cb04e25dcdd99ba4f2f9ff28b3988", + "static/workbench.js": "167a48d39b0831ba814c31321f0f936d969e9b322c71b11a25a3f24ca9fe889b" + }, + "actual_caption_entrypoint_includes_controlled_regression": true, + "command": "node tests/browser_captions.cjs", + "outcome": "passed", + "original_failure": { + "run": 39349523584, + "job": 118390149863, + "conclusion": "cancelled", + "decoded_log": "/tmp/tuldok-pr105-obsolete-duplicate-cancelled-job-118390149863.log", + "decoded_log_sha256": "abbea44875271000483136d48225c05bfe3be72352bcf608e69b6405f40f5dab", + "raw_response": "/tmp/tuldok-pr105-obsolete-duplicate-cancelled-job-118390149863-raw.json", + "raw_response_sha256": "0b57354b2329b8c74b5ca90b9b83df31d31d295be5df061c6723b99f74b6f3c3", + "late_modal_blocks_diagnostics": "Source-supported hypothesis; no original Page.javascriptDialogOpening trace was captured" + }, + "controlled_regression": "Socket fixture:180ms opening defeats old100ms handling, then blocked diagnostic terminates in25ms; delayed three decisions and absent/wrong-dialog/request-lifecycle cases pass", + "environment": { + "postgres": "PostgreSQL 17.6 on x86_64-pc-linux-gnu, compiled by gcc (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0, 64-bit", + "python": "Python 3.12.14", + "node": "v24.19.0", + "browser": "Chrome/151.0.7922.173", + "mode": "headless, no-sandbox, disable-gpu; isolated browser profile; normal PostgreSQL/HTTP app via existing local controlled generation fixture" + }, + "local_reservation_launcher": "Precreate only the reserved dataset_id through normal Metadata before tests/browser_server.py; all five records are generated, edited and reviewed by the unchanged browser journey. No handlers/data substitutions added.", + "limits": [ + "Controlled CDP timing reproduction is not a trace of the original hosted scheduling.", + "Actual workflow uses the existing controlled generation provider, not a live external model.", + "No performance/model-quality/scientific/CUDA claim; original cancelled run remains cancelled.", + "No full heavy CPU suite rerun for a test-only caption harness change." + ], + "actual_browser_raw_log": { + "path": "/tmp/tuldok-caption-dialog-repair/actual-captions.log", + "bytes": 5967, + "sha256": "9ab368c6f9acd1c9d2efeb42199cf5f381171f1706c9572e63e7475d143a05b2", + "committed_log_projection": "Same lines; trailing whitespace removed from Chromium stderr for git diff --check. Raw bytes retained unchanged at the recorded path." + } +} diff --git a/docs/plans/lab-workflow-reliability/reports/caption-dialog-ci-20261020/receipt.json b/docs/plans/lab-workflow-reliability/reports/caption-dialog-ci-20261020/receipt.json new file mode 100644 index 00000000..3f99276f --- /dev/null +++ b/docs/plans/lab-workflow-reliability/reports/caption-dialog-ci-20261020/receipt.json @@ -0,0 +1,41 @@ +{ + "status": "passed", + "command": "node tests/browser_captions.cjs", + "source_commit": "0ba4db01f04a77c262eb4b397f88849e9688b5ab", + "source_sha256": { + "tests/browser_captions.cjs": "5cc3fd4b7e9dc299c8e9c22ad6d8f9100ddfa5b1c214e36f58e1b562d7349689", + "tests/browser_cdp_session.cjs": "ecfac048e343e0be85a02d22d284ea2c731617ec80807762a0691276f2bec40f", + "tests/test_browser_cdp_session.cjs": "1742a8b29c33b4ceb35ce76310df581cdb7e44b21aede4c1998526222e9eb7d2", + "tests/browser_owned_cleanup.cjs": "1cddb8bfe88999c89193b2c3f11f57ce842cb04e25dcdd99ba4f2f9ff28b3988", + "static/workbench.js": "167a48d39b0831ba814c31321f0f936d969e9b322c71b11a25a3f24ca9fe889b" + }, + "browser": "Chrome/151.0.7922.173", + "catalog_id": "b0725dd405a3495e9ca4c2dadffca047", + "dialog_checks": [ + { + "type": "confirm", + "message": "Discard unsaved annotation, rights-note, response or judgment edits?", + "accept": false + }, + { + "type": "confirm", + "message": "Discard unsaved annotation, rights-note, response or judgment edits?", + "accept": true + }, + { + "type": "confirm", + "message": "Discard unsaved annotation, rights-note, response or judgment edits?", + "accept": true + } + ], + "exported_records": 4, + "zip_sha256": "9be25ae2096fda32205f4428172ae7b46e90af98007ddd14f4a010f7690d431d", + "checks": [ + "cancel preserves unsaved caption", + "discard restores saved caption", + "keyboard edit resets review; discard restores human review", + "draft exclusion and exact caption ZIP", + "reopen, filters, repeated submit and390px layout" + ], + "scope": "Actual app/PostgreSQL/Chromium caption journey with existing controlled generation provider; no model-quality claim" +} diff --git a/docs/plans/lab-workflow-reliability/reports/caption-dialog-ci-20261020/schema-cleanup-attempt-01.json b/docs/plans/lab-workflow-reliability/reports/caption-dialog-ci-20261020/schema-cleanup-attempt-01.json new file mode 100644 index 00000000..6d9bb38f --- /dev/null +++ b/docs/plans/lab-workflow-reliability/reports/caption-dialog-ci-20261020/schema-cleanup-attempt-01.json @@ -0,0 +1,13 @@ +{ + "schema": "catalog_a9ff5f061eb54f17a1e2836abcc095de", + "reserved_before_creation": true, + "recorded_catalog_matches_actual_receipt": true, + "owned_children_stopped": true, + "temporary_removed": true, + "counts": { + "workbench_records": 5, + "workspace_runs": 0 + }, + "removed_and_queried_absent": true, + "observed_at": "2026-10-20T03:28:45.861799+00:00" +} diff --git a/docs/plans/lab-workflow-reliability/reports/caption-dialog-ci-20261020/schema-cleanup.json b/docs/plans/lab-workflow-reliability/reports/caption-dialog-ci-20261020/schema-cleanup.json new file mode 100644 index 00000000..b3a312f9 --- /dev/null +++ b/docs/plans/lab-workflow-reliability/reports/caption-dialog-ci-20261020/schema-cleanup.json @@ -0,0 +1,15 @@ +{ + "schema": "catalog_b0725dd405a3495e9ca4c2dadffca047", + "reserved_before_creation": true, + "actual_browser_catalog_identity_matches": true, + "owned_children_exited_before_drop": true, + "temporary_removed_before_drop": true, + "observed_counts": { + "workbench_records": 5, + "workspace_runs": 0 + }, + "removed_and_queried_absent": true, + "preliminary_schema_independently_still_absent": "catalog_a9ff5f061eb54f17a1e2836abcc095de", + "observed_at": "2026-10-20T03:30:02.919478+00:00", + "scope": "Only the two exact task-created, preregistered fresh schemas; no other catalogs touched" +} diff --git a/docs/plans/lab-workflow-reliability/reports/caption-dialog-ci-20261020/schema-reservation-attempt-01.json b/docs/plans/lab-workflow-reliability/reports/caption-dialog-ci-20261020/schema-reservation-attempt-01.json new file mode 100644 index 00000000..897a53a0 --- /dev/null +++ b/docs/plans/lab-workflow-reliability/reports/caption-dialog-ci-20261020/schema-reservation-attempt-01.json @@ -0,0 +1,8 @@ +{ + "schema": "catalog_a9ff5f061eb54f17a1e2836abcc095de", + "dataset_id": "a9ff5f061eb54f17a1e2836abcc095de", + "owner": "caption dialog CI harness qualification", + "state": "created-by-normal-Metadata", + "server": "PostgreSQL 17.6 on x86_64-pc-linux-gnu, compiled by gcc (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0, 64-bit", + "catalog_path": "/tmp/tuldok-caption-browser-kGXtzS/data" +} diff --git a/docs/plans/lab-workflow-reliability/reports/caption-dialog-ci-20261020/schema-reservation.json b/docs/plans/lab-workflow-reliability/reports/caption-dialog-ci-20261020/schema-reservation.json new file mode 100644 index 00000000..573499e1 --- /dev/null +++ b/docs/plans/lab-workflow-reliability/reports/caption-dialog-ci-20261020/schema-reservation.json @@ -0,0 +1,8 @@ +{ + "schema": "catalog_b0725dd405a3495e9ca4c2dadffca047", + "dataset_id": "b0725dd405a3495e9ca4c2dadffca047", + "owner": "caption dialog CI harness final qualification", + "state": "created-by-normal-Metadata", + "server": "PostgreSQL 17.6 on x86_64-pc-linux-gnu, compiled by gcc (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0, 64-bit", + "catalog_path": "/tmp/tuldok-caption-browser-hAbzQw/data" +} diff --git a/tests/browser_captions.cjs b/tests/browser_captions.cjs index 6b996592..e2892691 100644 --- a/tests/browser_captions.cjs +++ b/tests/browser_captions.cjs @@ -5,34 +5,42 @@ const fetch = fixtureAuth.fetch; const {qaDirectory,screenshotOptions}=require('./qa_artifacts.cjs'); const assert=require('node:assert/strict'); const fs=require('node:fs'),os=require('node:os'),path=require('node:path'); -const {spawn}=require('node:child_process'); +const {spawn,execFileSync}=require('node:child_process'); +const {cdpSession}=require('./browser_cdp_session.cjs'); +const {stopOwnedChildren}=require('./browser_owned_cleanup.cjs'); +const {waitForDebugger}=require('./browser_startup.cjs'); const {pageLoadTracker}=require('./browser_page_load.cjs'); const loads=pageLoadTracker(); const root=path.resolve(__dirname,'..'),temporary=fs.mkdtempSync(path.join(os.tmpdir(),'tuldok-caption-browser-')),children=[]; -let ws, inspect; +const report=qaDirectory(root,'image-caption-exports'); +const sourceHashes=Object.fromEntries(['tests/browser_captions.cjs','tests/browser_cdp_session.cjs','tests/test_browser_cdp_session.cjs','tests/browser_owned_cleanup.cjs','static/workbench.js'].map(file=>[file,require('node:crypto').createHash('sha256').update(fs.readFileSync(path.join(root,file))).digest('hex')])); +let ws, session, inspect; +const dialogChecks=[]; const errors=[]; const pause=ms=>new Promise(resolve=>setTimeout(resolve,ms)); -async function until(fn){for(let i=0;i<150;i++){const value=await fn();if(value)return value;await pause(100);}throw Error('Timed out');} +async function until(fn){const deadline=performance.now()+15000;while(performance.now(){ - fs.mkdirSync(qaDirectory(root,'image-caption-exports'),{recursive:true}); + await require('./test_browser_cdp_session.cjs').verifyCDPSession(); const server=launch('python3',['-u','tests/browser_server.py','--port','0','--data',path.join(temporary,'data')],{cwd:root,stdio:['ignore','pipe','pipe']}); let output='',stderr='';server.stdout.on('data',data=>output+=data);server.stderr.on('data',data=>stderr+=data); const port=await until(()=>output.match(/127\.0\.0\.1:(\d+)/)?.[1]); await fixtureAuth.register('http://127.0.0.1:'+port,path.join(temporary,'data'),root); - launch(process.env.BROWSER||'/usr/bin/chromium',['--headless','--no-sandbox','--disable-gpu','--no-first-run','--no-default-browser-check','--remote-debugging-port=0','--window-size=1400,950','--user-data-dir='+path.join(temporary,'browser'),'about:blank'],{stdio:['ignore','ignore','inherit'],env:{...process.env,HOME:temporary,XDG_CONFIG_HOME:temporary,XDG_CACHE_HOME:temporary}}); + const browser=launch(process.env.BROWSER||'/usr/bin/chromium',['--headless','--no-sandbox','--disable-gpu','--no-first-run','--no-default-browser-check','--remote-debugging-port=0','--window-size=1400,950','--user-data-dir='+path.join(temporary,'browser'),'about:blank'],{stdio:['ignore','ignore','inherit'],env:{...process.env,XDG_CONFIG_HOME:temporary,XDG_CACHE_HOME:temporary}}); const active=path.join(temporary,'browser','DevToolsActivePort'); - const debugPort=await until(()=>fs.existsSync(active)&&fs.readFileSync(active,'utf8').split('\n')[0]); - const tabs=await(await fetch('http://127.0.0.1:'+debugPort+'/json')).json(); + const debugPort=await waitForDebugger(browser,active); + const tabs=await(await fetch('http://127.0.0.1:'+debugPort+'/json',{signal:AbortSignal.timeout(10000)})).json(); const target=tabs.find(tab=>tab.type==='page' && tab.url==='about:blank'); assert.ok(target,'Expected the explicitly launched blank page target'); ws=new WebSocket(target.webSocketDebuggerUrl); - await new Promise((resolve,reject)=>{ws.onopen=resolve;ws.onerror=reject;}); - let id=0;const pending=new Map(); - ws.onmessage=event=>{const message=JSON.parse(event.data);loads.observe(message);if(message.id){const task=pending.get(message.id);pending.delete(message.id);message.error?task.reject(message.error):task.resolve(message.result);}if(message.method==='Runtime.exceptionThrown')errors.push(message.params.exceptionDetails);}; - const send=(method,params={})=>new Promise((resolve,reject)=>{pending.set(++id,{resolve,reject});ws.send(JSON.stringify({id,method,params}));}); + await new Promise((resolve,reject)=>{ + const timer=setTimeout(()=>reject(Error('CDP connection timed out')),10000); + ws.onopen=()=>{clearTimeout(timer);resolve();};ws.onerror=()=>{clearTimeout(timer);reject(Error('CDP connection failed'));}; + }); + session=cdpSession(ws,{onEvent:message=>{loads.observe(message);if(message.method==='Runtime.exceptionThrown')errors.push(message.params.exceptionDetails);}}); + const {send}=session; const evaluate=async expression=>{const result=await send('Runtime.evaluate',{expression,awaitPromise:true,returnByValue:true});if(result.exceptionDetails)throw Error(JSON.stringify(result.exceptionDetails));return result.result.value;}; - inspect=()=>evaluate('JSON.stringify({url:location.href,ready:document.readyState,notice:document.getElementById("notice")?.textContent,body:document.body?.innerText.slice(0,1500),viewport:innerWidth,documentWidth:document.documentElement.scrollWidth,overflow:[...document.querySelectorAll("body *")].filter(e=>e.getBoundingClientRect().right>innerWidth || e.scrollWidth>e.clientWidth+1).map(e=>({tag:e.tagName,id:e.id,class:e.className,right:e.getBoundingClientRect().right,width:e.getBoundingClientRect().width})).slice(0,30)})'); + inspect=()=>send('Runtime.evaluate',{expression:'JSON.stringify({url:location.href,ready:document.readyState,notice:document.getElementById("notice")?.textContent,body:document.body?.innerText.slice(0,1500),viewport:innerWidth,documentWidth:document.documentElement.scrollWidth,overflow:[...document.querySelectorAll("body *")].filter(e=>e.getBoundingClientRect().right>innerWidth || e.scrollWidth>e.clientWidth+1).map(e=>({tag:e.tagName,id:e.id,class:e.className,right:e.getBoundingClientRect().right,width:e.getBoundingClientRect().width})).slice(0,30)})',awaitPromise:true,returnByValue:true},1000).then(value=>value.result?.value); const click=id=>evaluate('document.getElementById('+JSON.stringify(id)+').click()'); const fill=(id,value)=>evaluate('(()=>{const e=document.getElementById('+JSON.stringify(id)+');e.value='+JSON.stringify(value)+';e.dispatchEvent(new Event("input",{bubbles:true}));e.dispatchEvent(new Event("change",{bubbles:true}));})()'); await send('Page.enable');await send('Page.setLifecycleEventsEnabled',{enabled:true});await send('Runtime.enable'); @@ -67,10 +75,14 @@ function launch(command,args,options={}){const child=spawn(command,args,options) assert.equal(await evaluate('document.getElementById("caption").value'),'A dark canvas in a procedural rectangle test scene.'); // Unsaved caption changes survive cancellation of a navigation prompt. await fill('caption','Keep this unsaved caption'); - const blockedNavigation=click('reload'); - await pause(100);await send('Page.handleJavaScriptDialog',{accept:false});await blockedNavigation; + const discardMessage='Discard unsaved annotation, rights-note, response or judgment edits?'; + const answerDiscard=async accept=>{ + const event=await session.withDialog(()=>click('reload'),{accept,message:discardMessage}); + dialogChecks.push({type:event.type,message:event.message,accept}); + }; + await answerDiscard(false); assert.equal(await evaluate('document.getElementById("caption").value'),'Keep this unsaved caption'); - const reload=click('reload');await pause(100);await send('Page.handleJavaScriptDialog',{accept:true});await reload; + await answerDiscard(true); await until(()=>evaluate('document.getElementById("caption").value==="A dark canvas in a procedural rectangle test scene."')); await fill('record-review','human_reviewed'); await evaluate('document.getElementById("editor").requestSubmit();document.getElementById("editor").requestSubmit();'); @@ -84,7 +96,7 @@ function launch(command,args,options={}){const child=spawn(command,args,options) await evaluate('document.getElementById("caption").focus();document.getElementById("caption").setSelectionRange(0,0)'); await send('Input.insertText',{text:'A test view. '}); assert.equal(await evaluate('document.getElementById("record-review").value'),'draft'); - const discard=click('reload');await pause(100);await send('Page.handleJavaScriptDialog',{accept:true});await discard; + await answerDiscard(true); await until(()=>evaluate('document.getElementById("record-review").value==="human_reviewed"')); // A matching draft caption is visible before review filtering and must be excluded afterward. await openName(names[4]);await fill('task','image_caption');await fill('caption','A dark canvas in a procedural rectangle test scene.'); @@ -101,14 +113,25 @@ function launch(command,args,options={}){const child=spawn(command,args,options) const release=await evaluate('document.querySelector("#release-result a").href'); const archive=await fetch(release);assert.equal(archive.status,200); const zipPath=path.join(temporary,'captions.zip');fs.writeFileSync(zipPath,Buffer.from(await archive.arrayBuffer())); - const {execFileSync}=require('node:child_process'); execFileSync('python3',['-c',`import json,zipfile;z=zipfile.ZipFile(${JSON.stringify(zipPath)});m=json.loads(z.read('manifest.json'));assert m['format']=='image_caption_v1';assert len(m['records'])==4;assert len(m['warnings'])==4;assert all(w.startswith('Small image:') for w in m['warnings']);assert set(m['split_mapping'].values())=={'train','val','test'};[(lambda rows: (len(rows)>0 or (_ for _ in ()).throw(AssertionError('Empty split'))))([json.loads(x) for x in z.read(s+'/metadata.jsonl').splitlines()]) for s in ('train','val','test')]`]); assert.ok((await evaluate('document.getElementById("notice").textContent')).includes('4 export warnings; inspect manifest.json.')); const desktop=await send('Page.captureScreenshot',screenshotOptions); - fs.writeFileSync(path.join(qaDirectory(root,'image-caption-exports'),'captions-desktop.jpg'),Buffer.from(desktop.data,'base64')); + fs.writeFileSync(path.join(report,'captions-desktop.jpg'),Buffer.from(desktop.data,'base64')); await send('Emulation.setDeviceMetricsOverride',{width:390,height:844,deviceScaleFactor:1,mobile:false}); const shot=await send('Page.captureScreenshot',screenshotOptions); - fs.writeFileSync(path.join(qaDirectory(root,'image-caption-exports'),'captions-narrow.jpg'),Buffer.from(shot.data,'base64')); + fs.writeFileSync(path.join(report,'captions-narrow.jpg'),Buffer.from(shot.data,'base64')); assert.ok(await evaluate('document.documentElement.scrollWidth<=innerWidth'), 'Narrow caption layout must not overflow'); - assert.deepEqual(errors,[]);console.log('Caption Chromium edit/draft/review/reopen/filter/export, keyboard review reset, discard, repeated-submit and narrow layout passed.'); -})().catch(async error=>{console.error(error);console.error('Runtime errors:',JSON.stringify(errors));if(inspect)try{console.error('Page diagnostics:',await inspect());}catch(diagnostic){console.error('Diagnostics failed:',diagnostic);}process.exitCode=1;}).finally(async()=>{if(ws)ws.close();for(const child of children)child.kill();await pause(200);fs.rmSync(temporary,{recursive:true,force:true});}); + assert.deepEqual(errors,[]); + fs.writeFileSync(path.join(report,'receipt.json'),JSON.stringify({status:'passed',command:'node tests/browser_captions.cjs',source_commit:execFileSync('git',['rev-parse','HEAD'],{cwd:root,encoding:'utf8'}).trim(),source_sha256:sourceHashes,browser:(await send('Browser.getVersion')).product,catalog_id:JSON.parse(fs.readFileSync(path.join(temporary,'data','.tuldok.json'),'utf8')).dataset_id,dialog_checks:dialogChecks,exported_records:4,zip_sha256:require('node:crypto').createHash('sha256').update(fs.readFileSync(zipPath)).digest('hex'),checks:['cancel preserves unsaved caption','discard restores saved caption','keyboard edit resets review; discard restores human review','draft exclusion and exact caption ZIP','reopen, filters, repeated submit and390px layout'],scope:'Actual app/PostgreSQL/Chromium caption journey with existing controlled generation provider; no model-quality claim'},null,2)+'\n'); + console.log('Caption Chromium edit/draft/review/reopen/filter/export, keyboard review reset, discard, repeated-submit and narrow layout passed.',report); +})().catch(async error=>{ + console.error(error);console.error('Runtime errors:',JSON.stringify(errors)); + if(session?.hasDialog())console.error('Page diagnostics skipped: JavaScript dialog is open.'); + else if(inspect)try{console.error('Page diagnostics:',await inspect());}catch(diagnostic){console.error('Diagnostics failed:',diagnostic);} + process.exitCode=1; +}).finally(async()=>{ + if(session)session.close();else if(ws)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:'Only test-owned child handles; all exited before temporary removal'},null,2)+'\n'); +}).catch(error=>{console.error('Owned caption fixture cleanup failed:',error);process.exitCode=1;}); diff --git a/tests/browser_cdp_session.cjs b/tests/browser_cdp_session.cjs new file mode 100644 index 00000000..0be82af4 --- /dev/null +++ b/tests/browser_cdp_session.cjs @@ -0,0 +1,58 @@ +'use strict'; +// Test-owned protocol requests and modal expectations share one socket lifetime. +function cdpSession(socket,{timeoutMs=15000,onEvent=()=>{}}={}){ + let sequence=0,closed=false,dialog=null,expectation=null; + const pending=new Map(); + function fail(error){ + closed=true; + for(const task of pending.values()){clearTimeout(task.timer);task.reject(error);} + pending.clear();expectation?.reject(error); + } + function send(method,params={},budgetMs=timeoutMs){ + return new Promise((resolve,reject)=>{ + if(closed){reject(Error('CDP socket is closed'));return;} + const id=++sequence; + const timer=setTimeout(()=>{pending.delete(id);reject(Error('CDP timeout: '+method));},budgetMs); + pending.set(id,{resolve,reject,timer}); + try{socket.send(JSON.stringify({id,method,params}));} + catch(error){clearTimeout(timer);pending.delete(id);reject(error);} + }); + } + socket.addEventListener('message',event=>{ + const message=JSON.parse(event.data); + if(message.id){ + const task=pending.get(message.id); + if(!task)return; // Replies may arrive after the owning request timed out. + pending.delete(message.id);clearTimeout(task.timer); + message.error?task.reject(Object.assign(Error(message.error.message),{code:message.error.code})):task.resolve(message.result); + return; + } + if(message.method==='Page.javascriptDialogOpening'){ + dialog=message.params;expectation?.resolve(dialog); + } + if(message.method==='Page.javascriptDialogClosed')dialog=null; + onEvent(message); + }); + socket.addEventListener('close',()=>fail(Error('CDP socket closed'))); + socket.addEventListener('error',()=>fail(Error('CDP socket failed'))); + async function withDialog(action,{accept,type='confirm',message:expectedMessage,waitMs=timeoutMs}={}){ + if(expectation||dialog)throw Error('A CDP dialog is already owned'); + let timer,triggered,own; + const opened=new Promise((resolve,reject)=>{ + own={resolve,reject};expectation=own; + timer=setTimeout(()=>reject(Error('CDP dialog opening timed out')),waitMs); + }); + try{ + // Arm the event before triggering. The action itself may wait for dismissal. + triggered=Promise.resolve().then(action); + triggered.catch(error=>own.reject(error)); + const event=await opened; + if(event.type!==type||event.message!==expectedMessage)throw Error('Unexpected CDP dialog: '+JSON.stringify(event)); + await send('Page.handleJavaScriptDialog',{accept}); + await triggered; + return event; + }finally{clearTimeout(timer);if(expectation===own)expectation=null;} + } + return {send,withDialog,hasDialog:()=>dialog!==null,close(){fail(Error('CDP session disposed'));socket.close();}}; +} +module.exports={cdpSession}; diff --git a/tests/test_browser_cdp_session.cjs b/tests/test_browser_cdp_session.cjs new file mode 100644 index 00000000..2f05d359 --- /dev/null +++ b/tests/test_browser_cdp_session.cjs @@ -0,0 +1,67 @@ +'use strict'; +// Controlled protocol timing proves synchronization/deadlines, not product UI. +const assert=require('node:assert/strict'); +const {cdpSession}=require('./browser_cdp_session.cjs'); +const pause=ms=>new Promise(resolve=>setTimeout(resolve,ms)); +const message='Discard unsaved annotation, rights-note, response or judgment edits?'; +class Socket extends EventTarget{ + constructor(){super();this.dialog=false;this.decisions=[];this.held=[];this.timers=[];} + receive(value){this.dispatchEvent(new MessageEvent('message',{data:JSON.stringify(value)}));} + send(raw){ + const {id,method,params}=JSON.parse(raw);this.lastId=id; + if(method==='Runtime.evaluate'&¶ms.expression==='open'){ + this.held.push(id); + this.timers.push(setTimeout(()=>{this.dialog=true;this.receive({method:'Page.javascriptDialogOpening',params:{type:'confirm',message}});},180));return; + } + if(method==='Runtime.never'||method==='Runtime.evaluate'&&this.dialog){this.held.push(id);return;} + if(method==='Page.handleJavaScriptDialog'){ + if(!this.dialog){this.receive({id,error:{code:-32602,message:'No dialog is showing'}});return;} + this.decisions.push(params.accept);this.dialog=false;this.receive({method:'Page.javascriptDialogClosed',params:{result:params.accept}}); + for(const held of this.held.splice(0))this.receive({id:held,result:{result:{value:true}}}); + } + this.receive({id,result:{result:{value:true}}}); + } + close(){this.timers.forEach(clearTimeout);this.dispatchEvent(new Event('close'));} +} +async function verifyCDPSession(){ + const oldSocket=new Socket(),old=cdpSession(oldSocket,{timeoutMs:500}); + try{ + const click=old.send('Runtime.evaluate',{expression:'open'});click.catch(()=>{}); + await pause(100); + await assert.rejects(old.send('Page.handleJavaScriptDialog',{accept:false}),error=>error.code===-32602&&error.message==='No dialog is showing'); + await pause(100);assert.equal(old.hasDialog(),true,'The real protocol opening arrives after the obsolete fixed wait'); + const started=performance.now(); + await assert.rejects(old.send('Runtime.evaluate',{expression:'diagnostics'},25),/CDP timeout: Runtime.evaluate/); + assert.ok(performance.now()-started<250,'Late-modal diagnostics must terminate within their own budget'); + console.log('PASS reproduced fixed100ms race: delayed180ms confirm rejects premature handling; blocked diagnostics time out'); + }finally{old.close();} + const socket=new Socket(),session=cdpSession(socket,{timeoutMs:500}); + try{ + for(const accept of [false,true,true]){ + const event=await session.withDialog(()=>session.send('Runtime.evaluate',{expression:'open'}),{accept,message}); + assert.equal(event.type,'confirm');assert.equal(session.hasDialog(),false); + } + assert.deepEqual(socket.decisions,[false,true,true],'Each newly observed dialog is handled exactly once with its selected decision'); + await assert.rejects(session.withDialog(()=>Promise.resolve(),{accept:true,message,waitMs:20}),/dialog opening timed out/); + await assert.rejects(session.withDialog(()=>Promise.reject(Error('Trigger failed')),{accept:true,message}),/Trigger failed/); + await assert.rejects(session.send('Runtime.never',{},20),/CDP timeout/); + assert.doesNotThrow(()=>socket.receive({id:socket.lastId,result:{}}),'Late timed-out replies are harmless'); + await session.send('Browser.getVersion'); + const pending=session.send('Runtime.never');const rejected=assert.rejects(pending,/socket closed/);socket.close();await rejected; + console.log('PASS fresh delayed cancel/accept/discard, absent dialog, trigger failure, bounded request, late reply and socket-close drainage'); + }finally{session.close();} + for(const kind of ['wrong','error']){ + const socket=new Socket(),session=cdpSession(socket,{timeoutMs:500}); + try{ + if(kind==='wrong'){ + await assert.rejects(session.withDialog(()=>session.send('Runtime.evaluate',{expression:'open'}),{accept:true,message:'Wrong prompt'}),/Unexpected CDP dialog/); + assert.deepEqual(socket.decisions,[],'An unexpected prompt is never autoaccepted'); + }else{ + const pending=session.send('Runtime.never'),rejected=assert.rejects(pending,/socket failed/);socket.dispatchEvent(new Event('error'));await rejected; + } + }finally{session.close();} + } + console.log('PASS unexpected dialog fails closed and socket error drains owned requests'); +} +module.exports={verifyCDPSession}; +if(require.main===module)verifyCDPSession().catch(error=>{console.error(error);process.exitCode=1;});