Files
magpie/internal/gui/tests
yetone 04dc3bca72 panel: a Routing tab shows the gateway's latest requests, and the allowances' tab is named Allowances (v on Discord #feedback: the panel's Usage tab and the window's Usage page mean different things, and the panel had no routing) — the tray panel had Agents, Usage and Profiles, its Usage being each subscription's allowance while the window's Usage counts requests and Routing shows where they went. The panel's tab now reads Allowances (额度; the window keeps Usage/用量, and the tab's id and remembered choice are unchanged), and a new Routing tab (路由) lists the gateway's last 8 requests as they come, from the trace the Routing page already long-polls (/api/gateway/trace, which routing.js polled in the panel too; no new endpoint): each a two-line row with the agent's dot, icon and name and the model it asked for, the time, then where it went (provider · account or key), the model that try sent and, when the reply named another model (the new served/swapped fields), the amber "served gpt-6-luna" (实际 …) tag of the Requests list, beside the tokens and time taken; a failed one says its status and why (429 · rate limited), one under way who is answering. Over them, today's calls and tokens (the Usage page's /api/usage?period=today, read when the tab is shown and after a request ends, at most every 5 s) and Open Routing. A click on a request opens the window's Routing page on it: POST /api/window/main takes req beside view=routing (mainView lets only a positive id through), and the page, loaded so, picks that request as a click in its list would (its story told) and drops req from the address. The panel's height counts the tab as its tallest, as the others; the tab redraws keeping the panel's scroll, the click moves nothing, no border stripe. Tested with a new internal/gui/tests/panel-routing.test.cjs in Chromium and WebKit, en and zh: the Routing and Allowances labels, four fixture requests listed newest first with agent, model, "Relay · team" and "Claude · ann@example.com", the served tag on the swapped one only (not on a dated name), the 429 row, times, no row overflowing, today's 42 calls · 1.2M tokens, a click with the panel scrolled asking for view=routing&req=100 and leaving the scroll as it was, Open Routing, and the window opened on ?view=routing&req=101 with that request picked and its story shown; and TestMainView. go build -tags nogui (and GOOS=linux, GOOS=windows), go test -tags nogui ./... and the whole GUI suite (102 tests) pass. Not tried in the real tray panel or with a live gateway: the window opening on the request is tested in the page, not through Wails.
2026-09-29 20:22:19 +08:00
..
agents: Antigravity CLI (agy) takes magpie's models (Discord Str: asked to use agy through magpie) — agy is on the Agents page like the others. Picking one of magpie's models writes ~/.gemini/antigravity-cli/settings.json: modelProvider "gemini", an entry per magpie model under customModelsConfig.customModels (magpie/<ref> → {apiProvider: API_PROVIDER_GOOGLE_GEMINI, modelName: <ref>}) beside the user's own, and model. Every other key is kept, and "as installed" gives back the user's model and modelProvider and takes magpie's entries out (the file byte-identical to before). agy reads its endpoint and key only from GEMINI_API_KEY / GOOGLE_GEMINI_BASE_URL, and in agy 1.2.13 a custom model chosen by settings or its picker fails ("model is empty"): only --model resolves it. So the agent has a Launch command, GEMINI_API_KEY=magpie GOOGLE_GEMINI_BASE_URL=<gateway> agy --model '<model>' (a $env: form on Windows), which magpie agy prints as its notice and the row's new square copies in one click (tooltip shows it; no scroll; zh strings). Check says unwired when the entry is gone or modelProvider isn't gemini. Tested with TestAgy (set/switch/drift/restore, a user's custom model kept, a file with nothing of the user's left as it was) and agy-launch.test.cjs in Chromium and WebKit, en and zh (fails without the app.js change); end to end in a sandbox HOME with agy 1.2.13 against a sandbox gateway on 38472 and a fake OpenAI upstream: magpie agy magpie/fakeup/echo-1 wrote the settings, agy (interactive and --print) answered from the upstream through the gateway, magpie agy default restored the file. Not tested: the Windows command, a real Gemini upstream; agy's requests show in usage as google-genai-sdk (its UA), and its title requests for gemini-3.1-flash-lite-preview get a 404. go build (nogui, linux, windows) and go test -tags nogui ./... pass; the GUI suite passes but for click-scroll's WebKit case, failing already
2026-09-29 19:41:29 +08:00
balance: a custom provider's balance token can't be left where it is never asked, and new-api's refusals say what to do (TJHHHH: OK了,希望有防呆机制 — a new-api relay's System Access Token pasted beside a Balance URL of /api/usage/token, no New-Api-User header, and the Usage page never showed a balance) — new-api has two balance queries: /api/usage/token tells a key what is left on it and takes the key alone, and /api/user/self tells the account's quota to the access token with the user's id in New-Api-User. A token beside the first could only ever be turned down, and the second without the header answered {"success":false,"message":"无权进行此操作,未提供 New-Api-User"}, which a 401 showed as JSON and a 200 read as a field missing. The editor now says so under the token as soon as it is there: beside …/api/usage/token, or with no Balance URL at all (the token field is now offered on every custom provider, not only one with a URL saved), a warning with one button, Use https://<site>/api/user/self, which sets the URL, sets the Balance field to $data.quota / 500000 when it was empty or named /api/usage/token's fields, and opens More endpoints to show it; once the URL is /api/user/self and no New-Api-User header has a value, it asks for New-Api-User = your user ID (shown in the site's personal settings) until one is typed. The URL is not switched behind the user's back: what the editor shows is what is asked, and the field typed for /api/usage/token's reply wouldn't read /api/user/self's anyway. Behind it, Balance never sends a token to /api/usage/token and says what to set instead; a reply of success:false with a message, at a 401 or a 200, is that message as the error, with the header to add put in front when it is about New-Api-User (not provided, format error or mismatch, in new-api's Chinese or English); and /api/user/self with an empty field reads the quota, $1 to 500000. The Balance field has a Check balance button (POST /api/provider/balance) that asks as the form has it, before a Save, the saved provider with the form's URL, field, headers and token; it and the Usage page's card say either fix plainly, the relay's own words on hover. A JWT beside new-api's URL is not warned about: new-api's own dashboard sign-ins are JWTs now, so a token's shape can't tell new-api from a sub2api panel. Tested with new TestKeyUsageWithAccessToken (never asked, the fix said, the key alone still asks), TestUserSelfDefaultField and TestNewAPIUserRefusal (401 and 200, Chinese and English, the mismatch, and an unrelated refusal left as it came), which all fail on main; and a new internal/gui/tests/balance-fix.test.cjs in Chromium and WebKit (the warning on the saved setup, the click, the header hint going once typed, Check balance with and without the header and what it posted, Save, the Usage card's words, every new string in Chinese, and a new provider with a token and no URL). go build (and GOOS=linux, GOOS=windows), go test -tags nogui ./... and the other GUI tests pass
2026-09-29 17:56:43 +08:00
agents: each agent's CLI shows its version and updates in one click (#202 JoeyMa-zh: Agents 页面增加"更新 CLI"功能 — 检查各 agent CLI 的版本并一键更新) — the Agents page now puts each CLI's installed version after its name (read with <cli> --version, 10s timeout, no stdin) and, when a newer one is out and magpie can tell how it was installed, a small "Update to x.y.z" pill. How it was installed comes from the resolved binary: the vendor's own installer first (Claude Code's ~/.local/share/claude/versions → claude update, Codex standalone → codex update, ~/.opencode/bin → opencode upgrade), then a global node_modules (bun, pnpm, else npm, run as npm install -g --prefix <prefix> pkg@latest with that prefix's npm, and only where the prefix is writable), then a Homebrew formula or cask allowlisted per agent (brew upgrade [--cask]); anything else shows the version alone, with no button. The latest comes from the npm registry or brew info --json=v2 (no auto-update), cached 6h (15min after a failure); installed versions are cached by the binary's path, size and mtime. GET /api/agents/cli waits at most 3s and says pending, and the page asks again; POST /api/agents/cli/{id} runs the update (one at a time per agent, 10min cap), reads the version again and says so if it did not change, or gives the command's last lines when it fails. The pill goes busy in place and the page never scrolls; not in the tray panel. zh strings added. Tested with a new internal/agent/cliupdate_test.go (version order and parsing; npm/bun/pnpm/brew formula and cask/native installs, a read-only prefix, a local node_modules and unknown binaries in a fake PATH; an update against a fake registry and updater) and a new internal/gui/tests/cli-update.test.cjs in Chromium and WebKit (versions, pills only where magpie can update, row heights, an update with the view scrolled, a failing one, zh). No real update was run. go build (nogui, GOOS=windows, GOOS=linux), go test -tags nogui ./... and the GUI tests pass
2026-09-29 17:56:43 +08:00
providers: ChatGPT and Claude accounts come in from CLIProxyAPI's auth files (#124 Mrhe525: 要是能把那什么cpa的json文件可以导入直接使用就无敌了) — the ChatGPT (Codex) and Claude subscriptions now take the import Antigravity had: Import accounts from a file… under the accounts, and Import from a file instead… while a browser sign-in waits. It reads CLIProxyAPI's files ({type:"codex"|"claude", access_token, refresh_token, id_token, account_id, email, …}), one or a list or {accounts:[…]}, Codex's own auth.json ({tokens:{…}}), Claude Code's .credentials.json ({claudeAiOauth:{…}}) and bare refresh tokens one a line. Each account is checked as a sign-in in magpie is finished: its refresh token traded with the agent's own OAuth client (codexRefresh / claudeRefresh), then the ChatGPT claims or Claude's profile read (plan, tier, account and organization), and it is kept as that sign-in would keep it: Codex's or Claude Code's own if the agent has none, else added and ticked. The refresh rotates the token, so the file's copy stops working; the box says so. A refresh token magpie holds already (saved or the agent's own) is "Already in magpie" and never refreshed; another app's file ("a claude sign-in, not Codex's"), an API key, a token in the files twice and one the vendor turns down ("used since … expired or signed out") are listed as not added with the reason; tokens are never in what comes back. codexExchange and claudeExchange now build their sign-in through the same codexLogin / claudeLogin. Also: a Claude sign-in with no ~/.claude yet failed writing .credentials.json (the directory is made now), and the magpie://import dialog was drawn by the subscription import's renderImport, declared later under the same name (renamed renderLoginImport). zh strings added. Tested with new TestImportCodexLogins (a fake token endpoint: a CPA file and a native auth.json added with the new tokens and the file's account id, the live account untouched and reported as there, a Claude file, a spent token and a duplicate refused, a second import "exists", garbage an error) and TestImportClaudeLogins (with no sign-in the CPA file becomes Claude Code's credentials, plan max, active; a spent .credentials.json and a Codex file refused), and a new internal/gui/tests/login-import.test.cjs in Chromium and WebKit, en and zh (both ways in, the texts, what is posted, the outcomes, the account listed). go build -tags nogui (and GOOS=windows, GOOS=linux), go test -tags nogui ./... and the other GUI tests pass on fe22692 (click-scroll's WebKit case fails after f1a8caf, with or without this change)
2026-09-29 18:34:08 +08:00
agents: each agent's CLI shows its version and updates in one click (#202 JoeyMa-zh: Agents 页面增加"更新 CLI"功能 — 检查各 agent CLI 的版本并一键更新) — the Agents page now puts each CLI's installed version after its name (read with <cli> --version, 10s timeout, no stdin) and, when a newer one is out and magpie can tell how it was installed, a small "Update to x.y.z" pill. How it was installed comes from the resolved binary: the vendor's own installer first (Claude Code's ~/.local/share/claude/versions → claude update, Codex standalone → codex update, ~/.opencode/bin → opencode upgrade), then a global node_modules (bun, pnpm, else npm, run as npm install -g --prefix <prefix> pkg@latest with that prefix's npm, and only where the prefix is writable), then a Homebrew formula or cask allowlisted per agent (brew upgrade [--cask]); anything else shows the version alone, with no button. The latest comes from the npm registry or brew info --json=v2 (no auto-update), cached 6h (15min after a failure); installed versions are cached by the binary's path, size and mtime. GET /api/agents/cli waits at most 3s and says pending, and the page asks again; POST /api/agents/cli/{id} runs the update (one at a time per agent, 10min cap), reads the version again and says so if it did not change, or gives the command's last lines when it fails. The pill goes busy in place and the page never scrolls; not in the tray panel. zh strings added. Tested with a new internal/agent/cliupdate_test.go (version order and parsing; npm/bun/pnpm/brew formula and cask/native installs, a read-only prefix, a local node_modules and unknown binaries in a fake PATH; an update against a fake registry and updater) and a new internal/gui/tests/cli-update.test.cjs in Chromium and WebKit (versions, pills only where magpie can update, row heights, an update with the view scrolled, a failing one, zh). No real update was run. go build (nogui, GOOS=windows, GOOS=linux), go test -tags nogui ./... and the GUI tests pass
2026-09-29 17:56:43 +08:00
tray: expanding and collapsing on the Agents page keeps what was clicked where it is, and "Show {n} more" unrolls the rest under it (碳碳双键: 这个地方展开 ui 会偶现疯狂抖动,必现拉扯; #199: agent页面展开与收纳会抽搐 甚至跑在窗口外面去; willz: 点击展开的时候,列表不是向下展开 而是向上展开) — the rest unrolled above the button, so in a panel at its tallest with the list scrolled the button slid down and off the foot of the view. What a click holds in place was passed over while anything played on it, a colour easing in included: the button's own hover fade and its label's fade meant the page was held by the list instead, so the button went off-screen for ~300ms and then the page snapped 90px back to it; and a button slid out from under the pointer and back fades its hover again, so the page swung between the two (484 → 563 → 484 → 572 … frame by frame in Chromium). Held elements were also put back a frame late, from requestAnimationFrame, so what was clicked trembled by a frame's growth. Now only a play that moves an element (transform, top, margin …) passes it over, the hold is put back as soon as a growing part is laid out (a ResizeObserver, before paint), and the button comes before what it unrolls: the rest come in under it, the rows above stay with it, and in a view with no room below the view goes down with them (scrollOnPurpose with the click), led by the fold's foot frame by frame, never past the button's top; Show less rolls them up under the button, which stays put. Tested in Playwright Chromium and WebKit with a stub panel (13 agents, Codex open, list scrolled to its end) and the window: new internal/gui/tests/panel-fold.test.cjs checks each painted frame of Show more (the view only goes down, the row above keeps with the button, the rest below it, then in sight), Show less and a row opened and closed (what was clicked within 2px); it fails on main (button moved 99px, the swinging in Chromium). All node tests and go test -tags nogui ./internal/gui/... pass; click-scroll's "the view must be at its end" also flakes on main under load
2026-09-29 16:40:03 +08:00
panel: a profile saved or deleted shows at once and stays in sight (Image #62: 点击保存没有效果,删除方案也没有效果。你为什么UI UX做这么烂?) — both did their work (profiles.json had the profile), but the page showed nothing of it: every profile action answers with the whole state, which read logins.json (166KB, the accounts' credentials) again for each agent's drift and models, dozens of times — 2.5–4s a state on a real setup — and only then redrew the chips. And with no profiles yet the chip saved came in above + Save current, which the click held where it was on the screen: the hold gave the view room and scrolled it 30px, and the chip went up under the tabs, out of sight. Now the chip saved is there at once, dimmed till magpie answers, the name field closed; × takes the chip away at once; a save or delete refused puts the list back and says why. A view at its top when clicked stays at its top instead of being given room to scroll (what came in above the control moves it down). logins.json is parsed once until it changes (filememo, a copy to each caller): a state went from 2.4s to 0.9s here. Tested with a new case in internal/gui/tests/panel-profiles.test.cjs, Chromium and WebKit, en and zh: magpie answering after 1.5s with no profiles, the chip there and dimmed before the answer, the list unscrolled and the chip below the tabs, × removing it before the answer, a refused save taken off with its reason (fails on main); against a sandbox magpie web in WebKit, the view stays at 0 where main scrolled it to 30. click-scroll passes in both engines; go test -tags nogui ./... (TestClaudeSignedOut flaky under the full run, passes alone), go build for linux and windows
2026-09-29 19:23:14 +08:00
panel: a Routing tab shows the gateway's latest requests, and the allowances' tab is named Allowances (v on Discord #feedback: the panel's Usage tab and the window's Usage page mean different things, and the panel had no routing) — the tray panel had Agents, Usage and Profiles, its Usage being each subscription's allowance while the window's Usage counts requests and Routing shows where they went. The panel's tab now reads Allowances (额度; the window keeps Usage/用量, and the tab's id and remembered choice are unchanged), and a new Routing tab (路由) lists the gateway's last 8 requests as they come, from the trace the Routing page already long-polls (/api/gateway/trace, which routing.js polled in the panel too; no new endpoint): each a two-line row with the agent's dot, icon and name and the model it asked for, the time, then where it went (provider · account or key), the model that try sent and, when the reply named another model (the new served/swapped fields), the amber "served gpt-6-luna" (实际 …) tag of the Requests list, beside the tokens and time taken; a failed one says its status and why (429 · rate limited), one under way who is answering. Over them, today's calls and tokens (the Usage page's /api/usage?period=today, read when the tab is shown and after a request ends, at most every 5 s) and Open Routing. A click on a request opens the window's Routing page on it: POST /api/window/main takes req beside view=routing (mainView lets only a positive id through), and the page, loaded so, picks that request as a click in its list would (its story told) and drops req from the address. The panel's height counts the tab as its tallest, as the others; the tab redraws keeping the panel's scroll, the click moves nothing, no border stripe. Tested with a new internal/gui/tests/panel-routing.test.cjs in Chromium and WebKit, en and zh: the Routing and Allowances labels, four fixture requests listed newest first with agent, model, "Relay · team" and "Claude · ann@example.com", the served tag on the swapped one only (not on a dated name), the 429 row, times, no row overflowing, today's 42 calls · 1.2M tokens, a click with the panel scrolled asking for view=routing&req=100 and leaving the scroll as it was, Open Routing, and the window opened on ?view=routing&req=101 with that request picked and its story shown; and TestMainView. go build -tags nogui (and GOOS=linux, GOOS=windows), go test -tags nogui ./... and the whole GUI suite (102 tests) pass. Not tried in the real tray panel or with a live gateway: the window opening on the request is tested in the page, not through Wails.
2026-09-29 20:22:19 +08:00
panel: a Routing tab shows the gateway's latest requests, and the allowances' tab is named Allowances (v on Discord #feedback: the panel's Usage tab and the window's Usage page mean different things, and the panel had no routing) — the tray panel had Agents, Usage and Profiles, its Usage being each subscription's allowance while the window's Usage counts requests and Routing shows where they went. The panel's tab now reads Allowances (额度; the window keeps Usage/用量, and the tab's id and remembered choice are unchanged), and a new Routing tab (路由) lists the gateway's last 8 requests as they come, from the trace the Routing page already long-polls (/api/gateway/trace, which routing.js polled in the panel too; no new endpoint): each a two-line row with the agent's dot, icon and name and the model it asked for, the time, then where it went (provider · account or key), the model that try sent and, when the reply named another model (the new served/swapped fields), the amber "served gpt-6-luna" (实际 …) tag of the Requests list, beside the tokens and time taken; a failed one says its status and why (429 · rate limited), one under way who is answering. Over them, today's calls and tokens (the Usage page's /api/usage?period=today, read when the tab is shown and after a request ends, at most every 5 s) and Open Routing. A click on a request opens the window's Routing page on it: POST /api/window/main takes req beside view=routing (mainView lets only a positive id through), and the page, loaded so, picks that request as a click in its list would (its story told) and drops req from the address. The panel's height counts the tab as its tallest, as the others; the tab redraws keeping the panel's scroll, the click moves nothing, no border stripe. Tested with a new internal/gui/tests/panel-routing.test.cjs in Chromium and WebKit, en and zh: the Routing and Allowances labels, four fixture requests listed newest first with agent, model, "Relay · team" and "Claude · ann@example.com", the served tag on the swapped one only (not on a dated name), the 429 row, times, no row overflowing, today's 42 calls · 1.2M tokens, a click with the panel scrolled asking for view=routing&req=100 and leaving the scroll as it was, Open Routing, and the window opened on ?view=routing&req=101 with that request picked and its story shown; and TestMainView. go build -tags nogui (and GOOS=linux, GOOS=windows), go test -tags nogui ./... and the whole GUI suite (102 tests) pass. Not tried in the real tray panel or with a live gateway: the window opening on the request is tested in the page, not through Wails.
2026-09-29 20:22:19 +08:00
gateway: a call Codex makes for itself says so on the Routing page, and goes to a pool account with what Codex says about it (碳碳双键: 为何 codex 桌面选的 5.6sol,但是请求记录…显示的全是 gpt-6-luna) — magpie changes no model: Codex sends its own calls on Luna (the guardian review of an approval, x-openai-subagent: guardian; a thread's title; memories) and, on Luna Reserve once the plan's allowance is used up, every turn with x-openai-codex-luna-reserve. Relayed to Codex's own sign-in these headers went on as they came, but a turn the pool of Codex's accounts served was signed afresh and went without them. Now x-openai-*, x-codex-* (turn metadata, installation, window, parent thread), session_id, conversation_id, x-client-request-id and version go on with it too; never a sign-in (Authorization, chatgpt-account-id, anything with authorization in its name) nor Codex's device attestation, the pool account's own going in its place, nor what holds for Codex's own account alone (x-openai-codex-luna-reserve, as another account's allowance may be left, and x-codex-turn-state, the backend's routing of the turn for that account). The trace, the Recent calls and the usage log keep the call's kind (kind, from x-openai-subagent; memgen; luna_reserve), and the Requests list, a replay and the request's story show it as a grey tag by the model — Approval check / 审批判定, Title / 标题, Memory / 记忆, Subagent / 子代理, Review / 审查, Compaction / 压缩, Luna Reserve / Luna 储备, an unknown one as Codex names it — the story saying Codex made the call itself and picks its model. The Requests list's model column fits its content (up to 36%) so model and tag keep their room, the tag giving way first. Tested with a fake ChatGPT backend on the pool path (acct-1 used up, acct-2 answering: both get the headers and neither the client's sign-in; fails without the change), callKind/codexHeader cases, and a new internal/gui/tests/routing-kind.test.cjs in Chromium and WebKit, en and zh (tags, the model shown whole, the story, a click leaving the row where it is); the other GUI tests, go build -tags nogui, GOOS=windows go build and go test -tags nogui pass
2026-09-29 17:56:43 +08:00
routing: a request answered by another model than the one asked for is marked, from the model the vendor's reply names (v on Discord #feedback: 模型路由情况其实有的时候还是想知道的,比如之前 openai 会把 sol 降智到 luna 这样的,现在只有走 s2a 和 newapi 这些网关服务才能明确看到) — magpie read the tokens off each reply but not the model it says answered, so a vendor serving sol's requests with luna looked, on the Routing page, like sol answering. The reply's model is now kept as it goes by, never delayed or changed: on a relayed reply by the usage sniffer, which already reads every event and JSON body (Chat Completions' model in its JSON and each chunk, the Responses API's response.model in response.created/completed and model in its JSON, Anthropic's message.model in message_start and model in its JSON); on a translated one from the decoders' start event, which already carried it (Chat, Responses, Anthropic, and Gemini Code Assist's modelVersion), streamed or collected; on a Claude subscription's from the CLI's message_start; and on Codex's own models relayed to the ChatGPT backend. It goes in Usage.Served (and so the request log), each try's Served and the route's (the last try's, when it answered), with Swapped when it is another model than the one the try sent the vendor. Vendors mostly echo a dated or pinned name, which is the same model: the names are compared without a maker or path prefix (openai/, models/, Bedrock's us.anthropic.), case, and a version at the end — a date (-2025-08-07, -20250929, -06-05), a build number (-0613, -001), Bedrock's -v1:0, -latest, -preview — so gpt-5 answered as gpt-5-2025-08-07 or claude-3-5-sonnet-latest as claude-3-5-sonnet-20241022 is not marked, while gpt-5 as gpt-5-mini, gpt-5 as gpt-5.1, claude-opus-4 as claude-opus-4-1-… and deepseek-v3 as deepseek-v2 are. A reply that names no model marks nothing. The Routing page's Requests list puts an amber "served luna" (实际 luna) tag by the answer, its title saying what it means, and the request's story a line "requested sol · served luna" (请求 sol · 实际 luna) with the same sentence; no border stripe, and picking the request leaves the page where it is. Cursor, Devin, Kiro and Qoder name the model they were asked for themselves, so they are never marked.
2026-09-29 19:41:29 +08:00
providers: Claude Code is signed in to its next account once the one it is on is spent, as Codex is, and an account keeps its own affinity, backoff and usage across a switch (#208 Cyb3rN8: Claude Code 用自己的模型时,登录账号额度用满不会自动切换 · #209: Smart 选中的账号和 Claude Code 登录账号互不同步…切换登录账号还会把按 id 记的状态串到另一个账号上) — Claude Code on its own model asks Anthropic as the account it is signed in to, never through magpie, so with A spent its sessions got "You've hit your session limit" while the gateway had moved on to B. The Codex watcher (KeepCodexOnAnAccountWithRoom, now KeepOnAnAccountWithRoom, same place: started by the magpie serving the gateway, a minute in and every 5 minutes) now looks at Claude Code too: when the account it is signed in to is spent and another ticked account's allowance is known and has room, it is signed in to that one through SwitchLogin (credentials and .claude.json's oauthAccount), as Use on the Accounts page does; not when the Claude account was removed from magpie. The rule for #209: the sign-in follows Smart, not the other way round — it moves at the share Smart counts an account spent (provider.SpentShare = 98, which the gateway's usedShare now is), for Codex too (it moved at 100%). The Routing field of a Codex or Claude subscription with several accounts says so: routing picks the account for what comes through magpie; the agent on its own uses the one it is signed in to, moved on at 98%. Sessions already running keep the account they started with until restarted, as SwitchLogin says; routing Claude Code's own model through the gateway (the codexFailover way) is not done here. #209's second part was a real bug: the account the agent is on is the candidate "claude" and the others "claude@user", so a switch handed "claude" from A to B — a conversation A answered was kept on B and traced kept=true, and the Least used tokens and the backoff count followed the id. who(), routed.used and routed.failures are now keyed by restKey (provider@user, as rests already were); rest itself (group members, the trace) is unchanged. Tested with new TestClaudeSwitchedWhenSpent (sandbox HOME, keychain off, a fake usage endpoint: 97% stays, 98% signs Claude Code in to b — credentials and .claude.json — a stays ticked; on b it stays; both spent it stays), TestCodexSwitchedWhenUsedUp still passing, new TestLoginSwitchKeepsEachAccountItself (fails on main: the conversation went to b, kept) and new internal/gui/tests/routing-signin.test.cjs in Chromium and WebKit, en and zh; login-import and routing-kind GUI tests pass. go build -tags nogui (and GOOS=linux, GOOS=windows), go test -tags nogui ./... pass (TestUserPath, TestClaudeAccountNamedAsItsLogin, TestClaudeIdentityServedWhileAsked and TestCodexVersion timed out at 5s under the full run's load and pass on their own). Not tested against real Claude accounts or a running Claude Code.
2026-09-29 18:56:33 +08:00
settings: the warm-ups and WorkBuddy's check-in sit under Codex, Claude Code and WorkBuddy headings, with short lines (#124 Mrhe525: a tidier Settings page) — after Open at login, Dock and Proxy the Preferences list ran on with Codex warm-up, Codex daily warm-up, Claude Code warm-up, Claude Code daily warm-up and Check in to WorkBuddy daily, each with a long grey sentence, one flat list. They now come after it in three groups under small headings as Local network has: Codex (Warm up on reset · 窗口重置时预热, Daily warm-up · 每日定时预热), Claude Code (the same two) and WorkBuddy (Daily check-in · 每日自动签到), the rows' names no longer saying the service. The lines say the purpose alone ("One tiny request starts the next window at once", "Starts each account's 5-hour window at this time every day", "Claims each signed-in China account's check-in credits once a day"), the last warm-up (上次启动于17:01) and each account's check-in (今日已签到 +1…) still after them; the rest (06:00 gives three by 21:00, sent through Claude Code, 签到 as in WorkBuddy) moved into the lines' tooltips with the fine print already there. The WorkBuddy group, heading and all, shows only while an account is signed in or the check-in is on, as the row did; a daily warm-up's time field was already there only while it is on. Same settings keys and the same /api/settings posts; en and zh strings replaced together, the old ones removed. No border stripes. Tested with a new internal/gui/tests/settings-groups.test.cjs in Chromium and WebKit, en and zh: the headings in order after Preferences and before Local network, each group's rows and none left in Preferences, the status on the lines, no left border; with the page scrolled, each segment (Weekly, Weekly and 5-hour, daily On, a new time, Off, check-in On) posts codexWarmup, claudeWarmup, codexWarmAt, claudeWarmAt and workbuddyCheckin as before with the page left where it was, the time field coming and going with its On/Off; no WorkBuddy group with no account. All internal/gui/tests/*.cjs pass (108), go build -tags nogui ./... and go test -tags nogui ./... pass.
2026-09-29 20:22:09 +08:00

Dropdown browser regression

menu-scroll.test.cjs loads the real HTML, CSS and JavaScript with isolated API fixtures. It checks session folder/model filters and the main model picker in Chromium and WebKit, including wheel, scrollbar track/thumb, keyboard selection and dismissal. It does not start the backend or read local user configuration.

click-scroll.test.cjs holds the rule that a click never moves the page (see "where the reader is" in app.js): on the Routing page, Live and a day picked in turn with the list scrolled to its end; a click whose handler sets scrollTop, grows the page above, redraws itself, or scrolls from a timer or a shared helper; a control under what it unrolls (data-unrolls) going down with it; a click that asks to go somewhere with scrollOnPurpose(e); the room kept at the foot going as the reader scrolls back; and the wheel.

panel-fold.test.cjs expands and collapses on the Agents page with the list scrolled to its end, in the tray panel (one agent open) and in the window: "Show {n} more" unrolls the rest under the button, the view going down with them and never back up; "Show less", and a row opened and closed, leave what was clicked where it is on every frame.

gateway-fold.test.cjs folds Connect on the Gateway page with the view scrolled: its fields hide, the head keeps the base URL and a copy button, the head stays where it was, and the fold is remembered across a reload.

routing-kind.test.cjs lists calls Codex makes for itself (a guardian review, a title, memories, a turn on Luna Reserve, a kind it does not know yet): each has a grey tag by its model in the Requests list, in English and Chinese, the model keeping its room first, and the request's story says what it was.

routing-served.test.cjs lists a request whose vendor's reply names another model than the one asked for (gpt-6-sol served as gpt-6-luna), one answered under the model's dated name and ones naming none: only the first is marked "served gpt-6-luna" by its answer in the Requests list and "requested gpt-6-sol · served gpt-6-luna" in its story, in English and Chinese, and the click that picks it leaves the page where it is.

balance-fix.test.cjs opens a custom provider whose balance token sits beside new-api's /api/usage/token (and a new one with a token and no Balance URL): the editor says so, one click moves it to /api/user/self with the quota as its field, the New-Api-User header is asked for until it is typed, Check balance asks as the form has it and says the fix plainly, and the Usage page's card does too.

cli-update.test.cjs draws the agents' CLI versions on the Agents page (#202) from a faked API: the version after each name, "Update to x.y.z" only where magpie knows how the CLI was installed, the rows' height kept, an update clicked with the view scrolled (busy, then the new version, the page left where it was), a failed one giving its reason and the pill back, and the words in Chinese. Nothing is installed.

panel-profiles.test.cjs saves a profile in the tray panel with the list scrolled to its end: "+ Save current" opens the name field beside it, the list not moving, field and button in sight, one focus ring; the button then reads Save and a click on it saves, as Enter does; an empty name keeps the field; Escape closes it; in English and Chinese.

panel-routing.test.cjs opens the tray panel's Routing tab: the gateway's latest requests from a faked trace, newest first, each with its agent, the model asked for, the provider and account it went to, the model that answered ("served gpt-6-luna" on the one a vendor answered with another, not on a dated name), a failure's status, and the time; today's calls and tokens over them; the allowances' tab named Allowances. A click, with the panel scrolled, asks for the window's Routing page on that request and moves nothing; the window opened so has that request picked. In English and Chinese.

login-import.test.cjs brings ChatGPT accounts in from CLIProxyAPI's auth files: offered from the accounts list and from a browser sign-in under way, the box says the refresh spends the file's sign-in, what is pasted is posted as it is, and each account's outcome is listed, in English and Chinese.

agy-launch.test.cjs gives Antigravity CLI's row the square that copies the command starting agy on magpie: the command in its tooltip, a click copying it with the page left where it was, none on other agents, in the tray panel's opened row too, in English and Chinese.

currency.test.cjs shows a cost in dollars by default and in yuan, at magpie's cached exchange rate, once the Settings page's Currency row picks cny (#212): the Usage page's total converts, the row's tooltip carries the rate, picking it with the settings list scrolled well down moves nothing, and the choice survives a reload — in English and Chinese.

settings-groups.test.cjs groups the Settings page's warm-ups and check-in by service (#124): Codex, Claude Code and WorkBuddy headings after Preferences, before Local network, their rows named without the service, the last warm-up and today's check-in still on the short lines, no left-border stripe; a daily warm-up's time field only while it is on; each control posting the setting it did, with the page scrolled and left where it was; the WorkBuddy group only with an account signed in; in English and Chinese.

With Node.js and Playwright available:

node --test internal/gui/tests/menu-scroll.test.cjs internal/gui/tests/click-scroll.test.cjs internal/gui/tests/panel-fold.test.cjs internal/gui/tests/panel-routing.test.cjs internal/gui/tests/gateway-fold.test.cjs internal/gui/tests/routing-kind.test.cjs internal/gui/tests/balance-fix.test.cjs internal/gui/tests/cli-update.test.cjs internal/gui/tests/login-import.test.cjs internal/gui/tests/agy-launch.test.cjs internal/gui/tests/currency.test.cjs

If Playwright is installed outside the repository, set NODE_PATH to the directory containing its package. The suite uses Playwright's Chromium and WebKit binaries (playwright install chromium webkit). No frontend dependency is needed by the app itself. Tested with Playwright 1.63.0.

Set BROWSER=chromium or BROWSER=webkit for one engine. Set ARTIFACT_DIR to an external directory to retain screenshots and Playwright traces, including failed assertions. These browser checks run separately from make test.