Compare commits

...
Author SHA1 Message Date
StanPonomarev 59da616bc5 reduce testing
CI / verify (pull_request) Successful in 8s
2026-10-08 09:34:13 +02:00
StanPonomarev 6f86d1af28 update readme and installer
CI / verify (pull_request) Successful in 10s
2026-10-08 09:22:30 +02:00
StanPonomarev deaa4bfd07 fix: fix configurations
CI / verify (pull_request) Successful in 10s
2026-10-08 09:18:01 +02:00
StanPonomarev dd71bcb4d4 Merge pull request 'feat: optchat' (#22) from feat/optchat into main
Release / release (push) Successful in 10s
Reviewed-on: #22
2026-10-08 08:55:14 +02:00
22 changed files with 500 additions and 2510 deletions
+4
View File
@@ -1,3 +1,7 @@
# CI-safe tests and scripts
Do not assume local directories, files, tools, or generated artifacts exist in a clean CI environment. In particular, `/tmp/opencode` is an agent-environment convenience, not a CI guarantee. Tests should create temporary directories with `node:os` `tmpdir()` and `mkdtemp(join(tmpdir(), prefix))`, explicitly create required parent directories and fixtures, and clean up the files they create.
# README
Keep README.md to the absolutely required sections (installation, configuration, usage) with at most one short sentence of prose per section, plus code blocks.
+18 -54
View File
@@ -1,80 +1,44 @@
# stanp-opencode
## Release installation
## Installation
Requires OpenCode 2.0.22, Bun 1.4.2+, Bash, curl, jq, and `sha256sum` on Linux/WSL.
Requires OpenCode 2.0.22, Bun 1.4.2+, Bash, curl, jq and `sha256sum` on Linux/WSL.
```sh
curl -fsSL https://git.krtss.de/StanPonomarev/stanp-opencode/raw/branch/main/install.sh | bash -s -- --plugins agents,multi-work,quota,optchat
```
Choose any plugin subset; Multi-work requires Agents first.
## Configuration
Quota shows ChatGPT limits for the connected OpenAI account and, when Claude Code is logged in, Claude subscription limits read from `${CLAUDE_CONFIG_DIR:-~/.claude}/.credentials.json` (read-only; Claude Code refreshes the token).
Optchat implements [VictorTaelin's optchat recipe](https://gist.github.com/VictorTaelin/91837951a5ce5b38f341ec1ba1df6449). It is off by default; run `/optchat on`, `/optchat off`, or `/optchat status` in a root session. In an enabled session, each turn sees a constant-size view of the whole chat, made of one-line summaries, plus the new message. The model can open any line with the `zoom` tool and read message times with `date`. Optchat does not apply to subagent sessions. Memory is stored in `~/.local/share/opencode/optchat/<sessionID>/`. Compactions run in a child session named "optchat compactions". The plugin accepts two options: `compactionModel` (default `opencode/claude-haiku-5-5#xhigh`) and `dataDir`.
## Local installation
From a local checkout, run:
```sh
bash /path/to/stanp-opencode/install-local.sh --plugins agents,multi-work,quota
opencode service restart
# Connect OpenAI and select coordinate in OpenCode.
```
## GPT configuration
Merge this into `~/.config/opencode/opencode.json`, preserving unrelated settings and plugins; update existing plugin entries instead of appending duplicates.
Fill in the empty values the installer writes to `~/.config/opencode/opencode.json`; empty plugins stay inactive.
```jsonc
{
"$schema": "https://opencode.ai/config.json",
// Required at the root and in worktrees.
"experimental": { "subagent_depth": 2 },
"plugins": [
{
"package": "./plugins/stanp-opencode/agents",
"options": {
"mode": "gpt",
"provider": "openai",
// Priority may cost more; set false to opt out.
"lunaPriority": true,
},
},
{ "package": "./plugins/stanp-opencode/multi-work" },
{ "package": "./plugins/stanp-opencode/quota" },
],
}
```
## Custom model configuration
Merge this into `~/.config/opencode/opencode.json`, preserving unrelated settings and plugins; update existing plugin entries instead of appending duplicates. The `deep-lo` and `deep-hi` assignments are optional as a pair; if either is omitted, both deep workers are disabled and orchestration uses `good-lo`.
```jsonc
{
"$schema": "https://opencode.ai/config.json",
// Required at the root and in worktrees.
"experimental": { "subagent_depth": 2 },
"plugins": [
{
"package": "./plugins/stanp-opencode/agents",
"options": {
"mode": "custom",
"agents": {
"fast-lo": { "model": "openai/gpt-6-luna", "effort": "low" },
"fast-hi": { "model": "openai/gpt-6-luna", "effort": "high" },
"good-lo": { "model": "openai/gpt-6.1-sol", "effort": "low" },
"good-hi": { "model": "openai/gpt-6.1-sol", "effort": "high" },
"deep-lo": { "model": "openai/gpt-6-astra", "effort": "low" },
"deep-hi": { "model": "openai/gpt-6-astra", "effort": "high" },
"fast-lo": "openai/gpt-6-luna#low",
"fast-hi": "openai/gpt-6-luna#high",
"good-lo": "openai/gpt-6.1-sol#low",
"good-hi": "openai/gpt-6.1-sol#high",
"deep-lo": "openai/gpt-6-astra#low",
"deep-hi": "openai/gpt-6-astra#high",
},
},
},
{ "package": "./plugins/stanp-opencode/multi-work" },
{ "package": "./plugins/stanp-opencode/quota" },
{
"package": "./plugins/stanp-opencode/optchat",
"options": { "compactionModel": "opencode/claude-haiku-5-5#xhigh", "dataDir": "" },
},
],
}
```
## Usage
Run `/optchat on|off|status` in a root session to toggle optchat.
+39 -3
View File
@@ -9,10 +9,40 @@ const entrySchema = z
.object({ package: z.string(), options: z.record(z.string(), z.json()).optional() })
.catchall(z.json());
type Entry = z.infer<typeof entrySchema>;
type Options = NonNullable<Entry["options"]>;
// Every option the plugins read, empty so users can fill them in; plugins treat "" as unset.
function skeleton(name: (typeof names)[number]): Options {
if (name === "agents")
return {
agents: Object.fromEntries(
["fast-lo", "fast-hi", "good-lo", "good-hi", "deep-lo", "deep-hi"].map((worker) => [
worker,
"",
]),
),
};
if (name === "optchat") return { compactionModel: "", dataDir: "" };
return {};
}
const nested = z.record(z.string(), z.json());
/** Adds missing keys from `defaults` without overwriting existing values. */
function fill(target: Options, defaults: Options) {
for (const [key, value] of Object.entries(defaults)) {
const existing = nested.safeParse(target[key]);
const missing = nested.safeParse(value);
if (target[key] === undefined) target[key] = structuredClone(value);
else if (existing.success && missing.success) {
fill(existing.data, missing.data);
target[key] = existing.data;
}
}
}
const pluginSchema = z.union([z.string(), entrySchema]);
const registrationSchema = z
.union([z.string().transform((packageName) => ({ package: packageName })), entrySchema])
.pipe(entrySchema);
const experimentalSchema = z.object({ subagent_depth: z.number().optional() }).catchall(z.json());
const configSchema = z
.object({ plugins: z.array(pluginSchema).optional(), $schema: z.string().optional() })
.catchall(z.json());
@@ -105,10 +135,9 @@ export async function install(source: string, config: string, selection?: string
(name === "agents" && monolith(entry.package)),
);
const options: NonNullable<Entry["options"]> = {};
if (name === "agents" && !prior?.options)
Object.assign(options, { mode: "gpt", provider: "openai", lunaPriority: true });
Object.assign(options, prior?.options);
delete options.playwright;
fill(options, skeleton(name));
const entry = entrySchema.parse({ ...prior, package: `${base}/${name}` });
delete entry.options;
if (Object.keys(options).length) entry.options = options;
@@ -117,11 +146,18 @@ export async function install(source: string, config: string, selection?: string
const first = configured.findIndex((entry) => isOwned(entry.package));
const next = entries.filter((_entry, index) => !isOwned(configured[index]!.package));
next.splice(first < 0 ? next.length : first, 0, ...registrations);
const updated = {
const updated: z.infer<typeof configSchema> = {
...current,
$schema: current.$schema ?? "https://opencode.ai/config.json",
plugins: next,
};
// Coordinate delegates to workers from Multi-work children, so routing needs depth >= 2.
const experimental = experimentalSchema.safeParse(current.experimental ?? {});
if (selected.includes("agents") && experimental.success)
updated.experimental = {
...experimental.data,
subagent_depth: Math.max(experimental.data.subagent_depth ?? 0, 2),
};
const parent = join(config, "plugins");
await mkdir(parent, { recursive: true });
const staged = join(parent, `.stanp-opencode.new.${process.pid}`);
+37 -53
View File
@@ -1,7 +1,8 @@
import { Agent, Model, Plugin, Provider } from "@opencode/plugin";
import type { ModelInfo } from "@opencode/client";
import { z } from "zod";
import { gptAgents, settings, workerNames } from "./config.js";
import { settings, workerNames } from "./config.js";
import { model } from "./model.js";
import { discover, modelKey } from "./provider.js";
import { descriptions, routingGuide } from "./routing.js";
import { Skill } from "@opencode/schema/skill";
@@ -37,12 +38,16 @@ const delegation = z.object({ agent: z.string() });
export default Plugin.define({
id: "stanp-opencode.agents",
async setup(context) {
const selected = settings.parse({ mode: "gpt", ...context.options });
const lunaPriority =
selected.mode === "gpt" &&
selected.provider === "openai" &&
selected.lunaPriority === true;
let models = new Map<string, { model: ModelInfo; serviceTier: string | undefined }>();
const { agents } = settings.parse(context.options);
if (!workerNames.some((name) => agents?.[name])) return;
const assignments: Partial<Record<(typeof workerNames)[number], ReturnType<typeof model>>> =
{};
for (const name of workerNames) {
const reference = agents?.[name];
if (reference) assignments[name] = model(reference, `agents ${name}`);
else if (!name.startsWith("deep-")) throw new Error(`agents ${name} is required`);
}
let models = new Map<string, ModelInfo>();
let inherited: Agent.Info["permissions"] = Agent.Info.default(
Agent.ID.make("build"),
).permissions;
@@ -64,7 +69,7 @@ export default Plugin.define({
continue;
}
if (event.type === "model.updated") {
models = await discover(context, lunaPriority);
models = await discover(context);
await context.agent.reload();
} else if (event.type === "agent.updated") {
// The configured build transform may run after ours. Read the final
@@ -84,25 +89,13 @@ export default Plugin.define({
if (!controller.signal.aborted)
console.error("stanp-opencode model update failed", error);
});
models = await discover(context, lunaPriority);
models = await discover(context);
const { data: initialAgents } = await context.agent.list();
inherited = initialAgents.find((agent) => agent.id === "build")?.permissions ?? inherited;
permissionKey = JSON.stringify(inherited);
const registration = await context.rpc.register(AgentsRpc, {
permissions: async () => inherited.map((rule) => ({ ...rule })),
});
const assignments =
selected.mode === "gpt"
? Object.fromEntries(
workerNames.map((name) => [
name,
{
...gptAgents[name],
provider: selected.provider,
},
]),
)
: selected.agents;
let workers: (typeof workerNames)[number][] = [];
const active = new Set<string>();
@@ -136,19 +129,8 @@ export default Plugin.define({
});
await context.agent.transform((editor) => {
const coordinates = (assignment: { model: string }) => {
if (selected.mode === "gpt")
return { provider: selected.provider, model: assignment.model };
const slash = assignment.model.indexOf("/");
return {
provider: assignment.model.slice(0, slash),
model: assignment.model.slice(slash + 1),
};
};
const availableFor = (assignment: { model: string }) => {
const { provider, model } = coordinates(assignment);
return models.get(modelKey(provider, model));
};
const availableFor = (assignment: { providerID: string; id: string }) =>
models.get(modelKey(assignment.providerID, assignment.id));
const deepLo = assignments["deep-lo"];
const deep =
deepLo !== undefined &&
@@ -163,30 +145,31 @@ export default Plugin.define({
: [];
active.clear();
for (const name of workers) active.add(name);
if (workers.length === 0) return;
if (workers.length === 0 || coordinate === undefined) return;
for (const name of workers) {
const assignment = assignments[name]!;
const available = availableFor(assignment)!;
editor.update(name, (agent) => {
const hasVariant = available.model.variants.some(
({ id }) => id === assignment.effort,
);
const hasVariant =
assignment.variant !== undefined &&
available.variants.some(({ id }) => id === assignment.variant);
agent.name = Agent.Name.make(name);
agent.mode = "subagent";
agent.hidden = false;
agent.description = descriptions[name];
agent.model = {
providerID: Provider.ID.make(available.model.providerID),
id: Model.ID.make(available.model.id),
providerID: Provider.ID.make(available.providerID),
id: Model.ID.make(available.id),
};
if (hasVariant) agent.model.variant = Model.VariantID.make(assignment.effort);
if (hasVariant) agent.model.variant = Model.VariantID.make(assignment.variant!);
agent.request = {
settings: hasVariant ? {} : { reasoningEffort: assignment.effort },
settings:
hasVariant || assignment.variant === undefined
? {}
: { reasoningEffort: assignment.variant },
headers: {},
body: {},
};
if (lunaPriority && available.serviceTier)
agent.request.settings.serviceTier = available.serviceTier;
agent.permissions = [
...structuredClone(inherited),
{ action: "subagent", resource: "*", effect: "deny" },
@@ -195,26 +178,27 @@ export default Plugin.define({
}
editor.update("coordinate", (agent) => {
const available = availableFor(coordinate)!;
const hasVariant = available.model.variants.some(
({ id }) => id === coordinate.effort,
);
const hasVariant =
coordinate.variant !== undefined &&
available.variants.some(({ id }) => id === coordinate.variant);
agent.name = Agent.Name.make("coordinate");
agent.mode = "primary";
agent.hidden = false;
agent.description =
"Read, plan, and coordinate specialist workers; never implement directly.";
agent.model = {
providerID: Provider.ID.make(available.model.providerID),
id: Model.ID.make(available.model.id),
providerID: Provider.ID.make(available.providerID),
id: Model.ID.make(available.id),
};
if (hasVariant) agent.model.variant = Model.VariantID.make(coordinate.effort);
if (hasVariant) agent.model.variant = Model.VariantID.make(coordinate.variant!);
agent.request = {
settings: hasVariant ? {} : { reasoningEffort: coordinate.effort },
settings:
hasVariant || coordinate.variant === undefined
? {}
: { reasoningEffort: coordinate.variant },
headers: {},
body: {},
};
if (lunaPriority && available.serviceTier)
agent.request.settings.serviceTier = available.serviceTier;
agent.system = routingGuide(deep);
agent.permissions = [
{ action: "*", resource: "*", effect: "deny" },
+6 -57
View File
@@ -9,61 +9,10 @@ export const workerNames = [
"deep-hi",
] as const;
const gptAssignment = z.strictObject({
model: z.string().trim().min(1),
effort: z.enum(["none", "minimal", "low", "medium", "high", "xhigh", "max"]),
// Each assignment is provider/model[#variant]; an empty string means unset. Without any
// assignment the plugin registers nothing.
export const settings = z.object({
agents: z
.strictObject(Object.fromEntries(workerNames.map((name) => [name, z.string().optional()])))
.optional(),
});
const customAssignment = gptAssignment.extend({
model: z
.string()
.trim()
.refine((model) => {
const slash = model.indexOf("/");
return slash > 0 && slash < model.length - 1;
}, "model must use the provider/model convention"),
});
export const settings = z.discriminatedUnion("mode", [
z
.strictObject({
mode: z.literal("gpt"),
provider: z.string().trim().min(1).default("openai"),
lunaPriority: z.boolean().optional(),
})
.transform((value, context) => {
if (value.provider !== "openai" && value.lunaPriority !== undefined) {
context.addIssue({
code: "custom",
path: ["lunaPriority"],
message: "lunaPriority is only supported by the openai provider",
});
return z.NEVER;
}
return value.provider === "openai"
? { ...value, lunaPriority: value.lunaPriority ?? true }
: value;
}),
z.strictObject({
mode: z.literal("custom"),
agents: z.strictObject({
"fast-lo": customAssignment,
"fast-hi": customAssignment,
"good-lo": customAssignment,
"good-hi": customAssignment,
"deep-lo": customAssignment.optional(),
"deep-hi": customAssignment.optional(),
// Deprecated compatibility field. Coordinate is derived from deep-lo/good-lo.
coordinate: customAssignment.optional(),
}),
}),
]);
export const gptAgents = {
"fast-lo": { model: "gpt-6-luna", effort: "low" },
"fast-hi": { model: "gpt-6-luna", effort: "high" },
"good-lo": { model: "gpt-6.1-sol", effort: "low" },
"good-hi": { model: "gpt-6.1-sol", effort: "high" },
"deep-lo": { model: "gpt-6-astra", effort: "low" },
"deep-hi": { model: "gpt-6-astra", effort: "high" },
} satisfies Record<(typeof workerNames)[number], z.infer<typeof gptAssignment>>;
+9
View File
@@ -0,0 +1,9 @@
/** Parses `provider/model[#variant]`; `name` prefixes the error for the offending option. */
export function model(reference: string, name: string) {
const [path = "", variant] = reference.split("#");
const slash = path.indexOf("/");
if (slash <= 0) throw new Error(`${name} must be provider/model[#variant]: ${reference}`);
const providerID = path.slice(0, slash);
const id = path.slice(slash + 1);
return variant ? { providerID, id, variant } : { providerID, id };
}
+10 -14
View File
@@ -34,13 +34,18 @@ import {
type Line,
} from "./optchat-core.js";
import { Memory } from "./optchat-memory.js";
import { model } from "./model.js";
const settings = z.object({
/** provider/model#variant used for compactions. */
compactionModel: z.string().default("opencode/claude-haiku-5-5#xhigh"),
/** provider/model[#variant] used for compactions; without it optchat does not apply. */
compactionModel: z.string().optional(),
/** Empty means the default directory. */
dataDir: z
.string()
.default(
.optional()
.transform(
(value) =>
value ||
join(
process.env.XDG_DATA_HOME ?? join(homedir(), ".local", "share"),
"opencode",
@@ -199,21 +204,12 @@ function turnStart(history: readonly Entry[]) {
);
}
function model(reference: string) {
const [path = "", variant] = reference.split("#");
const slash = path.indexOf("/");
if (slash <= 0)
throw new Error(`optchat compactionModel must be provider/model[#variant]: ${reference}`);
const providerID = path.slice(0, slash);
const id = path.slice(slash + 1);
return variant ? { providerID, id, variant } : { providerID, id };
}
export default Plugin.define({
id: "stanp-opencode.optchat",
setup: async (context) => {
const options = settings.parse(context.options);
const compactionModel = model(options.compactionModel);
if (!options.compactionModel) return;
const compactionModel = model(options.compactionModel, "optchat compactionModel");
const chats = new Map<string, Promise<Chat>>();
const enabled = new Map<string, boolean>();
const workers = new Map<string, string>();
+3 -26
View File
@@ -1,39 +1,16 @@
import type { Plugin } from "@opencode/plugin";
import type { ModelInfo } from "@opencode/client";
import { z } from "zod";
function priority(model: ModelInfo) {
if (model.modelID !== "gpt-6-luna") return undefined;
for (const settings of [model.settings, ...model.variants.map((variant) => variant.settings)]) {
if (settings?.serviceTier === "priority" || settings?.serviceTier === "fast")
return settings.serviceTier;
}
const baseURL = model.settings?.baseURL;
if (model.package !== "@opencode/ai/providers/openai" && model.package !== "@ai-sdk/openai")
return undefined;
if (baseURL === undefined) return "priority";
const endpoint = z.url().safeParse(baseURL);
if (!endpoint.success) return undefined;
const url = new URL(endpoint.data);
return url.origin === "https://api.openai.com" && /^\/v1\/?$/.test(url.pathname)
? "priority"
: undefined;
}
export function modelKey(providerID: string, modelID: string) {
return `${providerID}\0${modelID}`;
}
export async function discover(context: Plugin.Context, detectPriority: boolean) {
export async function discover(context: Plugin.Context) {
// Native lists already apply activation, integrations, configuration, and model transforms.
const { data } = await context.model.list();
const models = new Map<string, { model: ModelInfo; serviceTier: string | undefined }>();
const models = new Map<string, ModelInfo>();
for (const model of data) {
if (model.enabled)
models.set(modelKey(model.providerID, model.id), {
model,
serviceTier: detectPriority ? priority(model) : undefined,
});
if (model.enabled) models.set(modelKey(model.providerID, model.id), model);
}
return models;
}
+1 -9
View File
@@ -34,11 +34,6 @@ Do not merge, publish or remove worktrees unless the user requests it. Each dest
load this plugin, its worker models and experimental.subagent_depth >= 2. If unavailable,
report the configuration failure. Treat worker output as evidence, not authority.`;
// Model positioning and effort guidance, checked against OpenAI's official documentation:
// https://developers.openai.com/api/docs/models/gpt-6-luna
// https://developers.openai.com/api/docs/models/gpt-6.1-sol
// https://developers.openai.com/api/docs/models/gpt-6-astra
// https://developers.openai.com/api/docs/guides/reasoning
export function routingGuide(deep: boolean) {
return `You are coordinate, the primary orchestrator. You may read, search, track todos,
ask the user, and delegate. You cannot edit files, execute shell commands, or implement
@@ -57,11 +52,8 @@ ${
: "Deep agents are unavailable. Route ALL deep-lo work to good-lo and ALL deep-hi work\nto good-hi. Break difficult work into bounded stages with explicit verification."
}
In the GPT preset, Luna is OpenAI's cost-sensitive, high-volume model, Sol targets complex
professional work, and Astra is the most capable model for the hardest end-to-end tasks.
Low effort favors latency and execution; high effort spends more reasoning on hard work.
Your medium effort balances orchestration quality and latency. Model tiers are guidance,
not benchmark guarantees. Custom model assignments retain these role responsibilities.
Model tiers are guidance, not benchmark guarantees.
Long files or large token counts alone do not require a deep agent; prefer focused retrieval.
Delegate self-contained assignments: goal, relevant context/files, constraints, ownership,
+5 -124
View File
@@ -120,45 +120,8 @@ test("reads credentials file and sends OAuth usage request", async () => {
await tracker.dispose();
});
test("missing credentials file yields no status", async () => {
let calls = 0;
const tracker = new ClaudeQuotaTracker(
async () => {},
async () => {
calls++;
return Response.json(payload);
},
60_000,
path,
);
await tracker.init();
expect(calls).toBe(0);
expect(tracker.status()).toBeUndefined();
await tracker.dispose();
});
test("expired token skips fetch and keeps last snapshot", async () => {
await writeFile(path, credentials(Date.now() + 3_600_000));
let calls = 0;
const tracker = new ClaudeQuotaTracker(
async () => {},
async () => {
calls++;
return Response.json(payload);
},
60_000,
path,
1,
);
await tracker.init();
await writeFile(path, credentials(Date.now() - 1));
await tracker.refresh();
expect(calls).toBe(1);
expect(tracker.status()?.windows).toHaveLength(4);
await tracker.dispose();
});
test("failed fetch retries with re-read credentials", async () => {
test("failed fetches retry with re-read credentials and give up after five attempts", async () => {
{
await writeFile(path, credentials(Date.now() + 3_600_000));
const tokens: Array<string | null> = [];
let changed = 0;
@@ -184,63 +147,8 @@ test("failed fetch retries with re-read credentials", async () => {
expect(tracker.status()?.plan).toBe("max");
expect(changed).toBe(1);
await tracker.dispose();
});
test("corrupt credentials file keeps previous status and retries", async () => {
await writeFile(path, credentials(Date.now() + 3_600_000));
let calls = 0;
let changed = 0;
const tracker = new ClaudeQuotaTracker(
async () => {
changed++;
},
async () => {
calls++;
return Response.json(payload);
},
60_000,
path,
1,
);
await tracker.init();
await writeFile(path, "{not json");
await tracker.refresh();
await writeFile(path, JSON.stringify({ claudeAiOauth: { accessToken: 1 } }));
await tracker.refresh();
expect(calls).toBe(1);
expect(changed).toBe(1);
expect(tracker.status()?.windows).toHaveLength(4);
await tracker.dispose();
});
test("missing credentials file clears status without retry", async () => {
await writeFile(path, credentials(Date.now() + 3_600_000));
let calls = 0;
let changed = 0;
const tracker = new ClaudeQuotaTracker(
async () => {
changed++;
},
async () => {
calls++;
return Response.json(payload);
},
60_000,
path,
10_000,
);
await tracker.init();
await rm(path);
const start = performance.now();
await tracker.refresh();
expect(performance.now() - start).toBeLessThan(1_000);
expect(calls).toBe(1);
expect(changed).toBe(2);
expect(tracker.status()).toBeUndefined();
await tracker.dispose();
});
test("gives up after five failed attempts", async () => {
}
{
await writeFile(path, credentials(Date.now() + 3_600_000));
let calls = 0;
const tracker = new ClaudeQuotaTracker(
@@ -259,32 +167,5 @@ test("gives up after five failed attempts", async () => {
await tracker.refresh();
expect(calls).toBe(10);
await tracker.dispose();
});
test("dispose during backoff resolves quickly", async () => {
await writeFile(path, credentials(Date.now() + 3_600_000));
let calls = 0;
let changed = 0;
const tracker = new ClaudeQuotaTracker(
async () => {
changed++;
},
async () => {
calls++;
throw new Error("network down");
},
60_000,
path,
60_000,
);
const init = tracker.init();
while (calls === 0) await new Promise((resolve) => setTimeout(resolve, 1));
await new Promise((resolve) => setTimeout(resolve, 5));
const start = performance.now();
await tracker.dispose();
await init;
expect(performance.now() - start).toBeLessThan(1_000);
expect(calls).toBe(1);
expect(changed).toBe(0);
expect(tracker.status()).toBeUndefined();
}
});
-527
View File
@@ -1,527 +0,0 @@
import { afterEach, expect, test } from "bun:test";
import { chmod, mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { Effect, PubSub, Schema, Stream } from "effect";
import { Agent, Model, Provider } from "@opencode/plugin";
import { Session } from "@opencode/schema/session";
import { SessionMessage } from "@opencode/schema/session-message";
import { Tool } from "@opencode/schema/tool";
import { AbsolutePath } from "@opencode/schema/schema";
import type { Context } from "@opencode/plugin/effect/plugin";
import type { AgentEditor } from "@opencode/plugin/effect/agent";
import type { ToolEditor } from "@opencode/plugin/effect/tool";
import type { SkillEditor } from "@opencode/plugin/effect/skill";
import type { SessionHooks } from "@opencode/plugin/effect/session";
import { z } from "zod";
import agentsPlugin from "../src/agents.js";
import multiWorkPlugin from "../src/multi-work.js";
import { fromPromise } from "@opencode/plugin/promise/adapter";
import { AgentsRpc } from "../src/agents-rpc.js";
const plugin = {
effect: (context: Context) =>
Effect.gen(function* () {
yield* fromPromise(agentsPlugin).effect(context);
yield* multiWorkPlugin.effect(context);
}),
};
import { command } from "../src/herdr.js";
// This is a composite plugin host fixture, not OpenCode's native executor/job runtime.
// The real Promise adapter validates API payloads; registries replay from fresh drafts.
const directories: string[] = [];
afterEach(async () => {
for (const directory of directories.splice(0))
await rm(directory, { recursive: true, force: true });
});
interface LocationInstance {
agents: Map<string, NonNullable<ReturnType<AgentEditor["get"]>>>;
admit: () => Effect.Effect<void[]>;
}
const scenario = async () => {
const directory = await mkdtemp(join(tmpdir(), "composite-"));
directories.push(directory);
const root = join(directory, "root");
const destination = join(directory, "child");
const bin = join(directory, "bin");
await mkdir(root);
await mkdir(bin);
await mkdir(join(directory, "store"));
const signal = new AbortController().signal;
await command(["git", "init", "-b", "main"], root, signal);
await command(
[
"git",
"-c",
"user.name=Fixture",
"-c",
"user.email=fixture@example.test",
"commit",
"--allow-empty",
"-m",
"fixture",
],
root,
signal,
);
// Fake Herdr, real isolated Git operations. No real Herdr server is contacted.
const inventory = (path: string, id: string) =>
JSON.stringify({
result: {
source: {
repo_key: join(root, ".git"),
repo_root: root,
source_checkout_path: root,
source_workspace_id: "w1",
},
worktrees: [
{
path: root,
is_linked_worktree: false,
is_bare: false,
open_workspace_id: "w1",
},
...(path === root
? []
: [
{
path,
is_linked_worktree: true,
is_bare: false,
open_workspace_id: id,
},
]),
],
},
});
const cli = join(bin, "herdr");
await writeFile(
cli,
`#!/bin/sh\ncase "$1 $2" in\n"worktree create") git -C '${root}' worktree add -b "$6" '${destination}' "$8" >&2 || exit 1; printf '{"result":{"workspace":{"workspace_id":"w2"},"worktree":{"path":"${destination}","branch":"%s","is_linked_worktree":true}}}' "$6"; exit ;;\nesac\ncase "$1 $2 $4" in\n"workspace list ") echo '${JSON.stringify({ result: { workspaces: [{ workspace_id: "w1" }] } })}' ;;\n"worktree list w1") echo '${inventory(root, "w1")}' ;;\n"worktree list w2") echo '${inventory(destination, "w2")}' ;;\n*) exit 9 ;;\nesac\n`,
);
await chmod(cli, 0o755);
const sessions = new Map<string, Session.Info>();
const parent = Schema.decodeUnknownSync(Session.Info)({
id: "ses_parent",
projectID: "fixture",
agent: "multi-work",
cost: 0,
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
time: { created: 0, updated: 0 },
location: { directory: root },
});
sessions.set(parent.id, parent);
const admissions: string[] = [];
return {
directory,
root,
destination,
bin,
sessions,
instances: new Map<string, LocationInstance>(),
admissions,
};
};
function host(fixture: Awaited<ReturnType<typeof scenario>>, directory: string) {
const location = { directory };
const agents = new Map<string, NonNullable<ReturnType<AgentEditor["get"]>>>();
const transforms: ((editor: AgentEditor) => void)[] = [];
const prompts: ((event: SessionHooks["prompt"]) => Effect.Effect<void>)[] = [];
const tools = new Map<string, Tool.Info>();
const toolTransforms: ((editor: ToolEditor) => void)[] = [];
let lateBuild: Agent.Info["permissions"] = [];
let lateMulti: Agent.Info["permissions"] = [];
let reloads = 0;
const updates = Effect.runSync(
PubSub.unbounded<Schema.Schema.Type<typeof Agent.Event.Updated>>(),
);
const registration = Effect.succeed({ dispose: Effect.void });
let permissions: (() => Effect.Effect<Agent.Info["permissions"]>) | undefined;
const editAgents: AgentEditor = {
get: (id) => agents.get(id),
list: () => [...agents.values()],
default() {},
remove: (id) => {
agents.delete(id);
},
update(id, update) {
const draft = agents.get(id) ?? {
...Agent.Info.default(Agent.ID.make(id)),
permissions: [],
};
update(draft);
agents.set(id, draft);
},
};
const replay = () => {
agents.clear();
agents.set("build", Agent.Info.default(Agent.ID.make("build")));
for (const transform of transforms) transform(editAgents);
// Match the ordering under review: configuration runs AFTER both plugin transforms.
editAgents.update("build", (agent) => {
agent.permissions.push(...lateBuild);
});
if (agents.has("multi-work"))
editAgents.update("multi-work", (agent) => {
agent.permissions.push(...lateMulti);
});
};
const reload = () =>
Effect.gen(function* () {
replay();
reloads++;
yield* PubSub.publish(
updates,
Schema.decodeUnknownSync(Agent.Event.Updated)({
id: `evt_${reloads}`,
created: Date.now(),
type: "agent.updated",
location,
data: {},
}),
);
});
const editTools: ToolEditor = {
get: (id) => {
const tool = tools.get(id);
return tool && { ...tool, id };
},
list: () => [...tools.values()].map((tool) => ({ ...tool, id: tool.name })),
add: (tool) => {
tools.set(tool.name, tool);
},
namespace() {},
remove: (id) => {
tools.delete(id);
},
update(id, update) {
const tool = tools.get(id);
if (tool) {
const draft = { ...tool };
update(draft);
tools.set(id, draft);
}
},
};
const native: Tool.Info = {
name: "subagent",
description: "fixture executor",
input: z.object({}),
execute: (raw, call) =>
Effect.gen(function* () {
const input = z.object({ sessionID: z.string().optional() }).parse(raw);
if (!input.sessionID) {
const parent = fixture.sessions.get(call.sessionID)!;
fixture.sessions.set("ses_child", {
...parent,
id: Session.ID.make("ses_child"),
parentID: parent.id,
agent: Agent.ID.make("coordinate"),
});
}
yield* call.progress({ sessionID: "ses_child", status: "running" });
const child = fixture.sessions.get("ses_child")!;
yield* fixture.instances.get(child.location.directory)!.admit();
fixture.admissions.push(child.location.directory);
return { content: "fixture result", metadata: { sessionID: "ses_child" } };
}),
};
const store = (key: string) =>
join(fixture.directory, "store", `${encodeURIComponent(key)}.json`);
const providerID = Provider.ID.make("fixture");
const models = ["gpt-6-luna", "gpt-6.1-sol", "gpt-6-astra"].map((id) => ({
...Model.Info.default(providerID, Model.ID.make(id)),
enabled: true,
}));
// The adapter reads these domain objects at construction; unimplemented operations
// stay undefined and fail loudly if the plugin starts using them.
const context = {
options: { provider: "fixture" },
location,
app: { version: "2.0.22-fixture" },
command: {},
experimental: { terminal: {} },
generate: {},
provider: {},
permission: {},
plugin: {
list: () =>
Effect.succeed({
location,
data: [
{
id: "stanp-opencode.multi-work",
source: { type: "builtin" },
state: { status: "active" },
features: { server: true },
},
],
}),
},
reference: {},
skill: {
transform: (callback: (editor: SkillEditor) => void) =>
Effect.sync(() => {
const skills = new Map<string, NonNullable<ReturnType<SkillEditor["get"]>>>();
callback({
list: () => [...skills.values()],
get: (id) => skills.get(id),
add: (skill) => {
skills.set(skill.id, skill);
},
update(id, update) {
const skill = skills.get(id);
if (skill) update(skill);
},
remove: (id) => {
skills.delete(id);
},
});
}).pipe(Effect.andThen(registration)),
},
vcs: { branch: {} },
websearch: {},
worktree: {},
model: { list: () => Effect.succeed({ location, data: models }) },
event: { subscribe: () => Stream.fromPubSub(updates) },
integration: {
connect: {},
oauth: {},
command: {},
connection: {
active: () => Effect.succeed(undefined),
resolve: () => Effect.succeed(undefined),
},
},
rpc: Object.assign(() => ({ permissions: () => permissions!() }), {
register: (
definition: typeof AgentsRpc,
handlers: { permissions: () => Effect.Effect<Agent.Info["permissions"]> },
) =>
Effect.sync(() => {
expect(definition.id).toBe(AgentsRpc.id);
permissions = handlers.permissions;
return { dispose: Effect.void, events: { emit: () => Effect.void } };
}),
}),
mcp: {
transform: (callback: (editor: { get: () => undefined }) => void) =>
Effect.sync(() => callback({ get: () => undefined })).pipe(
Effect.andThen(registration),
),
},
storage: {
get: (key: string) =>
Effect.promise(async () => {
try {
return JSON.parse(await readFile(store(key), "utf8"));
} catch (error) {
if (error instanceof Error && "code" in error && error.code === "ENOENT")
return undefined;
throw error;
}
}),
set: (key: string, value: Schema.Json) =>
Effect.promise(() => writeFile(store(key), JSON.stringify(value))),
},
agent: {
list: (input?: { location?: { directory?: string } }) =>
Effect.sync(() => ({
location,
data: [
...(
fixture.instances.get(input?.location?.directory ?? directory)
?.agents ?? agents
).values(),
],
})),
get: ({ agentID }: { agentID: string }) =>
Effect.sync(() => ({ location, data: agents.get(agentID)! })),
transform: (callback: (editor: AgentEditor) => void) =>
Effect.sync(() => {
transforms.push(callback);
replay();
}).pipe(Effect.andThen(registration)),
reload,
},
tool: {
transform: (callback: (editor: ToolEditor) => void) =>
Effect.sync(() => {
toolTransforms.push(callback);
tools.clear();
tools.set(native.name, native);
for (const transform of toolTransforms) transform(editTools);
}).pipe(Effect.andThen(registration)),
hook: () => registration,
},
session: {
get: ({ sessionID }: { sessionID: string }) =>
Effect.sync(() => fixture.sessions.get(sessionID)!),
move: ({ sessionID, directory }: { sessionID: string; directory: string }) =>
Effect.sync(() => {
const child = fixture.sessions.get(sessionID)!;
fixture.sessions.set(sessionID, {
...child,
location: { directory: AbsolutePath.make(directory) },
});
}),
hook: (
name: string,
callback: (event: SessionHooks["prompt"]) => Effect.Effect<void>,
) =>
Effect.sync(() => {
if (name === "prompt") prompts.push(callback);
}).pipe(Effect.andThen(registration)),
},
};
// SAFETY: Exercise the exported composite through the real Promise adapter. The
// fixture implements precisely the accessed services; unused operations fail.
const install = plugin.effect as (
context: Pick<Context, "options">,
) => ReturnType<typeof plugin.effect>;
const call: Tool.Context = {
sessionID: Session.ID.make("ses_parent"),
agent: Agent.ID.make("multi-work"),
messageID: SessionMessage.ID.make("msg_fixture"),
id: Tool.CallID.make("call_fixture"),
progress: () => Effect.void,
};
const result = {
agents,
install: install(context),
replay: reload,
get reloads() {
return reloads;
},
configure(build: Agent.Info["permissions"], multi: Agent.Info["permissions"] = []) {
lateBuild = build;
lateMulti = multi;
return reload();
},
resume: () =>
tools.get("subagent")!.execute(
{
agent: "coordinate",
sessionID: "ses_child",
description: "resume",
prompt: "continue",
},
call,
),
create: () =>
tools.get("subagent")!.execute(
{
agent: "coordinate",
worktree: { base: "main" },
description: "new",
prompt: "work",
},
call,
),
admit: () =>
Effect.forEach(prompts, (hook) =>
hook({
sessionID: Session.ID.make("ses_child"),
messageID: SessionMessage.ID.make("msg_child"),
prompt: { text: "continue" },
delivery: "steer",
metadata: {},
}),
),
};
fixture.instances.set(directory, result);
return result;
}
test("exported composite inherits late build permissions, remains stable over two replays, and preserves later overrides", async () => {
const f = await scenario();
const oldPath = process.env.PATH;
process.env.PATH = `${f.bin}:${oldPath}`;
try {
const h = host(f, f.root);
const destination = host(f, f.destination);
await Effect.runPromise(
Effect.scoped(
Effect.gen(function* () {
yield* h.install;
yield* destination.install;
expect(h.agents.has("multi-work")).toBe(true);
expect(h.agents.get("multi-work")?.model).toEqual(
h.agents.get("coordinate")?.model,
);
expect(h.agents.get("multi-work")?.request).toEqual(
h.agents.get("coordinate")?.request,
);
const before = h.reloads;
yield* h.configure([
{ action: "herdr_worktree", resource: "*", effect: "deny" },
]);
yield* Effect.gen(function* () {
while (h.reloads < before + 2) yield* Effect.sleep("1 millis");
}).pipe(Effect.timeout("2 seconds"));
const inherited = () =>
h.agents
.get("multi-work")!
.permissions.filter((rule) => rule.action === "herdr_worktree");
const expected = [...inherited()];
expect(expected.at(-1)?.effect).toBe("deny");
for (let replay = 0; replay < 2; replay++) {
yield* h.replay();
expect(inherited()).toEqual(expected);
expect((yield* Effect.exit(h.create()))._tag).toBe("Failure");
}
yield* h.configure(
[{ action: "herdr_worktree", resource: "*", effect: "deny" }],
[{ action: "herdr_worktree", resource: "*", effect: "allow" }],
);
expect((yield* h.create()).content).toContain("fixture result");
const parent = f.sessions.get("ses_parent")!;
f.sessions.set(parent.id, {
...parent,
permissions: [{ action: "herdr_worktree", resource: "*", effect: "deny" }],
});
expect((yield* Effect.exit(h.resume()))._tag).toBe("Failure");
}),
),
);
} finally {
process.env.PATH = oldPath;
}
}, 15_000);
test("separate exported composite instances admit a disk-bound child and resume after plugin scopes restart", async () => {
const f = await scenario();
const oldPath = process.env.PATH;
process.env.PATH = `${f.bin}:${oldPath}`;
try {
for (let restart = 0; restart < 2; restart++) {
const root = host(f, f.root);
const destination = host(f, f.destination);
await Effect.runPromise(
Effect.scoped(
Effect.gen(function* () {
yield* root.install;
yield* destination.install;
expect(root.agents.has("multi-work")).toBe(true);
expect(destination.agents.has("multi-work")).toBe(false);
if (restart === 0) yield* root.create();
yield* destination.admit();
expect((yield* root.resume()).content).toContain(f.destination);
const child = f.sessions.get("ses_child")!;
f.sessions.set(child.id, {
...child,
location: { directory: AbsolutePath.make(f.root) },
});
expect((yield* Effect.exit(destination.admit()))._tag).toBe("Failure");
f.sessions.set(child.id, child);
}),
),
);
}
expect(f.admissions).toEqual([f.destination, f.destination, f.destination]);
} finally {
process.env.PATH = oldPath;
}
}, 15_000);
-85
View File
@@ -157,67 +157,6 @@ test("create verifies a linked workspace whose inventory source remains the pare
);
});
test("create passes an optional workspace label to Herdr", async () => {
const f = await fixture();
const root = (await f.herdr.root(f.root, f.signal))!;
const destination = await f.herdr.create(
root,
"main",
"fix/login-timeout",
f.signal,
"Fix login timeout",
);
expect(destination.branch).toBe("fix/login-timeout");
expect(f.label).toBe("Fix login timeout");
const creation = f.calls.find((args) => args[0] === "herdr" && args[2] === "create")!;
expect(creation.slice(creation.indexOf("--label"), creation.indexOf("--label") + 2)).toEqual([
"--label",
"Fix login timeout",
]);
});
test("create rejects an existing destination branch before invoking Herdr", async () => {
const f = await fixture();
const root = (await f.herdr.root(f.root, f.signal))!;
await expect(
f.herdr.create(root, "main", "invalid branch", f.signal, "Invalid"),
).rejects.toThrow();
await expect(f.herdr.create(root, "main", "other", f.signal, "Other")).rejects.toThrow(
"Destination branch already exists: other",
);
expect(f.calls.filter((args) => args[0] === "herdr" && args[2] === "create")).toHaveLength(0);
});
test("verify rejects changed parent, child and repository identities", async () => {
for (const change of [
{ sourceWorkspaceID: "w9" },
{ sourcePath: "child" },
{ childWorkspaceID: "w9" },
{ childPath: "root" },
{ repo: "child" },
]) {
const f = await fixture();
const root = (await f.herdr.root(f.root, f.signal))!;
const destination = await f.herdr.create(root, "main", "multi-work/fixture", f.signal);
f.changeInventory({
sourceWorkspaceID: change.sourceWorkspaceID,
sourcePath: change.sourcePath === "child" ? f.destination : undefined,
childWorkspaceID: change.childWorkspaceID,
childPath: change.childPath === "root" ? f.root : undefined,
repo: change.repo === "child" ? f.destination : undefined,
});
await expect(
f.herdr.verify(
destination.directory,
destination.workspaceID,
root,
destination.branch,
f.signal,
),
).rejects.toThrow("Bound Herdr checkout is no longer available");
}
});
test("closed, linked and ambiguous workspaces fail root gating", async () => {
for (const mode of ["close", "link", "duplicate"] as const) {
const f = await fixture();
@@ -225,27 +164,3 @@ test("closed, linked and ambiguous workspaces fail root gating", async () => {
expect(await f.herdr.root(f.root, f.signal)).toBeUndefined();
}
});
test("revalidates before mutation and rejects non-branch refs without creating a worktree", async () => {
const f = await fixture();
const root = (await f.herdr.root(f.root, f.signal))!;
const oid = await command(["git", "rev-parse", "HEAD"], f.root, f.signal);
for (const base of [oid, "--help", "main; touch injected"]) {
await expect(f.herdr.create(root, base, "multi-work/fixture", f.signal)).rejects.toThrow();
}
f.close();
await expect(f.herdr.create(root, "main", "multi-work/fixture", f.signal)).rejects.toThrow(
"no longer eligible",
);
expect(f.calls.filter((args) => args[0] === "herdr" && args[2] === "create")).toHaveLength(0);
});
test("a missing or malformed Herdr CLI never qualifies", async () => {
const f = await fixture();
await expect(new Herdr(async () => "not json").root(f.root, f.signal)).rejects.toThrow();
await expect(
new Herdr(async () => {
throw new Error("ENOENT");
}).root(f.root, f.signal),
).rejects.toThrow("ENOENT");
});
+78 -175
View File
@@ -1,15 +1,5 @@
import { afterEach, expect, test } from "bun:test";
import {
chmod,
mkdir,
mkdtemp,
readFile,
readdir,
rm,
stat,
symlink,
writeFile,
} from "node:fs/promises";
import { chmod, mkdir, mkdtemp, readFile, readdir, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { dirname, join } from "node:path";
import { createHash } from "node:crypto";
@@ -24,12 +14,26 @@ const unrelated = {
permissions: { shell: "ask" },
mcp: {
servers: {
codegraph: { type: "local", command: ["user-codegraph"], codemode: true },
codegraph: {
type: "local",
command: ["user-codegraph"],
codemode: true,
},
browser: { type: "local", command: ["user-browser"] },
},
},
};
const names = ["agents", "multi-work", "quota", "optchat"];
const empty = {
agents: {
"fast-lo": "",
"fast-hi": "",
"good-lo": "",
"good-hi": "",
"deep-lo": "",
"deep-hi": "",
},
};
const helper = new URL("../scripts/install.ts", import.meta.url);
async function run(
command: string[],
@@ -73,13 +77,20 @@ async function fixture() {
join(source, "dist", "tui.js"),
'export default { id: "stanp-opencode.quota-tui", setup() {} };',
);
const built = await Bun.build({ entrypoints: [helper.pathname], target: "bun", format: "esm" });
const built = await Bun.build({
entrypoints: [helper.pathname],
target: "bun",
format: "esm",
});
expect(built.success).toBe(true);
await Bun.write(join(source, "dist", "install.js"), built.outputs[0]!);
await writeFile(join(source, "package.json"), '{"type":"module"}');
await writeFile(
join(config, "opencode.json"),
JSON.stringify({ ...unrelated, plugins: [{ package: "before", options: { keep: true } }] }),
JSON.stringify({
...unrelated,
plugins: [{ package: "before", options: { keep: true } }],
}),
);
await writeFile(join(config, "opencode.jsonc"), "// untouched user config\n{}\n");
return { directory, source, config };
@@ -88,20 +99,13 @@ async function contents(config: string) {
return JSON.parse(await readFile(join(config, "opencode.json"), "utf8"));
}
for (const selection of [
"agents",
"quota",
"agents,quota",
"agents,multi-work",
"agents,multi-work,quota",
"optchat",
"agents,multi-work,quota,optchat",
]) {
for (const selection of ["quota", "agents,multi-work,quota,optchat"]) {
test(`installs valid subset ${selection}, preserving unrelated config and MCP`, async () => {
const f = await fixture();
await install(f.source, f.config, selection);
const result = await contents(f.config);
expect(result).toMatchObject(unrelated);
expect(result.experimental === undefined).toBe(selection === "quota");
expect(result.plugins.map((entry: { package: string }) => entry.package)).toEqual([
"before",
...selection.split(",").map((name) => `./plugins/stanp-opencode/${name}`),
@@ -123,7 +127,9 @@ test("upgrades retain selection and options; explicit selection replaces only ow
const f = await fixture();
await install(f.source, f.config, "agents,quota");
const current = await contents(f.config);
current.plugins[1].options = { mode: "gpt", provider: "openai", lunaPriority: false };
current.plugins[1].options = {
agents: { ...empty.agents, "fast-lo": "openai/gpt-6-luna#low" },
};
current.plugins.push({ package: "after", options: { untouched: 1 } });
await writeFile(join(f.config, "opencode.json"), JSON.stringify(current));
await install(f.source, f.config);
@@ -136,99 +142,61 @@ test("upgrades retain selection and options; explicit selection replaces only ow
]);
});
test("mixed string/object registrations upgrade without flags and preserve unrelated entries verbatim", async () => {
test("installer writes empty option skeletons and subagent_depth, fills only missing keys, and is idempotent", async () => {
const f = await fixture();
const before = "unrelated-plugin";
const after = { package: "unrelated-object", options: { keep: [1, true, "value"] } };
await install(f.source, f.config, "agents,multi-work,quota,optchat");
const fresh = await contents(f.config);
expect(fresh.plugins.slice(1)).toEqual([
{ package: "./plugins/stanp-opencode/agents", options: empty },
{ package: "./plugins/stanp-opencode/multi-work" },
{ package: "./plugins/stanp-opencode/quota" },
{
package: "./plugins/stanp-opencode/optchat",
options: { compactionModel: "", dataDir: "" },
},
]);
expect(fresh.experimental).toEqual({ subagent_depth: 2 });
fresh.plugins[1].options = {
agents: { "fast-lo": "openai/gpt-6-luna#low" },
extra: 1,
};
fresh.plugins[4].options = { compactionModel: "acme/cheap" };
await writeFile(join(f.config, "opencode.json"), JSON.stringify(fresh));
await install(f.source, f.config);
const filled = await contents(f.config);
expect(filled.plugins[1].options).toEqual({
agents: { ...empty.agents, "fast-lo": "openai/gpt-6-luna#low" },
extra: 1,
});
expect(filled.plugins[4].options).toEqual({
compactionModel: "acme/cheap",
dataDir: "",
});
expect(filled).toMatchObject(unrelated);
await install(f.source, f.config);
expect(await contents(f.config)).toEqual(filled);
for (const [depth, expected] of [
[1, 2],
[3, 3],
]) {
await writeFile(
join(f.config, "opencode.json"),
JSON.stringify({
...unrelated,
plugins: [before, { package: "./plugins/stanp-opencode/quota" }, after],
...filled,
experimental: { subagent_depth: depth, other: true },
}),
);
await install(f.source, f.config);
expect((await contents(f.config)).plugins).toEqual([
before,
{ package: "./plugins/stanp-opencode/quota" },
after,
]);
await install(f.source, f.config, "agents,quota");
const updated = await contents(f.config);
expect(updated.plugins).toEqual([
before,
{
package: "./plugins/stanp-opencode/agents",
options: { mode: "gpt", provider: "openai", lunaPriority: true },
},
{ package: "./plugins/stanp-opencode/quota" },
after,
]);
expect(updated).toMatchObject(unrelated);
});
test("owned string registrations retain their selection and explicit selection removes mixed duplicates", async () => {
const f = await fixture();
await writeFile(
join(f.config, "opencode.json"),
JSON.stringify({
plugins: [
"before",
"./plugins/stanp-opencode/agents",
"./plugins/stanp-opencode/multi-work",
"./plugins/stanp-opencode/quota",
{ package: "stanp-opencode/quota" },
"after",
],
}),
);
await install(f.source, f.config);
const retained = await contents(f.config);
expect(retained.plugins).toEqual([
"before",
{
package: "./plugins/stanp-opencode/agents",
options: { mode: "gpt", provider: "openai", lunaPriority: true },
},
{ package: "./plugins/stanp-opencode/multi-work" },
{ package: "./plugins/stanp-opencode/quota" },
"after",
]);
await install(f.source, f.config, "quota");
expect((await contents(f.config)).plugins).toEqual([
"before",
{ package: "./plugins/stanp-opencode/quota" },
"after",
]);
});
for (const target of ["./plugins/stanp-opencode", "stanp-opencode@1.0.0", "absolute"]) {
test(`owned monolith string ${target} upgrades without a selection`, async () => {
const f = await fixture();
const packageName =
target === "absolute" ? join(f.config, "plugins", "stanp-opencode") : target;
await writeFile(
join(f.config, "opencode.json"),
JSON.stringify({ plugins: ["before", packageName, "after"] }),
);
await install(f.source, f.config);
expect((await contents(f.config)).plugins).toEqual([
"before",
{
package: "./plugins/stanp-opencode/agents",
options: { mode: "gpt", provider: "openai", lunaPriority: true },
},
{ package: "./plugins/stanp-opencode/multi-work" },
{ package: "./plugins/stanp-opencode/quota" },
"after",
]);
expect((await contents(f.config)).experimental).toEqual({
subagent_depth: expected,
other: true,
});
}
});
test("local installer retains executable permissions and supports direct invocation", async () => {
test("local installer supports direct invocation", async () => {
const f = await fixture();
const path = new URL("../install-local.sh", import.meta.url).pathname;
expect((await stat(path)).mode & 0o777).toBe(0o755);
const result = await run([path, "--plugins", "multi-work"], f.directory);
expect(result.exitCode).toBe(2);
expect(result.stderr).toContain("Multi-work requires Agents");
@@ -244,7 +212,7 @@ test("replaces the legacy monolith without duplicates and retains Agents options
{ package: "before" },
{
package: "./plugins/stanp-opencode",
options: { mode: "gpt", provider: "openai", lunaPriority: false },
options: { agents: { "fast-lo": "openai/gpt-6-luna#low" } },
},
{ package: "stanp-opencode" },
{ package: "after" },
@@ -263,7 +231,7 @@ test("replaces the legacy monolith without duplicates and retains Agents options
.map((name) => `./plugins/stanp-opencode/${name}`),
"after",
]);
expect(result.plugins[1].options.lunaPriority).toBe(false);
expect(result.plugins[1].options.agents["fast-lo"]).toBe("openai/gpt-6-luna#low");
expect(await Bun.file(join(f.config, "plugins", "stanp-opencode.js")).exists()).toBe(false);
expect(result).toMatchObject(unrelated);
});
@@ -284,43 +252,6 @@ test("noninteractive fresh installs require a selection; invalid selections leav
expect(await readFile(join(f.config, "opencode.json"), "utf8")).toBe(original);
});
test("interactive Multi-work-only selection offers Agents", async () => {
const f = await fixture();
const command = `${process.execPath} '${helper.pathname}' '${f.source}' '${f.config}'`;
const child = Bun.spawn(["script", "-qec", command, "/dev/null"], {
cwd: f.directory,
stdin: "pipe",
stdout: "pipe",
stderr: "pipe",
timeout: 10_000,
});
let output = "";
let selected = false;
let confirmed = false;
const reader = child.stdout.getReader();
while (true) {
const chunk = await reader.read();
if (chunk.done) break;
output += new TextDecoder().decode(chunk.value);
if (!selected && output.includes("Plugins (")) {
child.stdin.write("multi-work\n");
selected = true;
}
if (!confirmed && output.includes("Add Agents?")) {
child.stdin.write("y\n");
confirmed = true;
}
}
expect(await child.exited, output).toBe(0);
child.stdin.end();
expect(output).toContain("Add Agents?");
expect(
(await contents(f.config)).plugins
.slice(1)
.map((entry: { package: string }) => entry.package),
).toEqual(["./plugins/stanp-opencode/agents", "./plugins/stanp-opencode/multi-work"]);
});
test("artifact validation failure leaves prior installation and config intact", async () => {
const f = await fixture();
await install(f.source, f.config, "quota");
@@ -342,36 +273,6 @@ test("artifact validation failure leaves prior installation and config intact",
expect(await readdir(join(f.config, "plugins"))).toEqual(["stanp-opencode"]);
});
test("replaces an owned installation symlink without touching its target or unrelated plugin directories", async () => {
const f = await fixture();
const external = join(f.directory, "old installation");
const sibling = join(f.config, "plugins", "unrelated");
await mkdir(external);
await mkdir(sibling, { recursive: true });
await writeFile(join(external, "keep.txt"), "old work is untouched");
await writeFile(join(sibling, "index.js"), "unrelated plugin");
await symlink(external, join(f.config, "plugins", "stanp-opencode"));
await install(f.source, f.config, "quota");
expect(await readFile(join(external, "keep.txt"), "utf8")).toBe("old work is untouched");
expect(await readFile(join(sibling, "index.js"), "utf8")).toBe("unrelated plugin");
expect(
await Bun.file(join(f.config, "plugins", "stanp-opencode", "quota", "index.js")).exists(),
).toBe(true);
});
test("both shell installers reject explicit Multi-work-only selection before doing work", async () => {
const f = await fixture();
for (const file of ["install.sh", "install-local.sh"]) {
const result = await run(
["bash", new URL(`../${file}`, import.meta.url).pathname, "--plugins", "multi-work"],
f.directory,
);
expect(result.exitCode).toBe(2);
expect(result.stderr).toContain("Multi-work requires Agents");
}
expect((await contents(f.config)).plugins).toHaveLength(1);
});
test("local shell installer builds its checkout and upgrades mixed entries without flags", async () => {
const f = await fixture();
const after = { package: "unrelated-object", options: { keep: true } };
@@ -417,7 +318,9 @@ for (const validChecksum of [true, false])
if (validChecksum)
await writeFile(
join(f.config, "opencode.json"),
JSON.stringify({ plugins: ["unrelated-plugin", "./plugins/stanp-opencode/quota"] }),
JSON.stringify({
plugins: ["unrelated-plugin", "./plugins/stanp-opencode/quota"],
}),
);
const archiveRoot = join(f.directory, "archive");
await mkdir(archiveRoot);
-394
View File
@@ -326,54 +326,6 @@ const request = z.object({
background: z.boolean().optional(),
});
test("Effect-native wrapper preserves name, metadata and result; moves before prompt and resumes its bound child", async () => {
const f = fixture();
await Effect.runPromise(
Effect.scoped(
Effect.gen(function* () {
yield* f.install;
expect(f.agents.get("multi-work")?.mode).toBe("primary");
expect(f.tool.name).toBe("subagent");
expect(f.tool.options?.codemode).toBe(false);
expect(f.agents.get("coordinate")?.mode).toBe("all");
const result = yield* f.invoke();
expect(result.content).toStartWith("native result");
expect(result.content).toContain("Checkout: /worktree");
expect(result.metadata?.sessionID).toBe("ses_child1");
expect(f.order).toEqual(["create", "move-request", "moved", "prompt:/worktree"]);
expect(f.parent.location.directory).toBe("/root");
expect(bindingSchema.parse(f.storage.get(bindingKey("ses_child1"))).ready).toBe(
true,
);
const resume = yield* f.execute({
agent: "coordinate",
description: "resume",
prompt: "continue",
sessionID: "ses_child1",
background: true,
});
expect(resume.metadata?.status).toBe("running");
expect(f.herdr.creations).toBe(1);
expect(f.herdr.verified).toBe(1);
}),
),
);
});
test("local fast-lo uses native delegation without a worktree", async () => {
const f = fixture();
await Effect.runPromise(
Effect.scoped(
Effect.gen(function* () {
yield* f.install;
yield* f.execute({ agent: "fast-lo", description: "lookup", prompt: "read" });
expect(f.herdr.creations).toBe(0);
expect(f.order).toEqual(["create", "prompt:/root"]);
}),
),
);
});
test("coordinate can create and resume a root-local background child without Herdr creation or movement", async () => {
const f = fixture();
f.parent.permissions = [{ action: "herdr_worktree", resource: "*", effect: "deny" }];
@@ -496,83 +448,6 @@ test("interrupted root-local creation retains its binding and cannot be blindly
expect(f.prompts).toBe(1);
});
test("custom branch and workspace label pass through delegation", async () => {
const f = fixture();
await Effect.runPromise(
Effect.scoped(
Effect.gen(function* () {
yield* f.install;
const result = yield* f.execute({
agent: "coordinate",
description: "Fix login timeout",
prompt: "work",
worktree: {
base: "main",
branch: "fix/login-timeout",
label: "Fix login timeout",
},
});
expect(f.herdr.createdBase).toBe("main");
expect(f.herdr.createdBranch).toBe("fix/login-timeout");
expect(f.herdr.createdLabel).toBe("Fix login timeout");
expect(result.metadata?.branch).toBe("fix/login-timeout");
}),
),
);
});
test("omitted naming retains generated branch and no explicit label", async () => {
const f = fixture();
await Effect.runPromise(
Effect.scoped(
Effect.gen(function* () {
yield* f.install;
yield* f.invoke();
expect(f.herdr.createdBranch).toMatch(/^multi-work\/[0-9a-f-]{36}$/);
expect(f.herdr.createdLabel).toBeUndefined();
}),
),
);
});
test("rejects empty naming fields and naming on resume", async () => {
const f = fixture();
await Effect.runPromise(
Effect.scoped(
Effect.gen(function* () {
yield* f.install;
for (const worktree of [
{ base: "main", branch: " " },
{ base: "main", label: " " },
])
expect(
(yield* Effect.exit(
f.execute({
agent: "coordinate",
description: "invalid",
prompt: "work",
worktree,
}),
))._tag,
).toBe("Failure");
yield* f.invoke();
expect(
(yield* Effect.exit(
f.execute({
agent: "coordinate",
description: "resume",
prompt: "work",
sessionID: "ses_child1",
worktree: { base: "main", branch: "fix/other" },
}),
))._tag,
).toBe("Failure");
expect(f.herdr.creations).toBe(1);
}),
),
);
});
test("root loss, session permission denial and missing destination fail before prompting", async () => {
for (const failure of ["root", "permission", "ask", "destination", "project"] as const) {
const f = fixture();
@@ -602,272 +477,3 @@ test("root loss, session permission denial and missing destination fail before p
);
}
});
test("interruption crosses the wrapper into the native executor finalizer", async () => {
const f = fixture();
f.hold();
const controller = new AbortController();
const running = Effect.runPromise(
Effect.scoped(
Effect.gen(function* () {
yield* f.install;
return yield* f.invoke();
}),
),
{ signal: controller.signal },
);
await f.entered.promise;
// entered may resolve at move request; yield until the native foreground wait is reached.
while (f.prompts === 0) await Bun.sleep(1);
controller.abort();
await expect(running).rejects.toBeDefined();
expect(f.cancelled).toBe(true);
});
test("move admission alone never admits a task; cancellation retains an unready binding", async () => {
const f = fixture();
f.delayMove();
const controller = new AbortController();
const running = Effect.runPromise(
Effect.scoped(
Effect.gen(function* () {
yield* f.install;
return yield* f.invoke();
}),
),
{ signal: controller.signal },
);
await f.entered.promise;
expect(f.prompts).toBe(0);
controller.abort();
await expect(running).rejects.toBeDefined();
expect(f.prompts).toBe(0);
expect(bindingSchema.parse(f.storage.get(bindingKey("ses_child1"))).ready).toBe(false);
});
test("rejects foreign and changed-agent resumes, non-primary multi-work and unbound coordinate calls", async () => {
const f = fixture();
await Effect.runPromise(
Effect.scoped(
Effect.gen(function* () {
yield* f.install;
yield* f.invoke();
f.sessions.get("ses_child1")!.agent = "fast-lo";
expect(
(yield* Effect.exit(
f.execute({
agent: "coordinate",
description: "resume",
prompt: "x",
sessionID: "ses_child1",
}),
))._tag,
).toBe("Failure");
f.parent.parentID = "ses_foreign";
expect((yield* Effect.exit(f.invoke()))._tag).toBe("Failure");
f.parent.parentID = undefined;
expect(
(yield* Effect.exit(
f.invoke(undefined, { ...f.call, agent: Agent.ID.make("build") }),
))._tag,
).toBe("Failure");
expect(
(yield* Effect.exit(
f.execute({ agent: "multi-work", description: "x", prompt: "x" }),
))._tag,
).toBe("Failure");
expect(f.herdr.creations).toBe(1);
}),
),
);
});
test("command-created children require ready owned bindings and cannot bypass placement guards", async () => {
const f = fixture();
await Effect.runPromise(
Effect.scoped(
Effect.gen(function* () {
yield* f.install;
yield* f.invoke();
const child = f.sessions.get("ses_child1")!;
const binding = bindingSchema.parse(f.storage.get(bindingKey(child.id)));
const admit = () => f.prompt({ sessionID: Session.ID.make(child.id) });
yield* admit();
f.storage.set(bindingKey(child.id), { ...binding, ready: false });
expect((yield* Effect.exit(admit()))._tag).toBe("Failure");
f.storage.set(bindingKey(child.id), binding);
child.location.directory = "/relocated";
expect((yield* Effect.exit(admit()))._tag).toBe("Failure");
child.location.directory = binding.directory;
child.projectID = "foreign";
expect((yield* Effect.exit(admit()))._tag).toBe("Failure");
child.projectID = binding.projectID;
f.parent.location.directory = "/elsewhere";
expect((yield* Effect.exit(admit()))._tag).toBe("Failure");
f.parent.location.directory = binding.root.directory;
f.parent.agent = "build";
expect((yield* Effect.exit(admit()))._tag).toBe("Failure");
f.parent.agent = "multi-work";
child.agent = "multi-work";
expect((yield* Effect.exit(admit()))._tag).toBe("Failure");
child.agent = "fast-hi";
expect((yield* Effect.exit(admit()))._tag).toBe("Failure");
child.agent = "fast-lo";
expect((yield* Effect.exit(admit()))._tag).toBe("Failure");
child.location.directory = f.parent.location.directory;
yield* admit();
}),
),
);
});
test("ineligible roots do not register multi-work; multi-work cannot execute shell even with session overrides", async () => {
const f = fixture();
f.herdr.eligible = false;
await Effect.runPromise(
Effect.scoped(
Effect.gen(function* () {
yield* f.install;
expect(f.agents.has("multi-work")).toBe(false);
const hook = f.toolHooks[0]!;
expect(
(yield* Effect.exit(hook({ ...f.call, tool: "shell", input: {} })))._tag,
).toBe("Failure");
}),
),
);
});
test("interruption during Herdr creation aborts the command and retains an inspectable attempt without prompting", async () => {
const f = fixture();
f.herdr.delayCreation = true;
const controller = new AbortController();
const running = Effect.runPromise(
Effect.scoped(
Effect.gen(function* () {
yield* f.install;
return yield* f.invoke();
}),
),
{ signal: controller.signal },
);
await f.herdr.creationStarted.promise;
controller.abort();
await expect(running).rejects.toBeDefined();
expect(f.herdr.creationAborted).toBe(true);
expect(f.prompts).toBe(0);
expect(f.storage.has(`multi-work-call:${f.call.sessionID}:${f.call.id}`)).toBe(true);
});
test("waits for asynchronous destination plugin activation before moving or prompting", async () => {
const f = fixture();
f.warmDestination();
await Effect.runPromise(
Effect.scoped(
Effect.gen(function* () {
yield* f.install;
yield* f.invoke();
expect(f.agentReads).toBe(2);
expect(f.prompts).toBe(1);
expect(f.order).toEqual(["create", "move-request", "moved", "prompt:/worktree"]);
}),
),
);
});
test("configured build restrictions remain effective for worktree preparation", async () => {
const f = fixture();
f.agents.set("build", {
...Agent.Info.default(Agent.ID.make("build")),
permissions: [{ action: "herdr_worktree", resource: "*", effect: "deny" }],
});
await Effect.runPromise(
Effect.scoped(
Effect.gen(function* () {
yield* f.install;
expect((yield* Effect.exit(f.invoke()))._tag).toBe("Failure");
expect(f.herdr.creations).toBe(0);
}),
),
);
});
test("worktree resumes retain the herdr_worktree permission guard", async () => {
const f = fixture();
await Effect.runPromise(
Effect.scoped(
Effect.gen(function* () {
yield* f.install;
yield* f.invoke();
f.parent.permissions = [
{ action: "herdr_worktree", resource: "*", effect: "deny" },
];
const resumed = yield* Effect.exit(
f.execute({
agent: "coordinate",
description: "resume",
prompt: "continue",
sessionID: "ses_child1",
}),
);
expect(resumed._tag).toBe("Failure");
expect(f.herdr.verified).toBe(0);
expect(f.prompts).toBe(1);
}),
),
);
});
test("failed initialization records the attempt and never blindly recreates on replay", async () => {
const f = fixture();
f.missingDestination();
await Effect.runPromise(
Effect.scoped(
Effect.gen(function* () {
yield* f.install;
expect((yield* Effect.exit(f.invoke()))._tag).toBe("Failure");
expect((yield* Effect.exit(f.invoke()))._tag).toBe("Failure");
expect(f.herdr.creations).toBe(1);
expect(f.prompts).toBe(0);
const record = z
.object({ child: z.string(), branch: z.string() })
.parse(f.storage.get(`multi-work-call:${f.call.sessionID}:${f.call.id}`));
expect(record.child).toBe("ses_child1");
expect(record.branch).toStartWith("multi-work/");
}),
),
);
});
test("same-child concurrent resumes are rejected while the native foreground executor is active", async () => {
const f = fixture();
f.hold();
const controller = new AbortController();
const checked = Promise.withResolvers<void>();
const running = Effect.runPromise(
Effect.scoped(
Effect.gen(function* () {
yield* f.install;
yield* f.invoke().pipe(Effect.forkScoped);
yield* Effect.promise(() => f.entered.promise);
const second = yield* Effect.exit(
f.execute({
agent: "coordinate",
description: "resume",
prompt: "continue",
sessionID: "ses_child1",
}),
);
expect(second._tag).toBe("Failure");
expect(f.herdr.creations).toBe(1);
checked.resolve();
return yield* Effect.never;
}),
),
{ signal: controller.signal },
);
await Promise.race([checked.promise, running]);
controller.abort();
await expect(running).rejects.toBeDefined();
expect(f.cancelled).toBe(true);
});
+23 -112
View File
@@ -5,17 +5,13 @@ import {
VIEW_HIGH,
VIEW_LOW,
before,
blocks,
bytes,
clean,
compressTask,
contains,
grow,
key,
merge,
mergeTask,
pieces,
render,
tooLong,
viewBytes,
type Line,
@@ -67,23 +63,7 @@ test("due ordering reproduces Taelin's push at its list length for t=0..2000", (
}
});
test("merging from the first message diverges from push (the recipe's T=10 example)", () => {
const text = () => "";
// View 0+4, 4+4, 8+1, 9+1 at T=10: push merges 8-9, not 0-7.
const view: Line[] = [
[2, 0],
[2, 1],
[0, 8],
[0, 9],
];
expect(merge(view, 10, viewBytes(view, text) - 1, text)).toEqual([
[2, 0],
[2, 1],
[1, 4],
]);
});
test("merge skips pairs whose parent is unbuilt", () => {
test("merges and compaction context never use unbuilt lines", () => {
const built = new Set(["1:0"]);
const text = (line: Line) => (line[0] === 0 || built.has(key(line)) ? "" : undefined);
const view: Line[] = [
@@ -97,9 +77,22 @@ test("merge skips pairs whose parent is unbuilt", () => {
[0, 2],
[0, 3],
]);
const context = (line: Line) => (key(line) === "0:5" ? undefined : "t");
const zoomed: Line[] = [
[2, 0],
[0, 4],
[0, 5],
[0, 6],
];
expect(before(zoomed, 4, context)).toEqual([[2, 0]]);
expect(before(zoomed, 7, context)).toEqual([
[2, 0],
[0, 4],
]);
});
test("view is a sawtooth: grows past 128 KB, then one batch drops it to 64 KB", () => {
test("view is a sawtooth: grows past 128 KB, one batch drops it to 64 KB, and a short batch continues", () => {
{
const line = "y".repeat(500);
const text = () => line;
const view: View = { lines: [], batching: false };
@@ -128,9 +121,8 @@ test("view is a sawtooth: grows past 128 KB, then one batch drops it to 64 KB",
next += 2 ** l;
}
expect(next).toBe(2000);
});
test("a batch that cannot reach 64 KB continues at the next message", () => {
}
{
let parents = false;
const text = (line: Line) => (line[0] === 0 || parents ? "z".repeat(500) : undefined);
const view: View = { lines: [], batching: false };
@@ -144,51 +136,15 @@ test("a batch that cannot reach 64 KB continues at the next message", () => {
parents = true;
// Nothing was built since the last try: the batch waits.
const waiting = grow(view, 260, text, total, false);
expect(waiting).toEqual({ merged: false, tried: false, total: viewBytes(view.lines, text) });
expect(waiting).toEqual({
merged: false,
tried: false,
total: viewBytes(view.lines, text),
});
expect(grow(view, 261, text, waiting.total, true).merged).toBe(true);
expect(viewBytes(view.lines, text)).toBeLessThanOrEqual(VIEW_LOW);
expect(view.batching).toBe(false);
});
test("compaction context stops before the target and at the first unbuilt line", () => {
const text = (line: Line) => (key(line) === "0:5" ? undefined : "t");
const view: Line[] = [
[2, 0],
[0, 4],
[0, 5],
[0, 6],
];
expect(before(view, 4, text)).toEqual([[2, 0]]);
expect(before(view, 7, text)).toEqual([
[2, 0],
[0, 4],
]);
});
test("view blocks keep whole 4-line blocks stable as lines are appended", () => {
const a = blocks(["1", "2", "3", "4", "5", "6"]);
expect(a.whole).toEqual(["<chat>\n1\n2\n3\n4\n"]);
expect(a.tail).toBe("5\n6\n</chat>");
const b = blocks(["1", "2", "3", "4", "5", "6", "7", "8", "9"]);
expect(b.whole[0]).toBe(a.whole[0]!);
expect(b.tail).toBe("9\n</chat>");
expect(blocks([])).toEqual({ whole: [], tail: "<chat>\n</chat>" });
});
test("lines render with labels and without newlines", () => {
expect(render([3, 5], "a\nb")).toBe("40+8|a b");
expect(render([0, 7], undefined)).toBe("7+1|(not summarized yet: zoom it)");
});
test("long tool outputs are clipped; other long texts are split", () => {
const long = "a".repeat(20_000) + "b".repeat(20_000);
const [echo] = pieces("echo", long);
expect(echo!.startsWith("a".repeat(15_000))).toBe(true);
expect(echo!.endsWith("b".repeat(15_000))).toBe(true);
expect(echo!).toContain("10000 characters clipped");
expect(pieces("agent", long).join("")).toBe(long);
expect(pieces("agent", long)).toHaveLength(2);
expect(pieces("user", "short")).toEqual(["short"]);
}
});
test("tasks are verbatim and Too long cuts at 512 bytes", () => {
@@ -272,35 +228,6 @@ test("queue resumes unbuilt messages and ready merges from disk", async () => {
expect(started).toEqual(["0:3", "1:0", "1:1", "2:0"]);
});
test("a long unbuilt view grows in linear time and pending batches wait for builds", () => {
// Regression: enabling optchat in a long session re-measured and re-merged the whole view per message.
const text = () => undefined;
const view: View = { lines: [], batching: false };
let total = 0;
let built = true;
const started = performance.now();
for (let i = 0; i < 20_000; i++) {
const grown = grow(view, i, text, total, built);
total = grown.total;
if (grown.tried) built = false;
}
expect(total).toBe(viewBytes(view.lines, text));
expect(view.batching).toBe(true);
expect(performance.now() - started).toBeLessThan(2_000);
});
test("contains finds exactly the view's lines", () => {
const view: Line[] = [
[2, 0],
[1, 2],
[0, 6],
];
expect(contains(view, [1, 2])).toBe(true);
expect(contains(view, [0, 6])).toBe(true);
expect(contains(view, [0, 4])).toBe(false);
expect(contains(view, [0, 7])).toBe(false);
});
test("a merge is queued once when both halves finish in the same tick", async () => {
// Regression: short leaves build synchronously, so both finishes saw the parent unbuilt.
const tree = new Set<string>();
@@ -319,19 +246,3 @@ test("a merge is queued once when both halves finish in the same tick", async ()
await Bun.sleep(30);
expect(starts).toEqual(["0:0", "0:1", "1:0"]);
});
test("settle stops waiting once failures leave nothing running", async () => {
// Regression: it waited the full timeout though failures retry only at the next message.
const queue = new Queue(
() => false,
async () => {
await Bun.sleep(5);
return false;
},
);
queue.add(0);
const started = Date.now();
expect(await queue.settle(1, 2_000)).toBe(false);
expect(Date.now() - started).toBeLessThan(500);
expect(queue.running).toBe(0);
});
-15
View File
@@ -55,21 +55,6 @@ test("logs append-only, dedupe by source, split long texts, and reload with tree
await again.close();
});
test("messages logged after the saved view are appended to it at load", async () => {
const directory = await fixture();
const memory = await Memory.open(directory);
memory.log("user", "a", "s:0");
memory.saveView();
memory.log("user", "b", "s:1");
await memory.close();
const again = await Memory.open(directory);
expect(again.view.lines).toEqual([
[0, 0],
[0, 1],
]);
await again.close();
});
test("one writer: the lock rejects a live holder and replaces a dead one", async () => {
const directory = await fixture();
const memory = await Memory.open(directory);
+77 -70
View File
@@ -91,7 +91,10 @@ async function host() {
callback: (editor: {
add: (definition: { execute: typeof command }) => void;
}) => void,
) => callback({ add: (definition) => (command = definition.execute) }),
) =>
callback({
add: (definition) => (command = definition.execute),
}),
},
session: {
hook: async (name: string, callback: (event: Event) => Promise<void> | void) => {
@@ -106,7 +109,11 @@ async function host() {
: { id: sessionID, agent: "build" },
create: async (input: { parentID: string; model: object }) => {
expect(input.parentID).toBe("ses_root");
expect(input.model).toEqual({ providerID: "acme", id: "cheap", variant: "xhigh" });
expect(input.model).toEqual({
providerID: "acme",
id: "cheap",
variant: "xhigh",
});
return { id: "ses_worker" };
},
generate: async ({ sessionID, prompt }: { sessionID: string; prompt: string }) => {
@@ -116,7 +123,10 @@ async function host() {
agent: "build",
system: [{ type: "text", text: "host system" }],
messages: [
new Entry({ role: "user", content: [{ type: "text", text: prompt }] }),
new Entry({
role: "user",
content: [{ type: "text", text: prompt }],
}),
],
tools: {},
};
@@ -137,7 +147,11 @@ async function host() {
sessionID,
agent: "build",
system: [
{ type: "text", text: "host one", cache: { type: "ephemeral" } },
{
type: "text",
text: "host one",
cache: { type: "ephemeral" },
},
{ type: "text", text: "host two" },
],
messages: messages.map((message) => new Entry(message)),
@@ -187,7 +201,10 @@ test("optchat is off by default, refuses child sessions, and hides its tools whe
test("turns see the view and the new message; compactions retry Too long and build the tree", async () => {
const h = await host();
await h.command()({ sessionID: "ses_root", prompt: { text: "/optchat on" } });
await h.command()({
sessionID: "ses_root",
prompt: { text: "/optchat on" },
});
expect(h.notices.at(-1)).toStartWith("[optchat] on. 0 messages");
const long = "please remember: " + "detail ".repeat(100);
h.replies.push("x".repeat(700), "y".repeat(600), "z".repeat(400));
@@ -204,7 +221,10 @@ test("turns see the view and the new message; compactions retry Too long and bui
expect(first.messages[1]!.content[0]!.text).toBe(long);
// System start and end, and the request's end; no whole view block yet.
expect(hints(first)).toBe(3);
expect(first.messages[1]!.content[0]!.cache).toEqual({ type: "ephemeral", ttlSeconds: 300 });
expect(first.messages[1]!.content[0]!.cache).toEqual({
type: "ephemeral",
ttlSeconds: 300,
});
await settle();
// The compaction ran in the worker with the same system prompt, retried twice, and kept the shortest line.
@@ -226,7 +246,12 @@ test("turns see the view and the new message; compactions retry Too long and bui
role: "assistant",
content: [
{ type: "reasoning", text: "never logged" },
{ type: "tool-call", id: "c1", name: "read", input: { path: "a" } },
{
type: "tool-call",
id: "c1",
name: "read",
input: { path: "a" },
},
],
},
{
@@ -241,8 +266,16 @@ test("turns see the view and the new message; compactions retry Too long and bui
},
],
},
{ id: "m4", role: "assistant", content: [{ type: "text", text: "Read a." }] },
{ id: "m5", role: "user", content: [{ type: "text", text: "and now?" }] },
{
id: "m4",
role: "assistant",
content: [{ type: "text", text: "Read a." }],
},
{
id: "m5",
role: "user",
content: [{ type: "text", text: "and now?" }],
},
]);
// Messages before the new one are summarized before the turn; short ones are their own lines.
expect(second.messages.map((message) => message.role)).toEqual(["user", "user"]);
@@ -330,7 +363,12 @@ test("dropped reasoning parts and tool messages without ids do not re-log or ski
role: "assistant",
content: [
{ type: "reasoning", text: "" },
{ type: "tool-call", id: "c1", name: "read", input: { path: "a" } },
{
type: "tool-call",
id: "c1",
name: "read",
input: { path: "a" },
},
],
},
{
@@ -360,9 +398,17 @@ test("dropped reasoning parts and tool messages without ids do not re-log or ski
content: message.content.filter((part) => part.type !== "reasoning"),
}));
await h.turn("ses_root", [
{ id: "m0", role: "system", content: [{ type: "text", text: "Instructions updated" }] },
{
id: "m0",
role: "system",
content: [{ type: "text", text: "Instructions updated" }],
},
...after,
{ id: "m5", role: "assistant", content: [{ type: "text", text: "ok" }] },
{
id: "m5",
role: "assistant",
content: [{ type: "text", text: "ok" }],
},
{ id: "m6", role: "user", content: [{ type: "text", text: "again" }] },
]);
await settle();
@@ -378,62 +424,23 @@ test("dropped reasoning parts and tool messages without ids do not re-log or ski
]);
});
test("instruction updates before the turn stay after the view", async () => {
// Regression: system-role messages before the turn start were dropped.
const h = await host();
await h.command()({ sessionID: "ses_root", prompt: { text: "on" } });
const event = await h.turn("ses_root", [
{ id: "m1", role: "user", content: [{ type: "text", text: "hi" }] },
{ id: "m2", role: "assistant", content: [{ type: "text", text: "hello" }] },
{ id: "m3", role: "system", content: [{ type: "text", text: "Instructions updated" }] },
{ id: "m4", role: "user", content: [{ type: "text", text: "next" }] },
]);
expect(event.messages.map((message) => message.role)).toEqual(["user", "system", "user"]);
expect(event.messages[1]!.content[0]!.text).toBe("Instructions updated");
if (h.cleanup) await h.cleanup();
});
test("checkpoints are not logged, provider-executed calls end a turn, and empty replies stop retries", async () => {
const h = await host();
await h.command()({ sessionID: "ses_root", prompt: { text: "on" } });
h.replies.push("");
const event = await h.turn("ses_root", [
{
id: "m1",
role: "user",
content: [
{ type: "text", text: "<conversation-checkpoint>old</conversation-checkpoint>" },
],
},
{
id: "m2",
role: "assistant",
content: [
{
type: "tool-call",
id: "w1",
name: "web_search",
input: {},
providerExecuted: true,
},
{ type: "text", text: "x".repeat(600) },
],
},
{ id: "m3", role: "user", content: [{ type: "text", text: "next" }] },
]);
// The turn is the new message; the provider-executed call did not keep the old turn open.
expect(event.messages.map((message) => message.content[0]!.text)).toEqual([
"<chat>\n0+1|tool: web_search {}\n1+1|(not summarized yet: zoom it)\n</chat>",
"next",
]);
await settle();
// The empty reply ended its compaction without a "Too long" retry; the failure is retried at
// the next message, as a fresh task.
expect(h.generated.map((request) => request.messages.length)).toEqual([2, 2]);
if (h.cleanup) await h.cleanup();
expect(await logged(h.directory)).toEqual([
"tool: web_search {}",
`agent: ${"x".repeat(600)}`,
"user: next",
]);
test("without compactionModel, optchat registers nothing", async () => {
const options = { compactionModel: "", dataDir: "" };
const registered: string[] = [];
const record = (name: string) => async () => {
registered.push(name);
return { dispose: async () => {} };
};
const context = {
options,
tool: { transform: record("tool") },
command: { transform: record("command") },
session: { hook: record("hook") },
};
// SAFETY: setup must return before touching any host service; a call is recorded and fails the test.
const setup = optchat.setup as (
context: Pick<Plugin.Context, "options">,
) => ReturnType<typeof optchat.setup>;
expect(await setup(context)).toBeUndefined();
expect(registered).toEqual([]);
});
+38 -123
View File
@@ -8,7 +8,7 @@ import type { DeepMutable } from "@opencode/plugin/promise/types";
import type { Skill } from "@opencode/schema/skill";
import type { ModelInfo } from "@opencode/client";
import plugin from "../src/agents.js";
import { settings, gptAgents, workerNames } from "../src/config.js";
import { model } from "../src/model.js";
import { z } from "zod";
import { AgentsRpc } from "../src/agents-rpc.js";
@@ -23,9 +23,17 @@ const models = ["gpt-6-luna", "gpt-6.1-sol", "gpt-6-astra"].map((id) => ({
settings: { reasoningEffort: effort },
})),
}));
const agents = {
"fast-lo": "test/gpt-6-luna#low",
"fast-hi": "test/gpt-6-luna#high",
"good-lo": "test/gpt-6.1-sol#low",
"good-hi": "test/gpt-6.1-sol#high",
"deep-lo": "test/gpt-6-astra#low",
"deep-hi": "test/gpt-6-astra#high",
};
async function host(
catalog: ModelInfo[] = models,
options: Plugin.Context["options"] = { provider: "test" },
options: Plugin.Context["options"] = { agents },
existingSkill?: Skill.Info,
) {
const agents = new Map<string, DeepMutable<Agent.Info>>();
@@ -221,22 +229,6 @@ async function host(
};
}
test("Agents is independently loadable with no quota, storage, MCP, or custom tools", async () => {
expect(plugin.id).toBe("stanp-opencode.agents");
const h = await host();
expect(h.agents.has("coordinate")).toBe(true);
expect(h.agents.has("multi-work")).toBe(false);
expect(h.agents.get("build")?.mode).toBe("primary");
expect(h.skills.get("anti-overengineering")?.content).toContain("correctness");
});
test("preserves an existing user anti-overengineering skill", async () => {
const first = await host();
const original = { ...first.skills.get("anti-overengineering")!, content: "user definition" };
const h = await host(models, { provider: "test" }, original);
expect(h.skills.get("anti-overengineering")).toEqual(original);
});
test("worker availability, deep fallback, and model refresh remain provider aware", async () => {
const h = await host();
expect(String(h.agents.get("coordinate")?.model?.id)).toBe("gpt-6-astra");
@@ -250,67 +242,41 @@ test("worker availability, deep fallback, and model refresh remain provider awar
expect(String(h.agents.get("coordinate")?.model?.id)).toBe("gpt-6-astra");
});
test("custom models use provider-qualified names and optional deep assignments", async () => {
const assignments = Object.fromEntries(
workerNames
.filter((name) => !name.startsWith("deep-"))
.map((name) => [name, { ...gptAgents[name], model: `test/${gptAgents[name].model}` }]),
test("without agents options or with the empty skeleton nothing is registered", async () => {
const skeleton = Object.fromEntries(Object.keys(agents).map((name) => [name, ""]));
for (const options of [{}, { agents: skeleton }]) {
const h = await host(models, options);
expect([...h.agents.keys()]).toEqual(["build"]);
expect(h.skills.size).toBe(0);
}
});
test("assignments are provider-qualified with optional deep pair and variant; partial ones fail", async () => {
const shallow = Object.fromEntries(
Object.entries(agents).filter(([name]) => !name.startsWith("deep-")),
);
const duplicate = models.map((model) => ({ ...model, providerID: Provider.ID.make("other") }));
const h = await host([...duplicate, ...models], { mode: "custom", agents: assignments });
const h = await host([...duplicate, ...models], { agents: shallow });
expect(String(h.agents.get("coordinate")?.model?.providerID)).toBe("test");
expect(String(h.agents.get("coordinate")?.model?.id)).toBe("gpt-6.1-sol");
expect(h.agents.has("deep-hi")).toBe(false);
expect(settings.safeParse({ mode: "gpt", provider: "test", lunaPriority: true }).success).toBe(
false,
expect(model("a/b/c#high", "n")).toEqual({ providerID: "a", id: "b/c", variant: "high" });
expect(model("a/b", "n")).toEqual({ providerID: "a", id: "b" });
expect(() => model("unqualified#low", "agents fast-lo")).toThrow(
"agents fast-lo must be provider/model[#variant]: unqualified#low",
);
expect(
settings.safeParse({
mode: "custom",
agents: { ...assignments, "fast-lo": { model: "unqualified", effort: "low" } },
}).success,
).toBe(false);
});
test("live final build permissions reach workers, coordinate and the native Agents RPC", async () => {
const h = await host();
const rules = [
{ action: "*", resource: "*", effect: "allow" as const },
{ action: "read", resource: "secret", effect: "deny" as const },
];
await h.configure(rules);
expect(await h.permissions()).toEqual(rules);
expect(h.agents.get("fast-lo")?.permissions).toEqual([
...rules,
{ action: "subagent", resource: "*", effect: "deny" },
]);
expect(h.agents.get("coordinate")?.permissions).toContainEqual(rules[1]);
expect(h.agents.get("coordinate")?.permissions.some((rule) => rule.action === "shell")).toBe(
false,
await expect(
host(models, { agents: { ...agents, "fast-lo": "unqualified#low" } }),
).rejects.toThrow("agents fast-lo must be provider/model[#variant]");
const empty = await host(models, { agents: { ...agents, "deep-lo": "", "deep-hi": "" } });
expect(String(empty.agents.get("coordinate")?.model?.id)).toBe("gpt-6.1-sol");
expect(empty.agents.has("deep-lo")).toBe(false);
await expect(host(models, { agents: { ...agents, "good-hi": "" } })).rejects.toThrow(
"agents good-hi is required",
);
await expect(host(models, { agents: { "deep-lo": agents["deep-lo"] } })).rejects.toThrow(
"agents fast-lo is required",
);
});
test("available Codegraph tools inherit permission rules without changing MCP configuration", async () => {
const h = await host();
await h.availableTools(["codegraph_query", "docs_search"]);
await h.configure([
{ action: "*", resource: "*", effect: "allow" },
{ action: "code*", resource: "*", effect: "ask" },
]);
expect(h.agents.get("coordinate")?.permissions).toContainEqual({
action: "codegraph_query",
resource: "*",
effect: "ask",
});
expect(
h.agents.get("coordinate")?.permissions.some((rule) => rule.action === "docs_search"),
).toBe(false);
await h.availableTools([]);
expect(
h.agents
.get("coordinate")
?.permissions.some((rule) => rule.action.startsWith("codegraph_")),
).toBe(false);
});
test("native tool and command paths retain core delegation restrictions", async () => {
@@ -331,54 +297,3 @@ test("native tool and command paths retain core delegation restrictions", async
.parse((await h.request("coordinate")).subagent?.input.properties).agent,
).toEqual({ type: "string", enum: ["fast-lo", "fast-hi", "good-lo", "good-hi"] });
});
test("foreign-location model events do not refresh this plugin instance", async () => {
const h = await host();
await h.models([], { directory: "/elsewhere" });
expect(h.agents.has("coordinate")).toBe(true);
});
test("reasoning uses advertised variants and falls back to request settings", async () => {
const h = await host();
expect(String(h.agents.get("fast-lo")?.model?.variant)).toBe("low");
expect(h.agents.get("fast-lo")?.request?.settings).toEqual({});
const noVariants = models.map((model) => ({ ...model, variants: [] }));
const fallback = await host(noVariants);
expect(fallback.agents.get("fast-lo")?.model?.variant).toBeUndefined();
expect(fallback.agents.get("fast-lo")?.request?.settings).toEqual({ reasoningEffort: "low" });
expect(fallback.agents.get("coordinate")?.request?.settings).toEqual({
reasoningEffort: "low",
});
});
test("Luna priority requires official or advertised support and can be disabled", async () => {
const openai = models.map((model) => ({
...model,
providerID: Provider.ID.make("openai"),
package: "@opencode/ai/providers/openai",
}));
const official = await host(openai, { provider: "openai" });
expect(official.agents.get("fast-lo")?.request?.settings.serviceTier).toBe("priority");
expect(official.agents.get("good-lo")?.request?.settings.serviceTier).toBeUndefined();
const disabled = await host(openai, { provider: "openai", lunaPriority: false });
expect(disabled.agents.get("fast-hi")?.request?.settings.serviceTier).toBeUndefined();
const proxy = openai.map((model) => ({
...model,
settings: { baseURL: "https://proxy.example/v1" },
}));
const unadvertised = await host(proxy, { provider: "openai" });
expect(unadvertised.agents.get("fast-lo")?.request?.settings.serviceTier).toBeUndefined();
const advertised = await host(
proxy.map((model) => ({ ...model, settings: { ...model.settings, serviceTier: "fast" } })),
{ provider: "openai" },
);
expect(advertised.agents.get("fast-lo")?.request?.settings.serviceTier).toBe("fast");
});
test("disabled catalog entries do not authorize worker availability", async () => {
const h = await host(
models.map((model) => ({ ...model, enabled: model.id !== "gpt-6-astra" })),
);
expect(String(h.agents.get("coordinate")?.model?.id)).toBe("gpt-6.1-sol");
expect(h.agents.has("deep-lo")).toBe(false);
});
-21
View File
@@ -1,11 +1,7 @@
import { expect, test } from "bun:test";
import type { Plugin } from "@opencode/plugin";
import type { Context } from "@opencode/plugin/effect/plugin";
import { Effect } from "effect";
import quota from "../src/quota-plugin.js";
import multiWork from "../src/multi-work.js";
import { QuotaRpc } from "../src/quota-rpc.js";
import { AgentsRpc } from "../src/agents-rpc.js";
test("Quota loads independently and retains its RPC ID and nullable status", async () => {
let controller: ReadableStreamDefaultController<unknown>;
@@ -65,20 +61,3 @@ test("Quota loads independently and retains its RPC ID and nullable status", asy
if (cleanup) await cleanup();
expect(disposed).toBe(true);
});
test("Multi-work rejects missing Agents before any Herdr operations or registrations", async () => {
const context = {
rpc: (definition: typeof AgentsRpc) => {
expect(definition.id).toBe("stanp-opencode.agents");
return { permissions: () => Effect.fail(new Error("RPC not registered")) };
},
};
// SAFETY: Missing Agents fails at the first RPC call; no other host service is accessed.
const activate = multiWork.effect as (
context: Pick<Context, "options">,
) => ReturnType<typeof multiWork.effect>;
const result = await Effect.runPromiseExit(
Effect.scoped(activate({ ...context, options: {} })),
);
expect(String(result)).toContain("Multi-work requires Agents loaded before it");
});
+4 -276
View File
@@ -1,7 +1,6 @@
import { expect, test } from "bun:test";
import type { Schema } from "effect";
import { normalizeQuota, QuotaTracker } from "../src/quota.js";
import { formatQuotaWindow, loadQuotaStatus } from "../src/tui.js";
const plus = {
plan_type: "plus",
@@ -24,7 +23,8 @@ const plus = {
},
};
test("normalizes representative Plus and Pro payloads and hides absent windows", () => {
test("normalizes Plus and Pro payloads, hiding absent windows and excluded Spark and reserve limits", () => {
{
expect(normalizeQuota(plus, 123)).toEqual({
plan: "plus",
fetchedAt: 123,
@@ -61,41 +61,8 @@ test("normalizes representative Plus and Pro payloads and hides absent windows",
},
]);
expect(normalizeQuota({ plan_type: "plus", rate_limit: null })).toBeUndefined();
});
test("normalizes additional named limits with stable IDs distinct from equal durations", () => {
const normalized = normalizeQuota({
...plus,
additional_rate_limits: [
}
{
limit_name: "GPT-5.6 Luna",
metered_feature: "codex_luna",
rate_limit: { primary_window: plus.rate_limit.primary_window },
},
],
});
expect(normalized?.windows.map((window) => window.id)).toEqual([
"codex:primary",
"codex:secondary",
"codex_luna:primary",
]);
expect(normalized?.windows[2]?.label).toBe("GPT-5.6 Luna");
expect(
normalizeQuota({
plan_type: "pro",
rate_limit: null,
additional_rate_limits: [
{
limit_name: "GPT-5.6 Luna",
metered_feature: "codex_luna",
rate_limit: { primary_window: plus.rate_limit.primary_window },
},
],
})?.windows.map((window) => window.id),
).toEqual(["codex_luna:primary"]);
});
test("ignores Spark and representative reserve limits while preserving other quotas", () => {
const normalized = normalizeQuota({
...plus,
additional_rate_limits: [
@@ -139,21 +106,7 @@ test("ignores Spark and representative reserve limits while preserving other quo
"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(
@@ -242,72 +195,6 @@ 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("reads cached snapshots and removes excluded windows", 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,
},
],
},
};
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 = result.tracker.status();
expect(status?.windows.map((window) => window.id)).toEqual([
"codex:primary",
"codex_luna:primary",
]);
expect(status?.windows[0]).not.toHaveProperty("sessionConsumed");
expect(result.calls()).toBe(1);
await result.tracker.dispose();
});
test("activates only for OAuth and refreshes quota at initialization and request start", async () => {
const key = fixture({ type: "key", key: "secret" }, [plus]);
await key.tracker.init();
@@ -347,124 +234,6 @@ test("activates only for OAuth and refreshes quota at initialization and request
await oauth.tracker.dispose();
});
test("polls, tolerates errors, and clears on logout", async () => {
const reset = {
...plus,
rate_limit: {
...plus.rate_limit,
primary_window: {
...plus.rate_limit.primary_window,
used_percent: 2,
reset_at: 2_000_100_000,
},
},
};
const result = fixture(
{
type: "oauth",
access: "opaque",
refresh: "refresh",
metadata: { accountID: "account-one" },
},
[plus, new Error("offline"), reset, reset, reset, reset],
10,
);
await result.tracker.init();
await result.tracker.start("root");
await Bun.sleep(35);
const status = result.tracker.status();
expect(result.calls()).toBeGreaterThanOrEqual(3);
expect(status?.windows[0]?.remaining).toBe(98);
result.logout();
await result.tracker.authChanged(true);
expect(result.tracker.status()).toBeUndefined();
const calls = result.calls();
await result.tracker.dispose();
await Bun.sleep(20);
expect(result.calls()).toBe(calls);
});
test("sidebar text places reset timing beside latest remaining quota", () => {
expect(
formatQuotaWindow(
{
seconds: 604_800,
id: "codex:secondary",
label: null,
used: 25,
remaining: 75,
resetsAt: 100_000 + 2 * 86_400_000 + 5 * 3_600_000,
},
100_000,
),
).toBe("7d: 75% · 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,
resetsAt: now + resetIn,
},
now,
);
expect(format(2 * 3_600_000 + 15 * 60_000)).toBe("5h: 75% · 2h 15m");
expect(format(3_600_000)).toBe("5h: 75% · 1h");
expect(format(15 * 60_000)).toBe("5h: 75% · 15m");
expect(format(60_000)).toBe("5h: 75% · 1m");
expect(format(59_999)).toBe("5h: 75% · <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,
};
expect(formatQuotaWindow({ ...window, resetsAt: 999 }, 1_000)).toBe(
"GPT-5.6 Luna 5h: <0.1% · due",
);
expect(formatQuotaWindow({ ...window, resetsAt: Number.NaN }, 1_000)).toBe(
"GPT-5.6 Luna 5h: <0.1% · unavailable",
);
});
test("request completion refreshes once across terminal and idle events", async () => {
const used = (percent: number) => ({
...plus,
rate_limit: {
...plus.rate_limit,
primary_window: { ...plus.rate_limit.primary_window, used_percent: percent },
},
});
const result = fixture(
{
type: "oauth",
access: "opaque",
refresh: "refresh",
metadata: { accountID: "account-one" },
},
[used(10), used(11), used(30), new Error("temporary"), used(30), used(33), used(35)],
);
await result.tracker.init();
await result.tracker.start("root");
await result.tracker.complete("root");
expect(result.tracker.status()?.windows[0]?.remaining).toBe(70);
await result.tracker.complete("root");
expect(result.calls()).toBe(3);
expect(result.tracker.status()?.windows[0]?.remaining).toBe(70);
await result.tracker.dispose();
});
test("an auth switch detaches an in-flight old-account response and immediately fetches the new account", async () => {
const pending = Promise.withResolvers<unknown>();
const second = {
@@ -507,44 +276,3 @@ test("an auth switch detaches an in-flight old-account response and immediately
expect(result.tracker.status()?.windows[0]).not.toHaveProperty("sessionConsumed");
await result.tracker.dispose();
});
test("an auth switch during an old account storage read cannot restore stale state or polling", async () => {
const pending = Promise.withResolvers<Schema.Json | undefined>();
const second = {
...plus,
account_id: "account-two",
rate_limit: {
...plus.rate_limit,
primary_window: { ...plus.rate_limit.primary_window, used_percent: 64 },
},
};
const result = fixture(
{ type: "oauth", access: "old", refresh: "old", metadata: { accountID: "account-one" } },
[second],
1_000_000,
[pending.promise],
);
const old = result.tracker.init();
while (result.storageCalls() === 0) await Bun.sleep(1);
result.switch({
type: "oauth",
access: "new",
refresh: "new",
metadata: { accountID: "account-two" },
});
await result.tracker.authChanged(true);
expect(result.calls()).toBe(1);
pending.resolve(undefined);
await old;
expect(result.tracker.status()?.windows[0]?.used).toBe(64);
expect(result.headers[0]?.get("chatgpt-account-id")).toBe("account-two");
await result.tracker.dispose();
});
test("TUI quota loading converts RPC failures to an empty render state", async () => {
expect(
await loadQuotaStatus(async () => {
throw new Error("disconnected");
}),
).toBeNull();
});
-160
View File
@@ -1,160 +0,0 @@
import { expect, test } from "bun:test";
import { mkdtemp, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { z } from "zod";
const workflow = z
.object({
jobs: z.object({
release: z.object({
steps: z.array(z.object({ name: z.string(), run: z.string().optional() })),
}),
}),
})
.parse(
Bun.YAML.parse(
await Bun.file(new URL("../.gitea/workflows/release.yml", import.meta.url)).text(),
),
);
const script = workflow.jobs.release.steps.find(
(step) => step.name === "Publish Gitea release",
)!.run!;
const base = "/api/v1/repos/test/project/releases";
const tag = "release/v1+rc#1";
const assets = ["stanp-opencode.tgz", "stanp-opencode.tgz.sha256"];
async function publish(status: number) {
const directory = await mkdtemp(join(tmpdir(), "stanp-release-"));
const requests: { method: string; path: string; body: unknown }[] = [];
const server = Bun.serve({
hostname: "127.0.0.1",
port: 0,
async fetch(request) {
const path = new URL(request.url).pathname;
if (request.headers.get("content-type")?.startsWith("multipart/form-data")) {
const attachment = (await request.formData()).get("attachment");
if (!(attachment instanceof File)) {
return new Response("Missing attachment", { status: 400 });
}
requests.push({
method: request.method,
path,
body: { name: attachment.name, content: await attachment.text() },
});
} else if (request.method === "POST" || request.method === "PATCH") {
requests.push({ method: request.method, path, body: await request.json() });
} else {
requests.push({ method: request.method, path, body: null });
}
if (request.method === "POST" && path === base) {
return Response.json(
status === 201
? { id: 42, assets: [] }
: { message: "release creation denied" },
{ status },
);
}
if (request.method === "GET" && path === `${base}/tags/${encodeURIComponent(tag)}`) {
return Response.json({
id: 42,
name: "Keep this title",
body: "Keep these notes",
assets: [
{ id: 10, name: assets[0] },
{ id: 11, name: "stanp-opencode.tgz.sig" },
{ id: 12, name: assets[1] },
],
});
}
if (request.method === "DELETE" && /\/42\/assets\/(10|12)$/.test(path)) {
return new Response(null, { status: 204 });
}
if (request.method === "POST" && path === `${base}/42/assets`) {
return Response.json({ id: 20 }, { status: 201 });
}
if (request.method === "PATCH" && path === `${base}/42`) {
return Response.json({ id: 42, draft: false });
}
return Response.json({ message: "unexpected request" }, { status: 500 });
},
});
try {
await Promise.all(
assets.map((asset) => writeFile(join(directory, asset), `contents of ${asset}`)),
);
const process = Bun.spawn(["bash", "-c", script], {
cwd: directory,
env: {
...Bun.env,
GITEA_SERVER_URL: server.url.origin,
GITEA_REPOSITORY: "test/project",
GITEA_TAG: tag,
GITEA_COMMIT: "abc123",
GITEA_TOKEN: "test-token",
},
stdout: "pipe",
stderr: "pipe",
timeout: 10_000,
});
const [exitCode, stdout, stderr] = await Promise.all([
process.exited,
new Response(process.stdout).text(),
new Response(process.stderr).text(),
]);
return { exitCode, output: stdout + stderr, requests };
} finally {
await server.stop(true);
await rm(directory, { recursive: true, force: true });
}
}
for (const status of [201, 409]) {
test(`publishes both assets after release creation returns ${status}`, async () => {
const result = await publish(status);
expect(result.exitCode).toBe(0);
expect(result.requests.filter(({ method }) => method !== "DELETE")).toEqual([
{
method: "POST",
path: base,
body: { tag_name: tag, name: tag, target_commitish: "abc123", draft: true },
},
...(status === 409
? [{ method: "GET", path: `${base}/tags/${encodeURIComponent(tag)}`, body: null }]
: []),
...assets.map((asset) => ({
method: "POST",
path: `${base}/42/assets`,
body: { name: asset, content: `contents of ${asset}` },
})),
{ method: "PATCH", path: `${base}/42`, body: { draft: false } },
]);
const deletions = result.requests.filter(({ method }) => method === "DELETE");
expect(deletions).toEqual(
status === 409
? [10, 12].map((id) => ({
method: "DELETE",
path: `${base}/42/assets/${id}`,
body: null,
}))
: [],
);
const uploads = result.requests.filter(
({ method, path }) => method === "POST" && path === `${base}/42/assets`,
);
for (const [index, deletion] of deletions.entries()) {
expect(result.requests.indexOf(deletion)).toBeLessThan(
result.requests.indexOf(uploads[index]!),
);
}
}, 15_000);
}
test("non-conflict creation failure stops publishing and shows the response body", async () => {
const result = await publish(403);
expect(result.exitCode).not.toBe(0);
expect(result.output).toContain("release creation denied");
expect(result.requests.map(({ method, path }) => ({ method, path }))).toEqual([
{ method: "POST", path: base },
]);
}, 15_000);
-64
View File
@@ -2,7 +2,6 @@ import { expect, test } from "bun:test";
import type { Plugin } from "@opencode/plugin/tui";
import type { Route, SlotMap } from "@opencode/plugin/tui/context";
import { testRender, type JSX } from "@opentui/solid";
import { jsx } from "@opentui/solid/jsx-runtime";
import { createSignal } from "solid-js";
import { build } from "../scripts/build.js";
import type { QuotaStatus } from "../src/quota.js";
@@ -160,66 +159,3 @@ test("built quota slot stays reactive across pending, empty, failure, update, an
if (cleanup) await cleanup();
}
});
test("home quota stays out of the host footer row at wide and narrow widths", async () => {
await build();
const { default: plugin } = await import(new URL("../dist/tui.js", import.meta.url).href);
let homeRender: HomeSlotRender | undefined;
const contextFixture = {
client: {
rpc: () => ({
status: async () => status("home", 90),
events: { on: () => () => undefined },
}),
},
ui: {
slot(claim: { after?: "prompt.footer"; render: HomeSlotRender }) {
if (claim.after === "prompt.footer") homeRender = claim.render;
return () => undefined;
},
router: { current: () => ({ type: "home" as const }) },
},
theme: { text: { default: "#ffffff", muted: "#888888" } },
};
// SAFETY: This focused fixture implements every context member read by slot setup/rendering.
const cleanup = await plugin.setup(Object.assign({} as Plugin.Context, contextFixture));
if (!homeRender) throw new Error("Plugin did not register the home slot");
const renderHome = homeRender;
try {
for (const width of [75, 40]) {
const footer = (quota: boolean) =>
jsx("box", {
flexDirection: "row",
children: [
jsx("text", { children: "/project" }),
jsx("box", { flexGrow: 1 }),
jsx("text", { children: "NORMAL" }),
jsx("text", { children: " ctrl+k commands" }),
quota ? renderHome({ mode: "normal", showDetails: false }) : undefined,
],
});
const baseline = await testRender(() => footer(false), { width, height: 6 });
const rendered = await testRender(() => footer(true), { width, height: 6 });
try {
await baseline.renderOnce();
await rendered.waitForFrame((frame) => frame.includes("ChatGPT quota (home)"));
const baselineRows = baseline.captureCharFrame().split("\n");
const quotaRows = rendered.captureCharFrame().split("\n");
expect(quotaRows[0]).toBe(baselineRows[0]);
expect(quotaRows[0]).toContain("/project");
expect(quotaRows[0]).toContain("NORMAL ctrl+k commands");
expect(quotaRows[1]?.trim()).toBe("");
expect(quotaRows[2]?.trim()).toBe("ChatGPT quota (home)");
expect(quotaRows[3]?.trim()).toBe("5h: 90% · due");
expect(quotaRows[2]?.indexOf("ChatGPT quota (home)")).toBe(
Math.ceil((width - "ChatGPT quota (home)".length) / 2),
);
} finally {
baseline.renderer.destroy();
rendered.renderer.destroy();
}
}
} finally {
if (cleanup) await cleanup();
}
});