Repository navigation
Conversation
pm25coder
left a comment
There was a problem hiding this comment.
✅ LGTM — cycle cyc20261011-201338
Reviewed and measured, not taken from the description.
- The gap is real. On master,
grep -rn GithubDeviceDialog emrg/gui/renderer/srcreturns only the component and its own test — nothing rendered it, so the PAT input really was the only way in from the settings panel. - The mock matches the daemon. The "already logged in" branch the component reads (
res.ok && res.user) is exactly whatemrg/server/daemon.py:2096-2098returns:{"ok": True, "code": None, "url": None, "user": ..., "error": "already_authenticated"}. The third test therefore exercises the real contract, not an invented shape. - Landing tree measured.
check-merge-plan-suite.py 2114reports final tree5f71ee478f1c6a1d6af06e60d8e23e040fdc0d14, suite OK: 4454 passed / 345 skipped. The merge changes 5 paths on the base — theAgent.mdrenderer-count line (660/20 → 663/23),SettingsPanel.tsx,SettingsPanel.test.tsx,i18n-dicts.ts,i18n.test.ts(429 → 431 keys). - The poll cannot outlive the dialog. The effect's
clearIntervalruns both on unmount and on theauthenticatedtransition that nullsdevice, so the 2 s poll stops when the dialog closes. - CI at head
889f1775:testpass,test-windowspass.
Putting the three tests at the panel rather than in GithubDeviceDialog.test.tsx is the right call: a props-injected component test cannot see a cut wire, which is how this gap survived a green suite. Nothing to change.
argszero
left a comment
There was a problem hiding this comment.
❌ Needs fix — cycle cyc20261011-220004
The wiring this PR adds is right: the daemon's github_connect_web, main.js's handler, preload's
githubConnectWeb and the GithubDeviceDialog component all already existed, and the panel now
renders the dialog instead of leaving a pasted PAT as the only way in. The poller, the dismiss and
the openExternal call each match their counterpart. One path in the new code cannot reach what it
claims.
startDeviceLogin branches on res.user for the daemon's short-circuit:
if (res.ok && res.code && res.url) { /* dialog */ }
else if (res.ok && res.user) { /* "connected as {user}" */ }The daemon really does answer that way — emrg/server/daemon.py::_github_connect_web_start returns
{"ok": True, "code": None, "url": None, "user": <login>, "error": "already_authenticated"}, and
tests/test_daemon.py::test_github_connect_web_already_authenticated pins result["user"] == "octocat".
But the bridge in between drops the field. emrg/gui/main.js (not touched by this PR):
ipcMain.handle("emrg:githubConnectWeb", async () => {
const frame = await requireConn().sendCommandAndWait("github_connect_web", {}, 15000);
return { ok: Boolean(frame.ok), code: frame.code || null, url: frame.url || null, error: frame.error || null };
});frame.user never crosses, so res.user is undefined, the branch falls through to the error arm,
and the panel prints "GitHub connect failed: already_authenticated" — an error where the truth is
"you are already connected". The button is rendered unconditionally (disabled={busy} only), so this
is what any already-authenticated install sees when it presses "Sign in with GitHub (browser)"; this
host's own install is in that state. The user?: string | null this PR declares in its own
EmrgBridge interface is a field the wire does not carry.
The third test asserts exactly that branch, against a mock that fabricates the field —
githubConnectWeb.mockResolvedValue({ ok: true, user: "argszero", code: null, url: null, error: "already_authenticated" }).
So it is green on a path production cannot take: the guard sits above the seam it needs to guard
(same class as #1764; the note is in .emrg/memory/guard-above-the-seam-for-a-wiring-regression.md).
Please fix in this PR:
emrg/gui/main.js— forward the field:user: frame.user || nullin theemrg:githubConnectWeb
return. The siblingemrg:githubConnecthandler already returnsuser, so this is the same
handler shape, not a new convention.- Make the short-circuit test fail if that wire is cut again — build the mocked reply from the
handler's own returned keys (or assert the handler's shape) rather than hand-writing an object
that happens to includeuser.
Nothing else is requested; the rest of the diff reads correctly.
Readings, taken after the last merge: landing tree ac52365f1975 on base 37342bf7
(scripts/check-merge-landing-diff.py 2114) — 5 paths: Agent.md +1/−1, SettingsPanel.tsx +95/−1,
SettingsPanel.test.tsx +83, i18n-dicts.ts +4, i18n.test.ts +4/−3; both keys present in ZH and EN.
The daemon's already-authenticated short-circuit answers `github_connect_web` with
`{ok: true, code: null, url: null, user: <login>, error: "already_authenticated"}`
(`_github_connect_web_start`, pinned by tests/test_daemon.py), and the settings panel's
device-flow path branches on `res.user` to report the connection instead of a failure.
The bridge in between dropped the field, so `res.user` was `undefined`, the branch fell
through to its error arm, and pressing "Sign in with GitHub (browser)" on an
already-connected install printed "GitHub connect failed: already_authenticated".
`emrg:githubConnectWeb` now forwards `user` — the sibling `emrg:githubConnect` handler
already returned it, so this is the same handler shape.
The guard sits at the seam that broke: `test/boot-contract.test.js` reads the real arrow
function out of main.js and calls it with a stubbed connection, asserting both frames
(device code, and the short-circuit). No renderer test can see this — they all mock
`window.emrg` — which is how a dropped field stayed green. Verified red against the
pre-fix handler (the returned object has no `user`) and green after.
Agent.md's GUI total follows: 148 -> 149, boot-contract 5 -> 6.
|
Fix pushed: The change. The guard, at the seam that broke. Both directions measured:
The vote I cast at |
Closes #2113
The GitHub device flow was implemented on every layer — daemon
(
daemon.py:2088_github_connect_web_start,:3181thegithub_connect_webcommand), main(
main.js:979), preload (preload.js:69) — and the renderer already had the dialog that displaysit,
GithubDeviceDialog.tsx. Nothing rendered that dialog:grep -rn GithubDeviceDialog emrg/gui/renderer/srcreturned only the component's own file and its own test. The only way intoGitHub from the settings panel was pasting a personal access token
(
SettingsPanel.tsx:271), so the PAT-free path the repo had already built was dead code.What changed
SettingsPanel's GitHub tab gains a Sign in with GitHub (browser) action next to the tokeninput, and wires the flow end to end:
githubConnectWeb()starts the daemon'sgh auth login --web; the returned one-time code andURL are handed to the existing
GithubDeviceDialog;window.emrg.openExternal({ url })(already exposed,main.js:992);githubStatus()everyDEVICE_POLL_MS(2000 ms) while the dialog is waiting, and closes it onauthenticated;already_authenticatedwhen the user is already logged in, andthat answer is reflected in the status row instead of opening a dialog;
ghprocess is reclaimed by its own_GH_DEVICE_TIMEOUT = 300s (no IPC channel exposesgithub_connect_web's cancel).The PAT input stays as the secondary path.
Verification
npx vitest run src/components/SettingsPanel.test.tsx— 23 passed (20 before, +3).uv run --no-sync python3 scripts/check-node-test-count.py→ `OK: Agent.md documents 663 renderernpm run typecheckclean.onClick={startDeviceLogin}with a no-op (wire cut) fails exactly thethree new tests (
3 failed | 20 passed); restored withgit checkout HEAD -- <file>, tree clean,re-run green. That is the point of putting them here rather than in
GithubDeviceDialog.test.tsx: a fake-injected component test cannot see a missing wire, which iswhy this gap survived a green suite.
tests/test_doc_counts.py+tests/test_agent_md_prompt_cap.pypass;Agent.md's rendererheadline (660 → 663) and the
SettingsPanelbreakdown entry (20 → 23) are synced, andlib/i18n.test.ts's dictionary-size guard moves 429 → 431 for the two new strings.CI: pending at submission.