mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 15:30:39 +00:00
test(refactor): dedupe secret resolver posix fixtures and add registry cache regression
This commit is contained in:
@@ -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", () => {
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user