mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-04 20:00:22 +00:00
CLI: speed up command secret gateway tests
This commit is contained in:
@@ -6,7 +6,10 @@ import {
|
||||
TALK_TEST_PROVIDER_API_KEY_PATH,
|
||||
TALK_TEST_PROVIDER_API_KEY_PATH_SEGMENTS,
|
||||
} from "../test-utils/talk-test-provider.js";
|
||||
import { resolveCommandSecretRefsViaGateway } from "./command-secret-gateway.js";
|
||||
import {
|
||||
__testing as commandSecretGatewayTesting,
|
||||
resolveCommandSecretRefsViaGateway,
|
||||
} from "./command-secret-gateway.js";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
callGateway: vi.fn(),
|
||||
@@ -29,6 +32,7 @@ vi.mock("../utils/message-channel.js", () => ({
|
||||
|
||||
beforeEach(() => {
|
||||
callGateway.mockReset();
|
||||
commandSecretGatewayTesting.resetDepsForTest();
|
||||
});
|
||||
|
||||
describe("resolveCommandSecretRefsViaGateway", () => {
|
||||
@@ -172,6 +176,42 @@ describe("resolveCommandSecretRefsViaGateway", () => {
|
||||
});
|
||||
|
||||
it("enforces unresolved checks only for allowed paths when provided", async () => {
|
||||
const restoreDeps = commandSecretGatewayTesting.setDepsForTest({
|
||||
analyzeCommandSecretAssignmentsFromSnapshot: () =>
|
||||
({
|
||||
assignments: [
|
||||
{
|
||||
path: "channels.discord.accounts.ops.token",
|
||||
pathSegments: ["channels", "discord", "accounts", "ops", "token"],
|
||||
value: "ops-token",
|
||||
},
|
||||
],
|
||||
diagnostics: [],
|
||||
inactive: [],
|
||||
unresolved: [],
|
||||
}) as never,
|
||||
collectConfigAssignments: ({ context }) => {
|
||||
context.assignments.push(
|
||||
{ path: "channels.discord.accounts.ops.token" } as never,
|
||||
{ path: "channels.discord.accounts.chat.token" } as never,
|
||||
);
|
||||
},
|
||||
discoverConfigSecretTargetsByIds: () =>
|
||||
[
|
||||
{
|
||||
entry: { expectedResolvedValue: "string" },
|
||||
path: "channels.discord.accounts.ops.token",
|
||||
pathSegments: ["channels", "discord", "accounts", "ops", "token"],
|
||||
value: { source: "env", provider: "default", id: "DISCORD_OPS_TOKEN" },
|
||||
},
|
||||
{
|
||||
entry: { expectedResolvedValue: "string" },
|
||||
path: "channels.discord.accounts.chat.token",
|
||||
pathSegments: ["channels", "discord", "accounts", "chat", "token"],
|
||||
value: { source: "env", provider: "default", id: "DISCORD_CHAT_TOKEN" },
|
||||
},
|
||||
] as never,
|
||||
});
|
||||
callGateway.mockResolvedValueOnce({
|
||||
assignments: [
|
||||
{
|
||||
@@ -183,31 +223,35 @@ describe("resolveCommandSecretRefsViaGateway", () => {
|
||||
diagnostics: [],
|
||||
});
|
||||
|
||||
const result = await resolveCommandSecretRefsViaGateway({
|
||||
config: {
|
||||
channels: {
|
||||
discord: {
|
||||
accounts: {
|
||||
ops: {
|
||||
token: { source: "env", provider: "default", id: "DISCORD_OPS_TOKEN" },
|
||||
},
|
||||
chat: {
|
||||
token: { source: "env", provider: "default", id: "DISCORD_CHAT_TOKEN" },
|
||||
try {
|
||||
const result = await resolveCommandSecretRefsViaGateway({
|
||||
config: {
|
||||
channels: {
|
||||
discord: {
|
||||
accounts: {
|
||||
ops: {
|
||||
token: { source: "env", provider: "default", id: "DISCORD_OPS_TOKEN" },
|
||||
},
|
||||
chat: {
|
||||
token: { source: "env", provider: "default", id: "DISCORD_CHAT_TOKEN" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig,
|
||||
commandName: "message",
|
||||
targetIds: new Set(["channels.discord.accounts.*.token"]),
|
||||
allowedPaths: new Set(["channels.discord.accounts.ops.token"]),
|
||||
});
|
||||
} as OpenClawConfig,
|
||||
commandName: "message",
|
||||
targetIds: new Set(["channels.discord.accounts.*.token"]),
|
||||
allowedPaths: new Set(["channels.discord.accounts.ops.token"]),
|
||||
});
|
||||
|
||||
expect(result.resolvedConfig.channels?.discord?.accounts?.ops?.token).toBe("ops-token");
|
||||
expect(result.targetStatesByPath).toEqual({
|
||||
"channels.discord.accounts.ops.token": "resolved_gateway",
|
||||
});
|
||||
expect(result.hadUnresolvedTargets).toBe(false);
|
||||
expect(result.resolvedConfig.channels?.discord?.accounts?.ops?.token).toBe("ops-token");
|
||||
expect(result.targetStatesByPath).toEqual({
|
||||
"channels.discord.accounts.ops.token": "resolved_gateway",
|
||||
});
|
||||
expect(result.hadUnresolvedTargets).toBe(false);
|
||||
} finally {
|
||||
restoreDeps();
|
||||
}
|
||||
});
|
||||
|
||||
it("fails fast when gateway-backed resolution is unavailable", async () => {
|
||||
@@ -275,128 +319,173 @@ describe("resolveCommandSecretRefsViaGateway", () => {
|
||||
});
|
||||
|
||||
it("falls back to local resolution for web search SecretRefs when gateway is unavailable", async () => {
|
||||
const restoreDeps = commandSecretGatewayTesting.setDepsForTest({
|
||||
collectConfigAssignments: ({ context }) => {
|
||||
context.assignments.push({
|
||||
path: "plugins.entries.google.config.webSearch.apiKey",
|
||||
} as never);
|
||||
},
|
||||
resolveManifestContractOwnerPluginId: (params) =>
|
||||
params.contract === "webSearchProviders" && params.value === "gemini"
|
||||
? "google"
|
||||
: undefined,
|
||||
});
|
||||
const envKey = "WEB_SEARCH_GEMINI_API_KEY_LOCAL_FALLBACK";
|
||||
await withEnvValue(envKey, "gemini-local-fallback-key", async () => {
|
||||
try {
|
||||
callGateway.mockRejectedValueOnce(new Error("gateway closed"));
|
||||
const result = await resolveCommandSecretRefsViaGateway({
|
||||
config: {
|
||||
plugins: {
|
||||
entries: {
|
||||
google: {
|
||||
config: {
|
||||
webSearch: {
|
||||
apiKey: { source: "env", provider: "default", id: envKey },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
tools: {
|
||||
web: {
|
||||
search: {
|
||||
provider: "gemini",
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown as OpenClawConfig,
|
||||
commandName: "agent",
|
||||
targetIds: new Set(["plugins.entries.google.config.webSearch.apiKey"]),
|
||||
});
|
||||
|
||||
const googleWebSearchConfig = result.resolvedConfig.plugins?.entries?.google?.config as
|
||||
| { webSearch?: { apiKey?: unknown } }
|
||||
| undefined;
|
||||
expect(googleWebSearchConfig?.webSearch?.apiKey).toBe("gemini-local-fallback-key");
|
||||
expect(result.targetStatesByPath["plugins.entries.google.config.webSearch.apiKey"]).toBe(
|
||||
"resolved_local",
|
||||
);
|
||||
expectGatewayUnavailableLocalFallbackDiagnostics(result);
|
||||
} finally {
|
||||
restoreDeps();
|
||||
}
|
||||
});
|
||||
}, 300_000);
|
||||
|
||||
it("falls back to local resolution for web fetch provider SecretRefs when gateway is unavailable", async () => {
|
||||
const restoreDeps = commandSecretGatewayTesting.setDepsForTest({
|
||||
collectConfigAssignments: ({ context }) => {
|
||||
context.assignments.push({
|
||||
path: "plugins.entries.firecrawl.config.webFetch.apiKey",
|
||||
} as never);
|
||||
},
|
||||
resolveManifestContractOwnerPluginId: (params) =>
|
||||
params.contract === "webFetchProviders" && params.value === "firecrawl"
|
||||
? "firecrawl"
|
||||
: undefined,
|
||||
});
|
||||
const envKey = "WEB_FETCH_FIRECRAWL_API_KEY_LOCAL_FALLBACK";
|
||||
await withEnvValue(envKey, "firecrawl-local-fallback-key", async () => {
|
||||
try {
|
||||
callGateway.mockRejectedValueOnce(new Error("gateway closed"));
|
||||
const result = await resolveCommandSecretRefsViaGateway({
|
||||
config: {
|
||||
plugins: {
|
||||
entries: {
|
||||
firecrawl: {
|
||||
config: {
|
||||
webFetch: {
|
||||
apiKey: { source: "env", provider: "default", id: envKey },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
tools: {
|
||||
web: {
|
||||
fetch: {
|
||||
provider: "firecrawl",
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown as OpenClawConfig,
|
||||
commandName: "agent",
|
||||
targetIds: new Set(["plugins.entries.firecrawl.config.webFetch.apiKey"]),
|
||||
});
|
||||
|
||||
const firecrawlConfig = result.resolvedConfig.plugins?.entries?.firecrawl?.config as
|
||||
| { webFetch?: { apiKey?: unknown } }
|
||||
| undefined;
|
||||
expect(firecrawlConfig?.webFetch?.apiKey).toBe("firecrawl-local-fallback-key");
|
||||
expect(result.targetStatesByPath["plugins.entries.firecrawl.config.webFetch.apiKey"]).toBe(
|
||||
"resolved_local",
|
||||
);
|
||||
expectGatewayUnavailableLocalFallbackDiagnostics(result);
|
||||
} finally {
|
||||
restoreDeps();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("marks web SecretRefs inactive when the web surface is disabled during local fallback", async () => {
|
||||
const restoreDeps = commandSecretGatewayTesting.setDepsForTest({
|
||||
collectConfigAssignments: ({ context }) => {
|
||||
context.assignments.push({
|
||||
path: "plugins.entries.google.config.webSearch.apiKey",
|
||||
} as never);
|
||||
},
|
||||
resolveManifestContractOwnerPluginId: (params) =>
|
||||
params.contract === "webSearchProviders" && params.value === "gemini"
|
||||
? "google"
|
||||
: undefined,
|
||||
});
|
||||
try {
|
||||
callGateway.mockRejectedValueOnce(new Error("gateway closed"));
|
||||
const result = await resolveCommandSecretRefsViaGateway({
|
||||
config: {
|
||||
tools: {
|
||||
web: {
|
||||
search: {
|
||||
enabled: false,
|
||||
provider: "gemini",
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
entries: {
|
||||
google: {
|
||||
config: {
|
||||
webSearch: {
|
||||
apiKey: { source: "env", provider: "default", id: envKey },
|
||||
apiKey: {
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "WEB_SEARCH_DISABLED_KEY",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
tools: {
|
||||
web: {
|
||||
search: {
|
||||
provider: "gemini",
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown as OpenClawConfig,
|
||||
} as OpenClawConfig,
|
||||
commandName: "agent",
|
||||
targetIds: new Set(["plugins.entries.google.config.webSearch.apiKey"]),
|
||||
});
|
||||
|
||||
const googleWebSearchConfig = result.resolvedConfig.plugins?.entries?.google?.config as
|
||||
| { webSearch?: { apiKey?: unknown } }
|
||||
| undefined;
|
||||
expect(googleWebSearchConfig?.webSearch?.apiKey).toBe("gemini-local-fallback-key");
|
||||
expect(result.hadUnresolvedTargets).toBe(false);
|
||||
expect(result.targetStatesByPath["plugins.entries.google.config.webSearch.apiKey"]).toBe(
|
||||
"resolved_local",
|
||||
"inactive_surface",
|
||||
);
|
||||
expectGatewayUnavailableLocalFallbackDiagnostics(result);
|
||||
});
|
||||
}, 300_000);
|
||||
|
||||
it("falls back to local resolution for web fetch provider SecretRefs when gateway is unavailable", async () => {
|
||||
const envKey = "WEB_FETCH_FIRECRAWL_API_KEY_LOCAL_FALLBACK";
|
||||
await withEnvValue(envKey, "firecrawl-local-fallback-key", async () => {
|
||||
callGateway.mockRejectedValueOnce(new Error("gateway closed"));
|
||||
const result = await resolveCommandSecretRefsViaGateway({
|
||||
config: {
|
||||
plugins: {
|
||||
entries: {
|
||||
firecrawl: {
|
||||
config: {
|
||||
webFetch: {
|
||||
apiKey: { source: "env", provider: "default", id: envKey },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
tools: {
|
||||
web: {
|
||||
fetch: {
|
||||
provider: "firecrawl",
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown as OpenClawConfig,
|
||||
commandName: "agent",
|
||||
targetIds: new Set(["plugins.entries.firecrawl.config.webFetch.apiKey"]),
|
||||
});
|
||||
|
||||
const firecrawlConfig = result.resolvedConfig.plugins?.entries?.firecrawl?.config as
|
||||
| { webFetch?: { apiKey?: unknown } }
|
||||
| undefined;
|
||||
expect(firecrawlConfig?.webFetch?.apiKey).toBe("firecrawl-local-fallback-key");
|
||||
expect(result.targetStatesByPath["plugins.entries.firecrawl.config.webFetch.apiKey"]).toBe(
|
||||
"resolved_local",
|
||||
);
|
||||
expectGatewayUnavailableLocalFallbackDiagnostics(result);
|
||||
});
|
||||
});
|
||||
|
||||
it("marks web SecretRefs inactive when the web surface is disabled during local fallback", async () => {
|
||||
callGateway.mockRejectedValueOnce(new Error("gateway closed"));
|
||||
const result = await resolveCommandSecretRefsViaGateway({
|
||||
config: {
|
||||
tools: {
|
||||
web: {
|
||||
search: {
|
||||
enabled: false,
|
||||
provider: "gemini",
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
entries: {
|
||||
google: {
|
||||
config: {
|
||||
webSearch: {
|
||||
apiKey: {
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "WEB_SEARCH_DISABLED_KEY",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig,
|
||||
commandName: "agent",
|
||||
targetIds: new Set(["plugins.entries.google.config.webSearch.apiKey"]),
|
||||
});
|
||||
|
||||
expect(result.hadUnresolvedTargets).toBe(false);
|
||||
expect(result.targetStatesByPath["plugins.entries.google.config.webSearch.apiKey"]).toBe(
|
||||
"inactive_surface",
|
||||
);
|
||||
expect(
|
||||
result.diagnostics.some((entry) =>
|
||||
entry.includes(
|
||||
"plugins.entries.google.config.webSearch.apiKey: tools.web.search is disabled.",
|
||||
expect(
|
||||
result.diagnostics.some((entry) =>
|
||||
entry.includes(
|
||||
"plugins.entries.google.config.webSearch.apiKey: tools.web.search is disabled.",
|
||||
),
|
||||
),
|
||||
),
|
||||
).toBe(true);
|
||||
).toBe(true);
|
||||
} finally {
|
||||
restoreDeps();
|
||||
}
|
||||
});
|
||||
|
||||
it("returns a version-skew hint when gateway does not support secrets.resolve", async () => {
|
||||
|
||||
@@ -58,6 +58,41 @@ type GatewaySecretsResolveResult = {
|
||||
const WEB_RUNTIME_SECRET_TARGET_ID_PREFIXES = ["tools.web.search", "plugins.entries."] as const;
|
||||
const WEB_RUNTIME_SECRET_PATH_PREFIXES = ["tools.web.search.", "plugins.entries."] as const;
|
||||
|
||||
type CommandSecretGatewayDeps = {
|
||||
analyzeCommandSecretAssignmentsFromSnapshot: typeof analyzeCommandSecretAssignmentsFromSnapshot;
|
||||
collectConfigAssignments: typeof collectConfigAssignments;
|
||||
discoverConfigSecretTargetsByIds: typeof discoverConfigSecretTargetsByIds;
|
||||
resolveManifestContractOwnerPluginId: typeof resolveManifestContractOwnerPluginId;
|
||||
resolveRuntimeWebTools: typeof resolveRuntimeWebTools;
|
||||
};
|
||||
|
||||
const commandSecretGatewayDeps: CommandSecretGatewayDeps = {
|
||||
analyzeCommandSecretAssignmentsFromSnapshot,
|
||||
collectConfigAssignments,
|
||||
discoverConfigSecretTargetsByIds,
|
||||
resolveManifestContractOwnerPluginId,
|
||||
resolveRuntimeWebTools,
|
||||
};
|
||||
|
||||
export const __testing = {
|
||||
setDepsForTest(overrides: Partial<CommandSecretGatewayDeps>): () => void {
|
||||
const previous = { ...commandSecretGatewayDeps };
|
||||
Object.assign(commandSecretGatewayDeps, overrides);
|
||||
return () => {
|
||||
Object.assign(commandSecretGatewayDeps, previous);
|
||||
};
|
||||
},
|
||||
resetDepsForTest(): void {
|
||||
Object.assign(commandSecretGatewayDeps, {
|
||||
analyzeCommandSecretAssignmentsFromSnapshot,
|
||||
collectConfigAssignments,
|
||||
discoverConfigSecretTargetsByIds,
|
||||
resolveManifestContractOwnerPluginId,
|
||||
resolveRuntimeWebTools,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
function pluginIdFromRuntimeWebPath(path: string): string | undefined {
|
||||
const match = /^plugins\.entries\.([^.]+)\.config\.(webSearch|webFetch)\.apiKey$/.exec(path);
|
||||
return match?.[1];
|
||||
@@ -117,7 +152,7 @@ function classifyRuntimeWebTargetPathState(params: {
|
||||
if (!configuredProvider) {
|
||||
return "active";
|
||||
}
|
||||
return resolveManifestContractOwnerPluginId({
|
||||
return commandSecretGatewayDeps.resolveManifestContractOwnerPluginId({
|
||||
contract: "webFetchProviders",
|
||||
value: configuredProvider,
|
||||
origin: "bundled",
|
||||
@@ -135,7 +170,7 @@ function classifyRuntimeWebTargetPathState(params: {
|
||||
if (!configuredProvider) {
|
||||
return "active";
|
||||
}
|
||||
return resolveManifestContractOwnerPluginId({
|
||||
return commandSecretGatewayDeps.resolveManifestContractOwnerPluginId({
|
||||
contract: "webSearchProviders",
|
||||
value: configuredProvider,
|
||||
origin: "bundled",
|
||||
@@ -195,7 +230,7 @@ function describeInactiveRuntimeWebTargetPath(params: {
|
||||
const configuredProvider =
|
||||
typeof search?.provider === "string" ? search.provider.trim().toLowerCase() : "";
|
||||
const configuredPluginId = configuredProvider
|
||||
? resolveManifestContractOwnerPluginId({
|
||||
? commandSecretGatewayDeps.resolveManifestContractOwnerPluginId({
|
||||
contract: "webSearchProviders",
|
||||
value: configuredProvider,
|
||||
origin: "bundled",
|
||||
@@ -254,7 +289,10 @@ function collectConfiguredTargetRefPaths(params: {
|
||||
}): Set<string> {
|
||||
const defaults = params.config.secrets?.defaults;
|
||||
const configuredTargetRefPaths = new Set<string>();
|
||||
for (const target of discoverConfigSecretTargetsByIds(params.config, params.targetIds)) {
|
||||
for (const target of commandSecretGatewayDeps.discoverConfigSecretTargetsByIds(
|
||||
params.config,
|
||||
params.targetIds,
|
||||
)) {
|
||||
if (params.allowedPaths && !params.allowedPaths.has(target.path)) {
|
||||
continue;
|
||||
}
|
||||
@@ -289,7 +327,7 @@ function classifyConfiguredTargetRefs(params: {
|
||||
sourceConfig: params.config,
|
||||
env: process.env,
|
||||
});
|
||||
collectConfigAssignments({
|
||||
commandSecretGatewayDeps.collectConfigAssignments({
|
||||
config: structuredClone(params.config),
|
||||
context,
|
||||
});
|
||||
@@ -394,13 +432,13 @@ async function resolveCommandSecretRefsLocally(params: {
|
||||
env: process.env,
|
||||
});
|
||||
const localResolutionDiagnostics: string[] = [];
|
||||
const discoveredTargets = discoverConfigSecretTargetsByIds(sourceConfig, params.targetIds).filter(
|
||||
(target) => !params.allowedPaths || params.allowedPaths.has(target.path),
|
||||
);
|
||||
const discoveredTargets = commandSecretGatewayDeps
|
||||
.discoverConfigSecretTargetsByIds(sourceConfig, params.targetIds)
|
||||
.filter((target) => !params.allowedPaths || params.allowedPaths.has(target.path));
|
||||
const runtimeWebTargets = discoveredTargets.filter((target) =>
|
||||
targetsRuntimeWebPath(target.path),
|
||||
);
|
||||
collectConfigAssignments({
|
||||
commandSecretGatewayDeps.collectConfigAssignments({
|
||||
config: structuredClone(params.config),
|
||||
context,
|
||||
});
|
||||
@@ -412,7 +450,7 @@ async function resolveCommandSecretRefsLocally(params: {
|
||||
!runtimeWebTargets.every((target) => isDirectRuntimeWebTargetPath(target.path))
|
||||
) {
|
||||
try {
|
||||
await resolveRuntimeWebTools({
|
||||
await commandSecretGatewayDeps.resolveRuntimeWebTools({
|
||||
sourceConfig,
|
||||
resolvedConfig,
|
||||
context,
|
||||
@@ -474,7 +512,7 @@ async function resolveCommandSecretRefsLocally(params: {
|
||||
localResolutionDiagnostics,
|
||||
});
|
||||
}
|
||||
const analyzed = analyzeCommandSecretAssignmentsFromSnapshot({
|
||||
const analyzed = commandSecretGatewayDeps.analyzeCommandSecretAssignmentsFromSnapshot({
|
||||
sourceConfig,
|
||||
resolvedConfig,
|
||||
targetIds: params.targetIds,
|
||||
@@ -728,7 +766,7 @@ export async function resolveCommandSecretRefsViaGateway(params: {
|
||||
parsed.inactiveRefPaths.length > 0
|
||||
? new Set(parsed.inactiveRefPaths)
|
||||
: collectInactiveSurfacePathsFromDiagnostics(parsed.diagnostics);
|
||||
const analyzed = analyzeCommandSecretAssignmentsFromSnapshot({
|
||||
const analyzed = commandSecretGatewayDeps.analyzeCommandSecretAssignmentsFromSnapshot({
|
||||
sourceConfig: params.config,
|
||||
resolvedConfig,
|
||||
targetIds: params.targetIds,
|
||||
|
||||
Reference in New Issue
Block a user