Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
672 changes: 672 additions & 0 deletions docs/internal/2026-09-04-altimate-base-e2e-harness-plan.md

Large diffs are not rendered by default.

95 changes: 95 additions & 0 deletions packages/opencode/test/altimate/_fixtures/altimate-base-harness.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
// Shared hermetic-test bootstrap for the Altimate Base e2e suites.
//
// Extracted from `test/altimate/altimate-base.test.ts`'s top-of-file isolated-environment
// pattern so every new suite (and that file) imports one implementation instead of
// re-copy-pasting it. See docs/internal/2026-09-04-altimate-base-e2e-harness-plan.md,
// Deliverable 2, for the full design rationale.
import { randomBytes } from "node:crypto"
import fs from "node:fs"
import os from "node:os"
import path from "node:path"
import { afterAll } from "bun:test"
import { FreeTierCapability } from "../../../src/altimate/free/capability"

const ISOLATED_ENV = [
"XDG_DATA_HOME",
"XDG_CONFIG_HOME",
"XDG_CACHE_HOME",
"XDG_STATE_HOME",
"OPENCODE_TEST_HOME",
] as const

/**
* Call once at module scope in each suite file, BEFORE importing `../../src/altimate/free/*`
* (the client reads Global.Path lazily per-call, but isolating env before any import keeps every
* suite file identical to how the existing altimate-base.test.ts already does it).
*
* Gives the file its own temp XDG/home tree so its credential store, config, and cache never
* touch a real user directory or another suite file's directory. Registers an `afterAll` that
* restores the previous env values and removes the temp tree.
*/
export function isolateAltimateBaseHome(prefix: string): string {
const original = Object.fromEntries(ISOLATED_ENV.map((key) => [key, process.env[key]]))
const home = fs.mkdtempSync(path.join(os.tmpdir(), `${prefix}-`))
process.env.XDG_DATA_HOME = path.join(home, "data")
process.env.XDG_CONFIG_HOME = path.join(home, "config")
process.env.XDG_CACHE_HOME = path.join(home, "cache")
process.env.XDG_STATE_HOME = path.join(home, "state")
process.env.OPENCODE_TEST_HOME = home

afterAll(() => {
for (const key of ISOLATED_ENV) {
const value = original[key]
if (value === undefined) delete process.env[key]
else process.env[key] = value
}
fs.rmSync(home, { recursive: true, force: true })
})
return home
}

/**
* Call in `beforeEach`: clears both current and legacy gateway env vars, then points the client
* at the fake gateway's URL. Mirrors `altimate-base.test.ts`'s existing `beforeEach` gateway-env
* reset so every suite starts from the same known configuration state.
*/
export function resetGatewayEnv(gatewayUrl: string): void {
delete process.env.ALTIMATE_BASE_GATEWAY_URL
delete process.env.ALTIMATE_FREE_GATEWAY_URL
process.env.ALTIMATE_BASE_GATEWAY_URL = gatewayUrl

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: resetGatewayEnv() permanently mutates process env: it deletes ALTIMATE_FREE_GATEWAY_URL and sets ALTIMATE_BASE_GATEWAY_URL to the fake gateway URL, but no teardown restores the prior values. isolateAltimateBaseHome()'s afterAll only restores the ISOLATED_ENV keys, so in a shared bun test worker the fake gateway URL stays set and the legacy var stays deleted after the suite. Follow the file's own restoration pattern: capture the original values once (e.g., alongside the ISOLATED_ENV snapshot in isolateAltimateBaseHome) and restore them in the afterAll.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/opencode/test/altimate/_fixtures/altimate-base-harness.ts, line 59:

<comment>resetGatewayEnv() permanently mutates process env: it deletes ALTIMATE_FREE_GATEWAY_URL and sets ALTIMATE_BASE_GATEWAY_URL to the fake gateway URL, but no teardown restores the prior values. isolateAltimateBaseHome()'s afterAll only restores the ISOLATED_ENV keys, so in a shared `bun test` worker the fake gateway URL stays set and the legacy var stays deleted after the suite. Follow the file's own restoration pattern: capture the original values once (e.g., alongside the ISOLATED_ENV snapshot in isolateAltimateBaseHome) and restore them in the afterAll.</comment>

<file context>
@@ -0,0 +1,95 @@
+export function resetGatewayEnv(gatewayUrl: string): void {
+  delete process.env.ALTIMATE_BASE_GATEWAY_URL
+  delete process.env.ALTIMATE_FREE_GATEWAY_URL
+  process.env.ALTIMATE_BASE_GATEWAY_URL = gatewayUrl
+}
+
</file context>

}

