test: share subagent and policy test fixtures

This commit is contained in:
Peter Steinberger
2026-03-26 16:03:48 +00:00
parent 22520a2058
commit bac603a63e
6 changed files with 266 additions and 385 deletions

View File

@@ -22,6 +22,52 @@ async function loadFreshAuthProfilesModuleForTest() {
await import("./auth-profiles.js"));
}
function withAgentDirEnv(prefix: string, run: (agentDir: string) => void | Promise<void>) {
const agentDir = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
const previousAgentDir = process.env.OPENCLAW_AGENT_DIR;
const previousPiAgentDir = process.env.PI_CODING_AGENT_DIR;
try {
process.env.OPENCLAW_AGENT_DIR = agentDir;
process.env.PI_CODING_AGENT_DIR = agentDir;
return run(agentDir);
} finally {
if (previousAgentDir === undefined) {
delete process.env.OPENCLAW_AGENT_DIR;
} else {
process.env.OPENCLAW_AGENT_DIR = previousAgentDir;
}
if (previousPiAgentDir === undefined) {
delete process.env.PI_CODING_AGENT_DIR;
} else {
process.env.PI_CODING_AGENT_DIR = previousPiAgentDir;
}
fs.rmSync(agentDir, { recursive: true, force: true });
}
}
function writeAuthStore(agentDir: string, key: string) {
const authPath = path.join(agentDir, "auth-profiles.json");
fs.writeFileSync(
authPath,
`${JSON.stringify(
{
version: AUTH_STORE_VERSION,
profiles: {
"openai:default": {
type: "api_key",
provider: "openai",
key,
},
},
},
null,
2,
)}\n`,
"utf8",
);
return authPath;
}
describe("auth profile store cache", () => {
beforeEach(async () => {
await loadFreshAuthProfilesModuleForTest();
@@ -33,98 +79,24 @@ describe("auth profile store cache", () => {
vi.clearAllMocks();
});
it("reuses the synced auth store while auth-profiles.json is unchanged", () => {
const agentDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-auth-store-cache-"));
const previousAgentDir = process.env.OPENCLAW_AGENT_DIR;
const previousPiAgentDir = process.env.PI_CODING_AGENT_DIR;
try {
process.env.OPENCLAW_AGENT_DIR = agentDir;
process.env.PI_CODING_AGENT_DIR = agentDir;
fs.writeFileSync(
path.join(agentDir, "auth-profiles.json"),
`${JSON.stringify(
{
version: AUTH_STORE_VERSION,
profiles: {
"openai:default": {
type: "api_key",
provider: "openai",
key: "sk-test",
},
},
},
null,
2,
)}\n`,
"utf8",
);
it("reuses the synced auth store while auth-profiles.json is unchanged", async () => {
await withAgentDirEnv("openclaw-auth-store-cache-", (agentDir) => {
writeAuthStore(agentDir, "sk-test");
ensureAuthProfileStore(agentDir);
ensureAuthProfileStore(agentDir);
expect(mocks.syncExternalCliCredentials).toHaveBeenCalledTimes(1);
} finally {
if (previousAgentDir === undefined) {
delete process.env.OPENCLAW_AGENT_DIR;
} else {
process.env.OPENCLAW_AGENT_DIR = previousAgentDir;
}
if (previousPiAgentDir === undefined) {
delete process.env.PI_CODING_AGENT_DIR;
} else {
process.env.PI_CODING_AGENT_DIR = previousPiAgentDir;
}
fs.rmSync(agentDir, { recursive: true, force: true });
}
});
});
it("refreshes the cached auth store after auth-profiles.json changes", async () => {
const agentDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-auth-store-refresh-"));
const previousAgentDir = process.env.OPENCLAW_AGENT_DIR;
const previousPiAgentDir = process.env.PI_CODING_AGENT_DIR;
try {
process.env.OPENCLAW_AGENT_DIR = agentDir;
process.env.PI_CODING_AGENT_DIR = agentDir;
const authPath = path.join(agentDir, "auth-profiles.json");
fs.writeFileSync(
authPath,
`${JSON.stringify(
{
version: AUTH_STORE_VERSION,
profiles: {
"openai:default": {
type: "api_key",
provider: "openai",
key: "sk-test-1",
},
},
},
null,
2,
)}\n`,
"utf8",
);
await withAgentDirEnv("openclaw-auth-store-refresh-", async (agentDir) => {
const authPath = writeAuthStore(agentDir, "sk-test-1");
ensureAuthProfileStore(agentDir);
fs.writeFileSync(
authPath,
`${JSON.stringify(
{
version: AUTH_STORE_VERSION,
profiles: {
"openai:default": {
type: "api_key",
provider: "openai",
key: "sk-test-2",
},
},
},
null,
2,
)}\n`,
"utf8",
);
writeAuthStore(agentDir, "sk-test-2");
const bumpedMtime = new Date(Date.now() + 2_000);
fs.utimesSync(authPath, bumpedMtime, bumpedMtime);
@@ -134,19 +106,7 @@ describe("auth profile store cache", () => {
expect(reloaded.profiles["openai:default"]).toMatchObject({
key: "sk-test-2",
});
} finally {
if (previousAgentDir === undefined) {
delete process.env.OPENCLAW_AGENT_DIR;
} else {
process.env.OPENCLAW_AGENT_DIR = previousAgentDir;
}
if (previousPiAgentDir === undefined) {
delete process.env.PI_CODING_AGENT_DIR;
} else {
process.env.PI_CODING_AGENT_DIR = previousPiAgentDir;
}
fs.rmSync(agentDir, { recursive: true, force: true });
}
});
});
it("re-syncs external CLI credentials after the cache ttl when auth-profiles.json is absent", () => {

View File

@@ -2,6 +2,8 @@ import os from "node:os";
import { beforeEach, describe, expect, it, vi } from "vitest";
import {
createSubagentSpawnTestConfig,
expectPersistedRuntimeModel,
installSessionStoreCaptureMock,
loadSubagentSpawnModuleForTest,
setupAcceptedSubagentGatewayMock,
} from "./subagent-spawn.test-helpers.js";
@@ -56,18 +58,12 @@ describe("spawnSubagentDirect runtime model persistence", () => {
return {};
});
let persistedStore: Record<string, Record<string, unknown>> | undefined;
updateSessionStoreMock.mockImplementation(
async (
_storePath: string,
mutator: (store: Record<string, Record<string, unknown>>) => unknown,
) => {
operations.push("store:update");
const store: Record<string, Record<string, unknown>> = {};
await mutator(store);
installSessionStoreCaptureMock(updateSessionStoreMock, {
operations,
onStore: (store) => {
persistedStore = store;
return store;
},
);
});
const result = await spawnSubagentDirect(
{
@@ -85,10 +81,10 @@ describe("spawnSubagentDirect runtime model persistence", () => {
modelApplied: true,
});
expect(updateSessionStoreMock).toHaveBeenCalledTimes(1);
const [persistedKey, persistedEntry] = Object.entries(persistedStore ?? {})[0] ?? [];
expect(persistedKey).toMatch(/^agent:main:subagent:/);
expect(persistedEntry).toMatchObject({
modelProvider: "openai-codex",
expectPersistedRuntimeModel({
persistedStore,
sessionKey: /^agent:main:subagent:/,
provider: "openai-codex",
model: "gpt-5.4",
});
expect(pruneLegacyStoreKeysMock).toHaveBeenCalledTimes(1);

View File

@@ -1,12 +1,21 @@
import os from "node:os";
import { vi } from "vitest";
import { expect, vi } from "vitest";
type MockFn = (...args: unknown[]) => unknown;
type MockImplementationTarget = {
mockImplementation: (implementation: (opts: { method?: string }) => Promise<unknown>) => unknown;
};
type SessionStore = Record<string, Record<string, unknown>>;
type SessionStoreMutator = (store: SessionStore) => unknown;
type HookRunner = {
hasHooks: (name?: string) => boolean;
runSubagentSpawning?: (...args: unknown[]) => Promise<unknown>;
};
export function createSubagentSpawnTestConfig(workspaceDir = os.tmpdir()) {
export function createSubagentSpawnTestConfig(
workspaceDir = os.tmpdir(),
overrides?: Record<string, unknown>,
) {
return {
session: {
mainKey: "main",
@@ -27,6 +36,7 @@ export function createSubagentSpawnTestConfig(workspaceDir = os.tmpdir()) {
workspace: workspaceDir,
},
},
...overrides,
};
}
@@ -57,11 +67,58 @@ export function createDefaultSessionHelperMocks() {
};
}
export function installSessionStoreCaptureMock(
updateSessionStoreMock: {
mockImplementation: (
implementation: (storePath: string, mutator: SessionStoreMutator) => Promise<SessionStore>,
) => unknown;
},
params?: {
operations?: string[];
onStore?: (store: SessionStore) => void;
},
) {
updateSessionStoreMock.mockImplementation(
async (_storePath: string, mutator: SessionStoreMutator) => {
params?.operations?.push("store:update");
const store: SessionStore = {};
await mutator(store);
params?.onStore?.(store);
return store;
},
);
}
export function expectPersistedRuntimeModel(params: {
persistedStore: SessionStore | undefined;
sessionKey: string | RegExp;
provider: string;
model: string;
}) {
const [persistedKey, persistedEntry] = Object.entries(params.persistedStore ?? {})[0] ?? [];
if (typeof params.sessionKey === "string") {
expect(persistedKey).toBe(params.sessionKey);
} else {
expect(persistedKey).toMatch(params.sessionKey);
}
expect(persistedEntry).toMatchObject({
modelProvider: params.provider,
model: params.model,
});
}
export async function loadSubagentSpawnModuleForTest(params: {
callGatewayMock: MockFn;
loadConfig?: () => Record<string, unknown>;
updateSessionStoreMock?: MockFn;
pruneLegacyStoreKeysMock?: MockFn;
registerSubagentRunMock?: MockFn;
emitSessionLifecycleEventMock?: MockFn;
hookRunner?: HookRunner;
resolveAgentConfig?: (cfg: Record<string, unknown>, agentId: string) => unknown;
resolveAgentWorkspaceDir?: (cfg: Record<string, unknown>, agentId: string) => string;
resolveSubagentSpawnModelSelection?: () => string | undefined;
resolveSandboxRuntimeStatus?: () => { sandboxed: boolean };
workspaceDir?: string;
sessionStorePath?: string;
}) {
@@ -106,14 +163,17 @@ export async function loadSubagentSpawnModuleForTest(params: {
});
}
vi.doMock("./subagent-registry.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("./subagent-registry.js")>();
return {
...actual,
countActiveRunsForSession: () => 0,
registerSubagentRun: () => {},
};
});
if (params.emitSessionLifecycleEventMock) {
vi.doMock("../sessions/session-lifecycle-events.js", async (importOriginal) => {
const actual =
await importOriginal<typeof import("../sessions/session-lifecycle-events.js")>();
return {
...actual,
emitSessionLifecycleEvent: (...args: unknown[]) =>
params.emitSessionLifecycleEventMock?.(...args),
};
});
}
vi.doMock("./subagent-announce.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("./subagent-announce.js")>();
@@ -127,7 +187,9 @@ export async function loadSubagentSpawnModuleForTest(params: {
const actual = await importOriginal<typeof import("./agent-scope.js")>();
return {
...actual,
resolveAgentWorkspaceDir: () => params.workspaceDir ?? os.tmpdir(),
resolveAgentConfig: params.resolveAgentConfig ?? actual.resolveAgentConfig,
resolveAgentWorkspaceDir:
params.resolveAgentWorkspaceDir ?? (() => params.workspaceDir ?? os.tmpdir()),
};
});
@@ -135,19 +197,55 @@ export async function loadSubagentSpawnModuleForTest(params: {
getSubagentDepthFromSessionStore: () => 0,
}));
vi.doMock("./model-selection.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("./model-selection.js")>();
return {
...actual,
resolveSubagentSpawnModelSelection:
params.resolveSubagentSpawnModelSelection ?? actual.resolveSubagentSpawnModelSelection,
};
});
vi.doMock("./sandbox/runtime-status.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("./sandbox/runtime-status.js")>();
return {
...actual,
resolveSandboxRuntimeStatus:
params.resolveSandboxRuntimeStatus ?? actual.resolveSandboxRuntimeStatus,
};
});
vi.doMock("../plugins/hook-runner-global.js", () => ({
getGlobalHookRunner: () => ({ hasHooks: () => false }),
getGlobalHookRunner: () => params.hookRunner ?? { hasHooks: () => false },
}));
vi.doMock("../utils/delivery-context.js", () => ({
normalizeDeliveryContext: identityDeliveryContext,
}));
vi.doMock("../utils/delivery-context.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../utils/delivery-context.js")>();
return {
...actual,
normalizeDeliveryContext: identityDeliveryContext,
};
});
vi.doMock("./tools/sessions-helpers.js", () => createDefaultSessionHelperMocks());
vi.doMock("./tools/sessions-helpers.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("./tools/sessions-helpers.js")>();
return {
...actual,
...createDefaultSessionHelperMocks(),
};
});
const { resetSubagentRegistryForTests } = await import("./subagent-registry.js");
const subagentRegistry = await import("./subagent-registry.js");
if (params.registerSubagentRunMock) {
vi.spyOn(subagentRegistry, "registerSubagentRun").mockImplementation(
(...args: Parameters<typeof subagentRegistry.registerSubagentRun>) =>
params.registerSubagentRunMock?.(...args) as ReturnType<
typeof subagentRegistry.registerSubagentRun
>,
);
}
return {
...(await import("./subagent-spawn.js")),
resetSubagentRegistryForTests,
resetSubagentRegistryForTests: subagentRegistry.resetSubagentRegistryForTests,
};
}

View File

@@ -1,8 +1,10 @@
import os from "node:os";
import { beforeEach, describe, expect, it, vi } from "vitest";
import {
createDefaultSessionHelperMocks,
identityDeliveryContext,
createSubagentSpawnTestConfig,
expectPersistedRuntimeModel,
installSessionStoreCaptureMock,
loadSubagentSpawnModuleForTest,
} from "./subagent-spawn.test-helpers.js";
import { installAcceptedSubagentGatewayMock } from "./test-helpers/subagent-gateway.js";
@@ -15,89 +17,11 @@ const hoisted = vi.hoisted(() => ({
configOverride: {} as Record<string, unknown>,
}));
vi.mock("../gateway/call.js", () => ({
callGateway: (opts: unknown) => hoisted.callGatewayMock(opts),
}));
vi.mock("../config/config.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../config/config.js")>();
return {
...actual,
loadConfig: () => hoisted.configOverride,
};
});
vi.mock("../config/sessions.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../config/sessions.js")>();
return {
...actual,
updateSessionStore: (...args: unknown[]) => hoisted.updateSessionStoreMock(...args),
};
});
vi.mock("../gateway/session-utils.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../gateway/session-utils.js")>();
return {
...actual,
resolveGatewaySessionStoreTarget: (params: { key: string }) => ({
agentId: "main",
storePath: "/tmp/subagent-spawn-session-store.json",
canonicalKey: params.key,
storeKeys: [params.key],
}),
pruneLegacyStoreKeys: (...args: unknown[]) => hoisted.pruneLegacyStoreKeysMock(...args),
};
});
vi.mock("./subagent-registry.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("./subagent-registry.js")>();
return {
...actual,
countActiveRunsForSession: () => 0,
registerSubagentRun: (args: unknown) => hoisted.registerSubagentRunMock(args),
};
});
vi.mock("../sessions/session-lifecycle-events.js", () => ({
emitSessionLifecycleEvent: (args: unknown) => hoisted.emitSessionLifecycleEventMock(args),
}));
vi.mock("./subagent-announce.js", () => ({
buildSubagentSystemPrompt: () => "system-prompt",
}));
vi.mock("./subagent-depth.js", () => ({
getSubagentDepthFromSessionStore: () => 0,
}));
vi.mock("./model-selection.js", () => ({
resolveSubagentSpawnModelSelection: () => "openai-codex/gpt-5.4",
}));
vi.mock("./sandbox/runtime-status.js", () => ({
resolveSandboxRuntimeStatus: () => ({ sandboxed: false }),
}));
vi.mock("../plugins/hook-runner-global.js", () => ({
getGlobalHookRunner: () => ({ hasHooks: () => false }),
}));
vi.mock("../utils/delivery-context.js", () => ({
normalizeDeliveryContext: identityDeliveryContext,
}));
vi.mock("./tools/sessions-helpers.js", () => createDefaultSessionHelperMocks());
vi.mock("./agent-scope.js", () => ({
resolveAgentConfig: () => undefined,
}));
let resetSubagentRegistryForTests: typeof import("./subagent-registry.js").resetSubagentRegistryForTests;
let spawnSubagentDirect: typeof import("./subagent-spawn.js").spawnSubagentDirect;
function createConfigOverride(overrides?: Record<string, unknown>) {
return {
session: {
mainKey: "main",
scope: "per-sender",
},
return createSubagentSpawnTestConfig(os.tmpdir(), {
agents: {
defaults: {
workspace: os.tmpdir(),
@@ -110,12 +34,24 @@ function createConfigOverride(overrides?: Record<string, unknown>) {
],
},
...overrides,
};
});
}
describe("spawnSubagentDirect seam flow", () => {
beforeEach(() => {
vi.resetModules();
beforeEach(async () => {
({ resetSubagentRegistryForTests, spawnSubagentDirect } = await loadSubagentSpawnModuleForTest({
callGatewayMock: hoisted.callGatewayMock,
loadConfig: () => hoisted.configOverride,
updateSessionStoreMock: hoisted.updateSessionStoreMock,
pruneLegacyStoreKeysMock: hoisted.pruneLegacyStoreKeysMock,
registerSubagentRunMock: hoisted.registerSubagentRunMock,
emitSessionLifecycleEventMock: hoisted.emitSessionLifecycleEventMock,
resolveAgentConfig: () => undefined,
resolveSubagentSpawnModelSelection: () => "openai-codex/gpt-5.4",
resolveSandboxRuntimeStatus: () => ({ sandboxed: false }),
sessionStorePath: "/tmp/subagent-spawn-session-store.json",
}));
resetSubagentRegistryForTests();
hoisted.callGatewayMock.mockReset();
hoisted.updateSessionStoreMock.mockReset();
hoisted.pruneLegacyStoreKeysMock.mockReset();
@@ -137,7 +73,6 @@ describe("spawnSubagentDirect seam flow", () => {
});
it("accepts a spawned run across session patching, runtime-model persistence, registry registration, and lifecycle emission", async () => {
const { spawnSubagentDirect } = await import("./subagent-spawn.js");
const operations: string[] = [];
let persistedStore: Record<string, Record<string, unknown>> | undefined;
@@ -151,18 +86,12 @@ describe("spawnSubagentDirect seam flow", () => {
}
return {};
});
hoisted.updateSessionStoreMock.mockImplementation(
async (
_storePath: string,
mutator: (store: Record<string, Record<string, unknown>>) => unknown,
) => {
operations.push("store:update");
const store: Record<string, Record<string, unknown>> = {};
await mutator(store);
installSessionStoreCaptureMock(hoisted.updateSessionStoreMock, {
operations,
onStore: (store) => {
persistedStore = store;
return store;
},
);
});
const result = await spawnSubagentDirect(
{
@@ -216,10 +145,10 @@ describe("spawnSubagentDirect seam flow", () => {
label: undefined,
});
const [persistedKey, persistedEntry] = Object.entries(persistedStore ?? {})[0] ?? [];
expect(persistedKey).toBe(childSessionKey);
expect(persistedEntry).toMatchObject({
modelProvider: "openai-codex",
expectPersistedRuntimeModel({
persistedStore,
sessionKey: childSessionKey,
provider: "openai-codex",
model: "gpt-5.4",
});
expect(operations.indexOf("gateway:sessions.patch")).toBeGreaterThan(-1);

View File

@@ -1,9 +1,9 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import {
createDefaultSessionHelperMocks,
identityDeliveryContext,
createSubagentSpawnTestConfig,
loadSubagentSpawnModuleForTest,
setupAcceptedSubagentGatewayMock,
} from "./subagent-spawn.test-helpers.js";
import { installAcceptedSubagentGatewayMock } from "./test-helpers/subagent-gateway.js";
type TestAgentConfig = {
id?: string;
@@ -30,18 +30,7 @@ const hoisted = vi.hoisted(() => ({
}));
let spawnSubagentDirect: typeof import("./subagent-spawn.js").spawnSubagentDirect;
vi.mock("../gateway/call.js", () => ({
callGateway: (opts: unknown) => hoisted.callGatewayMock(opts),
}));
vi.mock("../config/config.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../config/config.js")>();
return {
...actual,
loadConfig: () => hoisted.configOverride,
};
});
let resetSubagentRegistryForTests: typeof import("./subagent-registry.js").resetSubagentRegistryForTests;
vi.mock("@mariozechner/pi-ai/oauth", async () => {
const actual = await vi.importActual<typeof import("@mariozechner/pi-ai/oauth")>(
@@ -54,51 +43,8 @@ vi.mock("@mariozechner/pi-ai/oauth", async () => {
};
});
vi.mock("./subagent-registry.js", () => ({
countActiveRunsForSession: () => 0,
registerSubagentRun: (args: unknown) => hoisted.registerSubagentRunMock(args),
}));
vi.mock("./subagent-announce.js", () => ({
buildSubagentSystemPrompt: () => "system-prompt",
}));
vi.mock("./subagent-depth.js", () => ({
getSubagentDepthFromSessionStore: () => 0,
}));
vi.mock("./model-selection.js", () => ({
resolveSubagentSpawnModelSelection: () => undefined,
}));
vi.mock("./sandbox/runtime-status.js", () => ({
resolveSandboxRuntimeStatus: () => ({ sandboxed: false }),
}));
vi.mock("../plugins/hook-runner-global.js", () => ({
getGlobalHookRunner: () => hoisted.hookRunner,
}));
vi.mock("../utils/delivery-context.js", () => ({
normalizeDeliveryContext: identityDeliveryContext,
}));
vi.mock("./tools/sessions-helpers.js", () => createDefaultSessionHelperMocks());
vi.mock("./agent-scope.js", () => ({
resolveAgentConfig: (cfg: TestConfig, agentId: string) =>
cfg.agents?.list?.find((entry) => entry.id === agentId),
resolveAgentWorkspaceDir: (cfg: TestConfig, agentId: string) =>
cfg.agents?.list?.find((entry) => entry.id === agentId)?.workspace ??
`/tmp/workspace-${agentId}`,
}));
function createConfigOverride(overrides?: Record<string, unknown>) {
return {
session: {
mainKey: "main",
scope: "per-sender",
},
return createSubagentSpawnTestConfig("/tmp/workspace-main", {
agents: {
list: [
{
@@ -108,60 +54,15 @@ function createConfigOverride(overrides?: Record<string, unknown>) {
],
},
...overrides,
};
});
}
function setupGatewayMock() {
installAcceptedSubagentGatewayMock(hoisted.callGatewayMock);
function resolveTestAgentConfig(cfg: Record<string, unknown>, agentId: string) {
return (cfg as TestConfig).agents?.list?.find((entry) => entry.id === agentId);
}
async function loadFreshSubagentSpawnWorkspaceModuleForTest() {
vi.resetModules();
vi.doMock("../gateway/call.js", () => ({
callGateway: (opts: unknown) => hoisted.callGatewayMock(opts),
}));
vi.doMock("../config/config.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../config/config.js")>();
return {
...actual,
loadConfig: () => hoisted.configOverride,
};
});
vi.doMock("./subagent-registry.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("./subagent-registry.js")>();
return {
...actual,
countActiveRunsForSession: () => 0,
registerSubagentRun: (args: unknown) => hoisted.registerSubagentRunMock(args),
};
});
vi.doMock("./subagent-announce.js", () => ({
buildSubagentSystemPrompt: () => "system-prompt",
}));
vi.doMock("./subagent-depth.js", () => ({
getSubagentDepthFromSessionStore: () => 0,
}));
vi.doMock("./model-selection.js", () => ({
resolveSubagentSpawnModelSelection: () => undefined,
}));
vi.doMock("./sandbox/runtime-status.js", () => ({
resolveSandboxRuntimeStatus: () => ({ sandboxed: false }),
}));
vi.doMock("../plugins/hook-runner-global.js", () => ({
getGlobalHookRunner: () => hoisted.hookRunner,
}));
vi.doMock("../utils/delivery-context.js", () => ({
normalizeDeliveryContext: identityDeliveryContext,
}));
vi.doMock("./tools/sessions-helpers.js", () => createDefaultSessionHelperMocks());
vi.doMock("./agent-scope.js", () => ({
resolveAgentConfig: (cfg: TestConfig, agentId: string) =>
cfg.agents?.list?.find((entry) => entry.id === agentId),
resolveAgentWorkspaceDir: (cfg: TestConfig, agentId: string) =>
cfg.agents?.list?.find((entry) => entry.id === agentId)?.workspace ??
`/tmp/workspace-${agentId}`,
}));
({ spawnSubagentDirect } = await import("./subagent-spawn.js"));
function resolveTestAgentWorkspace(cfg: Record<string, unknown>, agentId: string) {
return resolveTestAgentConfig(cfg, agentId)?.workspace ?? `/tmp/workspace-${agentId}`;
}
function getRegisteredRun() {
@@ -193,14 +94,22 @@ async function expectAcceptedWorkspace(params: { agentId: string; expectedWorksp
describe("spawnSubagentDirect workspace inheritance", () => {
beforeEach(async () => {
await loadFreshSubagentSpawnWorkspaceModuleForTest();
({ resetSubagentRegistryForTests, spawnSubagentDirect } = await loadSubagentSpawnModuleForTest({
callGatewayMock: hoisted.callGatewayMock,
loadConfig: () => hoisted.configOverride,
registerSubagentRunMock: hoisted.registerSubagentRunMock,
hookRunner: hoisted.hookRunner,
resolveAgentConfig: resolveTestAgentConfig,
resolveAgentWorkspaceDir: resolveTestAgentWorkspace,
}));
resetSubagentRegistryForTests();
hoisted.callGatewayMock.mockClear();
hoisted.registerSubagentRunMock.mockClear();
hoisted.hookRunner.hasHooks.mockReset();
hoisted.hookRunner.hasHooks.mockImplementation(() => false);
hoisted.hookRunner.runSubagentSpawning.mockReset();
hoisted.configOverride = createConfigOverride();
setupGatewayMock();
setupAcceptedSubagentGatewayMock(hoisted.callGatewayMock);
});
it("uses the target agent workspace for cross-agent spawns", async () => {

View File

@@ -6,6 +6,31 @@ import {
type DummyTool = { name: string };
function runAllowlistWarningStep(params: {
allow: string[];
label: string;
suppressUnavailableCoreToolWarning?: boolean;
}) {
const warnings: string[] = [];
const tools = [{ name: "exec" }] as unknown as DummyTool[];
applyToolPolicyPipeline({
// oxlint-disable-next-line typescript/no-explicit-any
tools: tools as any,
// oxlint-disable-next-line typescript/no-explicit-any
toolMeta: () => undefined,
warn: (msg) => warnings.push(msg),
steps: [
{
policy: { allow: params.allow },
label: params.label,
stripPluginOnlyAllowlist: true,
suppressUnavailableCoreToolWarning: params.suppressUnavailableCoreToolWarning,
},
],
});
return warnings;
}
describe("tool-policy-pipeline", () => {
beforeEach(() => {
resetToolPolicyWarningCacheForTest();
@@ -53,43 +78,19 @@ describe("tool-policy-pipeline", () => {
});
test("suppresses built-in profile warnings for unavailable gated core tools", () => {
const warnings: string[] = [];
const tools = [{ name: "exec" }] as unknown as DummyTool[];
applyToolPolicyPipeline({
// oxlint-disable-next-line typescript/no-explicit-any
tools: tools as any,
// oxlint-disable-next-line typescript/no-explicit-any
toolMeta: () => undefined,
warn: (msg) => warnings.push(msg),
steps: [
{
policy: { allow: ["apply_patch"] },
label: "tools.profile (coding)",
stripPluginOnlyAllowlist: true,
suppressUnavailableCoreToolWarning: true,
},
],
const warnings = runAllowlistWarningStep({
allow: ["apply_patch"],
label: "tools.profile (coding)",
suppressUnavailableCoreToolWarning: true,
});
expect(warnings).toEqual([]);
});
test("still warns for profile steps when explicit alsoAllow entries are present", () => {
const warnings: string[] = [];
const tools = [{ name: "exec" }] as unknown as DummyTool[];
applyToolPolicyPipeline({
// oxlint-disable-next-line typescript/no-explicit-any
tools: tools as any,
// oxlint-disable-next-line typescript/no-explicit-any
toolMeta: () => undefined,
warn: (msg) => warnings.push(msg),
steps: [
{
policy: { allow: ["apply_patch"] },
label: "tools.profile (coding)",
stripPluginOnlyAllowlist: true,
suppressUnavailableCoreToolWarning: false,
},
],
const warnings = runAllowlistWarningStep({
allow: ["apply_patch"],
label: "tools.profile (coding)",
suppressUnavailableCoreToolWarning: false,
});
expect(warnings.length).toBe(1);
expect(warnings[0]).toContain("unknown entries (apply_patch)");
@@ -99,21 +100,9 @@ describe("tool-policy-pipeline", () => {
});
test("still warns for explicit allowlists that mention unavailable gated core tools", () => {
const warnings: string[] = [];
const tools = [{ name: "exec" }] as unknown as DummyTool[];
applyToolPolicyPipeline({
// oxlint-disable-next-line typescript/no-explicit-any
tools: tools as any,
// oxlint-disable-next-line typescript/no-explicit-any
toolMeta: () => undefined,
warn: (msg) => warnings.push(msg),
steps: [
{
policy: { allow: ["apply_patch"] },
label: "tools.allow",
stripPluginOnlyAllowlist: true,
},
],
const warnings = runAllowlistWarningStep({
allow: ["apply_patch"],
label: "tools.allow",
});
expect(warnings.length).toBe(1);
expect(warnings[0]).toContain("unknown entries (apply_patch)");