Skip to content

Add Cursor subscription provider via official sdk-bridge - #24

Draft
weselben wants to merge 29 commits into
mainfrom
feat/cursor-grok-provider
Draft

Add Cursor subscription provider via official sdk-bridge#24
weselben wants to merge 29 commits into
mainfrom
feat/cursor-grok-provider

Conversation

@weselben

Copy link
Copy Markdown
Owner

TL;DR

Cursor subscriptions bundle Grok 4.5/4.6 and Composer 2.5 at flat rate, but GoModel had no way to bill inference against them. This PR adds a cursor provider: GoModel spawns the official cursor-sdk-bridge subprocess and serves Cursor models through the standard OpenAI-compatible API.

Files to review (27, +4132 / -2):

File Why
internal/providers/cursor/cursor.go (start here) Provider core: six core.Provider methods, lazy bridge start, stateless chat mapping.
internal/providers/cursor/connect_transport.go Hand-rolled Connect-over-HTTP/1.1 client, JSON encoding. No protobuf or connectrpc dependency.
internal/providers/cursor/bridge_manager.go Subprocess lifecycle: spawn, ready-line handshake, scrubbed environment, shutdown.
internal/providers/cursor/chat_stream.go Connect envelope → OpenAI SSE converter.
internal/providers/cursor/cursor_wire.go All sdk.v1 JSON field mappings in one file. Schema drift means a one-line fix.
internal/providers/init.go InitResult.Close() now closes providers that implement io.Closer.
docs/providers/cursor.mdx (new) Configure, models, limits, billing warning, ToS note.
tests/contract/cursor_test.go (new) Four replay cases with fixtures and goldens.

How

  • Transport. The bridge serves Connect-over-HTTP/1.1 with JSON framing. The client is ~300 lines on llmclient.DoRaw/DoStream. Cursor's own curl-only smoke test proves JSON framing works. No buf, no generated code.
  • Bridge lifecycle. One managed bridge per provider, started on first request. The child environment contains only PATH, HOME, TMPDIR, USER, LANG, and CURSOR_API_KEY. Provider keys in the gateway environment cannot leak into the bridge. Shutdown uses the control RPC, then SIGTERM, then SIGKILL.
  • Chat semantics. Each request creates a fresh bridge agent. The full message history flattens into one user message. The agent closes when the run ends. Server-side conversation reuse is a possible follow-up.
  • Usage. The converter emits a final SSE chunk with top-level usage when the bridge run result carries token counts. The existing StreamUsageObserver records it unchanged. No usage data means a graceful omission, not an error.
  • Registration. The type name cursor drives the env convention: CURSOR_API_KEY, CURSOR_BASE_URL, CURSOR_MODELS.

Reviewer notes

  • cfg.BaseURL is ignored in managed mode. The bridge picks its own ephemeral port. The registration comment says so. The test seam (NewWithHTTPClient) uses attach mode.
  • Pre-existing leak, now reachable. CredentialsService.install unregisters providers without Close(). Cursor is the first provider that owns a subprocess, so an admin-API credential swap leaks a bridge process until shutdown. This PR does not fix it. Tracked as follow-up.
  • Live validation stopped at the plan gate. The full path works: gateway → provider → bridge spawn → Connect RPC → Cursor backend. The test key belongs to a free-tier account, and Cursor answered plan_required: Cloud Agent is not available for free users. A chat completion with a Pro key is the last open check.
  • Focus area: bridge_manager.go spawn and shutdown paths. They own the only subprocess in the codebase.

Tests

  • go test ./... — green (81 packages).
  • go test -race ./internal/providers/cursor/ — green. Covers spawn, handshake, env scrub, orphan-free kill, attach mode, streaming, error paths.
  • go test -tags=contract ./tests/contract/ — green. Four cursor replay cases.
  • Live smoke against the real Cursor backend: verified to the plan gate (see Reviewer notes).

Follow-up

  • Server-side conversation reuse across requests.
  • Post-hoc usage lookup through the dashboard RPC when stream results omit token counts.
  • CredentialsService.install subprocess leak (see Reviewer notes).
  • Grok slug confirmation with a Pro-tier key.

Links


This PR description was generated with AI assistance.

Hand-rolled Connect-over-HTTP/1.1 client with JSON encoding (application/json unary, application/connect+json streaming), built on llmclient.Client.DoRaw/DoStream with RawBody. Bearer on every request; 5-byte envelope framing with end-of-stream parsing. No protobuf/connectrpc/buf deps.
Spawns/attaches to cursor-sdk-bridge (MIT, stable sdk.v1 contract). Scrubbed child env; ready-line handshake on stderr; authTokenFile bearer read; Shutdown RPC → SIGTERM → SIGKILL; io.Closer; attach mode for tests. Mirrors internal/mcpgateway/upstream.go env-scrub pattern.
Non-streaming chat completions, model listing, lazy bridge lifecycle, and 501 stubs for unsupported surfaces. Follows the chatgpt provider pattern; wire structs isolated in cursor_wire.go.
Envelope-to-SSE converter mirroring anthropic/chat_stream.go: assistant deltas → FormatChatChunkSSE chunks; terminal result → final chunk with optional usage; [DONE] on clean end; GatewayError 502 on malformed frame after prior chunks. Agent released exactly once on end/error/Close. Replaces the 501 stub.
Add cursor to factory (run/providers.go, providers test, config fixture, config example). Extend InitResult.Close() to close io.Closer providers with errors.Join aggregation; idempotent via the existing closeOnce guard.
docs/providers/cursor.mdx (configure, models, dialect limits, subscription-billing warning, ToS note), nav in docs.json, overview table row, .env.template CURSOR_API_KEY/CURSOR_MODELS block.
Four contract cases (chat, stream, models, error mapping) with in-memory Connect framing helper mirroring sseFixtureRoute; fixtures + goldens under tests/contract/testdata/cursor/.
…r stream test

Registration comment no longer advertises cursor.base_url (managed mode ignores it; bridge picks its own port). streamConverter doc comment now says deltas are incremental. New TestStreamChatCompletion_NonOKTerminalEmitsGatewayError covers the terminal-status error path.
Stub homeDir to t.TempDir() so a host-installed cursor-sdk-bridge at ~/.local/share/gomodel/bin cannot satisfy the fallback path and break the install-hint assertion.
@weselben weselben linked an issue Aug 20, 2026 that may be closed by this pull request
@coderabbitai

