mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-27 09:21:35 +07:00
test: share subagent and policy test fixtures
This commit is contained in:
@@ -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", () => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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)");
|
||||
|
||||
Reference in New Issue
Block a user