switch to v2
Release / release (push) Successful in 8s

This commit is contained in:
2026-09-09 11:18:03 +02:00
parent 0378edf5f8
commit bb95f9334b
11 changed files with 1450 additions and 577 deletions
+206
View File
@@ -0,0 +1,206 @@
import { afterEach, expect, test } from "bun:test";
import { createHash } from "node:crypto";
import {
chmod,
mkdir,
mkdtemp,
readFile,
readdir,
readlink,
rm,
symlink,
writeFile,
} from "node:fs/promises";
import { tmpdir } from "node:os";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
const installer = fileURLToPath(new URL("../install.sh", import.meta.url));
const directories: string[] = [];
const configContents = '{\n // Keep user settings and comments.\n "theme": "dark"\n}\n';
const previousPlugin = 'export default { id: "previous", setup() { return "preserved"; } };\n';
const previousTarget = "../stanp-opencode/install.previous";
afterEach(async () => {
await Promise.all(
directories.splice(0).map((path) => rm(path, { recursive: true, force: true })),
);
});
async function run(command: string[], cwd: string, env: NodeJS.ProcessEnv) {
const child = Bun.spawn(command, {
cwd,
env,
stdout: "pipe",
stderr: "pipe",
timeout: 20_000,
});
const [exitCode, stdout, stderr] = await Promise.all([
child.exited,
new Response(child.stdout).text(),
new Response(child.stderr).text(),
]);
return { exitCode, stdout, stderr };
}
async function install(mode: "native" | "checksum" | "legacy") {
const directory = await mkdtemp(join(tmpdir(), "stanp-install-"));
directories.push(directory);
const config =
mode === "native"
? join(directory, "xdg", "opencode")
: join(directory, "custom config with spaces");
const bin = join(directory, "bin");
const fixture = join(directory, "package");
await Promise.all([
mkdir(bin),
mkdir(join(fixture, "dist"), { recursive: true }),
mkdir(join(config, "plugins"), { recursive: true }),
mkdir(join(config, "stanp-opencode", "install.previous"), { recursive: true }),
]);
await Promise.all([
writeFile(join(config, "opencode.jsonc"), configContents),
symlink(previousTarget, join(config, "plugins", "stanp-opencode")),
writeFile(join(config, "stanp-opencode", "install.previous", "index.js"), previousPlugin),
writeFile(
join(config, "stanp-opencode", "install.previous", "package.json"),
'{"name":"stanp-opencode","type":"module","main":"./index.js"}\n',
),
writeFile(join(fixture, "index.js"), 'export { default } from "./dist/index.js";\n'),
writeFile(
join(fixture, "package.json"),
JSON.stringify({
name: "stanp-opencode",
version: "1.0.0",
type: "module",
main: "./dist/index.js",
exports: "./dist/index.js",
scripts: { postinstall: "bun ./postinstall.js" },
}),
),
writeFile(
join(fixture, "postinstall.js"),
'await Bun.write(process.env.POSTINSTALL_MARKER, "executed");\n',
),
writeFile(
join(fixture, "dist", "index.js"),
'await Bun.write(process.env.IMPORT_MARKER, "imported");\n' +
(mode === "legacy"
? 'export default { id: "stanp-opencode", async server() { return {}; } };\n'
: 'export default { id: "stanp-opencode", setup() { return "native-ready"; } };\n'),
),
]);
const env = {
...Bun.env,
PATH: `${bin}:${dirname(process.execPath)}:${Bun.env.PATH ?? "/usr/bin:/bin"}`,
HOME: directory,
XDG_CONFIG_HOME: join(directory, "xdg"),
OPENCODE_CONFIG_DIR: mode === "native" ? "" : config,
FIXTURE_DIR: directory,
POSTINSTALL_MARKER: join(directory, "postinstall-ran"),
IMPORT_MARKER: join(directory, "import-ran"),
};
const packed = await run(["tar", "-czf", "stanp-opencode.tgz", "package"], directory, env);
expect(packed, packed.stderr).toMatchObject({ exitCode: 0 });
const digest =
mode === "checksum"
? "0".repeat(64)
: createHash("sha256")
.update(await readFile(join(directory, "stanp-opencode.tgz")))
.digest("hex");
await Promise.all([
writeFile(join(directory, "stanp-opencode.tgz.sha256"), `${digest} stanp-opencode.tgz\n`),
writeFile(
join(directory, "release.json"),
JSON.stringify({
tag_name: "v1.0.0",
assets: ["stanp-opencode.tgz", "stanp-opencode.tgz.sha256"].map((name) => ({
name,
browser_download_url: `https://fixtures.invalid/${name}`,
})),
}),
),
writeFile(
join(bin, "curl"),
`#!/usr/bin/env bash
set -euo pipefail
url=""
output=""
while (($#)); do
case "$1" in
-o|--output) output="$2"; shift 2 ;;
https://*) url="$1"; shift ;;
*) shift ;;
esac
done
case "$url" in
https://git.krtss.de/api/v1/repos/StanPonomarev/stanp-opencode/releases/latest) source=release.json ;;
https://fixtures.invalid/stanp-opencode.tgz) source=stanp-opencode.tgz ;;
https://fixtures.invalid/stanp-opencode.tgz.sha256) source=stanp-opencode.tgz.sha256 ;;
*) printf 'Unexpected fixture URL: %s\\n' "$url" >&2; exit 1 ;;
esac
if [[ -n "$output" ]]; then
cp "$FIXTURE_DIR/$source" "$output"
else
cat "$FIXTURE_DIR/$source"
fi
`,
),
]);
await chmod(join(bin, "curl"), 0o755);
const result = await run(["bash", installer], directory, env);
return { directory, config, env, result };
}
test("installs an importable native plugin under XDG without changing user configuration or running scripts", async () => {
const { directory, config, env, result } = await install("native");
expect(result, result.stderr).toMatchObject({ exitCode: 0 });
// The generated package path is only known at runtime; exercise its module-loading boundary.
const imported = await run(
[
process.execPath,
"-e",
"const { default: plugin } = await import(process.argv[1]); console.log(JSON.stringify([plugin.id, plugin.setup()]));",
join(config, "plugins", "stanp-opencode"),
],
directory,
env,
);
expect(imported, imported.stderr).toMatchObject({
exitCode: 0,
stdout: '["stanp-opencode","native-ready"]\n',
});
expect(await readFile(join(config, "opencode.jsonc"), "utf8")).toBe(configContents);
expect(
await readFile(join(config, "stanp-opencode", "install.previous", "index.js"), "utf8"),
).toBe(previousPlugin);
expect(await Bun.file(env.POSTINSTALL_MARKER).exists()).toBe(false);
}, 60_000);
for (const mode of ["checksum", "legacy"] as const) {
test(`${mode} failure preserves the installed plugin in a custom path containing spaces`, async () => {
const { directory, config, env, result } = await install(mode);
expect(result.exitCode, result.stderr).not.toBe(0);
expect(await readlink(join(config, "plugins", "stanp-opencode"))).toBe(previousTarget);
expect(await readFile(join(config, "opencode.jsonc"), "utf8")).toBe(configContents);
expect(
await readFile(join(config, "stanp-opencode", "install.previous", "index.js"), "utf8"),
).toBe(previousPlugin);
expect(await readdir(join(config, "stanp-opencode"))).toEqual(["install.previous"]);
expect(await readdir(join(config, "plugins"))).toEqual(["stanp-opencode"]);
// Load the preserved package from this test's runtime-selected configuration directory.
const imported = await run(
[
process.execPath,
"-e",
"const { default: plugin } = await import(process.argv[1]); console.log(plugin.setup());",
join(config, "plugins", "stanp-opencode"),
],
directory,
env,
);
expect(imported, imported.stderr).toMatchObject({ exitCode: 0, stdout: "preserved\n" });
expect(await Bun.file(env.POSTINSTALL_MARKER).exists()).toBe(false);
expect(await Bun.file(env.IMPORT_MARKER).exists()).toBe(mode === "legacy");
}, 60_000);
}
+335 -170
View File
@@ -1,186 +1,351 @@
import { afterAll, beforeAll, expect, test } from "bun:test";
import { mkdtemp, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { createOpencodeClient } from "@opencode-ai/sdk";
import type { Config, PluginOptions } from "@opencode-ai/plugin";
import plugin from "../src/index.js";
import { z } from "zod";
import { afterEach, expect, test } from "bun:test";
import { Agent, Model, Plugin, Provider } from "@opencode/plugin";
import type { AgentEditor } from "@opencode/plugin/promise/agent";
import type { DeepMutable } from "@opencode/plugin/promise/types";
import type { Info, ToolContext, ToolEditor } from "@opencode/plugin/promise/tool";
import type { JsonValue, ModelInfo, SessionGetInput } from "@opencode/client";
import type { SessionContext } from "@opencode/plugin/promise/session";
import { gptAgents } from "../src/config.js";
import plugin from "../src/index.js";
let directory: string;
const originalModels = process.env.OPENCODE_MODELS_PATH;
const originalAuth = process.env.OPENCODE_AUTH_CONTENT;
beforeAll(async () => {
directory = await mkdtemp(join(tmpdir(), "stanp-tests-"));
const path = join(directory, "models.json");
await writeFile(
path,
JSON.stringify({
test: {
id: "test",
env: [],
npm: "@ai-sdk/openai-compatible",
models: Object.fromEntries(
["gpt-5.6-luna", "gpt-5.6-sol", "gpt-6-astra"].map((id) => [id, { id }]),
),
},
}),
);
process.env.OPENCODE_MODELS_PATH = path;
process.env.OPENCODE_AUTH_CONTENT = "{}";
const providerID = Provider.ID.make("test");
function model(id: string): ModelInfo {
return Model.Info.default(providerID, Model.ID.make(id));
}
const allModels = ["gpt-5.6-luna", "gpt-5.6-sol", "gpt-6-astra"].map(model);
const cleanups: Plugin.Cleanup[] = [];
afterEach(async () => {
for (const cleanup of cleanups.splice(0)) await cleanup();
});
type ModelContext = Pick<SessionContext, "agent" | "tools">;
type HookEvent = ModelContext | Pick<SessionGetInput, "sessionID">;
type FixtureSetup = (
context: Pick<Plugin.Context, "options">,
) => Plugin.Cleanup | void | Promise<Plugin.Cleanup | void>;
afterAll(async () => {
if (originalModels === undefined) delete process.env.OPENCODE_MODELS_PATH;
else process.env.OPENCODE_MODELS_PATH = originalModels;
if (originalAuth === undefined) delete process.env.OPENCODE_AUTH_CONTENT;
else process.env.OPENCODE_AUTH_CONTENT = originalAuth;
await rm(directory, { recursive: true, force: true });
});
async function hooks(caller = "build", options: PluginOptions = { provider: "test" }) {
return plugin.server(
{
client: createOpencodeClient({
baseUrl: "http://localhost:1",
fetch: async (request) =>
Response.json(
request.url.endsWith("/user")
? { info: { role: "user", agent: caller }, parts: [] }
: [
{
info: {
role: "assistant",
mode: "general",
parentID: "user",
},
parts: [],
},
],
),
}),
project: { id: "test", worktree: directory, time: { created: 0 } },
directory,
worktree: directory,
serverUrl: new URL("http://localhost:1"),
experimental_workspace: { register() {} },
$: Bun.$,
async function host(
models: ModelInfo[] = allModels,
options: Plugin.Context["options"] = { provider: "test" },
) {
const agents = new Map<string, DeepMutable<Agent.Info>>();
const build: DeepMutable<Agent.Info> = Agent.Info.default(Agent.ID.make("build"));
build.permissions.push({ action: "shell", resource: "git push *", effect: "ask" });
agents.set("build", build);
const originalBuild = structuredClone(build);
const sessionHooks: Record<string, (event: HookEvent) => Promise<void> | void> = {};
let before!: (event: { tool: string; agent: string; input: unknown }) => Promise<void> | void;
let parent = "build";
let child = "fast-lo";
let defaultAgent = "build";
const registration = { async dispose() {} };
let transform!: (editor: AgentEditor) => void;
let reloaded = Promise.withResolvers<void>();
let events!: ReadableStreamDefaultController<{ type: string }>;
const stream = new ReadableStream<{ type: string }>({
start(controller) {
events = controller;
},
});
const storage = new Map<string, JsonValue>();
const tools = new Map<string, Info>();
// SAFETY: This test host implements the native APIs used by setup; unrelated context
// services are deliberately absent so unexpected new dependencies fail the test.
const context = {
options,
);
catalog: {
model: {
async list() {
return { data: models };
},
},
},
event: {
subscribe({ signal }: { signal: AbortSignal }) {
signal.addEventListener("abort", () => events.close(), { once: true });
return stream;
},
},
storage: {
async get(key: string) {
return storage.get(key);
},
async set(key: string, value: JsonValue) {
storage.set(key, value);
},
},
agent: {
async list() {
return { data: [...agents.values()] };
},
async get({ agentID }: { agentID: string }) {
return { data: agents.get(agentID) };
},
async transform(callback: (editor: AgentEditor) => void) {
transform = callback;
callback({
get: (id) => agents.get(id),
list: () => [...agents.values()],
default(id) {
defaultAgent = id ?? "build";
},
remove(id) {
agents.delete(id);
},
update(id, update) {
const agent = agents.get(id) ?? Agent.Info.default(Agent.ID.make(id));
update(agent);
agents.set(id, agent);
},
});
return registration;
},
async reload() {
agents.clear();
agents.set("build", build);
await this.transform(transform);
reloaded.resolve();
},
},
session: {
async hook(name: string, callback: (event: HookEvent) => Promise<void> | void) {
sessionHooks[name] = callback;
return registration;
},
async get({ sessionID }: { sessionID: string }) {
return sessionID === "child"
? { agent: child, parentID: "parent" }
: { agent: parent };
},
},
tool: {
async transform(callback: (editor: ToolEditor) => void) {
callback({
get(id) {
const tool = tools.get(id);
return tool ? { ...tool, id } : undefined;
},
list: () => [...tools].map(([id, tool]) => ({ ...tool, id })),
namespace() {},
add(tool) {
tools.set(tool.name, tool);
},
update(id, update) {
const tool = tools.get(id);
if (tool) update(tool);
},
remove(id) {
tools.delete(id);
},
});
return registration;
},
async hook(_name: string, callback: typeof before) {
before = callback;
return registration;
},
},
};
// SAFETY: Exercise the real setup with the native operations implemented by this
// partial host; absent services intentionally fail instead of becoming inert mocks.
const setup = plugin.setup as FixtureSetup;
const cleanup = await setup(context);
if (cleanup) cleanups.push(cleanup);
return {
agents,
build,
defaultAgent,
originalBuild,
async refresh(next: ModelInfo[]) {
models = next;
reloaded = Promise.withResolvers<void>();
events.enqueue({ type: "catalog.updated" });
await reloaded.promise;
},
async changeBuildPermissions(permissions: Agent.Info["permissions"]) {
build.permissions = [...permissions];
reloaded = Promise.withResolvers<void>();
events.enqueue({ type: "agent.updated" });
await reloaded.promise;
},
async todo(name: string, sessionID: string, input: JsonValue = {}) {
// SAFETY: Todo executors use only sessionID and agent from the native context.
return tools
.get(name)!
.execute(input, { sessionID, agent: Agent.ID.make("build") } as ToolContext);
},
async delegate(caller: string, target: string) {
await before({ tool: "subagent", agent: caller, input: { agent: target } });
},
async command(caller: string, target: string) {
parent = caller;
child = target;
await sessionHooks.prompt!({ sessionID: "child" });
},
async tools(caller: string) {
const event: ModelContext = {
agent: Agent.ID.make(caller),
tools: {
subagent: {
description: "Native subagents",
input: {
type: "object",
properties: { agent: { type: "string" }, prompt: { type: "string" } },
},
},
},
};
await sessionHooks.context!(event);
return event.tools;
},
};
}
test("registration is atomic; only missing Astra degrades to four workers", async () => {
const full: Config = { provider: { test: {} } };
await (await hooks()).config(full);
// The host's /agent endpoint rejects undefined options rather than JSON-stringifying them.
z.json().parse(full.agent);
expect(Object.keys(full.agent ?? {})).toEqual([
"fast-lo",
"fast-hi",
"good-lo",
"good-hi",
"deep-lo",
"deep-hi",
"coordinate",
]);
const reduced: Config = { provider: { test: { blacklist: ["gpt-6-astra"] } } };
await (await hooks()).config(reduced);
expect(Object.keys(reduced.agent ?? {})).toEqual([
"fast-lo",
"fast-hi",
"good-lo",
"good-hi",
"coordinate",
]);
expect(reduced.agent?.coordinate?.model).toBe("test/gpt-5.6-sol");
for (const missing of ["gpt-5.6-luna", "gpt-5.6-sol"]) {
const config: Config = { provider: { test: { blacklist: [missing] } } };
await (await hooks()).config(config);
expect(config.agent).toBeUndefined();
}
});
test("disconnected and explicitly disabled providers register nothing", async () => {
const disconnected: Config = {};
await (await hooks()).config(disconnected);
expect(disconnected.agent).toBeUndefined();
const disabled: Config = { provider: { test: {} }, disabled_providers: ["test"] };
await (await hooks()).config(disabled);
expect(disabled.agent).toBeUndefined();
});
test("manual task targets cannot bypass coordinate's primary-only restriction", async () => {
const active = await hooks();
await active.config({ provider: { test: {} } });
await expect(
active["tool.execute.before"](
{ tool: "task", sessionID: "test", callID: "test" },
{ args: { subagent_type: "coordinate", prompt: "implement" } },
),
).rejects.toThrow("cannot be delegated to");
});
test("subtask bypass uses the parent user agent, not the synthetic assistant target", async () => {
const coordinate = await hooks("coordinate");
await coordinate.config({ provider: { test: { blacklist: ["gpt-6-astra"] } } });
for (const target of ["general", "deep-lo"]) {
await expect(
coordinate["tool.execute.before"](
{ tool: "task", sessionID: "test", callID: "test" },
{ args: { subagent_type: target } },
),
).rejects.toThrow("only to its available workers");
}
const worker = await hooks("fast-hi");
await worker.config({ provider: { test: {} } });
await expect(
worker["tool.execute.before"](
{ tool: "task", sessionID: "test", callID: "test" },
{ args: { subagent_type: "good-lo" } },
),
).rejects.toThrow("Workers cannot delegate");
});
test("custom assignments do not silently take the GPT fallback", async () => {
const custom = await hooks("build", {
test("missing required models disables registration; only GPT mode degrades without Astra", async () => {
const full = await host();
expect(full.agents.has("deep-hi")).toBe(true);
expect(String(full.agents.get("coordinate")?.model?.id)).toBe("gpt-6-astra");
const reduced = await host(allModels.slice(0, 2));
expect(reduced.agents.has("deep-hi")).toBe(false);
expect(String(reduced.agents.get("coordinate")?.model?.id)).toBe("gpt-5.6-sol");
expect(reduced.agents.get("coordinate")?.system).toContain("Deep agents are unavailable");
expect((await host([])).agents.has("coordinate")).toBe(false);
expect((await host(allModels.slice(1))).agents.has("coordinate")).toBe(false);
const custom = await host([...allModels, model("local")], {
mode: "custom",
provider: "test",
agents: { ...gptAgents, coordinate: { model: "local", effort: "low" } },
});
const config: Config = { provider: { test: { models: { local: { name: "Local" } } } } };
await custom.config(config);
expect(config.agent?.coordinate?.model).toBe("test/local");
expect(config.agent?.coordinate?.options).toEqual({ reasoningEffort: "low" });
const missing: Config = {
provider: { test: { blacklist: ["gpt-6-astra"], models: { local: {} } } },
};
await custom.config(missing);
expect(missing.agent).toBeUndefined();
expect(String(custom.agents.get("coordinate")?.model?.id)).toBe("local");
expect(
(
await host(allModels.slice(0, 2), {
mode: "custom",
provider: "test",
agents: gptAgents,
})
).agents.has("coordinate"),
).toBe(false);
});
test("tier selection requires evidence and preserves the advertised fast tier", async () => {
const namedFast = { name: "Luna", variants: { fast: { reasoningEffort: "low" } } };
const ordinary: Config = { provider: { test: { models: { "gpt-5.6-luna": namedFast } } } };
await (await hooks()).config(ordinary);
expect(ordinary.agent?.["fast-lo"]?.options).toEqual({ reasoningEffort: "low" });
const advertised: Config = {
provider: {
test: {
models: { "gpt-5.6-luna": { options: { serviceTier: "fast" } } },
},
},
};
await (await hooks()).config(advertised);
expect(advertised.agent?.["fast-lo"]?.options).toEqual({
reasoningEffort: "low",
serviceTier: "fast",
test("worker inheritance and coordinator isolation preserve build and default", async () => {
const result = await host();
expect(result.defaultAgent).toBe("build");
expect(result.agents.get("build")).toEqual(result.originalBuild);
const worker = result.agents.get("fast-lo")!;
expect(worker.permissions).toContainEqual({
action: "shell",
resource: "git push *",
effect: "ask",
});
expect(worker.permissions.at(-1)).toEqual({
action: "subagent",
resource: "*",
effect: "deny",
});
const coordinate = result.agents.get("coordinate")!;
expect(coordinate.permissions[0]).toEqual({ action: "*", resource: "*", effect: "deny" });
expect(coordinate.permissions.some((rule) => rule.action === "shell")).toBe(false);
expect(coordinate.permissions).toContainEqual({
action: "read",
resource: "*.env",
effect: "ask",
});
expect(coordinate.permissions).toContainEqual({
action: "subagent",
resource: "fast-lo",
effect: "allow",
});
});
test("manual calls and command-created child sessions cannot bypass delegation restrictions", async () => {
const result = await host(allModels.slice(0, 2));
for (const invoke of [result.delegate, result.command]) {
await expect(invoke("build", "coordinate")).rejects.toThrow("cannot run as a subagent");
await expect(invoke("fast-hi", "good-lo")).rejects.toThrow("Workers cannot delegate");
await expect(invoke("coordinate", "deep-lo")).rejects.toThrow(
"only to its available workers",
);
await invoke("coordinate", "good-hi");
}
const tools = await result.tools("coordinate");
expect(tools.subagent?.input.properties).toEqual({
agent: { type: "string", enum: ["fast-lo", "fast-hi", "good-lo", "good-hi"] },
prompt: { type: "string" },
});
expect(await result.tools("fast-lo")).toEqual({});
expect((await result.tools("build")).subagent?.description).toBe("Native subagents");
});
test("reasoning settings and Luna priority use native advertised or official support", async () => {
const luna = {
...model("gpt-5.6-luna"),
package: "@opencode/ai/providers/openai",
settings: { baseURL: "https://api.openai.com/v1" },
};
const ordinary = await host();
expect(ordinary.agents.get("fast-lo")?.request.settings).toEqual({ reasoningEffort: "low" });
const official = await host([luna, ...allModels.slice(1)]);
expect(official.agents.get("fast-hi")?.request.settings).toEqual({
reasoningEffort: "high",
serviceTier: "priority",
});
const proxy = await host([
{ ...luna, settings: { baseURL: "https://proxy.example/v1" } },
...allModels.slice(1),
]);
expect(proxy.agents.get("fast-lo")?.request.settings).toEqual({ reasoningEffort: "low" });
const advertised = await host([
{ ...luna, package: "custom", settings: { serviceTier: "fast" } },
...allModels.slice(1),
]);
expect(advertised.agents.get("fast-lo")?.request.settings.serviceTier).toBe("fast");
const disabled = await host([luna, ...allModels.slice(1)], {
provider: "test",
lunaPriority: false,
});
expect(disabled.agents.get("fast-lo")?.request.settings).toEqual({ reasoningEffort: "low" });
});
test("catalog updates activate late models and remove unavailable agents", async () => {
const result = await host([]);
await result.refresh(allModels);
expect(String(result.agents.get("coordinate")?.model?.id)).toBe("gpt-6-astra");
await result.refresh(allModels.slice(0, 2));
expect(result.agents.has("deep-lo")).toBe(false);
expect(String(result.agents.get("coordinate")?.model?.id)).toBe("gpt-5.6-sol");
await result.refresh([]);
expect(result.agents.has("coordinate")).toBe(false);
});
test("late build permission overrides remain effective for workers and coordinator", async () => {
const result = await host();
await result.changeBuildPermissions([
...result.build.permissions,
{ action: "read", resource: "*.env", effect: "deny" },
{ action: "shell", resource: "git push *", effect: "deny" },
]);
expect(result.agents.get("fast-lo")?.permissions).toContainEqual({
action: "shell",
resource: "git push *",
effect: "deny",
});
expect(
result.agents
.get("coordinate")
?.permissions.filter((rule) => rule.action === "read" && rule.resource === "*.env")
.at(-1)?.effect,
).toBe("deny");
});
test("todo lists persist within one session without leaking to another", async () => {
const result = await host();
const todos = [{ id: "1", content: "Implement", status: "in_progress", priority: "high" }];
await result.todo("todowrite", "one", { todos });
expect((await result.todo("todoread", "one")).content).toBe(JSON.stringify(todos));
expect((await result.todo("todoread", "two")).content).toBe("[]");
result.build.permissions.push({ action: "todowrite", resource: "*", effect: "ask" });
await expect(result.todo("todowrite", "one", { todos: [] })).rejects.toThrow("cannot request");
expect((await result.todo("todoread", "one")).content).toBe(JSON.stringify(todos));
result.build.permissions.push({ action: "todoread", resource: "*", effect: "deny" });
await expect(result.todo("todoread", "one")).rejects.toThrow("(deny)");
});