9993c96907
Move provider auth and OAuth flows onto pi-ai Models, compose models.json and extension overlays through ModelRuntime, and retain ModelRegistry as an extension compatibility facade.
122 lines
4.7 KiB
TypeScript
122 lines
4.7 KiB
TypeScript
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
|
import {
|
|
clearConfigValueCache,
|
|
resolveConfigValue,
|
|
resolveConfigValueUncached,
|
|
} from "../src/core/resolve-config-value.ts";
|
|
import * as shellModule from "../src/utils/shell.ts";
|
|
|
|
describe("resolveConfigValue", () => {
|
|
let tempDir: string;
|
|
|
|
beforeEach(() => {
|
|
tempDir = join(tmpdir(), `pi-config-value-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
mkdirSync(tempDir, { recursive: true });
|
|
clearConfigValueCache();
|
|
});
|
|
|
|
afterEach(() => {
|
|
if (existsSync(tempDir)) rmSync(tempDir, { recursive: true });
|
|
clearConfigValueCache();
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
test("resolves literals, environment templates, and escapes", () => {
|
|
process.env.TEST_CONFIG_LEFT = "left";
|
|
process.env.TEST_CONFIG_RIGHT = "right";
|
|
try {
|
|
expect(resolveConfigValue("literal-key")).toBe("literal-key");
|
|
expect(resolveConfigValue("$TEST_CONFIG_LEFT")).toBe("left");
|
|
expect(resolveConfigValue("$" + "{TEST_CONFIG_LEFT}_$TEST_CONFIG_RIGHT")).toBe("left_right");
|
|
expect(resolveConfigValue("$$TEST_CONFIG_LEFT")).toBe("$TEST_CONFIG_LEFT");
|
|
expect(resolveConfigValue("$!literal-$TEST_CONFIG_RIGHT")).toBe("!literal-right");
|
|
} finally {
|
|
delete process.env.TEST_CONFIG_LEFT;
|
|
delete process.env.TEST_CONFIG_RIGHT;
|
|
}
|
|
});
|
|
|
|
test("uses credential-scoped environment before process.env", () => {
|
|
process.env.TEST_CONFIG_SCOPED = "process";
|
|
try {
|
|
expect(resolveConfigValue("$TEST_CONFIG_SCOPED", { TEST_CONFIG_SCOPED: "credential" })).toBe("credential");
|
|
} finally {
|
|
delete process.env.TEST_CONFIG_SCOPED;
|
|
}
|
|
});
|
|
|
|
test("executes shell commands and trims their output", () => {
|
|
expect(resolveConfigValue("!echo ' spaced-key '")).toBe("spaced-key");
|
|
expect(resolveConfigValue("!printf 'line1\\nline2'")).toBe("line1\nline2");
|
|
expect(resolveConfigValue("!echo 'hello world' | tr ' ' '-'")).toBe("hello-world");
|
|
});
|
|
|
|
test.each(["!exit 1", "!nonexistent-command-12345", "!printf ''"])(
|
|
"returns undefined when command resolution fails: %s",
|
|
(command) => {
|
|
expect(resolveConfigValue(command)).toBeUndefined();
|
|
},
|
|
);
|
|
|
|
test("caches successful and failed commands until explicitly cleared", () => {
|
|
const counterFile = join(tempDir, "counter");
|
|
writeFileSync(counterFile, "0");
|
|
const escapedPath = counterFile.replace(/\\/g, "/").replace(/"/g, '\\"');
|
|
const success = `!sh -c 'count=$(cat "${escapedPath}"); echo $((count + 1)) > "${escapedPath}"; echo value'`;
|
|
|
|
expect(resolveConfigValue(success)).toBe("value");
|
|
expect(resolveConfigValue(success)).toBe("value");
|
|
expect(readFileSync(counterFile, "utf-8").trim()).toBe("1");
|
|
|
|
clearConfigValueCache();
|
|
expect(resolveConfigValue(success)).toBe("value");
|
|
expect(readFileSync(counterFile, "utf-8").trim()).toBe("2");
|
|
|
|
const failure = `!sh -c 'count=$(cat "${escapedPath}"); echo $((count + 1)) > "${escapedPath}"; exit 1'`;
|
|
expect(resolveConfigValue(failure)).toBeUndefined();
|
|
expect(resolveConfigValue(failure)).toBeUndefined();
|
|
expect(readFileSync(counterFile, "utf-8").trim()).toBe("3");
|
|
});
|
|
|
|
test("does not cache environment values", () => {
|
|
process.env.TEST_CONFIG_DYNAMIC = "first";
|
|
try {
|
|
expect(resolveConfigValue("$TEST_CONFIG_DYNAMIC")).toBe("first");
|
|
process.env.TEST_CONFIG_DYNAMIC = "second";
|
|
expect(resolveConfigValue("$TEST_CONFIG_DYNAMIC")).toBe("second");
|
|
} finally {
|
|
delete process.env.TEST_CONFIG_DYNAMIC;
|
|
}
|
|
});
|
|
|
|
test("uncached resolution executes a command on every call", () => {
|
|
const counterFile = join(tempDir, "uncached-counter");
|
|
writeFileSync(counterFile, "0");
|
|
const escapedPath = counterFile.replace(/\\/g, "/").replace(/"/g, '\\"');
|
|
const command = `!sh -c 'count=$(cat "${escapedPath}"); echo $((count + 1)) > "${escapedPath}"; echo value'`;
|
|
expect(resolveConfigValueUncached(command)).toBe("value");
|
|
expect(resolveConfigValueUncached(command)).toBe("value");
|
|
expect(readFileSync(counterFile, "utf-8").trim()).toBe("2");
|
|
});
|
|
|
|
test("uses stdin when the configured Windows shell requires it", () => {
|
|
if (process.platform === "win32") return;
|
|
const platformDescriptor = Object.getOwnPropertyDescriptor(process, "platform");
|
|
vi.spyOn(shellModule, "getShellConfig").mockReturnValue({
|
|
shell: "/bin/bash",
|
|
args: ["-s"],
|
|
commandTransport: "stdin",
|
|
});
|
|
try {
|
|
Object.defineProperty(process, "platform", { configurable: true, value: "win32" });
|
|
const expansion = "$" + "{name}";
|
|
expect(resolveConfigValueUncached(`!name='World'; echo "Hello, ${expansion}!"`)).toBe("Hello, World!");
|
|
} finally {
|
|
if (platformDescriptor) Object.defineProperty(process, "platform", platformDescriptor);
|
|
}
|
|
});
|
|
});
|