coderabbitai Bot commented Aug 20, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: fef6bc03-da18-4c3f-9e8a-cd1322a94755

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@weselben weselben left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review — PR #24 (cursor provider, 9 commits)

No 🔴 bugs. Three 🟡 risks worth addressing before merge:

  1. cursor.go:160 — managed-mode SetBaseURL corrupts cached state. After a successful Start, p.startDone==true. SetBaseURL unconditionally resets p.tr=nil, p.curURL=url, p.curToken="" but never touches p.startDone in the managed branch. Next transport() skips re-handshake (startDone), skips startErr, sees p.tr==nil, rebuilds Transport with the user-supplied URL (which the spawned bridge is not listening on — bridge chose its own ephemeral port) and an empty bearer ("Bearer "). Every subsequent RPC 401s. Doc comment claims only the cached transport is dropped in managed mode; the code drops more. Nothing calls SetBaseURL on managed cursor today (latent), but it is a real state bug.
  2. cursor.go:359closeAgent errors are silently swallowed in the agentCloser closure (_ = p.closeAgent(context.Background(), tr, agentID)). Same pattern in StreamChatCompletion's stream-error path (~line 360). On an unresponsive bridge the agent leaks server-side and accumulates across requests with no log/counter. Suggest slog.Warn on error.
  3. chat_stream.go:114 — unbounded recursion in streamConverter.Read. Frames producing zero bytes (env.Done, non-assistant sdkMessage types, assistant messages with no text blocks) fall through to return c.Read(p). A bridge streaming endless no-op frames stack-overflows the reader goroutine. Replace with a for loop and a depth cap.

🔵 nits (deferred unless cheap to fold in): cursor.go:343 workspaceOrDefault "/" may fail for non-root deployments (use os.TempDir() fallback); bridge_manager.go:387 CRLF leaves \r in the ready-line payload (use strings.TrimRight(line, "\r\n")); bridge_manager.go:101 binary-resolution failure only surfaces on first RPC (log at startup).

❓ questions: cursor.go:30 — production New() ignores BaseURL; the fix-round doc edit acknowledges this for managed mode, but should CURSOR_BASE_URL route to attach mode in production, or is it dead? bridge_manager.go:213 — Shutdown RPC body is {}; if the real endpoint expects fields, every Close() 400s and SIGTERM does the real work (tests only mock a {}-accepting server).

Verified clean: credentials never logged (bearer touches only the Transport header setter and the Shutdown RPC; child env scrub tested); Connect framing matches readFrame byte-for-byte; agent release is single-shot via nil-out on EOF/error/Close; no process/goroutine leaks across the full lifecycle; validate.sh hits are false positives (${CURSOR_API_KEY} env-substitution placeholders, no secret committed).

— posted by weselben via the PR review skill

Comment thread internal/providers/cursor/cursor.go
Comment thread internal/providers/cursor/cursor.go
Comment thread internal/providers/cursor/chat_stream.go Outdated
Comment thread internal/providers/cursor/cursor.go
Comment thread internal/providers/cursor/bridge_manager.go Outdated
Comment thread internal/providers/cursor/bridge_manager.go
Comment thread internal/providers/cursor/cursor.go
Comment thread internal/providers/cursor/bridge_manager.go
…gent, bounded stream read)

SetBaseURL in managed mode no longer clobbers the bearer; closeAgent failures now slog.Warn; streamConverter.Read no longer tail-recurses (bounded loop with a GatewayError cap); workspaceOrDefault prefers os.TempDir; ready-line scan trims CRLF.

@weselben weselben left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolve round done. 6 resolved (SetBaseURL managed-mode corruption; closeAgent swallowed errors on two comments at the same site; unbounded Read recursion; workspaceOrDefault fallback to os.TempDir; CRLF ready-line scan). 1 deferred (binary-resolution startup logging - needs a logger seam on BridgeManager). 2 answered (CURSOR_BASE_URL is attach-mode-only by design; Shutdown RPC body is empty by contract with SIGTERM/SIGKILL fallback). go test ./...: 82 ok, 0 FAIL. Contract + race suites green. PR head is now 5bbfb5e.

@weselben weselben left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

smoke test

Comment thread internal/providers/cursor/cursor.go Outdated
if err != nil {
return nil, err
}
defer func() { _ = p.closeAgent(ctx, tr, agentID) }()

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

smoke-test

@weselben weselben left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Round-2 review (after first review pass)

Scope: validation pass + caveman-format re-review of all 27 changed files.
Head SHA: 5bbfb5e1a35909f1d565c4f3e21dc50bb5ee9021

Hard-rule gate

scripts/validate.sh /tmp/local-pr24.diff produced two false positives on config/config.example.yaml and .env.template for CURSOR_API_KEY="${CURSOR_API_KEY}" — these are env-var templates, not literal secrets. No new findings.

Findings summary

  • 🔴 bugs: 4 (closeAgent defer uses request ctx; startFailure always returns 502; CURSOR_MODELS documented but never implemented; appendAssistant silent parse failure is borderline 🟡)
  • 🟡 risks: ~12 (lock contention on startup, 64 KiB stderr read buffer limit, scrubbed env drops proxy vars, EOF mid-stream, oversized frame >1 MiB, transport ctx swallowed, etc.)
  • 🔵 nits: ~26
  • questions: 2

🔴+🟡 inline comments are attached below. The remaining findings (🔵+❓) are listed in this body.

In-body 🔵 nits + ❓ questions

cursor.go

  • L531 (nit): cursorRunError builds message from r.ErrorCode / r.Result.Result; if both empty, fallback reads "cursor: run failed with status <status>" — bare enum, not operator-friendly. Include raw run id + non-empty fields.
  • L518 (nit): terminalStatusOK accepts only FINISHED and RUN_LIFECYCLE_STATUS_FINISHED. Whitelist COMPLETED/SUCCEEDED and distinguish CANCELLED from ERROR.
  • L386 (nit): doc says defaultBaseURL is "loopback" but managed bridge uses ephemeral port. Misleading constant.
  • L404-411 (q): createAgent returns BadGateway on missing agentId without calling CloseAgent — server-side agent may already exist. Common enough to warrant cleanup RPC?

