relay-shutdown-acceptance.json
docs/evidence/relay-shutdown-acceptance.json
1{
2 "format": 1,
3 "recordedAt": "2026-10-25T00:07:53.655419+00:00",
4 "source": "54aebb89391f12c9883d6164b76b27c751734fcd",
5 "previousCi": {
6 "source": "107e9339dc36921a8a0fdbc6bddf0bd63cd8e254",
7 "url": "https://github.com/jasona/darkwind-web/actions/runs/40145062459",
8 "containerJob": 121024514762,
9 "container": "failure",
10 "core": "success",
11 "browserPlayer": "success",
12 "browserMultiplayer": "in_progress at recording",
13 "browserProbe": "in_progress at recording",
14 "failure": "SIGTERM exits cleanly: expected 0, actual 1, tools/cli/docker-smoke.ts:72",
15 "beforeFailure": "Image built; four production-only model imports, license routes, static assets, admission, two-client shared door and duplicate-writer lock assertion completed.",
16 "finding": "Docker smoke left player/worker asset bodies unread. A separate reproduction confirms a stalled large HTTP response can prevent the old close() from completing until the main-process watchdog exits with code 1."
17 },
18 "reproduction": {
19 "before": {
20 "request": "Paused 32 MiB HTTP asset response against createRelay",
21 "observation": "Still waiting after 3004 ms; client was explicitly destroyed for cleanup. This bounded observation is not a measured completion time."
22 },
23 "after": {
24 "request": "Same paused 32 MiB HTTP asset response",
25 "result": "closed",
26 "elapsedMs": 2001.16561
27 },
28 "production": {
29 "code": 0,
30 "signal": null,
31 "elapsedMs": 2012.3418719999997,
32 "mode": "compiled production server; paused 32 MiB asset response; SIGTERM"
33 },
34 "productionCommand": "npm run build:server && node docs/evidence/relay-shutdown-production.mjs",
35 "productionScript": "relay-shutdown-production.mjs"
36 },
37 "changes": [
38 "Fence new HTTP requests, upgrades, WebSocket messages and unfinished admissions when stopping begins.",
39 "Start HTTP close immediately, terminate room sockets and force remaining transport sockets closed after a two-second drain.",
40 "Close is idempotent; interrupted static streams release ownership.",
41 "Docker smoke consumes full player and render-worker downloads."
42 ],
43 "headless": {
44 "command": "npm test",
45 "tests": 246,
46 "pass": 246,
47 "fail": 0,
48 "skipped": 0,
49 "log": "relay-shutdown-headless.tap",
50 "provenance": "Full suite with animation work parked outside the checkout. Final runtime behavior tested; subsequent change only adds a TypeScript error-code cast."
51 },
52 "browser": {
53 "command": "npm run test:browser -- tests/browser/multiplayer.spec.ts",
54 "stats": {
55 "startTime": "2026-10-25T00:06:11.037Z",
56 "duration": 67148.349,
57 "expected": 4,
58 "skipped": 0,
59 "unexpected": 0,
60 "flaky": 0
61 },
62 "report": "relay-shutdown-browser.json",
63 "environment": "Chromium 141 software WebGPU; CI=1; one worker; isolated temporary production client and room storage",
64 "provenance": "Four real-browser multiplayer scenarios, including shared graphics recovery. The prior complete 26-case foliage run is retained in alpha-materials-acceptance.json."
65 },
66 "validation": {
67 "typeAndImports": "passed, 56 package source files",
68 "productionServerBuild": "passed",
69 "diffWhitespace": "passed",
70 "docker": "Current fix awaits remote Docker acceptance; local daemon unavailable."
71 },
72 "limits": [
73 "Two-second drain is a timer deadline; CPU starvation can delay timer delivery.",
74 "Incomplete HTTP downloads are terminated during shutdown and must be retried.",
75 "Does not establish a fix for the earlier graphics-capture stall; the player CI job passed on the foliage revision without a targeted capture fix.",
76 "No merge or deployment to main. Full engine plan remains open."
77 ]
78}
79