This commit is contained in:
@@ -0,0 +1,224 @@
|
||||
import { afterEach, expect, test } from "bun:test";
|
||||
import { chmod, mkdir, mkdtemp, readFile, readdir, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const installer = fileURLToPath(new URL("../install-local.sh", import.meta.url));
|
||||
const directories: string[] = [];
|
||||
const jsonc = '{\n // This file must remain untouched.\n "theme": "dark"\n}\n';
|
||||
const oldPlugin = 'export default { id: "old", setup() { return "old"; } };\n';
|
||||
type FixtureConfig =
|
||||
| { permissions: { shell: string }; plugins: { package: string }[] }
|
||||
| {
|
||||
theme: string;
|
||||
plugins: {
|
||||
package: string;
|
||||
options: { mode: string; coordinate: string; lunaPriority: boolean };
|
||||
}[];
|
||||
}
|
||||
| { plugins: string };
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(
|
||||
directories.splice(0).map((path) => rm(path, { recursive: true, force: true })),
|
||||
);
|
||||
});
|
||||
|
||||
async function run(command: string[], cwd: string, env: NodeJS.ProcessEnv) {
|
||||
const child = Bun.spawn(command, {
|
||||
cwd,
|
||||
env,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
timeout: 20_000,
|
||||
});
|
||||
const [exitCode, stdout, stderr] = await Promise.all([
|
||||
child.exited,
|
||||
new Response(child.stdout).text(),
|
||||
new Response(child.stderr).text(),
|
||||
]);
|
||||
return { exitCode, stdout, stderr };
|
||||
}
|
||||
|
||||
async function fixture(configValue?: FixtureConfig) {
|
||||
const directory = await mkdtemp(join(tmpdir(), "stanp-local-install-"));
|
||||
directories.push(directory);
|
||||
const checkout = join(directory, "checkout with spaces");
|
||||
const cwd = join(directory, "different cwd");
|
||||
const config = join(directory, "config with spaces");
|
||||
const bin = join(directory, "bin");
|
||||
const installed = join(config, "plugins", "stanp-opencode");
|
||||
await Promise.all([
|
||||
mkdir(checkout),
|
||||
mkdir(cwd),
|
||||
mkdir(bin),
|
||||
mkdir(installed, { recursive: true }),
|
||||
]);
|
||||
await Promise.all([
|
||||
Bun.write(join(checkout, "install-local.sh"), Bun.file(installer)),
|
||||
writeFile(
|
||||
join(checkout, "package.json"),
|
||||
JSON.stringify({
|
||||
name: "stanp-opencode",
|
||||
version: "1.0.0",
|
||||
type: "module",
|
||||
exports: { ".": "./dist/index.js", "./tui": { import: "./dist/tui.js" } },
|
||||
}),
|
||||
),
|
||||
writeFile(join(checkout, "bun.lock"), ""),
|
||||
writeFile(join(config, "opencode.jsonc"), jsonc),
|
||||
writeFile(
|
||||
join(config, "opencode.json"),
|
||||
JSON.stringify(
|
||||
configValue ?? {
|
||||
permissions: { shell: "ask" },
|
||||
plugins: [{ package: "existing-plugin" }],
|
||||
},
|
||||
),
|
||||
),
|
||||
writeFile(join(installed, "index.js"), oldPlugin),
|
||||
writeFile(
|
||||
join(installed, "package.json"),
|
||||
'{"name":"stanp-opencode","type":"module","main":"./index.js"}\n',
|
||||
),
|
||||
writeFile(join(config, "plugins", "stanp-opencode.js"), "legacy\n"),
|
||||
writeFile(
|
||||
join(bin, "bun"),
|
||||
`#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
if [[ "\${1:-}" == "install" ]]; then
|
||||
[[ "\${2:-}" == "--frozen-lockfile" ]]
|
||||
printf '%s\n' "$PWD" >> "$CALLS"
|
||||
[[ "$MODE" != "dependency" ]]
|
||||
exit
|
||||
fi
|
||||
if [[ "\${1:-}" == "run" && "\${2:-}" == "build" ]]; then
|
||||
printf '%s\n' "$PWD" >> "$CALLS"
|
||||
[[ "$MODE" != "build" ]]
|
||||
mkdir -p dist
|
||||
if [[ "$MODE" == "validation" ]]; then
|
||||
printf '%s\n' 'export default { id: "stanp-opencode", server() {} };' > dist/index.js
|
||||
else
|
||||
printf 'export default { id: "stanp-opencode", setup() { return "%s"; } };\n' "$GENERATION" > dist/index.js
|
||||
fi
|
||||
printf '%s\n' 'export default { id: "stanp-opencode.quota-tui", setup() {} };' > dist/tui.js
|
||||
exit
|
||||
fi
|
||||
exec "$REAL_BUN" "$@"
|
||||
`,
|
||||
),
|
||||
writeFile(
|
||||
join(bin, "curl"),
|
||||
`#!/usr/bin/env bash
|
||||
touch "$FETCH_MARKER"
|
||||
printf 'release fetching is forbidden\n' >&2
|
||||
exit 99
|
||||
`,
|
||||
),
|
||||
]);
|
||||
await Promise.all([chmod(join(bin, "bun"), 0o755), chmod(join(bin, "curl"), 0o755)]);
|
||||
const env = {
|
||||
...Bun.env,
|
||||
PATH: `${bin}:${dirname(process.execPath)}:${Bun.env.PATH ?? "/usr/bin:/bin"}`,
|
||||
HOME: join(directory, "home"),
|
||||
XDG_CONFIG_HOME: join(directory, "ignored-xdg"),
|
||||
OPENCODE_CONFIG_DIR: config,
|
||||
REAL_BUN: process.execPath,
|
||||
MODE: "success",
|
||||
GENERATION: "first",
|
||||
CALLS: join(directory, "calls"),
|
||||
FETCH_MARKER: join(directory, "release-fetch-attempted"),
|
||||
};
|
||||
return { directory, checkout, cwd, config, installed, env };
|
||||
}
|
||||
|
||||
test("builds from its checkout and installs defaults when invoked elsewhere", async () => {
|
||||
const setup = await fixture();
|
||||
const result = await run(
|
||||
["bash", join(setup.checkout, "install-local.sh")],
|
||||
setup.cwd,
|
||||
setup.env,
|
||||
);
|
||||
expect(result, result.stderr).toMatchObject({ exitCode: 0 });
|
||||
expect(await readFile(setup.env.CALLS, "utf8")).toBe(`${setup.checkout}\n${setup.checkout}\n`);
|
||||
expect(await Bun.file(setup.env.FETCH_MARKER).exists()).toBe(false);
|
||||
expect(await readFile(join(setup.config, "opencode.jsonc"), "utf8")).toBe(jsonc);
|
||||
expect(JSON.parse(await readFile(join(setup.config, "opencode.json"), "utf8"))).toEqual({
|
||||
$schema: "https://opencode.ai/config.json",
|
||||
permissions: { shell: "ask" },
|
||||
plugins: [
|
||||
{ package: "existing-plugin" },
|
||||
{
|
||||
package: "./plugins/stanp-opencode",
|
||||
options: { mode: "gpt", provider: "openai", lunaPriority: true },
|
||||
},
|
||||
],
|
||||
});
|
||||
const imported = await run(
|
||||
[
|
||||
process.execPath,
|
||||
"-e",
|
||||
"const { default: plugin } = await import(process.argv[1]); console.log(plugin.setup());",
|
||||
setup.installed,
|
||||
],
|
||||
setup.cwd,
|
||||
setup.env,
|
||||
);
|
||||
expect(imported, imported.stderr).toMatchObject({ exitCode: 0, stdout: "first\n" });
|
||||
expect((await readdir(setup.installed)).sort()).toEqual(["index.js", "package.json", "tui.js"]);
|
||||
expect(await Bun.file(join(setup.config, "plugins", "stanp-opencode.js")).exists()).toBe(false);
|
||||
});
|
||||
|
||||
test("reinstall replaces bundles while preserving existing settings and plugin options", async () => {
|
||||
const configured = {
|
||||
theme: "dark",
|
||||
plugins: [
|
||||
{
|
||||
package: "./plugins/stanp-opencode",
|
||||
options: { mode: "custom", coordinate: "provider/model", lunaPriority: false },
|
||||
},
|
||||
],
|
||||
};
|
||||
const setup = await fixture(configured);
|
||||
const first = await run(
|
||||
["bash", join(setup.checkout, "install-local.sh")],
|
||||
setup.cwd,
|
||||
setup.env,
|
||||
);
|
||||
expect(first, first.stderr).toMatchObject({ exitCode: 0 });
|
||||
const second = await run(["bash", join(setup.checkout, "install-local.sh")], setup.cwd, {
|
||||
...setup.env,
|
||||
GENERATION: "second",
|
||||
});
|
||||
expect(second, second.stderr).toMatchObject({ exitCode: 0 });
|
||||
expect(JSON.parse(await readFile(join(setup.config, "opencode.json"), "utf8"))).toEqual({
|
||||
$schema: "https://opencode.ai/config.json",
|
||||
...configured,
|
||||
});
|
||||
expect(await readFile(join(setup.installed, "index.js"), "utf8")).toContain('return "second"');
|
||||
});
|
||||
|
||||
for (const mode of ["dependency", "build", "validation", "config"] as const) {
|
||||
test(`${mode} failure leaves the previous installation and configuration untouched`, async () => {
|
||||
const invalidConfig = mode === "config" ? { plugins: "not-an-array" } : undefined;
|
||||
const setup = await fixture(invalidConfig);
|
||||
const beforeConfig = await readFile(join(setup.config, "opencode.json"), "utf8");
|
||||
const result = await run(["bash", join(setup.checkout, "install-local.sh")], setup.cwd, {
|
||||
...setup.env,
|
||||
MODE: mode === "config" ? "success" : mode,
|
||||
});
|
||||
expect(result.exitCode, result.stderr).not.toBe(0);
|
||||
expect(await readFile(join(setup.installed, "index.js"), "utf8")).toBe(oldPlugin);
|
||||
expect(await readFile(join(setup.config, "opencode.json"), "utf8")).toBe(beforeConfig);
|
||||
expect(await readFile(join(setup.config, "opencode.jsonc"), "utf8")).toBe(jsonc);
|
||||
expect(await readFile(join(setup.config, "plugins", "stanp-opencode.js"), "utf8")).toBe(
|
||||
"legacy\n",
|
||||
);
|
||||
expect((await readdir(join(setup.config, "plugins"))).sort()).toEqual([
|
||||
"stanp-opencode",
|
||||
"stanp-opencode.js",
|
||||
]);
|
||||
expect(await Bun.file(setup.env.FETCH_MARKER).exists()).toBe(false);
|
||||
});
|
||||
}
|
||||
+28
-2
@@ -46,8 +46,14 @@ async function host(
|
||||
const registration = { async dispose() {} };
|
||||
let transform!: (editor: AgentEditor) => void;
|
||||
let reloaded = Promise.withResolvers<void>();
|
||||
let events!: ReadableStreamDefaultController<{ type: string }>;
|
||||
const stream = new ReadableStream<{ type: string }>({
|
||||
let events!: ReadableStreamDefaultController<{
|
||||
type: string;
|
||||
location?: { directory: string; workspaceID?: string };
|
||||
}>;
|
||||
const stream = new ReadableStream<{
|
||||
type: string;
|
||||
location?: { directory: string; workspaceID?: string };
|
||||
}>({
|
||||
start(controller) {
|
||||
events = controller;
|
||||
},
|
||||
@@ -101,6 +107,7 @@ async function host(
|
||||
// SAFETY: This test host implements the native APIs used by setup; unrelated context
|
||||
// services are deliberately absent so unexpected new dependencies fail the test.
|
||||
const context = {
|
||||
location: { directory: "/test" },
|
||||
options,
|
||||
catalog: {
|
||||
model: {
|
||||
@@ -227,6 +234,10 @@ async function host(
|
||||
events.enqueue({ type: "agent.updated" });
|
||||
await reloaded.promise;
|
||||
},
|
||||
changeBuildPermissionsElsewhere(permissions: Agent.Info["permissions"]) {
|
||||
build.permissions = [...permissions];
|
||||
events.enqueue({ type: "agent.updated", location: { directory: "/other" } });
|
||||
},
|
||||
async changeCodegraph(
|
||||
server:
|
||||
| { type: "local"; command: string[]; codemode?: boolean; disabled?: boolean }
|
||||
@@ -477,6 +488,21 @@ test("late build permission overrides remain effective for workers and coordinat
|
||||
).toBe("deny");
|
||||
});
|
||||
|
||||
test("events from another location do not reload this location's agents", async () => {
|
||||
const result = await host();
|
||||
result.changeBuildPermissionsElsewhere([
|
||||
...result.build.permissions,
|
||||
{ action: "read", resource: "*.env", effect: "deny" },
|
||||
]);
|
||||
await result.refresh(allModels);
|
||||
expect(
|
||||
result.agents
|
||||
.get("coordinate")
|
||||
?.permissions.filter((rule) => rule.action === "read" && rule.resource === "*.env")
|
||||
.at(-1)?.effect,
|
||||
).toBe("ask");
|
||||
});
|
||||
|
||||
test("todo lists persist within one session without leaking to another", async () => {
|
||||
const result = await host();
|
||||
const todos = [{ id: "1", content: "Implement", status: "in_progress", priority: "high" }];
|
||||
|
||||
@@ -95,6 +95,67 @@ test("normalizes additional named limits with stable IDs distinct from equal dur
|
||||
).toEqual(["codex_luna:primary"]);
|
||||
});
|
||||
|
||||
test("ignores Spark and representative reserve limits while preserving other quotas", () => {
|
||||
const normalized = normalizeQuota({
|
||||
...plus,
|
||||
additional_rate_limits: [
|
||||
{
|
||||
limit_name: "gpt 5_3 codex spark",
|
||||
metered_feature: "CODEX-BENGALFOX",
|
||||
rate_limit: {
|
||||
primary_window: { used_percent: "invalid" },
|
||||
},
|
||||
},
|
||||
{
|
||||
limit_name: "Luna Reserve",
|
||||
metered_feature: "codex_luna",
|
||||
rate_limit: { primary_window: { used_percent: "invalid" } },
|
||||
},
|
||||
{
|
||||
limit_name: "Luna overflow",
|
||||
metered_feature: "codex_luna_reserve",
|
||||
rate_limit: { primary_window: { used_percent: "invalid" } },
|
||||
},
|
||||
{
|
||||
limit_name: "GPT-5.6 Luna",
|
||||
metered_feature: "codex_luna",
|
||||
rate_limit: { primary_window: plus.rate_limit.primary_window },
|
||||
},
|
||||
{
|
||||
limit_name: "Preserved capacity",
|
||||
metered_feature: "codex_preserved",
|
||||
rate_limit: { primary_window: plus.rate_limit.primary_window },
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(normalized?.windows.map((window) => window.id)).toEqual([
|
||||
"codex:primary",
|
||||
"codex:secondary",
|
||||
"codex_luna:primary",
|
||||
"codex_preserved:primary",
|
||||
]);
|
||||
expect(normalized?.windows.slice(2).map((window) => window.label)).toEqual([
|
||||
"GPT-5.6 Luna",
|
||||
"Preserved capacity",
|
||||
]);
|
||||
});
|
||||
|
||||
test("returns no quota for a Spark-only payload", () => {
|
||||
expect(
|
||||
normalizeQuota({
|
||||
plan_type: "pro",
|
||||
additional_rate_limits: [
|
||||
{
|
||||
limit_name: "GPT-5.3-Codex-Spark",
|
||||
metered_feature: "codex_bengalfox",
|
||||
rate_limit: { primary_window: plus.rate_limit.primary_window },
|
||||
},
|
||||
],
|
||||
}),
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
function fixture(
|
||||
credential:
|
||||
| {
|
||||
@@ -190,6 +251,96 @@ function fixture(
|
||||
};
|
||||
}
|
||||
|
||||
async function accountFingerprint(id: string) {
|
||||
const bytes = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(`account:${id}`));
|
||||
return Buffer.from(bytes).toString("hex");
|
||||
}
|
||||
|
||||
test("removes cached Spark and representative reserve windows and consumption while offline", async () => {
|
||||
const cached = {
|
||||
account: await accountFingerprint("account-one"),
|
||||
latest: {
|
||||
plan: "pro",
|
||||
fetchedAt: 123,
|
||||
windows: [
|
||||
{
|
||||
id: "codex:primary",
|
||||
label: null,
|
||||
seconds: 18_000,
|
||||
used: 12.5,
|
||||
resetsAt: 2_000_000_000_000,
|
||||
},
|
||||
{
|
||||
id: "CODEX-BENGALFOX:primary",
|
||||
label: "GPT 5.3 Codex Spark",
|
||||
seconds: 18_000,
|
||||
used: 80,
|
||||
resetsAt: 2_000_000_000_000,
|
||||
},
|
||||
{
|
||||
id: "codex_luna_reserve:primary",
|
||||
label: "Luna Reserve",
|
||||
seconds: 18_000,
|
||||
used: 60,
|
||||
resetsAt: 2_000_000_000_000,
|
||||
},
|
||||
{
|
||||
id: "codex_luna:primary",
|
||||
label: "GPT-5.6 Luna",
|
||||
seconds: 18_000,
|
||||
used: 20,
|
||||
resetsAt: 2_000_000_000_000,
|
||||
},
|
||||
],
|
||||
},
|
||||
sessions: {
|
||||
root: {
|
||||
"codex:primary": {
|
||||
resetsAt: 2_000_000_000_000,
|
||||
lastUsed: 12.5,
|
||||
consumed: 2,
|
||||
},
|
||||
"CODEX-BENGALFOX:primary": {
|
||||
resetsAt: 2_000_000_000_000,
|
||||
lastUsed: 80,
|
||||
consumed: 40,
|
||||
},
|
||||
"codex_luna_reserve:primary": {
|
||||
resetsAt: 2_000_000_000_000,
|
||||
lastUsed: 60,
|
||||
consumed: 30,
|
||||
},
|
||||
"codex_luna:primary": {
|
||||
resetsAt: 2_000_000_000_000,
|
||||
lastUsed: 20,
|
||||
consumed: 3,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const result = fixture(
|
||||
{
|
||||
type: "oauth",
|
||||
access: "opaque",
|
||||
refresh: "refresh",
|
||||
metadata: { accountID: "account-one" },
|
||||
},
|
||||
[new Error("offline")],
|
||||
1_000_000,
|
||||
[Promise.resolve(cached)],
|
||||
);
|
||||
|
||||
await result.tracker.init();
|
||||
const status = await result.tracker.status("root");
|
||||
expect(status?.windows.map((window) => window.id)).toEqual([
|
||||
"codex:primary",
|
||||
"codex_luna:primary",
|
||||
]);
|
||||
expect(status?.windows[0]?.sessionConsumed).toBe(2);
|
||||
expect(status?.windows[1]?.sessionConsumed).toBe(3);
|
||||
await result.tracker.dispose();
|
||||
});
|
||||
|
||||
test("activates only for OAuth and establishes an awaited root baseline shared by children", async () => {
|
||||
const key = fixture({ type: "key", key: "secret" }, [plus]);
|
||||
await key.tracker.init();
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import type { Plugin } from "@opencode/plugin/tui";
|
||||
import { testRender, type JSX } from "@opentui/solid";
|
||||
import { createSignal } from "solid-js";
|
||||
import { build } from "../scripts/build.js";
|
||||
import type { QuotaStatus } from "../src/quota.js";
|
||||
|
||||
type SlotRender = (input: { readonly sessionID: string }) => JSX.Element;
|
||||
|
||||
const status = (plan: string, remaining: number): QuotaStatus => ({
|
||||
plan,
|
||||
fetchedAt: 1,
|
||||
windows: [
|
||||
{
|
||||
id: "codex:primary",
|
||||
label: null,
|
||||
seconds: 18_000,
|
||||
used: 100 - remaining,
|
||||
resetsAt: 2,
|
||||
remaining,
|
||||
sessionConsumed: 1.5,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
test("built quota slot stays reactive across pending, empty, failure, update, and session states", async () => {
|
||||
await build();
|
||||
const { default: plugin } = await import("../dist/tui.js");
|
||||
const pending = Promise.withResolvers<QuotaStatus | null>();
|
||||
const responses: Array<() => Promise<QuotaStatus | null>> = [() => pending.promise];
|
||||
const sessions: string[] = [];
|
||||
let slotRender: SlotRender | undefined;
|
||||
let notifyUpdated: (() => void) | undefined;
|
||||
|
||||
const quota = {
|
||||
status(input: { sessionID: string }) {
|
||||
sessions.push(input.sessionID);
|
||||
const response = responses.shift();
|
||||
if (!response) throw new Error("Missing test response");
|
||||
return response();
|
||||
},
|
||||
events: {
|
||||
on(_event: "updated", callback: () => void) {
|
||||
notifyUpdated = callback;
|
||||
return () => undefined;
|
||||
},
|
||||
},
|
||||
};
|
||||
const contextFixture = {
|
||||
client: { rpc: () => quota },
|
||||
ui: {
|
||||
slot(claim: { render: SlotRender }) {
|
||||
slotRender = claim.render;
|
||||
return () => undefined;
|
||||
},
|
||||
},
|
||||
theme: { text: { default: "#ffffff", muted: "#888888" } },
|
||||
};
|
||||
// SAFETY: The slot setup only reads the client RPC, slot registration, and theme
|
||||
// members implemented by this focused rendering fixture.
|
||||
const context = Object.assign({} as Plugin.Context, contextFixture);
|
||||
const cleanup = await plugin.setup(context);
|
||||
if (!slotRender || !notifyUpdated) throw new Error("Plugin did not register its slot");
|
||||
|
||||
const [sessionID, setSessionID] = createSignal("session-one");
|
||||
const renderSlot = slotRender;
|
||||
const setup = await testRender(
|
||||
() =>
|
||||
renderSlot({
|
||||
get sessionID() {
|
||||
return sessionID();
|
||||
},
|
||||
}),
|
||||
{ width: 70, height: 5 },
|
||||
);
|
||||
|
||||
try {
|
||||
await setup.renderOnce();
|
||||
expect(setup.captureCharFrame()).not.toContain("ChatGPT quota");
|
||||
|
||||
pending.resolve(status("plus", 75));
|
||||
await setup.waitForFrame((frame) => frame.includes("ChatGPT quota (plus)"));
|
||||
expect(setup.captureCharFrame()).toContain("5h: 75% left · ~1.5pp this session");
|
||||
|
||||
responses.push(async () => null);
|
||||
notifyUpdated();
|
||||
await setup.waitForFrame((frame) => !frame.includes("ChatGPT quota"));
|
||||
|
||||
responses.push(async () => {
|
||||
throw new Error("disconnected");
|
||||
});
|
||||
notifyUpdated();
|
||||
await setup.waitFor(() => sessions.length === 3);
|
||||
expect(setup.captureCharFrame()).not.toContain("ChatGPT quota");
|
||||
|
||||
responses.push(async () => status("pro", 42));
|
||||
setSessionID("session-two");
|
||||
await setup.waitForFrame((frame) => frame.includes("ChatGPT quota (pro)"));
|
||||
expect(sessions).toEqual(["session-one", "session-one", "session-one", "session-two"]);
|
||||
} finally {
|
||||
setup.renderer.destroy();
|
||||
if (cleanup) await cleanup();
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user