// `FreeTierCapability.issueArmer()` hands out the process's ONE consent-arming capability and
// throws on a second call — see `src/altimate/free/capability.ts`. In production that single call
// happens once, at TUI worker boot (`cli/tui/worker.ts`). Every Altimate Base e2e suite plays the
// role of that TUI host and needs the same capability, but `bun test test/altimate/` loads multiple
// suite files into ONE worker process, so if each file called `issueArmer()` itself at module
// scope, the second (and every subsequent) file to load would crash with "Altimate Base consent
// armer already issued for this process" — reproducible even with just the two pre-existing files
// (`altimate-base.test.ts` and `altimate-base-harness-smoke.test.ts`).
//
// This module-level singleton is the fix: it claims `issueArmer()` lazily, the first time any
// suite asks for a token, and caches the returned armer closure here. Bun caches modules per
// process, so every suite file that imports `consented()` from this file — regardless of how many
// separate test files load it — shares this exact module instance and therefore this exact cache.
// `issueArmer()` is still claimed exactly once per process; this adds no way to reset, re-claim, or
// otherwise bypass that one-shot guarantee. It is purely a shared cache in front of the single
// legitimate call, so the underlying security property (only one in-process caller can ever obtain
// the ability to arm the production consent authority) is unchanged.
let cachedArmer: ((token: string) => void) | undefined

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: When the unforgeable-consent test runs alone with bun test -t, cachedArmer is still unset, so its direct issueArmer() call succeeds instead of throwing. Claim the shared armer during module setup so filtered tests still exercise a second claim.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/opencode/test/altimate/_fixtures/altimate-base-harness.ts, line 79:

<comment>When the unforgeable-consent test runs alone with `bun test -t`, `cachedArmer` is still unset, so its direct `issueArmer()` call succeeds instead of throwing. Claim the shared armer during module setup so filtered tests still exercise a second claim.</comment>

