mirror of
https://github.com/moltbot/moltbot.git
synced 2026-04-20 21:23:23 +00:00
test: stabilize agent auth and config suites
This commit is contained in:
@@ -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<
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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> = {
|
||||
|
||||
@@ -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"]);
|
||||
});
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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")>(
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"];
|
||||
|
||||
@@ -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"];
|
||||
|
||||
@@ -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"];
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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 }) =>
|
||||
|
||||
@@ -102,6 +102,10 @@ describe("runEmbeddedAttempt context injection", () => {
|
||||
contextEngine: {
|
||||
assemble: async ({ messages }) => ({ messages, estimatedTokens: 1 }),
|
||||
},
|
||||
attemptOverrides: {
|
||||
bootstrapContextMode: "full",
|
||||
bootstrapContextRunKind: "default",
|
||||
},
|
||||
sessionKey: "agent:main",
|
||||
tempPaths,
|
||||
});
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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"],
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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[] {
|
||||
|
||||
Reference in New Issue
Block a user