mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-26 20:35:39 -06:00
edecdbd05e
* refactor(config): consolidate media model lists * refactor(config): unify memory configuration * refactor(config): consolidate TTS ownership * refactor(config): move typing policy to agents * refactor(config): retire product-level config surfaces * refactor(config): share scoped tool policy type * chore(config): refresh generated baselines * fix(config): honor agent typing overrides * fix(config): migrate sibling config consumers * refactor(infra): keep base64url decoder private * fix(config): strip invalid legacy TTS values * chore(config): refresh rebased baseline hash * fix(doctor): route legacy messages.tts.realtime voice to talk during tts move * refactor(config): polish final layout names * refactor(config): freeze retired tuning defaults * feat(config): add fast mode default symmetry * refactor(config): key agent entries by id * docs(config): update final layout reference * test(config): cover final layout migrations * chore(config): refresh final layout baselines * fix(config): align final layout runtime readers * fix(config): align remaining readers * fix(config): stabilize final layout migrations * fix(config): finalize config projection proof * fix(config): address final layout review * docs(release): preserve historical config names * fix(config): complete keyed agent migration * fix(config): close final migration gaps * fix(config): finish full-branch review * fix(config): complete runtime secret detection * fix(config): close final review findings * fix(config): finish canonical docs and heartbeat migration * fix(config): integrate latest main after rebase * refactor(env): isolate test-only controls * refactor(env): isolate build and development controls * refactor(env): collapse process identity indirection * refactor(env): remove duplicate config and temp aliases * docs(env): define the operator-facing allowlist * ci(env): ratchet production variable count * fix(env): remove stale provider helper import * fix(env): make ratchet sorting explicit * test(env): keep test seam in dead-code audit * test(env): cover ratchet growth and boundary; document surface budgets * docs(config): document tier-eval consolidations * docs(config): clarify speech preference ownership * test(memory): align retired tuning fixtures * refactor(memory): freeze engine heuristics * refactor(config): apply tier-eval tranche * refactor(tts): move persona shaping to providers * refactor(compaction): move prompt policy to providers * test(config): align hookified prompt fixtures * chore(deadcode): classify test-only exports * chore(github): remove unused spawn helper * chore(deadcode): classify queue diagnostics * chore(deadcode): remove unused lane snapshot export * chore(plugin-sdk): ratchet consolidated surface * fix(config): integrate latest main after rebase
319 lines
8.9 KiB
TypeScript
319 lines
8.9 KiB
TypeScript
// Verifies secrets schema parsing and validation behavior.
|
|
import { describe, expect, it } from "vitest";
|
|
import {
|
|
INVALID_EXEC_SECRET_REF_IDS,
|
|
VALID_EXEC_SECRET_REF_IDS,
|
|
} from "../test-utils/secret-ref-test-vectors.js";
|
|
import { validateConfigObjectRaw } from "./validation.js";
|
|
|
|
function validateOpenAiApiKeyRef(apiKey: unknown) {
|
|
return validateConfigObjectRaw({
|
|
models: {
|
|
providers: {
|
|
openai: {
|
|
baseUrl: "https://api.openai.com/v1",
|
|
apiKey,
|
|
models: [{ id: "gpt-5", name: "gpt-5" }],
|
|
},
|
|
},
|
|
},
|
|
});
|
|
}
|
|
|
|
describe("config secret refs schema", () => {
|
|
it("accepts top-level secrets sources and model apiKey refs", () => {
|
|
const result = validateConfigObjectRaw({
|
|
secrets: {
|
|
providers: {
|
|
default: { source: "env" },
|
|
filemain: {
|
|
source: "file",
|
|
path: "~/.openclaw/secrets.json",
|
|
mode: "json",
|
|
timeoutMs: 10_000,
|
|
},
|
|
vault: {
|
|
source: "exec",
|
|
command: "/usr/local/bin/openclaw-secret-resolver",
|
|
args: ["resolve"],
|
|
},
|
|
},
|
|
},
|
|
models: {
|
|
providers: {
|
|
openai: {
|
|
baseUrl: "https://api.openai.com/v1",
|
|
apiKey: { source: "env", provider: "default", id: "OPENAI_API_KEY" },
|
|
models: [{ id: "gpt-5", name: "gpt-5" }],
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
expect(result.ok).toBe(true);
|
|
});
|
|
|
|
it("accepts openai-chatgpt-responses as a model api value", () => {
|
|
const result = validateConfigObjectRaw({
|
|
models: {
|
|
providers: {
|
|
openai: {
|
|
baseUrl: "https://chatgpt.com/backend-api",
|
|
api: "openai-chatgpt-responses",
|
|
models: [{ id: "gpt-5.4", name: "gpt-5.4" }],
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
expect(result.ok).toBe(true);
|
|
});
|
|
|
|
it("accepts skills entry apiKey refs", () => {
|
|
const result = validateConfigObjectRaw({
|
|
skills: {
|
|
entries: {
|
|
"review-pr": {
|
|
enabled: true,
|
|
apiKey: { source: "env", provider: "default", id: "SKILL_REVIEW_PR_API_KEY" },
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
expect(result.ok).toBe(true);
|
|
});
|
|
|
|
it("accepts media request secret refs for auth, headers, and tls material", () => {
|
|
const result = validateConfigObjectRaw({
|
|
tools: {
|
|
media: {
|
|
audio: {
|
|
enabled: true,
|
|
request: {
|
|
headers: {
|
|
"X-Tenant": { source: "env", provider: "default", id: "MEDIA_TENANT_HEADER" },
|
|
},
|
|
auth: {
|
|
mode: "authorization-bearer",
|
|
token: { source: "env", provider: "default", id: "MEDIA_AUDIO_TOKEN" },
|
|
},
|
|
proxy: {
|
|
mode: "explicit-proxy",
|
|
url: "http://proxy.example:8080",
|
|
tls: {
|
|
ca: { source: "file", provider: "filemain", id: "/tls/proxy-ca" },
|
|
},
|
|
},
|
|
tls: {
|
|
cert: { source: "file", provider: "filemain", id: "/tls/client-cert" },
|
|
key: { source: "file", provider: "filemain", id: "/tls/client-key" },
|
|
passphrase: { source: "exec", provider: "vault", id: "media/audio/passphrase" },
|
|
},
|
|
},
|
|
},
|
|
models: [{ provider: "openai", model: "gpt-4o-mini-transcribe" }],
|
|
},
|
|
},
|
|
});
|
|
|
|
expect(result.ok).toBe(true);
|
|
});
|
|
|
|
it("accepts model provider request secret refs for auth, headers, and tls material", () => {
|
|
const result = validateConfigObjectRaw({
|
|
models: {
|
|
providers: {
|
|
openai: {
|
|
baseUrl: "https://api.openai.com/v1",
|
|
request: {
|
|
headers: {
|
|
"X-Tenant": { source: "env", provider: "default", id: "OPENAI_TENANT_HEADER" },
|
|
},
|
|
auth: {
|
|
mode: "authorization-bearer",
|
|
token: { source: "env", provider: "default", id: "OPENAI_PROVIDER_TOKEN" },
|
|
},
|
|
proxy: {
|
|
mode: "explicit-proxy",
|
|
url: "http://proxy.example:8080",
|
|
tls: {
|
|
ca: { source: "file", provider: "filemain", id: "/tls/provider-proxy-ca" },
|
|
},
|
|
},
|
|
tls: {
|
|
cert: { source: "file", provider: "filemain", id: "/tls/provider-cert" },
|
|
key: { source: "file", provider: "filemain", id: "/tls/provider-key" },
|
|
},
|
|
},
|
|
models: [{ id: "gpt-5", name: "gpt-5" }],
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
expect(result.ok).toBe(true);
|
|
});
|
|
|
|
it("accepts model provider header SecretRef values", () => {
|
|
const result = validateConfigObjectRaw({
|
|
models: {
|
|
providers: {
|
|
openai: {
|
|
baseUrl: "https://api.openai.com/v1",
|
|
api: "openai-completions",
|
|
headers: {
|
|
Authorization: {
|
|
source: "env",
|
|
provider: "default",
|
|
id: "OPENAI_HEADER_TOKEN",
|
|
},
|
|
},
|
|
models: [],
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
expect(result.ok).toBe(true);
|
|
if (result.ok) {
|
|
expect(result.config.models?.providers?.openai?.headers?.Authorization).toEqual({
|
|
source: "env",
|
|
provider: "default",
|
|
id: "OPENAI_HEADER_TOKEN",
|
|
});
|
|
}
|
|
});
|
|
|
|
it("rejects model provider request proxy url secret refs", () => {
|
|
const result = validateConfigObjectRaw({
|
|
models: {
|
|
providers: {
|
|
openai: {
|
|
baseUrl: "https://api.openai.com/v1",
|
|
request: {
|
|
proxy: {
|
|
mode: "explicit-proxy",
|
|
url: { source: "env", provider: "default", id: "PROVIDER_PROXY_URL" },
|
|
},
|
|
},
|
|
models: [{ id: "gpt-5", name: "gpt-5" }],
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
expect(result.ok).toBe(false);
|
|
if (!result.ok) {
|
|
expect(
|
|
result.issues.some((issue) => issue.path.includes("models.providers.openai.request.proxy")),
|
|
).toBe(true);
|
|
}
|
|
});
|
|
|
|
it('accepts file refs with id "value" for singleValue mode providers', () => {
|
|
const result = validateConfigObjectRaw({
|
|
secrets: {
|
|
providers: {
|
|
rawfile: {
|
|
source: "file",
|
|
path: "~/.openclaw/token.txt",
|
|
mode: "singleValue",
|
|
},
|
|
},
|
|
},
|
|
models: {
|
|
providers: {
|
|
openai: {
|
|
baseUrl: "https://api.openai.com/v1",
|
|
apiKey: { source: "file", provider: "rawfile", id: "value" },
|
|
models: [{ id: "gpt-5", name: "gpt-5" }],
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
expect(result.ok).toBe(true);
|
|
});
|
|
|
|
it("rejects invalid secret ref id", () => {
|
|
const result = validateOpenAiApiKeyRef({
|
|
source: "env",
|
|
provider: "default",
|
|
id: "bad id with spaces",
|
|
});
|
|
|
|
expect(result.ok).toBe(false);
|
|
if (!result.ok) {
|
|
expect(
|
|
result.issues.some((issue) => issue.path.includes("models.providers.openai.apiKey")),
|
|
).toBe(true);
|
|
}
|
|
});
|
|
|
|
it("rejects env refs that are not env var names", () => {
|
|
const result = validateOpenAiApiKeyRef({
|
|
source: "env",
|
|
provider: "default",
|
|
id: "/providers/openai/apiKey",
|
|
});
|
|
|
|
expect(result.ok).toBe(false);
|
|
if (!result.ok) {
|
|
expect(
|
|
result.issues.some(
|
|
(issue) =>
|
|
issue.path.includes("models.providers.openai.apiKey") &&
|
|
issue.message.includes("Env secret reference id"),
|
|
),
|
|
).toBe(true);
|
|
}
|
|
});
|
|
|
|
it("rejects file refs that are not absolute JSON pointers", () => {
|
|
const result = validateOpenAiApiKeyRef({
|
|
source: "file",
|
|
provider: "default",
|
|
id: "providers/openai/apiKey",
|
|
});
|
|
|
|
expect(result.ok).toBe(false);
|
|
if (!result.ok) {
|
|
expect(
|
|
result.issues.some(
|
|
(issue) =>
|
|
issue.path.includes("models.providers.openai.apiKey") &&
|
|
issue.message.includes("absolute JSON pointer"),
|
|
),
|
|
).toBe(true);
|
|
}
|
|
});
|
|
|
|
it("accepts valid exec secret reference ids", () => {
|
|
for (const id of VALID_EXEC_SECRET_REF_IDS) {
|
|
const result = validateOpenAiApiKeyRef({
|
|
source: "exec",
|
|
provider: "vault",
|
|
id,
|
|
});
|
|
expect(result.ok, `expected valid exec ref id: ${id}`).toBe(true);
|
|
}
|
|
});
|
|
|
|
it("rejects invalid exec secret reference ids", () => {
|
|
for (const id of INVALID_EXEC_SECRET_REF_IDS) {
|
|
const result = validateOpenAiApiKeyRef({
|
|
source: "exec",
|
|
provider: "vault",
|
|
id,
|
|
});
|
|
expect(result.ok, `expected invalid exec ref id: ${id}`).toBe(false);
|
|
if (!result.ok) {
|
|
expect(
|
|
result.issues.some((issue) => issue.path.includes("models.providers.openai.apiKey")),
|
|
).toBe(true);
|
|
}
|
|
}
|
|
});
|
|
});
|