Skip to content

fix: OpenCode Go session-cookie mode and DeepSeek/Moonshot dashboard tabs - #139

Merged
prakersh merged 15 commits into
mainfrom
fix/opencode-console-cookie
Sep 28, 2026
Merged

prakersh merged 15 commits into
mainfrom
fix/opencode-console-cookie

Conversation

@prakersh

Copy link
Copy Markdown
Contributor

Two dashboard/provider fixes, one commit each.

OpenCode Go session-cookie mode (fixes #134)

OpenCode retired /workspace/<id>/go (it now redirects to login), so cookie users only saw opencode: unauthorized. Session-cookie mode now reads GET /console/api/go/status with the __Host-console_session cookie and an x-org-id header, the same endpoint the service-account key mode from #136 uses. The dead HTML scraper is removed.

  • The cookie setting accepts a bare value, name=value, a full cookie header, or a copied Cookie: ... line. A rejected cookie gets an error that names the cookie to paste.
  • Meters are stored in USD for both modes (cards show $3.80 / $30.00 plus the percent). Utilization is unchanged, so charts and cycle history are unaffected.
  • Behaviour change: absolute notification overrides for OpenCode now compare in dollars, and snapshots from before the upgrade stay in percent, so logging history briefly mixes units. Both are documented in docs/OPENCODE_SETUP.md.

DeepSeek and Moonshot tabs (fixes #137)

getCurrentProvider() had no branch for either grid, so both tabs resolved to Synthetic.

  • Balance cards, a balance chart (currency axis/tooltips, uncapped), logging history and a Balance cycle overview for both tabs.
  • DeepSeek hardcoded CNY in eight places, so USD accounts got empty summary, insights and cycles. It now uses the requested currency, else the latest snapshot's.
  • Logging history uses the shared crossQuotas row shape; cycle rows carry cycleId; the duplicated cycle-overview builders are one helper per provider.

Not in scope: the All-providers view still does not render balance providers (DeepSeek, Moonshot, OpenRouter), and Moonshot's API reports no currency, so its cards show bare amounts.

Testing

  • ./app.sh --test (race + coverage) passes; go vet clean.
  • New tests: cookie header forms, rejected-cookie hint without echo, redirect not followed, per-credential reuse, USD mapping; DeepSeek USD fallback across insights/summary/cycles/overview, logging-history row shape for both providers, dashboard wiring.
  • Checked in a browser against a seeded local server (no agents): DeepSeek in USD and CNY, Moonshot, and OpenCode currency cards.

Comment thread tests/e2e/conftest.py Fixed
Comment thread tests/e2e/conftest.py Fixed
OpenCode retired the /workspace/<id>/go page: it now redirects to login,
so cookie users only ever saw "opencode: unauthorized" (#134).

Session-cookie mode now calls GET /console/api/go/status with the
__Host-console_session cookie and an x-org-id header, the same endpoint
the service-account key mode (#136) uses. The dead HTML scraper is
removed.

- The cookie setting accepts a bare value, name=value, a full cookie
  header, or a copied "Cookie: ..." line. A rejected cookie gets an
  error that names the cookie to paste; bodies and cookies are never
  echoed.
- Meters are stored in USD (currency format) for both modes instead of
  a percent of 100. Utilization is unchanged, so charts and cycle
  history are unaffected. Absolute notification overrides now compare
  in dollars, and older snapshots stay in percent; both are called out
  in docs/OPENCODE_SETUP.md.
- Status responses are reused per credential for 60s; 3xx is treated
  as unauthorized and never followed.
- Docs, README, .env.example and the settings hints describe the
  session cookie instead of the old auth cookie.
getCurrentProvider() had no branch for the DeepSeek or Moonshot grids,
so both tabs resolved to "synthetic": no cards, Synthetic chart
labels, and failed insights.

Dashboard:
- Detect both tabs; render Total/Granted/Topped Up (DeepSeek) and
  Available/Voucher/Cash (Moonshot) balance cards, updated in place.
- Balance chart with currency-formatted axis and tooltips, uncapped
  y-axis, and legend-hidden series kept hidden across refreshes.
- Logging history and a Balance cycle overview, formatted as amounts.

Backend:
- DeepSeek hardcoded CNY in eight places, so USD accounts got empty
  summary, insights and cycles. Use the requested currency, else the
  latest snapshot's.
- Logging history for both providers now uses the shared crossQuotas
  row shape the table reads, one column per balance field.
- Cycle rows carry cycleId, and the four duplicated cycle-overview
  builders are one helper per provider.
…per log

- Before the first poll the DeepSeek/Moonshot current payload is a zero
  placeholder with no status. Show "No balance data yet" instead of
  zero-balance cards marked Healthy.
- DeepSeek logging history labels rows with one currency, so rows taken
  in another currency (an account that switched CNY/USD) are skipped
  instead of shown with the wrong symbol.
Windows:
- getCredentialsFilePath and Setsid tests are Unix-only; move them
  behind !windows so the test binaries build on Windows (#134).
- New "Windows" CI job on windows-latest: go vet, go test and the new
  provider e2e test. The daemon logs are printed on failure.

E2E:
- The mock server serves the OpenCode Go console status API (session
  cookie + x-org-id or Bearer, rejecting the old auth cookie) and the
  DeepSeek and Moonshot balance APIs.
- OPENCODE_GO_BASE_URL, DEEPSEEK_BASE_URL and MOONSHOT_BASE_URL point
  those clients at a proxy or the mock (same pattern as
  COMMANDCODE_BASE_URL).
- test_provider_balances.py runs a dedicated daemon with real agents
  and checks the OpenCode dollar cards (#134) and the DeepSeek and
  Moonshot cards, chart and logging rows (#137).
- The harness uses tempdir paths, .exe names and USERPROFILE, and logs
  the daemon to a file instead of an unread pipe.

Credential safety: the Anthropic agent wires keychain credential refresh
even for an explicit token, so the fixture now pins Anthropic to
statusline mode (no keychain read, no API, no refresh). Cursor tokens are
also read from the macOS Keychain and can be refreshed, so the suite
refuses to run on a non-CI Mac unless ONWATCH_E2E_ALLOW_HOST=1.
… bugs it hid

The first go test ./... on windows-latest failed in 13 of 20 packages
and hung two. Most failures were tests that only set HOME, which
Windows ignores in favour of USERPROFILE; that also meant tests read
and wrote the runner's real profile, including
%USERPROFILE%\.claude\.credentials.json.

Credential safety:
- New internal/testutil/testhome: SandboxHome (TestMain) and
  SetTestHome point HOME, USERPROFILE and LOCALAPPDATA at a temp dir
  and clear provider path overrides. api, agent, web, cmd/onwatch and
  update use it instead of per-package copies.
- In api test mode the Claude credentials-file helpers refuse the real
  home on Windows and Unix; cmd/onwatch and web tests now enable test
  mode (web tests could previously query the real macOS Keychain).
- cmd/onwatch tests use a network-free updater and ONWATCH_STAR=no;
  the update test replaces a copied binary, not the running test.

Windows product fixes:
- Statusline bridge: only installed when Git Bash exists (Claude Code
  runs the command in PowerShell otherwise), an unrunnable leftover is
  removed, and the Windows command uses the absolute data dir. A
  bridge synced from another OS is left alone instead of rewritten on
  every health check.
- Antigravity: the PowerShell/CIM probe matched its own process;
  netstat parsing no longer requires the English LISTENING (German
  Windows), preferring LISTENING rows when present.
- onwatch update could not see or stop a running daemon (signal 0,
  SIGTERM and ps); uses processAlive/stopProcess and the image name.
- Grok binary fallback, DB migration and Gemini detection built paths
  from $HOME; they use os.UserHomeDir.
- procscan's Windows tasklist check honours the scan timeout.
- perf-monitor: PID file location/format, process liveness (always
  false before, on every OS), .exe name, stop via Kill on Windows, no
  os.Chdir, and it never signals a stale PID that is not onWatch.

Test portability: Unix permission checks only on Unix, filepath-built
expectations, pipes drained while writing (fixes the perf-monitor
hang), fakes via command hooks instead of #!/bin/sh scripts, and child
processes stopped with Kill where signals do not exist.
All jobs run in parallel on separate runners, so wall time is the
slowest job rather than the sum:

- Lint: gofmt must be clean; go vet for linux, darwin and windows, and
  with -tags menubar for linux and windows.
- Test: go test ./... on Linux, macOS and Windows (-race on Linux and
  macOS), plus the menubar-tagged tray packages.
- Test (Alpine): the full suite on musl/busybox, the Docker base.
- Build: all six release binaries exactly as release.yml builds them.
- E2E: the full Playwright suite on Linux, macOS and Windows.
- Installer: install.sh on Linux and macOS, install.ps1 on PowerShell
  5.1 and 7.
- Docker: build both images and check each serves /login.
- Nix: nix build .#onwatch (catches a stale vendorHash).
- CI OK: fails if any job did not pass.

A new push to a PR cancels its stale run.

Also address the code-quality bot on tests/e2e/conftest.py: close the
parent's log handle once the daemon starts, and explain the ignored
cleanup error.
CI now fails on unformatted Go files. No functional changes.
main now passes the per-connection pragmas in the DSN, but a path that
already carried a query (e.g. file:/data/onwatch.db?_txlock=immediate)
was left untouched, so every connection again ran with busy_timeout=0
and foreign keys off. Append the pragmas the caller did not set itself
instead.
tr -dc ... </dev/urandom | head -c 12 never ends when SIGPIPE is
ignored (CI runners, Node-spawned terminals such as VS Code's): BSD tr
only checks write errors at end of input, and /dev/urandom never ends.
Read a bounded 512 bytes instead.
…he profiles dir

- Process name and zombie checks read /proc on Linux instead of ps,
  which is absent in the Nix build sandbox and the distroless image,
  and busybox's has no -p. macOS still uses ps.
- listCodexProfiles only treats a missing directory as "no profiles".
  Windows reports a file in the directory's place as "path not found",
  which was silently read as an empty list.
- perf-monitor: a local request can measure 0s on Windows' coarse
  clock, so check the duration aggregates are consistent instead of
  strictly positive.
- Alpine: remove /.dockerenv and run as a non-root user, so the job
  tests the musl/busybox userland rather than onWatch's Docker mode,
  and permission tests are meaningful.
- E2E harness: each daemon gets its own LOCALAPPDATA. On Windows the
  test PID file lives there, so the second daemon stopped the first and
  every later test errored.
- Provider e2e: wait with function-form wait_for_function (the
  dashboard's CSP forbids the eval Playwright uses for expression
  strings), wait for the logging row value rather than the template
  placeholder, and put URL, grid, page errors and API state in the
  failure message.
- Update two stale assertions to the current UI: "Updated HH:MM:SS"
  and the logging-history columns (#, Time, one per quota).
- On failure, upload Playwright traces and print the daemon's own log.
comm is the name the process was started as, which is what ps -o comm=
reported. exe resolves symlinks, so onWatch launched through a symlink
named onwatch (or the tests' nc listener) was no longer recognised.
initSettingsPage attached the password handler only after awaiting the
menubar and settings loads, so clicking "Update Password" during a slow
load did nothing (the Windows e2e run hit this every time). Wire it
first, and mark .settings-page data-ready once every control is wired;
the e2e settings fixtures now wait for that marker.
@prakersh
prakersh force-pushed the fix/opencode-console-cookie branch from 0e36e1f to 94582f9 Compare September 28, 2026 09:22
The read-only scope check built file:C:%5C... for Windows paths, so it
always failed: profiles without Mistral cookies still reached the
credential store, and imports failed. Use file:///C:/...?mode=ro.

Also make two tests portable: the browser roots test used a path that is
not absolute on Windows, and the settings init test did not handle a
CRLF checkout of app.js.
The handler can still be serving a request after Run returns, so the
plain int counter raced with the assertion under -race.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant