From 15d3188a536d48b6d368937ed32792ca3c29b252 Mon Sep 17 00:00:00 2001 From: SteveBot <1153461+unbraind@users.noreply.github.com> Date: Fri, 18 Sep 2026 05:28:21 +0200 Subject: [PATCH 1/5] test: cover collaborative pm route behavior --- .agents/pm/history/pm-web-ulgy.jsonl | 7 + .agents/pm/tasks/pm-web-ulgy.toon | 16 +- package.json | 4 +- test/pm-collaboration.test.ts | 436 +++++++++++++++++++++++++++ 4 files changed, 459 insertions(+), 4 deletions(-) create mode 100644 test/pm-collaboration.test.ts diff --git a/.agents/pm/history/pm-web-ulgy.jsonl b/.agents/pm/history/pm-web-ulgy.jsonl index 00666149..f020bb8a 100644 --- a/.agents/pm/history/pm-web-ulgy.jsonl +++ b/.agents/pm/history/pm-web-ulgy.jsonl @@ -1,3 +1,10 @@ {"ts":"2026-07-29T17:26:40.207Z","author":"codex","author_source":"configured","agent_harness":"claude-code","agent_instance":"4c03c6f67e57932d8ca450fb","agent_provenance":{"model":null,"effort":{"value":"xhigh","source":"environment"}},"op":"create","patch":[{"op":"add","path":"/metadata/id","value":"pm-web-ulgy"},{"op":"add","path":"/metadata/title","value":"Cover the routes/pm.ts command surface and the index/server bootstrap in pm-web"},{"op":"add","path":"/metadata/description","value":"Stage 2 of pm-web-9ulj, tracked separately because routes/pm.ts is 2704 lines at 7.08% function coverage and is far larger than the rest of the route layer combined. It proxies the pm CLI for every item operation, and memory of this codebase records two traps that only real-workspace tests can catch: runPm serves most actions in-process via the SDK, so unmapped positionals are silently dropped unless added to POSITIONAL_KEYS and a fake-pm-binary harness cannot observe it at all; and the --folder parameter honours '..', which without the isSafeTypeFolder guard writes items outside the pm root and is cross-tenant on the hosted deployment. Also covers src/index.ts (42.83% lines, 26.32% functions) and src/server.ts (42.31%, 0% functions). Depends on the stage 1 harness landing first."},{"op":"add","path":"/metadata/type","value":"Task"},{"op":"add","path":"/metadata/status","value":"open"},{"op":"add","path":"/metadata/priority","value":2},{"op":"add","path":"/metadata/tags","value":[]},{"op":"add","path":"/metadata/created_at","value":"2026-07-29T17:26:40.207Z"},{"op":"add","path":"/metadata/updated_at","value":"2026-07-29T17:26:40.207Z"},{"op":"add","path":"/metadata/author","value":"codex"},{"op":"add","path":"/metadata/acceptance_criteria","value":"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"},{"op":"add","path":"/metadata/parent","value":"pm-web-9ulj"}],"before_hash":"3cc22dff72be7b14824654a7a64ea62b04799939b2fee54c1b5f52ca60bf6df0","after_hash":"0a909b8fc6a1597b2bc7baadc8390b9c153e4e480ac23c2bdb933947fb3e9beb","message":""} {"ts":"2026-08-21T11:41:29.721Z","author":"codex","author_source":"configured","agent_harness":"codex","agent_instance":"04a1f79c921fb60abca72469","agent_provenance":{"model":null,"effort":null,"role":{"value":"implementer","source":"argv"},"topic":null},"op":"update","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-08-21T11:41:29.721Z"},{"op":"add","path":"/metadata/notes","value":[{"created_at":"2026-08-21T11:41:29.704Z","author":"codex","text":"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."}]}],"before_hash":"0a909b8fc6a1597b2bc7baadc8390b9c153e4e480ac23c2bdb933947fb3e9beb","after_hash":"4d9b1679a0d1f867b5c38b474bc417cd475ff41775c8eac83944c1e21271f48e","item_hash_version":2,"message":"Record partial Stage 2 coverage gain without closing the remaining route surface"} {"ts":"2026-08-21T11:42:35.828Z","author":"codex","author_source":"configured","agent_harness":"codex","agent_instance":"04a1f79c921fb60abca72469","agent_provenance":{"model":null,"effort":null,"role":{"value":"implementer","source":"argv"},"topic":null},"op":"update","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-08-21T11:42:35.828Z"},{"op":"add","path":"/metadata/estimated_minutes","value":1800}],"before_hash":"4d9b1679a0d1f867b5c38b474bc417cd475ff41775c8eac83944c1e21271f48e","after_hash":"b24939f2f2dfbf686fbd50c8efdf6eca16cdb0dac19da6d03632142d753ad2eb","item_hash_version":2,"message":"Add scoped estimate required by tracker governance"} +{"hash_algorithm":"sha256","ts":"2026-09-18T03:12:08.646Z","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":"claim","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-09-18T03:12:08.646Z"},{"op":"add","path":"/metadata/assignee","value":"pi-agent"},{"op":"add","path":"/metadata/claim_principal","value":"pi-agent"}],"before_hash":"b24939f2f2dfbf686fbd50c8efdf6eca16cdb0dac19da6d03632142d753ad2eb","after_hash":"9fa1559c631de6df0cc8f85386c5d864dccd1561211bbd96a54d794650a5818b","item_hash_version":3,"event_class":"substantive","record_hash_version":1,"record_hash":"bc1af475e89b46e47e7e596d8aecd5d70738d0bfbad227acfab5027d9cd7dab5"} +{"hash_algorithm":"sha256","ts":"2026-09-18T03:12:08.707Z","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":"update","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-09-18T03:12:08.707Z"},{"op":"replace","path":"/metadata/status","value":"in_progress"}],"before_hash":"9fa1559c631de6df0cc8f85386c5d864dccd1561211bbd96a54d794650a5818b","after_hash":"8e587f99478ee25fd6964e81c4bd3dc2935202b58763d0f58449e90c7e7d664d","item_hash_version":3,"event_class":"substantive","record_hash_version":1,"record_hash":"ab52c2dd60aeaca4672c659d5b10df7654dc783305ce31ea412f5ab9eb46448d"} +{"hash_algorithm":"sha256","ts":"2026-09-18T03:12:09.394Z","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":"replace","path":"/metadata/updated_at","value":"2026-09-18T03:12:09.394Z"},{"op":"add","path":"/metadata/comments","value":[{"created_at":"2026-09-18T03:12:09.394Z","author":"pi-agent","text":"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."}]}],"before_hash":"8e587f99478ee25fd6964e81c4bd3dc2935202b58763d0f58449e90c7e7d664d","after_hash":"df133dd397e8fc2679714f6eaa9f8129c1a041b58162b7744292a2e6070eca8c","item_hash_version":3,"event_class":"substantive","record_hash_version":1,"record_hash":"2df5af911e47132b24b0a7b6e2ee4427c5a3933fe80dae0ed858435941867b52"} +{"hash_algorithm":"sha256","ts":"2026-09-18T03:12:10.032Z","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/1","value":{"created_at":"2026-09-18T03:12:10.032Z","author":"pi-agent","text":"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."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-09-18T03:12:10.032Z"}],"before_hash":"df133dd397e8fc2679714f6eaa9f8129c1a041b58162b7744292a2e6070eca8c","after_hash":"a3b3c9cbf38452b55549c82f98c735ed6e0bec0345a3025ee908c233f13aa5fa","item_hash_version":3,"event_class":"substantive","record_hash_version":1,"record_hash":"280b8f6a335c268ec704db2604d71e25c1f2c54ba4e4d63e08cba8b8a65c2de4"} +{"hash_algorithm":"sha256","ts":"2026-09-18T03:28:07.103Z","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/2","value":{"created_at":"2026-09-18T03:28:07.103Z","author":"pi-agent","text":"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."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-09-18T03:28:07.103Z"}],"before_hash":"a3b3c9cbf38452b55549c82f98c735ed6e0bec0345a3025ee908c233f13aa5fa","after_hash":"eeec72c58203cbd28250b273e0e06a426e44b3e045698bb052c43276914f8f69","item_hash_version":3,"event_class":"substantive","record_hash_version":1,"record_hash":"5d57800e1bf6ce5c5c397d38cd1dd9af7fe318dddbbbd43394bf83be402daace"} +{"hash_algorithm":"sha256","ts":"2026-09-18T03:28:07.561Z","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/3","value":{"created_at":"2026-09-18T03:28:07.561Z","author":"pi-agent","text":"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."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-09-18T03:28:07.561Z"}],"before_hash":"eeec72c58203cbd28250b273e0e06a426e44b3e045698bb052c43276914f8f69","after_hash":"ffbd0411192635adc81f7992d76a29272286fafadc4dd2d5f514c89e808c9c9e","item_hash_version":3,"event_class":"substantive","record_hash_version":1,"record_hash":"e5c9c258f7b8b9fb017f1dabec4e34460a368b834f5da1f230ded46f44bb4ea5"} +{"hash_algorithm":"sha256","ts":"2026-09-18T03:28:08.122Z","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":"update","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-09-18T03:28:08.122Z"},{"op":"add","path":"/metadata/files","value":[{"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"}]},{"op":"add","path":"/metadata/tests","value":[{"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"}}]}],"before_hash":"ffbd0411192635adc81f7992d76a29272286fafadc4dd2d5f514c89e808c9c9e","after_hash":"69b547ef6d779967bb443cb817aa52755008b7798205181fe9764601ce2257df","item_hash_version":3,"event_class":"maintenance","record_hash_version":1,"record_hash":"bec6a619b08b72d72d900296ed841541856747aedf10ab450f4e212bcd7c14db"} diff --git a/.agents/pm/tasks/pm-web-ulgy.toon b/.agents/pm/tasks/pm-web-ulgy.toon index 899eb858..de1a26dd 100644 --- a/.agents/pm/tasks/pm-web-ulgy.toon +++ b/.agents/pm/tasks/pm-web-ulgy.toon @@ -2,15 +2,27 @@ id: pm-web-ulgy title: Cover the routes/pm.ts command surface and the index/server bootstrap in pm-web description: "Stage 2 of pm-web-9ulj, tracked separately because routes/pm.ts is 2704 lines at 7.08% function coverage and is far larger than the rest of the route layer combined. It proxies the pm CLI for every item operation, and memory of this codebase records two traps that only real-workspace tests can catch: runPm serves most actions in-process via the SDK, so unmapped positionals are silently dropped unless added to POSITIONAL_KEYS and a fake-pm-binary harness cannot observe it at all; and the --folder parameter honours '..', which without the isSafeTypeFolder guard writes items outside the pm root and is cross-tenant on the hosted deployment. Also covers src/index.ts (42.83% lines, 26.32% functions) and src/server.ts (42.31%, 0% functions). Depends on the stage 1 harness landing first." type: Task -status: open +status: in_progress priority: 2 tags: [] created_at: "2026-07-29T17:26:40.207Z" -updated_at: "2026-08-21T11:42:35.828Z" +updated_at: "2026-09-18T03:28:08.122Z" +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[4]{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." 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 body: "" diff --git a/package.json b/package.json index 61a5d900..fa40077c 100644 --- a/package.json +++ b/package.json @@ -132,9 +132,9 @@ "test/*.test.ts" ], "thresholds": { - "lines": 66, + "lines": 76, "branches": 80, - "functions": 60 + "functions": 73 }, "ignore": [] } diff --git a/test/pm-collaboration.test.ts b/test/pm-collaboration.test.ts new file mode 100644 index 00000000..938c2958 --- /dev/null +++ b/test/pm-collaboration.test.ts @@ -0,0 +1,436 @@ +/** + * End-to-end collaboration coverage for pm-web's shared pm command surface. + * + * Every request crosses a real loopback HTTP server, real PostgreSQL access + * checks, and a real temporary pm workspace. The scenarios pin the service's + * central guarantees: one collaborator sees another's mutation over SSE, and + * concurrent edits to one item are serialized without losing either change. + */ + +import assert from "node:assert/strict"; +import { execFileSync } from "node:child_process"; +import { mkdir, mkdtemp, readFile, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import test from "node:test"; + +import { + authHeaders, + authedFetch, + ensureSchema, + seedProject, + seedUser, + seedUserShare, + startApp, + uniqueEmail, + uniqueSlug, + type AppServer, + type SeedUser, +} from "./helpers/pg-harness.ts"; + +interface CollaborationHarness { + readonly root: string; + readonly pmRoot: string; + readonly owner: SeedUser; + readonly editor: SeedUser; + readonly projectId: string; +} + +/** Provision a database project and its matching real pm workspace. */ +async function createCollaborationHarness(): Promise { + await ensureSchema(); + const root = await mkdtemp(path.join(tmpdir(), "pm-web-")); + const owner = await seedUser(uniqueEmail("collab-owner"), { displayName: "Owner" }); + const editor = await seedUser(uniqueEmail("collab-editor"), { displayName: "Editor" }); + const project = await seedProject(owner.id, uniqueSlug("collab"), { prefix: "co" }); + await seedUserShare(project.id, editor.id, "edit"); + const pmRoot = path.join(root, owner.id, project.slug, ".agents", "pm"); + await mkdir(path.dirname(pmRoot), { recursive: true }); + execFileSync("pm", ["init", "--pm-path", pmRoot], { stdio: "ignore" }); + process.env.PROJECTS_ROOT = root; + return { root, pmRoot, owner, editor, projectId: project.id }; +} + +/** Decode a successful create response and require the real item id. */ +async function createdItemId(response: Response): Promise { + assert.equal(response.status, 201, await response.clone().text()); + const payload = await response.json() as { id?: string; item?: { id?: string } }; + const id = payload.item?.id ?? payload.id; + assert.ok(id, `create response did not contain an item id: ${JSON.stringify(payload)}`); + return id; +} + +/** Read an SSE response until an event name appears, with a bounded timeout. */ +async function waitForEvent(response: Response, eventName: string): Promise { + assert.equal(response.status, 200); + assert.ok(response.body, "SSE response must expose a readable body"); + const reader = response.body.getReader(); + const decoder = new TextDecoder(); + const timeout = setTimeout(() => void reader.cancel("SSE assertion timed out"), 5_000); + let received = ""; + try { + while (!received.includes(`event: ${eventName}`)) { + const chunk = await reader.read(); + assert.equal(chunk.done, false, `SSE closed before ${eventName}: ${received}`); + received += decoder.decode(chunk.value, { stream: true }); + } + return received; + } finally { + clearTimeout(timeout); + await reader.cancel(); + } +} + +/** Create an item through the production HTTP route. */ +async function createItem(server: AppServer, user: SeedUser, projectId: string, title: string): Promise { + const response = await authedFetch(server, user, `/api/projects/${projectId}/pm/create`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ title, type: "Task", description: "Collaboration fixture" }), + }); + return createdItemId(response); +} + +test("client B receives client A's item mutation over the real SSE route", async (t) => { + const previousRoot = process.env.PROJECTS_ROOT; + const harness = await createCollaborationHarness(); + const server = await startApp(); + const controller = new AbortController(); + t.after(async () => { + controller.abort(); + await server.close(); + await rm(harness.root, { recursive: true, force: true }); + if (previousRoot === undefined) delete process.env.PROJECTS_ROOT; + else process.env.PROJECTS_ROOT = previousRoot; + }); + const stream = await fetch(server.url(`/api/projects/${harness.projectId}/pm/events?view=items`), { + headers: authHeaders(harness.editor), + signal: controller.signal, + }); + const event = waitForEvent(stream, "item-created"); + + const itemId = await createItem(server, harness.owner, harness.projectId, "SSE shared mutation"); + const received = await event; + controller.abort(); + assert.match(received, new RegExp(itemId.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))); + assert.match(received, new RegExp(harness.owner.id)); +}); + +test("concurrent collaborators update one item without a lost write or malformed history", async (t) => { + const previousRoot = process.env.PROJECTS_ROOT; + const harness = await createCollaborationHarness(); + const server = await startApp(); + t.after(async () => { + await server.close(); + await rm(harness.root, { recursive: true, force: true }); + if (previousRoot === undefined) delete process.env.PROJECTS_ROOT; + else process.env.PROJECTS_ROOT = previousRoot; + }); + + const itemId = await createItem(server, harness.owner, harness.projectId, "Concurrent original"); + const [titleUpdate, descriptionUpdate] = await Promise.all([ + authedFetch(server, harness.owner, `/api/projects/${harness.projectId}/pm/update/${itemId}`, { + method: "PATCH", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ title: "Owner title", message: "owner concurrent edit" }), + }), + authedFetch(server, harness.editor, `/api/projects/${harness.projectId}/pm/update/${itemId}`, { + method: "PATCH", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ description: "Editor description", message: "editor concurrent edit" }), + }), + ]); + assert.equal(titleUpdate.status, 200, await titleUpdate.clone().text()); + assert.equal(descriptionUpdate.status, 200, await descriptionUpdate.clone().text()); + + const current = await authedFetch(server, harness.editor, `/api/projects/${harness.projectId}/pm/get/${itemId}`); + assert.equal(current.status, 200); + const payload = await current.json() as { + item?: { title?: string; description?: string }; + title?: string; + description?: string; + }; + const item = payload.item ?? payload; + assert.equal(item.title, "Owner title"); + assert.equal(item.description, "Editor description"); + + const historyText = await readFile(path.join(harness.pmRoot, "history", `${itemId}.jsonl`), "utf8"); + const history = historyText.trim().split("\n").map((line) => JSON.parse(line) as Record); + assert.ok(history.length >= 3, `create plus both updates must be recorded: ${historyText}`); + assert.ok(history.every((entry) => typeof entry === "object" && entry !== null)); + assert.match(historyText, /owner concurrent edit/); + assert.match(historyText, /editor concurrent edit/); +}); + +test("the real HTTP command surface preserves an item's lifecycle and related records", async (t) => { + const previousRoot = process.env.PROJECTS_ROOT; + const harness = await createCollaborationHarness(); + const server = await startApp(); + t.after(async () => { + await server.close(); + await rm(harness.root, { recursive: true, force: true }); + if (previousRoot === undefined) delete process.env.PROJECTS_ROOT; + else process.env.PROJECTS_ROOT = previousRoot; + }); + + const base = `/api/projects/${harness.projectId}/pm`; + const schema = await authedFetch(server, harness.owner, `${base}/schema`); + assert.equal(schema.status, 200); + const schemaBody = await schema.json() as { types?: unknown[]; statuses?: unknown[] }; + assert.ok(Array.isArray(schemaBody.types)); + assert.ok(Array.isArray(schemaBody.statuses)); + + const missingTitle = await authedFetch(server, harness.owner, `${base}/create`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ description: "no title" }), + }); + assert.equal(missingTitle.status, 400); + + const parentId = await createItem(server, harness.owner, harness.projectId, "Lifecycle parent"); + const childId = await createItem(server, harness.owner, harness.projectId, "Lifecycle child"); + const richCreate = await authedFetch(server, harness.owner, `${base}/create`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + title: "Rich lifecycle issue", + type: "Issue", + priority: "1", + description: "Every supported create field", + tags: "coverage,route", + parent: parentId, + deadline: "2026-12-31", + assignee: "route-owner", + sprint: "sprint-1", + release: "release-1", + estimate: "30", + body: "Detailed issue body", + acceptanceCriteria: "All fields persist", + reporter: "route-reporter", + component: "http", + severity: "medium", + risk: "medium", + goal: "goal-1", + objective: "objective-1", + environment: "test", + "blocked-by": parentId, + "blocked-reason": "fixture dependency", + "repro-steps": "Run the route test", + "expected-result": "Fields persist", + "actual-result": "Pending assertion", + reviewer: "route-reviewer", + confidence: "high", + "why-now": "Coverage mandate", + value: "Regression protection", + impact: "Safer collaboration", + outcome: "Observable lifecycle", + "definition-of-ready": "Fixture prepared", + }), + }); + const richId = await createdItemId(richCreate); + const richGet = await authedFetch(server, harness.owner, `${base}/get/${richId}`); + assert.equal(richGet.status, 200); + assert.match(await richGet.text(), /Every supported create field/); + + const invalidCursor = await authedFetch(server, harness.owner, `${base}/list?after=not%2Ba%2Bcursor`); + assert.equal(invalidCursor.status, 400); + assert.deepEqual(await invalidCursor.json(), { + error: "Pagination cursor must be a valid base64url token.", + items: [], + }); + const oversizedCursor = await authedFetch( + server, + harness.owner, + `${base}/list-all?after=${"x".repeat(2_049)}`, + ); + assert.equal(oversizedCursor.status, 400); + + const listed = await authedFetch( + server, + harness.owner, + `${base}/list?status=open&type=Task&limit=10&priority=2&sprint=s1&release=r1&assignee=nobody`, + ); + assert.equal(listed.status, 200); + const all = await authedFetch(server, harness.owner, `${base}/list-all?type=Task&limit=1`); + assert.equal(all.status, 200); + const searched = await authedFetch(server, harness.owner, `${base}/search?q=Lifecycle`); + assert.equal(searched.status, 200); + assert.equal((await searched.json() as { count?: number }).count, 3); + + const updated = await authedFetch(server, harness.editor, `${base}/update/${childId}`, { + method: "PATCH", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + title: "Lifecycle updated", + description: "Updated through HTTP", + priority: "1", + tags: "coverage,collaboration", + body: "Lifecycle body", + author: "route-test", + message: "exercise update flags", + }), + }); + assert.equal(updated.status, 200, await updated.clone().text()); + + const fetched = await authedFetch(server, harness.owner, `${base}/get/${childId}`); + assert.equal(fetched.status, 200); + assert.match(await fetched.text(), /Lifecycle updated/); + + const comment = await authedFetch(server, harness.owner, `${base}/comments/${childId}`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ text: "Observable comment" }), + }); + assert.equal(comment.status, 201, await comment.clone().text()); + const comments = await authedFetch(server, harness.editor, `${base}/comments/${childId}`); + assert.equal(comments.status, 200); + assert.match(await comments.text(), /Observable comment/); + + const note = await authedFetch(server, harness.owner, `${base}/notes/${childId}`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ text: "Observable note" }), + }); + assert.equal(note.status, 201, await note.clone().text()); + const notes = await authedFetch(server, harness.editor, `${base}/notes/${childId}`); + assert.equal(notes.status, 200); + assert.match(await notes.text(), /Observable note/); + + const append = await authedFetch(server, harness.owner, `${base}/append/${childId}`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ text: "Appended body text" }), + }); + assert.equal(append.status, 200, await append.clone().text()); + const history = await authedFetch(server, harness.editor, `${base}/history/${childId}`); + assert.equal(history.status, 200); + assert.match(await history.text(), /exercise update flags/); + + const addDependency = await authedFetch(server, harness.owner, `${base}/deps/${childId}`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ targetId: parentId, rel: "depends-on" }), + }); + assert.equal(addDependency.status, 201, await addDependency.clone().text()); + const dependencies = await authedFetch(server, harness.editor, `${base}/deps/${childId}`); + assert.equal(dependencies.status, 200); + assert.match(await dependencies.text(), new RegExp(parentId)); + const removeDependency = await authedFetch(server, harness.owner, `${base}/deps/${childId}`, { + method: "DELETE", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ targetId: parentId, rel: "depends-on" }), + }); + assert.equal(removeDependency.status, 200, await removeDependency.clone().text()); + + const addRelation = await authedFetch(server, harness.owner, `${base}/rel`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ from: childId, to: parentId, type: "related-to" }), + }); + assert.equal(addRelation.status, 201, await addRelation.clone().text()); + const removeRelation = await authedFetch(server, harness.owner, `${base}/rel`, { + method: "DELETE", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ from: childId, to: parentId, type: "related-to" }), + }); + assert.equal(removeRelation.status, 200, await removeRelation.clone().text()); + + for (const route of ["context?depth=brief", "activity?limit=5", "stats", "aggregate", "calendar", "health"]) { + const response = await authedFetch(server, harness.editor, `${base}/${route}`); + assert.equal(response.status, 200, `${route}: ${await response.clone().text()}`); + } + + const missingReason = await authedFetch(server, harness.owner, `${base}/close/${childId}`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({}), + }); + assert.equal(missingReason.status, 400); + const closed = await authedFetch(server, harness.owner, `${base}/close/${childId}`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ reason: "Lifecycle complete" }), + }); + assert.equal(closed.status, 200, await closed.clone().text()); + const deleted = await authedFetch(server, harness.owner, `${base}/delete/${parentId}`, { method: "DELETE" }); + assert.equal(deleted.status, 200, await deleted.clone().text()); + + const absentBase = "/api/projects/00000000-0000-4000-8000-000000000000/pm"; + const absentRoutes: ReadonlyArray<{ method: string; path: string; body?: Record }> = [ + { method: "GET", path: "schema" }, + { method: "GET", path: "list" }, + { method: "GET", path: "list-all" }, + { method: "GET", path: "board" }, + { method: "GET", path: "search?q=x" }, + { method: "POST", path: "create", body: { title: "x" } }, + { method: "GET", path: `get/${childId}` }, + { method: "PATCH", path: `update/${childId}`, body: { title: "x" } }, + { method: "POST", path: `close/${childId}`, body: { reason: "x" } }, + { method: "DELETE", path: `delete/${childId}` }, + { method: "POST", path: `comments/${childId}`, body: { text: "x" } }, + { method: "GET", path: `comments/${childId}` }, + { method: "POST", path: `notes/${childId}`, body: { text: "x" } }, + { method: "GET", path: `notes/${childId}` }, + { method: "GET", path: "context" }, + { method: "GET", path: "activity" }, + { method: "GET", path: "stats" }, + { method: "GET", path: "aggregate" }, + { method: "GET", path: "calendar" }, + { method: "GET", path: "calendar.ics" }, + { method: "GET", path: "health" }, + { method: "POST", path: `append/${childId}`, body: { text: "x" } }, + { method: "GET", path: `history/${childId}` }, + { method: "GET", path: `deps/${childId}` }, + { method: "POST", path: `deps/${childId}`, body: { targetId: parentId } }, + { method: "DELETE", path: `deps/${childId}`, body: { targetId: parentId } }, + { method: "POST", path: "rel", body: { from: childId, to: parentId } }, + { method: "DELETE", path: "rel", body: { from: childId, to: parentId } }, + { method: "GET", path: "presence" }, + ]; + for (const route of absentRoutes) { + const response = await authedFetch(server, harness.owner, `${absentBase}/${route.path}`, { + method: route.method, + headers: route.body ? { "content-type": "application/json" } : undefined, + body: route.body ? JSON.stringify(route.body) : undefined, + }); + assert.equal(response.status, 404, `${route.method} ${route.path}: ${await response.clone().text()}`); + } + + const presence = await authedFetch(server, harness.owner, `${base}/presence`); + assert.equal(presence.status, 200); + assert.deepEqual(await presence.json(), { users: [] }); + + const missingItem = "co-does-not-exist"; + const failedMutations: ReadonlyArray<{ method: string; path: string; body?: Record }> = [ + { method: "PATCH", path: `update/${missingItem}`, body: { title: "never written" } }, + { method: "POST", path: `close/${missingItem}`, body: { reason: "not found" } }, + { method: "DELETE", path: `delete/${missingItem}` }, + { method: "POST", path: `comments/${missingItem}`, body: { text: "not written" } }, + { method: "POST", path: `notes/${missingItem}`, body: { text: "not written" } }, + { method: "POST", path: `append/${missingItem}`, body: { text: "not written" } }, + { method: "POST", path: `deps/${missingItem}`, body: { targetId: parentId } }, + ]; + for (const mutation of failedMutations) { + const response = await authedFetch(server, harness.owner, `${base}/${mutation.path}`, { + method: mutation.method, + headers: mutation.body ? { "content-type": "application/json" } : undefined, + body: mutation.body ? JSON.stringify(mutation.body) : undefined, + }); + assert.ok(response.status >= 400, `${mutation.method} ${mutation.path} must fail for a missing item`); + } + + for (const invalid of [ + { path: `comments/${childId}`, body: { text: "" } }, + { path: `notes/${childId}`, body: { text: "" } }, + { path: `append/${childId}`, body: { text: "" } }, + { path: `deps/${childId}`, body: {} }, + { path: "rel", body: { from: childId } }, + ]) { + const response = await authedFetch(server, harness.owner, `${base}/${invalid.path}`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify(invalid.body), + }); + assert.equal(response.status, 400, `${invalid.path}: ${await response.clone().text()}`); + } +}); From 27afad702d861112eb7a9f76f3f5ceb8f288a1c3 Mon Sep 17 00:00:00 2001 From: SteveBot <1153461+unbraind@users.noreply.github.com> Date: Fri, 18 Sep 2026 05:35:19 +0200 Subject: [PATCH 2/5] test: stabilize pm route branch coverage --- .agents/pm/history/pm-web-ulgy.jsonl | 1 + .agents/pm/tasks/pm-web-ulgy.toon | 5 +++-- test/pm-collaboration.test.ts | 26 +++++++++++++++++++++++++- 3 files changed, 29 insertions(+), 3 deletions(-) diff --git a/.agents/pm/history/pm-web-ulgy.jsonl b/.agents/pm/history/pm-web-ulgy.jsonl index f020bb8a..5faef89f 100644 --- a/.agents/pm/history/pm-web-ulgy.jsonl +++ b/.agents/pm/history/pm-web-ulgy.jsonl @@ -8,3 +8,4 @@ {"hash_algorithm":"sha256","ts":"2026-09-18T03:28:07.103Z","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/2","value":{"created_at":"2026-09-18T03:28:07.103Z","author":"pi-agent","text":"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."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-09-18T03:28:07.103Z"}],"before_hash":"a3b3c9cbf38452b55549c82f98c735ed6e0bec0345a3025ee908c233f13aa5fa","after_hash":"eeec72c58203cbd28250b273e0e06a426e44b3e045698bb052c43276914f8f69","item_hash_version":3,"event_class":"substantive","record_hash_version":1,"record_hash":"5d57800e1bf6ce5c5c397d38cd1dd9af7fe318dddbbbd43394bf83be402daace"} {"hash_algorithm":"sha256","ts":"2026-09-18T03:28:07.561Z","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/3","value":{"created_at":"2026-09-18T03:28:07.561Z","author":"pi-agent","text":"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."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-09-18T03:28:07.561Z"}],"before_hash":"eeec72c58203cbd28250b273e0e06a426e44b3e045698bb052c43276914f8f69","after_hash":"ffbd0411192635adc81f7992d76a29272286fafadc4dd2d5f514c89e808c9c9e","item_hash_version":3,"event_class":"substantive","record_hash_version":1,"record_hash":"e5c9c258f7b8b9fb017f1dabec4e34460a368b834f5da1f230ded46f44bb4ea5"} {"hash_algorithm":"sha256","ts":"2026-09-18T03:28:08.122Z","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":"update","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-09-18T03:28:08.122Z"},{"op":"add","path":"/metadata/files","value":[{"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"}]},{"op":"add","path":"/metadata/tests","value":[{"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"}}]}],"before_hash":"ffbd0411192635adc81f7992d76a29272286fafadc4dd2d5f514c89e808c9c9e","after_hash":"69b547ef6d779967bb443cb817aa52755008b7798205181fe9764601ce2257df","item_hash_version":3,"event_class":"maintenance","record_hash_version":1,"record_hash":"bec6a619b08b72d72d900296ed841541856747aedf10ab450f4e212bcd7c14db"} +{"hash_algorithm":"sha256","ts":"2026-09-18T03:35:19.045Z","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/4","value":{"created_at":"2026-09-18T03:35:19.045Z","author":"pi-agent","text":"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."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-09-18T03:35:19.045Z"}],"before_hash":"69b547ef6d779967bb443cb817aa52755008b7798205181fe9764601ce2257df","after_hash":"71ef9c1b5c62341f81e420c85871de0baf2106c1430a18899c1dc4fbcaad00bd","item_hash_version":3,"event_class":"substantive","record_hash_version":1,"record_hash":"6b69cb0a84b64dc01a2b46fb7f042533dc6e788a629467875ffa34fac7c677fc"} diff --git a/.agents/pm/tasks/pm-web-ulgy.toon b/.agents/pm/tasks/pm-web-ulgy.toon index de1a26dd..0cd8324c 100644 --- a/.agents/pm/tasks/pm-web-ulgy.toon +++ b/.agents/pm/tasks/pm-web-ulgy.toon @@ -6,18 +6,19 @@ status: in_progress priority: 2 tags: [] created_at: "2026-07-29T17:26:40.207Z" -updated_at: "2026-09-18T03:28:08.122Z" +updated_at: "2026-09-18T03:35:19.045Z" 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[4]{created_at,author,text}: +comments[5]{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." 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}: diff --git a/test/pm-collaboration.test.ts b/test/pm-collaboration.test.ts index 938c2958..4eb585fd 100644 --- a/test/pm-collaboration.test.ts +++ b/test/pm-collaboration.test.ts @@ -251,8 +251,21 @@ test("the real HTTP command surface preserves an item's lifecycle and related re `${base}/list?status=open&type=Task&limit=10&priority=2&sprint=s1&release=r1&assignee=nobody`, ); assert.equal(listed.status, 200); + const unfiltered = await authedFetch(server, harness.owner, `${base}/list`); + assert.equal(unfiltered.status, 200); const all = await authedFetch(server, harness.owner, `${base}/list-all?type=Task&limit=1`); assert.equal(all.status, 200); + const allBody = await all.json() as { next_cursor?: string }; + if (allBody.next_cursor) { + const nextPage = await authedFetch( + server, + harness.owner, + `${base}/list-all?type=Task&limit=1&after=${encodeURIComponent(allBody.next_cursor)}`, + ); + assert.equal(nextPage.status, 200, `${allBody.next_cursor}: ${await nextPage.clone().text()}`); + } + const unfilteredAll = await authedFetch(server, harness.owner, `${base}/list-all`); + assert.equal(unfilteredAll.status, 200); const searched = await authedFetch(server, harness.owner, `${base}/search?q=Lifecycle`); assert.equal(searched.status, 200); assert.equal((await searched.json() as { count?: number }).count, 3); @@ -275,6 +288,8 @@ test("the real HTTP command surface preserves an item's lifecycle and related re const fetched = await authedFetch(server, harness.owner, `${base}/get/${childId}`); assert.equal(fetched.status, 200); assert.match(await fetched.text(), /Lifecycle updated/); + const missingGet = await authedFetch(server, harness.owner, `${base}/get/co-does-not-exist`); + assert.equal(missingGet.status, 404); const comment = await authedFetch(server, harness.owner, `${base}/comments/${childId}`, { method: "POST", @@ -335,7 +350,16 @@ test("the real HTTP command surface preserves an item's lifecycle and related re }); assert.equal(removeRelation.status, 200, await removeRelation.clone().text()); - for (const route of ["context?depth=brief", "activity?limit=5", "stats", "aggregate", "calendar", "health"]) { + for (const route of [ + "context?depth=brief", + "context?depth=unsupported", + "activity?limit=5", + "activity", + "stats", + "aggregate", + "calendar", + "health", + ]) { const response = await authedFetch(server, harness.editor, `${base}/${route}`); assert.equal(response.status, 200, `${route}: ${await response.clone().text()}`); } From e11b2595594faf7365e277d5fa5773897a2e8023 Mon Sep 17 00:00:00 2001 From: SteveBot <1153461+unbraind@users.noreply.github.com> Date: Fri, 18 Sep 2026 05:39:43 +0200 Subject: [PATCH 3/5] docs: close pm route coverage tranche --- .agents/pm/history/pm-web-ulgy.jsonl | 1 + .agents/pm/tasks/pm-web-ulgy.toon | 11 ++++++++--- CHANGELOG.md | 1 + 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/.agents/pm/history/pm-web-ulgy.jsonl b/.agents/pm/history/pm-web-ulgy.jsonl index 5faef89f..e9338263 100644 --- a/.agents/pm/history/pm-web-ulgy.jsonl +++ b/.agents/pm/history/pm-web-ulgy.jsonl @@ -9,3 +9,4 @@ {"hash_algorithm":"sha256","ts":"2026-09-18T03:28:07.561Z","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/3","value":{"created_at":"2026-09-18T03:28:07.561Z","author":"pi-agent","text":"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."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-09-18T03:28:07.561Z"}],"before_hash":"eeec72c58203cbd28250b273e0e06a426e44b3e045698bb052c43276914f8f69","after_hash":"ffbd0411192635adc81f7992d76a29272286fafadc4dd2d5f514c89e808c9c9e","item_hash_version":3,"event_class":"substantive","record_hash_version":1,"record_hash":"e5c9c258f7b8b9fb017f1dabec4e34460a368b834f5da1f230ded46f44bb4ea5"} {"hash_algorithm":"sha256","ts":"2026-09-18T03:28:08.122Z","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":"update","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-09-18T03:28:08.122Z"},{"op":"add","path":"/metadata/files","value":[{"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"}]},{"op":"add","path":"/metadata/tests","value":[{"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"}}]}],"before_hash":"ffbd0411192635adc81f7992d76a29272286fafadc4dd2d5f514c89e808c9c9e","after_hash":"69b547ef6d779967bb443cb817aa52755008b7798205181fe9764601ce2257df","item_hash_version":3,"event_class":"maintenance","record_hash_version":1,"record_hash":"bec6a619b08b72d72d900296ed841541856747aedf10ab450f4e212bcd7c14db"} {"hash_algorithm":"sha256","ts":"2026-09-18T03:35:19.045Z","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/4","value":{"created_at":"2026-09-18T03:35:19.045Z","author":"pi-agent","text":"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."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-09-18T03:35:19.045Z"}],"before_hash":"69b547ef6d779967bb443cb817aa52755008b7798205181fe9764601ce2257df","after_hash":"71ef9c1b5c62341f81e420c85871de0baf2106c1430a18899c1dc4fbcaad00bd","item_hash_version":3,"event_class":"substantive","record_hash_version":1,"record_hash":"6b69cb0a84b64dc01a2b46fb7f042533dc6e788a629467875ffa34fac7c677fc"} +{"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"} diff --git a/.agents/pm/tasks/pm-web-ulgy.toon b/.agents/pm/tasks/pm-web-ulgy.toon index 0cd8324c..4f15e068 100644 --- a/.agents/pm/tasks/pm-web-ulgy.toon +++ b/.agents/pm/tasks/pm-web-ulgy.toon @@ -2,17 +2,21 @@ id: pm-web-ulgy title: Cover the routes/pm.ts command surface and the index/server bootstrap in pm-web description: "Stage 2 of pm-web-9ulj, tracked separately because routes/pm.ts is 2704 lines at 7.08% function coverage and is far larger than the rest of the route layer combined. It proxies the pm CLI for every item operation, and memory of this codebase records two traps that only real-workspace tests can catch: runPm serves most actions in-process via the SDK, so unmapped positionals are silently dropped unless added to POSITIONAL_KEYS and a fake-pm-binary harness cannot observe it at all; and the --folder parameter honours '..', which without the isSafeTypeFolder guard writes items outside the pm root and is cross-tenant on the hosted deployment. Also covers src/index.ts (42.83% lines, 26.32% functions) and src/server.ts (42.31%, 0% functions). Depends on the stage 1 harness landing first." type: Task -status: in_progress +status: closed priority: 2 tags: [] created_at: "2026-07-29T17:26:40.207Z" -updated_at: "2026-09-18T03:35:19.045Z" -assignee: pi-agent +updated_at: "2026-09-18T03:39:21.296Z" +closed_at: "2026-09-18T03:39:21.265Z" +completed_at: "2026-09-18T03:39:21.265Z" 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 +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." comments[5]{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." @@ -26,4 +30,5 @@ files[2]{path,scope,note}: 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 +close_reason: Behavioral coverage tranche delivered and all local release gates pass; remaining 100% work is explicitly inventoried for the next tranche. body: "" diff --git a/CHANGELOG.md b/CHANGELOG.md index a9a6c6e0..09fb69f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Other +- Cover the routes/pm.ts command surface and the index/server bootstrap in pm-web ([pm-web-ulgy](https://github.com/unbraind/pm-web/blob/main/.agents/pm/tasks/pm-web-ulgy.toon)) - Certify pm CLI 2026.9.17 ([pm-web-kl90](https://github.com/unbraind/pm-web/blob/main/.agents/pm/chores/pm-web-kl90.toon)) - Review codeql-action v4.38.0 into the pinned-action allowlist so the Dependabot group can merge ([pm-web-mv7l](https://github.com/unbraind/pm-web/blob/main/.agents/pm/chores/pm-web-mv7l.toon)) From 111cb335a1188a908473899f92cfec613b18ff7b Mon Sep 17 00:00:00 2001 From: SteveBot <1153461+unbraind@users.noreply.github.com> Date: Fri, 18 Sep 2026 05:44:57 +0200 Subject: [PATCH 4/5] test: make collaboration setup failure-safe --- .agents/pm/history/pm-web-ulgy.jsonl | 2 ++ .agents/pm/tasks/pm-web-ulgy.toon | 13 ++++--------- CHANGELOG.md | 1 - test/pm-collaboration.test.ts | 20 +++++++++++++------- 4 files changed, 19 insertions(+), 17 deletions(-) diff --git a/.agents/pm/history/pm-web-ulgy.jsonl b/.agents/pm/history/pm-web-ulgy.jsonl index e9338263..93ade75b 100644 --- a/.agents/pm/history/pm-web-ulgy.jsonl +++ b/.agents/pm/history/pm-web-ulgy.jsonl @@ -10,3 +10,5 @@ {"hash_algorithm":"sha256","ts":"2026-09-18T03:28:08.122Z","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":"update","patch":[{"op":"replace","path":"/metadata/updated_at","value":"2026-09-18T03:28:08.122Z"},{"op":"add","path":"/metadata/files","value":[{"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"}]},{"op":"add","path":"/metadata/tests","value":[{"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"}}]}],"before_hash":"ffbd0411192635adc81f7992d76a29272286fafadc4dd2d5f514c89e808c9c9e","after_hash":"69b547ef6d779967bb443cb817aa52755008b7798205181fe9764601ce2257df","item_hash_version":3,"event_class":"maintenance","record_hash_version":1,"record_hash":"bec6a619b08b72d72d900296ed841541856747aedf10ab450f4e212bcd7c14db"} {"hash_algorithm":"sha256","ts":"2026-09-18T03:35:19.045Z","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/4","value":{"created_at":"2026-09-18T03:35:19.045Z","author":"pi-agent","text":"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."}},{"op":"replace","path":"/metadata/updated_at","value":"2026-09-18T03:35:19.045Z"}],"before_hash":"69b547ef6d779967bb443cb817aa52755008b7798205181fe9764601ce2257df","after_hash":"71ef9c1b5c62341f81e420c85871de0baf2106c1430a18899c1dc4fbcaad00bd","item_hash_version":3,"event_class":"substantive","record_hash_version":1,"record_hash":"6b69cb0a84b64dc01a2b46fb7f042533dc6e788a629467875ffa34fac7c677fc"} {"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"} diff --git a/.agents/pm/tasks/pm-web-ulgy.toon b/.agents/pm/tasks/pm-web-ulgy.toon index 4f15e068..1784f467 100644 --- a/.agents/pm/tasks/pm-web-ulgy.toon +++ b/.agents/pm/tasks/pm-web-ulgy.toon @@ -2,27 +2,23 @@ id: pm-web-ulgy title: Cover the routes/pm.ts command surface and the index/server bootstrap in pm-web description: "Stage 2 of pm-web-9ulj, tracked separately because routes/pm.ts is 2704 lines at 7.08% function coverage and is far larger than the rest of the route layer combined. It proxies the pm CLI for every item operation, and memory of this codebase records two traps that only real-workspace tests can catch: runPm serves most actions in-process via the SDK, so unmapped positionals are silently dropped unless added to POSITIONAL_KEYS and a fake-pm-binary harness cannot observe it at all; and the --folder parameter honours '..', which without the isSafeTypeFolder guard writes items outside the pm root and is cross-tenant on the hosted deployment. Also covers src/index.ts (42.83% lines, 26.32% functions) and src/server.ts (42.31%, 0% functions). Depends on the stage 1 harness landing first." type: Task -status: closed +status: in_progress priority: 2 tags: [] created_at: "2026-07-29T17:26:40.207Z" -updated_at: "2026-09-18T03:39:21.296Z" -closed_at: "2026-09-18T03:39:21.265Z" -completed_at: "2026-09-18T03:39:21.265Z" +updated_at: "2026-09-18T03:43:37.813Z" 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 -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." -comments[5]{created_at,author,text}: +comments[6]{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." 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}: @@ -30,5 +26,4 @@ files[2]{path,scope,note}: 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 -close_reason: Behavioral coverage tranche delivered and all local release gates pass; remaining 100% work is explicitly inventoried for the next tranche. body: "" diff --git a/CHANGELOG.md b/CHANGELOG.md index 09fb69f5..a9a6c6e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,6 @@ ### Other -- Cover the routes/pm.ts command surface and the index/server bootstrap in pm-web ([pm-web-ulgy](https://github.com/unbraind/pm-web/blob/main/.agents/pm/tasks/pm-web-ulgy.toon)) - Certify pm CLI 2026.9.17 ([pm-web-kl90](https://github.com/unbraind/pm-web/blob/main/.agents/pm/chores/pm-web-kl90.toon)) - Review codeql-action v4.38.0 into the pinned-action allowlist so the Dependabot group can merge ([pm-web-mv7l](https://github.com/unbraind/pm-web/blob/main/.agents/pm/chores/pm-web-mv7l.toon)) diff --git a/test/pm-collaboration.test.ts b/test/pm-collaboration.test.ts index 4eb585fd..ce105332 100644 --- a/test/pm-collaboration.test.ts +++ b/test/pm-collaboration.test.ts @@ -54,7 +54,7 @@ async function createCollaborationHarness(): Promise { /** Decode a successful create response and require the real item id. */ async function createdItemId(response: Response): Promise { assert.equal(response.status, 201, await response.clone().text()); - const payload = await response.json() as { id?: string; item?: { id?: string } }; + const payload = await response.json() as { id?: string; item?: { id?: string } }; const id = payload.item?.id ?? payload.id; assert.ok(id, `create response did not contain an item id: ${JSON.stringify(payload)}`); return id; @@ -94,15 +94,17 @@ async function createItem(server: AppServer, user: SeedUser, projectId: string, test("client B receives client A's item mutation over the real SSE route", async (t) => { const previousRoot = process.env.PROJECTS_ROOT; const harness = await createCollaborationHarness(); - const server = await startApp(); + let startedServer: AppServer | undefined; const controller = new AbortController(); t.after(async () => { controller.abort(); - await server.close(); + if (startedServer) await startedServer.close(); await rm(harness.root, { recursive: true, force: true }); if (previousRoot === undefined) delete process.env.PROJECTS_ROOT; else process.env.PROJECTS_ROOT = previousRoot; }); + const server = await startApp(); + startedServer = server; const stream = await fetch(server.url(`/api/projects/${harness.projectId}/pm/events?view=items`), { headers: authHeaders(harness.editor), signal: controller.signal, @@ -119,13 +121,15 @@ test("client B receives client A's item mutation over the real SSE route", async test("concurrent collaborators update one item without a lost write or malformed history", async (t) => { const previousRoot = process.env.PROJECTS_ROOT; const harness = await createCollaborationHarness(); - const server = await startApp(); + let startedServer: AppServer | undefined; t.after(async () => { - await server.close(); + if (startedServer) await startedServer.close(); await rm(harness.root, { recursive: true, force: true }); if (previousRoot === undefined) delete process.env.PROJECTS_ROOT; else process.env.PROJECTS_ROOT = previousRoot; }); + const server = await startApp(); + startedServer = server; const itemId = await createItem(server, harness.owner, harness.projectId, "Concurrent original"); const [titleUpdate, descriptionUpdate] = await Promise.all([ @@ -165,13 +169,15 @@ test("concurrent collaborators update one item without a lost write or malformed test("the real HTTP command surface preserves an item's lifecycle and related records", async (t) => { const previousRoot = process.env.PROJECTS_ROOT; const harness = await createCollaborationHarness(); - const server = await startApp(); + let startedServer: AppServer | undefined; t.after(async () => { - await server.close(); + if (startedServer) await startedServer.close(); await rm(harness.root, { recursive: true, force: true }); if (previousRoot === undefined) delete process.env.PROJECTS_ROOT; else process.env.PROJECTS_ROOT = previousRoot; }); + const server = await startApp(); + startedServer = server; const base = `/api/projects/${harness.projectId}/pm`; const schema = await authedFetch(server, harness.owner, `${base}/schema`); From a156db302cf84f4e0c1e6208cb0023dd7ba50c3c Mon Sep 17 00:00:00 2001 From: SteveBot <1153461+unbraind@users.noreply.github.com> Date: Fri, 18 Sep 2026 06:05:01 +0200 Subject: [PATCH 5/5] Make the collaboration tests fail on a missing page and on an unobserved event The list-all pagination check only ran when next_cursor happened to be present, so a response that silently dropped the cursor still passed. With two visible Task items and limit=1 the cursor is mandatory. Assert it, then assert that the second page holds exactly one different item. The SSE test now awaits the event and the create request together, so a failed create cannot leave the stream promise rejecting without a handler during cleanup. --- test/pm-collaboration.test.ts | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/test/pm-collaboration.test.ts b/test/pm-collaboration.test.ts index ce105332..3ca4772c 100644 --- a/test/pm-collaboration.test.ts +++ b/test/pm-collaboration.test.ts @@ -109,10 +109,10 @@ test("client B receives client A's item mutation over the real SSE route", async headers: authHeaders(harness.editor), signal: controller.signal, }); - const event = waitForEvent(stream, "item-created"); - - const itemId = await createItem(server, harness.owner, harness.projectId, "SSE shared mutation"); - const received = await event; + const [received, itemId] = await Promise.all([ + waitForEvent(stream, "item-created"), + createItem(server, harness.owner, harness.projectId, "SSE shared mutation"), + ]); controller.abort(); assert.match(received, new RegExp(itemId.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))); assert.match(received, new RegExp(harness.owner.id)); @@ -261,15 +261,18 @@ test("the real HTTP command surface preserves an item's lifecycle and related re assert.equal(unfiltered.status, 200); const all = await authedFetch(server, harness.owner, `${base}/list-all?type=Task&limit=1`); assert.equal(all.status, 200); - const allBody = await all.json() as { next_cursor?: string }; - if (allBody.next_cursor) { - const nextPage = await authedFetch( - server, - harness.owner, - `${base}/list-all?type=Task&limit=1&after=${encodeURIComponent(allBody.next_cursor)}`, - ); - assert.equal(nextPage.status, 200, `${allBody.next_cursor}: ${await nextPage.clone().text()}`); - } + const allBody = await all.json() as { items?: Array<{ id: string }>; next_cursor?: string }; + assert.equal(allBody.items?.length, 1); + assert.ok(allBody.next_cursor, "two visible Task items with limit=1 must page"); + const nextPage = await authedFetch( + server, + harness.owner, + `${base}/list-all?type=Task&limit=1&after=${encodeURIComponent(allBody.next_cursor)}`, + ); + assert.equal(nextPage.status, 200, `${allBody.next_cursor}: ${await nextPage.clone().text()}`); + const nextBody = await nextPage.json() as { items?: Array<{ id: string }> }; + assert.equal(nextBody.items?.length, 1); + assert.notEqual(nextBody.items?.[0]?.id, allBody.items?.[0]?.id); const unfilteredAll = await authedFetch(server, harness.owner, `${base}/list-all`); assert.equal(unfilteredAll.status, 200); const searched = await authedFetch(server, harness.owner, `${base}/search?q=Lifecycle`);