<file context>
@@ -0,0 +1,95 @@
+// otherwise bypass that one-shot guarantee. It is purely a shared cache in front of the single
+// legitimate call, so the underlying security property (only one in-process caller can ever obtain
+// the ability to arm the production consent authority) is unchanged.
+let cachedArmer: ((token: string) => void) | undefined
+
+function armer(): (token: string) => void {
</file context>


function armer(): (token: string) => void {
if (!cachedArmer) cachedArmer = FreeTierCapability.issueArmer()
return cachedArmer
Comment on lines +81 to +83

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Claim the consent armer before filtered tests run

Because the shared armer is now claimed lazily, running only the existing unforgeable consent: no in-process caller can mint an independent authority test means no earlier call to consented() has occurred. Its direct FreeTierCapability.issueArmer() call therefore succeeds even though the test expects it to throw, so common bun test -t ... workflows fail. Claim the armer during module setup or explicitly initialize it in that test before checking the second-claim behavior.

Useful? React with 👍 / 👎.

}

/**
* Mints a fresh one-shot consent token and arms it against the production consent authority,
* via the shared, process-wide armer above. Every suite should call this instead of claiming
* `FreeTierCapability.issueArmer()` itself.
*/
export function consented(): string {
const token = randomBytes(32).toString("hex")
armer()(token)
return token
}
174 changes: 174 additions & 0 deletions packages/opencode/test/altimate/_fixtures/fake-gateway.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
// Fetch-shaped fake for the two real Altimate Base gateway routes (`POST /register`,
// `POST /v1/chat/completions`). Installed via `spyOn(globalThis, "fetch")` — the same seam
// `test/altimate/altimate-base.test.ts` already uses — so every suite stays hermetic: no port
// binding, no real network, no external dependency. See
// docs/internal/2026-09-04-altimate-base-e2e-harness-plan.md, Deliverable 2, for why an in-process
// fetch fake was chosen over a real local HTTP server.
import { spyOn } from "bun:test"

export const GATEWAY_URL = "https://gateway.test"
export const MODEL_ID = "altimate-base"

export interface RegisterCall {
url: string
installSecretHash: string
cliVersion: string
}
export interface ChatCall {
url: string
authorization: string | null
body: unknown
}

export type RegisterMode =
| { kind: "ok"; apiKey?: string; expiresAt?: string | null; baseUrl?: string; model?: string }
| { kind: "http"; status: number; headers?: Record<string, string> }
| { kind: "network" }
| { kind: "malformed-json" }

export type ChatMode =
| { kind: "ok"; content?: string; status?: number }
| { kind: "throttle-tokens" } // 429 throttling_error, "Limit type: tokens" — non-retryable
| { kind: "throttle-burst"; retryAfterSeconds?: number } // 429 throttling_error, generic — retryable
| { kind: "budget-wallet" } // 429 budget_exceeded, "ExceededBudget: User="
| { kind: "budget-global" } // 429 budget_exceeded, "Budget has been exceeded"
| { kind: "budget-unknown" } // 429 budget_exceeded, neither substring
| { kind: "too-large"; requestBytes?: number; limitBytes?: number } // 413 request_too_large
| { kind: "unauthorized" } // 401
| { kind: "server-error"; status?: number } // 5xx
| { kind: "timeout" } // never resolves until the request's AbortSignal fires
| { kind: "malformed-json" }

/**
* Fetch-shaped fake for the two real gateway routes. Install with `.install()` (typically in
* `beforeEach`), script the next response with `.registerNext()` / `.chatNext()` (each call
* enqueues one response; unscripted calls default to `{ kind: "ok" }`), and read
* `.registerCalls` / `.chatCalls` to assert what was actually sent. Restore with `.restore()`
* (typically in `afterEach`) to remove the `fetch` spy.
*
* One instance per test file. Do not share an instance across files — `bun test` runs each file
* in its own worker process by default, so there is no cross-file state to worry about, but
* sharing an instance across `describe` blocks within one file mixes their scripted queues.
*/
export class FakeGateway {
registerCalls: RegisterCall[] = []
chatCalls: ChatCall[] = []
private registerQueue: RegisterMode[] = []
private chatQueue: ChatMode[] = []
private spy?: ReturnType<typeof spyOn>

registerNext(mode: RegisterMode): this {
this.registerQueue.push(mode)
return this
}
chatNext(mode: ChatMode): this {
this.chatQueue.push(mode)
return this
}

/** Clears scripted queues and call logs without touching the installed spy. */
reset(): this {
this.registerCalls = []
this.chatCalls = []
this.registerQueue = []
this.chatQueue = []
return this
}

install(): this {
this.spy = spyOn(globalThis, "fetch").mockImplementation(
(async (input: RequestInfo | URL, init?: RequestInit) => {
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url
if (url.endsWith("/register")) return this.handleRegister(url, init)
if (url.includes("/v1/chat/completions")) return this.handleChat(url, init)
throw new Error(`FakeGateway: unhandled URL ${url}`)
Comment on lines +81 to +84

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Validate HTTP methods and exact routes in the fake gateway

The fake dispatches only by URL suffix/substring and never checks init.method, so it accepts requests that the real gateway rejects—for example, a registration accidentally changed from POST to GET, or a chat request sent to /v1/chat/completions-invalid. Because replacing global fetch also bypasses native rejection of a GET request with a body, the registration contract tests can remain green while the shipped client fails before reaching the gateway. Match the exact pathname and require POST for both routes.

Useful? React with 👍 / 👎.

}) as typeof fetch,
)
return this
}

restore(): void {
this.spy?.mockRestore()
this.spy = undefined
}

private async handleRegister(url: string, init?: RequestInit): Promise<Response> {
const body = JSON.parse(String(init?.body))
this.registerCalls.push({ url, installSecretHash: body.install_secret_hash, cliVersion: body.cli_version })
const mode = this.registerQueue.shift() ?? { kind: "ok" as const }
if (mode.kind === "network") throw new Error("connection reset")
if (mode.kind === "http") return new Response("", { status: mode.status, headers: mode.headers })
if (mode.kind === "malformed-json") return new Response("{not json", { status: 200 })
return json({
api_key: mode.apiKey ?? "sk-altimate-base-fake",
base_url: mode.baseUrl ?? GATEWAY_URL,
model: mode.model ?? MODEL_ID,
...(mode.expiresAt === null
? {}
: { expires_at: mode.expiresAt ?? new Date(Date.now() + 86_400_000).toISOString() }),
})
}

private async handleChat(url: string, init?: RequestInit): Promise<Response> {
const authorization = new Headers(init?.headers).get("Authorization")
this.chatCalls.push({ url, authorization, body: init?.body ? JSON.parse(String(init.body)) : undefined })
const mode = this.chatQueue.shift() ?? { kind: "ok" as const }
switch (mode.kind) {
case "ok":
return json(
{ choices: [{ message: { content: mode.content ?? "hello from altimate-base" } }] },
mode.status ?? 200,
)
case "throttle-tokens":
return throttleError("Limit type: tokens. Key=sk-fake. Current: 300000, Limit: 262144")
case "throttle-burst":
return throttleError("burst limit exceeded", mode.retryAfterSeconds)
case "budget-wallet":
return budgetError("ExceededBudget: User=principal-fake over budget. Spend=0.26, Budget=0.25")
case "budget-global":
return budgetError("Budget has been exceeded! Current cost: 50.01, Max budget: 50")
case "budget-unknown":
return budgetError("spend limit reached")
case "too-large": {
const size = mode.requestBytes ?? 179_608
const limit = mode.limitBytes ?? 128_000
const message = `Request is ${size} bytes; the free tier limit is ${limit} bytes.`
return json(
{
error: {
message,
code: "413",
provider_specific_fields: { error: { code: "request_too_large", message } },
},
},
413,
)
}
case "unauthorized":
return new Response("", { status: 401 })
case "server-error":
return new Response("upstream error", { status: mode.status ?? 500 })
case "timeout":

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: The timeout chat mode never settles when the request has no AbortSignal, and also hangs if the signal is already aborted before the listener attaches. Check the signal and reject immediately in those cases so a mis-scripted test degrades to a clear rejection instead of a hang until Bun's global timeout.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/opencode/test/altimate/_fixtures/fake-gateway.ts, line 151:

<comment>The `timeout` chat mode never settles when the request has no AbortSignal, and also hangs if the signal is already aborted before the listener attaches. Check the signal and reject immediately in those cases so a mis-scripted test degrades to a clear rejection instead of a hang until Bun's global timeout.</comment>

<file context>
@@ -0,0 +1,174 @@
+        return new Response("", { status: 401 })
+      case "server-error":
+        return new Response("upstream error", { status: mode.status ?? 500 })
+      case "timeout":
+        return new Promise<Response>((_resolve, reject) => {
+          init?.signal?.addEventListener("abort", () => reject(init.signal?.reason), { once: true })
</file context>
Suggested change
case "timeout":
case "timeout": {
const signal = init?.signal
if (signal?.aborted) return Promise.reject(signal.reason)
return new Promise<Response>((_resolve, reject) => {
signal?.addEventListener("abort", () => reject(signal.reason), { once: true })
})
}

return new Promise<Response>((_resolve, reject) => {
init?.signal?.addEventListener("abort", () => reject(init.signal?.reason), { once: true })
})
case "malformed-json":
return new Response("{not json", { status: 200 })
}
}
}

function json(body: Record<string, unknown>, status = 200): Response {
return new Response(JSON.stringify(body), { status, headers: { "Content-Type": "application/json" } })
}

function throttleError(message: string, retryAfterSeconds?: number): Response {
return new Response(JSON.stringify({ error: { type: "throttling_error", message } }), {
status: 429,
headers: retryAfterSeconds !== undefined ? { "retry-after": String(retryAfterSeconds) } : {},
})
}

function budgetError(message: string): Response {
return new Response(JSON.stringify({ error: { type: "budget_exceeded", message } }), { status: 429 })
}
Loading
Loading