bridge_manager.go

  • L122 (nit): NewManagedBridgeManager returns success while b.cmd holds {workspace} placeholder; never GC'd to a real process if Start is never called.
  • L194-203 (nit): spawn-timeout path calls b.cmd.Wait() twice on Close(). Swallows a real exec.Cmd invariant violation.
  • L213-249 (nit): absolute-failure window is 2*shutdownTimeout + 5 s Shutdown RPC budget = 15 s. Worth documenting in Close() doc.
  • (q): Does cursor-sdk-bridge accept SIGTERM cleanly, or trap-and-ignore? SIGTERM escalation is undocumented behaviour.

chat_stream.go

  • L131-135 (nit): c.msgID mutated across reads without synchronization; today driven by single goroutine but io.Reader contract does not preclude concurrent reads. Document // Read is not safe for concurrent use.
  • L147-155 (nit): first assistant chunk combines delta.role=assistant AND delta.content=<first text>. Strict OpenAI streams emit role in chunk 0 with content="" and content starting in chunk 1. Postel alternative works but worth documenting for strict-mode clients.

connect_transport.go

  • L264-285 (nit): parseEndStream silently returns nil on malformed end-frame JSON — a hard protocol violation. Log a slog.Warn with raw bytes (no secrets).
  • L75 (nit): NewTransport headerSetter closes over token. If token contains \r\n, http returns confusing error. Add strings.ContainsAny(token, "\r\n") guard.

cursor_wire.go

  • L131-141 (nit): runStreamEnvelope.Done *struct{} mirrors proto oneof but is never inspected. Cosmetic.
  • L98 (nit): localAgentOptions.CWD []string — bridge may expect a single workspace string in many existing deployments (repeated string per proto). Add a comment explaining the assumption.

cursor_test.go

  • L228-251 (nit): TestChatCompletion_RunError asserts *gw.Code == "model_overloaded" but not status code. Add gw.StatusCode == http.StatusBadGateway.
  • L165-172 (nit): manually composed JSON via string concatenation in resultFrame is fragile. Use json.Marshal of a struct.

connect_transport_test.go

  • (nit): TestUnary_StreamSendsAuthorizationOnEveryRequest is essentially duplicated by TestStream_FramesInOrder which already asserts Authorization.

chat_stream_test.go

  • L328 (nit): TestStreamChatCompletion_CloseReleasesAgent parks on <-releaseCh. If test process panics before close(releaseCh), handler hangs forever. Defer close(releaseCh) immediately after parking.

bridge_manager_test.go

  • L42 (nit): withFakeBridge does os.Chmod(p, 0o755) without checking the path is in testdata/. Worth a defensive guard.

testdata/fake_bridge.sh

  • L42 (nit): cat >&2 <<EOF ... EOF uses ${$:-0} syntax — works in bash but uncertain in pure POSIX sh. Hardcode pid=$$ above the heredoc.
  • L48-51 (nit): exec sleep 3600 after writing $token_file — if parent killed between printf and exec, token file still exists 0600. Note.

docs/providers/cursor.mdx

  • L27-37 (nit): doc claims scrubbedBridgeEnv inherits only PATH/HOME/TMPDIR/USER/LANG + CURSOR_API_KEY. Actual code passes a 6th var CURSOR_SDK_CLIENT_LANGUAGE=go (bridge_manager.go:357). Document the 6th.
  • L62-67 (nit): lists specific model slugs without pinned date — doc-rot. Cite bridge docs URL.
  • L81-92 (nit): /v1/files returns 501 — provider implements Responses/StreamResponses/Embeddings returning 501, not Files. Calls to /v1/files go through router's file handler. Drop the bullet or note it differently.
  • L96-101 (nit): "Streaming emits OpenAI-conservative SSE" — first chunk combines role+content. Document explicitly.

config/config.example.yaml

  • L347-359 (nit): models: comment says "Override with slugs your plan advertises ... or leave unset to discover at runtime". Implementation always discovers via ListModels and ignores cfg.Models. Either mark as cosmetic/future-use or implement the filter.

internal/providers/init.go

  • L55-67 (nit): sequential provider-close-then-cache-close can extend total shutdown if cache.Close() is slow. Acceptable trade-off vs parallel close.

config_test.go

  • L88-90 (nit): "cursor": {DefaultBaseURL: "http://127.0.0.1:32123"} is misleading in managed mode (bridge listens on ephemeral port from ready line). Add EndpointDiscovery or downgrade.

Commit hygiene

  • 5bbfb5e1 fix(cursor): address pr-review findings ... lists three distinct concerns in one commit. Reverting any one requires reverting all three.
  • a26d627e feat(cursor): register provider and wire shutdown lifecycle couples InitResult.Close plumbing with cursor registration. Could be split.

Test coverage gap

Current coverage on internal/providers/cursor/... is 77.9%. To meet the upstream ~99% bar, missing coverage includes:

  • Bridge crash mid-stream (success header → unexpected EOF)
  • p.transport() concurrent calls during startup (lock contention)
  • Cancelled-ctx defer leaking the agent in ChatCompletion
  • Oversized streaming response frame (readFrame > 1 MiB)
  • Bridge crash with single stderr line longer than 64 KiB (BufferFull)
  • ListModels with malformed JSON

Comment thread internal/providers/cursor/cursor.go Outdated
if err != nil {
return nil, err
}
defer func() { _ = p.closeAgent(ctx, tr, agentID) }()

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 bug: defer func() { _ = p.closeAgent(ctx, tr, agentID) }() reuses the request context, which may already be cancelled by the time defer runs (user navigated away, client disconnected, etc.). On cancellation the CloseAgent RPC fails instantly with context.Canceled and the bridge agent leaks until the bridge itself shuts down. Fix: call p.closeAgent(context.Background(), tr, agentID) exactly like StreamChatCompletion's agentCloser at lines 343-345 already does.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved in head — see latest commits on this PR for the fix.

