diff --git a/.agents/pm/history/pm-web-ulgy.jsonl b/.agents/pm/history/pm-web-ulgy.jsonl index 93ade75..a3d7ae3 100644 --- a/.agents/pm/history/pm-web-ulgy.jsonl +++ b/.agents/pm/history/pm-web-ulgy.jsonl @@ -12,3 +12,8 @@ {"hash_algorithm":"sha256","ts":"2026-09-18T03:39:21.296Z","author":"pi-agent","author_source":"asserted","agent_harness":"pi","agent_model":"gpt-5.6-sol","agent_model_source":"environment","agent_instance":"3f43cca22c8538dbf0bf9483","agent_provenance":{"model":{"value":"gpt-5.6-sol","source":"environment"},"effort":null,"role":{"value":"implementer","source":"argv"},"topic":null},"op":"close","patch":[{"op":"remove","path":"/metadata/assignee"},{"op":"replace","path":"/metadata/updated_at","value":"2026-09-18T03:39:21.296Z"},{"op":"replace","path":"/metadata/status","value":"closed"},{"op":"add","path":"/metadata/closed_at","value":"2026-09-18T03:39:21.265Z"},{"op":"add","path":"/metadata/completed_at","value":"2026-09-18T03:39:21.265Z"},{"op":"add","path":"/metadata/resolution","value":"Added real HTTP/PostgreSQL/pm-workspace collaboration tests for cross-client SSE delivery, concurrent no-loss writes with valid history, and broad routes/pm.ts lifecycle behavior; ratcheted coverage floors without weakening the branch gate."},{"op":"add","path":"/metadata/expected_result","value":"The collaboration guarantees and pm route command surface are behaviorally covered, npm run release:check passes, and measured coverage floors only move upward."},{"op":"add","path":"/metadata/actual_result","value":"npm run release:check exits 0 with 398/398 tests passing and aggregate src coverage L/B/F 76.82/80.32/73.00; floors are now 76/80/73. Remaining uncovered files and exact lines are recorded in the preceding item comment; public/src remains intentionally unmeasured at 14,884 TypeScript lines."},{"op":"add","path":"/metadata/close_reason","value":"Behavioral coverage tranche delivered and all local release gates pass; remaining 100% work is explicitly inventoried for the next tranche."}],"before_hash":"71ef9c1b5c62341f81e420c85871de0baf2106c1430a18899c1dc4fbcaad00bd","after_hash":"e2fc273e9bf0b64081ffe2145aac1e10a547a1a4a0a772d60d2a314e50c4eb66","item_hash_version":3,"event_class":"substantive","record_hash_version":1,"record_hash":"74dfb60e1b2f8b6f445da8e561644b9607e87460c3a3178b9379e93404d33853"} {"hash_algorithm":"sha256","ts":"2026-09-18T03:43:37.356Z","author":"pi-agent","author_source":"asserted","agent_harness":"pi","agent_model":"gpt-5.6-sol","agent_model_source":"environment","agent_instance":"3f43cca22c8538dbf0bf9483","agent_provenance":{"model":{"value":"gpt-5.6-sol","source":"environment"},"effort":null,"role":null,"topic":null},"op":"reopen","patch":[{"op":"remove","path":"/metadata/close_reason"},{"op":"remove","path":"/metadata/actual_result"},{"op":"remove","path":"/metadata/expected_result"},{"op":"remove","path":"/metadata/resolution"},{"op":"remove","path":"/metadata/completed_at"},{"op":"remove","path":"/metadata/closed_at"},{"op":"replace","path":"/metadata/updated_at","value":"2026-09-18T03:43:37.356Z"},{"op":"replace","path":"/metadata/status","value":"in_progress"}],"before_hash":"e2fc273e9bf0b64081ffe2145aac1e10a547a1a4a0a772d60d2a314e50c4eb66","after_hash":"4be4dff3693b6aeac7b01bc175decd4ec4fb54cd67275081b15c122d21dbbad9","item_hash_version":3,"message":"Reopened after PR review so remaining acceptance work is not lost","context":{"recurrence":{"reason":"Review correctly identified unfinished index/server bootstrap acceptance coverage; keep the item active for the next tranche.","from_status":"closed","to_status":"in_progress","previous_terminal":{"close_reason":"Behavioral coverage tranche delivered and all local release gates pass; remaining 100% work is explicitly inventoried for the next tranche.","resolution":"Added real HTTP/PostgreSQL/pm-workspace collaboration tests for cross-client SSE delivery, concurrent no-loss writes with valid history, and broad routes/pm.ts lifecycle behavior; ratcheted coverage floors without weakening the branch gate.","expected_result":"The collaboration guarantees and pm route command surface are behaviorally covered, npm run release:check passes, and measured coverage floors only move upward.","actual_result":"npm run release:check exits 0 with 398/398 tests passing and aggregate src coverage L/B/F 76.82/80.32/73.00; floors are now 76/80/73. Remaining uncovered files and exact lines are recorded in the preceding item comment; public/src remains intentionally unmeasured at 14,884 TypeScript lines."}}},"event_class":"substantive","record_hash_version":1,"record_hash":"94d58c69b46bc1e839246dd880dd576a2b275423f37a4de9b6e4fb23da65dad8"} {"hash_algorithm":"sha256","ts":"2026-09-18T03:43:37.813Z","author":"pi-agent","author_source":"asserted","agent_harness":"pi","agent_model":"gpt-5.6-sol","agent_model_source":"environment","agent_instance":"3f43cca22c8538dbf0bf9483","agent_provenance":{"model":{"value":"gpt-5.6-sol","source":"environment"},"effort":null,"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/5","value":{"created_at":"2026-09-18T03:43:37.813Z","author":"pi-agent","text":"PR review finding accepted: the task remains in_progress because successful src/index.ts and src/server.ts bootstrap coverage and additional positional actions remain unfinished. This PR is a measured ratchet tranche, not completion of the full item."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-09-18T03:43:37.813Z"}],"before_hash":"4be4dff3693b6aeac7b01bc175decd4ec4fb54cd67275081b15c122d21dbbad9","after_hash":"1e4b5df72cc79f33b77776d5c6192e30f5c8587f118e30fc75ca5c3db35378cc","item_hash_version":3,"event_class":"substantive","record_hash_version":1,"record_hash":"9374a7dff4f7b0c7669848c744ade9ec2717a49ee1477e94cc24c82e383bac04"} +{"hash_algorithm":"sha256","ts":"2026-09-18T04:23:30.934Z","author":"pi-agent","author_source":"asserted","agent_harness":"pi","agent_model":"gpt-5.6-sol","agent_model_source":"environment","agent_instance":"daeba66ee0c70117756542f5","agent_provenance":{"model":{"value":"gpt-5.6-sol","source":"environment"},"effort":null,"role":{"value":"implementer","source":"argv"},"topic":null},"op":"claim","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-09-18T04:23:30.934Z"},{"op":"add","path":"/metadata/assignee","value":"pi-agent"}],"before_hash":"1e4b5df72cc79f33b77776d5c6192e30f5c8587f118e30fc75ca5c3db35378cc","after_hash":"ce5e6cb62560bc1f5e4726a8d0146c945658c61ae12196ab9966dc8b76338fd0","item_hash_version":3,"event_class":"substantive","record_hash_version":1,"record_hash":"99551afee87ed8d802b0657505a666a54d492699bd73be26035d657257798da7"} +{"hash_algorithm":"sha256","ts":"2026-09-18T04:47:05.242Z","author":"pi-agent","author_source":"asserted","agent_harness":"pi","agent_model":"gpt-5.6-sol","agent_model_source":"environment","agent_instance":"daeba66ee0c70117756542f5","agent_provenance":{"model":{"value":"gpt-5.6-sol","source":"environment"},"effort":null,"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/6","value":{"created_at":"2026-09-18T04:47:05.242Z","author":"pi-agent","text":"Bootstrap tranche measured with npm run coverage: 400/400 pass; aggregate src L/B/F 80.10/80.05/75.93 (baseline 76.82/80.29/73.00). Real src/server.ts child starts on an ephemeral loopback port against the test PostgreSQL database and temporary PROJECTS_ROOT, answers /healthz, then shuts down gracefully on SIGTERM. Real host command dispatch covers status up/down/degraded, doctor free/occupied port, and stop absent/stale/live PID behavior. Ratcheted floors to L/B/F 80/80/75. Remaining uncovered lines by file: app.ts 77-78,93-94,153-156,182-183,219-221; auth.ts 10-11; board.ts 49; db.ts 20-27,200-204; health.ts 117-118,222-226,355-365; index.ts 27-30,196-211,277,297-298,344-399,460,491-494; oidc.ts 136-138,161-162,164-165,170-171,229-230,299-300,315-316,318-319,327-328,404-405,434-435,441-442,444-445,451-452,567-572; routes/admin.ts 37-39,90-92,107-109,125-127,136-138,149-151,165-167,186-188,202-204,225-227,244-245; routes/auth.ts 84-86,123-125,145-147,161-163,186-188,200-202; routes/extensions.ts 218-229,237-248,256-267; routes/github.ts 120-133,138-161,166-195,249-251,262-264,266-272,274-275,283-290,295-380,385-444,449-466; routes/groups.ts 28-30,60-62,101-103,125-127,143-145,195-197,232-234; routes/oidc.ts 46-52,65-78,91-108,147-152,154-156,163-188,192-247; routes/pm.ts 42-55,71-74,82-93,104-107,167-169,182-184,187-189,191-194,207-213,226-237,287-394,422-452,458,481-484,557-561,592-593,603,766-768,786-788,879-881,916-950,1178-1238,1401-1403,1432-1434,1452-1454,1464-1466,1479-1501,1506-1533,1538-1562,1567-1590,1595-1604,1609-1625,1630-1644,1649-1663,1668-1682,1687-1701,1706-1715,1720-1739,1744-1752,1757-1765,1770-1785,1790-1807,1812-1828,1833-1841,1846-1854,1859-1877,1882-1890,1895-1904,1909-1919,1937-1939,1945-1970,1972-2008,2018-2070,2075-2120,2128-2198,2203-2211,2216-2239,2244-2256,2261-2268,2273-2276,2281-2284,2289-2292,2297-2300,2305-2309,2314-2323,2342-2365,2384-2406,2411-2421,2426-2443,2448-2465,2470-2489,2494-2511,2516-2533,2538-2558,2563-2580,2585-2602,2607-2625,2630-2653,2658-2678,2683-2701,2709-2718,2722-2736,2741-2758,2768-2771,2784-2787,2804-2809; routes/projects.ts 78-80,117-129,137-139,152-154,172-174,191-193; routes/sharing.ts 59-61,156-158,181-183,210-212; server.ts 28-29,70-72,76-77,93-94; services/mutation-event-watcher.ts 109; services/pm-runner.ts 123-130,162-167,207-213,231-233,326-333,367-369,487-488,534-540,566-572,712-714,803-809,876-878,886-888,1006-1007; services/project-watcher.ts 47,156-157,205-206; services/realtime-bus.ts 187-190,192-196,216-221; services/sse.ts 322-323,344-345,405-429. Item remains in_progress because all metrics are not 100."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-09-18T04:47:05.242Z"}],"before_hash":"ce5e6cb62560bc1f5e4726a8d0146c945658c61ae12196ab9966dc8b76338fd0","after_hash":"0905e22ba0a988fb6b354a1fed6d323bb57ebc9d231e33910f8dae51cba3b22d","item_hash_version":3,"event_class":"substantive","record_hash_version":1,"record_hash":"650475e847a9104ebdbce724c8eaa3a382a034dd6386f0b53529438e69058d7b"} +{"hash_algorithm":"sha256","ts":"2026-09-18T04:47:22.708Z","author":"pi-agent","author_source":"asserted","agent_harness":"pi","agent_model":"gpt-5.6-sol","agent_model_source":"environment","agent_instance":"daeba66ee0c70117756542f5","agent_provenance":{"model":{"value":"gpt-5.6-sol","source":"environment"},"effort":null,"role":{"value":"implementer","source":"argv"},"topic":null},"op":"update","patch":[{"op":"add","path":"/metadata/tests/1","value":{"command":"npm run coverage","scope":"project","provenance":{"author":"pi-agent","created_at":"2026-09-18T04:47:22.446Z","source_kind":"local_mutation","source_ref":"test/cover-pm-web-bootstrap-and-remaining-routes"}}},{"op":"add","path":"/metadata/files/2","value":{"path":"test/smoke.test.ts","scope":"project"}},{"op":"add","path":"/metadata/files/3","value":{"path":"src/server.ts","scope":"project"}},{"op":"add","path":"/metadata/files/4","value":{"path":"test/extension.test.ts","scope":"project"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-09-18T04:47:22.708Z"}],"before_hash":"0905e22ba0a988fb6b354a1fed6d323bb57ebc9d231e33910f8dae51cba3b22d","after_hash":"b8f8ef459e8f495cb65fc7b57e8f99f0d40e1fd6e6623d429f55e8d21fab91ed","item_hash_version":3,"event_class":"maintenance","record_hash_version":1,"record_hash":"826dcac23b34a1b1dddf1908d6528ad5c3cdd5fcac77c7f7156755ea1b57f398"} +{"hash_algorithm":"sha256","ts":"2026-09-18T05:12:25.878Z","author":"pi-agent","author_source":"asserted","agent_harness":"pi","agent_model":"gpt-5.6-sol","agent_model_source":"environment","agent_instance":"daeba66ee0c70117756542f5","agent_provenance":{"model":{"value":"gpt-5.6-sol","source":"environment"},"effort":null,"role":null,"topic":null},"op":"comment_add","patch":[{"op":"add","path":"/metadata/comments/7","value":{"created_at":"2026-09-18T05:12:25.877Z","author":"pi-agent","text":"PR review follow-up: accepted both Greptile findings. Graceful signal shutdown now ends and removes all active SSE clients before server.close so streaming responses cannot block shutdown; added direct observable coverage. The live-PID test now tracks its helper child outside the body and always terminates/awaits it in finally. CI also required freshly built tracked dist/server.js and source map, which are included."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-09-18T05:12:25.878Z"}],"before_hash":"b8f8ef459e8f495cb65fc7b57e8f99f0d40e1fd6e6623d429f55e8d21fab91ed","after_hash":"c7c618be24f5b50ea3f3745c6d5efd466c1d18404d095131a8f59f5207a35ea6","item_hash_version":3,"event_class":"substantive","record_hash_version":1,"record_hash":"009beffe99acba2dcdaa0ad8fa95f1fd0994f947d7b6314329b3c6f807ffa520"} +{"hash_algorithm":"sha256","ts":"2026-09-18T05:12:26.831Z","author":"pi-agent","author_source":"asserted","agent_harness":"pi","agent_model":"gpt-5.6-sol","agent_model_source":"environment","agent_instance":"daeba66ee0c70117756542f5","agent_provenance":{"model":{"value":"gpt-5.6-sol","source":"environment"},"effort":null,"role":{"value":"implementer","source":"argv"},"topic":null},"op":"update","patch":[{"op":"add","path":"/metadata/files/5","value":{"path":"src/services/sse.ts","scope":"project"}},{"op":"add","path":"/metadata/files/6","value":{"path":"test/sse.test.ts","scope":"project"}},{"op":"add","path":"/metadata/files/7","value":{"path":"dist/server.js","scope":"project"}},{"op":"add","path":"/metadata/files/8","value":{"path":"dist/server.js.map","scope":"project"}},{"op":"replace","path":"/metadata/updated_at","value":"2026-09-18T05:12:26.831Z"}],"before_hash":"c7c618be24f5b50ea3f3745c6d5efd466c1d18404d095131a8f59f5207a35ea6","after_hash":"e301d0660871aeeb2acf716cf047e8a3550462fe7851131aa009298442613986","item_hash_version":3,"event_class":"maintenance","record_hash_version":1,"record_hash":"c8f1644814cc41c8f64125ab98e6b8a49076b8df5baab2ac0e74537209b2446a"} diff --git a/.agents/pm/tasks/pm-web-ulgy.toon b/.agents/pm/tasks/pm-web-ulgy.toon index 1784f46..bc7db29 100644 --- a/.agents/pm/tasks/pm-web-ulgy.toon +++ b/.agents/pm/tasks/pm-web-ulgy.toon @@ -6,24 +6,60 @@ status: in_progress priority: 2 tags: [] created_at: "2026-07-29T17:26:40.207Z" -updated_at: "2026-09-18T03:43:37.813Z" +updated_at: "2026-09-18T05:12:26.831Z" +assignee: pi-agent claim_principal: pi-agent author: codex estimated_minutes: 1800 acceptance_criteria: routes/pm.ts is exercised against a real pm workspace rather than a fake pm binary so in-process SDK dispatch is observable; a regression test asserts that a '..' folder traversal is rejected and that items never land outside the pm root; POSITIONAL_KEYS coverage proves no positional is silently dropped; index.ts and server.ts bootstrap paths are covered; thresholds ratchet again parent: pm-web-9ulj -comments[6]{created_at,author,text}: +comments[8]{created_at,author,text}: "2026-09-18T03:12:09.394Z",pi-agent,"Baseline npm run coverage: 395/395 tests pass; aggregate src coverage L/B/F 71.06/81.28/65.71. Per-file baseline is captured in the coverage run for this branch; largest gaps are routes/pm.ts 30.67/58.95/14.41, index.ts 52.40/94.59/30.00, routes/github.ts 45.63/71.79/35.71, routes/oidc.ts 48.00/100.00/22.22, server.ts 62.79/33.33/33.33, and db.ts 37.56/70.00/66.67." "2026-09-18T03:12:10.032Z",pi-agent,"Scope note: public/src is intentionally excluded from this PR's coverage gate and is currently unmeasured. It contains 14,884 TypeScript source lines by find public/src -type f -name '*.ts' | xargs wc -l." "2026-09-18T03:28:07.103Z",pi-agent,"Milestone: added real loopback HTTP + real PostgreSQL + real temporary pm workspace coverage. Client B now opens the production SSE endpoint and observes client A's create event. Two edit collaborators concurrently PATCH distinct fields on one item; both writes survive and every history JSONL row parses, with both messages present. A lifecycle scenario exercises rich create, reads, updates, comments, notes, append/history, dependencies, relationships, status views, close/delete, authorization misses, validation errors, and missing-item failures." "2026-09-18T03:28:07.561Z",pi-agent,"Measured after npm run coverage: 398/398 pass; aggregate src L/B/F 76.82/80.06/73.00 (baseline 71.06/81.28/65.71). Ratcheted declared floors to L/B/F 76/80/73; branch floor remains at 80 rather than weakening. Remaining uncovered source lines by file: app.ts 77-78,93-94,153-156,182-183,219-221; auth.ts 10-11; board.ts 49; db.ts 20-27,86-205; health.ts 117-118,222-226,355-365; index.ts 27-30,182-185,196-211,214-236,239-249,251-254,268-279,290-299,311-319,344-399,415-435,451-505,521-582; oidc.ts 136-138,161-162,164-165,170-171,229-230,238-239,299-300,315-316,318-319,327-328,404-405,434-435,441-442,444-445,451-452,567-572; routes/admin.ts 37-39,90-92,107-109,125-127,136-138,149-151,165-167,186-188,202-204,225-227,244-245; routes/auth.ts 84-86,123-125,145-147,161-163,186-188,200-202; routes/extensions.ts 218-229,237-248,256-267; routes/github.ts 120-133,138-161,166-195,249-251,262-264,266-272,274-275,283-290,295-380,385-444,449-466; routes/groups.ts 28-30,60-62,101-103,125-127,143-145,195-197,232-234; routes/oidc.ts 46-52,65-78,91-108,147-152,154-156,163-188,192-247; routes/pm.ts 42-55,71-74,82-93,104-107,167-169,182-184,187-189,191-194,207-213,226-237,287-394,422-452,458,481-484,557-561,592-593,603,766-768,786-788,879-881,916-950,1178-1238,1401-1403,1432-1434,1452-1454,1464-1466,1479-1501,1506-1533,1538-1562,1567-1590,1595-1604,1609-1625,1630-1644,1649-1663,1668-1682,1687-1701,1706-1715,1720-1739,1744-1752,1757-1765,1770-1785,1790-1807,1812-1828,1833-1841,1846-1854,1859-1877,1882-1890,1895-1904,1909-1919,1937-1939,1945-1970,1972-2008,2018-2070,2075-2120,2128-2198,2203-2211,2216-2239,2244-2256,2261-2268,2273-2276,2281-2284,2289-2292,2297-2300,2305-2309,2314-2323,2342-2365,2384-2406,2411-2421,2426-2443,2448-2465,2470-2489,2494-2511,2516-2533,2538-2558,2563-2580,2585-2602,2607-2625,2630-2653,2658-2678,2683-2701,2709-2718,2722-2736,2741-2758,2768-2771,2784-2787,2804-2809; routes/projects.ts 78-80,117-129,137-139,152-154,172-174,191-193; routes/sharing.ts 59-61,156-158,181-183,210-212; server.ts 28-29,57-86; services/mutation-event-watcher.ts 109; services/pm-runner.ts 123-130,162-167,207-213,231-233,326-333,367-369,487-488,534-540,566-572,712-714,803-809,876-878,886-888,1006-1007; services/project-watcher.ts 44-49,156-157,205-206,325-333; services/realtime-bus.ts 187-190,192-196,216-221; services/sse.ts 322-323,344-345,405-429. Files already at 100 lines: crypto.ts, csrf.ts, ical.ts, middleware/auth.ts, rate-limit.ts, route-params.ts, services/fleet-snapshot.ts, services/package-catalog.ts." "2026-09-18T03:35:19.045Z",pi-agent,"First release:check run exposed branch-coverage nondeterminism at 79.97% versus the preserved 80% floor. Added observable alternate-path coverage for unfiltered list/list-all, an SDK-issued compatible pagination cursor, invalid context depth, activity without a limit, presence without connected clients, and a missing item read. The cursor test initially omitted its original filters and correctly received the pm SDK fingerprint-mismatch 400; it now replays the originating type/limit filters and passes." "2026-09-18T03:43:37.813Z",pi-agent,"PR review finding accepted: the task remains in_progress because successful src/index.ts and src/server.ts bootstrap coverage and additional positional actions remain unfinished. This PR is a measured ratchet tranche, not completion of the full item." + "2026-09-18T04:47:05.242Z",pi-agent,"Bootstrap tranche measured with npm run coverage: 400/400 pass; aggregate src L/B/F 80.10/80.05/75.93 (baseline 76.82/80.29/73.00). Real src/server.ts child starts on an ephemeral loopback port against the test PostgreSQL database and temporary PROJECTS_ROOT, answers /healthz, then shuts down gracefully on SIGTERM. Real host command dispatch covers status up/down/degraded, doctor free/occupied port, and stop absent/stale/live PID behavior. Ratcheted floors to L/B/F 80/80/75. Remaining uncovered lines by file: app.ts 77-78,93-94,153-156,182-183,219-221; auth.ts 10-11; board.ts 49; db.ts 20-27,200-204; health.ts 117-118,222-226,355-365; index.ts 27-30,196-211,277,297-298,344-399,460,491-494; oidc.ts 136-138,161-162,164-165,170-171,229-230,299-300,315-316,318-319,327-328,404-405,434-435,441-442,444-445,451-452,567-572; routes/admin.ts 37-39,90-92,107-109,125-127,136-138,149-151,165-167,186-188,202-204,225-227,244-245; routes/auth.ts 84-86,123-125,145-147,161-163,186-188,200-202; routes/extensions.ts 218-229,237-248,256-267; routes/github.ts 120-133,138-161,166-195,249-251,262-264,266-272,274-275,283-290,295-380,385-444,449-466; routes/groups.ts 28-30,60-62,101-103,125-127,143-145,195-197,232-234; routes/oidc.ts 46-52,65-78,91-108,147-152,154-156,163-188,192-247; routes/pm.ts 42-55,71-74,82-93,104-107,167-169,182-184,187-189,191-194,207-213,226-237,287-394,422-452,458,481-484,557-561,592-593,603,766-768,786-788,879-881,916-950,1178-1238,1401-1403,1432-1434,1452-1454,1464-1466,1479-1501,1506-1533,1538-1562,1567-1590,1595-1604,1609-1625,1630-1644,1649-1663,1668-1682,1687-1701,1706-1715,1720-1739,1744-1752,1757-1765,1770-1785,1790-1807,1812-1828,1833-1841,1846-1854,1859-1877,1882-1890,1895-1904,1909-1919,1937-1939,1945-1970,1972-2008,2018-2070,2075-2120,2128-2198,2203-2211,2216-2239,2244-2256,2261-2268,2273-2276,2281-2284,2289-2292,2297-2300,2305-2309,2314-2323,2342-2365,2384-2406,2411-2421,2426-2443,2448-2465,2470-2489,2494-2511,2516-2533,2538-2558,2563-2580,2585-2602,2607-2625,2630-2653,2658-2678,2683-2701,2709-2718,2722-2736,2741-2758,2768-2771,2784-2787,2804-2809; routes/projects.ts 78-80,117-129,137-139,152-154,172-174,191-193; routes/sharing.ts 59-61,156-158,181-183,210-212; server.ts 28-29,70-72,76-77,93-94; services/mutation-event-watcher.ts 109; services/pm-runner.ts 123-130,162-167,207-213,231-233,326-333,367-369,487-488,534-540,566-572,712-714,803-809,876-878,886-888,1006-1007; services/project-watcher.ts 47,156-157,205-206; services/realtime-bus.ts 187-190,192-196,216-221; services/sse.ts 322-323,344-345,405-429. Item remains in_progress because all metrics are not 100." + "2026-09-18T05:12:25.877Z",pi-agent,"PR review follow-up: accepted both Greptile findings. Graceful signal shutdown now ends and removes all active SSE clients before server.close so streaming responses cannot block shutdown; added direct observable coverage. The live-PID test now tracks its helper child outside the body and always terminates/awaits it in finally. CI also required freshly built tracked dist/server.js and source map, which are included." notes[1]{created_at,author,text}: "2026-08-21T11:41:29.704Z",codex,"The certified complete-read tranche adds a real-workspace end-to-end test for paginated list-all,complete board/search/export/calendar views and raises routes/pm.ts from 27.28 to 30.70 percent lines and from 7.08 to 14.41 percent functions; branches are 58.95 percent. The shared output-owner test also raises src/index.ts to 51.20 lines,94.12 branches,and 31.58 functions. Most command and bootstrap paths remain uncovered,so this task stays open." -files[2]{path,scope,note}: - test/pm-collaboration.test.ts,project,Real HTTP collaboration and command lifecycle coverage - package.json,project,Coverage floors ratcheted to measured whole-number floors -tests[1]{command,path,scope,timeout_seconds,provenance{author,created_at,source_kind,source_ref}}: - npm run coverage,.,project,1200,pi-agent,"2026-09-18T03:28:08.077Z",local_mutation,test/cover-pm-web-server-routes-and-services +files[9]: + - path: test/pm-collaboration.test.ts + scope: project + note: Real HTTP collaboration and command lifecycle coverage + - path: package.json + scope: project + note: Coverage floors ratcheted to measured whole-number floors + - path: test/smoke.test.ts + scope: project + - path: src/server.ts + scope: project + - path: test/extension.test.ts + scope: project + - path: src/services/sse.ts + scope: project + - path: test/sse.test.ts + scope: project + - path: dist/server.js + scope: project + - path: dist/server.js.map + scope: project +tests[2]: + - command: npm run coverage + path: . + scope: project + timeout_seconds: 1200 + provenance: + author: pi-agent + created_at: "2026-09-18T03:28:08.077Z" + source_kind: local_mutation + source_ref: test/cover-pm-web-server-routes-and-services + - command: npm run coverage + scope: project + provenance: + author: pi-agent + created_at: "2026-09-18T04:47:22.446Z" + source_kind: local_mutation + source_ref: test/cover-pm-web-bootstrap-and-remaining-routes body: "" diff --git a/dist/server.js b/dist/server.js index 591552b..e6aa1fc 100644 --- a/dist/server.js +++ b/dist/server.js @@ -4,7 +4,7 @@ import { fileURLToPath } from "node:url"; import { initSchema, assertDbConfigured, pool } from "./db.js"; import { createApp } from "./app.js"; import { projectsRoot } from "./services/pm-runner.js"; -import { cleanupStaleClients } from "./services/sse.js"; +import { cleanupStaleClients, closeAllSSEClients } from "./services/sse.js"; import { startRealtimeBus } from "./services/realtime-bus.js"; import { startProjectWatcher } from "./services/project-watcher.js"; import { startMutationEventWatcher } from "./services/mutation-event-watcher.js"; @@ -70,9 +70,23 @@ initSchema() console.error(`Server error on :${PORT}:`, err.message); process.exit(1); }); - server.on("close", () => { stopProjectWatcher(); stopMutationEventWatcher(); void closeRealtimeBus(); }); // Periodic cleanup of stale SSE clients - setInterval(cleanupStaleClients, 5 * 60 * 1000); + const staleClientTimer = setInterval(cleanupStaleClients, 5 * 60 * 1000); + server.on("close", () => { + clearInterval(staleClientTimer); + stopProjectWatcher(); + stopMutationEventWatcher(); + void closeRealtimeBus() + .finally(() => pool.end()) + .finally(() => process.exit(0)); + }); + /** End streaming responses before waiting for the HTTP server to close. */ + const shutdown = () => { + closeAllSSEClients(); + server.close(); + }; + process.once("SIGINT", shutdown); + process.once("SIGTERM", shutdown); }) .catch((err) => { console.error("Failed to initialize pm-web runtime:", err instanceof Error ? err.message : err); diff --git a/dist/server.js.map b/dist/server.js.map index 3dd06e5..ff6b8ae 100644 --- a/dist/server.js.map +++ b/dist/server.js.map @@ -1 +1 @@ -{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/dist/services/sse.d.ts b/dist/services/sse.d.ts index f5f3008..b418705 100644 --- a/dist/services/sse.d.ts +++ b/dist/services/sse.d.ts @@ -152,6 +152,13 @@ export declare function getProjectPresence(projectId: string): PresenceUser[]; export declare function setupSSEHeaders(res: Response): void; /** Return the total number of currently connected SSE clients. */ export declare function getSSEClientCount(): number; +/** + * End every active SSE response and remove it from the connection indexes. + * + * Server shutdown calls this before `server.close()` because open streaming + * responses otherwise keep the HTTP server's close event pending indefinitely. + */ +export declare function closeAllSSEClients(): void; /** * Close long-lived clients and prune stale signal entries. * diff --git a/dist/services/sse.js b/dist/services/sse.js index 7141a74..d3eb032 100644 --- a/dist/services/sse.js +++ b/dist/services/sse.js @@ -352,6 +352,28 @@ export function setupSSEHeaders(res) { export function getSSEClientCount() { return byId.size; } +/** + * End every active SSE response and remove it from the connection indexes. + * + * Server shutdown calls this before `server.close()` because open streaming + * responses otherwise keep the HTTP server's close event pending indefinitely. + */ +export function closeAllSSEClients() { + for (const client of [...byId.values()]) { + try { + client.res.end(); + } + catch (error) { + // One response that cannot end (e.g. its socket already failed) must not + // abort shutdown: the remaining streams still have to close, and the + // caller still has to reach server.close(). + console.error(`SSE client ${client.id} failed to end during shutdown:`, error instanceof Error ? error.message : error); + } + finally { + removeClient(client); + } + } +} /** * Close long-lived clients and prune stale signal entries. * diff --git a/dist/services/sse.js.map b/dist/services/sse.js.map index 5ccfcae..eddb81a 100644 --- a/dist/services/sse.js.map +++ b/dist/services/sse.js.map @@ -1 +1 @@ -{"version":3,"file":"sse.js","sourceRoot":"","sources":["../../src/services/sse.ts"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"sse.js","sourceRoot":"","sources":["../../src/services/sse.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/package.json b/package.json index fa40077..85037b8 100644 --- a/package.json +++ b/package.json @@ -132,9 +132,9 @@ "test/*.test.ts" ], "thresholds": { - "lines": 76, + "lines": 80, "branches": 80, - "functions": 73 + "functions": 75 }, "ignore": [] } diff --git a/src/server.ts b/src/server.ts index afa5e32..3436f28 100644 --- a/src/server.ts +++ b/src/server.ts @@ -4,7 +4,7 @@ import { fileURLToPath } from "node:url"; import { initSchema, assertDbConfigured, pool } from "./db.ts"; import { createApp } from "./app.ts"; import { projectsRoot } from "./services/pm-runner.ts"; -import { cleanupStaleClients } from "./services/sse.ts"; +import { cleanupStaleClients, closeAllSSEClients } from "./services/sse.ts"; import { startRealtimeBus } from "./services/realtime-bus.ts"; import { startProjectWatcher } from "./services/project-watcher.ts"; import { startMutationEventWatcher } from "./services/mutation-event-watcher.ts"; @@ -76,9 +76,23 @@ initSchema() console.error(`Server error on :${PORT}:`, err.message); process.exit(1); }); - server.on("close", () => { stopProjectWatcher(); stopMutationEventWatcher(); void closeRealtimeBus(); }); // Periodic cleanup of stale SSE clients - setInterval(cleanupStaleClients, 5 * 60 * 1000); + const staleClientTimer = setInterval(cleanupStaleClients, 5 * 60 * 1000); + server.on("close", () => { + clearInterval(staleClientTimer); + stopProjectWatcher(); + stopMutationEventWatcher(); + void closeRealtimeBus() + .finally(() => pool.end()) + .finally(() => process.exit(0)); + }); + /** End streaming responses before waiting for the HTTP server to close. */ + const shutdown = (): void => { + closeAllSSEClients(); + server.close(); + }; + process.once("SIGINT", shutdown); + process.once("SIGTERM", shutdown); }) .catch((err) => { console.error("Failed to initialize pm-web runtime:", err instanceof Error ? err.message : err); diff --git a/src/services/sse.ts b/src/services/sse.ts index 3596db7..6dc8297 100644 --- a/src/services/sse.ts +++ b/src/services/sse.ts @@ -394,6 +394,27 @@ export function getSSEClientCount(): number { return byId.size; } +/** + * End every active SSE response and remove it from the connection indexes. + * + * Server shutdown calls this before `server.close()` because open streaming + * responses otherwise keep the HTTP server's close event pending indefinitely. + */ +export function closeAllSSEClients(): void { + for (const client of [...byId.values()]) { + try { + client.res.end(); + } catch (error) { + // One response that cannot end (e.g. its socket already failed) must not + // abort shutdown: the remaining streams still have to close, and the + // caller still has to reach server.close(). + console.error(`SSE client ${client.id} failed to end during shutdown:`, error instanceof Error ? error.message : error); + } finally { + removeClient(client); + } + } +} + /** * Close long-lived clients and prune stale signal entries. * diff --git a/test/extension.test.ts b/test/extension.test.ts index 552561b..879a367 100644 --- a/test/extension.test.ts +++ b/test/extension.test.ts @@ -21,8 +21,10 @@ test("resolvePort defaults to 4000", () => { assert.strictEqual(resolvePort({}, {}), "4000"); }); -test("resolvePort ignores empty flag and empty env", () => { +test("resolvePort ignores absent values while retaining numeric zero", () => { assert.strictEqual(resolvePort({ port: "" }, { PORT: "" }), "4000"); + assert.strictEqual(resolvePort({ port: null }, { PORT: "4555" }), "4555"); + assert.strictEqual(resolvePort({ port: 0 }, { PORT: "4555" }), "0"); }); test("pidfilePath keys by port in the temp dir by default", () => { @@ -33,6 +35,7 @@ test("pidfilePath keys by port in the temp dir by default", () => { test("pidfilePath honors PM_WEB_STATE_DIR", () => { const p = pidfilePath("4000", { PM_WEB_STATE_DIR: "/var/state" }, os.tmpdir()); assert.strictEqual(p, path.join("/var/state", "pm-web-4000.pid")); + assert.strictEqual(pidfilePath("4001", { PM_WEB_STATE_DIR: "" }, "/tmp/fallback"), path.join("/tmp/fallback", "pm-web-4001.pid")); }); test("nodeVersionMeetsRequirement enforces the package engine floor", () => { @@ -41,6 +44,8 @@ test("nodeVersionMeetsRequirement enforces the package engine floor", () => { assert.strictEqual(nodeVersionMeetsRequirement("22.18.0"), true); assert.strictEqual(nodeVersionMeetsRequirement("22.19.0"), true); assert.strictEqual(nodeVersionMeetsRequirement("23.0.0"), true); + assert.strictEqual(nodeVersionMeetsRequirement("not-a-version"), false); + assert.strictEqual(nodeVersionMeetsRequirement("v22.18.0-nightly"), true); }); test("shapeStatusResult marks a reachable server as up", () => { diff --git a/test/smoke.test.ts b/test/smoke.test.ts index 863f97e..943e7ef 100644 --- a/test/smoke.test.ts +++ b/test/smoke.test.ts @@ -1,5 +1,10 @@ import assert from "node:assert/strict"; -import { execFileSync } from "node:child_process"; +import { execFileSync, spawn } from "node:child_process"; +import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs"; +import { createServer as createHttpServer } from "node:http"; +import { createServer as createNetServer } from "node:net"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; import test from "node:test"; import { isHostOutputSuppressed } from "@unbrained/pm-cli/sdk"; @@ -27,6 +32,28 @@ async function harness() { return created; } +/** Reserve and release an ephemeral loopback port for a child server test. */ +async function ephemeralPort(): Promise { + const reserver = createNetServer(); + await new Promise((resolve, reject) => { + reserver.once("error", reject); + reserver.listen(0, "127.0.0.1", resolve); + }); + const address = reserver.address(); + assert.ok(address && typeof address !== "string"); + const port = address.port; + await new Promise((resolve, reject) => { + reserver.close((err) => err ? reject(err) : resolve()); + }); + return port; +} + +/** Extract the extension value retained by the real host command runner. */ +function commandResult(value: { handled: boolean; result: unknown }): unknown { + assert.equal(value.handled, true); + return isHostOutputSuppressed(value.result) ? value.result.result : value.result; +} + test("pm-web extension activates cleanly against the real pm host", async () => { const ext = await harness(); assert.strictEqual(ext.name, "pm-web"); @@ -96,6 +123,145 @@ test("server entrypoint exits non-zero without DATABASE_URL", () => { assert.notEqual(status, 0, "expected a non-zero exit code"); }); +test("production server bootstrap listens on an ephemeral port and answers HTTP", { timeout: 30_000 }, async () => { + const port = await ephemeralPort(); + const root = mkdtempSync(join(tmpdir(), "pm-web-")); + const child = spawn(process.execPath, ["src/server.ts"], { + cwd: process.cwd(), + env: { + ...process.env, + PORT: String(port), + PROJECTS_ROOT: root, + PM_REALTIME_MUTATION_EVENTS: "false", + }, + stdio: ["ignore", "pipe", "pipe"], + }); + let stdout = ""; + let stderr = ""; + child.stdout.setEncoding("utf8"); + child.stderr.setEncoding("utf8"); + child.stdout.on("data", (chunk: string) => { stdout += chunk; }); + child.stderr.on("data", (chunk: string) => { stderr += chunk; }); + + try { + await new Promise((resolve, reject) => { + const timer = setTimeout(() => reject(new Error(`server did not start; stdout=${stdout} stderr=${stderr}`)), 20_000); + const inspect = () => { + if (stdout.includes(`pm-web running on :${port}`)) { + clearTimeout(timer); + resolve(); + } + }; + child.stdout.on("data", inspect); + child.once("exit", (code, signal) => { + clearTimeout(timer); + reject(new Error(`server exited before listening: code=${code} signal=${signal}; stderr=${stderr}`)); + }); + }); + + const response = await fetch(`http://127.0.0.1:${port}/healthz`); + const body = await response.json() as { ok: boolean; version: string }; + assert.equal(response.status, 200); + assert.equal(body.ok, true); + assert.match(body.version, /^2026\./); + } finally { + child.kill("SIGTERM"); + await new Promise((resolve) => { + if (child.exitCode !== null || child.signalCode !== null) resolve(); + else child.once("exit", () => resolve()); + }); + rmSync(root, { recursive: true, force: true }); + } +}); + +test("registered status, doctor, and stop commands use real environment state", { timeout: 30_000 }, async () => { + const port = await ephemeralPort(); + const root = mkdtempSync(join(tmpdir(), "pm-web-")); + const pmRoot = join(root, ".agents", "pm"); + const stateDir = join(root, "state"); + mkdirSync(pmRoot, { recursive: true }); + mkdirSync(stateDir, { recursive: true }); + writeFileSync(join(pmRoot, "settings.json"), "{}\n", "utf8"); + const previousProjectsRoot = process.env.PROJECTS_ROOT; + const previousStateDir = process.env.PM_WEB_STATE_DIR; + process.env.PROJECTS_ROOT = root; + process.env.PM_WEB_STATE_DIR = stateDir; + let ext: Awaited> | undefined; + let target: ReturnType | null = null; + + try { + ext = await harness(); + const down = commandResult(await ext.runCommand({ command: "web status", options: { port }, pmRoot })) as { status?: string; port?: number }; + assert.equal(down.status, "down"); + assert.equal(down.port, port); + + const doctor = commandResult(await ext.runCommand({ command: "web doctor", options: { port }, pmRoot })) as { version?: string; port?: number }; + assert.match(doctor.version ?? "", /^2026\./); + assert.equal(doctor.port, port); + + let healthStatus = 200; + let healthBody = JSON.stringify({ ok: true, version: "test-version" }); + const healthServer = createHttpServer((_request, response) => { + response.writeHead(healthStatus, { "content-type": "application/json" }); + response.end(healthBody); + }); + await new Promise((resolve, reject) => { + healthServer.once("error", reject); + healthServer.listen(port, "127.0.0.1", resolve); + }); + try { + const up = commandResult(await ext.runCommand({ command: "web status", options: { port }, global: { json: false }, pmRoot })) as { status?: string; version?: string }; + assert.equal(up.status, "up"); + assert.equal(up.version, "test-version"); + + healthStatus = 503; + healthBody = "not-json"; + const degraded = commandResult(await ext.runCommand({ command: "web status", options: { port }, global: { json: false }, pmRoot })) as { status?: string; version?: string | null }; + assert.equal(degraded.status, "degraded"); + assert.equal(degraded.version, null); + + const occupiedDoctor = commandResult(await ext.runCommand({ command: "web doctor", options: { port }, pmRoot })) as { checks?: Array<{ name: string; ok: boolean }> }; + assert.equal(occupiedDoctor.checks?.find((check) => check.name === "port_available")?.ok, false); + } finally { + await new Promise((resolve, reject) => { + healthServer.close((err) => err ? reject(err) : resolve()); + }); + } + + const stopped = commandResult(await ext.runCommand({ command: "web stop", options: { port }, pmRoot })) as { status?: string; port?: number }; + assert.equal(stopped.status, "not_running"); + assert.equal(stopped.port, port); + + writeFileSync(join(stateDir, `pm-web-${port}.pid`), "2147483647\n", "utf8"); + const stale = commandResult(await ext.runCommand({ command: "web stop", options: { port }, pmRoot })) as { status?: string; pid?: number }; + assert.equal(stale.status, "not_running"); + assert.equal(stale.pid, 2147483647); + + const startedTarget = spawn(process.execPath, ["-e", "setInterval(() => {}, 1000)"], { stdio: "ignore" }); + target = startedTarget; + assert.equal(typeof startedTarget.pid, "number"); + writeFileSync(join(stateDir, `pm-web-${port}.pid`), `${startedTarget.pid}\n`, "utf8"); + const terminated = commandResult(await ext.runCommand({ command: "web stop", options: { port }, pmRoot })) as { status?: string; pid?: number }; + assert.equal(terminated.status, "stopped"); + assert.equal(terminated.pid, startedTarget.pid); + await new Promise((resolve) => { + if (startedTarget.exitCode !== null || startedTarget.signalCode !== null) resolve(); + else startedTarget.once("exit", () => resolve()); + }); + } finally { + if (target && target.exitCode === null && target.signalCode === null) { + target.kill("SIGTERM"); + await new Promise((resolve) => target?.once("exit", () => resolve())); + } + await ext?.deactivate(); + if (previousProjectsRoot === undefined) delete process.env.PROJECTS_ROOT; + else process.env.PROJECTS_ROOT = previousProjectsRoot; + if (previousStateDir === undefined) delete process.env.PM_WEB_STATE_DIR; + else process.env.PM_WEB_STATE_DIR = previousStateDir; + rmSync(root, { recursive: true, force: true }); + } +}); + test("no command redeclares a host-owned global flag", async () => { // Guards the whole surface, not just the commands that regressed: // registering any of these makes the host reject the command outright, and diff --git a/test/sse.test.ts b/test/sse.test.ts index 9840de7..5315ddf 100644 --- a/test/sse.test.ts +++ b/test/sse.test.ts @@ -5,6 +5,7 @@ import type { Response } from "express"; import { addSSEClient, broadcastProjectEvent, + closeAllSSEClients, configureProjectEventPublisher, consumeSignaledItemMutation, getProjectPresence, @@ -100,6 +101,29 @@ test("project events and presence are isolated per project and clients are index assert.equal(getProjectPresence(projectId).length, 0); }); +test("shutdown closes and removes every active SSE client", () => { + let ended = 0; + const response = { + write: () => true, + end: () => { ended += 1; }, + } as unknown as Response; + addSSEClient({ + id: "shutdown-a", projectId, userId: "user-a", displayName: "User A", + currentView: "items", res: response, connectedAt: new Date(), + }); + addSSEClient({ + id: "shutdown-b", projectId: otherProjectId, userId: "user-b", displayName: "User B", + currentView: "items", res: response, connectedAt: new Date(), + }); + + closeAllSSEClients(); + + assert.equal(ended, 2); + assert.equal(getSSEClientCount(), 0); + assert.deepEqual(getProjectPresence(projectId), []); + assert.deepEqual(getProjectPresence(otherProjectId), []); +}); + test("broadcastProjectEvent notes per-item signal when data has a string itemId", () => { // A route broadcast with a granular itemId payload should register a per-item // signal that the mutation-event watcher can consume to skip the duplicate. @@ -140,3 +164,30 @@ test("consumeSignaledItemMutation is isolated per project", () => { assert.equal(consumeSignaledItemMutation(otherProjectId, "shared-item"), false, "different project has no signal"); assert.equal(consumeSignaledItemMutation(projectId, "shared-item"), true, "original project has the signal"); }); + +test("shutdown keeps closing the remaining SSE clients when one response fails to end", () => { + let ended = 0; + const failing = { + write: () => true, + end: () => { throw new Error("socket already destroyed"); }, + } as unknown as Response; + const healthy = { + write: () => true, + end: () => { ended += 1; }, + } as unknown as Response; + addSSEClient({ + id: "shutdown-failing", projectId, userId: "user-a", displayName: "User A", + currentView: "items", res: failing, connectedAt: new Date(), + }); + addSSEClient({ + id: "shutdown-healthy", projectId: otherProjectId, userId: "user-b", displayName: "User B", + currentView: "items", res: healthy, connectedAt: new Date(), + }); + + assert.doesNotThrow(() => closeAllSSEClients()); + + assert.equal(ended, 1); + assert.equal(getSSEClientCount(), 0); + assert.deepEqual(getProjectPresence(projectId), []); + assert.deepEqual(getProjectPresence(otherProjectId), []); +});