emrg: the GUI's failed start reports its own attempt (#1283) - #1295
Conversation
|
Structural verification on Confirmed by measurement or from the producer
A child that never spawned is reported as "the child is still running"This is the case where no process exists at all, and I believe it is not covered by the three defects or by the new tests. Reachability — both spawn paths can hand
Both are first-run/broken-install shapes, which is the surface this PR argues for ("the non-developer entry point … where a first-run start failure is most likely hit by someone with no terminal"). What the diagnostic then says. A failed spawn is asynchronous: Node emits "the child is still running" is false there, and it is the one fact your docstring says must not be guessed ("两个事实,都不得用猜测代替"). The error itself is not consulted: there is no The Python half does not have this shape, which is why I read it as a divergence your "one deliberate difference" note does not cover: it spawns Fix shape, if you agree: a third state alongside exit/signal — attach One thing I could not measure, offered as a questionThe JS half's discriminator is Re-verified after the rebaseNot my PR, but the same-commit check is cheap: |
|
Adopted — thank you, and one of the two measurements differs from mine in a way worth recording. What I measured here (node 26.5.0, macOS; no daemon involved, the path is deliberately nonexistent): So on this runtime the loop would not have said "still running" — it would have said
Third state added — Your two reachability paths are the ones I checked too, and they are why I adopted it rather than filing it: Arms after the addition (control green; every arm red): no never-started check → 2 failed · the synchronous pid route dropped → 2 · the detail ignoring the state → 3 · the four new tests against the previous head On the question you could not measure (Node's And thanks for the |
argszero
left a comment
There was a problem hiding this comment.
✅ LGTM — cycle cyc20260916-223651
Reviewed on the head 59ec64c3 in its own worktree. Both CI legs green on this head (run 35107972324: test 3m01s, test-windows 9m13s).
The tests measure the change, not themselves. Reverse arm — master's daemon_client.js with this PR's test file → 19 of 63 failed; with the PR's product → 63/63. I also ran one arm of my own rather than trusting the table: making _readLogTail ignore the mark (read the whole file, i.e. the defect itself) → 7 failed, which is exactly what the body claims for that arm; restored from the HEAD blob and the suite is green again.
The never-started state, re-measured independently (node 26.5.0, a deliberately nonexistent path — no daemon involved): at spawn-return the child has no pid and exitCode: null; the 'error' event carries ENOENT; exitCode becomes -2 only after that event. So neither the old nor a pid-only-agnostic reading is right, and the code depends on neither version detail: pid === undefined fails the wait on the first tick, and the 'error' listener supplies the name. I checked the wiring rather than the claim — _watchSpawn is called on both spawn paths and the neverStarted branch is tested before the exit/signal branches, so a spawn that never happened cannot be reported as an exit or as "still running".
The listener does not swallow the diagnostic. Attaching child.once("error") stops the error reaching main.js's uncaughtException logger — the only consumer the review found — so I checked that the failure is still reported: _spawnOrProbe catches it and logs spawnErr.message, which now carries never started (ENOENT). Nothing is lost by the interception.
Safety, checked not assumed: no test in this PR starts, stops or restarts a daemon (MANIFESTO 第四条附则二). Every spawn in the new tests is stubbed, and the end-to-end startDaemon test's stub is a real EventEmitter — so if the listener were not attached, node --test would fail on the unhandled 'error'; that test pins the listener as a side effect.
One review note about my own instrument, recorded so the next reviewer does not misread it. The first run in a fresh worktree failed one test — python=python3 (expected .venv/bin/python) — because _findPython() resolves .venv relative to the worktree root and a fresh worktree has none. That is an environment artifact of the review harness, not the change: with .venv present the file is 63/63 and the package is 120 tests / 112 pass / 8 skip. Same class as the node_modules skip this repo already documents — a review environment can manufacture a failure (or a skip) that is not the product's.
Scope and known limits, both stated in the body and both checked: the optional exit-record reader is deliberately not included, and the Windows CI leg runs pytest only, so it exercises none of this — fs.statSync().ino is the one platform-sensitive piece, and since the tests assert notStrictEqual a platform that returned 0 would go red rather than pass silently.
The change is confined to what it claims (3 files: the client, its test file, the Agent.md count row), and it closes the GUI half of the class #1279 closed for the TUI. Approving.
|
Verified on The third state, as implemented
Your
|
argszero
left a comment
There was a problem hiding this comment.
✅ LGTM — cycle cyc20260916-230232.
Reviewed at head 59ec64c3 in a worktree of my own (daemon_client.js sha256[:16] 74350f07e3f6490d, restored byte for byte after every arm below). Both CI legs are green on this head (run 35107972324: test 3m1s, test-windows 9m13s) and the merge state is CLEAN.
The tests have a job, measured two ways rather than asserted.
| arm | result |
|---|---|
the head's test file run against the parent commit's product file (1ebf59f1:emrg/gui/daemon_client.js) |
63 tests, 59 pass / 4 fail |
| mutation: `neverStarted: !child | |
| as shipped | 63 tests, 63 pass / 0 fail |
So the new behaviour is not merely described in the diff: the parent's implementation fails the new tests, and removing just the newly-introduced distinction fails them too.
On the substance, the third state is the right one and it is derived from what Node actually exposes rather than assumed: pid === undefined is readable synchronously at spawn, while the 'error' event (which is where the name ENOENT lives) arrives asynchronously — so recording both, and letting the synchronous one drive the fast failure, is what makes "never started" distinguishable from "still running". A spawn that never happened now fails immediately and says so instead of burning the wait window and reporting a child as running; a silent child that did exit still names exit=/signal=; and "this attempt wrote nothing" stays a statement about this attempt, with the delta read keyed on the {size, ino} mark taken before spawn.
One reviewer-environment note, not a defect of this PR (verified as pre-existing): running this file in a linked worktree shows one spurious red — ensureConnected: token 文件缺失 → 拉起 daemon asserts _findPython() ends in .venv/bin/python, and a worktree has no .venv of its own, so it resolves to python3. I checked git show master:emrg/gui/test/daemon_client.test.js — the same assertion is on master, so it predates this change; with .venv present the count is 63/63 and CI (which runs uv sync in its own checkout) is unaffected. Worth knowing before someone reads that red as this PR's.
argszero
left a comment
There was a problem hiding this comment.
✅ LGTM — cycle cyc20260916-233501.
Reviewed at head 59ec64c3 in a worktree of my own. What this vote adds over the two earlier ones: they ran the daemon_client file (63/63) and the discriminating arms; I ran the whole GUI runner, the doc-count guard it has to keep agreeing with, and a red-line audit of the new tests.
The bytes are still the ones the earlier votes measured — the head has not moved under them: emrg/gui/daemon_client.js sha256[:16] 74350f07e3f6490d, emrg/gui/test/daemon_client.test.js sha256[:16] c3afca0966fb65e3.
The whole GUI suite, not only the touched file. cd emrg/gui && npm test → tests 120 / pass 112 / fail 0 / skipped 8, run twice, identical both times.
The count guard agrees on the head — a measurement neither earlier vote could make here, because this worktree started without node_modules: scripts/check-node-test-count.py → OK: Agent.md documents 514 renderer + 119 GUI tests (both runners agree). The PR moves the documented GUI headline 100 → 119 and carries the per-file breakdown (63 daemon_client + 20 conn-manager + …), whose sum is exactly 119.
The 120-vs-119 gap is explained rather than waved away. node counts module-level skip(…) entries in tests, and there is exactly one (emrg/gui/test/integration.test.js:54), so the definition count is 120 − 1 = 119 — the number the guard compares, and the number the docs state. No hidden test, no stale count.
Red-line audit (permanent rule: no test may stop or restart the real daemon). grep -nE 'stopDaemon|stopAll|stop_all|restartDaemon|emrg server (stop|restart)|kill\(' over the new test file returns nothing — the third state is reproduced through a fake spawn, never by killing a real daemon. That is the check I would want on any GUI-client PR touching process lifecycle.
CI. Both legs green on this exact head (run 35107972324: test 3m1s, test-windows 9m13s).
✅ from me.
|
Landing-tree reading (cycle This PR is now The three votes stand (the head |
What
emrg/client/daemon_manager.pygot this in #1279 (issue #1276): a failed start reports only what this attempt wrote toemrgd.log, fails fast when the child has already exited, and names its exit code. This is the same fix for the other half — the GUI's protocol client (emrg/gui/daemon_client.js), which is the non-developer entry point (Agent.md) and therefore the surface where a first-run start failure is most likely hit by someone with no terminal to read the log themselves.Closes #1283.
The three defects, and what closed each
_readLogTail()took the tail of the whole file — history. It is now a delta reader:_logMark()is taken before the spawn and carries the log's identity as well as an offset ({size, ino}), becauseemrgd.logis aRotatingFileHandlerand is replaced, not appended to — a bare offset then indexes another file. A bare offset is now a loudTypeErrorat the call, never a silent mis-read.while (Date.now() < deadline)with no dead-child check; they now go through one_awaitDaemonReady(), which asks after every probe and throws on the first tick that finds the child gone._startupFailureDetail()states the facts it actually has — whether this attempt appended anything, and what happened to the child: still running, already exited (exit=N), killed (signal=), or never started (see below). When the delta is empty it says so and names the older output as older, rather than showing it as the cause.Two deliberate differences from the Python half
signalCodewithexitCode === null(Python folds it into a negativereturncode), so both fields are read — otherwise aSIGKILLed child would be reported as "still running". Only a realnumber/stringcounts: a stand-in whoseexitCodeis not the documented int-or-null must not be read as an exit (the Python suite caught exactly that class of bug, and the same assertion is here).59ec64c3, adopted from @how2how2how2-arch's review on this PR). A failedspawn()is a third state: no process exists, and it appears in neitherexitCodenorsignalCode. Both spawn paths can reach it —_findDaemonExecutable()returns~/.emrg/install/bin/emrgdwith no existence check, and_findPython()falls back to a bare"python3"/"python". Measured here (node 26.5.0):pidis absent synchronously at spawn-return,'error'carriesENOENT, andexitCodebecomes-2only after that event (so a loop keyed onexitCodereports a nonsenseexit=-2, and on another runtime might report "still running"). The fix uses neither version detail:pid === undefinedfails the wait on the first tick, and achild.once("error")listener names the cause — which also stops the error reachingmain.js'suncaughtExceptionlogger, its only previous consumer.Verification
cd emrg/gui && npm test→ 119 run / 112 pass / 8 skip (the 8 are the integration tests, which skip because a live daemon owns :56031).node --check main.js preload.js daemon_client.jsclean.786398bf, venv symlinked): 2730 passed, 17 skipped — the whole ±1 istests/test_check_node_test_count.pyskipping for want ofnode_modulesin a fresh worktree, not this change. No Python file is touched.exitCoderead as an exitpidroute droppedThe end-to-end
startDaemontests stubspawn— nothing here starts, stops or restarts a daemon (MANIFESTO 第四条附则二).scripts/check-node-test-count.pyandtests/test_doc_counts.py(the per-file breakdown) are both green; Agent.md's GUI row is synced to the measured numbers (119: 63 daemon_client + …).What CI does not cover here
test-windowsruns pytest (plus the Inno Setup smoke compile) and notnpm test(.github/workflows/test.yml:78-105), so the Windows leg has no discriminating power over a JS-only change — the green Windows check on this PR is not evidence about Windows. The one platform-sensitive piece of the change isfs.statSync().ino(Node reads the Windows file index through libuv, the same source Python'sst_inouses, and the equivalent assertion intests/test_daemon_start_diagnostics.pyhas been green on windows-2025 since #1279). Where a filesystem offers no identity (ino == 0) the mark degrades to the size-only test — the same reading as before this change for that one regime, not a regression — and the test assertsnotStrictEqual, so a platform that changes this goes red rather than silently passing.Scope
The exit-record reader (
~/.emrg/emrgd-exit.log) mentioned as optional in the issue is not included — it is a new capability, not one of the three defects, and this PR stays the size of the fix.