stricter checks
CI / verify (pull_request) Successful in 12s

This commit is contained in:
2026-10-10 15:16:53 +02:00
parent ced43fbb11
commit 513d51f702
30 changed files with 984 additions and 470 deletions
+10 -5
View File
@@ -97,7 +97,10 @@ test("reads credentials file and sends OAuth usage request", async () => {
changed++;
},
async (input, init) => {
calls.push({ url: String(input), headers: new Headers(init?.headers) });
calls.push({
url: input instanceof Request ? input.url : String(input),
headers: new Headers(init?.headers),
});
return Response.json(payload);
},
60_000,
@@ -105,10 +108,12 @@ test("reads credentials file and sends OAuth usage request", async () => {
);
await tracker.init();
expect(calls).toHaveLength(1);
expect(calls[0]!.url).toBe("https://api.anthropic.com/api/oauth/usage");
expect(calls[0]!.headers.get("authorization")).toBe("Bearer token-123");
expect(calls[0]!.headers.get("anthropic-beta")).toBe("oauth-2025-04-20");
expect(calls[0]!.headers.get("accept")).toBe("application/json");
const [call] = calls;
if (!call) throw new Error("Missing usage request");
expect(call.url).toBe("https://api.anthropic.com/api/oauth/usage");
expect(call.headers.get("authorization")).toBe("Bearer token-123");
expect(call.headers.get("anthropic-beta")).toBe("oauth-2025-04-20");
expect(call.headers.get("accept")).toBe("application/json");
expect(tracker.status()?.plan).toBe("max");
expect(tracker.status()?.windows[0]?.remaining).toBe(87.5);
expect(changed).toBe(1);
+5 -5
View File
@@ -10,13 +10,13 @@ export default Plugin.define({
const providerID = Provider.ID.make("fixture");
yield* context.provider.transform((editor) =>
editor.add({
info: {
...Provider.Info.empty(providerID),
// Copy own fields like the former spread; the editor accepts plain info.
info: Object.assign({}, Provider.Info.empty(providerID), {
name: "Isolated fixture",
activation: "enabled",
activation: "enabled" as const,
package: "@opencode/ai/providers/openai-compatible",
settings: { baseURL: process.env.FIXTURE_MODEL_URL! },
},
settings: { baseURL: process.env["FIXTURE_MODEL_URL"] ?? "" },
}),
models: ["gpt-6-luna", "gpt-6.1-sol", "gpt-6-astra"].map((id) =>
Model.Info.default(providerID, Model.ID.make(id)),
),
+2 -2
View File
@@ -37,7 +37,7 @@ export default Plugin.define({
const child = yield* context.session.get({ sessionID: event.sessionID });
yield* Effect.promise(() =>
appendFile(
process.env.FIXTURE_TRACE!,
process.env["FIXTURE_TRACE"] ?? "",
JSON.stringify({
id: child.id,
parent: child.parentID,
@@ -59,7 +59,7 @@ export default Plugin.define({
description: "native host proof",
prompt: "Reply with FIXTURE_COMPLETE",
background: input.background,
...(input.child
...(input.child !== undefined && input.child !== ""
? { sessionID: input.child }
: {
worktree: {
+8 -7
View File
@@ -45,9 +45,9 @@ async function fixture() {
let childWorkspaceIDOverride: string | undefined;
let childPathOverride: string | undefined;
let repoOverride: string | undefined;
const run = async (args: string[], cwd: string, signal: AbortSignal) => {
const run = async (args: string[], cwd: string, abort: AbortSignal) => {
calls.push(args);
if (args[0] !== "herdr") return command(args, cwd, signal);
if (args[0] !== "herdr") return command(args, cwd, abort);
if (args[1] === "workspace")
return JSON.stringify({
result: {
@@ -57,7 +57,7 @@ async function fixture() {
},
});
if (args[2] === "list") {
const id = args[4]!;
const id = args[4] ?? "";
const path = id === "w2" ? destination : root;
const sourceWorkspaceID = sourceWorkspaceIDOverride ?? (id === "w2" ? "w1" : id);
const sourcePath = sourcePathOverride ?? root;
@@ -93,10 +93,10 @@ async function fixture() {
expect(args[2]).toBe("create");
expect(args.slice(3, 5)).toEqual(["--cwd", root]);
expect(args.at(-1)).toBe("--no-focus");
branch = args[args.indexOf("--branch") + 1]!;
branch = args[args.indexOf("--branch") + 1] ?? "";
const labelIndex = args.indexOf("--label");
label = labelIndex === -1 ? undefined : args[labelIndex + 1];
const base = args[args.indexOf("--base") + 1]!;
const base = args[args.indexOf("--base") + 1] ?? "";
// Real Git worktree behavior, but only in an isolated temporary repository.
await command(["git", "worktree", "add", "-b", branch, destination, base], root, signal);
return JSON.stringify({
@@ -144,14 +144,15 @@ test("create verifies a linked workspace whose inventory source remains the pare
const f = await fixture();
const root = await f.herdr.root(f.root, f.signal);
expect(root?.workspaceID).toBe("w1");
const destination = await f.herdr.create(root!, "other", "multi-work/fixture", f.signal);
if (!root) throw new Error("Missing Herdr root");
const destination = await f.herdr.create(root, "other", "multi-work/fixture", f.signal);
expect(destination.directory).toBe(f.destination);
expect(destination.branch).toStartWith("multi-work/");
expect(await command(["git", "branch", "--show-current"], f.root, f.signal)).toBe("main");
await f.herdr.verify(
destination.directory,
destination.workspaceID,
root!,
root,
destination.branch,
f.signal,
);
+51 -27
View File
@@ -3,6 +3,7 @@ import { chmod, mkdir, mkdtemp, readFile, readdir, rm, writeFile } from "node:fs
import { tmpdir } from "node:os";
import { dirname, join } from "node:path";
import { createHash } from "node:crypto";
import { z } from "zod";
import { install } from "../scripts/install.js";
const directories: string[] = [];
@@ -34,6 +35,15 @@ const empty = {
"deep-hi": "",
},
};
const pluginEntry = z
.object({ package: z.string(), options: z.record(z.string(), z.json()).optional() })
.loose();
const configFile = z
.object({ plugins: z.array(z.json()), experimental: z.json().optional() })
.loose();
const manifestFile = z.object({ exports: z.record(z.string(), z.json()) }).loose();
const entries = (plugins: z.infer<typeof configFile>["plugins"]) =>
z.array(pluginEntry).parse(plugins);
const helper = new URL("../scripts/install.ts", import.meta.url);
async function run(
command: string[],
@@ -50,8 +60,8 @@ async function run(
timeout: 30_000,
});
if (input !== undefined && process.stdin) {
process.stdin.write(input);
process.stdin.end();
void process.stdin.write(input);
void process.stdin.end();
}
const [exitCode, stdout, stderr] = await Promise.all([
process.exited,
@@ -83,7 +93,9 @@ async function fixture() {
format: "esm",
});
expect(built.success).toBe(true);
await Bun.write(join(source, "dist", "install.js"), built.outputs[0]!);
const [output] = built.outputs;
if (output === undefined) throw new Error("Missing installer build output");
await Bun.write(join(source, "dist", "install.js"), output);
await writeFile(join(source, "package.json"), '{"type":"module"}');
await writeFile(
join(config, "opencode.json"),
@@ -96,7 +108,7 @@ async function fixture() {
return { directory, source, config };
}
async function contents(config: string) {
return JSON.parse(await readFile(join(config, "opencode.json"), "utf8"));
return configFile.parse(JSON.parse(await readFile(join(config, "opencode.json"), "utf8")));
}
for (const selection of ["quota", "agents,multi-work,quota"]) {
@@ -106,14 +118,16 @@ for (const selection of ["quota", "agents,multi-work,quota"]) {
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([
expect(entries(result.plugins).map((entry) => entry.package)).toEqual([
"before",
...selection.split(",").map((name) => `./plugins/stanp-opencode/${name}`),
]);
const target = join(f.config, "plugins", "stanp-opencode");
expect((await readdir(target)).sort()).toEqual(selection.split(",").sort());
expect((await readdir(target)).toSorted()).toEqual(selection.split(",").toSorted());
for (const name of selection.split(",")) {
const manifest = await Bun.file(join(target, name, "package.json")).json();
const manifest = manifestFile.parse(
await Bun.file(join(target, name, "package.json")).json(),
);
expect(manifest.exports["./tui"] !== undefined).toBe(name === "quota");
expect(await Bun.file(join(target, name, "tui.js")).exists()).toBe(name === "quota");
}
@@ -127,18 +141,23 @@ 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 = {
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));
const [before, agents, quota] = entries(current.plugins);
if (!before || !agents || !quota) throw new Error("Missing installed plugins");
const after = { package: "after", options: { untouched: 1 } };
const plugins = [
before,
{ ...agents, options: { agents: { ...empty.agents, "fast-lo": "openai/gpt-6-luna#low" } } },
quota,
after,
];
await writeFile(join(f.config, "opencode.json"), JSON.stringify({ ...current, plugins }));
await install(f.source, f.config);
expect((await contents(f.config)).plugins).toEqual(current.plugins);
expect(entries((await contents(f.config)).plugins)).toEqual(plugins);
await install(f.source, f.config, "quota");
expect((await contents(f.config)).plugins).toEqual([
current.plugins[0],
expect(entries((await contents(f.config)).plugins)).toEqual([
before,
{ package: "./plugins/stanp-opencode/quota" },
current.plugins[3],
after,
]);
});
@@ -152,14 +171,17 @@ test("installer writes empty option skeletons and subagent_depth, fills only mis
{ package: "./plugins/stanp-opencode/quota" },
]);
expect(fresh.experimental).toEqual({ subagent_depth: 2 });
fresh.plugins[1].options = {
agents: { "fast-lo": "openai/gpt-6-luna#low" },
extra: 1,
};
await writeFile(join(f.config, "opencode.json"), JSON.stringify(fresh));
const [before, agents, ...rest] = entries(fresh.plugins);
if (!before || !agents) throw new Error("Missing installed plugins");
const plugins = [
before,
{ ...agents, options: { agents: { "fast-lo": "openai/gpt-6-luna#low" }, extra: 1 } },
...rest,
];
await writeFile(join(f.config, "opencode.json"), JSON.stringify({ ...fresh, plugins }));
await install(f.source, f.config);
const filled = await contents(f.config);
expect(filled.plugins[1].options).toEqual({
expect(entries(filled.plugins)[1]?.options).toEqual({
agents: { ...empty.agents, "fast-lo": "openai/gpt-6-luna#low" },
extra: 1,
});
@@ -169,7 +191,7 @@ test("installer writes empty option skeletons and subagent_depth, fills only mis
for (const [depth, expected] of [
[1, 2],
[3, 3],
]) {
] as const) {
await writeFile(
join(f.config, "opencode.json"),
JSON.stringify({
@@ -214,12 +236,14 @@ test("replaces the legacy monolith without duplicates and retains Agents options
await writeFile(join(f.config, "plugins", "stanp-opencode.js"), "legacy");
await install(f.source, f.config);
const result = await contents(f.config);
expect(result.plugins.map((entry: { package: string }) => entry.package)).toEqual([
expect(entries(result.plugins).map((entry) => entry.package)).toEqual([
"before",
...names.map((name) => `./plugins/stanp-opencode/${name}`),
"after",
]);
expect(result.plugins[1].options.agents["fast-lo"]).toBe("openai/gpt-6-luna#low");
expect(result.plugins[1]).toMatchObject({
options: { agents: { "fast-lo": "openai/gpt-6-luna#low" } },
});
expect(await Bun.file(join(f.config, "plugins", "stanp-opencode.js")).exists()).toBe(false);
expect(result).toMatchObject(unrelated);
});
@@ -283,7 +307,7 @@ test("local shell installer builds its checkout and upgrades mixed entries witho
await chmod(join(bin, "bun"), 0o755);
const env = {
...Bun.env,
PATH: `${bin}:${dirname(process.execPath)}:${Bun.env.PATH}`,
PATH: `${bin}:${dirname(process.execPath)}:${Bun.env["PATH"]}`,
REAL_BUN: process.execPath,
CALLS: join(f.directory, "calls"),
OPENCODE_CONFIG_DIR: f.config,
@@ -357,7 +381,7 @@ for (const validChecksum of [true, false])
f.directory,
{
...Bun.env,
PATH: `${bin}:${dirname(process.execPath)}:${Bun.env.PATH}`,
PATH: `${bin}:${dirname(process.execPath)}:${Bun.env["PATH"]}`,
ARCHIVE: archive,
CHECKSUM: checksum,
RELEASE: release,
+29 -20
View File
@@ -70,6 +70,19 @@ interface FixtureSession {
permissions: Agent.Info["permissions"];
}
const defaultInput = {
agent: "coordinate",
description: "test",
prompt: "work",
worktree: { base: "main" },
};
function required<T>(map: Map<string, T>, key: string) {
const value = map.get(key);
if (value === undefined) throw new Error(`Missing fixture entry ${key}`);
return value;
}
function fixture() {
const herdr = new FixtureHerdr();
const storage = new Map<string, Parameters<Context["storage"]["set"]>[1]>();
@@ -112,7 +125,7 @@ function fixture() {
})
.parse(raw);
const id = Session.ID.make(input.sessionID ?? `ses_child${++serial}`);
if (!input.sessionID)
if (input.sessionID === undefined || input.sessionID === "")
sessions.set(id, {
...parent,
id,
@@ -122,7 +135,7 @@ function fixture() {
});
order.push("create");
yield* call.progress({ sessionID: id, status: "running" });
order.push(`prompt:${sessions.get(id)!.location.directory}`);
order.push(`prompt:${required(sessions, id).location.directory}`);
prompts++;
entered.resolve();
if (hold)
@@ -135,7 +148,10 @@ function fixture() {
);
return {
content: "native result",
metadata: { sessionID: id, status: input.background ? "running" : "completed" },
metadata: {
sessionID: id,
status: input.background === true ? "running" : "completed",
},
};
}),
};
@@ -144,7 +160,7 @@ function fixture() {
[];
const hooks: ((event: SessionContext) => Effect.Effect<void>)[] = [];
let prompt!: (event: Pick<SessionContext, "sessionID">) => Effect.Effect<void>;
let transform!: (editor: AgentEditor) => void;
let transform: ((editor: AgentEditor) => void) | undefined;
const registration = Effect.succeed({ dispose: Effect.void });
const editAgents = (): AgentEditor => ({
get: (id) => agents.get(id),
@@ -185,16 +201,16 @@ function fixture() {
}),
},
agent: {
transform: (callback: typeof transform) =>
transform: (callback: (editor: AgentEditor) => void) =>
Effect.sync(() => {
transform = callback;
callback(editAgents());
}).pipe(Effect.andThen(registration)),
reload: () =>
Effect.sync(() => {
if (transform) transform(editAgents());
transform?.(editAgents());
}),
get: () => Effect.succeed({ data: agents.get("multi-work")! }),
get: () => Effect.succeed({ data: required(agents, "multi-work") }),
list: () =>
Effect.sync(() => {
agentReads++;
@@ -204,14 +220,15 @@ function fixture() {
},
session: {
get: ({ sessionID }: { sessionID: string }) =>
Effect.sync(() => sessions.get(sessionID)!),
Effect.sync(() => required(sessions, sessionID)),
move: ({ sessionID, directory }: { sessionID: string; directory: string }) =>
Effect.sync(() => {
order.push("move-request");
if (!delayMove) {
sessions.get(sessionID)!.location.directory = directory;
required(sessions, sessionID).location.directory = directory;
order.push("moved");
if (crossProject) sessions.get(sessionID)!.projectID = "another-project";
if (crossProject)
required(sessions, sessionID).projectID = "another-project";
}
entered.resolve();
}),
@@ -272,15 +289,7 @@ function fixture() {
Agent.Info.default(Agent.ID.make("build")).permissions,
herdr,
),
invoke: (
input = {
agent: "coordinate",
description: "test",
prompt: "work",
worktree: { base: "main" },
},
caller = call,
) => tool.execute(input, caller),
invoke: (input = defaultInput, caller = call) => tool.execute(input, caller),
execute: (input: z.infer<typeof request>, caller = call) => tool.execute(input, caller),
hold() {
hold = true;
@@ -381,7 +390,7 @@ test("root-local resume rejects location, binding, and live-root changes", async
prompt: "work",
});
if (tamper === "session")
f.sessions.get("ses_child1")!.location.directory = "/other";
required(f.sessions, "ses_child1").location.directory = "/other";
else if (tamper === "live-root") f.herdr.rootWorkspaceID = "foreign";
else {
const binding = bindingSchema.parse(
+13 -4
View File
@@ -3,6 +3,7 @@ import { mkdir, mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { Host } from "@opencode/plugin/host";
import { z } from "zod";
import { build } from "../scripts/build.js";
import { install } from "../scripts/install.js";
@@ -23,13 +24,19 @@ test("packed package has independent native plugins and quota-only TUI discovery
});
expect(await unpack.exited).toBe(0);
const source = join(directory, "package");
const manifest = await Bun.file(join(source, "package.json")).json();
const manifest = z
.object({ exports: z.record(z.string(), z.object({ import: z.string() }).loose()) })
.loose()
.parse(await Bun.file(join(source, "package.json")).json());
const entrypoint = (name: string) =>
join(source, z.string().parse(manifest.exports[name]?.import));
expect(manifest.exports["."]).toBeUndefined();
expect(await Bun.file(join(source, "index.js")).exists()).toBe(false);
expect(await Bun.file(join(source, "dist", "index.js")).exists()).toBe(false);
for (const name of ["agents", "multi-work", "quota"]) {
const entrypoint = join(source, manifest.exports[`./${name}`].import);
const { default: plugin } = await import(entrypoint);
const { default: plugin } = z
.object({ default: z.object({ id: z.string() }).loose() })
.parse(await import(entrypoint(`./${name}`)));
expect(plugin.id).toBe(`stanp-opencode.${name}`);
}
const config = join(directory, "config");
@@ -42,7 +49,9 @@ test("packed package has independent native plugins and quota-only TUI discovery
expect(entries.server).toBeDefined();
expect(entries.tui !== undefined).toBe(name === "quota");
}
const { QuotaRpc } = await import(join(source, manifest.exports["./quota-rpc"].import));
const { QuotaRpc } = z
.object({ QuotaRpc: z.object({ id: z.string() }).loose() })
.parse(await import(entrypoint("./quota-rpc")));
expect(QuotaRpc.id).toBe("stanp-opencode.quota");
} finally {
await rm(directory, { recursive: true, force: true });
+24 -22
View File
@@ -31,24 +31,29 @@ const agents = {
"deep-lo": "test/gpt-6-astra#low",
"deep-hi": "test/gpt-6-astra#high",
};
const defaultOptions = { agents };
const defaultLocation = { directory: "/fixture" };
async function flush() {
await Bun.sleep(10);
}
async function host(
catalog: ModelInfo[] = models,
options: Plugin.Context["options"] = { agents },
options: Plugin.Context["options"] = defaultOptions,
existingSkill?: Skill.Info,
) {
const agents = new Map<string, DeepMutable<Agent.Info>>();
const registry = new Map<string, DeepMutable<Agent.Info>>();
let build: DeepMutable<Agent.Info> = Agent.Info.default(Agent.ID.make("build"));
const skills = new Map<string, Skill.Info>(
existingSkill ? [[existingSkill.id, existingSkill]] : [],
);
let transform: (editor: AgentEditor) => void;
let transform: ((editor: AgentEditor) => void) | undefined;
let contextHook: (event: Pick<SessionContext, "agent" | "tools">) => Promise<void> | void;
let promptHook: (event: { sessionID: string }) => Promise<void> | void;
let before: (event: { tool: string; agent: string; input: unknown }) => Promise<void> | void;
let parent = "build";
let child = "fast-lo";
let tools: string[] = [];
let toolTransform: (editor: ToolEditor) => void;
let toolTransform: ((editor: ToolEditor) => void) | undefined;
let permissions: () => Promise<Agent.Info["permissions"]>;
let controller: ReadableStreamDefaultController<unknown>;
const stream = new ReadableStream({
@@ -58,20 +63,20 @@ async function host(
});
const registration = { async dispose() {} };
const replay = () => {
agents.clear();
agents.set("build", structuredClone(build));
registry.clear();
registry.set("build", structuredClone(build));
transform?.({
get: (id) => agents.get(id),
list: () => [...agents.values()],
get: (id) => registry.get(id),
list: () => [...registry.values()],
default() {},
remove: (id) => {
agents.delete(id);
registry.delete(id);
},
update(id, update) {
const draft =
agents.get(id) ?? structuredClone(Agent.Info.default(Agent.ID.make(id)));
registry.get(id) ?? structuredClone(Agent.Info.default(Agent.ID.make(id)));
update(draft);
agents.set(id, draft);
registry.set(id, draft);
},
});
};
@@ -110,7 +115,7 @@ async function host(
},
agent: {
async list() {
return { data: [...agents.values()] };
return { data: [...registry.values()] };
},
async reload() {
replay();
@@ -182,11 +187,8 @@ async function host(
replay();
const cleanup = await setup(context);
if (cleanup) cleanups.push(cleanup);
const flush = async () => {
await Bun.sleep(10);
};
return {
agents,
agents: registry,
skills,
permissions: () => permissions(),
async configure(rules: Agent.Info["permissions"]) {
@@ -195,7 +197,7 @@ async function host(
controller.enqueue({ type: "agent.updated" });
await flush();
},
async models(next: ModelInfo[], location = { directory: "/fixture" }) {
async models(next: ModelInfo[], location = defaultLocation) {
catalog = next;
controller.enqueue({ type: "model.updated", location });
await flush();
@@ -205,10 +207,10 @@ async function host(
replayTools();
await flush();
},
async delegate(agent: string, target: string) {
delegate: async (agent: string, target: string) => {
await before({ tool: "subagent", agent, input: { agent: target } });
},
async command(owner: string, target: string) {
command: async (owner: string, target: string) => {
parent = owner;
child = target;
await promptHook({ sessionID: "child" });
@@ -231,7 +233,7 @@ async function host(
test("worker availability, deep fallback, and model refresh remain provider aware", async () => {
const h = await host();
expect([...h.skills.keys()].sort()).toEqual(["anti-overengineering", "task-planning"]);
expect([...h.skills.keys()].toSorted()).toEqual(["anti-overengineering", "task-planning"]);
expect(String(h.agents.get("coordinate")?.model?.id)).toBe("gpt-6-astra");
await h.models(models.slice(0, 2));
expect(String(h.agents.get("coordinate")?.model?.id)).toBe("gpt-6.1-sol");
@@ -256,7 +258,7 @@ test("assignments are provider-qualified with optional deep pair and variant; pa
const shallow = Object.fromEntries(
Object.entries(agents).filter(([name]) => !name.startsWith("deep-")),
);
const duplicate = models.map((model) => ({ ...model, providerID: Provider.ID.make("other") }));
const duplicate = models.map((entry) => ({ ...entry, providerID: Provider.ID.make("other") }));
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");
@@ -295,6 +297,6 @@ test("native tool and command paths retain core delegation restrictions", async
expect(
z
.object({ agent: z.object({ type: z.string(), enum: z.array(z.string()) }) })
.parse((await h.request("coordinate")).subagent?.input.properties).agent,
.parse((await h.request("coordinate"))["subagent"]?.input["properties"]).agent,
).toEqual({ type: "string", enum: ["fast-lo", "fast-hi", "good-lo", "good-hi"] });
});
+2 -4
View File
@@ -18,9 +18,7 @@ test("Quota loads independently and retains its RPC ID and nullable status", asy
location: { directory: "/fixture" },
integration: {
connection: {
async active() {
return undefined;
},
async active() {},
},
},
event: {
@@ -56,7 +54,7 @@ test("Quota loads independently and retains its RPC ID and nullable status", asy
) => ReturnType<typeof quota.setup>;
const cleanup = await setup(context);
expect(quota.id).toBe("stanp-opencode.quota");
expect(await status!()).toBeNull();
expect(await status?.()).toBeNull();
expect(modelHook).toBe(true);
if (cleanup) await cleanup();
expect(disposed).toBe(true);
+2 -2
View File
@@ -186,8 +186,8 @@ function fixture(
changes: () => changes,
headers,
stored,
switch(credential: typeof current) {
current = credential;
switch(next: typeof current) {
current = next;
},
logout: () => {
current = undefined;