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