mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 07:20:45 +00:00
test(config): reuse fixtures for faster validation
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterAll, describe, expect, it } from "vitest";
|
||||
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
||||
import { clearPluginManifestRegistryCache } from "../plugins/manifest-registry.js";
|
||||
import { validateConfigObjectWithPlugins } from "./config.js";
|
||||
|
||||
async function writePluginFixture(params: {
|
||||
@@ -31,27 +32,44 @@ async function writePluginFixture(params: {
|
||||
}
|
||||
|
||||
describe("config plugin validation", () => {
|
||||
const fixtureRoot = path.join(os.tmpdir(), "openclaw-config-plugin-validation");
|
||||
let caseIndex = 0;
|
||||
let fixtureRoot = "";
|
||||
let suiteHome = "";
|
||||
const envSnapshot = {
|
||||
OPENCLAW_STATE_DIR: process.env.OPENCLAW_STATE_DIR,
|
||||
OPENCLAW_PLUGIN_MANIFEST_CACHE_MS: process.env.OPENCLAW_PLUGIN_MANIFEST_CACHE_MS,
|
||||
};
|
||||
|
||||
function createCaseHome() {
|
||||
const home = path.join(fixtureRoot, `case-${caseIndex++}`);
|
||||
return fs.mkdir(home, { recursive: true }).then(() => home);
|
||||
}
|
||||
|
||||
const validateInHome = (home: string, raw: unknown) => {
|
||||
process.env.OPENCLAW_STATE_DIR = path.join(home, ".openclaw");
|
||||
const validateInSuite = (raw: unknown) => {
|
||||
process.env.OPENCLAW_STATE_DIR = path.join(suiteHome, ".openclaw");
|
||||
return validateConfigObjectWithPlugins(raw);
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
fixtureRoot = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-config-plugin-validation-"));
|
||||
suiteHome = path.join(fixtureRoot, "home");
|
||||
await fs.mkdir(suiteHome, { recursive: true });
|
||||
process.env.OPENCLAW_PLUGIN_MANIFEST_CACHE_MS = "10000";
|
||||
clearPluginManifestRegistryCache();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await fs.rm(fixtureRoot, { recursive: true, force: true });
|
||||
clearPluginManifestRegistryCache();
|
||||
if (envSnapshot.OPENCLAW_STATE_DIR === undefined) {
|
||||
delete process.env.OPENCLAW_STATE_DIR;
|
||||
} else {
|
||||
process.env.OPENCLAW_STATE_DIR = envSnapshot.OPENCLAW_STATE_DIR;
|
||||
}
|
||||
if (envSnapshot.OPENCLAW_PLUGIN_MANIFEST_CACHE_MS === undefined) {
|
||||
delete process.env.OPENCLAW_PLUGIN_MANIFEST_CACHE_MS;
|
||||
} else {
|
||||
process.env.OPENCLAW_PLUGIN_MANIFEST_CACHE_MS = envSnapshot.OPENCLAW_PLUGIN_MANIFEST_CACHE_MS;
|
||||
}
|
||||
});
|
||||
|
||||
it("rejects missing plugin load paths", async () => {
|
||||
const home = await createCaseHome();
|
||||
const missingPath = path.join(home, "missing-plugin");
|
||||
const res = validateInHome(home, {
|
||||
const missingPath = path.join(suiteHome, "missing-plugin");
|
||||
const res = validateInSuite({
|
||||
agents: { list: [{ id: "pi" }] },
|
||||
plugins: { enabled: false, load: { paths: [missingPath] } },
|
||||
});
|
||||
@@ -66,8 +84,7 @@ describe("config plugin validation", () => {
|
||||
});
|
||||
|
||||
it("warns for missing plugin ids in entries instead of failing validation", async () => {
|
||||
const home = await createCaseHome();
|
||||
const res = validateInHome(home, {
|
||||
const res = validateInSuite({
|
||||
agents: { list: [{ id: "pi" }] },
|
||||
plugins: { enabled: false, entries: { "missing-plugin": { enabled: true } } },
|
||||
});
|
||||
@@ -82,8 +99,7 @@ describe("config plugin validation", () => {
|
||||
});
|
||||
|
||||
it("rejects missing plugin ids in allow/deny/slots", async () => {
|
||||
const home = await createCaseHome();
|
||||
const res = validateInHome(home, {
|
||||
const res = validateInSuite({
|
||||
agents: { list: [{ id: "pi" }] },
|
||||
plugins: {
|
||||
enabled: false,
|
||||
@@ -105,9 +121,8 @@ describe("config plugin validation", () => {
|
||||
});
|
||||
|
||||
it("warns for removed legacy plugin ids instead of failing validation", async () => {
|
||||
const home = await createCaseHome();
|
||||
const removedId = "google-antigravity-auth";
|
||||
const res = validateInHome(home, {
|
||||
const res = validateInSuite({
|
||||
agents: { list: [{ id: "pi" }] },
|
||||
plugins: {
|
||||
enabled: false,
|
||||
@@ -147,8 +162,7 @@ describe("config plugin validation", () => {
|
||||
});
|
||||
|
||||
it("surfaces plugin config diagnostics", async () => {
|
||||
const home = await createCaseHome();
|
||||
const pluginDir = path.join(home, "bad-plugin");
|
||||
const pluginDir = path.join(suiteHome, "bad-plugin");
|
||||
await writePluginFixture({
|
||||
dir: pluginDir,
|
||||
id: "bad-plugin",
|
||||
@@ -162,7 +176,7 @@ describe("config plugin validation", () => {
|
||||
},
|
||||
});
|
||||
|
||||
const res = validateInHome(home, {
|
||||
const res = validateInSuite({
|
||||
agents: { list: [{ id: "pi" }] },
|
||||
plugins: {
|
||||
enabled: true,
|
||||
@@ -182,8 +196,7 @@ describe("config plugin validation", () => {
|
||||
});
|
||||
|
||||
it("accepts known plugin ids", async () => {
|
||||
const home = await createCaseHome();
|
||||
const res = validateInHome(home, {
|
||||
const res = validateInSuite({
|
||||
agents: { list: [{ id: "pi" }] },
|
||||
plugins: { enabled: false, entries: { discord: { enabled: true } } },
|
||||
});
|
||||
@@ -191,8 +204,7 @@ describe("config plugin validation", () => {
|
||||
});
|
||||
|
||||
it("accepts channels.modelByChannel", async () => {
|
||||
const home = await createCaseHome();
|
||||
const res = validateInHome(home, {
|
||||
const res = validateInSuite({
|
||||
agents: { list: [{ id: "pi" }] },
|
||||
channels: {
|
||||
modelByChannel: {
|
||||
@@ -206,8 +218,7 @@ describe("config plugin validation", () => {
|
||||
});
|
||||
|
||||
it("accepts plugin heartbeat targets", async () => {
|
||||
const home = await createCaseHome();
|
||||
const pluginDir = path.join(home, "bluebubbles-plugin");
|
||||
const pluginDir = path.join(suiteHome, "bluebubbles-plugin");
|
||||
await writePluginFixture({
|
||||
dir: pluginDir,
|
||||
id: "bluebubbles-plugin",
|
||||
@@ -215,7 +226,7 @@ describe("config plugin validation", () => {
|
||||
schema: { type: "object" },
|
||||
});
|
||||
|
||||
const res = validateInHome(home, {
|
||||
const res = validateInSuite({
|
||||
agents: { defaults: { heartbeat: { target: "bluebubbles" } }, list: [{ id: "pi" }] },
|
||||
plugins: { enabled: false, load: { paths: [pluginDir] } },
|
||||
});
|
||||
@@ -223,8 +234,7 @@ describe("config plugin validation", () => {
|
||||
});
|
||||
|
||||
it("rejects unknown heartbeat targets", async () => {
|
||||
const home = await createCaseHome();
|
||||
const res = validateInHome(home, {
|
||||
const res = validateInSuite({
|
||||
agents: { defaults: { heartbeat: { target: "not-a-channel" } }, list: [{ id: "pi" }] },
|
||||
});
|
||||
expect(res.ok).toBe(false);
|
||||
@@ -237,8 +247,7 @@ describe("config plugin validation", () => {
|
||||
});
|
||||
|
||||
it("accepts heartbeat directPolicy enum values", async () => {
|
||||
const home = await createCaseHome();
|
||||
const res = validateInHome(home, {
|
||||
const res = validateInSuite({
|
||||
agents: {
|
||||
defaults: { heartbeat: { target: "last", directPolicy: "block" } },
|
||||
list: [{ id: "pi", heartbeat: { directPolicy: "allow" } }],
|
||||
@@ -248,8 +257,7 @@ describe("config plugin validation", () => {
|
||||
});
|
||||
|
||||
it("rejects invalid heartbeat directPolicy values", async () => {
|
||||
const home = await createCaseHome();
|
||||
const res = validateInHome(home, {
|
||||
const res = validateInSuite({
|
||||
agents: {
|
||||
defaults: { heartbeat: { directPolicy: "maybe" } },
|
||||
list: [{ id: "pi" }],
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { beforeAll, describe, expect, it } from "vitest";
|
||||
import { buildConfigSchema } from "./schema.js";
|
||||
import { applyDerivedTags, CONFIG_TAGS, deriveTagsForPath } from "./schema.tags.js";
|
||||
|
||||
describe("config schema", () => {
|
||||
let baseSchema: ReturnType<typeof buildConfigSchema>;
|
||||
|
||||
beforeAll(() => {
|
||||
baseSchema = buildConfigSchema();
|
||||
});
|
||||
|
||||
it("exports schema + hints", () => {
|
||||
const res = buildConfigSchema();
|
||||
const res = baseSchema;
|
||||
const schema = res.schema as { properties?: Record<string, unknown> };
|
||||
expect(schema.properties?.gateway).toBeTruthy();
|
||||
expect(schema.properties?.agents).toBeTruthy();
|
||||
@@ -148,7 +154,7 @@ describe("config schema", () => {
|
||||
});
|
||||
|
||||
it("covers core/built-in config paths with tags", () => {
|
||||
const schema = buildConfigSchema();
|
||||
const schema = baseSchema;
|
||||
const allowed = new Set<string>(CONFIG_TAGS);
|
||||
for (const [key, hint] of Object.entries(schema.uiHints)) {
|
||||
if (!key.includes(".")) {
|
||||
|
||||
@@ -44,10 +44,43 @@ describe("sessions", () => {
|
||||
}): Promise<{ storePath: string }> {
|
||||
const dir = await createCaseDir(params.prefix);
|
||||
const storePath = path.join(dir, "sessions.json");
|
||||
await fs.writeFile(storePath, JSON.stringify(params.entries, null, 2), "utf-8");
|
||||
await fs.writeFile(storePath, JSON.stringify(params.entries), "utf-8");
|
||||
return { storePath };
|
||||
}
|
||||
|
||||
async function createAgentSessionsLayout(label: string): Promise<{
|
||||
stateDir: string;
|
||||
mainStorePath: string;
|
||||
bot2SessionPath: string;
|
||||
outsidePath: string;
|
||||
}> {
|
||||
const stateDir = await createCaseDir(label);
|
||||
const mainSessionsDir = path.join(stateDir, "agents", "main", "sessions");
|
||||
const bot1SessionsDir = path.join(stateDir, "agents", "bot1", "sessions");
|
||||
const bot2SessionsDir = path.join(stateDir, "agents", "bot2", "sessions");
|
||||
await fs.mkdir(mainSessionsDir, { recursive: true });
|
||||
await fs.mkdir(bot1SessionsDir, { recursive: true });
|
||||
await fs.mkdir(bot2SessionsDir, { recursive: true });
|
||||
|
||||
const mainStorePath = path.join(mainSessionsDir, "sessions.json");
|
||||
await fs.writeFile(mainStorePath, "{}", "utf-8");
|
||||
|
||||
const bot2SessionPath = path.join(bot2SessionsDir, "sess-1.jsonl");
|
||||
await fs.writeFile(bot2SessionPath, "{}", "utf-8");
|
||||
|
||||
const outsidePath = path.join(stateDir, "outside", "not-a-session.jsonl");
|
||||
await fs.mkdir(path.dirname(outsidePath), { recursive: true });
|
||||
await fs.writeFile(outsidePath, "{}", "utf-8");
|
||||
|
||||
return { stateDir, mainStorePath, bot2SessionPath, outsidePath };
|
||||
}
|
||||
|
||||
async function normalizePathForComparison(filePath: string): Promise<string> {
|
||||
const parentDir = path.dirname(filePath);
|
||||
const canonicalParent = await fs.realpath(parentDir).catch(() => parentDir);
|
||||
return path.join(canonicalParent, path.basename(filePath));
|
||||
}
|
||||
|
||||
const deriveSessionKeyCases = [
|
||||
{
|
||||
name: "returns normalized per-sender key",
|
||||
@@ -534,17 +567,19 @@ describe("sessions", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("resolves cross-agent absolute sessionFile paths", () => {
|
||||
const stateDir = path.resolve("/home/user/.openclaw");
|
||||
it("resolves cross-agent absolute sessionFile paths", async () => {
|
||||
const { stateDir, bot2SessionPath } = await createAgentSessionsLayout("cross-agent");
|
||||
const canonicalBot2SessionPath = await fs
|
||||
.realpath(bot2SessionPath)
|
||||
.catch(() => bot2SessionPath);
|
||||
withStateDir(stateDir, () => {
|
||||
const bot2Session = path.join(stateDir, "agents", "bot2", "sessions", "sess-1.jsonl");
|
||||
// Agent bot1 resolves a sessionFile that belongs to agent bot2
|
||||
const sessionFile = resolveSessionFilePath(
|
||||
"sess-1",
|
||||
{ sessionFile: bot2Session },
|
||||
{ sessionFile: bot2SessionPath },
|
||||
{ agentId: "bot1" },
|
||||
);
|
||||
expect(sessionFile).toBe(bot2Session);
|
||||
expect(sessionFile).toBe(canonicalBot2SessionPath);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -609,38 +644,32 @@ describe("sessions", () => {
|
||||
expect(resolved?.sessionsDir).toBe(path.dirname(path.resolve(storePath)));
|
||||
});
|
||||
|
||||
it("resolves sibling agent absolute sessionFile using alternate agentId from options", () => {
|
||||
const stateDir = path.resolve("/home/user/.openclaw");
|
||||
it("resolves sibling agent absolute sessionFile using alternate agentId from options", async () => {
|
||||
const { stateDir, mainStorePath, bot2SessionPath } =
|
||||
await createAgentSessionsLayout("sibling-agent");
|
||||
const canonicalBot2SessionPath = await fs
|
||||
.realpath(bot2SessionPath)
|
||||
.catch(() => bot2SessionPath);
|
||||
withStateDir(stateDir, () => {
|
||||
const mainStorePath = path.join(stateDir, "agents", "main", "sessions", "sessions.json");
|
||||
const bot2Session = path.join(stateDir, "agents", "bot2", "sessions", "sess-1.jsonl");
|
||||
const opts = resolveSessionFilePathOptions({
|
||||
agentId: "bot2",
|
||||
storePath: mainStorePath,
|
||||
});
|
||||
|
||||
const sessionFile = resolveSessionFilePath("sess-1", { sessionFile: bot2Session }, opts);
|
||||
expect(sessionFile).toBe(bot2Session);
|
||||
const sessionFile = resolveSessionFilePath("sess-1", { sessionFile: bot2SessionPath }, opts);
|
||||
expect(sessionFile).toBe(canonicalBot2SessionPath);
|
||||
});
|
||||
});
|
||||
|
||||
it("falls back to derived transcript path when sessionFile is outside agent sessions directories", () => {
|
||||
withStateDir(path.resolve("/home/user/.openclaw"), () => {
|
||||
const sessionFile = resolveSessionFilePath(
|
||||
"sess-1",
|
||||
{ sessionFile: path.resolve("/etc/passwd") },
|
||||
{ agentId: "bot1" },
|
||||
);
|
||||
expect(sessionFile).toBe(
|
||||
path.join(
|
||||
path.resolve("/home/user/.openclaw"),
|
||||
"agents",
|
||||
"bot1",
|
||||
"sessions",
|
||||
"sess-1.jsonl",
|
||||
),
|
||||
);
|
||||
});
|
||||
it("falls back to derived transcript path when sessionFile is outside agent sessions directories", async () => {
|
||||
const { stateDir, outsidePath } = await createAgentSessionsLayout("outside-fallback");
|
||||
const sessionFile = withStateDir(stateDir, () =>
|
||||
resolveSessionFilePath("sess-1", { sessionFile: outsidePath }, { agentId: "bot1" }),
|
||||
);
|
||||
const expectedPath = path.join(stateDir, "agents", "bot1", "sessions", "sess-1.jsonl");
|
||||
expect(await normalizePathForComparison(sessionFile)).toBe(
|
||||
await normalizePathForComparison(expectedPath),
|
||||
);
|
||||
});
|
||||
|
||||
it("updateSessionStoreEntry merges concurrent patches", async () => {
|
||||
@@ -723,7 +752,7 @@ describe("sessions", () => {
|
||||
providerOverride: "anthropic",
|
||||
updatedAt: 124,
|
||||
};
|
||||
await fs.writeFile(storePath, JSON.stringify(externalStore, null, 2), "utf-8");
|
||||
await fs.writeFile(storePath, JSON.stringify(externalStore), "utf-8");
|
||||
await fs.utimes(storePath, originalStat.atime, originalStat.mtime);
|
||||
|
||||
await updateSessionStoreEntry({
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import type { SecretProviderConfig } from "../config/types.secrets.js";
|
||||
import { resolveSecretRefString, resolveSecretRefValue } from "./resolve.js";
|
||||
|
||||
async function writeSecureFile(filePath: string, content: string, mode = 0o600): Promise<void> {
|
||||
@@ -13,92 +12,69 @@ async function writeSecureFile(filePath: string, content: string, mode = 0o600):
|
||||
}
|
||||
|
||||
describe("secret ref resolver", () => {
|
||||
const cleanupRoots: string[] = [];
|
||||
const execRef = { source: "exec", provider: "execmain", id: "openai/api-key" } as const;
|
||||
const fileRef = { source: "file", provider: "filemain", id: "/providers/openai/apiKey" } as const;
|
||||
let fixtureRoot = "";
|
||||
let caseId = 0;
|
||||
let execProtocolV1ScriptPath = "";
|
||||
let execPlainScriptPath = "";
|
||||
let execProtocolV2ScriptPath = "";
|
||||
let execMissingIdScriptPath = "";
|
||||
let execInvalidJsonScriptPath = "";
|
||||
|
||||
function isWindows(): boolean {
|
||||
return process.platform === "win32";
|
||||
}
|
||||
const createCaseDir = async (label: string): Promise<string> => {
|
||||
const dir = path.join(fixtureRoot, `${label}-${caseId++}`);
|
||||
await fs.mkdir(dir, { recursive: true });
|
||||
return dir;
|
||||
};
|
||||
|
||||
async function createTempRoot(prefix: string): Promise<string> {
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), prefix));
|
||||
cleanupRoots.push(root);
|
||||
return root;
|
||||
}
|
||||
beforeAll(async () => {
|
||||
fixtureRoot = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-secrets-resolve-"));
|
||||
const sharedExecDir = path.join(fixtureRoot, "shared-exec");
|
||||
await fs.mkdir(sharedExecDir, { recursive: true });
|
||||
|
||||
function createProviderConfig(
|
||||
providerId: string,
|
||||
provider: SecretProviderConfig,
|
||||
): OpenClawConfig {
|
||||
return {
|
||||
secrets: {
|
||||
providers: {
|
||||
[providerId]: provider,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function resolveWithProvider(params: {
|
||||
ref: Parameters<typeof resolveSecretRefString>[0];
|
||||
providerId: string;
|
||||
provider: SecretProviderConfig;
|
||||
}) {
|
||||
return await resolveSecretRefString(params.ref, {
|
||||
config: createProviderConfig(params.providerId, params.provider),
|
||||
});
|
||||
}
|
||||
|
||||
function createExecProvider(
|
||||
command: string,
|
||||
overrides?: Record<string, unknown>,
|
||||
): SecretProviderConfig {
|
||||
return {
|
||||
source: "exec",
|
||||
command,
|
||||
passEnv: ["PATH"],
|
||||
...overrides,
|
||||
} as SecretProviderConfig;
|
||||
}
|
||||
|
||||
async function expectExecResolveRejects(
|
||||
provider: SecretProviderConfig,
|
||||
message: string,
|
||||
): Promise<void> {
|
||||
await expect(
|
||||
resolveWithProvider({
|
||||
ref: execRef,
|
||||
providerId: "execmain",
|
||||
provider,
|
||||
}),
|
||||
).rejects.toThrow(message);
|
||||
}
|
||||
|
||||
async function createSymlinkedPlainExecCommand(
|
||||
root: string,
|
||||
targetRoot = root,
|
||||
): Promise<{ scriptPath: string; symlinkPath: string }> {
|
||||
const scriptPath = path.join(targetRoot, "resolver-target.mjs");
|
||||
const symlinkPath = path.join(root, "resolver-link.mjs");
|
||||
execProtocolV1ScriptPath = path.join(sharedExecDir, "resolver-v1.sh");
|
||||
await writeSecureFile(
|
||||
scriptPath,
|
||||
["#!/usr/bin/env node", "process.stdout.write('plain-secret');"].join("\n"),
|
||||
execProtocolV1ScriptPath,
|
||||
[
|
||||
"#!/bin/sh",
|
||||
'printf \'{"protocolVersion":1,"values":{"openai/api-key":"value:openai/api-key"}}\'',
|
||||
].join("\n"),
|
||||
0o700,
|
||||
);
|
||||
await fs.symlink(scriptPath, symlinkPath);
|
||||
return { scriptPath, symlinkPath };
|
||||
}
|
||||
|
||||
afterEach(async () => {
|
||||
vi.restoreAllMocks();
|
||||
while (cleanupRoots.length > 0) {
|
||||
const root = cleanupRoots.pop();
|
||||
if (!root) {
|
||||
continue;
|
||||
}
|
||||
await fs.rm(root, { recursive: true, force: true });
|
||||
execPlainScriptPath = path.join(sharedExecDir, "resolver-plain.sh");
|
||||
await writeSecureFile(
|
||||
execPlainScriptPath,
|
||||
["#!/bin/sh", "printf 'plain-secret'"].join("\n"),
|
||||
0o700,
|
||||
);
|
||||
|
||||
execProtocolV2ScriptPath = path.join(sharedExecDir, "resolver-v2.sh");
|
||||
await writeSecureFile(
|
||||
execProtocolV2ScriptPath,
|
||||
["#!/bin/sh", 'printf \'{"protocolVersion":2,"values":{"openai/api-key":"x"}}\''].join("\n"),
|
||||
0o700,
|
||||
);
|
||||
|
||||
execMissingIdScriptPath = path.join(sharedExecDir, "resolver-missing-id.sh");
|
||||
await writeSecureFile(
|
||||
execMissingIdScriptPath,
|
||||
["#!/bin/sh", 'printf \'{"protocolVersion":1,"values":{}}\''].join("\n"),
|
||||
0o700,
|
||||
);
|
||||
|
||||
execInvalidJsonScriptPath = path.join(sharedExecDir, "resolver-invalid-json.sh");
|
||||
await writeSecureFile(
|
||||
execInvalidJsonScriptPath,
|
||||
["#!/bin/sh", "printf 'not-json'"].join("\n"),
|
||||
0o700,
|
||||
);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
if (!fixtureRoot) {
|
||||
return;
|
||||
}
|
||||
await fs.rm(fixtureRoot, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("resolves env refs via implicit default env provider", async () => {
|
||||
@@ -114,10 +90,10 @@ describe("secret ref resolver", () => {
|
||||
});
|
||||
|
||||
it("resolves file refs in json mode", async () => {
|
||||
if (isWindows()) {
|
||||
if (process.platform === "win32") {
|
||||
return;
|
||||
}
|
||||
const root = await createTempRoot("openclaw-secrets-resolve-file-");
|
||||
const root = await createCaseDir("file");
|
||||
const filePath = path.join(root, "secrets.json");
|
||||
await writeSecureFile(
|
||||
filePath,
|
||||
@@ -130,111 +106,140 @@ describe("secret ref resolver", () => {
|
||||
}),
|
||||
);
|
||||
|
||||
const value = await resolveWithProvider({
|
||||
ref: fileRef,
|
||||
providerId: "filemain",
|
||||
provider: {
|
||||
source: "file",
|
||||
path: filePath,
|
||||
mode: "json",
|
||||
const value = await resolveSecretRefString(
|
||||
{ source: "file", provider: "filemain", id: "/providers/openai/apiKey" },
|
||||
{
|
||||
config: {
|
||||
secrets: {
|
||||
providers: {
|
||||
filemain: {
|
||||
source: "file",
|
||||
path: filePath,
|
||||
mode: "json",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
);
|
||||
expect(value).toBe("sk-file-value");
|
||||
});
|
||||
|
||||
it("resolves exec refs with protocolVersion 1 response", async () => {
|
||||
if (isWindows()) {
|
||||
if (process.platform === "win32") {
|
||||
return;
|
||||
}
|
||||
const root = await createTempRoot("openclaw-secrets-resolve-exec-");
|
||||
const scriptPath = path.join(root, "resolver.mjs");
|
||||
await writeSecureFile(
|
||||
scriptPath,
|
||||
[
|
||||
"#!/usr/bin/env node",
|
||||
"import fs from 'node:fs';",
|
||||
"const req = JSON.parse(fs.readFileSync(0, 'utf8'));",
|
||||
"const values = Object.fromEntries((req.ids ?? []).map((id) => [id, `value:${id}`]));",
|
||||
"process.stdout.write(JSON.stringify({ protocolVersion: 1, values }));",
|
||||
].join("\n"),
|
||||
0o700,
|
||||
);
|
||||
|
||||
const value = await resolveWithProvider({
|
||||
ref: execRef,
|
||||
providerId: "execmain",
|
||||
provider: {
|
||||
source: "exec",
|
||||
command: scriptPath,
|
||||
passEnv: ["PATH"],
|
||||
const value = await resolveSecretRefString(
|
||||
{ source: "exec", provider: "execmain", id: "openai/api-key" },
|
||||
{
|
||||
config: {
|
||||
secrets: {
|
||||
providers: {
|
||||
execmain: {
|
||||
source: "exec",
|
||||
command: execProtocolV1ScriptPath,
|
||||
passEnv: ["PATH"],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
);
|
||||
expect(value).toBe("value:openai/api-key");
|
||||
});
|
||||
|
||||
it("supports non-JSON single-value exec output when jsonOnly is false", async () => {
|
||||
if (isWindows()) {
|
||||
if (process.platform === "win32") {
|
||||
return;
|
||||
}
|
||||
const root = await createTempRoot("openclaw-secrets-resolve-exec-plain-");
|
||||
const scriptPath = path.join(root, "resolver-plain.mjs");
|
||||
await writeSecureFile(
|
||||
scriptPath,
|
||||
["#!/usr/bin/env node", "process.stdout.write('plain-secret');"].join("\n"),
|
||||
0o700,
|
||||
);
|
||||
|
||||
const value = await resolveWithProvider({
|
||||
ref: execRef,
|
||||
providerId: "execmain",
|
||||
provider: {
|
||||
source: "exec",
|
||||
command: scriptPath,
|
||||
passEnv: ["PATH"],
|
||||
jsonOnly: false,
|
||||
const value = await resolveSecretRefString(
|
||||
{ source: "exec", provider: "execmain", id: "openai/api-key" },
|
||||
{
|
||||
config: {
|
||||
secrets: {
|
||||
providers: {
|
||||
execmain: {
|
||||
source: "exec",
|
||||
command: execPlainScriptPath,
|
||||
passEnv: ["PATH"],
|
||||
jsonOnly: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
);
|
||||
expect(value).toBe("plain-secret");
|
||||
});
|
||||
|
||||
it("rejects symlink command paths unless allowSymlinkCommand is enabled", async () => {
|
||||
if (isWindows()) {
|
||||
if (process.platform === "win32") {
|
||||
return;
|
||||
}
|
||||
const root = await createTempRoot("openclaw-secrets-resolve-exec-link-");
|
||||
const { symlinkPath } = await createSymlinkedPlainExecCommand(root);
|
||||
await expectExecResolveRejects(
|
||||
createExecProvider(symlinkPath, { jsonOnly: false }),
|
||||
"must not be a symlink",
|
||||
);
|
||||
const root = await createCaseDir("exec-link-reject");
|
||||
const symlinkPath = path.join(root, "resolver-link.mjs");
|
||||
await fs.symlink(execPlainScriptPath, symlinkPath);
|
||||
|
||||
await expect(
|
||||
resolveSecretRefString(
|
||||
{ source: "exec", provider: "execmain", id: "openai/api-key" },
|
||||
{
|
||||
config: {
|
||||
secrets: {
|
||||
providers: {
|
||||
execmain: {
|
||||
source: "exec",
|
||||
command: symlinkPath,
|
||||
passEnv: ["PATH"],
|
||||
jsonOnly: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
).rejects.toThrow("must not be a symlink");
|
||||
});
|
||||
|
||||
it("allows symlink command paths when allowSymlinkCommand is enabled", async () => {
|
||||
if (isWindows()) {
|
||||
if (process.platform === "win32") {
|
||||
return;
|
||||
}
|
||||
const root = await createTempRoot("openclaw-secrets-resolve-exec-link-");
|
||||
const { symlinkPath } = await createSymlinkedPlainExecCommand(root);
|
||||
const trustedRoot = await fs.realpath(root);
|
||||
const root = await createCaseDir("exec-link-allow");
|
||||
const symlinkPath = path.join(root, "resolver-link.mjs");
|
||||
await fs.symlink(execPlainScriptPath, symlinkPath);
|
||||
const trustedRoot = await fs.realpath(fixtureRoot);
|
||||
|
||||
const value = await resolveWithProvider({
|
||||
ref: execRef,
|
||||
providerId: "execmain",
|
||||
provider: createExecProvider(symlinkPath, {
|
||||
jsonOnly: false,
|
||||
allowSymlinkCommand: true,
|
||||
trustedDirs: [trustedRoot],
|
||||
}),
|
||||
});
|
||||
const value = await resolveSecretRefString(
|
||||
{ source: "exec", provider: "execmain", id: "openai/api-key" },
|
||||
{
|
||||
config: {
|
||||
secrets: {
|
||||
providers: {
|
||||
execmain: {
|
||||
source: "exec",
|
||||
command: symlinkPath,
|
||||
passEnv: ["PATH"],
|
||||
jsonOnly: false,
|
||||
allowSymlinkCommand: true,
|
||||
trustedDirs: [trustedRoot],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
expect(value).toBe("plain-secret");
|
||||
});
|
||||
|
||||
it("handles Homebrew-style symlinked exec commands with args only when explicitly allowed", async () => {
|
||||
if (isWindows()) {
|
||||
if (process.platform === "win32") {
|
||||
return;
|
||||
}
|
||||
|
||||
const root = await createTempRoot("openclaw-secrets-resolve-homebrew-");
|
||||
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 });
|
||||
@@ -245,12 +250,9 @@ describe("secret ref resolver", () => {
|
||||
await writeSecureFile(
|
||||
targetCommand,
|
||||
[
|
||||
`#!${process.execPath}`,
|
||||
"import fs from 'node:fs';",
|
||||
"const req = JSON.parse(fs.readFileSync(0, 'utf8'));",
|
||||
"const suffix = process.argv[2] ?? 'missing';",
|
||||
"const values = Object.fromEntries((req.ids ?? []).map((id) => [id, `${suffix}:${id}`]));",
|
||||
"process.stdout.write(JSON.stringify({ protocolVersion: 1, values }));",
|
||||
"#!/bin/sh",
|
||||
'suffix="${1:-missing}"',
|
||||
'printf \'{"protocolVersion":1,"values":{"openai/api-key":"%s:openai/api-key"}}\' "$suffix"',
|
||||
].join("\n"),
|
||||
0o700,
|
||||
);
|
||||
@@ -258,139 +260,182 @@ describe("secret ref resolver", () => {
|
||||
const trustedRoot = await fs.realpath(root);
|
||||
|
||||
await expect(
|
||||
resolveWithProvider({
|
||||
ref: execRef,
|
||||
providerId: "execmain",
|
||||
provider: {
|
||||
source: "exec",
|
||||
command: symlinkCommand,
|
||||
args: ["brew"],
|
||||
passEnv: ["PATH"],
|
||||
resolveSecretRefString(
|
||||
{ source: "exec", provider: "execmain", id: "openai/api-key" },
|
||||
{
|
||||
config: {
|
||||
secrets: {
|
||||
providers: {
|
||||
execmain: {
|
||||
source: "exec",
|
||||
command: symlinkCommand,
|
||||
args: ["brew"],
|
||||
passEnv: ["PATH"],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
),
|
||||
).rejects.toThrow("must not be a symlink");
|
||||
|
||||
const value = await resolveWithProvider({
|
||||
ref: execRef,
|
||||
providerId: "execmain",
|
||||
provider: {
|
||||
source: "exec",
|
||||
command: symlinkCommand,
|
||||
args: ["brew"],
|
||||
allowSymlinkCommand: true,
|
||||
trustedDirs: [trustedRoot],
|
||||
const value = await resolveSecretRefString(
|
||||
{ source: "exec", provider: "execmain", id: "openai/api-key" },
|
||||
{
|
||||
config: {
|
||||
secrets: {
|
||||
providers: {
|
||||
execmain: {
|
||||
source: "exec",
|
||||
command: symlinkCommand,
|
||||
args: ["brew"],
|
||||
allowSymlinkCommand: true,
|
||||
trustedDirs: [trustedRoot],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
);
|
||||
expect(value).toBe("brew:openai/api-key");
|
||||
});
|
||||
|
||||
it("checks trustedDirs against resolved symlink target", async () => {
|
||||
if (isWindows()) {
|
||||
if (process.platform === "win32") {
|
||||
return;
|
||||
}
|
||||
const root = await createTempRoot("openclaw-secrets-resolve-exec-link-");
|
||||
const outside = await createTempRoot("openclaw-secrets-resolve-exec-out-");
|
||||
const { symlinkPath } = await createSymlinkedPlainExecCommand(root, outside);
|
||||
await expectExecResolveRejects(
|
||||
createExecProvider(symlinkPath, {
|
||||
jsonOnly: false,
|
||||
allowSymlinkCommand: true,
|
||||
trustedDirs: [root],
|
||||
}),
|
||||
"outside trustedDirs",
|
||||
);
|
||||
const root = await createCaseDir("exec-link-trusted");
|
||||
const symlinkPath = path.join(root, "resolver-link.mjs");
|
||||
await fs.symlink(execPlainScriptPath, symlinkPath);
|
||||
|
||||
await expect(
|
||||
resolveSecretRefString(
|
||||
{ source: "exec", provider: "execmain", id: "openai/api-key" },
|
||||
{
|
||||
config: {
|
||||
secrets: {
|
||||
providers: {
|
||||
execmain: {
|
||||
source: "exec",
|
||||
command: symlinkPath,
|
||||
passEnv: ["PATH"],
|
||||
jsonOnly: false,
|
||||
allowSymlinkCommand: true,
|
||||
trustedDirs: [root],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
).rejects.toThrow("outside trustedDirs");
|
||||
});
|
||||
|
||||
it("rejects exec refs when protocolVersion is not 1", async () => {
|
||||
if (isWindows()) {
|
||||
if (process.platform === "win32") {
|
||||
return;
|
||||
}
|
||||
const root = await createTempRoot("openclaw-secrets-resolve-exec-protocol-");
|
||||
const scriptPath = path.join(root, "resolver-protocol.mjs");
|
||||
await writeSecureFile(
|
||||
scriptPath,
|
||||
[
|
||||
"#!/usr/bin/env node",
|
||||
"process.stdout.write(JSON.stringify({ protocolVersion: 2, values: { 'openai/api-key': 'x' } }));",
|
||||
].join("\n"),
|
||||
0o700,
|
||||
);
|
||||
|
||||
await expectExecResolveRejects(createExecProvider(scriptPath), "protocolVersion must be 1");
|
||||
await expect(
|
||||
resolveSecretRefString(
|
||||
{ source: "exec", provider: "execmain", id: "openai/api-key" },
|
||||
{
|
||||
config: {
|
||||
secrets: {
|
||||
providers: {
|
||||
execmain: {
|
||||
source: "exec",
|
||||
command: execProtocolV2ScriptPath,
|
||||
passEnv: ["PATH"],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
).rejects.toThrow("protocolVersion must be 1");
|
||||
});
|
||||
|
||||
it("rejects exec refs when response omits requested id", async () => {
|
||||
if (isWindows()) {
|
||||
if (process.platform === "win32") {
|
||||
return;
|
||||
}
|
||||
const root = await createTempRoot("openclaw-secrets-resolve-exec-id-");
|
||||
const scriptPath = path.join(root, "resolver-missing-id.mjs");
|
||||
await writeSecureFile(
|
||||
scriptPath,
|
||||
[
|
||||
"#!/usr/bin/env node",
|
||||
"process.stdout.write(JSON.stringify({ protocolVersion: 1, values: {} }));",
|
||||
].join("\n"),
|
||||
0o700,
|
||||
);
|
||||
|
||||
await expectExecResolveRejects(
|
||||
createExecProvider(scriptPath),
|
||||
'response missing id "openai/api-key"',
|
||||
);
|
||||
await expect(
|
||||
resolveSecretRefString(
|
||||
{ source: "exec", provider: "execmain", id: "openai/api-key" },
|
||||
{
|
||||
config: {
|
||||
secrets: {
|
||||
providers: {
|
||||
execmain: {
|
||||
source: "exec",
|
||||
command: execMissingIdScriptPath,
|
||||
passEnv: ["PATH"],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
).rejects.toThrow('response missing id "openai/api-key"');
|
||||
});
|
||||
|
||||
it("rejects exec refs with invalid JSON when jsonOnly is true", async () => {
|
||||
if (isWindows()) {
|
||||
if (process.platform === "win32") {
|
||||
return;
|
||||
}
|
||||
const root = await createTempRoot("openclaw-secrets-resolve-exec-json-");
|
||||
const scriptPath = path.join(root, "resolver-invalid-json.mjs");
|
||||
await writeSecureFile(
|
||||
scriptPath,
|
||||
["#!/usr/bin/env node", "process.stdout.write('not-json');"].join("\n"),
|
||||
0o700,
|
||||
);
|
||||
|
||||
await expect(
|
||||
resolveWithProvider({
|
||||
ref: execRef,
|
||||
providerId: "execmain",
|
||||
provider: {
|
||||
source: "exec",
|
||||
command: scriptPath,
|
||||
passEnv: ["PATH"],
|
||||
jsonOnly: true,
|
||||
resolveSecretRefString(
|
||||
{ source: "exec", provider: "execmain", id: "openai/api-key" },
|
||||
{
|
||||
config: {
|
||||
secrets: {
|
||||
providers: {
|
||||
execmain: {
|
||||
source: "exec",
|
||||
command: execInvalidJsonScriptPath,
|
||||
passEnv: ["PATH"],
|
||||
jsonOnly: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
),
|
||||
).rejects.toThrow("returned invalid JSON");
|
||||
});
|
||||
|
||||
it("supports file singleValue mode with id=value", async () => {
|
||||
if (isWindows()) {
|
||||
if (process.platform === "win32") {
|
||||
return;
|
||||
}
|
||||
const root = await createTempRoot("openclaw-secrets-resolve-single-value-");
|
||||
const root = await createCaseDir("file-single-value");
|
||||
const filePath = path.join(root, "token.txt");
|
||||
await writeSecureFile(filePath, "raw-token-value\n");
|
||||
|
||||
const value = await resolveWithProvider({
|
||||
ref: { source: "file", provider: "rawfile", id: "value" },
|
||||
providerId: "rawfile",
|
||||
provider: {
|
||||
source: "file",
|
||||
path: filePath,
|
||||
mode: "singleValue",
|
||||
const value = await resolveSecretRefString(
|
||||
{ source: "file", provider: "rawfile", id: "value" },
|
||||
{
|
||||
config: {
|
||||
secrets: {
|
||||
providers: {
|
||||
rawfile: {
|
||||
source: "file",
|
||||
path: filePath,
|
||||
mode: "singleValue",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
);
|
||||
expect(value).toBe("raw-token-value");
|
||||
});
|
||||
|
||||
it("times out file provider reads when timeoutMs elapses", async () => {
|
||||
if (isWindows()) {
|
||||
if (process.platform === "win32") {
|
||||
return;
|
||||
}
|
||||
const root = await createTempRoot("openclaw-secrets-resolve-timeout-");
|
||||
const root = await createCaseDir("file-timeout");
|
||||
const filePath = path.join(root, "secrets.json");
|
||||
await writeSecureFile(
|
||||
filePath,
|
||||
@@ -404,7 +449,7 @@ describe("secret ref resolver", () => {
|
||||
);
|
||||
|
||||
const originalReadFile = fs.readFile.bind(fs);
|
||||
vi.spyOn(fs, "readFile").mockImplementation(((
|
||||
const readFileSpy = vi.spyOn(fs, "readFile").mockImplementation(((
|
||||
targetPath: Parameters<typeof fs.readFile>[0],
|
||||
options?: Parameters<typeof fs.readFile>[1],
|
||||
) => {
|
||||
@@ -414,18 +459,29 @@ describe("secret ref resolver", () => {
|
||||
return originalReadFile(targetPath, options);
|
||||
}) as typeof fs.readFile);
|
||||
|
||||
await expect(
|
||||
resolveWithProvider({
|
||||
ref: fileRef,
|
||||
providerId: "filemain",
|
||||
provider: {
|
||||
source: "file",
|
||||
path: filePath,
|
||||
mode: "json",
|
||||
timeoutMs: 5,
|
||||
},
|
||||
}),
|
||||
).rejects.toThrow('File provider "filemain" timed out');
|
||||
try {
|
||||
await expect(
|
||||
resolveSecretRefString(
|
||||
{ source: "file", provider: "filemain", id: "/providers/openai/apiKey" },
|
||||
{
|
||||
config: {
|
||||
secrets: {
|
||||
providers: {
|
||||
filemain: {
|
||||
source: "file",
|
||||
path: filePath,
|
||||
mode: "json",
|
||||
timeoutMs: 5,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
).rejects.toThrow('File provider "filemain" timed out');
|
||||
} finally {
|
||||
readFileSpy.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
it("rejects misconfigured provider source mismatches", async () => {
|
||||
@@ -433,7 +489,15 @@ describe("secret ref resolver", () => {
|
||||
resolveSecretRefValue(
|
||||
{ source: "exec", provider: "default", id: "abc" },
|
||||
{
|
||||
config: createProviderConfig("default", { source: "env" }),
|
||||
config: {
|
||||
secrets: {
|
||||
providers: {
|
||||
default: {
|
||||
source: "env",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
).rejects.toThrow('has source "env" but ref requests "exec"');
|
||||
|
||||
@@ -1,50 +1,19 @@
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { chmodSync } from "node:fs";
|
||||
import { mkdir, mkdtemp, writeFile } from "node:fs/promises";
|
||||
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
||||
|
||||
const SCRIPT = path.join(process.cwd(), "scripts", "ios-team-id.sh");
|
||||
const XCODE_PLIST_PATH = path.join("Library", "Preferences", "com.apple.dt.Xcode.plist");
|
||||
|
||||
const DEFAULTS_WITH_ACCOUNT_SCRIPT = `#!/usr/bin/env bash
|
||||
if [[ "$3" == "DVTDeveloperAccountManagerAppleIDLists" ]]; then
|
||||
echo '(identifier = "dev@example.com";)'
|
||||
exit 0
|
||||
fi
|
||||
exit 0`;
|
||||
let fixtureRoot = "";
|
||||
let caseId = 0;
|
||||
|
||||
async function writeExecutable(filePath: string, body: string): Promise<void> {
|
||||
await writeFile(filePath, body, "utf8");
|
||||
chmodSync(filePath, 0o755);
|
||||
}
|
||||
|
||||
async function setupFixture(params?: {
|
||||
provisioningProfiles?: Record<string, string>;
|
||||
}): Promise<{ homeDir: string; binDir: string }> {
|
||||
const homeDir = await mkdtemp(path.join(os.tmpdir(), "openclaw-ios-team-id-"));
|
||||
const binDir = path.join(homeDir, "bin");
|
||||
await mkdir(binDir, { recursive: true });
|
||||
await mkdir(path.join(homeDir, "Library", "Preferences"), { recursive: true });
|
||||
await writeFile(path.join(homeDir, XCODE_PLIST_PATH), "");
|
||||
|
||||
const provisioningProfiles = params?.provisioningProfiles;
|
||||
if (provisioningProfiles) {
|
||||
const profilesDir = path.join(homeDir, "Library", "MobileDevice", "Provisioning Profiles");
|
||||
await mkdir(profilesDir, { recursive: true });
|
||||
for (const [name, body] of Object.entries(provisioningProfiles)) {
|
||||
await writeFile(path.join(profilesDir, name), body);
|
||||
}
|
||||
}
|
||||
|
||||
return { homeDir, binDir };
|
||||
}
|
||||
|
||||
async function writeDefaultsWithSignedInAccount(binDir: string): Promise<void> {
|
||||
await writeExecutable(path.join(binDir, "defaults"), DEFAULTS_WITH_ACCOUNT_SCRIPT);
|
||||
}
|
||||
|
||||
function runScript(
|
||||
homeDir: string,
|
||||
extraEnv: Record<string, string> = {},
|
||||
@@ -79,19 +48,51 @@ function runScript(
|
||||
}
|
||||
|
||||
describe("scripts/ios-team-id.sh", () => {
|
||||
beforeAll(async () => {
|
||||
fixtureRoot = await mkdtemp(path.join(os.tmpdir(), "openclaw-ios-team-id-"));
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
if (!fixtureRoot) {
|
||||
return;
|
||||
}
|
||||
await rm(fixtureRoot, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
async function createHomeDir(): Promise<string> {
|
||||
const homeDir = path.join(fixtureRoot, `case-${caseId++}`);
|
||||
await mkdir(homeDir, { recursive: true });
|
||||
return homeDir;
|
||||
}
|
||||
|
||||
it("falls back to Xcode-managed provisioning profiles when preference teams are empty", async () => {
|
||||
const { homeDir, binDir } = await setupFixture({
|
||||
provisioningProfiles: {
|
||||
"one.mobileprovision": "stub",
|
||||
},
|
||||
const homeDir = await createHomeDir();
|
||||
const binDir = path.join(homeDir, "bin");
|
||||
await mkdir(binDir, { recursive: true });
|
||||
await mkdir(path.join(homeDir, "Library", "Preferences"), { recursive: true });
|
||||
await mkdir(path.join(homeDir, "Library", "MobileDevice", "Provisioning Profiles"), {
|
||||
recursive: true,
|
||||
});
|
||||
await writeFile(path.join(homeDir, "Library", "Preferences", "com.apple.dt.Xcode.plist"), "");
|
||||
await writeFile(
|
||||
path.join(homeDir, "Library", "MobileDevice", "Provisioning Profiles", "one.mobileprovision"),
|
||||
"stub",
|
||||
);
|
||||
|
||||
await writeExecutable(
|
||||
path.join(binDir, "plutil"),
|
||||
`#!/usr/bin/env bash
|
||||
echo '{}'`,
|
||||
);
|
||||
await writeDefaultsWithSignedInAccount(binDir);
|
||||
await writeExecutable(
|
||||
path.join(binDir, "defaults"),
|
||||
`#!/usr/bin/env bash
|
||||
if [[ "$3" == "DVTDeveloperAccountManagerAppleIDLists" ]]; then
|
||||
echo '(identifier = "dev@example.com";)'
|
||||
exit 0
|
||||
fi
|
||||
exit 0`,
|
||||
);
|
||||
await writeExecutable(
|
||||
path.join(binDir, "security"),
|
||||
`#!/usr/bin/env bash
|
||||
@@ -119,7 +120,11 @@ exit 0`,
|
||||
});
|
||||
|
||||
it("prints actionable guidance when Xcode account exists but no Team ID is resolvable", async () => {
|
||||
const { homeDir, binDir } = await setupFixture();
|
||||
const homeDir = await createHomeDir();
|
||||
const binDir = path.join(homeDir, "bin");
|
||||
await mkdir(binDir, { recursive: true });
|
||||
await mkdir(path.join(homeDir, "Library", "Preferences"), { recursive: true });
|
||||
await writeFile(path.join(homeDir, "Library", "Preferences", "com.apple.dt.Xcode.plist"), "");
|
||||
|
||||
await writeExecutable(
|
||||
path.join(binDir, "plutil"),
|
||||
@@ -149,19 +154,37 @@ exit 1`,
|
||||
});
|
||||
|
||||
it("honors IOS_PREFERRED_TEAM_ID when multiple profile teams are available", async () => {
|
||||
const { homeDir, binDir } = await setupFixture({
|
||||
provisioningProfiles: {
|
||||
"one.mobileprovision": "stub1",
|
||||
"two.mobileprovision": "stub2",
|
||||
},
|
||||
const homeDir = await createHomeDir();
|
||||
const binDir = path.join(homeDir, "bin");
|
||||
await mkdir(binDir, { recursive: true });
|
||||
await mkdir(path.join(homeDir, "Library", "Preferences"), { recursive: true });
|
||||
await mkdir(path.join(homeDir, "Library", "MobileDevice", "Provisioning Profiles"), {
|
||||
recursive: true,
|
||||
});
|
||||
await writeFile(path.join(homeDir, "Library", "Preferences", "com.apple.dt.Xcode.plist"), "");
|
||||
await writeFile(
|
||||
path.join(homeDir, "Library", "MobileDevice", "Provisioning Profiles", "one.mobileprovision"),
|
||||
"stub1",
|
||||
);
|
||||
await writeFile(
|
||||
path.join(homeDir, "Library", "MobileDevice", "Provisioning Profiles", "two.mobileprovision"),
|
||||
"stub2",
|
||||
);
|
||||
|
||||
await writeExecutable(
|
||||
path.join(binDir, "plutil"),
|
||||
`#!/usr/bin/env bash
|
||||
echo '{}'`,
|
||||
);
|
||||
await writeDefaultsWithSignedInAccount(binDir);
|
||||
await writeExecutable(
|
||||
path.join(binDir, "defaults"),
|
||||
`#!/usr/bin/env bash
|
||||
if [[ "$3" == "DVTDeveloperAccountManagerAppleIDLists" ]]; then
|
||||
echo '(identifier = "dev@example.com";)'
|
||||
exit 0
|
||||
fi
|
||||
exit 0`,
|
||||
);
|
||||
await writeExecutable(
|
||||
path.join(binDir, "security"),
|
||||
`#!/usr/bin/env bash
|
||||
@@ -190,14 +213,26 @@ exit 0`,
|
||||
});
|
||||
|
||||
it("matches preferred team IDs even when parser output uses CRLF line endings", async () => {
|
||||
const { homeDir, binDir } = await setupFixture();
|
||||
const homeDir = await createHomeDir();
|
||||
const binDir = path.join(homeDir, "bin");
|
||||
await mkdir(binDir, { recursive: true });
|
||||
await mkdir(path.join(homeDir, "Library", "Preferences"), { recursive: true });
|
||||
await writeFile(path.join(homeDir, "Library", "Preferences", "com.apple.dt.Xcode.plist"), "");
|
||||
|
||||
await writeExecutable(
|
||||
path.join(binDir, "plutil"),
|
||||
`#!/usr/bin/env bash
|
||||
echo '{}'`,
|
||||
);
|
||||
await writeDefaultsWithSignedInAccount(binDir);
|
||||
await writeExecutable(
|
||||
path.join(binDir, "defaults"),
|
||||
`#!/usr/bin/env bash
|
||||
if [[ "$3" == "DVTDeveloperAccountManagerAppleIDLists" ]]; then
|
||||
echo '(identifier = "dev@example.com";)'
|
||||
exit 0
|
||||
fi
|
||||
exit 0`,
|
||||
);
|
||||
await writeExecutable(
|
||||
path.join(binDir, "fake-python"),
|
||||
`#!/usr/bin/env bash
|
||||
|
||||
Reference in New Issue
Block a user