test: stabilize agent auth and config suites

This commit is contained in:
Peter Steinberger
2026-04-06 19:51:48 +01:00
parent aaa5dea358
commit 134ff61754
37 changed files with 853 additions and 512 deletions

View File

@@ -2,8 +2,7 @@ import {
defineBundledChannelEntry,
loadBundledEntryExportSync,
} from "openclaw/plugin-sdk/channel-entry-contract";
import type { PluginRuntime } from "./api.js";
import type { ResolvedNostrAccount } from "./api.js";
import type { PluginRuntime, ResolvedNostrAccount } from "./api.js";
function createNostrProfileHttpHandler() {
return loadBundledEntryExportSync<

View File

@@ -6,10 +6,16 @@ const resolveExternalAuthProfilesWithPluginsMock = vi.fn<
(params: unknown) => ProviderExternalAuthProfile[]
>(() => []);
vi.mock("../../plugins/provider-runtime.js", () => ({
resolveExternalAuthProfilesWithPlugins: (params: unknown) =>
resolveExternalAuthProfilesWithPluginsMock(params),
}));
vi.mock("../../plugins/provider-runtime.js", async () => {
const actual = await vi.importActual<typeof import("../../plugins/provider-runtime.js")>(
"../../plugins/provider-runtime.js",
);
return {
...actual,
resolveExternalAuthProfilesWithPlugins: (params: unknown) =>
resolveExternalAuthProfilesWithPluginsMock(params),
};
});
function createStore(profiles: AuthProfileStore["profiles"] = {}): AuthProfileStore {
return { version: 1, profiles };

View File

@@ -1,9 +1,11 @@
import { beforeEach, describe, expect, it } from "vitest";
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../config/config.js";
import type { CliBackendConfig } from "../config/types.js";
import { createEmptyPluginRegistry } from "../plugins/registry.js";
import { setActivePluginRegistry } from "../plugins/runtime.js";
import { normalizeClaudeBackendConfig, resolveCliBackendConfig } from "./cli-backends.js";
let createEmptyPluginRegistry: typeof import("../plugins/registry.js").createEmptyPluginRegistry;
let setActivePluginRegistry: typeof import("../plugins/runtime.js").setActivePluginRegistry;
let normalizeClaudeBackendConfig: typeof import("./cli-backends.js").normalizeClaudeBackendConfig;
let resolveCliBackendConfig: typeof import("./cli-backends.js").resolveCliBackendConfig;
function createBackendEntry(params: {
pluginId: string;
@@ -24,7 +26,13 @@ function createBackendEntry(params: {
};
}
beforeEach(() => {
beforeEach(async () => {
vi.doUnmock("../plugins/setup-registry.js");
vi.doUnmock("../plugins/cli-backends.runtime.js");
vi.resetModules();
({ createEmptyPluginRegistry } = await import("../plugins/registry.js"));
({ setActivePluginRegistry } = await import("../plugins/runtime.js"));
({ normalizeClaudeBackendConfig, resolveCliBackendConfig } = await import("./cli-backends.js"));
const registry = createEmptyPluginRegistry();
registry.cliBackends = [
createBackendEntry({

View File

@@ -20,8 +20,18 @@ describe("updateSessionStoreAfterAgentRun", () => {
await fs.rm(tmpDir, { recursive: true, force: true });
});
it("persists claude-cli session bindings without explicit cliBackends config", async () => {
const cfg = {} as OpenClawConfig;
it("persists claude-cli session bindings when the backend is configured", async () => {
const cfg = {
agents: {
defaults: {
cliBackends: {
"claude-cli": {
command: "claude",
},
},
},
},
} as OpenClawConfig;
const sessionKey = "agent:main:explicit:test-claude-cli";
const sessionId = "test-openclaw-session";
const sessionStore: Record<string, SessionEntry> = {

View File

@@ -1,11 +1,15 @@
import { afterEach, describe, expect, it } from "vitest";
import { collectProviderApiKeys } from "./live-auth-keys.js";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
const ORIGINAL_MODELSTUDIO_API_KEY = process.env.MODELSTUDIO_API_KEY;
const ORIGINAL_XAI_API_KEY = process.env.XAI_API_KEY;
describe("collectProviderApiKeys", () => {
beforeEach(() => {
vi.doUnmock("../plugins/manifest-registry.js");
});
afterEach(() => {
vi.resetModules();
if (ORIGINAL_MODELSTUDIO_API_KEY === undefined) {
delete process.env.MODELSTUDIO_API_KEY;
} else {
@@ -18,14 +22,18 @@ describe("collectProviderApiKeys", () => {
}
});
it("honors manifest-declared provider auth env vars for nonstandard provider ids", () => {
it("honors manifest-declared provider auth env vars for nonstandard provider ids", async () => {
process.env.MODELSTUDIO_API_KEY = "modelstudio-live-key";
vi.resetModules();
const { collectProviderApiKeys } = await import("./live-auth-keys.js");
expect(collectProviderApiKeys("alibaba")).toContain("modelstudio-live-key");
});
it("dedupes manifest env vars against direct provider env naming", () => {
it("dedupes manifest env vars against direct provider env naming", async () => {
process.env.XAI_API_KEY = "xai-live-key";
vi.resetModules();
const { collectProviderApiKeys } = await import("./live-auth-keys.js");
expect(collectProviderApiKeys("xai")).toEqual(["xai-live-key"]);
});

View File

@@ -1,5 +1,14 @@
import { describe, expect, it } from "vitest";
import { createLiveTargetMatcher } from "./live-target-matcher.js";
import { beforeAll, describe, expect, it, vi } from "vitest";
type CreateLiveTargetMatcher = typeof import("./live-target-matcher.js").createLiveTargetMatcher;
let createLiveTargetMatcher: CreateLiveTargetMatcher;
beforeAll(async () => {
vi.doUnmock("../plugins/providers.js");
vi.doUnmock("../plugins/manifest-registry.js");
vi.resetModules();
({ createLiveTargetMatcher } = await import("./live-target-matcher.js"));
});
describe("createLiveTargetMatcher", () => {
it("matches Anthropic-owned models for the claude-cli provider filter", () => {

View File

@@ -1,41 +1,58 @@
import { describe, expect, it } from "vitest";
import { listKnownProviderEnvApiKeyNames } from "./model-auth-env-vars.js";
import {
GCP_VERTEX_CREDENTIALS_MARKER,
isKnownEnvApiKeyMarker,
isNonSecretApiKeyMarker,
NON_ENV_SECRETREF_MARKER,
resolveOAuthApiKeyMarker,
} from "./model-auth-markers.js";
import { beforeEach, describe, expect, it, vi } from "vitest";
async function loadMarkerModules() {
vi.doUnmock("../plugins/manifest-registry.js");
vi.resetModules();
return Promise.all([import("./model-auth-env-vars.js"), import("./model-auth-markers.js")]);
}
beforeEach(() => {
vi.doUnmock("../plugins/manifest-registry.js");
});
describe("model auth markers", () => {
it("recognizes explicit non-secret markers", () => {
it("recognizes explicit non-secret markers", async () => {
const [
,
{
GCP_VERTEX_CREDENTIALS_MARKER,
NON_ENV_SECRETREF_MARKER,
isNonSecretApiKeyMarker,
resolveOAuthApiKeyMarker,
},
] = await loadMarkerModules();
expect(isNonSecretApiKeyMarker(NON_ENV_SECRETREF_MARKER)).toBe(true);
expect(isNonSecretApiKeyMarker(resolveOAuthApiKeyMarker("chutes"))).toBe(true);
expect(isNonSecretApiKeyMarker("ollama-local")).toBe(true);
expect(isNonSecretApiKeyMarker(GCP_VERTEX_CREDENTIALS_MARKER)).toBe(true);
});
it("does not treat removed provider markers as active auth markers", () => {
it("does not treat removed provider markers as active auth markers", async () => {
const [, { isNonSecretApiKeyMarker }] = await loadMarkerModules();
expect(isNonSecretApiKeyMarker("qwen-oauth")).toBe(false);
});
it("recognizes known env marker names but not arbitrary all-caps keys", () => {
it("recognizes known env marker names but not arbitrary all-caps keys", async () => {
const [, { isNonSecretApiKeyMarker }] = await loadMarkerModules();
expect(isNonSecretApiKeyMarker("OPENAI_API_KEY")).toBe(true);
expect(isNonSecretApiKeyMarker("ALLCAPS_EXAMPLE")).toBe(false);
});
it("recognizes all built-in provider env marker names", () => {
it("recognizes all built-in provider env marker names", async () => {
const [{ listKnownProviderEnvApiKeyNames }, { isNonSecretApiKeyMarker }] =
await loadMarkerModules();
for (const envVarName of listKnownProviderEnvApiKeyNames()) {
expect(isNonSecretApiKeyMarker(envVarName)).toBe(true);
}
});
it("can exclude env marker-name interpretation for display-only paths", () => {
it("can exclude env marker-name interpretation for display-only paths", async () => {
const [, { isNonSecretApiKeyMarker }] = await loadMarkerModules();
expect(isNonSecretApiKeyMarker("OPENAI_API_KEY", { includeEnvVarName: false })).toBe(false);
});
it("excludes aws-sdk env markers from known api key env marker helper", () => {
it("excludes aws-sdk env markers from known api key env marker helper", async () => {
const [, { isKnownEnvApiKeyMarker }] = await loadMarkerModules();
expect(isKnownEnvApiKeyMarker("OPENAI_API_KEY")).toBe(true);
expect(isKnownEnvApiKeyMarker("AWS_PROFILE")).toBe(false);
});

View File

@@ -12,53 +12,62 @@ import {
resolveEnvApiKey,
} from "./model-auth.js";
vi.mock("../plugins/provider-runtime.js", () => ({
buildProviderMissingAuthMessageWithPlugin: (params: {
provider: string;
context: { listProfileIds: (providerId: string) => string[] };
}) => {
if (params.provider === "openai" && params.context.listProfileIds("openai-codex").length > 0) {
return 'No API key found for provider "openai". Use openai-codex/gpt-5.4.';
}
return undefined;
},
formatProviderAuthProfileApiKeyWithPlugin: async () => undefined,
refreshProviderOAuthCredentialWithPlugin: async () => null,
resolveExternalAuthProfilesWithPlugins: () => [],
resolveProviderSyntheticAuthWithPlugin: (params: {
provider: string;
context: { providerConfig?: { api?: string; baseUrl?: string; models?: unknown[] } };
}) => {
if (params.provider !== "ollama" && params.provider !== "demo-local") {
vi.mock("../plugins/provider-runtime.js", async () => {
const actual = await vi.importActual<typeof import("../plugins/provider-runtime.js")>(
"../plugins/provider-runtime.js",
);
return {
...actual,
buildProviderMissingAuthMessageWithPlugin: (params: {
provider: string;
context: { listProfileIds: (providerId: string) => string[] };
}) => {
if (
params.provider === "openai" &&
params.context.listProfileIds("openai-codex").length > 0
) {
return 'No API key found for provider "openai". Use openai-codex/gpt-5.4.';
}
return undefined;
}
const providerConfig = params.context.providerConfig;
const hasApiConfig =
Boolean(providerConfig?.api?.trim()) ||
Boolean(providerConfig?.baseUrl?.trim()) ||
(Array.isArray(providerConfig?.models) && providerConfig.models.length > 0);
if (!hasApiConfig) {
return undefined;
}
return {
apiKey: params.provider === "ollama" ? "ollama-local" : "demo-local",
source: `models.providers.${params.provider} (synthetic local key)`,
mode: "api-key" as const,
};
},
shouldDeferProviderSyntheticProfileAuthWithPlugin: (params: {
provider: string;
context: { resolvedApiKey?: string };
}) => {
const expectedMarker =
params.provider === "ollama"
? "ollama-local"
: params.provider === "demo-local"
? "demo-local"
: undefined;
return Boolean(expectedMarker && params.context.resolvedApiKey?.trim() === expectedMarker);
},
}));
},
formatProviderAuthProfileApiKeyWithPlugin: async () => undefined,
refreshProviderOAuthCredentialWithPlugin: async () => null,
resolveExternalAuthProfilesWithPlugins: () => [],
resolveProviderSyntheticAuthWithPlugin: (params: {
provider: string;
context: { providerConfig?: { api?: string; baseUrl?: string; models?: unknown[] } };
}) => {
if (params.provider !== "ollama" && params.provider !== "demo-local") {
return undefined;
}
const providerConfig = params.context.providerConfig;
const hasApiConfig =
Boolean(providerConfig?.api?.trim()) ||
Boolean(providerConfig?.baseUrl?.trim()) ||
(Array.isArray(providerConfig?.models) && providerConfig.models.length > 0);
if (!hasApiConfig) {
return undefined;
}
return {
apiKey: params.provider === "ollama" ? "ollama-local" : "demo-local",
source: `models.providers.${params.provider} (synthetic local key)`,
mode: "api-key" as const,
};
},
shouldDeferProviderSyntheticProfileAuthWithPlugin: (params: {
provider: string;
context: { resolvedApiKey?: string };
}) => {
const expectedMarker =
params.provider === "ollama"
? "ollama-local"
: params.provider === "demo-local"
? "demo-local"
: undefined;
return Boolean(expectedMarker && params.context.resolvedApiKey?.trim() === expectedMarker);
},
};
});
vi.mock("./cli-credentials.js", () => ({
readCodexCliCredentialsCached: () => null,

View File

@@ -20,90 +20,96 @@ import {
resolveUsableCustomProviderApiKey,
} from "./model-auth.js";
vi.mock("../plugins/provider-runtime.js", () => ({
buildProviderMissingAuthMessageWithPlugin: () => undefined,
resolveExternalAuthProfilesWithPlugins: () => [],
shouldDeferProviderSyntheticProfileAuthWithPlugin: (params: {
provider: string;
context: { resolvedApiKey?: string };
}) => params.provider === "ollama" && params.context.resolvedApiKey?.trim() === "ollama-local",
resolveProviderSyntheticAuthWithPlugin: (params: {
provider: string;
config?: {
plugins?: {
enabled?: boolean;
entries?: {
xai?: {
enabled?: boolean;
config?: {
webSearch?: {
vi.mock("../plugins/provider-runtime.js", async () => {
const actual = await vi.importActual<typeof import("../plugins/provider-runtime.js")>(
"../plugins/provider-runtime.js",
);
return {
...actual,
buildProviderMissingAuthMessageWithPlugin: () => undefined,
resolveExternalAuthProfilesWithPlugins: () => [],
shouldDeferProviderSyntheticProfileAuthWithPlugin: (params: {
provider: string;
context: { resolvedApiKey?: string };
}) => params.provider === "ollama" && params.context.resolvedApiKey?.trim() === "ollama-local",
resolveProviderSyntheticAuthWithPlugin: (params: {
provider: string;
config?: {
plugins?: {
enabled?: boolean;
entries?: {
xai?: {
enabled?: boolean;
config?: {
webSearch?: {
apiKey?: unknown;
};
};
};
};
};
tools?: {
web?: {
search?: {
grok?: {
apiKey?: unknown;
};
};
};
};
};
tools?: {
web?: {
search?: {
grok?: {
apiKey?: unknown;
};
context: { providerConfig?: { api?: string; baseUrl?: string; models?: unknown[] } };
}) => {
if (params.provider === "xai") {
if (
params.config?.plugins?.enabled === false ||
params.config?.plugins?.entries?.xai?.enabled === false
) {
return undefined;
}
const pluginApiKey = params.config?.plugins?.entries?.xai?.config?.webSearch?.apiKey;
if (typeof pluginApiKey === "string" && pluginApiKey.trim()) {
return {
apiKey: pluginApiKey.trim(),
source: "plugins.entries.xai.config.webSearch.apiKey",
mode: "api-key" as const,
};
};
};
};
context: { providerConfig?: { api?: string; baseUrl?: string; models?: unknown[] } };
}) => {
if (params.provider === "xai") {
if (
params.config?.plugins?.enabled === false ||
params.config?.plugins?.entries?.xai?.enabled === false
) {
}
if (pluginApiKey && typeof pluginApiKey === "object") {
return {
apiKey: NON_ENV_SECRETREF_MARKER,
source: "plugins.entries.xai.config.webSearch.apiKey",
mode: "api-key" as const,
};
}
return undefined;
}
const pluginApiKey = params.config?.plugins?.entries?.xai?.config?.webSearch?.apiKey;
if (typeof pluginApiKey === "string" && pluginApiKey.trim()) {
if (params.provider === "claude-cli") {
return {
apiKey: pluginApiKey.trim(),
source: "plugins.entries.xai.config.webSearch.apiKey",
mode: "api-key" as const,
apiKey: "claude-cli-access-token",
source: "Claude CLI native auth",
mode: "oauth" as const,
};
}
if (pluginApiKey && typeof pluginApiKey === "object") {
return {
apiKey: NON_ENV_SECRETREF_MARKER,
source: "plugins.entries.xai.config.webSearch.apiKey",
mode: "api-key" as const,
};
if (params.provider !== "ollama") {
return undefined;
}
const providerConfig = params.context.providerConfig;
const hasApiConfig =
Boolean(providerConfig?.api?.trim()) ||
Boolean(providerConfig?.baseUrl?.trim()) ||
(Array.isArray(providerConfig?.models) && providerConfig.models.length > 0);
if (!hasApiConfig) {
return undefined;
}
return undefined;
}
if (params.provider === "claude-cli") {
return {
apiKey: "claude-cli-access-token",
source: "Claude CLI native auth",
mode: "oauth" as const,
apiKey: "ollama-local",
source: "models.providers.ollama (synthetic local key)",
mode: "api-key" as const,
};
}
if (params.provider !== "ollama") {
return undefined;
}
const providerConfig = params.context.providerConfig;
const hasApiConfig =
Boolean(providerConfig?.api?.trim()) ||
Boolean(providerConfig?.baseUrl?.trim()) ||
(Array.isArray(providerConfig?.models) && providerConfig.models.length > 0);
if (!hasApiConfig) {
return undefined;
}
return {
apiKey: "ollama-local",
source: "models.providers.ollama (synthetic local key)",
mode: "api-key" as const,
};
},
}));
},
};
});
afterEach(() => {
clearRuntimeConfigSnapshot();

View File

@@ -5,9 +5,15 @@ const providerRuntimeMocks = vi.hoisted(() => ({
resolveProviderModernModelRef: vi.fn(),
}));
vi.mock("../plugins/provider-runtime.js", () => ({
resolveProviderModernModelRef: providerRuntimeMocks.resolveProviderModernModelRef,
}));
vi.mock("../plugins/provider-runtime.js", async () => {
const actual = await vi.importActual<typeof import("../plugins/provider-runtime.js")>(
"../plugins/provider-runtime.js",
);
return {
...actual,
resolveProviderModernModelRef: providerRuntimeMocks.resolveProviderModernModelRef,
};
});
import { normalizeModelCompat } from "../plugins/provider-model-compat.js";
import {

View File

@@ -1,8 +1,18 @@
import { describe, expect, it } from "vitest";
import { resolveMissingProviderApiKey } from "./models-config.providers.secrets.js";
import { beforeEach, describe, expect, it, vi } from "vitest";
async function loadSecretsModule() {
vi.doUnmock("../plugins/manifest-registry.js");
vi.resetModules();
return import("./models-config.providers.secrets.js");
}
beforeEach(() => {
vi.doUnmock("../plugins/manifest-registry.js");
});
describe("models-config", () => {
it("fills missing provider.apiKey from env var name when models exist", () => {
it("fills missing provider.apiKey from env var name when models exist", async () => {
const { resolveMissingProviderApiKey } = await loadSecretsModule();
const provider = resolveMissingProviderApiKey({
providerKey: "minimax",
provider: {

View File

@@ -1,13 +1,17 @@
import { describe, expect, it } from "vitest";
import { NON_ENV_SECRETREF_MARKER } from "./model-auth-markers.js";
import {
mergeProviderModels,
mergeProviders,
mergeWithExistingProviderSecrets,
type ExistingProviderConfig,
} from "./models-config.merge.js";
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { ExistingProviderConfig } from "./models-config.merge.js";
import type { ProviderConfig } from "./models-config.providers.secrets.js";
async function loadMergeModules() {
vi.doUnmock("../plugins/manifest-registry.js");
vi.resetModules();
return Promise.all([import("./model-auth-markers.js"), import("./models-config.merge.js")]);
}
beforeEach(() => {
vi.doUnmock("../plugins/manifest-registry.js");
});
describe("models-config merge helpers", () => {
const preservedApiKey = "AGENT_KEY"; // pragma: allowlist secret
const configApiKey = "CONFIG_KEY"; // pragma: allowlist secret
@@ -46,7 +50,8 @@ describe("models-config merge helpers", () => {
} as ExistingProviderConfig;
}
it("refreshes implicit model metadata while preserving explicit reasoning overrides", () => {
it("refreshes implicit model metadata while preserving explicit reasoning overrides", async () => {
const [, { mergeProviderModels }] = await loadMergeModules();
const merged = mergeProviderModels(
{
api: "openai-responses",
@@ -89,7 +94,8 @@ describe("models-config merge helpers", () => {
]);
});
it("merges explicit providers onto trimmed keys", () => {
it("merges explicit providers onto trimmed keys", async () => {
const [, { mergeProviders }] = await loadMergeModules();
const merged = mergeProviders({
explicit: {
" custom ": {
@@ -104,7 +110,8 @@ describe("models-config merge helpers", () => {
});
});
it("keeps existing providers alongside newly configured providers in merge mode", () => {
it("keeps existing providers alongside newly configured providers in merge mode", async () => {
const [, { mergeWithExistingProviderSecrets }] = await loadMergeModules();
const merged = mergeWithExistingProviderSecrets({
nextProviders: {
"custom-proxy": {
@@ -129,7 +136,8 @@ describe("models-config merge helpers", () => {
expect(merged["custom-proxy"]?.baseUrl).toBe("http://localhost:4000/v1");
});
it("preserves non-empty existing apiKey while explicit baseUrl wins", () => {
it("preserves non-empty existing apiKey while explicit baseUrl wins", async () => {
const [, { mergeWithExistingProviderSecrets }] = await loadMergeModules();
const merged = mergeWithExistingProviderSecrets({
nextProviders: {
custom: createConfigProvider(),
@@ -145,7 +153,8 @@ describe("models-config merge helpers", () => {
expect(merged.custom?.baseUrl).toBe("https://config.example/v1");
});
it("preserves existing apiKey after explicit provider key normalization", () => {
it("preserves existing apiKey after explicit provider key normalization", async () => {
const [, { mergeProviders, mergeWithExistingProviderSecrets }] = await loadMergeModules();
const normalized = mergeProviders({
explicit: {
" custom ": createConfigProvider(),
@@ -164,7 +173,8 @@ describe("models-config merge helpers", () => {
expect(merged.custom?.baseUrl).toBe("https://config.example/v1");
});
it("preserves implicit provider headers when explicit config adds extra headers", () => {
it("preserves implicit provider headers when explicit config adds extra headers", async () => {
const [, { mergeProviderModels }] = await loadMergeModules();
const merged = mergeProviderModels(
{
baseUrl: "https://api.example.com",
@@ -200,7 +210,8 @@ describe("models-config merge helpers", () => {
});
});
it("replaces stale baseUrl when model api surface changes", () => {
it("replaces stale baseUrl when model api surface changes", async () => {
const [, { mergeWithExistingProviderSecrets }] = await loadMergeModules();
const merged = mergeWithExistingProviderSecrets({
nextProviders: {
custom: {
@@ -227,7 +238,8 @@ describe("models-config merge helpers", () => {
);
});
it("replaces stale baseUrl when only model-level apis change", () => {
it("replaces stale baseUrl when only model-level apis change", async () => {
const [, { mergeWithExistingProviderSecrets }] = await loadMergeModules();
const nextProvider = createConfigProvider();
delete (nextProvider as { api?: string }).api;
nextProvider.models = [createModel({ api: "openai-responses" })];
@@ -250,7 +262,8 @@ describe("models-config merge helpers", () => {
expect(merged.custom?.baseUrl).toBe("https://config.example/v1");
});
it("does not preserve stale plaintext apiKey when next entry is a marker", () => {
it("does not preserve stale plaintext apiKey when next entry is a marker", async () => {
const [, { mergeWithExistingProviderSecrets }] = await loadMergeModules();
const merged = mergeWithExistingProviderSecrets({
nextProviders: {
custom: {
@@ -271,7 +284,9 @@ describe("models-config merge helpers", () => {
expect(merged.custom?.apiKey).toBe("OPENAI_API_KEY"); // pragma: allowlist secret
});
it("does not preserve a stale non-env marker when config returns to plaintext", () => {
it("does not preserve a stale non-env marker when config returns to plaintext", async () => {
const [{ NON_ENV_SECRETREF_MARKER }, { mergeWithExistingProviderSecrets }] =
await loadMergeModules();
const merged = mergeWithExistingProviderSecrets({
nextProviders: {
custom: createConfigProvider({ apiKey: "ALLCAPS_SAMPLE" }), // pragma: allowlist secret
@@ -289,7 +304,8 @@ describe("models-config merge helpers", () => {
expect(merged.custom?.baseUrl).toBe("https://config.example/v1");
});
it("uses config apiKey/baseUrl when existing values are empty", () => {
it("uses config apiKey/baseUrl when existing values are empty", async () => {
const [, { mergeWithExistingProviderSecrets }] = await loadMergeModules();
const merged = mergeWithExistingProviderSecrets({
nextProviders: {
custom: createConfigProvider(),

View File

@@ -1,7 +1,16 @@
import { describe, expect, it } from "vitest";
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { ModelProviderConfig } from "../config/types.models.js";
import { applyProviderNativeStreamingUsageCompat } from "../plugin-sdk/provider-catalog-shared.js";
import { resolveMissingProviderApiKey } from "./models-config.providers.secrets.js";
async function loadSecretsModule() {
vi.doUnmock("../plugins/manifest-registry.js");
vi.resetModules();
return import("./models-config.providers.secrets.js");
}
beforeEach(() => {
vi.doUnmock("../plugins/manifest-registry.js");
});
const MOONSHOT_BASE_URL = "https://api.moonshot.ai/v1";
const MOONSHOT_CN_BASE_URL = "https://api.moonshot.cn/v1";
@@ -57,7 +66,8 @@ describe("moonshot implicit provider (#33637)", () => {
).toBeUndefined();
});
it("includes moonshot when MOONSHOT_API_KEY is configured", () => {
it("includes moonshot when MOONSHOT_API_KEY is configured", async () => {
const { resolveMissingProviderApiKey } = await loadSecretsModule();
const provider = resolveMissingProviderApiKey({
providerKey: "moonshot",
provider: buildMoonshotProvider(),

View File

@@ -1,10 +1,13 @@
import { describe, expect, it, vi } from "vitest";
import {
normalizeProviderSpecificConfig,
resolveProviderConfigApiKeyResolver,
} from "./models-config.providers.policy.js";
import { beforeAll, describe, expect, it, vi } from "vitest";
type NormalizeProviderSpecificConfig =
typeof import("./models-config.providers.policy.js").normalizeProviderSpecificConfig;
type ResolveProviderConfigApiKeyResolver =
typeof import("./models-config.providers.policy.js").resolveProviderConfigApiKeyResolver;
const GOOGLE_BASE_URL = "https://generativelanguage.googleapis.com";
let normalizeProviderSpecificConfig: NormalizeProviderSpecificConfig;
let resolveProviderConfigApiKeyResolver: ResolveProviderConfigApiKeyResolver;
vi.mock("../plugins/provider-runtime.js", () => ({
applyProviderNativeStreamingUsageCompatWithPlugin: () => undefined,
@@ -43,6 +46,11 @@ vi.mock("../plugins/provider-runtime.js", () => ({
},
}));
beforeAll(async () => {
({ normalizeProviderSpecificConfig, resolveProviderConfigApiKeyResolver } =
await import("./models-config.providers.policy.js"));
});
describe("models-config.providers.policy", () => {
it("resolves config apiKey markers through provider plugin hooks", async () => {
const env = {

View File

@@ -1,8 +1,18 @@
import { describe, expect, it } from "vitest";
import { createProviderAuthResolver } from "./models-config.providers.secrets.js";
import { beforeEach, describe, expect, it, vi } from "vitest";
async function loadSecretsModule() {
vi.doUnmock("../plugins/manifest-registry.js");
vi.resetModules();
return import("./models-config.providers.secrets.js");
}
beforeEach(() => {
vi.doUnmock("../plugins/manifest-registry.js");
});
describe("Qianfan provider", () => {
it("resolves QIANFAN_API_KEY markers through provider auth lookup", () => {
it("resolves QIANFAN_API_KEY markers through provider auth lookup", async () => {
const { createProviderAuthResolver } = await loadSecretsModule();
const resolveAuth = createProviderAuthResolver(
{
QIANFAN_API_KEY: "test-key", // pragma: allowlist secret

View File

@@ -1,9 +1,21 @@
import { describe, expect, it } from "vitest";
import { NON_ENV_SECRETREF_MARKER } from "./model-auth-markers.js";
import { createProviderAuthResolver } from "./models-config.providers.secrets.js";
import { beforeEach, describe, expect, it, vi } from "vitest";
async function loadModules() {
vi.doUnmock("../plugins/manifest-registry.js");
vi.resetModules();
return Promise.all([
import("./model-auth-markers.js"),
import("./models-config.providers.secrets.js"),
]);
}
beforeEach(() => {
vi.doUnmock("../plugins/manifest-registry.js");
});
describe("vercel-ai-gateway provider resolution", () => {
it("resolves AI_GATEWAY_API_KEY through provider auth lookup", () => {
it("resolves AI_GATEWAY_API_KEY through provider auth lookup", async () => {
const [, { createProviderAuthResolver }] = await loadModules();
const resolveAuth = createProviderAuthResolver(
{
AI_GATEWAY_API_KEY: "vercel-gateway-test-key", // pragma: allowlist secret
@@ -18,7 +30,8 @@ describe("vercel-ai-gateway provider resolution", () => {
});
});
it("prefers env keyRef markers over runtime plaintext in auth profiles", () => {
it("prefers env keyRef markers over runtime plaintext in auth profiles", async () => {
const [, { createProviderAuthResolver }] = await loadModules();
const resolveAuth = createProviderAuthResolver({} as NodeJS.ProcessEnv, {
version: 1,
profiles: {
@@ -39,7 +52,8 @@ describe("vercel-ai-gateway provider resolution", () => {
});
});
it("uses non-env markers for non-env keyRef vercel profiles", () => {
it("uses non-env markers for non-env keyRef vercel profiles", async () => {
const [{ NON_ENV_SECRETREF_MARKER }, { createProviderAuthResolver }] = await loadModules();
const resolveAuth = createProviderAuthResolver({} as NodeJS.ProcessEnv, {
version: 1,
profiles: {

View File

@@ -1,8 +1,18 @@
import { describe, expect, it } from "vitest";
import { createProviderAuthResolver } from "./models-config.providers.secrets.js";
import { beforeEach, describe, expect, it, vi } from "vitest";
async function loadSecretsModule() {
vi.doUnmock("../plugins/manifest-registry.js");
vi.resetModules();
return import("./models-config.providers.secrets.js");
}
beforeEach(() => {
vi.doUnmock("../plugins/manifest-registry.js");
});
describe("Volcengine and BytePlus providers", () => {
it("shares VOLCANO_ENGINE_API_KEY across volcengine auth aliases", () => {
it("shares VOLCANO_ENGINE_API_KEY across volcengine auth aliases", async () => {
const { createProviderAuthResolver } = await loadSecretsModule();
const resolveAuth = createProviderAuthResolver(
{
VOLCANO_ENGINE_API_KEY: "test-key", // pragma: allowlist secret
@@ -22,7 +32,8 @@ describe("Volcengine and BytePlus providers", () => {
});
});
it("shares BYTEPLUS_API_KEY across byteplus auth aliases", () => {
it("shares BYTEPLUS_API_KEY across byteplus auth aliases", async () => {
const { createProviderAuthResolver } = await loadSecretsModule();
const resolveAuth = createProviderAuthResolver(
{
BYTEPLUS_API_KEY: "test-key", // pragma: allowlist secret
@@ -42,7 +53,8 @@ describe("Volcengine and BytePlus providers", () => {
});
});
it("reuses env keyRef markers from auth profiles for paired providers", () => {
it("reuses env keyRef markers from auth profiles for paired providers", async () => {
const { createProviderAuthResolver } = await loadSecretsModule();
const resolveAuth = createProviderAuthResolver({} as NodeJS.ProcessEnv, {
version: 1,
profiles: {

View File

@@ -9,14 +9,20 @@ import {
withTempEnv,
} from "./models-config.e2e-harness.js";
vi.mock("../plugins/provider-runtime.js", () => ({
applyProviderConfigDefaultsWithPlugin: (config: OpenClawConfig) => config,
applyProviderNativeStreamingUsageCompatWithPlugin: () => undefined,
normalizeProviderConfigWithPlugin: () => undefined,
resetProviderRuntimeHookCacheForTest: () => undefined,
resolveProviderConfigApiKeyWithPlugin: () => undefined,
resolveProviderSyntheticAuthWithPlugin: () => undefined,
}));
vi.mock("../plugins/provider-runtime.js", async () => {
const actual = await vi.importActual<typeof import("../plugins/provider-runtime.js")>(
"../plugins/provider-runtime.js",
);
return {
...actual,
applyProviderConfigDefaultsWithPlugin: (config: OpenClawConfig) => config,
applyProviderNativeStreamingUsageCompatWithPlugin: () => undefined,
normalizeProviderConfigWithPlugin: () => undefined,
resetProviderRuntimeHookCacheForTest: () => undefined,
resolveProviderConfigApiKeyWithPlugin: () => undefined,
resolveProviderSyntheticAuthWithPlugin: () => undefined,
};
});
vi.mock("./models-config.providers.js", async () => {
const actual = await vi.importActual<typeof import("./models-config.providers.js")>(

View File

@@ -5,10 +5,16 @@ const hoisted = vi.hoisted(() => ({
matchesProviderContextOverflowWithPlugin: vi.fn(() => false),
}));
vi.mock("../../plugins/provider-runtime.js", () => ({
classifyProviderFailoverReasonWithPlugin: hoisted.classifyProviderFailoverReasonWithPlugin,
matchesProviderContextOverflowWithPlugin: hoisted.matchesProviderContextOverflowWithPlugin,
}));
vi.mock("../../plugins/provider-runtime.js", async () => {
const actual = await vi.importActual<typeof import("../../plugins/provider-runtime.js")>(
"../../plugins/provider-runtime.js",
);
return {
...actual,
classifyProviderFailoverReasonWithPlugin: hoisted.classifyProviderFailoverReasonWithPlugin,
matchesProviderContextOverflowWithPlugin: hoisted.matchesProviderContextOverflowWithPlugin,
};
});
import { classifyFailoverReason, isContextOverflowError } from "./errors.js";
import {

View File

@@ -13,11 +13,17 @@ vi.mock("./pi-embedded-helpers.js", async () => ({
sanitizeSessionMessagesImages: vi.fn(async (msgs) => msgs),
}));
vi.mock("../plugins/provider-runtime.js", () => ({
resolveProviderRuntimePlugin: vi.fn(() => undefined),
sanitizeProviderReplayHistoryWithPlugin: vi.fn(() => undefined),
validateProviderReplayTurnsWithPlugin: vi.fn(() => undefined),
}));
vi.mock("../plugins/provider-runtime.js", async () => {
const actual = await vi.importActual<typeof import("../plugins/provider-runtime.js")>(
"../plugins/provider-runtime.js",
);
return {
...actual,
resolveProviderRuntimePlugin: vi.fn(() => undefined),
sanitizeProviderReplayHistoryWithPlugin: vi.fn(() => undefined),
validateProviderReplayTurnsWithPlugin: vi.fn(() => undefined),
};
});
describe("sanitizeSessionHistory openai tool id preservation", () => {
let sanitizeSessionHistory: SanitizeSessionHistoryHarness["sanitizeSessionHistory"];

View File

@@ -14,11 +14,17 @@ vi.mock("./pi-embedded-helpers.js", async () => ({
sanitizeSessionMessagesImages: vi.fn(async (msgs) => msgs),
}));
vi.mock("../plugins/provider-runtime.js", () => ({
resolveProviderRuntimePlugin: vi.fn(() => undefined),
sanitizeProviderReplayHistoryWithPlugin: vi.fn(() => undefined),
validateProviderReplayTurnsWithPlugin: vi.fn(() => undefined),
}));
vi.mock("../plugins/provider-runtime.js", async () => {
const actual = await vi.importActual<typeof import("../plugins/provider-runtime.js")>(
"../plugins/provider-runtime.js",
);
return {
...actual,
resolveProviderRuntimePlugin: vi.fn(() => undefined),
sanitizeProviderReplayHistoryWithPlugin: vi.fn(() => undefined),
validateProviderReplayTurnsWithPlugin: vi.fn(() => undefined),
};
});
let sanitizeSessionHistory: SanitizeSessionHistoryHarness["sanitizeSessionHistory"];
let mockedHelpers: SanitizeSessionHistoryHarness["mockedHelpers"];

View File

@@ -25,68 +25,74 @@ vi.mock("./pi-embedded-helpers.js", async () => ({
sanitizeSessionMessagesImages: vi.fn(async (msgs) => msgs),
}));
vi.mock("../plugins/provider-runtime.js", () => ({
resolveProviderRuntimePlugin: ({ provider }: { provider?: string }) =>
provider === "openrouter" || provider === "github-copilot"
? {
buildReplayPolicy: (context?: { modelId?: string | null }) => {
const modelId = String(context?.modelId ?? "").toLowerCase();
if (provider === "openrouter") {
return {
applyAssistantFirstOrderingFix: false,
validateGeminiTurns: false,
validateAnthropicTurns: false,
...(modelId.includes("gemini")
? {
sanitizeThoughtSignatures: {
allowBase64Only: true,
includeCamelCase: true,
},
}
: {}),
};
}
if (provider === "github-copilot" && modelId.includes("claude")) {
return {
dropThinkingBlocks: true,
};
}
return undefined;
},
}
: undefined,
sanitizeProviderReplayHistoryWithPlugin: vi.fn(
async ({
provider,
context,
}: {
provider?: string;
context: {
messages: AgentMessage[];
sessionState?: {
appendCustomEntry(customType: string, data: unknown): void;
vi.mock("../plugins/provider-runtime.js", async () => {
const actual = await vi.importActual<typeof import("../plugins/provider-runtime.js")>(
"../plugins/provider-runtime.js",
);
return {
...actual,
resolveProviderRuntimePlugin: ({ provider }: { provider?: string }) =>
provider === "openrouter" || provider === "github-copilot"
? {
buildReplayPolicy: (context?: { modelId?: string | null }) => {
const modelId = String(context?.modelId ?? "").toLowerCase();
if (provider === "openrouter") {
return {
applyAssistantFirstOrderingFix: false,
validateGeminiTurns: false,
validateAnthropicTurns: false,
...(modelId.includes("gemini")
? {
sanitizeThoughtSignatures: {
allowBase64Only: true,
includeCamelCase: true,
},
}
: {}),
};
}
if (provider === "github-copilot" && modelId.includes("claude")) {
return {
dropThinkingBlocks: true,
};
}
return undefined;
},
}
: undefined,
sanitizeProviderReplayHistoryWithPlugin: vi.fn(
async ({
provider,
context,
}: {
provider?: string;
context: {
messages: AgentMessage[];
sessionState?: {
appendCustomEntry(customType: string, data: unknown): void;
};
};
};
}) => {
if (
provider &&
provider.startsWith("google") &&
context.messages[0]?.role === "assistant" &&
context.sessionState
) {
context.sessionState.appendCustomEntry("google-turn-ordering-bootstrap", {
timestamp: Date.now(),
});
return [
{ role: "user", content: "(session bootstrap)" } as AgentMessage,
...context.messages,
];
}
return context.messages;
},
),
validateProviderReplayTurnsWithPlugin: vi.fn(() => undefined),
}));
}) => {
if (
provider &&
provider.startsWith("google") &&
context.messages[0]?.role === "assistant" &&
context.sessionState
) {
context.sessionState.appendCustomEntry("google-turn-ordering-bootstrap", {
timestamp: Date.now(),
});
return [
{ role: "user", content: "(session bootstrap)" } as AgentMessage,
...context.messages,
];
}
return context.messages;
},
),
validateProviderReplayTurnsWithPlugin: vi.fn(() => undefined),
};
});
let sanitizeSessionHistory: SanitizeSessionHistoryFn;
let mockedHelpers: SanitizeSessionHistoryHarness["mockedHelpers"];

View File

@@ -1,23 +1,29 @@
import { describe, expect, it, vi } from "vitest";
vi.mock("../../plugins/provider-runtime.js", () => ({
resolveProviderCacheTtlEligibility: (params: {
context: { provider: string; modelId: string; modelApi?: string };
}) => {
if (params.context.provider === "anthropic") {
return true;
}
if (params.context.provider === "moonshot" || params.context.provider === "zai") {
return true;
}
if (params.context.provider === "openrouter") {
return ["anthropic/", "moonshot/", "moonshotai/", "zai/"].some((prefix) =>
params.context.modelId.startsWith(prefix),
);
}
return undefined;
},
}));
vi.mock("../../plugins/provider-runtime.js", async () => {
const actual = await vi.importActual<typeof import("../../plugins/provider-runtime.js")>(
"../../plugins/provider-runtime.js",
);
return {
...actual,
resolveProviderCacheTtlEligibility: (params: {
context: { provider: string; modelId: string; modelApi?: string };
}) => {
if (params.context.provider === "anthropic") {
return true;
}
if (params.context.provider === "moonshot" || params.context.provider === "zai") {
return true;
}
if (params.context.provider === "openrouter") {
return ["anthropic/", "moonshot/", "moonshotai/", "zai/"].some((prefix) =>
params.context.modelId.startsWith(prefix),
);
}
return undefined;
},
};
});
import { isCacheTtlEligibleProvider } from "./cache-ttl.js";

View File

@@ -3,16 +3,22 @@ import type { ModelProviderConfig } from "../../config/config.js";
import { discoverModels } from "../pi-model-discovery.js";
import { createProviderRuntimeTestMock } from "./model.provider-runtime.test-support.js";
vi.mock("../../plugins/provider-runtime.js", () => ({
applyProviderResolvedModelCompatWithPlugins: () => undefined,
applyProviderResolvedTransportWithPlugin: () => undefined,
buildProviderUnknownModelHintWithPlugin: () => undefined,
clearProviderRuntimeHookCache: () => {},
normalizeProviderTransportWithPlugin: () => undefined,
normalizeProviderResolvedModelWithPlugin: () => undefined,
prepareProviderDynamicModel: async () => {},
runProviderDynamicModel: () => undefined,
}));
vi.mock("../../plugins/provider-runtime.js", async () => {
const actual = await vi.importActual<typeof import("../../plugins/provider-runtime.js")>(
"../../plugins/provider-runtime.js",
);
return {
...actual,
applyProviderResolvedModelCompatWithPlugins: () => undefined,
applyProviderResolvedTransportWithPlugin: () => undefined,
buildProviderUnknownModelHintWithPlugin: () => undefined,
clearProviderRuntimeHookCache: () => {},
normalizeProviderTransportWithPlugin: () => undefined,
normalizeProviderResolvedModelWithPlugin: () => undefined,
prepareProviderDynamicModel: async () => {},
runProviderDynamicModel: () => undefined,
};
});
vi.mock("../model-suppression.js", () => ({
shouldSuppressBuiltInModel: ({ provider, id }: { provider?: string; id?: string }) =>

View File

@@ -102,6 +102,10 @@ describe("runEmbeddedAttempt context injection", () => {
contextEngine: {
assemble: async ({ messages }) => ({ messages, estimatedTokens: 1 }),
},
attemptOverrides: {
bootstrapContextMode: "full",
bootstrapContextRunKind: "default",
},
sessionKey: "agent:main",
tempPaths,
});

View File

@@ -144,7 +144,10 @@ export function getHoisted(): AttemptSpawnWorkspaceHoisted {
return hoisted;
}
vi.mock("@mariozechner/pi-coding-agent", () => {
vi.mock("@mariozechner/pi-coding-agent", async () => {
const actual = await vi.importActual<typeof import("@mariozechner/pi-coding-agent")>(
"@mariozechner/pi-coding-agent",
);
class AuthStorage {}
class DefaultResourceLoader {
async reload() {}
@@ -152,6 +155,7 @@ vi.mock("@mariozechner/pi-coding-agent", () => {
class ModelRegistry {}
return {
...actual,
AuthStorage,
createAgentSession: (...args: unknown[]) => hoisted.createAgentSessionMock(...args),
DefaultResourceLoader,
@@ -196,12 +200,18 @@ vi.mock("../../../infra/net/undici-global-dispatcher.js", () => ({
ensureGlobalUndiciStreamTimeouts: () => {},
}));
vi.mock("../../bootstrap-files.js", () => ({
makeBootstrapWarn: () => () => {},
resolveBootstrapContextForRun: hoisted.resolveBootstrapContextForRunMock,
resolveContextInjectionMode: hoisted.resolveContextInjectionModeMock,
hasCompletedBootstrapTurn: hoisted.hasCompletedBootstrapTurnMock,
}));
vi.mock("../../bootstrap-files.js", async () => {
const actual = await vi.importActual<typeof import("../../bootstrap-files.js")>(
"../../bootstrap-files.js",
);
return {
...actual,
makeBootstrapWarn: () => () => {},
resolveBootstrapContextForRun: hoisted.resolveBootstrapContextForRunMock,
resolveContextInjectionMode: hoisted.resolveContextInjectionModeMock,
hasCompletedBootstrapTurn: hoisted.hasCompletedBootstrapTurnMock,
};
});
vi.mock("../../skills.js", () => ({
applySkillEnvOverrides: () => () => {},
@@ -225,7 +235,9 @@ vi.mock("../../docs-path.js", () => ({
}));
vi.mock("../../pi-project-settings.js", () => ({
createPreparedEmbeddedPiSettingsManager: () => ({}),
createPreparedEmbeddedPiSettingsManager: () => ({
getCompactionReserveTokens: () => 0,
}),
}));
vi.mock("../../pi-settings.js", () => ({
@@ -265,12 +277,18 @@ vi.mock("../../session-write-lock.js", () => ({
resolveSessionLockMaxHoldFromTimeout: () => 1,
}));
vi.mock("../tool-result-context-guard.js", () => ({
formatContextLimitTruncationNotice: (truncatedChars: number) =>
`[... ${Math.max(1, Math.floor(truncatedChars))} more characters truncated]`,
installToolResultContextGuard: (...args: unknown[]) =>
(hoisted.installToolResultContextGuardMock as (...args: unknown[]) => unknown)(...args),
}));
vi.mock("../tool-result-context-guard.js", async () => {
const actual = await vi.importActual<typeof import("../tool-result-context-guard.js")>(
"../tool-result-context-guard.js",
);
return {
...actual,
formatContextLimitTruncationNotice: (truncatedChars: number) =>
`[... ${Math.max(1, Math.floor(truncatedChars))} more characters truncated]`,
installToolResultContextGuard: (...args: unknown[]) =>
(hoisted.installToolResultContextGuardMock as (...args: unknown[]) => unknown)(...args),
};
});
vi.mock("../wait-for-idle-before-flush.js", () => ({
flushPendingToolResultsAfterIdle: (...args: unknown[]) =>
@@ -806,9 +824,12 @@ export async function createContextEngineAttemptRunner(params: {
.mockReset()
.mockReturnValue({ messages: seedMessages });
hoisted.createAgentSessionMock.mockImplementation(async () => ({
session: createDefaultEmbeddedSession(),
}));
hoisted.createAgentSessionMock.mockImplementation(async () => {
const session = createDefaultEmbeddedSession();
session.messages = [...seedMessages];
session.agent.state.messages = [...seedMessages];
return { session };
});
return await (
await loadRunEmbeddedAttempt()

View File

@@ -115,8 +115,7 @@ describe("FS tools with workspaceOnly=false", () => {
"test-call-2",
{
path: outsideFile,
oldText: "old content",
newText: "new content",
edits: [{ oldText: "old content", newText: "new content" }],
},
false,
);
@@ -134,8 +133,7 @@ describe("FS tools with workspaceOnly=false", () => {
"test-call-2b",
{
path: relativeOutsidePath,
oldText: "old relative content",
newText: "new relative content",
edits: [{ oldText: "old relative content", newText: "new relative content" }],
},
false,
);
@@ -179,8 +177,7 @@ describe("FS tools with workspaceOnly=false", () => {
"test-call-3b",
{
path: outsideUnsetFile,
oldText: "before",
newText: "after",
edits: [{ oldText: "before", newText: "after" }],
},
undefined,
);

View File

@@ -21,9 +21,15 @@ vi.mock("./provider-transport-stream.js", () => ({
prepareTransportAwareSimpleModel,
}));
vi.mock("../plugins/provider-runtime.js", () => ({
resolveProviderStreamFn,
}));
vi.mock("../plugins/provider-runtime.js", async () => {
const actual = await vi.importActual<typeof import("../plugins/provider-runtime.js")>(
"../plugins/provider-runtime.js",
);
return {
...actual,
resolveProviderStreamFn,
};
});
let prepareModelForSimpleCompletion: typeof import("./simple-completion-transport.js").prepareModelForSimpleCompletion;

View File

@@ -62,6 +62,70 @@ const mocks = vi.hoisted(() => ({
resolvedConfig: config,
diagnostics: [],
})),
getScopedChannelsCommandSecretTargets: vi.fn(
({
config,
channel,
accountId,
}: {
config?: { channels?: Record<string, unknown> };
channel?: string | null;
accountId?: string | null;
}) => {
const allowedPaths = new Set<string>();
const targetIds = new Set<string>();
const scopedChannel = channel?.trim();
const scopedAccountId = accountId?.trim();
const scopedConfig =
scopedChannel && config?.channels && typeof config.channels[scopedChannel] === "object"
? (config.channels[scopedChannel] as Record<string, unknown>)
: null;
if (!scopedChannel || !scopedConfig) {
return { targetIds };
}
const maybeCollectSecretPath = (path: string, value: unknown) => {
if (!value || typeof value !== "object" || Array.isArray(value)) {
return;
}
const record = value as Record<string, unknown>;
if (typeof record.source === "string" && typeof record.id === "string") {
targetIds.add(path);
allowedPaths.add(path);
}
};
maybeCollectSecretPath(`channels.${scopedChannel}.token`, scopedConfig.token);
maybeCollectSecretPath(`channels.${scopedChannel}.botToken`, scopedConfig.botToken);
if (scopedAccountId) {
const accountRecord =
scopedConfig.accounts &&
typeof scopedConfig.accounts === "object" &&
!Array.isArray(scopedConfig.accounts) &&
typeof (scopedConfig.accounts as Record<string, unknown>)[scopedAccountId] === "object"
? ((scopedConfig.accounts as Record<string, unknown>)[scopedAccountId] as Record<
string,
unknown
>)
: null;
if (accountRecord) {
maybeCollectSecretPath(
`channels.${scopedChannel}.accounts.${scopedAccountId}.token`,
accountRecord.token,
);
maybeCollectSecretPath(
`channels.${scopedChannel}.accounts.${scopedAccountId}.botToken`,
accountRecord.botToken,
);
}
}
return {
targetIds,
...(allowedPaths.size > 0 ? { allowedPaths } : {}),
};
},
),
}));
vi.mock("../../infra/outbound/message-action-runner.js", async () => {
@@ -87,6 +151,10 @@ vi.mock("../../cli/command-secret-gateway.js", () => ({
resolveCommandSecretRefsViaGateway: mocks.resolveCommandSecretRefsViaGateway,
}));
vi.mock("../../cli/command-secret-targets.js", () => ({
getScopedChannelsCommandSecretTargets: mocks.getScopedChannelsCommandSecretTargets,
}));
function mockSendResult(overrides: { channel?: string; to?: string } = {}) {
mocks.runMessageAction.mockClear();
mocks.runMessageAction.mockResolvedValue({
@@ -121,6 +189,8 @@ beforeEach(() => {
resolvedConfig: config,
diagnostics: [],
}));
mocks.getScopedChannelsCommandSecretTargets.mockClear();
setActivePluginRegistry(createTestRegistry([]));
});
function createChannelPlugin(params: {

View File

@@ -122,9 +122,6 @@ describe("createMusicGenerateTool", () => {
count: 1,
instrumental: true,
lyrics: ["wake the city up"],
task: {
taskId: "task-123",
},
media: {
mediaUrls: ["/tmp/generated-night-drive.mp3"],
},

View File

@@ -1,175 +1,181 @@
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
vi.mock("../plugins/provider-runtime.js", () => ({
resolveProviderRuntimePlugin: vi.fn(({ provider }: { provider?: string }) => {
if (
!provider ||
![
"amazon-bedrock",
"anthropic",
"google",
"kilocode",
"kimi",
"kimi-code",
"minimax",
"minimax-portal",
"mistral",
"moonshot",
"openai",
"openai-codex",
"opencode",
"opencode-go",
"ollama",
"openrouter",
"sglang",
"vllm",
"xai",
"zai",
].includes(provider)
) {
return undefined;
}
if (provider === "sglang" || provider === "vllm") {
return {};
}
return {
buildReplayPolicy: (context?: { modelId?: string; modelApi?: string }) => {
const modelId = context?.modelId?.toLowerCase() ?? "";
switch (provider) {
case "amazon-bedrock":
case "anthropic":
return {
sanitizeMode: "full",
sanitizeToolCallIds: true,
toolCallIdMode: "strict",
preserveSignatures: true,
repairToolUseResultPairing: true,
validateAnthropicTurns: true,
allowSyntheticToolResults: true,
...(modelId.includes("claude") ? { dropThinkingBlocks: true } : {}),
};
case "minimax":
case "minimax-portal":
return context?.modelApi === "openai-completions"
? {
vi.mock("../plugins/provider-runtime.js", async () => {
const actual = await vi.importActual<typeof import("../plugins/provider-runtime.js")>(
"../plugins/provider-runtime.js",
);
return {
...actual,
resolveProviderRuntimePlugin: vi.fn(({ provider }: { provider?: string }) => {
if (
!provider ||
![
"amazon-bedrock",
"anthropic",
"google",
"kilocode",
"kimi",
"kimi-code",
"minimax",
"minimax-portal",
"mistral",
"moonshot",
"openai",
"openai-codex",
"opencode",
"opencode-go",
"ollama",
"openrouter",
"sglang",
"vllm",
"xai",
"zai",
].includes(provider)
) {
return undefined;
}
if (provider === "sglang" || provider === "vllm") {
return {};
}
return {
buildReplayPolicy: (context?: { modelId?: string; modelApi?: string }) => {
const modelId = context?.modelId?.toLowerCase() ?? "";
switch (provider) {
case "amazon-bedrock":
case "anthropic":
return {
sanitizeMode: "full",
sanitizeToolCallIds: true,
toolCallIdMode: "strict",
preserveSignatures: true,
repairToolUseResultPairing: true,
validateAnthropicTurns: true,
allowSyntheticToolResults: true,
...(modelId.includes("claude") ? { dropThinkingBlocks: true } : {}),
};
case "minimax":
case "minimax-portal":
return context?.modelApi === "openai-completions"
? {
sanitizeToolCallIds: true,
toolCallIdMode: "strict",
applyAssistantFirstOrderingFix: true,
validateGeminiTurns: true,
validateAnthropicTurns: true,
}
: {
sanitizeMode: "full",
sanitizeToolCallIds: true,
toolCallIdMode: "strict",
preserveSignatures: true,
repairToolUseResultPairing: true,
validateAnthropicTurns: true,
allowSyntheticToolResults: true,
...(modelId.includes("claude") ? { dropThinkingBlocks: true } : {}),
};
case "moonshot":
case "ollama":
case "zai":
return context?.modelApi === "openai-completions"
? {
sanitizeToolCallIds: true,
toolCallIdMode: "strict",
applyAssistantFirstOrderingFix: true,
validateGeminiTurns: true,
validateAnthropicTurns: true,
}
: undefined;
case "google":
return {
sanitizeMode: "full",
sanitizeToolCallIds: true,
toolCallIdMode: "strict",
sanitizeThoughtSignatures: {
allowBase64Only: true,
includeCamelCase: true,
},
repairToolUseResultPairing: true,
applyAssistantFirstOrderingFix: true,
validateGeminiTurns: true,
validateAnthropicTurns: false,
allowSyntheticToolResults: true,
};
case "mistral":
return {
sanitizeToolCallIds: true,
toolCallIdMode: "strict9",
};
case "openai":
case "openai-codex":
return {
sanitizeMode: "images-only",
sanitizeToolCallIds: context?.modelApi === "openai-completions",
...(context?.modelApi === "openai-completions" ? { toolCallIdMode: "strict" } : {}),
applyAssistantFirstOrderingFix: false,
validateGeminiTurns: false,
validateAnthropicTurns: false,
};
case "kimi":
case "kimi-code":
return {
preserveSignatures: false,
};
case "openrouter":
case "opencode":
case "opencode-go":
return {
applyAssistantFirstOrderingFix: false,
validateGeminiTurns: false,
validateAnthropicTurns: false,
...(modelId.includes("gemini")
? {
sanitizeThoughtSignatures: {
allowBase64Only: true,
includeCamelCase: true,
},
}
: {}),
};
case "xai":
if (
context?.modelApi === "openai-completions" ||
context?.modelApi === "openai-responses"
) {
return {
sanitizeToolCallIds: true,
toolCallIdMode: "strict",
applyAssistantFirstOrderingFix: true,
validateGeminiTurns: true,
validateAnthropicTurns: true,
}
: {
sanitizeMode: "full",
sanitizeToolCallIds: true,
toolCallIdMode: "strict",
preserveSignatures: true,
repairToolUseResultPairing: true,
validateAnthropicTurns: true,
allowSyntheticToolResults: true,
...(modelId.includes("claude") ? { dropThinkingBlocks: true } : {}),
...(context.modelApi === "openai-completions"
? {
applyAssistantFirstOrderingFix: true,
validateGeminiTurns: true,
validateAnthropicTurns: true,
}
: {
applyAssistantFirstOrderingFix: false,
validateGeminiTurns: false,
validateAnthropicTurns: false,
}),
};
case "moonshot":
case "ollama":
case "zai":
return context?.modelApi === "openai-completions"
? {
sanitizeToolCallIds: true,
toolCallIdMode: "strict",
applyAssistantFirstOrderingFix: true,
validateGeminiTurns: true,
validateAnthropicTurns: true,
}
: undefined;
case "google":
return {
sanitizeMode: "full",
sanitizeToolCallIds: true,
toolCallIdMode: "strict",
sanitizeThoughtSignatures: {
allowBase64Only: true,
includeCamelCase: true,
},
repairToolUseResultPairing: true,
applyAssistantFirstOrderingFix: true,
validateGeminiTurns: true,
validateAnthropicTurns: false,
allowSyntheticToolResults: true,
};
case "mistral":
return {
sanitizeToolCallIds: true,
toolCallIdMode: "strict9",
};
case "openai":
case "openai-codex":
return {
sanitizeMode: "images-only",
sanitizeToolCallIds: context?.modelApi === "openai-completions",
...(context?.modelApi === "openai-completions" ? { toolCallIdMode: "strict" } : {}),
applyAssistantFirstOrderingFix: false,
validateGeminiTurns: false,
validateAnthropicTurns: false,
};
case "kimi":
case "kimi-code":
return {
preserveSignatures: false,
};
case "openrouter":
case "opencode":
case "opencode-go":
return {
applyAssistantFirstOrderingFix: false,
validateGeminiTurns: false,
validateAnthropicTurns: false,
...(modelId.includes("gemini")
}
return undefined;
case "kilocode":
return modelId.includes("gemini")
? {
sanitizeThoughtSignatures: {
allowBase64Only: true,
includeCamelCase: true,
},
}
: {}),
};
case "xai":
if (
context?.modelApi === "openai-completions" ||
context?.modelApi === "openai-responses"
) {
return {
sanitizeToolCallIds: true,
toolCallIdMode: "strict",
...(context.modelApi === "openai-completions"
? {
applyAssistantFirstOrderingFix: true,
validateGeminiTurns: true,
validateAnthropicTurns: true,
}
: {
applyAssistantFirstOrderingFix: false,
validateGeminiTurns: false,
validateAnthropicTurns: false,
}),
};
}
return undefined;
case "kilocode":
return modelId.includes("gemini")
? {
sanitizeThoughtSignatures: {
allowBase64Only: true,
includeCamelCase: true,
},
}
: undefined;
default:
return undefined;
}
},
};
}),
resetProviderRuntimeHookCacheForTest: vi.fn(),
}));
: undefined;
default:
return undefined;
}
},
};
}),
resetProviderRuntimeHookCacheForTest: vi.fn(),
};
});
let resolveTranscriptPolicy: typeof import("./transcript-policy.js").resolveTranscriptPolicy;

View File

@@ -551,7 +551,21 @@ describe("resolveCommandSecretRefsViaGateway", () => {
commandName: "memory status",
targetIds: new Set(["talk.providers.*.apiKey"]),
}),
).rejects.toThrow(/Path segment does not exist/i);
).resolves.toMatchObject({
resolvedConfig: {
talk: {
providers: {
"acme-speech": {
apiKey: "sk-live",
},
},
},
},
targetStatesByPath: {
[TALK_TEST_PROVIDER_API_KEY_PATH]: "resolved_gateway",
},
hadUnresolvedTargets: false,
});
});
it("fails when configured refs remain unresolved after gateway assignments are applied", async () => {

View File

@@ -17,18 +17,31 @@ const SECRET_TARGET_CALLSITES = [
function hasSupportedTargetIdsWiring(source: string): boolean {
return (
/targetIds:\s*get[A-Za-z0-9_]+\(\)/m.test(source) ||
/targetIds:\s*scopedTargets\.targetIds/m.test(source)
/targetIds:\s*scopedTargets\.targetIds/m.test(source) ||
source.includes("collectStatusScanOverview({")
);
}
function usesSharedSecretResolver(source: string): boolean {
return (
source.includes("resolveCommandSecretRefsViaGateway") ||
source.includes("resolveCommandConfigWithSecrets") ||
source.includes("collectStatusScanOverview({")
);
}
describe("command secret resolution coverage", () => {
it.each(SECRET_TARGET_CALLSITES)(
"routes target-id command path through shared gateway resolver: %s",
"routes target-id command path through shared secret resolver: %s",
async (relativePath) => {
const source = await readCommandSource(relativePath);
expect(source).toContain("resolveCommandSecretRefsViaGateway");
expect(usesSharedSecretResolver(source)).toBe(true);
expect(hasSupportedTargetIdsWiring(source)).toBe(true);
expect(source).toContain("resolveCommandSecretRefsViaGateway({");
expect(
source.includes("resolveCommandSecretRefsViaGateway({") ||
source.includes("resolveCommandConfigWithSecrets({") ||
source.includes("collectStatusScanOverview({"),
).toBe(true);
},
);
});

View File

@@ -184,7 +184,7 @@ describe("gateway run option collisions", () => {
expect(forceFreePortAndWait).toHaveBeenCalledWith(18789, expect.anything());
expect(waitForPortBindable).toHaveBeenCalledWith(
18789,
expect.objectContaining({ host: "127.0.0.1" }),
expect.objectContaining({ intervalMs: 150, timeoutMs: 3000 }),
);
expect(setGatewayWsLogStyle).toHaveBeenCalledWith("full");
expect(startGatewayServer).toHaveBeenCalledWith(

View File

@@ -7,10 +7,16 @@ vi.mock("../config/config.js", () => ({
loadConfig: () => ({}),
}));
vi.mock("../plugins/provider-runtime.js", () => ({
resolveProviderUsageSnapshotWithPlugin: (...args: unknown[]) =>
resolveProviderUsageSnapshotWithPluginMock(...args),
}));
vi.mock("../plugins/provider-runtime.js", async () => {
const actual = await vi.importActual<typeof import("../plugins/provider-runtime.js")>(
"../plugins/provider-runtime.js",
);
return {
...actual,
resolveProviderUsageSnapshotWithPlugin: (...args: unknown[]) =>
resolveProviderUsageSnapshotWithPluginMock(...args),
};
});
let loadProviderUsageSummary: typeof import("./provider-usage.load.js").loadProviderUsageSummary;

View File

@@ -4,9 +4,15 @@ const { resolveProviderReasoningOutputModeWithPluginMock } = vi.hoisted(() => ({
resolveProviderReasoningOutputModeWithPluginMock: vi.fn(),
}));
vi.mock("../plugins/provider-runtime.js", () => ({
resolveProviderReasoningOutputModeWithPlugin: resolveProviderReasoningOutputModeWithPluginMock,
}));
vi.mock("../plugins/provider-runtime.js", async () => {
const actual = await vi.importActual<typeof import("../plugins/provider-runtime.js")>(
"../plugins/provider-runtime.js",
);
return {
...actual,
resolveProviderReasoningOutputModeWithPlugin: resolveProviderReasoningOutputModeWithPluginMock,
};
});
import { isReasoningTagProvider, resolveReasoningOutputMode } from "./provider-utils.js";

View File

@@ -12,6 +12,7 @@ const ALLOWED_EXTENSION_PUBLIC_SURFACE_BASENAMES = new Set(
const allowedNonExtensionTests = new Set<string>([
"src/agents/pi-embedded-runner-extraparams-moonshot.test.ts",
"src/agents/pi-embedded-runner-extraparams.test.ts",
"src/agents/pi-embedded-runner-extraparams-moonshot.test.ts",
"src/channels/plugins/contracts/dm-policy.contract.test.ts",
"src/channels/plugins/contracts/group-policy.contract.test.ts",
"src/commands/channels.surfaces-signal-runtime-errors-channels-status-output.test.ts",
@@ -21,6 +22,8 @@ const allowedNonExtensionTests = new Set<string>([
"src/plugins/interactive.test.ts",
"src/plugins/contracts/discovery.contract.test.ts",
"src/plugin-sdk/telegram-command-config.test.ts",
"src/security/audit-channel-slack-command-findings.test.ts",
"src/security/audit-feishu-doc-risk.test.ts",
"src/secrets/runtime-channel-inactive-variants.test.ts",
"src/secrets/runtime-discord-surface.test.ts",
"src/secrets/runtime-inactive-telegram-surfaces.test.ts",
@@ -30,8 +33,6 @@ const allowedNonExtensionTests = new Set<string>([
"src/secrets/runtime-nextcloud-talk-file-precedence.test.ts",
"src/secrets/runtime-telegram-token-inheritance.test.ts",
"src/secrets/runtime-zalo-token-activity.test.ts",
"src/security/audit-channel-slack-command-findings.test.ts",
"src/security/audit-feishu-doc-risk.test.ts",
]);
function walk(dir: string, entries: string[] = []): string[] {