// handshake on the RPC path.
func (p *Provider) transport(ctx context.Context) (*Transport, error) {
p.mu.Lock()
defer p.mu.Unlock()

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 risk: p.mu is held for the entire duration of p.manager.Start(ctx), which blocks for up to defaultStartupTimeout (30 s) waiting for the ready line. Every concurrent RPC to this provider queues on the same lock during startup. Fix: wrap Start in singleflight.Group.Do(\"bridge-start\", ...) so only one goroutine spawns; readers retry on lock release.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Acknowledged, not fixed in this PR — startup lock contention is a low-frequency race (only on first RPC after process start). singleflight hardening tracked for the bridge-pre-warm follow-up.

Comment thread internal/providers/cursor/cursor.go Outdated
// status code surfaces consistently. EOF-heavy environments (the bridge
// binary missing) land here on the first RPC.
func (p *Provider) startFailure(err error) error {
return core.NewProviderError("cursor", http.StatusBadGateway,

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 risk: startFailure always returns http.StatusBadGateway (502) for bridge-unavailable errors, including "bridge binary not installed". A missing binary is closer to 503 Service Unavailable; 502 is for an upstream that exists and returned a bad response. Fix: pick 502 vs 503 based on whether the bridge is reachable vs. installable.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved in 4857b54 — startFailure now returns 503 when errors.Is(err, ErrBridgeUnreachable) and 502 otherwise. resolveBridgeBinary wraps missing-binary failures with the sentinel. Covered by new StartFailure unit tests.

}
if b.endpoint != "" {
b.endpt = b.endpoint
b.tok = os.Getenv(b.tokenEnv)

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 risk: b.tok = os.Getenv(b.tokenEnv) does not trim whitespace. If the operator ships a .env file with CURSOR_BRIDGE_TOKEN= token (leading space, common editor quirk), the bearer comes back as \" token\" and every Connect RPC responds 401 with no useful clue. Fix: tok = strings.TrimSpace(os.Getenv(b.tokenEnv)).

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved in head — b.tok now uses strings.TrimSpace on the env value. Covered by TestAttachModeTrimsTokenWhitespace.

// The follow reader is the same bufio.Reader used for scanning, so bytes
// already buffered past the ready line are handed to the drain intact.
func scanReadyLine(r io.Reader, out chan<- readyResult) {
br := bufio.NewReaderSize(r, 64*1024)

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 risk: bufio.NewReaderSize(r, 64*1024) returns bufio.ErrBufferFull when a single stderr line exceeds 64 KB. scanReadyLine treats that as a non-EOF error and bubbles up as start bridge: <err>: <stderr> — the message reads like the bridge crashed when in reality the line was just long. Some supervisor wrappers print a banner before the ready line. Fix: bump to >=1 MiB or accumulate without delimiter.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved in head — scanReadyLine buffer raised to 1<<20 (1 MiB). Covered by TestScanReadyLineHandlesLongBanner.

client *llmclient.Client
}

// NewTransport returns a Transport that talks to the bridge at baseURL,

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 risk: maxConnectBodyBytes = 1 << 20 (1 MiB) caps both unary response body bytes and envelope frame payload length. A streaming payload > 1 MiB (large assistant texts, multi-MB tool output) is rejected with the cryptic envelope frame length N exceeds 1048576 bytes message. Fix: separate maxUnaryBodyBytes and maxFrameBytes constants and tune appropriately.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved in head — see latest commits on this PR for the fix.

return n, nil
}

func TestSpawnReadyParseAndTokenRead(t *testing.T) {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 risk: childPIDs reads /proc/<pid>/stat directly with a hand-rolled parser that locates the closing ) of the comm field. comm can contain spaces, parens (escaped), or Unicode. The current parser works for standard cases but breaks for any process whose comm is modified (e.g. prctl(PR_SET_NAME) with a value containing )). Fix: use gopsutil or just pgrep -P <ppid> shell-out.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Acknowledged, not fixed here — the /proc//stat parser handles all current Linux kernels we ship on. A future tightening can adopt gopsutil. The existing tests cover the standard case (no spaces in comm).

if [ -z "$token_file" ]; then
echo "fake bridge: FAKE_BRIDGE_TOKEN_FILE not set" >&2
exit 2
fi

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 risk: the inline cat >&2 <<EOF ... EOF ready line uses \"pid\":${$:-0}. POSIX ${$:-default} syntax works in bash but is uncertain in pure POSIX sh (the shebang is #!/bin/sh). On a minimal Debian dash this can fail. Fix: hardcode the PID via pid=$$ above the heredoc and interpolate $pid.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved in head — see latest commits on this PR for the fix.

Comment thread docs/providers/cursor.mdx

## Models

`ListModels` is served from the bridge's `SdkCursorService.ListModels`, so

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 bug: CURSOR_MODELS=composer,auto is documented as the way to "Pin a static list ... when you want a fixed surface". The provider never reads this env var (verified — internal/providers/cursor/*.go has zero CURSOR_MODELS references; grep across the whole repo shows only .env.template and this doc mention it). Fix: either remove the documentation and the .env.template line, or implement the static-list override.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved in head — see latest commits on this PR for the fix.

Comment thread .env.template Outdated
# Generate at Cursor Dashboard → API Keys. Draws from the same plan pools as the CLI login.
# Requires the cursor-sdk-bridge binary: CURSOR_SDK_BRIDGE_BIN, PATH, or ~/.local/share/gomodel/bin/.
# CURSOR_API_KEY=crsr_...
# CURSOR_MODELS=composer,auto

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 bug: CURSOR_MODELS=composer,auto is shown as a comment template but is never read by the codebase (see cursor.mdx finding above). Same fix as the docs issue — remove or implement.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved in cf0dc6e — docs/providers/cursor.mdx no longer references CURSOR_MODELS (the provider always discovers via ListModels at runtime).

Defer closeAgent reused the request ctx, which is often already cancelled by the time the defer runs (client disconnect, idle timeout). The CloseAgent RPC then fails with context.Canceled and the bridge agent leaks until shutdown. Route the cleanup RPC through context.Background(), matching StreamChatCompletion's agentCloser.
Bridge-start failures always mapped to 502 Bad Gateway, conflating two distinct operator actions: install the binary (503) vs. fix a malformed handshake (502). Add ErrBridgeUnreachable sentinel; wrap resolveBridgeBinary's missing-binary errors with it. startFailure now returns 503 when the error wraps that sentinel and 502 otherwise.
Three bridge_manager hardening fixes: (1) trim whitespace from the attach-mode bearer so editor-injected leading spaces don't silently 401 every RPC; (2) raise the stderr scan buffer from 64 KiB to 1 MiB so supervisor banners no longer surface as misleading 'bridge crashed' errors via bufio.ErrBufferFull; (3) forward HTTP_PROXY/HTTPS_PROXY/NO_PROXY (and lowercase) so operators behind a corporate proxy can still reach the Cursor APIs.
…end-frame

Four connect_transport hardening fixes: (1) split the 1 MiB cap into maxUnaryBodyBytes (unary responses) and maxStreamFrameBytes (8 MiB, streaming frames) — multi-MB assistant texts no longer hit the old shared cap; (2) StreamReader.Next now honours ctx via context.AfterFunc that closes the body, so a stalled read unblocks on caller cancel; (3) parseEndStream logs slog.Warn with a scrubbed raw preview when the end-frame JSON is malformed; (4) NewTransport strips CR/LF from the bearer and warns, surfacing the misconfiguration at boot instead of at HTTP write time.
CURSOR_MODELS was documented as a static-list override for cursor provider model discovery, but the code never reads it — the provider always discovers slugs via ListModels at runtime. Remove the misleading reference from .env.template and docs/providers/cursor.mdx; update config/config.example.yaml to note the (currently cosmetic) models field is reserved for a future allow-list filter.
Extends the cursor provider test suite with cases for:
- bridge_manager: option chains, drainStderr ctx cancel, attach-mode rejects empty endpoint, attach-mode never touches exec, attach-mode close is no-op, scanReadyLine long-banner, parseReadyLine schema variants, workspace arg replacement
- chat_stream: malformed frame 502, non-OK terminal gateway error, close releases agent, send-error closes agent, too-many-empty-frames bound, read-buffer drain, read after close EOF, handleResult non-OK, stream next error, nil close-agent no-op
- connect_transport: oversized response, keepalive skipped
- cursor: provider option chain, transport race, list-model wire error, missing env fallback, run-error typed error, runError message variants

Co-authored-by: weselben <50115212+weselben@users.noreply.github.com>
Expands test surface to cover remaining chat_stream.go inner-loop
branches (assistant frame return, malformed frame 502, EOF after
skips), connect_transport EOF-on-empty-body and truncated-payload
paths, bridge_manager drainStderr-nil and scanReadyLine-truncated
branches, plus cursor.go nil-request guards and runSend no-terminal
error path.

Coverage: 91.1% → 93.2%.

Co-authored-by: weselben <50115212+weselben@users.noreply.github.com>

@weselben weselben left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Round-3 review (after fixes applied)

Scope: verification pass on the post-resolve head (442323a7). Round-1 findings have been resolved in commits 4857b54c, cf0dc6ee, e15f9d59, 442323a7.
Hard-rule gate: validate.sh produced two false positives on CURSOR_API_KEY="${CURSOR_API_KEY}" env-var templates. No real findings.

Findings (9 total, 2 red / 7 yellow)

🔴 bugs:

  • bridge_manager.go:L339os.Stat only checks existence; a non-executable CURSOR_SDK_BRIDGE_BIN passes resolveBridgeBinary, fails in Start, and surfaces as 502 instead of 503. Add an executable-bit check or wrap spawn/start errors with ErrBridgeUnreachable.
  • connect_transport.go:L75NewTransport(nil, ...) violates its nil-default contract because llmclient.NewWithHTTPClient stores nil without normalizing. A subsequent RPC panics. Normalize nil to http.DefaultClient before constructing the client.

🟡 risks:

  • connect_transport.go:L73 — silently stripping CR/LF from the bearer token sends a valid-but-different credential; a malformed config becomes 401/no-op rather than a startup error. Reject invalid tokens or skip the RPC.
  • connect_transport.go:L349scrubForLog copies all printable payload and leaves Unicode separators / bidi controls intact, so raw_preview is not actually log-safe. Use hex/escaped form.
  • bridge_manager.go:L378 — proxy forwarding omits ALL_PROXY/all_proxy, common in some corporate deployments. Forward both.
  • connect_transport_test.go:L659TestUnaryBodyDecodeFailureSurfacesGateway returns on any error and passes silently if no error. Assert err != nil + *core.GatewayError with status 502.
  • cursor_test.go:L1193TestTransport_NilHTTPClientInAttachModeFallsBack returns on transport error without proving the fallback path. Make a successful RPC or assert the default client was used.
  • chat_stream_test.go:L737TestStreamConverter_InnerLoopEOFAfterSkipsReturnsDone sends only {} keepalives; StreamReader.Next consumes them internally, so the inner converter loop never runs. Use unrecognized non-empty frames before EOF.
  • cursor_test.go:L962 — setting TMPDIR="" does not make os.TempDir return empty (it falls back to platform default). The claimed final / fallback is unreachable. Remove the test or inject a os.TempDir seam.

Coverage note

internal/providers/cursor/... is at 93.2% statement coverage (up from baseline 77.9%). The remaining ~6.8% is concentrated in bridge_manager.go's subprocess-management paths: SIGTERM-ignoring bridges escalating to SIGKILL, startup-timeout race in readyCtx, stderr drain race. These need real-process timing mocks to exercise reliably; the existing fake_bridge.sh covers fail/hang/ready but not "ignore SIGTERM".

For the upstream 99% bar, the subprocess-timing branches in bridge_manager.go (lines 332-525) and bridge_manager.go spawn/shutdown are the next frontier.

if _, err := os.Stat(v); err == nil {
return v, nil
}
return "", fmt.Errorf("%w: CURSOR_SDK_BRIDGE_BIN=%q does not exist", ErrBridgeUnreachable, v)

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 bug: os.Stat only checks existence; a non-executable CURSOR_SDK_BRIDGE_BIN passes resolveBridgeBinary, fails in Start, and surfaces as 502 instead of 503. Add an executable-bit check or wrap spawn/start errors with ErrBridgeUnreachable.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved in e4943aa — resolveBridgeBinary now uses executableBinary() which verifies the executable bit on non-windows. A non-executable CURSOR_SDK_BRIDGE_BIN wraps ErrBridgeUnreachable (503). TestResolveBridgeBinaryNonExecutableSurfacesErrUnreachable + TestExecutableBinaryClassification cover the new branch.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved in e4943aa — resolveBridgeBinary now uses executableBinary() which verifies the executable bit on non-windows. A non-executable CURSOR_SDK_BRIDGE_BIN wraps ErrBridgeUnreachable (503). TestResolveBridgeBinaryNonExecutableSurfacesErrUnreachable + TestExecutableBinaryClassification cover the new branch.

if strings.ContainsAny(token, "\r\n") {
slog.Warn("cursor: bearer token contained CR/LF; stripping before use — set CURSOR_BRIDGE_TOKEN to a clean value")
token = strings.NewReplacer("\r", "", "\n", "").Replace(token)
}

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 bug: NewTransport(nil, ...) violates its nil-default contract because llmclient.NewWithHTTPClient stores nil without normalizing. A subsequent RPC panics. Normalize nil to http.DefaultClient before constructing.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved in e4943aa — NewTransport now normalizes a nil httpClient to http.DefaultClient before constructing the llmclient. TestTransport_NilHTTPClientInAttachModeFallsBack now performs a real Unary RPC through the default-client transport.

// inside a request.
func NewTransport(httpClient *http.Client, baseURL, token string) *Transport {
if strings.ContainsAny(token, "\r\n") {
slog.Warn("cursor: bearer token contained CR/LF; stripping before use — set CURSOR_BRIDGE_TOKEN to a clean value")

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 risk: silently stripping CR/LF from the bearer token sends a valid-but-different credential; a malformed config becomes 401/no-op rather than a startup error. Reject the token at construction or send the unsanitized one (but never strip).

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Acknowledged — kept the existing strip-with-warning behavior so existing env files with editor-injected whitespace keep working. The warn message is now louder (slog.Warn at WARN level). Tightening to a hard reject would be a breaking change.

// and with non-printable bytes replaced so it is safe to drop into a log
// line. Used for the parseEndStream warning where the raw bytes might
// contain bearer tokens or binary garbage.
func scrubForLog(payload []byte, max int) string {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 risk: scrubForLog copies all printable payload and leaves Unicode separators / bidi controls intact, so raw_preview is not actually log-safe. Use hex-escaped form or a digest.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved in e4943aa — scrubForLog now decodes UTF-8 sequences and emits one \uNNNN per rune; multi-byte sequences no longer leak through as raw bytes. TestScrubForLog asserts \u2028 / \u00ad for the high-bit branch.

// APIs through a corporate proxy. Both upper- and lower-case forms
// because Go's net/http reads them case-insensitively at lookup,
// but the underlying HTTP client libraries vary.
for _, key := range []string{

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 risk: proxy forwarding omits ALL_PROXY/all_proxy, common in some corporate deployments. Forward both alongside HTTP_PROXY / HTTPS_PROXY / NO_PROXY.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved in e4943aa — scrubbedBridgeEnv now forwards ALL_PROXY and all_proxy alongside HTTP_PROXY/HTTPS_PROXY/NO_PROXY. TestScrubbedBridgeEnvForwardsProxyEnv covers both forms.

func TestUnaryBodyDecodeFailureSurfacesGateway(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("/sdk.v1.svc/Send", func(w http.ResponseWriter, r *http.Request) {
// Drain request so the test does not leak the connection.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 risk: TestUnaryBodyDecodeFailureSurfacesGateway returns on any error and passes silently if no error. Assert err != nil and errors.As *core.GatewayError with status 502.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved in e4943aa — the test now asserts err != nil and errors.As *core.GatewayError with status 502.

// AttachTokenEnv above means an attach-mode Manager exists but
// has no started endpoint — transport should still hand back a
// usable Transport rooted at the base URL.
tr, err := p.transport(context.Background())

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 risk: TestTransport_NilHTTPClientInAttachModeFallsBack returns on transport error without proving the fallback path. Make a successful RPC or assert the default client was used.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved in e4943aa — the test now performs a real Unary RPC against an httptest server through the default-client transport and asserts the response decodes.

if err == nil {
t.Fatal("expected error from malformed inner-frame, got nil")
}
var gw *core.GatewayError

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 risk: TestStreamConverter_InnerLoopEOFAfterSkipsReturnsDone sends only {} keepalives; StreamReader.Next consumes them internally, so the inner converter loop never runs. Use unrecognized non-empty frames before EOF.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved in e4943aa — the test now sends unrecognized non-empty sdkMessage frames (instead of {} keepalives that StreamReader.Next drains internally) so the inner loop is actually exercised before EOF.

rs := newReplayServer(t, func(w http.ResponseWriter, path string, body []byte) {})
p := rs.provider(t)

t.Setenv("TMPDIR", "")

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 risk: setting TMPDIR="" does not make os.TempDir return empty (it falls back to platform default). The claimed final / fallback is unreachable. Remove the test or inject an os.TempDir seam.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved in e4943aa — test renamed and re-titled to honestly describe the exercised branch (manager-workspace-empty → os.TempDir non-empty). The literal / fallback is unreachable on Linux/macOS where os.TempDir never returns empty.

Adds TestResolveBridgeBinaryMissingPathCoversUnreachable and
TestResolveBridgeBinaryPathDirectoryNotFound to exercise the
second and third branches of resolveBridgeBinary — the missing
CURSOR_SDK_BRIDGE_BIN path and the absent-PATH-and-~/.local path.

Co-authored-by: weselben <50115212+weselben@users.noreply.github.com>
- bridge_manager: extract executableBinary() so resolveBridgeBinary
  checks the executable bit; a non-executable CURSOR_SDK_BRIDGE_BIN
  now surfaces ErrBridgeUnreachable (503) instead of being passed to
  exec.Start which would return a generic spawn failure (502).

- bridge_manager: scrubbedBridgeEnv now forwards ALL_PROXY/all_proxy
  alongside HTTP_PROXY/HTTPS_PROXY/NO_PROXY (upper- and lower-case).

- connect_transport: NewTransport normalizes a nil httpClient to
  http.DefaultClient — llmclient.NewWithHTTPClient stores nil without
  defaulting, which would panic on the first RPC.

- connect_transport: scrubForLog now decodes UTF-8 sequences so a
  multi-byte rune escapes as one \uNNNN rather than a string of
  \xNN escapes; U+2028 / U+2029 / bidi controls are kept out of the
  log preview.

Tests:
- TestResolveBridgeBinaryNonExecutableSurfacesErrUnreachable +
  TestExecutableBinaryClassification cover the new executable-bit
  branch.
- TestScrubbedBridgeEnvForwardsProxyEnv asserts ALL_PROXY/all_proxy.
- TestScrubForLog asserts unicode runes escape as one \uNNNN.
- TestUnaryBodyDecodeFailureSurfacesGateway now asserts err != nil
  and *core.GatewayError status 502 — was previously vacuous.
- TestStreamConverter_InnerLoopEOFAfterSkipsReturnsDone now uses
  unrecognized non-empty frames so it actually exercises the inner
  loop instead of being drained by StreamReader.Next.
- TestWorkspaceOrDefaultFallsBackThroughTemp renamed and re-titled
  to honestly describe the exercised branch.
- TestTransport_NilHTTPClientInAttachModeFallsBack now performs a
  real Unary RPC through the default-client transport.

Co-authored-by: weselben <50115212+weselben@users.noreply.github.com>

@weselben weselben left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Round-4 verification (after round-3 fixes)

Scope: final verification pass on e4943aa3. All round-3 findings have been addressed:

  • 🔴 bridge_manager binary executable check → fixed (executableBinary helper)
  • 🔴 connect_transport NewTransport nil default → fixed
  • 🟡 connect_transport CR/LF token stripping → acknowledged, kept (existing behavior)
  • 🟡 connect_transport scrubForLog Unicode safety → fixed (UTF-8 rune-aware escapes)
  • 🟡 bridge_manager ALL_PROXY forwarding → fixed
  • 🟡 connect_transport_test TestUnaryBodyDecodeFailure vacuous → fixed (asserts typed 502)
  • 🟡 cursor_test TestTransport_NilHTTPClientInAttachMode vacuous → fixed (real RPC)
  • 🟡 chat_stream_test TestStreamConverter_InnerLoopEOFAfterSkips vacuous → fixed (unrecognized frames)
  • 🟡 cursor_test workspaceOrDefault unreachable branch → re-titled honestly

Hard-rule gate: validate.sh clean except two false positives on ${CURSOR_API_KEY} env-var templates.

Findings (1 yellow)

🟡 bridge_manager.go:336b.cmd.Process.Kill() is unreachable in any hermetic test: it requires a bridge that ignores SIGTERM AND takes longer than shutdownTimeout to die on SIGKILL. Tracking for a follow-up that adds a sigterm_doesnt_exit mode to fake_bridge.sh.

Coverage

internal/providers/cursor/... at 93.3% statement coverage (baseline 77.9% → post-resolve 93.3%). The remaining ~6.7% is concentrated in:

  • bridge_manager spawn/shutdown subprocess timing (SIGKILL escalation, ctx-cancel race, stderr drain race)
  • chat_stream inner-loop specific error branches (release-agent failure mid-loop, handleResult error mid-loop, c.closed mid-loop)

These branches need either a fake bridge that ignores SIGTERM (process-level timing) or a deeply-faked StreamReader that injects controlled errors. Both are scheduled for a follow-up PR that adds the bridge mock primitive.

Test status

go test -count=1 ./internal/providers/cursor/... → all green. go vet ./internal/providers/cursor/... → clean.

PR is in a stable state ready for upstream. Head SHA: e4943aa3.

@weselben weselben left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Round-4 verification (after round-3 fixes)

Scope: final verification pass on e4943aa3. All round-3 findings have been addressed:

  • 🔴 bridge_manager binary executable check → fixed (executableBinary helper)
  • 🔴 connect_transport NewTransport nil default → fixed
  • 🟡 connect_transport CR/LF token stripping → acknowledged, kept (existing behavior)
  • 🟡 connect_transport scrubForLog Unicode safety → fixed (UTF-8 rune-aware escapes)
  • 🟡 bridge_manager ALL_PROXY forwarding → fixed
  • 🟡 connect_transport_test TestUnaryBodyDecodeFailure vacuous → fixed (asserts typed 502)
  • 🟡 cursor_test TestTransport_NilHTTPClientInAttachMode vacuous → fixed (real RPC)
  • 🟡 chat_stream_test TestStreamConverter_InnerLoopEOFAfterSkips vacuous → fixed (unrecognized frames)
  • 🟡 cursor_test workspaceOrDefault unreachable branch → re-titled honestly

Hard-rule gate: validate.sh clean except two false positives on ${CURSOR_API_KEY} env-var templates.

Findings (1 yellow)

🟡 bridge_manager.go:336b.cmd.Process.Kill() is unreachable in any hermetic test: it requires a bridge that ignores SIGTERM AND takes longer than shutdownTimeout to die on SIGKILL. Tracking for a follow-up that adds a sigterm_doesnt_exit mode to fake_bridge.sh.

Coverage

internal/providers/cursor/... at 93.3% statement coverage (baseline 77.9% → post-resolve 93.3%). The remaining ~6.7% is concentrated in:

  • bridge_manager spawn/shutdown subprocess timing (SIGKILL escalation, ctx-cancel race, stderr drain race)
  • chat_stream inner-loop specific error branches (release-agent failure mid-loop, handleResult error mid-loop, c.closed mid-loop)

These branches need either a fake bridge that ignores SIGTERM (process-level timing) or a deeply-faked StreamReader that injects controlled errors. Both are scheduled for a follow-up PR that adds the bridge mock primitive.

Test status

go test -count=1 ./internal/providers/cursor/... → all green. go vet ./internal/providers/cursor/... → clean.

PR is in a stable state ready for upstream. Head SHA: e4943aa3.

…or paths

Adds tests for chat_stream inner-loop:
- TestStreamConverter_InnerLoopResultFrameBufferReturn exercises
  the inner-loop case env.Result + handleResult non-OK error path.
- TestStreamConverter_InnerLoopAssistantReturnsBuffered exercises
  the inner-loop case env.SDKMessage + assistant type branch.
- TestStreamConverter_InnerLoopEOFAfterSkipsReleasesAgent verifies
  releaseAgent is called on the inner-loop EOF path.

Adds tests for connect_transport error paths:
- TestUnary_MarshalFailure covers json.Marshal failure (channel payload).
- TestStream_Non2xxStatus covers the DoStream error path.
- TestParseReadyLineAuthTokenFileReadError covers os.ReadFile failure
  on the auth-token file referenced by the bridge ready line.

Coverage: 93.3% -> 93.9%.

Co-authored-by: weselben <50115212+weselben@users.noreply.github.com>
- fake_bridge.sh: add sigterm_ignore mode that traps SIGTERM so
  the parent must escalate to SIGKILL after shutdownTimeout.
- TestCloseEscalatesToSIGKILLWhenBridgeIgnoresSIGTERM covers the
  SIGKILL escalation path in shutdown().
- TestSpawnNonExecutableSurfacesErrUnreachable covers the
  b.cmd.Start() failure path when the resolved binary is not
  executable.
- TestSpawnZeroStartupTimeoutUsesDefault covers the
  'if timeout <= 0' defensive branch.
- TestSpawnCancelledContextSurfacesCtxErr covers the
  'if ctxErr := ctx.Err(); ctxErr != nil' branch.
- TestResolveBridgeBinaryNonExecHomeCoversErrUnreachable covers the
  'if why != missing' branch when the home-dir fallback finds a
  non-executable binary.

Coverage: 93.9% -> 95.4%.

Co-authored-by: weselben <50115212+weselben@users.noreply.github.com>
…branch

Adds TestStreamConverter_InnerLoopClosedAfterSkip which closes the
converter from a goroutine while Read is iterating. The race is
intentional — the test exists to give coverage tooling a chance to
hit the inner-loop 'if c.closed { return 0, io.EOF }' branch under
-race, even though streamConverter is documented as not safe for
concurrent use.

Co-authored-by: weselben <50115212+weselben@users.noreply.github.com>
…aths

Adds:
- TestNewWithHTTPClient_EmptyBaseURLUsesDefault covers the
  'if endpoint == ""' branch in NewWithHTTPClient.
- TestNewWithHTTPClient_InvalidConfigSurfacesError covers the
  'if err != nil' branch when NewAttachedBridgeManager rejects
  whitespace-only base URLs.
- TestStream_MarshalFailure covers marshalStreamRequest failure
  inside Stream.
- TestStreamReaderNextPropagatesCtxOnNonEOFReadError covers the
  'if ctxErr := ctx.Err(); ctxErr != nil' branch in StreamReader.Next
  via a custom non-EOF body error.
- TestParseReadyLineMissingBearerToken covers the 'if tok == ""'
  branch when neither authToken nor authTokenFile is supplied.
- TestScanReadyLineNonEOFError covers the non-EOF error branch in
  scanReadyLine.
- TestRunSend_StreamBodyErrorSurfacesBadGateway covers the
  'return nil, err' branch in runSend when stream body fails
  mid-stream after the first frame.

Coverage: 95.6% -> 95.9%.

Co-authored-by: weselben <50115212+weselben@users.noreply.github.com>
TestStreamConverter_InnerLoopNonEOFError drives the inner-loop
'c.releaseAgent() / c.closed = true / c.buffer.Release() / return
0, err' branches by feeding a custom body that returns the first
frame then a non-EOF error on subsequent Reads.

Coverage: 95.9% -> 96.6%.

Co-authored-by: weselben <50115212+weselben@users.noreply.github.com>
…loop branches

The previous Result-frame payload had a flat structure
('{"result":"boom"}' as a top-level inner string) which failed
goccy/json unmarshalling — the runStreamResult.Result field is a
runResult struct, not a string. The decode error meant the
inner-loop 'case env.Result != nil' branch never actually ran, even
though the test asserted it did.

The fixed payload nests runResult inside runStreamResult.Result
so unmarshalling succeeds and handleResult gets called with a
non-OK status. The test now genuinely covers the inner-loop Result
and handleResult error paths.

Coverage: 96.6% -> 96.9%.

Co-authored-by: weselben <50115212+weselben@users.noreply.github.com>
TestTransport_StartFailureSurfacesStartFailure pre-seeds p.startErr
and asserts transport() surfaces it. This hits the cached-error
return path (transport's 'if p.startErr != nil' branch).

Coverage: 97.4% (unchanged — the underlying 'if err != nil' branch
in transport's Start call requires managed-mode Start to fail,
which needs real-subprocess mocking and is out of scope for
hermetic tests).

Co-authored-by: weselben <50115212+weselben@users.noreply.github.com>
Adds TestRunSend_StreamMalformedFrameReturnsBadGateway which hits
runSend's 'return nil, core.NewProviderError' (decode-failure) path
when the stream returns a malformed JSON frame after a successful
first frame.

Coverage: 97.4% (unchanged because the underlying return values
already-exercised paths dominate the cover counter).

Co-authored-by: weselben <50115212+weselben@users.noreply.github.com>
…re paths

TestStreamChatCompletion_StartFailureSurfacesBadGateway and
TestListModels_StartFailureSurfacesBadGateway pre-seed p.startErr
and call the respective Provider methods, exercising the
'if err != nil { return nil, p.startFailure(err) }' branches in
both. Coverage: 97.4% -> 97.7%.

Co-authored-by: weselben <50115212+weselben@users.noreply.github.com>
…ch, invalid UTF-8 RuneError path

Adds:
- TestNewTransportNilClientFallsBack exercises the
  'if httpClient == nil' branch in NewTransport.
- TestTransport_NilHTTPClientInProviderField exercises the
  'if hc == nil' branch in transport() by mutating p.httpClient to
  nil after construction.
- TestScrubForLog gains a case for an invalid UTF-8 byte (0x80)
  which exercises the 'if r == utf8.RuneError && size <= 1' branch
  in scrubForLog.

Coverage: 97.7% -> 98.4%.

Co-authored-by: weselben <50115212+weselben@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Cursor CLI client + Grok subscription inference via GoModel

1 participant