Skip to content

TestLoginUsageClaudeAskedMeanwhile flakes on Windows (3 of 5 on main) #1228

Closed

Description

@yetone

TestLoginUsageClaudeAskedMeanwhile in internal/provider fails on and off on a real Windows 11 machine. It failed 3 of 5 runs on main at fae8b8b, and the same way at #1226's head. It passes on macOS.

Found while reviewing #1226. Its cause isn't known yet; a likely suspect is timing in the test's wait for the in-flight usage read on Windows' coarser timer. To reproduce: cross-compile go test -c -tags nogui ./internal/provider for windows/amd64, then run -test.run TestLoginUsageClaudeAskedMeanwhile -test.count 5 with USERPROFILE/APPDATA/LOCALAPPDATA/TEMP pointed into a sandbox folder.

Activity

  1. ggbdpq commented on Oct 12, 2026

    @ggbdpq
    Contributor

    补充 #1228:提供可复红的修法及完整 standalone patch,不另开重复 issue,也不请求跳过全量验收直接合入。

    提交:c56eccab7ed38fee60b5d1c53db2c45a5aff86b9

    Fork 分支:fix/claude-usage-refresh-generation

    下载完整 patch;正文末尾也附 patch 全文,复现和审阅不依赖本机文件。

    合入时请保留原作者(git am/cherry-pick),或加 Co-authored-by: ggbdpq <ggbdpq@gmail.com>。

    1. 现象

    原有 TestLoginUsageClaudeAskedMeanwhile 的断言是对的:用户在 Claude /usage 读取进行中再次 ask,旧读取不应被保留为一分钟的缓存。Windows 上较粗的时钟更容易暴露失败,但出错的是通用生产代码的事件排序,不是 Windows 分支,也不需要给测试加 sleep 或放宽断言。

    本修复只处理 Claude usage 的三个缓存层及两个 ask 入口,不含 #1491 的 routing/usedCache reset 修法。测试、routing.go、账号文件及依赖版本没有修改。

    2. 最小复现

    原测试不改,Go 1.26.8,在独立且无 Git 祖先的临时 HOME/USERPROFILE/TEMP/TMP 下执行:

    go test -tags nogui -count=50 -timeout=120s -run '^TestLoginUsageClaudeAskedMeanwhile$' ./internal/provider
    

    原产品代码真实失败:

    --- FAIL: TestLoginUsageClaudeAskedMeanwhile (0.19s)
        claude_usage_fallback_test.go:263: a reading from before the ask was kept
    FAIL
    FAIL    github.com/yetone/magpie/internal/provider 11.434s
    

    临时诊断在缓存发布边界记录到真实时间相等(诊断已从最终源码移除):

    DIAGNOSTIC equal wall-clock tick: asked = 1791790183943949300 start = 1791790183943949300
    --- FAIL: TestLoginUsageClaudeAskedMeanwhile (0.15s)
        claude_usage_fallback_test.go:263: a reading from before the ask was kept
    

    running/release channel 已保证第二次 ask 晚于原读取开始,时钟却不能区分它们。对最新基线 8f2a314cf9779151b9caa5d60183e594239d0621 加最终修法,原测试 count=50 通过;只 stash 三份产品文件后 count=30 再次得到同一真实 FAIL(7.077s),保留原测试与文档,再 pop 恢复修法,count=5 通过(2.137s)。

    3. 根因

    • loginReading 在 goroutine 内记录 start,发布判断为 claudeAsked <= start.UnixNano()。同 tick 的晚一次 ask 等于 start,旧读取被错误保留。
    • SubscriptionUsage 用 claudeAsked > start.UnixNano() 清零 cache age,同 tick 时漏掉该失效。
    • claudeWindows 用 asked > entry.tried.UnixNano() 判断新 ask,相同时间戳也不能表达不同次请求。
    • 两个产品写入口是 AskClaudeUsage 及 Claude card 的 RefreshUsage,此前都只 Store 当前 UnixNano。

    时间戳适合 cache age、idle、floor 和窗口调度,不足以作为请求事件序号。历史 PR #1137 也记录过这一时钟原因,但没有交付本路径修法;#1093、#1092、#1078、#1071 仍将它列为已有 #1228 的红项。本次补充的增量是覆盖兄弟路径的 patch、产品-only 复红及最终回归证据,不宣称首次发现该根因。

    最后复查时 #1228 仍 OPEN,无 assignee、无评论;最新 upstream 的三个产品文件没有吸收本修法,没有发现相关完整修复 PR 或维护者接手说明。

    4. 修法与保留语义

    新增 claudeAskGeneration,两个 ask 入口共享 markClaudeUsageAsked:

    1. generation 的递增持有 subscription cache → login cache 两把锁;各自的读快照及发布检查/写入在自己的缓存锁内完成,不能“先查到旧 generation,再在 ask 之后发布”。只在锁内更新两个原子值,不运行 CLI、网络或回调,没有发现反向持有这两把缓存锁的路径。
    2. LoginUsage、SubscriptionUsage 在 launch goroutine 前捕获 generation;晚一次 ask 令旧账号读数不被缓存,或让页面缓存立即保持 due。
    3. Claude /usage entry 保存该次 run 的 generation,成功、失败和 headers 更新都保留正确的 answered generation。一次 run 期间再来 ask 不会被吞掉。

    首次未读仍由 tried.IsZero() 判定;已读且无新 ask 仍复用;claudeAsked 的 timestamp 和 zero sentinel 保留。30 秒 per-account floor、tried/wait 的 5–15 分钟调度、idle/used-since、窗口时间、错误 fallback、saved-account 只能由自己的 card refresh 读取等规则均保留。保存账号不会被切换,账号所属路径及 Windows 的配置路径没有改动。

    最终自审还补上 generation 更新与缓存发布的锁内串行化;原版仅靠原子 Load/Add,不能将“比较后写缓存”作为完整临界区。该补强与整套最终产品代码已经重新跑过下表的回归/race。

    依照仓库 subsystem 文档规则,同一提交对 docs/subsystems/providers-accounts.md 增加一条最小的顺序及锁所有权说明。最终提交仅三个产品文件及该设计页,28 insertions / 9 deletions;没有缓存、构建产物、测试或无关修改入库。

    5. 验证及未跑项

    最终独立提交基于 8f2a314cf9779151b9caa5d60183e594239d0621。先前 3d46d3a6 分支/commit 保留,未重写它。

    所有 go 验证使用外部独立 E 盘 sandbox,设置 HOME、USERPROFILE、TEMP、TMP、APPDATA、LOCALAPPDATA、XDG_CONFIG/DATA/STATE_HOME;GOPATH、GOMODCACHE、GOCACHE 固定原有缓存,不读真实账号。race 使用已安装的 MinGW gcc、进程级 PATH/CC/CGO_ENABLED=1;cross vet/build 使用 CGO_ENABLED=0。没有修改全局环境、作者身份或依赖版本。

    扩展 regex E:

    Test(LoginUsage|ClaudeWindows|ClaudeUsage|ClaudeHeader|ClaudeHeard|ClaudeWarm|SubscriptionUsage|RefreshUsage|RefreshOneCard|RefreshSavedClaudeUsage|SavedClaudeUsage|UsageClaudeVia)
    
    实际命令/步骤 结果
    原测试修前 -count=50 -timeout=120s 真实 FAIL,11.434s
    临时同 tick 诊断,原测试 -count=20 真实 FAIL,记录 asked == start;诊断不入库
    最新基线、最终修法,原测试 -count=50 -timeout=120s PASS,12.317s
    git stash push -- internal/provider/login_usage.go internal/provider/subscription_usage.go internal/provider/quota_refresh.go 后原测试 -count=30,随后 pop 真实 FAIL,7.077s;恢复修法
    pop 后原测试 -count=5 -timeout=90s PASS,2.137s
    主线程独立沙盒复核,原测试 -count=5 -timeout=90s PASS,2.476s;最终提交未再改产品代码
    go test -tags nogui -race -count=5 -timeout=240s -run E ./internal/provider,最终版本 PASS,122.691s
    早先相关 usage 子集 -count=3 PASS,51.536s;不是最终全包测试
    Windows go vet -tags nogui ./internal/provider,最终版本 EXIT 0
    GOOS=linux go vet -tags nogui ./internal/provider,最终版本 EXIT 0
    GOOS=darwin go vet -tags nogui ./internal/provider,最终版本 EXIT 0
    Windows 默认 go build -o <output> .,最终版本 EXIT 0
    GOOS=linux go build -tags nogui -o <output> .,最终版本 EXIT 0
    GOOS=darwin go build -tags nogui -o <output> .,最终版本 EXIT 0;不是 Darwin GUI build
    gofmt -l 三个修改的 Go 文件 0 输出
    staged diff --check / name-status 无错误,仅三个产品文件和 owning 设计页
    独立 format-patch 的 reverse apply check EXIT 0

    **未跑:**provider 全包、全量 go test -tags nogui ./...、全仓 vet、真实 Claude account、GUI 双引擎五语言、Darwin GUI build、Linux/Darwin 实机运行。此前整包/全量仍有其他未解决红项;本次没有靠定向绿声称全量验收通过。不请求直接关闭 #1228,合入前仍需维护者整合后的完整验收。

    Generated-by: Factory Droid (GPT-6.1 Sol)

    完整 standalone git format-patch
    From c56eccab7ed38fee60b5d1c53db2c45a5aff86b9 Mon Sep 17 00:00:00 2001
    From: ggbdpq <ggbdpq@gmail.com>
    Date: Mon, 12 Oct 2026 16:29:02 +0800
    Subject: [PATCH] provider: a Claude usage ask invalidates an older read even
     in the same clock tick
    
    Refs #1228. The original test fails with "a reading from before the ask
    was kept". A temporary diagnostic measured identical UnixNano values for
    the read's start and the later ask; that diagnostic is not shipped.
    
    What changed:
    - Count both whole-page and manual-card Claude asks with a generation.
      Advance it under subscription then login cache locks, so publication
      cannot check the old generation and then commit after the ask.
    - Capture the generation before launching LoginUsage/SubscriptionUsage;
      reject an older account read or leave the cards due for another read.
    - Record each Claude /usage run's ask generation. Keep timestamps, the
      zero sentinel, floor, idle behavior, saved-account scope and fallbacks.
    - Document ordering and lock ownership in the providers/accounts design.
      No tests, routing.go, account files or dependency versions changed.
    
    Run on upstream 8f2a314cf9779151b9caa5d60183e594239d0621:
    - Original test unchanged: count=50 passes (12.317s). Product-only stash
      restores real red, count=30 (7.077s); pop then count=5 passes (2.137s).
    - Expanded usage/header/saved-card paths race count=5 pass (122.691s),
      Go 1.26.8 and installed MinGW gcc, external sandbox home/temp and
      pinned Go caches.
    - Provider nogui vet passes on Windows, Linux, Darwin. Windows default
      build and Linux/Darwin nogui builds pass. gofmt and staged diff clean.
    - Provider full package and full ./... were not rerun; real account,
      GUI engines/languages, Darwin GUI build and cross-OS runtime not tried.
      Targeted evidence only, not a claim of complete merge acceptance.
    
    Generated-by: Factory Droid (GPT-6.1 Sol)
    
    Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
    ---
     docs/subsystems/providers-accounts.md   |  1 +
     internal/provider/login_usage.go        |  4 ++--
     internal/provider/quota_refresh.go      |  2 +-
     internal/provider/subscription_usage.go | 30 ++++++++++++++++++++-----
     4 files changed, 28 insertions(+), 9 deletions(-)
    
    diff --git a/docs/subsystems/providers-accounts.md b/docs/subsystems/providers-accounts.md
    index ba463dbf..10f733ad 100644
    --- a/docs/subsystems/providers-accounts.md
    +++ b/docs/subsystems/providers-accounts.md
    @@ -110,6 +110,7 @@ Account discovery can be turned off (#1535): Settings → Privacy → *Subscript
     
     - Each refresh token has exactly one holder. Vendors rotate tokens on refresh, so two copies of one token would sign each other out. `savedTokenMu` stops two requests refreshing one saved account at once.
     - A Claude subscription account is used only for Claude Code, through the genuine Claude Code binary (see [Claude subscription bridge](claude-subscription-bridge.md)).
    +- Claude usage asks are ordered by `claudeAskGeneration`, not wall-clock timestamps. `AskClaudeUsage` and a Claude card's `RefreshUsage` advance it while holding the subscription cache then the login cache lock (`markClaudeUsageAsked`). Each cache captures it under its own lock before launching a read and checks it under that lock when publishing: a newer ask prevents `LoginUsage` from keeping the older reading and leaves `SubscriptionUsage` due to read again. Each `claudeWindows` entry records the generation its run answered. The timestamp's zero sentinel, `tried`/`wait`, the 30-second floor, idle behavior and saved-account scope remain unchanged. [`login_usage.go`](../../internal/provider/login_usage.go), [`subscription_usage.go`](../../internal/provider/subscription_usage.go), [`quota_refresh.go`](../../internal/provider/quota_refresh.go).
     - A saved Claude account's allowance is read only when its own card is refreshed (#1630; `savedRefresh`: `RefreshUsage` for that account, so `cardRefresh.wants` it and the read is asked). The real Claude Code runs `/usage` with `CLAUDE_CONFIG_DIR` set to the account's `claudeSavedDir` (`readSavedClaudeUsage`, through the gateway's `claudeUsage`), so magpie never calls Anthropic with the token itself and the account Claude Code is signed in to is not switched. The Usage page opened, `magpie quota` and `magpie accounts` ask every card and so start no saved account's Claude Code: they keep its last reading, or `errClaudeSaved` before one. Background reads keep the last reading, and the 30-second per-account floor holds for repeated refreshes. Claude Code may refresh the account's token in that directory while it runs (its own lock file keeps two Claude Codes in one directory from refreshing at once). So `inSavedClaudeDir` holds `claudeDirRun(user)` for reading while it runs, which `switchSavedLogin` takes for writing first, and after the run `syncSavedClaude` copies a rotated sign-in back into `logins.json` under `savedTokenMu` and `loginsMu`. A saved account's warm-up runs the same way. [`claude_usage_saved_test.go`](../../internal/provider/claude_usage_saved_test.go) covers the refresh, the other accounts left alone, the floor, background reads, a switch waiting for the run and the rotated sign-in kept; `TestClaudeUsageRunsInSavedAccountDirectory` checks the directory the command runs in.
     - Claude Code's `/usage` prints a reset with no year when it falls this year ("Oct 9, 2:59pm (UTC)"), or, in older versions, as a time alone ("3pm"). `claudeResetTime` reads it as the end of its window, so never further ahead than the window's length (5 hours for the session, 7 days for a week) and `claudeResetSlack`: a date is in the first of last year, this year and next that is less than a day past and no further ahead, a time alone the next one no further ahead. Where none is, as for a reading days old, it is the latest one already past, so the window reads as renewed rather than full for a year. Where the clocks go back and show a time twice, the reading not yet past is taken. A week's time alone read just after its reset still reads as tomorrow's.
     - A Copilot account is known by its GitHub login and host (`CopilotAccountName`, #1220): `mona` on github.com, `mona@acme.ghe.com` on an enterprise's GHE.com. That name is its `logins.json` user, its row in every list, its usage card and its per-account routing settings, and it is what `SwitchLogin`, `SetLoginOn` and `ForgetLogin` take, so one login on two hosts is two accounts, neither written over by the other nor dropped as the editors' own. One saved before on an enterprise's host under its login alone is read under its name with the host (`copilotSavedName`), keeping its id; the editors' own remembered so is renamed in place, still hidden if it was (`copilotRenamed`). Either rename takes the account's per-account settings in `providers.json` (proxy, models, cap, concurrency) from the bare login to the new name before the new name is written to `logins.json` (`renameSettled`): a setting the new name has already stays, and while `providers.json` can't be read or written the account stays saved under its old name, its settings with it. `magpie accounts switch|forget copilot` takes the full name, the login with `--host <name>.ghe.com`, or the login alone when it names one account.
    diff --git a/internal/provider/login_usage.go b/internal/provider/login_usage.go
    index 2ecee8f2..9465e35d 100644
    --- a/internal/provider/login_usage.go
    +++ b/internal/provider/login_usage.go
    @@ -198,8 +198,8 @@ func loginReading(ctx context.Context, l Login) loginUsageEntry {
     		}
     		c.pending[key] = r
     		ctx, _ = quotaReading(ctx)
    +		asked := claudeAskGeneration.Load()
     		go func() {
    -			start := time.Now()
     			// read for all who wait for it: no one's ctx cuts it short, but
     			// it is bounded as the Usage page's refresh is
     			rctx, cancel := context.WithTimeout(context.WithoutCancel(ctx), subscriptionTimeout)
    @@ -211,7 +211,7 @@ func loginReading(ctx context.Context, l Login) loginUsageEntry {
     			// (AskClaudeUsage), as SubscriptionUsage reads it: neither is kept
     			if c.pending[key] == r {
     				delete(c.pending, key)
    -				if l.Agent != "claude" || claudeAsked.Load() <= start.UnixNano() {
    +				if l.Agent != "claude" || claudeAskGeneration.Load() == asked {
     					if c.m == nil {
     						c.m = map[string]loginUsageEntry{}
     					}
    diff --git a/internal/provider/quota_refresh.go b/internal/provider/quota_refresh.go
    index da55ef84..413a834d 100644
    --- a/internal/provider/quota_refresh.go
    +++ b/internal/provider/quota_refresh.go
    @@ -104,7 +104,7 @@ func RefreshUsage(ctx context.Context, provider, user string) {
     	ctx, seq := quotaReading(ctx)
     	if provider == "claude" {
     		// Claude Code's /usage, run when the user asks (claudeWindows)
    -		claudeAsked.Store(time.Now().UnixNano())
    +		markClaudeUsageAsked()
     	}
     	var wg sync.WaitGroup
     	wg.Add(3)
    diff --git a/internal/provider/subscription_usage.go b/internal/provider/subscription_usage.go
    index edb2c986..3fcc8807 100644
    --- a/internal/provider/subscription_usage.go
    +++ b/internal/provider/subscription_usage.go
    @@ -222,15 +222,15 @@ func SubscriptionUsage(ctx context.Context) []SubscriptionQuota {
     			bg = Asked(bg)
     		}
     		readCtx, seq := quotaReading(bg)
    +		asked := claudeAskGeneration.Load()
     		go func() {
    -			start := time.Now()
     			out := fetchSubscriptionUsage(readCtx)
     			noteDailyCredits(out, time.Now())
     			noteQuotaHistory(out, time.Now())
     			c.Lock()
     			c.data = cacheCards(c.data, out, seq, false)
     			c.at, c.pending = time.Now(), nil
    -			if claudeAsked.Load() > start.UnixNano() {
    +			if claudeAskGeneration.Load() != asked {
     				c.at = time.Time{} // asked meanwhile: read again
     			}
     			c.Unlock()
    @@ -616,6 +616,7 @@ type claudeUsageEntry struct {
     	ws    []QuotaWindow
     	heard time.Time     // when Claude Code last told it, answering
     	tried time.Time     // when /usage was last run, answered or not
    +	asked uint64        // the ask generation that run answered
     	wait  time.Duration // how long after tried it runs again unasked
     	err   error         // what the last run said, when it failed
     	whole bool          // /usage was read: ws has every window the account has
    @@ -677,12 +678,28 @@ var claudeUsedSince = func(t time.Time) bool {
     // nanoseconds; zero: never).
     var claudeAsked atomic.Int64
     
    +// claudeAskGeneration orders asks against readings, even when both happen
    +// in one wall-clock tick. tried and wait still schedule the next /usage.
    +var claudeAskGeneration atomic.Uint64
    +
    +func markClaudeUsageAsked() {
    +	// Serialize the ask with both caches' snapshots and publications:
    +	// neither may observe the old generation, then publish after this ask.
    +	// When both locks are needed, the subscription cache comes first.
    +	subscriptionUsageCache.Lock()
    +	loginUsageCache.Lock()
    +	claudeAsked.Store(time.Now().UnixNano())
    +	claudeAskGeneration.Add(1)
    +	loginUsageCache.Unlock()
    +	subscriptionUsageCache.Unlock()
    +}
    +
     // AskClaudeUsage is the user asking to see Claude's usage — opening the
     // Usage page, refreshing it, `magpie quota` — so Claude Code's /usage is
     // run at once rather than when the next unasked reading is due
     // (claudeUsageWait); the next SubscriptionUsage waits for it.
     func AskClaudeUsage() {
    -	claudeAsked.Store(time.Now().UnixNano())
    +	markClaudeUsageAsked()
     	AskUsage()
     	l := &loginUsageCache
     	l.Lock()
    @@ -727,11 +744,11 @@ func claudeWindows(ctx context.Context, user string, active bool) ([]QuotaWindow
     	key := strings.ToLower(user)
     	c := &claudeUsage
     	now := time.Now()
    -	asked := claudeAsked.Load()
    +	asked := claudeAskGeneration.Load()
     	c.Lock()
     	e, ok := c.m[key]
     	c.Unlock()
    -	due := e.tried.IsZero() || asked > e.tried.UnixNano() ||
    +	due := e.tried.IsZero() || claudeAsked.Load() != 0 && asked > e.asked ||
     		active && now.Sub(e.tried) >= e.wait && (e.heard.After(e.tried) || claudeUsedSince(e.tried))
     	// nor, with allowances read only when asked, one nobody asked for
     	read := (active || savedRefresh(ctx, user)) && due && now.Sub(e.tried) >= claudeAskFloor && !holding(ctx)
    @@ -746,6 +763,7 @@ func claudeWindows(ctx context.Context, user string, active bool) ([]QuotaWindow
     			c.m = map[string]claudeUsageEntry{}
     		}
     		e.tried, e.wait = now, claudeUsageWait()
    +		e.asked = asked
     		c.m[key] = e
     	}
     	c.Unlock()
    @@ -809,7 +827,7 @@ func claudeWindows(ctx context.Context, user string, active bool) ([]QuotaWindow
     	if c.m == nil {
     		c.m = map[string]claudeUsageEntry{}
     	}
    -	c.m[key] = claudeUsageEntry{at: now, ws: ws, heard: e.heard, tried: now, wait: e.wait, whole: true}
    +	c.m[key] = claudeUsageEntry{at: now, ws: ws, heard: e.heard, tried: now, asked: asked, wait: e.wait, whole: true}
     	c.Unlock()
     	// as of now, as a kept reading is: /usage gives a reset to the minute
     	// and can lag it, so a window it still counts full may have started
    -- 
    2.55.0.windows.4
    
    
  2. yetone commented on Oct 12, 2026

    @yetone
    OwnerAuthor

    @ggbdpq 回复晚了抱歉,补丁我们正在审阅。

    main 上 internal/provider/login_usage.go:214 仍然是 claudeAsked.Load() <= start.UnixNano():同一个时钟刻度内的两次刷新会互相漏掉,你分支上的 c56ecca(fix/claude-usage-refresh-generation)改成按代次判断,是对的方向。合入时会保留你的作者身份。

    这个 issue 保持打开,合入后会贴出提交和版本。

  3. yetone commented on Oct 12, 2026

    @yetone
    OwnerAuthor

    @ggbdpq 感谢补丁!已合入 main,作者署名保留为你:

    • 44b5b2b provider: Claude 用量的"刷新"请求即使和正在进行的读取落在同一个时钟 tick,也会让旧读取作废
    • 861c2da(我补的测试):LoginUsage、SubscriptionUsage 和 claudeWindows 三处都测了"在读取开始的同一 tick 里点刷新",旧代码上三个测试都是红的,去掉你的修复也会变红

    会随下一个版本发布。先不关 issue,等你确认新版本里正常再关。

  4. yetone commented on Oct 13, 2026

    @yetone
    OwnerAuthor

    A trace of the cause from @TryWorld2026, in #1071 and #1078 (not yet checked against the current test): loginReading registers its pending read and starts the goroutine that reads, and that goroutine takes start := time.Now() when it is first scheduled, not when it is started. When it is scheduled after the test's second AskClaudeUsage(), its start lands after the ask, so the guard keeps it as fresh, as the rule says. The <-running the test waits on is signalled by SubscriptionUsage's own Claude read, not by this goroutine, so the test reaches its assertion ("nothing is cached at all") without waiting for it. They saw it fail 2 of 20 under -race -count=20 on a Windows box at f1821f4, with no race reported. If that holds, the fix is to take the start time before the goroutine is started, or to make the test wait for loginReading's read too.

  5. yetone commented on Oct 13, 2026

    @yetone
    OwnerAuthor

    Correction to the comment above: on main the guard no longer compares a start time. 44b5b2b (10-12) takes claudeAskGeneration before the read's goroutine is started and keeps the reading only when no ask came in between, which removes the scheduling window that trace describes. This needs a fresh Windows run at current main before it is closed.

  6. ggbdpq commented on Oct 13, 2026

    @ggbdpq
    Contributor

    已在当前 main(e1b70c13,v0.1.1222-10-ge1b70c13)Windows 上确认(go 1.26.5,-tags nogui):

    • TestLoginUsageClaudeAskedMeanwhile:-count=50 全部通过
    • same-tick 回归(TestLoginUsageClaudeAskedOnTheReadsTick / TestSubscriptionUsageClaudeAskedOnTheReadsTick / TestClaudeWindowsAskedOnTheRunsTick):各 -count=20 全部通过

    本机已装 Magpie 0.1.1220 也含修复。我这边没有别的问题,可以关单。

  7. yetone commented on Oct 13, 2026

    @yetone
    OwnerAuthor

    @ggbdpq 感谢在 Windows 上的复测(e1b70c13 上 TestLoginUsageClaudeAskedMeanwhile -count=50 和三个同 tick 回归测试都通过),也再次感谢你的补丁(44b5b2b96)。按你的确认关闭这个 issue。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions