11 Commits
Author SHA1 Message Date
yetone 2da8fbe488 tests: every test that runs plugins on the bun on PATH runs it once first, through testenv.Bun, so a bun that has never run here doesn't time out internal/provider's and internal/plugin's plugin tests (#1306)
65dcc573 (ggbdpq's patch 1) warmed the bun on PATH before the root
package's one-minute plugin contexts. The same lookup of the bun on PATH
is in 40 more places in internal/provider (28), internal/gateway (8),
internal/plugin (3) and internal/gui (1), and in the root package's
TestPluginListSaysClash. Many of them then open a one- or two-minute
context (internal/provider's sign-in, fetch and move tests,
internal/plugin's sandbox), and there macOS's first-exec check of a
just-installed or upgraded bun landed inside the deadline. The rest
are covered the same way, so no test has to remember which is which.

testenv.Bun now does what the root package's pathBun did: it finds the
bun on PATH (skipping the test when there is none) and runs
`bun --version` before the test opens its context, once per bun per test
binary. pathBun is gone; every `exec.LookPath("bun")` + skip block in
the tests uses testenv.Bun instead. Three tests reach for the bun some
other way and are left as they were: TestHostRunsAsMagpieBun (7ddea2c2
covers its copy), the plugin_git test's realBun and netproxy's
TestBunThroughBridge, none of which runs the plugin host under a
deadline on the PATH bun. migrate_test.go needed two `err` declarations
that the removed block used to provide.

What was run (macOS arm64, bun 1.4.2), with a wrapper bun first on PATH
whose first exec sleeps 61s (marker-gated, then execs the real bun):
- Red, old code:
  - internal/provider TestPluginSignInTakesAPastedCallback: FAIL at
    61.1s, "pluginsignin_paste_test.go:36: starting plugins: context
    deadline exceeded".
  - internal/plugin TestFakePlugin: FAIL at 62.0s, "plugin_test.go:66:
    Plugins = [], starting plugins: context deadline exceeded".
  - internal/gateway TestPluginFailover passed on the old code too (its
    calls have no deadline that short), so the gateway is not shown red.
- Green, this change, same cold wrapper: the three tests passed (61.1s,
  62.9s, 61.2s).
- go test -tags nogui -count=1 for the root package and for
  internal/testenv, gateway, gui, plugin and provider, under a temp HOME
  with GOMODCACHE, GOCACHE and GOPATH pinned: all ok.
- gofmt -l on the changed files: clean. go vet of those packages for
  darwin (-tags nogui), GOOS=linux and GOOS=windows (-tags nogui), and
  GOOS=windows without the tag: clean.

Not run: a really cold bun on a loaded machine; the sleep stands in for
the check. Not run on Linux or Windows, where the warm-up is one extra
`bun --version` per test binary.
2026-10-10 10:22:59 +08:00
forever 8412a5d1a0 gateway tests: the keepalive tests count only the idle time a client waits through — the keepalive after the vendor's first words, and nothing after the reply's last event (#1163)
* gateway tests: the quiet mid-reply tests wait for, and count, the keepalive after the vendor's first words, as a held stream's before them is the agent's too (TestQuietStreamMidReplyKeptAlive failed once on main at GOMAXPROCS=2, load ~30)

The failing body began ": keepalive" before the role chunk and "hel".
That ordering is allowed, not a bug: quietFast lets watch keep the agent
alive 40ms into the try, and a vendor slow to answer (status 0, nothing
held) or one that flushed response.created and paused before "hel" (only
the role chunk, a lead, held) gets the held stream's keepalive first, as
TestSilentHeldStreamKeptAlive requires. Content that reaches the
holdWriter is scanned and flowed under h.mu, which keepQuiet and
keepAlive take, so no keepalive goes out after content the writer has
and before it is relayed. A vendor delayed 150ms before its first bytes,
or between response.created and "hel", gives the failing body every
time, with the keepalive sent at status 0 or with only the role chunk
held.

The tests released the vendor at the first keepalive and required "hel"
before it, so a slow first write made them fail, or test the held path
instead of the mid-reply one. readQuiet now releases the vendor at the
first keepalive after the given text, and aliveAfter asserts a keepalive
between "hel" and "lo": TestQuietStreamMidReplyKeptAlive and
TestQuietStreamsKeptAliveEveryProtocol's mid-reply cases.
TestQuietKeptAliveOnlySoLong counts only the keepalives after "hel", as
the held ones before them have their own 200ms. The every-protocol check
matches `"lo"` quoted: bare, it matched the anthropic cases'
content_block.

With a 150ms vendor delay injected, the old assertions fail all 5
mid-reply cases and the new ones pass. With keepQuiet's passing-branch
keepalive removed, all 6 mid-reply tests fail, delay or not. go vet;
go test -tags nogui -race -count=20 -run
'TestQuietStream|TestSilentHeld|TestQuietKeptAliveOnlySoLong'
./internal/gateway; the whole package with go test, and as a prebuilt
binary at GOMAXPROCS=2 three times (load 18-28), all pass.

* gateway tests: TestTranslatedStreamKeepsClientAlive stops counting the client's idle timeout once it has the reply's last event (red on ubuntu CI at ef5fba2c, run 37471004500: responses_to_chat "idle_timeout=true completed=true") — the gateway writes and flushes finish_reason, usage and data: [DONE] back to back in chatEncoder.finish, then serve's bookkeeping (NoteServed, saveSticks, usage.Append) runs before the handler returns and net/http sends the stream's close. On a loaded runner that, or the test's own goroutine being held, passed the 150 ms the test still counted after [DONE]. No client's idle timeout counts that wait: Codex hangs up on response.completed, and the clients that read on to the close give minutes. Chat's marker is now data: [DONE], the stream's last event, instead of finish_reason two events before it. With a 200 ms sleep after enc.finish() on the Chat path, the old test fails 20 of 20 exactly as CI did and the new one passes 20 of 20. The new test still fails 20 of 20 with the sleep before enc.finish() (completed=false), with readSSEAlive's keepalives taken out (#436), and with the Chat keepalive followed by a blank line again ("events with no data"). go test -race -count=20 -run '^TestTranslatedStreamKeepsClientAlive$' ./internal/gateway, go vet and go test -tags nogui ./... pass.

* plugin tests: the fake plugin's windows renew at times set when the host loads it, so the gateway's TestPluginFailover passes when an hour begins between two readings of an account's usage (waitingsnow)

The fake plugin told each account's five hours to renew an hour after
its usage was read, and its week a day after. TestPluginFailover weighs
the accounts by what the plugin tells (provider.Allowances), and Smart
routing puts first the account whose week renews sooner, to the hour;
accounts alike keep their order. A request that fails on an account's
quota, or in its stream, drops that account's reading
(staleAllowance), and the next request reads it again. A 401, as in
the gone round, is failOther and drops nothing (rest,
gateway/routing.go). When an hour began between a@fake's read after
the quota round and its read after the stream round, a@fake's week
renewed in the hour after b@fake's. b@fake then went first in the gone
round, failed with a 401 and kept its first reading, so the request
with both out was tried as b, a, full, which fails the test at
plugin_failover_test.go:210. An hour that began after the first
readings but before a@fake's read after the quota round put b@fake
first in the stream round instead, which failed b@fake and read it
again too, and the test passed.

The test fails every time an hour begins between those two reads of
a@fake, and passes otherwise: the 8 such runs below failed and the
other 13 passed. The reads are 12 to 76ms apart in the runs below
without the hold, and a reviewer's -race build measured 33 to 106ms in
6 runs, so a run at a random time fails about once in 34,000 to
300,000. CI's Linux job installs bun 1.4.2 and runs the test in its
Test and Race steps. Found by a review of the tests that compare
renewals to the hour; no CI run was checked for it.

The windows now renew an hour and a day after born, when the host
loaded the plugin, however often an account is read, as a vendor's
windows renew at times of their own. Run once in a process, as CI's
Test and Race steps run it, TestPluginFailover reads its accounts from
the one host started after its plugin.Add, so they renew together. Run
again in one process (-count), a run can still weigh an account by the
previous run's host, born perhaps in the hour before, until that
account fails and is read again: provider's allowances and usage
readings outlive fresh(t) and plugin.Settle. That account then goes
first, and the test's rounds fail it and read it again before the
checks that depend on order, so it still passes; clearing those caches
is left for its own commit.

TestPluginUsage now reads a@fake's card a second time from its refresh
button, 2ms later, and wants each window to renew when it did the first
time, as a vendor's do. That is the check that fails without this
change. Its other bounds (the five hours in 50 minutes or more, the
week in 23 to 25 hours, full@fake's five hours used up) hold while the
host is under 10 minutes old; no other test reads these times.

Verified under a temporary HOME with Go's caches pinned, every test run
with -tags nogui and bun 1.4.2. With 01aaa8e9's index.js put back,
TestPluginUsage fails every run:
  plugin_usage_test.go:109: a@fake's 5 hours renews at 2026-10-07
  12:16:35.567 +0000 UTC read again, at 2026-10-07 12:16:35.539 +0000
  UTC first
and with this one it passes, also with -count=5.

With 01aaa8e9's index.js, TestPluginFailover passes unless an hour
begins between those two reads. failgap.sh, a scratch script, builds
the gateway's test binary once and runs TestPluginFailover with the
old plugin and with this one. It makes the plugin's usage hook tell
its windows from Date.now() plus a shift, set at the first reading so
that an hour begins a set time later (Go's clock is left alone), and
logs each reading. Its copy of the test can hold 2s before the stream
round, which puts its lines 5 further down. With the hour 30ms after
the first reading, the old plugin failed 3 runs of 3, its two reads of
a@fake either side of the hour (at 23 and 42ms, 20 and 34, 18 and 37):
  plugin_failover_test.go:215: fake-claude with the others out: 200
  {...} (tried [b a full])
At 45ms it failed 1 of 3 (44 and 69ms). At 60, 80 and 120ms it passed
9 of 9, a@fake read twice before the hour, or once after it and b@fake
read again. Held 2s before the stream round, with the hour 50 or
1100ms after the first reading, it failed 4 of 4 (a@fake read at 24 to
34ms and 2044 to 2121ms). This plugin passed all 19 of those runs.
That shows only that the test passes with it: its windows are fixed
from born when the host loads it, before failgap's first reading moves
the clock, so the moved hour never reaches them. Held 2s with the hour
30 minutes away, both plugins passed 2 of 2. born.sh, run as a process
per run, logged the host and the born behind each of the plugin's
reads in 20 more runs of this one: 9 a run, each run's from one host
and one born. It logs the plugin's reads, not what routing weighed.

gofmt -l internal/provider/plugin_usage_test.go prints nothing. go vet
./..., go build ./..., GOOS=linux go build -tags nogui ./...,
GOOS=windows go build ./... and GOOS=windows go vet -tags nogui
./internal/provider/ pass. go test -tags nogui ./... passes, 43
packages ok, with bun 1.4.2 on PATH, and so do TestPluginFailover and
TestPluginUsage* with -race -count=20.

* gateway tests: TestTranslatedStreamKeepsClientAlive fails when the stream doesn't close within 5s of its last event, rather than waiting on it with no limit

After the reply's last event the read had no time limit, so a gateway
that never closed the stream would hang the test until the package
timeout (yetone on #1163). The idle timeout still stops at the last
event; a 5s limit on the close follows it.
2026-10-08 10:28:06 +08:00
forever 470d1e71a5 provider: a plugin account's usage reading begun before a request was answered no longer overrules the answer, so a 401's lapse mark isn't taken off by a clean reading already out (TestPluginFailover, about 1 run in 90) (waitingsnow) (#1176)
TestPluginFailover failed about 1 run in 90 at "a refused isn't marked".
With notePluginLapse logged, a failing run read: a background reading of
a's allowance (loginReading, after the "stream" round's StaleAllowance)
began at 31.985; the upstream answered a's request 401 at 32.001 and
notePluginSaid marked a lapsed at 32.003; the reading ended clean at
32.011 and notePluginLapse(200) took the mark off; the test read the
logins at 32.042. Users meet the same: a reading of the Usage page or of
routing's allowances that is out when a request is refused clears the
mark the refusal left, and routing goes on trying the refused account;
one that ends refused marks an account a request has just gone through
on.

What plugin accounts' sign-ins are told of is now counted under
loginsMu (pluginSignInTold, pluginSignInSaid). A request's answer, a
sign-in and the plugin's own signIn events are told now and apply as
before. pluginLoginQuota takes the count before plugin.AccountUsage, and
what its reading found applies only when nothing was told of that
account since (notePluginLapseSince), as Allowances leaves out an
account renewed while it read. A count rather than a time: Windows'
clock moves in ticks of up to 15.6ms, and a reading begun just after an
answer would read as begun with it.

TestPluginUsageReadOlderThanAnswer holds the fake plugin's usage on an
httptest server ($FAKE_USAGE, whose 401 the fake now reads as an expired
sign-in) while a request's answer is noted, both ways, and checks that
readings begun afterwards still mark and clear. Each direction fails
without the fix. TestPluginFailover now reads the allowances the
"stream" round left stale before the "gone" round: the fake's usage
doesn't ask the upstream that refuses, so a reading that asks after the
401 finds the sign-in good and rightly takes the mark off. A review
reproduced that with a delay injected before the count is taken.

Left as it is: a reading whose own renewal of the token tells "renewed"
before it asks skips the "expired" it then finds; the next 401 marks the
account. Not tried with a real plugin account.

Under a temp HOME with Go's caches pinned and bun 1.4.2: gofmt, go vet
-tags nogui ./..., builds for darwin, GOOS=linux -tags nogui and
GOOS=windows (and windows vet of provider and gateway) pass; go test
-tags nogui ./... passes; the changed provider tests and
TestPluginSaysSignIn/TestPluginFailover pass with -race -count=20; the
report's `go test -tags nogui -race -cpu 1,2,4 -count=30 -run
'^TestPluginFailover$' ./internal/gateway/` passed 3 rounds (270 runs),
where it failed 3 of about 54 runs before the fix on the same loaded
machine.
2026-10-08 06:48:41 +08:00
yetone 37f2a2851f gui: a Usage card has a refresh of its own, left of its "Updated" time, that reads that card alone (Hu9956, #840: 在更新时间左侧加一个刷新按钮,鼠标悬停在套餐卡片上时显示,可以单独刷新某个套餐的用量,不用刷新全部) — only the page's Refresh read the allowances again, every card at once. Each card now has a ↻ that opens left of "Updated 3 min ago" (or of a failed reading's reason, or under a balance) as the card is hovered; at rest the line starts where the card's others do. A click POSTs /api/usage/quotas/refresh?provider=&user=: provider.RefreshUsage reads that subscription account, plan key or balance key again (a Claude account by running Claude Code's own /usage, as the page's Refresh does) and puts it in place in the caches, the other cards not asked and kept as they were read; the button turns while it reads. zh, ja, de strings. TestRefreshOneCard (two keys of one relay and another provider: a refresh asks that key alone, the others keep their figures) and the plugin case in TestPluginUsage (one account of three read) fail on the old code; usage-card-refresh.test.cjs (WebKit, en and zh: aligned at rest, left of the time when hovered, a click reads that card alone, turns meanwhile, doesn't scroll; a failed card's own button) fails on the old code. usage-quota-head, usage-checkin, click-scroll, quota-read-at, balance-parts, claude-usage-asked, gui-ja and gui-de pass on WebKit. go vet, builds (darwin, GOOS=linux nogui, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass. 2026-10-05 08:59:47 +08:00
yetone 37809ac722 usage: WorkBuddy's credits are said beside their share — on the Routing page, the Usage page and the menu bar, used or left as the share is (#659)
Moody-Sin: the Routing page showed a WorkBuddy account as "已用 71%" only, which for a plan counted in credits says little. A quota window now carries its own count when the vendor counts in amounts — amount of limit, in unit (QuotaWindow, QuotaSpan for quota --json and /v1/magpie/quotas, and the routing trace's Weighed, from the window its share is of: Allowance.Count). WorkBuddy's built-in fills it from the resource summary, and @magpie-community/opencode-workbuddy-auth 0.1.8 tells it from auth.usage (amount / limit / unit, passed through host.js); the workbuddy mover's min is 0.1.8. The Routing page's stage and its list of what each account did, the Usage page's meter, the menu bar's tooltip and the TUI say "3,550 / 5,000 credits · 71% used", or "1,450 / 5,000 credits · 29% left" with Left on (积分 in Chinese); a window counted in shares alone reads as before.

quota-credits.test.cjs (WebKit, en/zh, used/left, Routing and Usage pages) fails on the old assets and passes; TestQuotaWindowCount, TestAllowanceCount, TestTraceTellsTheCount, TestPluginUsage (the count through the host) and TestWorkBuddyAccounts cover the Go side. Tried with the real WorkBuddy and WorkBuddy AI accounts in a sandbox magpie: moved onto the local plugin, /api/usage/quotas gives 33.29 / 3900 and 13.47 / 2250 credits and the Usage page reads "33.29 / 3,900 积分 · 已用 0.9%", "3,866.71 / 3,900 积分 · 剩余 99.1%" after a click on the count; not moved, the built-in gives the same for WorkBuddy. The Routing page with two real accounts of one provider wasn't tried (one account each). go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass.
2026-10-03 16:45:48 +08:00
yetone 78c32292e4 plugins: a models hook can say its account's sign-in expired, as a built-in whose model list the vendor refused marked the account (要保证插件订阅 provider 跟内置的订阅 provider所有逻辑都要一模一样) — a refusal in provider.models was only logged, so an account Zed or Qoder turned away while listing its models stayed unmarked where the built-in marked it. An error thrown with signIn: expired, kept or renewed now tells magpie, which reads it as it does an answer's X-Magpie-Sign-In. TestPluginModelsSayExpired fails without the change. go vet, go build -tags nogui (GOOS=linux), GOOS=windows go build, go test -tags nogui ./internal/provider ./internal/plugin ./internal/gateway pass. 2026-10-01 06:12:11 +08:00
yetone 1fa21850a3 plugins: a plugin can say its sign-in renewed, and what a usage read means for it (要保证插件跟内置的所有逻辑都要一模一样) — a built-in whose sign-in renewed took the lapse mark off whatever the request then met, and a plugin could only clear it with a success: X-Magpie-Sign-In: renewed now clears it on any status. A usage read marked a plugin account whenever its error said to sign in again and cleared it on any clean read, where Qoder's, Kiro's and others' built-ins marked none from usage: auth.usage may now say signIn: expired, kept or renewed, taken as the header is; one that doesn't say keeps the old reading. TestPluginSaysSignIn and TestPluginUsageLapse fail without the change. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./internal/gateway ./internal/provider ./internal/plugin pass. 2026-10-01 04:46:24 +08:00
yetone 566ae09db1 plugins: a moved provider's accounts keep what the built-in's had (你确保插件版的订阅 provider 和内置的一模一样了吗) — each account of a plugin provider has its own plan's models, not the first account's; a move carries each account's plan and a lapsed mark; a usage read that finds the sign-in gone marks the account and a clean one clears it, a network failure shows the last reading; a plugin that fails to load keeps its providers rather than taking them away; Kiro's API key is named and an OAuth account beside it stays off as in the built-in; Command Code Plan keeps its name. TestUnloadedPluginKeepsProviders, TestPluginUsageLapse, TestPluginAccountModels, TestMovedProviderName, TestKiroMover and the move test fail without the change. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./internal/provider ./internal/plugin ./internal/gateway pass. 2026-10-01 03:27:47 +08:00
yetone 1ce05f0270 plugins: a plugin's models show as the built-in's did (插件版…保持一模一样) — a model a plugin says its plan serves at no cost (WorkBuddy's x0.00 credits) is marked free, as the built-in marked it; Cursor's plugin models not named 1M hold what the catalog knows their base to (GPT-4o 128K, Kimi K2 131K), not Cursor's 200K, as the built-in's list; ZCode's own account moves marked as ZCode's, for the plugin (0.1.2) to read ZCode's own sign-in for each request and follow its switches, and comes back as ZCode's own with nothing saved. TestPluginUsage (free), TestCursorPluginContext and TestZCodeMover fail without the change. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass. 2026-10-01 01:18:04 +08:00
yetone 4a0fbc332c plugins: a built-in moved onto its plugin looks and works as it did (你确保插件版的订阅 provider 和其对应的内置的…都保持一模一样了吗?) — moved onto its plugin, a built-in lost pieces: its tile's name, icon, plans and risk note went to the plugin's own, "Add account" still opened the built-in's sign-in (in the app, on the server and in magpie accounts add), an agent's own sign-in the built-in still found showed a second usage card of the same account, the account's plan badge went, WorkBuddy's daily check-in stopped, the provider's icon and site were the plugin's, WorkBuddy AI's card was named WorkBuddy, and Devin, which had no card of its own, lost the plugin's. Now a moved built-in keeps its tile and risk sheet and signs in through its plugin; its cards take the built-in's place and name, and it has none of its own; the plan the plugin tells stays with the account; WorkBuddy checks in through the plugin, which signs the call; a card is left out only where its built-in's slot showed it. TestMovedBuiltinQuiet, TestWorkBuddyCheckinThroughPlugin, TestMoveToPlugin (Devin-like card) and TestPluginUsage (plan kept) fail without the change. Command Code and Devin moved and back in a sandbox HOME: one card each, plan, name. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass; plugin-move, plugin-signin, commandcode-risk, old-webkit pass; click-scroll's WebKit fails the same on origin/main under this load. 2026-10-01 00:40:22 +08:00
yetone f5f10bda4d plugins: a plugin's accounts show their allowance as the built-ins' do (the user: 所有插件要完整支持显示额度!跟内置一模一样!) — a plugin's auth.usage now tells plan, user, until, renew, balance, resets (count, until, by window, five hours, weekly) and windows (used past 100 when overspent, resets at/in, display, span, the models a window counts or leaves out, aside), and magpie shows them on the usage page, in the menu bar, beside each account and to the gateway, which passes over an account used up for the model asked; a built-in moved onto its plugin keeps its card's name and icon. Also: Command Code's subscription endpoint answering 200 {"success":false} was read as "No plan" by the built-in (and the plugin), it is now unread; a moved Devin was listed twice, the built-in still finding the Devin CLI's own sign-in; and moving back left the plugin's rows in logins.json, listed as empty accounts. Checked in a sandbox HOME with real Command Code and Devin accounts moved onto the community plugins (0.1.1): GOAT, its period, 5 hours / Weekly / Credits for Command Code, Teams, balance and Daily/Weekly for Devin, the same on the built-in after moving back, no plugin rows left. TestPluginUsage, TestCommandCodeSubscriptionUnread and TestMove (the plugin's rows after moving back) fail without the change. go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass (TestClaudeSignedOut and TestUserPath timed out once at load 22, pass alone). 2026-09-30 22:58:56 +08:00