Compare commits
10
Commits
v1.3.2
..
20f6087340
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
20f6087340 | ||
|
|
c401290080 | ||
|
|
9bd92ba53c | ||
|
|
f6cae21bfa | ||
|
|
1bc5c329a8 | ||
|
|
c973738b60 | ||
|
|
c24e38bbf2 | ||
|
|
15650d9bb7 | ||
|
|
5338a1f48b | ||
|
|
c9ff8dc629 |
@@ -12,6 +12,12 @@ Run the installer without sudo:
|
||||
curl -fsSL https://git.krtss.de/StanPonomarev/stanp-opencode/raw/branch/main/install.sh | bash
|
||||
```
|
||||
|
||||
Pass `--playwright` to let the plugin automatically configure a Playwright MCP server:
|
||||
|
||||
```sh
|
||||
curl -fsSL https://git.krtss.de/StanPonomarev/stanp-opencode/raw/branch/main/install.sh | bash -s -- --playwright
|
||||
```
|
||||
|
||||
Rerun the command to upgrade to the latest published release.
|
||||
|
||||
## Install from a local checkout
|
||||
@@ -22,6 +28,8 @@ This requires Bash, jq, and Bun 1.3.14+:
|
||||
bash /path/to/stanp-opencode/install-local.sh
|
||||
```
|
||||
|
||||
Add `--playwright` to this command to enable automatic Playwright MCP configuration.
|
||||
|
||||
The script installs dependencies and builds the checkout before installing it.
|
||||
|
||||
After either installation, restart `opencode2` or run `opencode2 service restart` for an existing service. Connect OpenAI and select `coordinate`. The required models must be available for the workers to run.
|
||||
@@ -36,7 +44,7 @@ For an installed plugin, use `./plugins/stanp-opencode` as the plugin package pa
|
||||
|
||||
## Playwright
|
||||
|
||||
The plugin prefers a local Playwright MCP installation. If ordinary Playwright is installed, it can download and run the MCP package with `npx`; otherwise Docker is required for the automatic fallback. Automatically configured servers allow five minutes for startup because the first launch may need to download a package or container image. Docker reuses its cached image on later launches. Existing `playwright` configuration, including its timeout, is preserved.
|
||||
When installed with `--playwright`, the plugin prefers a local Playwright MCP installation. If ordinary Playwright is installed, it can download and run the MCP package with `npx`; otherwise Docker is required for the automatic fallback. Automatically configured servers allow five minutes for startup because the first launch may need to download a package or container image. Docker reuses its cached image on later launches. Existing `playwright` configuration, including its timeout, is preserved.
|
||||
|
||||
If `opencode mcp list` reports `MCP error -32001: Request timed out`, prefetch the Docker fallback with `docker pull mcr.microsoft.com/playwright/mcp`, or install `@playwright/mcp` in the project so the plugin can launch it directly. Then reconnect the server from `/mcps` or restart OpenCode. A custom `playwright` entry under `mcp.servers` can also set its own `timeout.startup` in milliseconds.
|
||||
|
||||
|
||||
+16
-3
@@ -1,6 +1,15 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
playwright=false
|
||||
if (($#)); then
|
||||
if [[ "$#" -ne 1 || "$1" != "--playwright" ]]; then
|
||||
printf 'Usage: %s [--playwright]\n' "$0" >&2
|
||||
exit 2
|
||||
fi
|
||||
playwright=true
|
||||
fi
|
||||
|
||||
for command in bun jq; do
|
||||
command -v "$command" >/dev/null || { printf 'Required command not found: %s\n' "$command" >&2; exit 1; }
|
||||
done
|
||||
@@ -43,15 +52,19 @@ if [[ ! -e "$config_source" ]]; then
|
||||
config_source="$install/empty-config.json"
|
||||
printf '{}\n' > "$config_source"
|
||||
fi
|
||||
jq -e '
|
||||
jq -e --argjson playwright "$playwright" '
|
||||
if type != "object" then error("opencode.json must contain an object") else . end
|
||||
| if (.plugins // []) | type != "array" then error("opencode.json plugins must be an array") else . end
|
||||
| .["$schema"] = (.["$schema"] // "https://opencode.ai/config.json")
|
||||
| .plugins = (.plugins // [])
|
||||
| if any(.plugins[]?; .package == "./plugins/stanp-opencode") then . else
|
||||
| if any(.plugins[]?; .package == "./plugins/stanp-opencode") then
|
||||
if $playwright then
|
||||
.plugins |= map(if .package == "./plugins/stanp-opencode" then .options = ((.options // {}) + { playwright: true }) else . end)
|
||||
else . end
|
||||
else
|
||||
.plugins += [{
|
||||
package: "./plugins/stanp-opencode",
|
||||
options: { mode: "gpt", provider: "openai", lunaPriority: true }
|
||||
options: ({ mode: "gpt", provider: "openai", lunaPriority: true } + if $playwright then { playwright: true } else {} end)
|
||||
}]
|
||||
end
|
||||
' "$config_source" > "$install/opencode.json"
|
||||
|
||||
+16
-3
@@ -1,6 +1,15 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
playwright=false
|
||||
if (($#)); then
|
||||
if [[ "$#" -ne 1 || "$1" != "--playwright" ]]; then
|
||||
printf 'Usage: %s [--playwright]\n' "$0" >&2
|
||||
exit 2
|
||||
fi
|
||||
playwright=true
|
||||
fi
|
||||
|
||||
for command in bun curl jq sha256sum tar; do
|
||||
command -v "$command" >/dev/null || { printf 'Required command not found: %s\n' "$command" >&2; exit 1; }
|
||||
done
|
||||
@@ -57,15 +66,19 @@ if [[ ! -e "$config_source" ]]; then
|
||||
config_source="$install/empty-config.json"
|
||||
printf '{}\n' > "$config_source"
|
||||
fi
|
||||
jq -e '
|
||||
jq -e --argjson playwright "$playwright" '
|
||||
if type != "object" then error("opencode.json must contain an object") else . end
|
||||
| if (.plugins // []) | type != "array" then error("opencode.json plugins must be an array") else . end
|
||||
| .["$schema"] = (.["$schema"] // "https://opencode.ai/config.json")
|
||||
| .plugins = (.plugins // [])
|
||||
| if any(.plugins[]?; .package == "./plugins/stanp-opencode") then . else
|
||||
| if any(.plugins[]?; .package == "./plugins/stanp-opencode") then
|
||||
if $playwright then
|
||||
.plugins |= map(if .package == "./plugins/stanp-opencode" then .options = ((.options // {}) + { playwright: true }) else . end)
|
||||
else . end
|
||||
else
|
||||
.plugins += [{
|
||||
package: "./plugins/stanp-opencode",
|
||||
options: { mode: "gpt", provider: "openai", lunaPriority: true }
|
||||
options: ({ mode: "gpt", provider: "openai", lunaPriority: true } + if $playwright then { playwright: true } else {} end)
|
||||
}]
|
||||
end
|
||||
' "$config_source" > "$install/opencode.json"
|
||||
|
||||
@@ -17,6 +17,7 @@ const assignment = z.strictObject({
|
||||
const common = {
|
||||
provider: z.string().trim().min(1).default("openai"),
|
||||
lunaPriority: z.boolean().default(true),
|
||||
playwright: z.boolean().default(false),
|
||||
};
|
||||
|
||||
export const settings = z.discriminatedUnion("mode", [
|
||||
|
||||
+14
-6
@@ -114,10 +114,12 @@ export default Plugin.define({
|
||||
const assignments = selected.mode === "gpt" ? gptAgents : selected.agents;
|
||||
let workers: (typeof workerNames)[number][] = [];
|
||||
const active = new Set<string>();
|
||||
const playwrightCommand = await playwrightMcpCommand(context.location.directory);
|
||||
const playwrightCommand = selected.playwright
|
||||
? await playwrightMcpCommand(context.location.directory)
|
||||
: undefined;
|
||||
|
||||
await context.mcp.transform((editor) => {
|
||||
if (!editor.get("playwright"))
|
||||
if (playwrightCommand && !editor.get("playwright"))
|
||||
editor.set("playwright", {
|
||||
type: "local",
|
||||
command: playwrightCommand,
|
||||
@@ -166,6 +168,9 @@ export default Plugin.define({
|
||||
const assignment = assignments[name];
|
||||
const available = models.get(assignment.model)!;
|
||||
editor.update(name, (agent) => {
|
||||
const hasVariant = available.model.variants.some(
|
||||
({ id }) => id === assignment.effort,
|
||||
);
|
||||
agent.name = Agent.Name.make(name);
|
||||
agent.mode = "subagent";
|
||||
agent.hidden = false;
|
||||
@@ -174,10 +179,9 @@ export default Plugin.define({
|
||||
providerID: Provider.ID.make(available.model.providerID),
|
||||
id: Model.ID.make(available.model.id),
|
||||
};
|
||||
if (hasVariant) agent.model.variant = Model.VariantID.make(assignment.effort);
|
||||
agent.request = {
|
||||
settings: {
|
||||
reasoningEffort: assignment.effort,
|
||||
},
|
||||
settings: hasVariant ? {} : { reasoningEffort: assignment.effort },
|
||||
headers: {},
|
||||
body: {},
|
||||
};
|
||||
@@ -191,6 +195,9 @@ export default Plugin.define({
|
||||
}
|
||||
editor.update("coordinate", (agent) => {
|
||||
const available = models.get(coordinate.model)!;
|
||||
const hasVariant = available.model.variants.some(
|
||||
({ id }) => id === coordinate.effort,
|
||||
);
|
||||
agent.name = Agent.Name.make("coordinate");
|
||||
agent.mode = "primary";
|
||||
agent.hidden = false;
|
||||
@@ -200,8 +207,9 @@ export default Plugin.define({
|
||||
providerID: Provider.ID.make(available.model.providerID),
|
||||
id: Model.ID.make(available.model.id),
|
||||
};
|
||||
if (hasVariant) agent.model.variant = Model.VariantID.make(coordinate.effort);
|
||||
agent.request = {
|
||||
settings: { reasoningEffort: coordinate.effort },
|
||||
settings: hasVariant ? {} : { reasoningEffort: coordinate.effort },
|
||||
headers: {},
|
||||
body: {},
|
||||
};
|
||||
|
||||
+24
-3
@@ -3,23 +3,44 @@ import { createResource, createSignal, For, Show } from "solid-js";
|
||||
import type { QuotaStatus, QuotaWindow } from "./quota.js";
|
||||
import { QuotaRpc } from "./quota-rpc.js";
|
||||
|
||||
function percent(value: number) {
|
||||
return value < 0.1 && value > 0 ? "<0.1" : value.toFixed(value < 10 ? 1 : 0);
|
||||
}
|
||||
|
||||
function duration(seconds: number) {
|
||||
if (seconds % 86_400 === 0) return `${seconds / 86_400}d`;
|
||||
if (seconds % 3_600 === 0) return `${seconds / 3_600}h`;
|
||||
return `${Math.round(seconds / 60)}m`;
|
||||
}
|
||||
|
||||
function percent(value: number) {
|
||||
return value < 0.1 && value > 0 ? "<0.1" : value.toFixed(value < 10 ? 1 : 0);
|
||||
function resetTiming(resetsAt: number, now: number) {
|
||||
if (!Number.isFinite(resetsAt) || resetsAt <= 0 || !Number.isFinite(now)) return "unavailable";
|
||||
const remaining = resetsAt - now;
|
||||
if (remaining <= 0) return "due";
|
||||
if (remaining < 60_000) return "<1m";
|
||||
|
||||
const minutes = Math.floor(remaining / 60_000);
|
||||
const days = Math.floor(minutes / 1_440);
|
||||
if (days > 0) {
|
||||
const hours = Math.floor((minutes % 1_440) / 60);
|
||||
return `${days}d${hours > 0 ? ` ${hours}h` : ""}`;
|
||||
}
|
||||
const hours = Math.floor(minutes / 60);
|
||||
if (hours > 0) {
|
||||
const rest = minutes % 60;
|
||||
return `${hours}h${rest > 0 ? ` ${rest}m` : ""}`;
|
||||
}
|
||||
return `${minutes}m`;
|
||||
}
|
||||
|
||||
export function formatQuotaWindow(
|
||||
window: QuotaWindow & { remaining: number; sessionConsumed: number },
|
||||
now = Date.now(),
|
||||
) {
|
||||
const label = window.label
|
||||
? `${window.label} ${duration(window.seconds)}`
|
||||
: duration(window.seconds);
|
||||
return `${label}: ${percent(window.remaining)}% left · ~${percent(window.sessionConsumed)}pp this session`;
|
||||
return `${label}: ${percent(window.remaining)}% · ~${percent(window.sessionConsumed)}pp · ${resetTiming(window.resetsAt, now)}`;
|
||||
}
|
||||
|
||||
export async function loadQuotaStatus(load: () => Promise<QuotaStatus | null>) {
|
||||
|
||||
@@ -199,6 +199,18 @@ test("reinstall replaces bundles while preserving existing settings and plugin o
|
||||
expect(await readFile(join(setup.installed, "index.js"), "utf8")).toContain('return "second"');
|
||||
});
|
||||
|
||||
test("enables Playwright only when requested", async () => {
|
||||
const setup = await fixture();
|
||||
const result = await run(
|
||||
["bash", join(setup.checkout, "install-local.sh"), "--playwright"],
|
||||
setup.cwd,
|
||||
setup.env,
|
||||
);
|
||||
expect(result, result.stderr).toMatchObject({ exitCode: 0 });
|
||||
const installed = JSON.parse(await readFile(join(setup.config, "opencode.json"), "utf8"));
|
||||
expect(installed.plugins.at(-1).options.playwright).toBe(true);
|
||||
});
|
||||
|
||||
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;
|
||||
|
||||
+17
-2
@@ -44,7 +44,11 @@ async function run(command: string[], cwd: string, env: NodeJS.ProcessEnv) {
|
||||
return { exitCode, stdout, stderr };
|
||||
}
|
||||
|
||||
async function install(mode: "native" | "checksum" | "legacy", configured = false) {
|
||||
async function install(
|
||||
mode: "native" | "checksum" | "legacy",
|
||||
configured = false,
|
||||
playwright = false,
|
||||
) {
|
||||
const directory = await mkdtemp(join(tmpdir(), "stanp-install-"));
|
||||
directories.push(directory);
|
||||
const config =
|
||||
@@ -187,7 +191,11 @@ exec sha256sum "$@"
|
||||
),
|
||||
]);
|
||||
await Promise.all([chmod(join(bin, "curl"), 0o755), chmod(join(bin, "sha256sum"), 0o755)]);
|
||||
const result = await run(["bash", installer], directory, env);
|
||||
const result = await run(
|
||||
["bash", installer, ...(playwright ? ["--playwright"] : [])],
|
||||
directory,
|
||||
env,
|
||||
);
|
||||
return { directory, config, env, result };
|
||||
}
|
||||
|
||||
@@ -249,6 +257,13 @@ test("preserves an existing stanp-opencode configuration", async () => {
|
||||
expect(installed.permissions).toEqual(jsonConfig.permissions);
|
||||
}, 60_000);
|
||||
|
||||
test("enables Playwright only when requested", async () => {
|
||||
const { config, result } = await install("native", false, true);
|
||||
expect(result, result.stderr).toMatchObject({ exitCode: 0 });
|
||||
const installed = JSON.parse(await readFile(join(config, "opencode.json"), "utf8"));
|
||||
expect(installed.plugins.at(-1).options.playwright).toBe(true);
|
||||
}, 60_000);
|
||||
|
||||
for (const mode of ["checksum", "legacy"] as const) {
|
||||
test(`${mode} failure preserves the installed plugin in a custom path containing spaces`, async () => {
|
||||
const { directory, config, env, result } = await install(mode);
|
||||
|
||||
+60
-5
@@ -17,7 +17,16 @@ const providerID = Provider.ID.make("test");
|
||||
function model(id: string): ModelInfo {
|
||||
return Model.Info.default(providerID, Model.ID.make(id));
|
||||
}
|
||||
const allModels = ["gpt-5.6-luna", "gpt-5.6-sol", "gpt-6-astra"].map(model);
|
||||
function reasoningModel(id: string): ModelInfo {
|
||||
return {
|
||||
...model(id),
|
||||
variants: ["low", "medium", "high"].map((effort) => ({
|
||||
id: Model.VariantID.make(effort),
|
||||
settings: { reasoningEffort: effort },
|
||||
})),
|
||||
};
|
||||
}
|
||||
const allModels = ["gpt-5.6-luna", "gpt-5.6-sol", "gpt-6-astra"].map(reasoningModel);
|
||||
const cleanups: Plugin.Cleanup[] = [];
|
||||
afterEach(async () => {
|
||||
for (const cleanup of cleanups.splice(0)) await cleanup();
|
||||
@@ -442,7 +451,13 @@ test("playwright MCP prefers direct MCP, uses npx for Playwright, and preserves
|
||||
await writeFile(executable, "#!/usr/bin/env node\n");
|
||||
await chmod(executable, 0o755);
|
||||
|
||||
const local = await host(allModels, { provider: "test" }, {}, [], localProject);
|
||||
const local = await host(
|
||||
allModels,
|
||||
{ provider: "test", playwright: true },
|
||||
{},
|
||||
[],
|
||||
localProject,
|
||||
);
|
||||
expect(local.mcpServers.get("playwright")).toEqual({
|
||||
type: "local",
|
||||
command: [executable],
|
||||
@@ -475,7 +490,13 @@ test("playwright MCP prefers direct MCP, uses npx for Playwright, and preserves
|
||||
);
|
||||
await writeFile(ordinaryExecutable, "#!/usr/bin/env node\n");
|
||||
await chmod(ordinaryExecutable, 0o755);
|
||||
const ordinary = await host(allModels, { provider: "test" }, {}, [], ordinaryProject);
|
||||
const ordinary = await host(
|
||||
allModels,
|
||||
{ provider: "test", playwright: true },
|
||||
{},
|
||||
[],
|
||||
ordinaryProject,
|
||||
);
|
||||
expect(ordinary.mcpServers.get("playwright")?.command).toEqual([
|
||||
"npx",
|
||||
"-y",
|
||||
@@ -534,7 +555,7 @@ test("playwright MCP prefers direct MCP, uses npx for Playwright, and preserves
|
||||
await chmod(fakePlaywright, 0o755);
|
||||
expect((await playwrightMcpCommand(globalProject, ordinaryBin))[0]).toBe("docker");
|
||||
|
||||
const fallback = await host();
|
||||
const fallback = await host(allModels, { provider: "test", playwright: true });
|
||||
expect(fallback.mcpServers.get("playwright")).toEqual({
|
||||
type: "local",
|
||||
command: ["docker", "run", "-i", "--rm", "--init", "mcr.microsoft.com/playwright/mcp"],
|
||||
@@ -561,6 +582,9 @@ test("playwright MCP prefers direct MCP, uses npx for Playwright, and preserves
|
||||
timeout: { startup: 12_345 },
|
||||
});
|
||||
expect(existing.mcpServers.get("docs")?.codemode).toBe(true);
|
||||
|
||||
const disabled = await host();
|
||||
expect(disabled.mcpServers.has("playwright")).toBe(false);
|
||||
} finally {
|
||||
if (originalPath === undefined) delete process.env.PATH;
|
||||
else process.env.PATH = originalPath;
|
||||
@@ -594,7 +618,8 @@ test("reasoning settings and Luna priority use native advertised or official sup
|
||||
settings: { baseURL: "https://api.openai.com/v1" },
|
||||
};
|
||||
const ordinary = await host();
|
||||
expect(ordinary.agents.get("fast-lo")?.request.settings).toEqual({ reasoningEffort: "low" });
|
||||
expect(ordinary.agents.get("fast-lo")?.request.settings).toEqual({});
|
||||
expect(String(ordinary.agents.get("fast-lo")?.model?.variant)).toBe("low");
|
||||
const official = await host([luna, ...allModels.slice(1)]);
|
||||
expect(official.agents.get("fast-hi")?.request.settings).toEqual({
|
||||
reasoningEffort: "high",
|
||||
@@ -617,13 +642,43 @@ test("reasoning settings and Luna priority use native advertised or official sup
|
||||
expect(disabled.agents.get("fast-lo")?.request.settings).toEqual({ reasoningEffort: "low" });
|
||||
});
|
||||
|
||||
test("coordinator defaults expose their effort variant without relying on it for requests", async () => {
|
||||
for (const options of [{ provider: "test" }, { mode: "gpt", provider: "test" }] as const) {
|
||||
const result = await host(allModels, options);
|
||||
expect(result.agents.get("coordinate")?.model).toMatchObject({
|
||||
id: "gpt-6-astra",
|
||||
variant: "medium",
|
||||
});
|
||||
expect(result.agents.get("coordinate")?.request.settings.reasoningEffort).toBeUndefined();
|
||||
}
|
||||
|
||||
const fallback = await host(allModels.slice(0, 2));
|
||||
expect(fallback.agents.get("coordinate")?.model).toMatchObject({
|
||||
id: "gpt-5.6-sol",
|
||||
variant: "medium",
|
||||
});
|
||||
expect(fallback.agents.get("coordinate")?.request.settings.reasoningEffort).toBeUndefined();
|
||||
|
||||
const custom = await host([...allModels, model("local")], {
|
||||
mode: "custom",
|
||||
provider: "test",
|
||||
agents: { ...gptAgents, coordinate: { model: "local", effort: "xhigh" } },
|
||||
});
|
||||
expect(custom.agents.get("coordinate")?.model?.variant).toBeUndefined();
|
||||
expect(custom.agents.get("coordinate")?.request.settings.reasoningEffort).toBe("xhigh");
|
||||
});
|
||||
|
||||
test("catalog updates activate late models and remove unavailable agents", async () => {
|
||||
const result = await host([]);
|
||||
await result.refresh(allModels);
|
||||
expect(String(result.agents.get("coordinate")?.model?.id)).toBe("gpt-6-astra");
|
||||
expect(String(result.agents.get("coordinate")?.model?.variant)).toBe("medium");
|
||||
expect(result.agents.get("coordinate")?.request.settings.reasoningEffort).toBeUndefined();
|
||||
await result.refresh(allModels.slice(0, 2));
|
||||
expect(result.agents.has("deep-lo")).toBe(false);
|
||||
expect(String(result.agents.get("coordinate")?.model?.id)).toBe("gpt-5.6-sol");
|
||||
expect(String(result.agents.get("coordinate")?.model?.variant)).toBe("medium");
|
||||
expect(result.agents.get("coordinate")?.request.settings.reasoningEffort).toBeUndefined();
|
||||
await result.refresh([]);
|
||||
expect(result.agents.has("coordinate")).toBe(false);
|
||||
});
|
||||
|
||||
+49
-6
@@ -415,18 +415,61 @@ test("polls, carries consumption across quota resets, tolerates errors, and clea
|
||||
expect(result.calls()).toBe(calls);
|
||||
});
|
||||
|
||||
test("sidebar text places estimated percentage-point use beside latest remaining quota", () => {
|
||||
test("sidebar text places reset timing beside latest remaining quota", () => {
|
||||
expect(
|
||||
formatQuotaWindow({
|
||||
formatQuotaWindow(
|
||||
{
|
||||
seconds: 604_800,
|
||||
id: "codex:secondary",
|
||||
label: null,
|
||||
used: 25,
|
||||
remaining: 75,
|
||||
sessionConsumed: 2.25,
|
||||
resetsAt: 0,
|
||||
}),
|
||||
).toBe("7d: 75% left · ~2.3pp this session");
|
||||
sessionConsumed: 1.5,
|
||||
resetsAt: 100_000 + 2 * 86_400_000 + 5 * 3_600_000,
|
||||
},
|
||||
100_000,
|
||||
),
|
||||
).toBe("7d: 75% · ~1.5pp · 2d 5h");
|
||||
});
|
||||
|
||||
test("sidebar reset timing uses compact future units and handles boundaries", () => {
|
||||
const now = 1_000_000;
|
||||
const format = (resetIn: number) =>
|
||||
formatQuotaWindow(
|
||||
{
|
||||
seconds: 18_000,
|
||||
id: "codex:primary",
|
||||
label: null,
|
||||
used: 25,
|
||||
remaining: 75,
|
||||
sessionConsumed: 1.5,
|
||||
resetsAt: now + resetIn,
|
||||
},
|
||||
now,
|
||||
);
|
||||
|
||||
expect(format(2 * 3_600_000 + 15 * 60_000)).toBe("5h: 75% · ~1.5pp · 2h 15m");
|
||||
expect(format(3_600_000)).toBe("5h: 75% · ~1.5pp · 1h");
|
||||
expect(format(15 * 60_000)).toBe("5h: 75% · ~1.5pp · 15m");
|
||||
expect(format(60_000)).toBe("5h: 75% · ~1.5pp · 1m");
|
||||
expect(format(59_999)).toBe("5h: 75% · ~1.5pp · <1m");
|
||||
});
|
||||
|
||||
test("sidebar reset timing preserves labels and distinguishes stale and unavailable timestamps", () => {
|
||||
const window = {
|
||||
seconds: 18_000,
|
||||
id: "codex:primary",
|
||||
label: "GPT-5.6 Luna",
|
||||
used: 99.95,
|
||||
remaining: 0.05,
|
||||
sessionConsumed: 1.5,
|
||||
};
|
||||
expect(formatQuotaWindow({ ...window, resetsAt: 999 }, 1_000)).toBe(
|
||||
"GPT-5.6 Luna 5h: <0.1% · ~1.5pp · due",
|
||||
);
|
||||
expect(formatQuotaWindow({ ...window, resetsAt: Number.NaN }, 1_000)).toBe(
|
||||
"GPT-5.6 Luna 5h: <0.1% · ~1.5pp · unavailable",
|
||||
);
|
||||
});
|
||||
|
||||
test("inactive roots freeze, resume from a cached baseline on failure, and deactivate by family", async () => {
|
||||
|
||||
+1
-1
@@ -84,7 +84,7 @@ test("built quota slot stays reactive across pending, empty, failure, update, an
|
||||
|
||||
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");
|
||||
expect(setup.captureCharFrame()).toContain("5h: 75% · ~1.5pp · due");
|
||||
|
||||
responses.push(async () => null);
|
||||
notifyUpdated();
|
||||
|
||||
Reference in New Issue
Block a user