test: genericize talk provider fixtures

This commit is contained in:
Peter Steinberger
2026-04-05 18:52:09 +01:00
parent c71ee4d844
commit b8e2e5c251
12 changed files with 210 additions and 227 deletions

View File

@@ -31,6 +31,8 @@ vi.mock("../../tts/provider-registry.js", () => ({
vi.mock("../../tts/tts.js", () => ttsMocks);
const { handleTtsCommands } = await import("./commands-tts.js");
const PRIMARY_TTS_PROVIDER = "acme-speech";
const FALLBACK_TTS_PROVIDER = "backup-speech";
function buildTtsParams(commandBodyNormalized: string): Parameters<typeof handleTtsCommands>[0] {
return {
@@ -49,7 +51,7 @@ describe("handleTtsCommands status fallback reporting", () => {
ttsMocks.resolveTtsConfig.mockReturnValue({});
ttsMocks.resolveTtsPrefsPath.mockReturnValue("/tmp/tts-prefs.json");
ttsMocks.isTtsEnabled.mockReturnValue(true);
ttsMocks.getTtsProvider.mockReturnValue("elevenlabs");
ttsMocks.getTtsProvider.mockReturnValue(PRIMARY_TTS_PROVIDER);
ttsMocks.isTtsProviderConfigured.mockReturnValue(true);
ttsMocks.getTtsMaxLength.mockReturnValue(1500);
ttsMocks.isSummarizationEnabled.mockReturnValue(true);
@@ -62,18 +64,18 @@ describe("handleTtsCommands status fallback reporting", () => {
success: true,
textLength: 128,
summarized: false,
provider: "microsoft",
fallbackFrom: "elevenlabs",
attemptedProviders: ["elevenlabs", "microsoft"],
provider: FALLBACK_TTS_PROVIDER,
fallbackFrom: PRIMARY_TTS_PROVIDER,
attemptedProviders: [PRIMARY_TTS_PROVIDER, FALLBACK_TTS_PROVIDER],
attempts: [
{
provider: "elevenlabs",
provider: PRIMARY_TTS_PROVIDER,
outcome: "failed",
reasonCode: "provider_error",
latencyMs: 73,
},
{
provider: "microsoft",
provider: FALLBACK_TTS_PROVIDER,
outcome: "success",
reasonCode: "success",
latencyMs: 420,
@@ -84,10 +86,14 @@ describe("handleTtsCommands status fallback reporting", () => {
const result = await handleTtsCommands(buildTtsParams("/tts status"), true);
expect(result?.shouldContinue).toBe(false);
expect(result?.reply?.text).toContain("Fallback: elevenlabs -> microsoft");
expect(result?.reply?.text).toContain("Attempts: elevenlabs -> microsoft");
expect(result?.reply?.text).toContain(
"Attempt details: elevenlabs:failed(provider_error) 73ms, microsoft:success(ok) 420ms",
`Fallback: ${PRIMARY_TTS_PROVIDER} -> ${FALLBACK_TTS_PROVIDER}`,
);
expect(result?.reply?.text).toContain(
`Attempts: ${PRIMARY_TTS_PROVIDER} -> ${FALLBACK_TTS_PROVIDER}`,
);
expect(result?.reply?.text).toContain(
`Attempt details: ${PRIMARY_TTS_PROVIDER}:failed(provider_error) 73ms, ${FALLBACK_TTS_PROVIDER}:success(ok) 420ms`,
);
});
@@ -98,10 +104,10 @@ describe("handleTtsCommands status fallback reporting", () => {
textLength: 128,
summarized: false,
error: "TTS conversion failed",
attemptedProviders: ["elevenlabs", "microsoft"],
attemptedProviders: [PRIMARY_TTS_PROVIDER, FALLBACK_TTS_PROVIDER],
attempts: [
{
provider: "elevenlabs",
provider: PRIMARY_TTS_PROVIDER,
outcome: "failed",
reasonCode: "timeout",
latencyMs: 999,
@@ -113,8 +119,12 @@ describe("handleTtsCommands status fallback reporting", () => {
const result = await handleTtsCommands(buildTtsParams("/tts status"), true);
expect(result?.shouldContinue).toBe(false);
expect(result?.reply?.text).toContain("Error: TTS conversion failed");
expect(result?.reply?.text).toContain("Attempts: elevenlabs -> microsoft");
expect(result?.reply?.text).toContain("Attempt details: elevenlabs:failed(timeout) 999ms");
expect(result?.reply?.text).toContain(
`Attempts: ${PRIMARY_TTS_PROVIDER} -> ${FALLBACK_TTS_PROVIDER}`,
);
expect(result?.reply?.text).toContain(
`Attempt details: ${PRIMARY_TTS_PROVIDER}:failed(timeout) 999ms`,
);
});
it("persists fallback metadata from /tts audio and renders it in /tts status", async () => {
@@ -126,18 +136,18 @@ describe("handleTtsCommands status fallback reporting", () => {
ttsMocks.textToSpeech.mockResolvedValue({
success: true,
audioPath: "/tmp/fallback.ogg",
provider: "microsoft",
fallbackFrom: "elevenlabs",
attemptedProviders: ["elevenlabs", "microsoft"],
provider: FALLBACK_TTS_PROVIDER,
fallbackFrom: PRIMARY_TTS_PROVIDER,
attemptedProviders: [PRIMARY_TTS_PROVIDER, FALLBACK_TTS_PROVIDER],
attempts: [
{
provider: "elevenlabs",
provider: PRIMARY_TTS_PROVIDER,
outcome: "failed",
reasonCode: "provider_error",
latencyMs: 65,
},
{
provider: "microsoft",
provider: FALLBACK_TTS_PROVIDER,
outcome: "success",
reasonCode: "success",
latencyMs: 175,
@@ -153,11 +163,15 @@ describe("handleTtsCommands status fallback reporting", () => {
const statusResult = await handleTtsCommands(buildTtsParams("/tts status"), true);
expect(statusResult?.shouldContinue).toBe(false);
expect(statusResult?.reply?.text).toContain("Provider: microsoft");
expect(statusResult?.reply?.text).toContain("Fallback: elevenlabs -> microsoft");
expect(statusResult?.reply?.text).toContain("Attempts: elevenlabs -> microsoft");
expect(statusResult?.reply?.text).toContain(`Provider: ${FALLBACK_TTS_PROVIDER}`);
expect(statusResult?.reply?.text).toContain(
"Attempt details: elevenlabs:failed(provider_error) 65ms, microsoft:success(ok) 175ms",
`Fallback: ${PRIMARY_TTS_PROVIDER} -> ${FALLBACK_TTS_PROVIDER}`,
);
expect(statusResult?.reply?.text).toContain(
`Attempts: ${PRIMARY_TTS_PROVIDER} -> ${FALLBACK_TTS_PROVIDER}`,
);
expect(statusResult?.reply?.text).toContain(
`Attempt details: ${PRIMARY_TTS_PROVIDER}:failed(provider_error) 65ms, ${FALLBACK_TTS_PROVIDER}:success(ok) 175ms`,
);
});
});

View File

@@ -1,5 +1,11 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../config/config.js";
import {
buildTalkTestProviderConfig,
readTalkTestProviderApiKey as readTalkProviderApiKey,
TALK_TEST_PROVIDER_API_KEY_PATH,
TALK_TEST_PROVIDER_API_KEY_PATH_SEGMENTS,
} from "../test-utils/talk-test-provider.js";
import { resolveCommandSecretRefsViaGateway } from "./command-secret-gateway.js";
const mocks = vi.hoisted(() => ({
@@ -27,19 +33,7 @@ beforeEach(() => {
describe("resolveCommandSecretRefsViaGateway", () => {
function makeTalkProviderApiKeySecretRefConfig(envKey: string): OpenClawConfig {
return {
talk: {
providers: {
elevenlabs: {
apiKey: { source: "env", provider: "default", id: envKey },
},
},
},
} as unknown as OpenClawConfig;
}
function readTalkProviderApiKey(config: OpenClawConfig): unknown {
return config.talk?.providers?.elevenlabs?.apiKey;
return buildTalkTestProviderConfig({ source: "env", provider: "default", id: envKey });
}
async function withEnvValue(
@@ -101,13 +95,7 @@ describe("resolveCommandSecretRefsViaGateway", () => {
it("returns config unchanged when no target SecretRefs are configured", async () => {
const config = {
talk: {
providers: {
elevenlabs: {
apiKey: "plain", // pragma: allowlist secret
},
},
},
...buildTalkTestProviderConfig("plain"), // pragma: allowlist secret
} as unknown as OpenClawConfig;
const result = await resolveCommandSecretRefsViaGateway({
config,
@@ -152,22 +140,18 @@ describe("resolveCommandSecretRefsViaGateway", () => {
callGateway.mockResolvedValueOnce({
assignments: [
{
path: "talk.providers.elevenlabs.apiKey",
pathSegments: ["talk", "providers", "elevenlabs", "apiKey"],
path: TALK_TEST_PROVIDER_API_KEY_PATH,
pathSegments: [...TALK_TEST_PROVIDER_API_KEY_PATH_SEGMENTS],
value: "sk-live",
},
],
diagnostics: [],
});
const config = {
talk: {
providers: {
elevenlabs: {
apiKey: { source: "env", provider: "default", id: "TALK_API_KEY" },
},
},
},
} as OpenClawConfig;
const config = buildTalkTestProviderConfig({
source: "env",
provider: "default",
id: "TALK_API_KEY",
});
const result = await resolveCommandSecretRefsViaGateway({
config,
commandName: "memory status",
@@ -234,15 +218,11 @@ describe("resolveCommandSecretRefsViaGateway", () => {
try {
await expect(
resolveCommandSecretRefsViaGateway({
config: {
talk: {
providers: {
elevenlabs: {
apiKey: { source: "env", provider: "default", id: envKey },
},
},
},
} as unknown as OpenClawConfig,
config: buildTalkTestProviderConfig({
source: "env",
provider: "default",
id: envKey,
}),
commandName: "memory status",
targetIds: new Set(["talk.providers.*.apiKey"]),
}),
@@ -263,13 +243,11 @@ describe("resolveCommandSecretRefsViaGateway", () => {
try {
const result = await resolveCommandSecretRefsViaGateway({
config: {
talk: {
providers: {
elevenlabs: {
apiKey: { source: "env", provider: "default", id: "TALK_API_KEY" },
},
},
},
...buildTalkTestProviderConfig({
source: "env",
provider: "default",
id: "TALK_API_KEY",
}),
secrets: {
providers: {
default: { source: "env" },
@@ -452,15 +430,11 @@ describe("resolveCommandSecretRefsViaGateway", () => {
});
await expect(
resolveCommandSecretRefsViaGateway({
config: {
talk: {
providers: {
elevenlabs: {
apiKey: { source: "env", provider: "default", id: "TALK_API_KEY" },
},
},
},
} as unknown as OpenClawConfig,
config: buildTalkTestProviderConfig({
source: "env",
provider: "default",
id: "TALK_API_KEY",
}),
commandName: "memory status",
targetIds: new Set(["talk.providers.*.apiKey"]),
}),
@@ -471,8 +445,8 @@ describe("resolveCommandSecretRefsViaGateway", () => {
callGateway.mockResolvedValueOnce({
assignments: [
{
path: "talk.providers.elevenlabs.apiKey",
pathSegments: ["talk", "providers", "elevenlabs", "apiKey"],
path: TALK_TEST_PROVIDER_API_KEY_PATH,
pathSegments: [...TALK_TEST_PROVIDER_API_KEY_PATH_SEGMENTS],
value: "sk-live",
},
],
@@ -480,15 +454,11 @@ describe("resolveCommandSecretRefsViaGateway", () => {
});
await expect(
resolveCommandSecretRefsViaGateway({
config: {
talk: {
providers: {
elevenlabs: {
apiKey: { source: "env", provider: "default", id: "TALK_API_KEY" },
},
},
},
} as OpenClawConfig,
config: buildTalkTestProviderConfig({
source: "env",
provider: "default",
id: "TALK_API_KEY",
}),
commandName: "memory status",
targetIds: new Set(["talk.providers.*.apiKey"]),
}),
@@ -504,7 +474,10 @@ describe("resolveCommandSecretRefsViaGateway", () => {
await withEnvValue(envKey, undefined, async () => {
await expect(resolveTalkProviderApiKey({ envKey })).rejects.toThrow(
/talk\.providers\.elevenlabs\.apiKey is unresolved in the active runtime snapshot/i,
new RegExp(
`${TALK_TEST_PROVIDER_API_KEY_PATH.replaceAll(".", "\\.")} is unresolved in the active runtime snapshot`,
"i",
),
);
});
});
@@ -513,7 +486,7 @@ describe("resolveCommandSecretRefsViaGateway", () => {
callGateway.mockResolvedValueOnce({
assignments: [],
diagnostics: [
"talk.providers.elevenlabs.apiKey: secret ref is configured on an inactive surface; skipping command-time assignment.",
`${TALK_TEST_PROVIDER_API_KEY_PATH}: secret ref is configured on an inactive surface; skipping command-time assignment.`,
],
});
@@ -521,7 +494,7 @@ describe("resolveCommandSecretRefsViaGateway", () => {
expectTalkProviderApiKeySecretRef(result, "TALK_API_KEY");
expect(result.diagnostics).toEqual([
"talk.providers.elevenlabs.apiKey: secret ref is configured on an inactive surface; skipping command-time assignment.",
`${TALK_TEST_PROVIDER_API_KEY_PATH}: secret ref is configured on an inactive surface; skipping command-time assignment.`,
]);
});
@@ -529,7 +502,7 @@ describe("resolveCommandSecretRefsViaGateway", () => {
callGateway.mockResolvedValueOnce({
assignments: [],
diagnostics: ["talk api key inactive"],
inactiveRefPaths: ["talk.providers.elevenlabs.apiKey"],
inactiveRefPaths: [TALK_TEST_PROVIDER_API_KEY_PATH],
});
const result = await resolveTalkProviderApiKey({ envKey: "TALK_API_KEY" });
@@ -588,10 +561,10 @@ describe("resolveCommandSecretRefsViaGateway", () => {
});
expect(readTalkProviderApiKey(result.resolvedConfig)).toBeUndefined();
expect(result.hadUnresolvedTargets).toBe(true);
expect(result.targetStatesByPath["talk.providers.elevenlabs.apiKey"]).toBe("unresolved");
expect(result.targetStatesByPath[TALK_TEST_PROVIDER_API_KEY_PATH]).toBe("unresolved");
expect(
result.diagnostics.some((entry) =>
entry.includes("talk.providers.elevenlabs.apiKey is unavailable in this command path"),
entry.includes(`${TALK_TEST_PROVIDER_API_KEY_PATH} is unavailable in this command path`),
),
).toBe(true);
});
@@ -612,7 +585,7 @@ describe("resolveCommandSecretRefsViaGateway", () => {
});
expect(readTalkProviderApiKey(result.resolvedConfig)).toBeUndefined();
expect(result.hadUnresolvedTargets).toBe(true);
expect(result.targetStatesByPath["talk.providers.elevenlabs.apiKey"]).toBe("unresolved");
expect(result.targetStatesByPath[TALK_TEST_PROVIDER_API_KEY_PATH]).toBe("unresolved");
});
});
@@ -630,7 +603,7 @@ describe("resolveCommandSecretRefsViaGateway", () => {
});
expect(readTalkProviderApiKey(result.resolvedConfig)).toBe("recovered-locally");
expect(result.hadUnresolvedTargets).toBe(false);
expect(result.targetStatesByPath["talk.providers.elevenlabs.apiKey"]).toBe("resolved_local");
expect(result.targetStatesByPath[TALK_TEST_PROVIDER_API_KEY_PATH]).toBe("resolved_local");
expect(
result.diagnostics.some((entry) =>
entry.includes(
@@ -648,8 +621,8 @@ describe("resolveCommandSecretRefsViaGateway", () => {
callGateway.mockResolvedValueOnce({
assignments: [
{
path: "talk.providers.elevenlabs.apiKey",
pathSegments: ["talk", "providers", "elevenlabs", "apiKey"],
path: TALK_TEST_PROVIDER_API_KEY_PATH,
pathSegments: [...TALK_TEST_PROVIDER_API_KEY_PATH_SEGMENTS],
value: "resolved-by-gateway",
},
],
@@ -658,24 +631,18 @@ describe("resolveCommandSecretRefsViaGateway", () => {
try {
const result = await resolveCommandSecretRefsViaGateway({
config: {
talk: {
providers: {
elevenlabs: {
apiKey: { source: "env", provider: "default", id: locallyRecoveredKey },
},
},
},
} as unknown as OpenClawConfig,
config: buildTalkTestProviderConfig({
source: "env",
provider: "default",
id: locallyRecoveredKey,
}),
commandName: "message send",
targetIds: new Set(["talk.providers.*.apiKey"]),
});
expect(readTalkProviderApiKey(result.resolvedConfig)).toBe("resolved-by-gateway");
expect(result.hadUnresolvedTargets).toBe(false);
expect(result.targetStatesByPath["talk.providers.elevenlabs.apiKey"]).toBe(
"resolved_gateway",
);
expect(result.targetStatesByPath[TALK_TEST_PROVIDER_API_KEY_PATH]).toBe("resolved_gateway");
} finally {
if (priorLocallyRecoveredValue === undefined) {
delete process.env[locallyRecoveredKey];
@@ -697,13 +664,11 @@ describe("resolveCommandSecretRefsViaGateway", () => {
try {
const result = await resolveCommandSecretRefsViaGateway({
config: {
talk: {
providers: {
elevenlabs: {
apiKey: { source: "env", provider: "default", id: talkEnvKey },
},
},
},
...buildTalkTestProviderConfig({
source: "env",
provider: "default",
id: talkEnvKey,
}),
gateway: {
auth: {
password: { source: "env", provider: "default", id: gatewayEnvKey },
@@ -717,7 +682,7 @@ describe("resolveCommandSecretRefsViaGateway", () => {
expect(readTalkProviderApiKey(result.resolvedConfig)).toBe("target-only");
expect(result.hadUnresolvedTargets).toBe(false);
expect(result.targetStatesByPath["talk.providers.elevenlabs.apiKey"]).toBe("resolved_local");
expect(result.targetStatesByPath[TALK_TEST_PROVIDER_API_KEY_PATH]).toBe("resolved_local");
} finally {
if (priorTalkValue === undefined) {
delete process.env[talkEnvKey];
@@ -740,15 +705,11 @@ describe("resolveCommandSecretRefsViaGateway", () => {
try {
const result = await resolveCommandSecretRefsViaGateway({
config: {
talk: {
providers: {
elevenlabs: {
apiKey: { source: "env", provider: "default", id: envKey },
},
},
},
} as unknown as OpenClawConfig,
config: buildTalkTestProviderConfig({
source: "env",
provider: "default",
id: envKey,
}),
commandName: "channels resolve",
targetIds: new Set(["talk.providers.*.apiKey"]),
mode: "read_only_operational",
@@ -756,7 +717,7 @@ describe("resolveCommandSecretRefsViaGateway", () => {
expect(readTalkProviderApiKey(result.resolvedConfig)).toBeUndefined();
expect(result.hadUnresolvedTargets).toBe(true);
expect(result.targetStatesByPath["talk.providers.elevenlabs.apiKey"]).toBe("unresolved");
expect(result.targetStatesByPath[TALK_TEST_PROVIDER_API_KEY_PATH]).toBe("unresolved");
expect(
result.diagnostics.some((entry) =>
entry.includes("attempted local command-secret resolution"),

View File

@@ -2,6 +2,7 @@ import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { describe, expect, it } from "vitest";
import { TALK_TEST_PROVIDER_ID } from "../test-utils/talk-test-provider.js";
import { createConfigIO } from "./io.js";
import { buildTalkConfigResponse, normalizeTalkSection } from "./talk.js";
@@ -94,18 +95,18 @@ describe("talk normalization", () => {
it("preserves SecretRef apiKey values during normalization", () => {
const normalized = normalizeTalkSection({
provider: "elevenlabs",
provider: TALK_TEST_PROVIDER_ID,
providers: {
elevenlabs: {
[TALK_TEST_PROVIDER_ID]: {
apiKey: { source: "env", provider: "default", id: "ELEVENLABS_API_KEY" },
},
},
});
expect(normalized).toEqual({
provider: "elevenlabs",
provider: TALK_TEST_PROVIDER_ID,
providers: {
elevenlabs: {
[TALK_TEST_PROVIDER_ID]: {
apiKey: { source: "env", provider: "default", id: "ELEVENLABS_API_KEY" },
},
},

View File

@@ -1,5 +1,6 @@
import type { ErrorObject } from "ajv";
import { describe, expect, it } from "vitest";
import { TALK_TEST_PROVIDER_ID } from "../../test-utils/talk-test-provider.js";
import { formatValidationErrors, validateTalkConfigResult } from "./index.js";
const makeError = (overrides: Partial<ErrorObject>): ErrorObject => ({
@@ -69,9 +70,9 @@ describe("validateTalkConfigResult", () => {
validateTalkConfigResult({
config: {
talk: {
provider: "elevenlabs",
provider: TALK_TEST_PROVIDER_ID,
providers: {
elevenlabs: {
[TALK_TEST_PROVIDER_ID]: {
apiKey: {
source: "env",
provider: "default",
@@ -80,7 +81,7 @@ describe("validateTalkConfigResult", () => {
},
},
resolved: {
provider: "elevenlabs",
provider: TALK_TEST_PROVIDER_ID,
config: {
apiKey: {
source: "env",
@@ -100,9 +101,9 @@ describe("validateTalkConfigResult", () => {
validateTalkConfigResult({
config: {
talk: {
provider: "elevenlabs",
provider: TALK_TEST_PROVIDER_ID,
providers: {
elevenlabs: {
[TALK_TEST_PROVIDER_ID]: {
voiceId: "voice-normalized",
},
},

View File

@@ -1,4 +1,8 @@
import { describe, expect, it, vi } from "vitest";
import {
TALK_TEST_PROVIDER_API_KEY_PATH,
TALK_TEST_PROVIDER_API_KEY_PATH_SEGMENTS,
} from "../../test-utils/talk-test-provider.js";
import { createSecretsHandlers } from "./secrets.js";
async function invokeSecretsReload(params: {
@@ -90,13 +94,13 @@ describe("secrets handlers", () => {
const resolveSecrets = vi.fn().mockResolvedValue({
assignments: [
{
path: "talk.providers.elevenlabs.apiKey",
pathSegments: ["talk", "providers", "elevenlabs", "apiKey"],
path: TALK_TEST_PROVIDER_API_KEY_PATH,
pathSegments: [...TALK_TEST_PROVIDER_API_KEY_PATH_SEGMENTS],
value: "sk",
},
],
diagnostics: ["note"],
inactiveRefPaths: ["talk.providers.elevenlabs.apiKey"],
inactiveRefPaths: [TALK_TEST_PROVIDER_API_KEY_PATH],
});
const handlers = createHandlers({ resolveSecrets });
const respond = vi.fn();
@@ -114,13 +118,13 @@ describe("secrets handlers", () => {
ok: true,
assignments: [
{
path: "talk.providers.elevenlabs.apiKey",
pathSegments: ["talk", "providers", "elevenlabs", "apiKey"],
path: TALK_TEST_PROVIDER_API_KEY_PATH,
pathSegments: [...TALK_TEST_PROVIDER_API_KEY_PATH_SEGMENTS],
value: "sk",
},
],
diagnostics: ["note"],
inactiveRefPaths: ["talk.providers.elevenlabs.apiKey"],
inactiveRefPaths: [TALK_TEST_PROVIDER_API_KEY_PATH],
});
});
@@ -186,7 +190,7 @@ describe("secrets handlers", () => {
it("returns unavailable when secrets.resolve handler returns an invalid payload shape", async () => {
const resolveSecrets = vi.fn().mockResolvedValue({
assignments: [{ path: "talk.providers.elevenlabs.apiKey", pathSegments: [""], value: "sk" }],
assignments: [{ path: TALK_TEST_PROVIDER_API_KEY_PATH, pathSegments: [""], value: "sk" }],
diagnostics: [],
inactiveRefPaths: [],
});

View File

@@ -3,6 +3,10 @@ import path from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { resolveMainSessionKeyFromConfig } from "../config/sessions.js";
import { drainSystemEvents } from "../infra/system-events.js";
import {
TALK_TEST_PROVIDER_API_KEY_PATH,
TALK_TEST_PROVIDER_ID,
} from "../test-utils/talk-test-provider.js";
import { openTrackedWs } from "./device-authz.test-helpers.js";
import {
connectReq,
@@ -237,7 +241,7 @@ describe("gateway hot reload", () => {
await writeConfigFile({
talk: {
providers: {
elevenlabs: {
[TALK_TEST_PROVIDER_ID]: {
apiKey: { source: "env", provider: "default", id: refId },
},
},
@@ -769,7 +773,7 @@ describe("gateway hot reload", () => {
targetIds: ["talk.providers.*.apiKey"],
});
expect(preResolve.ok).toBe(true);
expect(preResolve.payload?.assignments?.[0]?.path).toBe("talk.providers.elevenlabs.apiKey");
expect(preResolve.payload?.assignments?.[0]?.path).toBe(TALK_TEST_PROVIDER_API_KEY_PATH);
expect(preResolve.payload?.assignments?.[0]?.value).toBe("talk-key-before-reload-failure");
delete process.env[refId];
@@ -785,7 +789,7 @@ describe("gateway hot reload", () => {
targetIds: ["talk.providers.*.apiKey"],
});
expect(postResolve.ok).toBe(true);
expect(postResolve.payload?.assignments?.[0]?.path).toBe("talk.providers.elevenlabs.apiKey");
expect(postResolve.payload?.assignments?.[0]?.path).toBe(TALK_TEST_PROVIDER_API_KEY_PATH);
expect(postResolve.payload?.assignments?.[0]?.value).toBe("talk-key-before-reload-failure");
} finally {
if (previousRefValue === undefined) {

View File

@@ -2,6 +2,11 @@ import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import {
TALK_TEST_PROVIDER_API_KEY_PATH,
TALK_TEST_PROVIDER_API_KEY_PATH_SEGMENTS,
TALK_TEST_PROVIDER_ID,
} from "../test-utils/talk-test-provider.js";
import { runSecretsApply } from "./apply.js";
import type { SecretsApplyPlan } from "./plan.js";
import { clearSecretsRuntimeSnapshot } from "./runtime.js";
@@ -526,7 +531,7 @@ describe("secrets apply", () => {
{
talk: {
providers: {
elevenlabs: {
[TALK_TEST_PROVIDER_ID]: {
apiKey: "sk-talk-plaintext", // pragma: allowlist secret
},
},
@@ -546,8 +551,8 @@ describe("secrets apply", () => {
targets: [
{
type: "talk.providers.*.apiKey",
path: "talk.providers.elevenlabs.apiKey",
pathSegments: ["talk", "providers", "elevenlabs", "apiKey"],
path: TALK_TEST_PROVIDER_API_KEY_PATH,
pathSegments: [...TALK_TEST_PROVIDER_API_KEY_PATH_SEGMENTS],
ref: { source: "env", provider: "default", id: "OPENAI_API_KEY" },
},
],
@@ -562,9 +567,9 @@ describe("secrets apply", () => {
expect(result.changed).toBe(true);
const nextConfig = JSON.parse(await fs.readFile(fixture.configPath, "utf8")) as {
talk?: { providers?: { elevenlabs?: { apiKey?: unknown } } };
talk?: { providers?: Record<string, { apiKey?: unknown }> };
};
expect(nextConfig.talk?.providers?.elevenlabs?.apiKey).toEqual({
expect(nextConfig.talk?.providers?.[TALK_TEST_PROVIDER_ID]?.apiKey).toEqual({
source: "env",
provider: "default",
id: "OPENAI_API_KEY",

View File

@@ -1,27 +1,20 @@
import { describe, expect, it } from "vitest";
import type { OpenClawConfig } from "../config/config.js";
import {
buildTalkTestProviderConfig,
TALK_TEST_PROVIDER_API_KEY_PATH,
TALK_TEST_PROVIDER_API_KEY_PATH_SEGMENTS,
} from "../test-utils/talk-test-provider.js";
import { collectCommandSecretAssignmentsFromSnapshot } from "./command-config.js";
describe("collectCommandSecretAssignmentsFromSnapshot", () => {
it("returns assignments from the active runtime snapshot for configured refs", () => {
const sourceConfig = {
talk: {
providers: {
elevenlabs: {
apiKey: { source: "env", provider: "default", id: "TALK_API_KEY" },
},
},
},
} as unknown as OpenClawConfig;
const resolvedConfig = {
talk: {
providers: {
elevenlabs: {
apiKey: "talk-key", // pragma: allowlist secret
},
},
},
} as unknown as OpenClawConfig;
const sourceConfig = buildTalkTestProviderConfig({
source: "env",
provider: "default",
id: "TALK_API_KEY",
});
const resolvedConfig = buildTalkTestProviderConfig("talk-key"); // pragma: allowlist secret
const result = collectCommandSecretAssignmentsFromSnapshot({
sourceConfig,
@@ -32,30 +25,20 @@ describe("collectCommandSecretAssignmentsFromSnapshot", () => {
expect(result.assignments).toEqual([
{
path: "talk.providers.elevenlabs.apiKey",
pathSegments: ["talk", "providers", "elevenlabs", "apiKey"],
path: TALK_TEST_PROVIDER_API_KEY_PATH,
pathSegments: [...TALK_TEST_PROVIDER_API_KEY_PATH_SEGMENTS],
value: "talk-key",
},
]);
});
it("throws when configured refs are unresolved in the snapshot", () => {
const sourceConfig = {
talk: {
providers: {
elevenlabs: {
apiKey: { source: "env", provider: "default", id: "TALK_API_KEY" },
},
},
},
} as unknown as OpenClawConfig;
const resolvedConfig = {
talk: {
providers: {
elevenlabs: {},
},
},
} as unknown as OpenClawConfig;
const sourceConfig = buildTalkTestProviderConfig({
source: "env",
provider: "default",
id: "TALK_API_KEY",
});
const resolvedConfig = buildTalkTestProviderConfig(undefined);
expect(() =>
collectCommandSecretAssignmentsFromSnapshot({
@@ -64,9 +47,7 @@ describe("collectCommandSecretAssignmentsFromSnapshot", () => {
commandName: "memory search",
targetIds: new Set(["talk.providers.*.apiKey"]),
}),
).toThrow(
/memory search: talk\.providers\.elevenlabs\.apiKey is unresolved in the active runtime snapshot/,
);
).toThrow(new RegExp(`memory search: ${TALK_TEST_PROVIDER_API_KEY_PATH} is unresolved`));
});
it("skips unresolved refs that are marked inactive by runtime warnings", () => {

View File

@@ -1,5 +1,9 @@
import { describe, expect, it } from "vitest";
import type { OpenClawConfig } from "../config/config.js";
import {
TALK_TEST_PROVIDER_API_KEY_PATH,
TALK_TEST_PROVIDER_ID,
} from "../test-utils/talk-test-provider.js";
import {
buildConfigureCandidates,
buildConfigureCandidatesForScope,
@@ -13,7 +17,7 @@ describe("secrets configure plan helpers", () => {
const config = {
talk: {
providers: {
elevenlabs: {
[TALK_TEST_PROVIDER_ID]: {
apiKey: "plain", // pragma: allowlist secret
},
},
@@ -27,7 +31,7 @@ describe("secrets configure plan helpers", () => {
const candidates = buildConfigureCandidates(config);
const paths = candidates.map((entry) => entry.path);
expect(paths).toContain("talk.providers.elevenlabs.apiKey");
expect(paths).toContain(TALK_TEST_PROVIDER_API_KEY_PATH);
expect(paths).toContain("channels.telegram.botToken");
});
@@ -89,7 +93,7 @@ describe("secrets configure plan helpers", () => {
config: {
talk: {
providers: {
elevenlabs: {
[TALK_TEST_PROVIDER_ID]: {
apiKey: {
source: "env",
provider: "default",
@@ -121,7 +125,7 @@ describe("secrets configure plan helpers", () => {
expect(candidates).toEqual(
expect.arrayContaining([
expect.objectContaining({
path: "talk.providers.elevenlabs.apiKey",
path: TALK_TEST_PROVIDER_API_KEY_PATH,
existingRef: {
source: "env",
provider: "default",
@@ -144,9 +148,9 @@ describe("secrets configure plan helpers", () => {
const candidates = buildConfigureCandidatesForScope({
config: {
talk: {
provider: "elevenlabs",
provider: TALK_TEST_PROVIDER_ID,
providers: {
elevenlabs: {
[TALK_TEST_PROVIDER_ID]: {
apiKey: "demo-talk-key", // pragma: allowlist secret
},
},
@@ -160,24 +164,22 @@ describe("secrets configure plan helpers", () => {
} as OpenClawConfig,
});
const normalized = candidates.find(
(entry) => entry.path === "talk.providers.elevenlabs.apiKey",
);
const normalized = candidates.find((entry) => entry.path === TALK_TEST_PROVIDER_API_KEY_PATH);
expect(normalized?.isDerived).toBe(true);
});
it("reports configure change presence and builds deterministic plan shape", () => {
const selected = new Map([
[
"talk.providers.elevenlabs.apiKey",
TALK_TEST_PROVIDER_API_KEY_PATH,
{
type: "talk.providers.*.apiKey",
path: "talk.providers.elevenlabs.apiKey",
pathSegments: ["talk", "providers", "elevenlabs", "apiKey"],
label: "talk.providers.elevenlabs.apiKey",
path: TALK_TEST_PROVIDER_API_KEY_PATH,
pathSegments: ["talk", "providers", TALK_TEST_PROVIDER_ID, "apiKey"],
label: TALK_TEST_PROVIDER_API_KEY_PATH,
configFile: "openclaw.json" as const,
expectedResolvedValue: "string" as const,
providerId: "elevenlabs",
providerId: TALK_TEST_PROVIDER_ID,
ref: {
source: "env" as const,
provider: "default",
@@ -205,7 +207,7 @@ describe("secrets configure plan helpers", () => {
generatedAt: "2026-02-28T00:00:00.000Z",
});
expect(plan.targets).toHaveLength(1);
expect(plan.targets[0]?.path).toBe("talk.providers.elevenlabs.apiKey");
expect(plan.targets[0]?.path).toBe(TALK_TEST_PROVIDER_API_KEY_PATH);
expect(plan.providerUpserts).toBeDefined();
expect(plan.options).toEqual({
scrubEnv: true,

View File

@@ -7,6 +7,11 @@ import {
INVALID_EXEC_SECRET_REF_IDS,
VALID_EXEC_SECRET_REF_IDS,
} from "../test-utils/secret-ref-test-vectors.js";
import {
TALK_TEST_PROVIDER_API_KEY_PATH,
TALK_TEST_PROVIDER_API_KEY_PATH_SEGMENTS,
TALK_TEST_PROVIDER_ID,
} from "../test-utils/talk-test-provider.js";
import { isSecretsApplyPlan } from "./plan.js";
import { isValidExecSecretRefId } from "./ref-contract.js";
import { materializePathTokens, parsePathPattern } from "./target-registry-pattern.js";
@@ -43,9 +48,9 @@ describe("exec SecretRef id parity", () => {
targets: [
{
type: "talk.providers.*.apiKey",
path: "talk.providers.elevenlabs.apiKey",
pathSegments: ["talk", "providers", "elevenlabs", "apiKey"],
providerId: "elevenlabs",
path: TALK_TEST_PROVIDER_API_KEY_PATH,
pathSegments: [...TALK_TEST_PROVIDER_API_KEY_PATH_SEGMENTS],
providerId: TALK_TEST_PROVIDER_ID,
ref: { source: "exec", provider: "vault", id },
},
],

View File

@@ -3,6 +3,11 @@ import {
INVALID_EXEC_SECRET_REF_IDS,
VALID_EXEC_SECRET_REF_IDS,
} from "../test-utils/secret-ref-test-vectors.js";
import {
TALK_TEST_PROVIDER_API_KEY_PATH,
TALK_TEST_PROVIDER_API_KEY_PATH_SEGMENTS,
TALK_TEST_PROVIDER_ID,
} from "../test-utils/talk-test-provider.js";
import { isSecretsApplyPlan, resolveValidatedPlanTarget } from "./plan.js";
describe("secrets plan validation", () => {
@@ -59,9 +64,9 @@ describe("secrets plan validation", () => {
targets: [
{
type: "talk.providers.*.apiKey",
path: "talk.providers.elevenlabs.apiKey",
pathSegments: ["talk", "providers", "elevenlabs", "apiKey"],
providerId: "elevenlabs",
path: TALK_TEST_PROVIDER_API_KEY_PATH,
pathSegments: [...TALK_TEST_PROVIDER_API_KEY_PATH_SEGMENTS],
providerId: TALK_TEST_PROVIDER_ID,
ref: { source: "env", provider: "default", id: "TALK_API_KEY" },
},
],
@@ -114,9 +119,9 @@ describe("secrets plan validation", () => {
targets: [
{
type: "talk.providers.*.apiKey",
path: "talk.providers.elevenlabs.apiKey",
pathSegments: ["talk", "providers", "elevenlabs", "apiKey"],
providerId: "elevenlabs",
path: TALK_TEST_PROVIDER_API_KEY_PATH,
pathSegments: [...TALK_TEST_PROVIDER_API_KEY_PATH_SEGMENTS],
providerId: TALK_TEST_PROVIDER_ID,
ref: { source: "exec", provider: "vault", id },
},
],
@@ -135,9 +140,9 @@ describe("secrets plan validation", () => {
targets: [
{
type: "talk.providers.*.apiKey",
path: "talk.providers.elevenlabs.apiKey",
pathSegments: ["talk", "providers", "elevenlabs", "apiKey"],
providerId: "elevenlabs",
path: TALK_TEST_PROVIDER_API_KEY_PATH,
pathSegments: [...TALK_TEST_PROVIDER_API_KEY_PATH_SEGMENTS],
providerId: TALK_TEST_PROVIDER_ID,
ref: { source: "exec", provider: "vault", id },
},
],

View File

@@ -2,6 +2,11 @@ import fs from "node:fs";
import path from "node:path";
import { describe, expect, it } from "vitest";
import type { OpenClawConfig } from "../config/config.js";
import {
buildTalkTestProviderConfig,
TALK_TEST_PROVIDER_API_KEY_PATH,
TALK_TEST_PROVIDER_ID,
} from "../test-utils/talk-test-provider.js";
import { buildSecretRefCredentialMatrix } from "./credential-matrix.js";
import {
discoverConfigSecretTargetsByIds,
@@ -83,13 +88,7 @@ describe("secret target registry", () => {
it("supports filtered discovery by target ids", () => {
const targets = discoverConfigSecretTargetsByIds(
{
talk: {
providers: {
elevenlabs: {
apiKey: { source: "env", provider: "default", id: "TALK_API_KEY" },
},
},
},
...buildTalkTestProviderConfig({ source: "env", provider: "default", id: "TALK_API_KEY" }),
gateway: {
remote: {
token: { source: "env", provider: "default", id: "REMOTE_TOKEN" },
@@ -101,7 +100,8 @@ describe("secret target registry", () => {
expect(targets).toHaveLength(1);
expect(targets[0]?.entry.id).toBe("talk.providers.*.apiKey");
expect(targets[0]?.path).toBe("talk.providers.elevenlabs.apiKey");
expect(targets[0]?.providerId).toBe(TALK_TEST_PROVIDER_ID);
expect(targets[0]?.path).toBe(TALK_TEST_PROVIDER_API_KEY_PATH);
});
it("resolves config targets by exact path including sibling ref metadata", () => {