test(refactor): dedupe secret resolver posix fixtures and add registry cache regression

This commit is contained in:
Peter Steinberger
2026-03-03 00:05:21 +00:00
parent 1d0a4d1be2
commit 4bfbf2dfff
2 changed files with 99 additions and 101 deletions

View File

@@ -75,6 +75,29 @@ describe("channel plugin registry", () => {
const pluginIds = listChannelPlugins().map((plugin) => plugin.id);
expect(pluginIds).toEqual(["telegram", "slack", "signal"]);
});
it("refreshes cached channel lookups when the same registry instance is re-activated", () => {
const registry = createTestRegistry([
{
pluginId: "slack",
plugin: createPlugin("slack"),
source: "test",
},
]);
setActivePluginRegistry(registry, "registry-test");
expect(listChannelPlugins().map((plugin) => plugin.id)).toEqual(["slack"]);
registry.channels = [
{
pluginId: "telegram",
plugin: createPlugin("telegram"),
source: "test",
},
] as typeof registry.channels;
setActivePluginRegistry(registry, "registry-test");
expect(listChannelPlugins().map((plugin) => plugin.id)).toEqual(["telegram"]);
});
});
describe("channel plugin catalog", () => {

View File

@@ -12,6 +12,14 @@ async function writeSecureFile(filePath: string, content: string, mode = 0o600):
}
describe("secret ref resolver", () => {
const isWindows = process.platform === "win32";
function itPosix(name: string, fn: () => Promise<void> | void) {
if (isWindows) {
it.skip(name, fn);
return;
}
it(name, fn);
}
let fixtureRoot = "";
let caseId = 0;
let execProtocolV1ScriptPath = "";
@@ -36,6 +44,12 @@ describe("secret ref resolver", () => {
trustedDirs?: string[];
args?: string[];
};
type FileProviderConfig = {
source: "file";
path: string;
mode: "json" | "singleValue";
timeoutMs?: number;
};
function createExecProviderConfig(
command: string,
@@ -67,6 +81,18 @@ describe("secret ref resolver", () => {
);
}
function createFileProviderConfig(
filePath: string,
overrides: Partial<FileProviderConfig> = {},
): FileProviderConfig {
return {
source: "file",
path: filePath,
mode: "json",
...overrides,
};
}
beforeAll(async () => {
fixtureRoot = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-secrets-resolve-"));
const sharedExecDir = path.join(fixtureRoot, "shared-exec");
@@ -133,10 +159,7 @@ describe("secret ref resolver", () => {
expect(value).toBe("sk-env-value");
});
it("resolves file refs in json mode", async () => {
if (process.platform === "win32") {
return;
}
itPosix("resolves file refs in json mode", async () => {
const root = await createCaseDir("file");
const filePath = path.join(root, "secrets.json");
await writeSecureFile(
@@ -156,11 +179,7 @@ describe("secret ref resolver", () => {
config: {
secrets: {
providers: {
filemain: {
source: "file",
path: filePath,
mode: "json",
},
filemain: createFileProviderConfig(filePath),
},
},
},
@@ -169,19 +188,12 @@ describe("secret ref resolver", () => {
expect(value).toBe("sk-file-value");
});
it("resolves exec refs with protocolVersion 1 response", async () => {
if (process.platform === "win32") {
return;
}
itPosix("resolves exec refs with protocolVersion 1 response", async () => {
const value = await resolveExecSecret(execProtocolV1ScriptPath);
expect(value).toBe("value:openai/api-key");
});
it("uses timeoutMs as the default no-output timeout for exec providers", async () => {
if (process.platform === "win32") {
return;
}
itPosix("uses timeoutMs as the default no-output timeout for exec providers", async () => {
const root = await createCaseDir("exec-delay");
const scriptPath = path.join(root, "resolver-delay.mjs");
await writeSecureFile(
@@ -215,19 +227,12 @@ describe("secret ref resolver", () => {
expect(value).toBe("ok");
});
it("supports non-JSON single-value exec output when jsonOnly is false", async () => {
if (process.platform === "win32") {
return;
}
itPosix("supports non-JSON single-value exec output when jsonOnly is false", async () => {
const value = await resolveExecSecret(execPlainScriptPath, { jsonOnly: false });
expect(value).toBe("plain-secret");
});
it("ignores EPIPE when exec provider exits before consuming stdin", async () => {
if (process.platform === "win32") {
return;
}
itPosix("ignores EPIPE when exec provider exits before consuming stdin", async () => {
const oversizedId = `openai/${"x".repeat(120_000)}`;
await expect(
resolveSecretRefString(
@@ -248,10 +253,7 @@ describe("secret ref resolver", () => {
).rejects.toThrow('Exec provider "execmain" returned empty stdout.');
});
it("rejects symlink command paths unless allowSymlinkCommand is enabled", async () => {
if (process.platform === "win32") {
return;
}
itPosix("rejects symlink command paths unless allowSymlinkCommand is enabled", async () => {
const root = await createCaseDir("exec-link-reject");
const symlinkPath = path.join(root, "resolver-link.mjs");
await fs.symlink(execPlainScriptPath, symlinkPath);
@@ -261,10 +263,7 @@ describe("secret ref resolver", () => {
);
});
it("allows symlink command paths when allowSymlinkCommand is enabled", async () => {
if (process.platform === "win32") {
return;
}
itPosix("allows symlink command paths when allowSymlinkCommand is enabled", async () => {
const root = await createCaseDir("exec-link-allow");
const symlinkPath = path.join(root, "resolver-link.mjs");
await fs.symlink(execPlainScriptPath, symlinkPath);
@@ -278,47 +277,43 @@ describe("secret ref resolver", () => {
expect(value).toBe("plain-secret");
});
it("handles Homebrew-style symlinked exec commands with args only when explicitly allowed", async () => {
if (process.platform === "win32") {
return;
}
itPosix(
"handles Homebrew-style symlinked exec commands with args only when explicitly allowed",
async () => {
const root = await createCaseDir("homebrew");
const binDir = path.join(root, "opt", "homebrew", "bin");
const cellarDir = path.join(root, "opt", "homebrew", "Cellar", "node", "25.0.0", "bin");
await fs.mkdir(binDir, { recursive: true });
await fs.mkdir(cellarDir, { recursive: true });
const root = await createCaseDir("homebrew");
const binDir = path.join(root, "opt", "homebrew", "bin");
const cellarDir = path.join(root, "opt", "homebrew", "Cellar", "node", "25.0.0", "bin");
await fs.mkdir(binDir, { recursive: true });
await fs.mkdir(cellarDir, { recursive: true });
const targetCommand = path.join(cellarDir, "node");
const symlinkCommand = path.join(binDir, "node");
await writeSecureFile(
targetCommand,
[
"#!/bin/sh",
'suffix="${1:-missing}"',
'printf \'{"protocolVersion":1,"values":{"openai/api-key":"%s:openai/api-key"}}\' "$suffix"',
].join("\n"),
0o700,
);
await fs.symlink(targetCommand, symlinkCommand);
const trustedRoot = await fs.realpath(root);
const targetCommand = path.join(cellarDir, "node");
const symlinkCommand = path.join(binDir, "node");
await writeSecureFile(
targetCommand,
[
"#!/bin/sh",
'suffix="${1:-missing}"',
'printf \'{"protocolVersion":1,"values":{"openai/api-key":"%s:openai/api-key"}}\' "$suffix"',
].join("\n"),
0o700,
);
await fs.symlink(targetCommand, symlinkCommand);
const trustedRoot = await fs.realpath(root);
await expect(resolveExecSecret(symlinkCommand, { args: ["brew"] })).rejects.toThrow(
"must not be a symlink",
);
await expect(resolveExecSecret(symlinkCommand, { args: ["brew"] })).rejects.toThrow(
"must not be a symlink",
);
const value = await resolveExecSecret(symlinkCommand, {
args: ["brew"],
allowSymlinkCommand: true,
trustedDirs: [trustedRoot],
});
expect(value).toBe("brew:openai/api-key");
},
);
const value = await resolveExecSecret(symlinkCommand, {
args: ["brew"],
allowSymlinkCommand: true,
trustedDirs: [trustedRoot],
});
expect(value).toBe("brew:openai/api-key");
});
it("checks trustedDirs against resolved symlink target", async () => {
if (process.platform === "win32") {
return;
}
itPosix("checks trustedDirs against resolved symlink target", async () => {
const root = await createCaseDir("exec-link-trusted");
const symlinkPath = path.join(root, "resolver-link.mjs");
await fs.symlink(execPlainScriptPath, symlinkPath);
@@ -332,37 +327,25 @@ describe("secret ref resolver", () => {
).rejects.toThrow("outside trustedDirs");
});
it("rejects exec refs when protocolVersion is not 1", async () => {
if (process.platform === "win32") {
return;
}
itPosix("rejects exec refs when protocolVersion is not 1", async () => {
await expect(resolveExecSecret(execProtocolV2ScriptPath)).rejects.toThrow(
"protocolVersion must be 1",
);
});
it("rejects exec refs when response omits requested id", async () => {
if (process.platform === "win32") {
return;
}
itPosix("rejects exec refs when response omits requested id", async () => {
await expect(resolveExecSecret(execMissingIdScriptPath)).rejects.toThrow(
'response missing id "openai/api-key"',
);
});
it("rejects exec refs with invalid JSON when jsonOnly is true", async () => {
if (process.platform === "win32") {
return;
}
itPosix("rejects exec refs with invalid JSON when jsonOnly is true", async () => {
await expect(resolveExecSecret(execInvalidJsonScriptPath, { jsonOnly: true })).rejects.toThrow(
"returned invalid JSON",
);
});
it("supports file singleValue mode with id=value", async () => {
if (process.platform === "win32") {
return;
}
itPosix("supports file singleValue mode with id=value", async () => {
const root = await createCaseDir("file-single-value");
const filePath = path.join(root, "token.txt");
await writeSecureFile(filePath, "raw-token-value\n");
@@ -373,11 +356,9 @@ describe("secret ref resolver", () => {
config: {
secrets: {
providers: {
rawfile: {
source: "file",
path: filePath,
rawfile: createFileProviderConfig(filePath, {
mode: "singleValue",
},
}),
},
},
},
@@ -386,10 +367,7 @@ describe("secret ref resolver", () => {
expect(value).toBe("raw-token-value");
});
it("times out file provider reads when timeoutMs elapses", async () => {
if (process.platform === "win32") {
return;
}
itPosix("times out file provider reads when timeoutMs elapses", async () => {
const root = await createCaseDir("file-timeout");
const filePath = path.join(root, "secrets.json");
await writeSecureFile(
@@ -422,12 +400,9 @@ describe("secret ref resolver", () => {
config: {
secrets: {
providers: {
filemain: {
source: "file",
path: filePath,
mode: "json",
filemain: createFileProviderConfig(filePath, {
timeoutMs: 5,
},
}),
},
},
},