test: dedupe secrets and guardrail fixtures

This commit is contained in:
Peter Steinberger
2026-03-26 17:39:58 +00:00
parent d3d8e316bd
commit e7e4fbcab9
3 changed files with 220 additions and 312 deletions

View File

@@ -35,41 +35,55 @@ function createEnv(stateDir: string): NodeJS.ProcessEnv {
};
}
async function createLegacyStateFixture(params?: { includePreKey?: boolean }) {
const root = await createTempDir();
const stateDir = path.join(root, ".openclaw");
const env = createEnv(stateDir);
const cfg = createConfig();
await fs.mkdir(path.join(stateDir, "sessions"), { recursive: true });
await fs.mkdir(path.join(stateDir, "agents", "worker-1", "sessions"), { recursive: true });
await fs.mkdir(path.join(stateDir, "agent"), { recursive: true });
await fs.mkdir(path.join(stateDir, "credentials"), { recursive: true });
await fs.writeFile(
path.join(stateDir, "sessions", "sessions.json"),
`${JSON.stringify({ legacyDirect: { sessionId: "legacy-direct", updatedAt: 10 } }, null, 2)}\n`,
"utf8",
);
await fs.writeFile(path.join(stateDir, "sessions", "trace.jsonl"), "{}\n", "utf8");
await fs.writeFile(
path.join(stateDir, "agents", "worker-1", "sessions", "sessions.json"),
`${JSON.stringify({ "group:123@g.us": { sessionId: "group-session", updatedAt: 5 } }, null, 2)}\n`,
"utf8",
);
await fs.writeFile(path.join(stateDir, "agent", "settings.json"), '{"ok":true}\n', "utf8");
await fs.writeFile(path.join(stateDir, "credentials", "creds.json"), '{"auth":true}\n', "utf8");
if (params?.includePreKey) {
await fs.writeFile(
path.join(stateDir, "credentials", "pre-key-1.json"),
'{"preKey":true}\n',
"utf8",
);
}
await fs.writeFile(path.join(stateDir, "credentials", "oauth.json"), '{"oauth":true}\n', "utf8");
await fs.writeFile(resolveChannelAllowFromPath("telegram", env), '["123","456"]\n', "utf8");
return {
root,
stateDir,
env,
cfg,
};
}
afterEach(async () => {
await tempDirs.cleanup();
});
describe("state migrations", () => {
it("detects legacy sessions, agent files, whatsapp auth, and telegram allowFrom copies", async () => {
const root = await createTempDir();
const stateDir = path.join(root, ".openclaw");
const env = createEnv(stateDir);
const cfg = createConfig();
await fs.mkdir(path.join(stateDir, "sessions"), { recursive: true });
await fs.mkdir(path.join(stateDir, "agents", "worker-1", "sessions"), { recursive: true });
await fs.mkdir(path.join(stateDir, "agent"), { recursive: true });
await fs.mkdir(path.join(stateDir, "credentials"), { recursive: true });
await fs.writeFile(
path.join(stateDir, "sessions", "sessions.json"),
`${JSON.stringify({ legacyDirect: { sessionId: "legacy-direct", updatedAt: 10 } }, null, 2)}\n`,
"utf8",
);
await fs.writeFile(path.join(stateDir, "sessions", "trace.jsonl"), "{}\n", "utf8");
await fs.writeFile(
path.join(stateDir, "agents", "worker-1", "sessions", "sessions.json"),
`${JSON.stringify({ "group:123@g.us": { sessionId: "group-session", updatedAt: 5 } }, null, 2)}\n`,
"utf8",
);
await fs.writeFile(path.join(stateDir, "agent", "settings.json"), '{"ok":true}\n', "utf8");
await fs.writeFile(path.join(stateDir, "credentials", "creds.json"), '{"auth":true}\n', "utf8");
await fs.writeFile(
path.join(stateDir, "credentials", "oauth.json"),
'{"oauth":true}\n',
"utf8",
);
await fs.writeFile(resolveChannelAllowFromPath("telegram", env), '["123","456"]\n', "utf8");
const { root, stateDir, env, cfg } = await createLegacyStateFixture();
const detected = await detectLegacyStateMigrations({
cfg,
@@ -99,40 +113,7 @@ describe("state migrations", () => {
});
it("runs legacy state migrations and canonicalizes the merged session store", async () => {
const root = await createTempDir();
const stateDir = path.join(root, ".openclaw");
const env = createEnv(stateDir);
const cfg = createConfig();
await fs.mkdir(path.join(stateDir, "sessions"), { recursive: true });
await fs.mkdir(path.join(stateDir, "agents", "worker-1", "sessions"), { recursive: true });
await fs.mkdir(path.join(stateDir, "agent"), { recursive: true });
await fs.mkdir(path.join(stateDir, "credentials"), { recursive: true });
await fs.writeFile(
path.join(stateDir, "sessions", "sessions.json"),
`${JSON.stringify({ legacyDirect: { sessionId: "legacy-direct", updatedAt: 10 } }, null, 2)}\n`,
"utf8",
);
await fs.writeFile(path.join(stateDir, "sessions", "trace.jsonl"), "{}\n", "utf8");
await fs.writeFile(
path.join(stateDir, "agents", "worker-1", "sessions", "sessions.json"),
`${JSON.stringify({ "group:123@g.us": { sessionId: "group-session", updatedAt: 5 } }, null, 2)}\n`,
"utf8",
);
await fs.writeFile(path.join(stateDir, "agent", "settings.json"), '{"ok":true}\n', "utf8");
await fs.writeFile(path.join(stateDir, "credentials", "creds.json"), '{"auth":true}\n', "utf8");
await fs.writeFile(
path.join(stateDir, "credentials", "pre-key-1.json"),
'{"preKey":true}\n',
"utf8",
);
await fs.writeFile(
path.join(stateDir, "credentials", "oauth.json"),
'{"oauth":true}\n',
"utf8",
);
await fs.writeFile(resolveChannelAllowFromPath("telegram", env), '["123","456"]\n', "utf8");
const { root, stateDir, env, cfg } = await createLegacyStateFixture({ includePreKey: true });
const detected = await detectLegacyStateMigrations({
cfg,

View File

@@ -164,6 +164,12 @@ let extensionSourceFilesCache: string[] | null = null;
let coreSourceFilesCache: string[] | null = null;
const extensionFilesCache = new Map<string, string[]>();
type SourceFileCollectorOptions = {
rootDir: string;
shouldSkipPath?: (normalizedFullPath: string) => boolean;
shouldSkipEntry?: (params: { entryName: string; normalizedFullPath: string }) => boolean;
};
function readSource(path: string): string {
const fullPath = resolve(REPO_ROOT, path);
const cached = sourceTextCache.get(fullPath);
@@ -179,6 +185,51 @@ function normalizePath(path: string): string {
return path.replaceAll("\\", "/");
}
function collectSourceFiles(
cached: string[] | undefined | null,
options: SourceFileCollectorOptions,
): string[] {
if (cached) {
return cached;
}
const files: string[] = [];
const stack = [options.rootDir];
while (stack.length > 0) {
const current = stack.pop();
if (!current) {
continue;
}
for (const entry of readdirSync(current, { withFileTypes: true })) {
const fullPath = resolve(current, entry.name);
const normalizedFullPath = normalizePath(fullPath);
if (entry.isDirectory()) {
if (entry.name === "node_modules" || entry.name === "dist" || entry.name === "coverage") {
continue;
}
if (options.shouldSkipPath?.(normalizedFullPath)) {
continue;
}
stack.push(fullPath);
continue;
}
if (!entry.isFile() || !/\.(?:[cm]?ts|[cm]?js|tsx|jsx)$/u.test(entry.name)) {
continue;
}
if (entry.name.endsWith(".d.ts")) {
continue;
}
if (
options.shouldSkipPath?.(normalizedFullPath) ||
options.shouldSkipEntry?.({ entryName: entry.name, normalizedFullPath })
) {
continue;
}
files.push(fullPath);
}
}
return files;
}
function readSetupBarrelImportBlock(path: string): string {
const lines = readSource(path).split("\n");
const targetLineIndex = lines.findIndex((line) =>
@@ -195,145 +246,55 @@ function readSetupBarrelImportBlock(path: string): string {
}
function collectExtensionSourceFiles(): string[] {
if (extensionSourceFilesCache) {
return extensionSourceFilesCache;
}
const extensionsDir = normalizePath(resolve(ROOT_DIR, "..", "extensions"));
const sharedExtensionsDir = normalizePath(resolve(extensionsDir, "shared"));
const files: string[] = [];
const stack = [resolve(ROOT_DIR, "..", "extensions")];
while (stack.length > 0) {
const current = stack.pop();
if (!current) {
continue;
}
for (const entry of readdirSync(current, { withFileTypes: true })) {
const fullPath = resolve(current, entry.name);
const normalizedFullPath = normalizePath(fullPath);
if (entry.isDirectory()) {
if (entry.name === "node_modules" || entry.name === "dist" || entry.name === "coverage") {
continue;
}
stack.push(fullPath);
continue;
}
if (!entry.isFile() || !/\.(?:[cm]?ts|[cm]?js|tsx|jsx)$/u.test(entry.name)) {
continue;
}
if (entry.name.endsWith(".d.ts") || normalizedFullPath.includes(sharedExtensionsDir)) {
continue;
}
if (normalizedFullPath.includes(`${extensionsDir}/shared/`)) {
continue;
}
if (
normalizedFullPath.includes(".test.") ||
normalizedFullPath.includes(".test-") ||
normalizedFullPath.includes(".fixture.") ||
normalizedFullPath.includes(".snap") ||
normalizedFullPath.includes("test-support") ||
entry.name === "api.ts" ||
entry.name === "runtime-api.ts"
) {
continue;
}
files.push(fullPath);
}
}
extensionSourceFilesCache = files;
return files;
extensionSourceFilesCache = collectSourceFiles(extensionSourceFilesCache, {
rootDir: resolve(ROOT_DIR, "..", "extensions"),
shouldSkipPath: (normalizedFullPath) =>
normalizedFullPath.includes(sharedExtensionsDir) ||
normalizedFullPath.includes(`${extensionsDir}/shared/`),
shouldSkipEntry: ({ entryName, normalizedFullPath }) =>
normalizedFullPath.includes(".test.") ||
normalizedFullPath.includes(".test-") ||
normalizedFullPath.includes(".fixture.") ||
normalizedFullPath.includes(".snap") ||
normalizedFullPath.includes("test-support") ||
entryName === "api.ts" ||
entryName === "runtime-api.ts",
});
return extensionSourceFilesCache;
}
function collectCoreSourceFiles(): string[] {
if (coreSourceFilesCache) {
return coreSourceFilesCache;
}
const srcDir = resolve(ROOT_DIR, "..", "src");
const normalizedPluginSdkDir = normalizePath(resolve(ROOT_DIR, "plugin-sdk"));
const files: string[] = [];
const stack = [srcDir];
while (stack.length > 0) {
const current = stack.pop();
if (!current) {
continue;
}
for (const entry of readdirSync(current, { withFileTypes: true })) {
const fullPath = resolve(current, entry.name);
const normalizedFullPath = normalizePath(fullPath);
if (entry.isDirectory()) {
if (entry.name === "node_modules" || entry.name === "dist" || entry.name === "coverage") {
continue;
}
stack.push(fullPath);
continue;
}
if (!entry.isFile() || !/\.(?:[cm]?ts|[cm]?js|tsx|jsx)$/u.test(entry.name)) {
continue;
}
if (entry.name.endsWith(".d.ts")) {
continue;
}
if (
normalizedFullPath.includes(".test.") ||
normalizedFullPath.includes(".mock-harness.") ||
normalizedFullPath.includes(".spec.") ||
normalizedFullPath.includes(".fixture.") ||
normalizedFullPath.includes(".snap") ||
// src/plugin-sdk is the curated bridge layer; validate its contracts with dedicated
// plugin-sdk guardrails instead of the generic "core should not touch extensions" rule.
normalizedFullPath.includes(`${normalizedPluginSdkDir}/`)
) {
continue;
}
files.push(fullPath);
}
}
coreSourceFilesCache = files;
return files;
coreSourceFilesCache = collectSourceFiles(coreSourceFilesCache, {
rootDir: srcDir,
shouldSkipEntry: ({ normalizedFullPath }) =>
normalizedFullPath.includes(".test.") ||
normalizedFullPath.includes(".mock-harness.") ||
normalizedFullPath.includes(".spec.") ||
normalizedFullPath.includes(".fixture.") ||
normalizedFullPath.includes(".snap") ||
// src/plugin-sdk is the curated bridge layer; validate its contracts with dedicated
// plugin-sdk guardrails instead of the generic "core should not touch extensions" rule.
normalizedFullPath.includes(`${normalizedPluginSdkDir}/`),
});
return coreSourceFilesCache;
}
function collectExtensionFiles(extensionId: string): string[] {
const cached = extensionFilesCache.get(extensionId);
if (cached) {
return cached;
}
const extensionDir = resolve(ROOT_DIR, "..", "extensions", extensionId);
const files: string[] = [];
const stack = [extensionDir];
while (stack.length > 0) {
const current = stack.pop();
if (!current) {
continue;
}
for (const entry of readdirSync(current, { withFileTypes: true })) {
const fullPath = resolve(current, entry.name);
const normalizedFullPath = normalizePath(fullPath);
if (entry.isDirectory()) {
if (entry.name === "node_modules" || entry.name === "dist" || entry.name === "coverage") {
continue;
}
stack.push(fullPath);
continue;
}
if (!entry.isFile() || !/\.(?:[cm]?ts|[cm]?js|tsx|jsx)$/u.test(entry.name)) {
continue;
}
if (entry.name.endsWith(".d.ts")) {
continue;
}
if (
normalizedFullPath.includes(".test.") ||
normalizedFullPath.includes(".test-") ||
normalizedFullPath.includes(".spec.") ||
normalizedFullPath.includes(".fixture.") ||
normalizedFullPath.includes(".snap") ||
entry.name === "runtime-api.ts"
) {
continue;
}
files.push(fullPath);
}
}
const files = collectSourceFiles(cached, {
rootDir: resolve(ROOT_DIR, "..", "extensions", extensionId),
shouldSkipEntry: ({ entryName, normalizedFullPath }) =>
normalizedFullPath.includes(".test.") ||
normalizedFullPath.includes(".test-") ||
normalizedFullPath.includes(".spec.") ||
normalizedFullPath.includes(".fixture.") ||
normalizedFullPath.includes(".snap") ||
entryName === "runtime-api.ts",
});
extensionFilesCache.set(extensionId, files);
return files;
}

View File

@@ -22,6 +22,11 @@ import {
vi.unmock("../version.js");
const OPENAI_ENV_KEY_REF = { source: "env", provider: "default", id: "OPENAI_API_KEY" } as const;
const OPENAI_FILE_KEY_REF = {
source: "file",
provider: "default",
id: "/providers/openai/apiKey",
} as const;
const allowInsecureTempSecretFile = process.platform === "win32";
function asConfig(value: unknown): OpenClawConfig {
@@ -35,6 +40,77 @@ function loadAuthStoreWithProfiles(profiles: AuthProfileStore["profiles"]): Auth
};
}
async function createOpenAIFileRuntimeFixture(home: string) {
const configDir = path.join(home, ".openclaw");
const secretFile = path.join(configDir, "secrets.json");
const agentDir = path.join(configDir, "agents", "main", "agent");
const authStorePath = path.join(agentDir, "auth-profiles.json");
await fs.mkdir(agentDir, { recursive: true });
await fs.chmod(configDir, 0o700).catch(() => {});
await fs.writeFile(
secretFile,
`${JSON.stringify({ providers: { openai: { apiKey: "sk-file-runtime" } } }, null, 2)}\n`,
{ encoding: "utf8", mode: 0o600 },
);
await fs.writeFile(
authStorePath,
`${JSON.stringify(
{
version: 1,
profiles: {
"openai:default": {
type: "api_key",
provider: "openai",
keyRef: OPENAI_FILE_KEY_REF,
},
},
},
null,
2,
)}\n`,
{ encoding: "utf8", mode: 0o600 },
);
return {
configDir,
secretFile,
agentDir,
};
}
function createOpenAIFileRuntimeConfig(secretFile: string): OpenClawConfig {
return asConfig({
secrets: {
providers: {
default: {
source: "file",
path: secretFile,
mode: "json",
...(allowInsecureTempSecretFile ? { allowInsecurePath: true } : {}),
},
},
},
models: {
providers: {
openai: {
baseUrl: "https://api.openai.com/v1",
apiKey: OPENAI_FILE_KEY_REF,
models: [],
},
},
},
});
}
function expectResolvedOpenAIRuntime(agentDir: string) {
expect(loadConfig().models?.providers?.openai?.apiKey).toBe("sk-file-runtime");
expect(ensureAuthProfileStore(agentDir).profiles["openai:default"]).toMatchObject({
type: "api_key",
key: "sk-file-runtime",
});
}
describe("secrets runtime snapshot integration", () => {
let envSnapshot: ReturnType<typeof captureEnv>;
@@ -106,68 +182,16 @@ describe("secrets runtime snapshot integration", () => {
return;
}
await withTempHome("openclaw-secrets-runtime-write-", async (home) => {
const configDir = path.join(home, ".openclaw");
const secretFile = path.join(configDir, "secrets.json");
const agentDir = path.join(configDir, "agents", "main", "agent");
const authStorePath = path.join(agentDir, "auth-profiles.json");
await fs.mkdir(agentDir, { recursive: true });
await fs.chmod(configDir, 0o700).catch(() => {});
await fs.writeFile(
secretFile,
`${JSON.stringify({ providers: { openai: { apiKey: "sk-file-runtime" } } }, null, 2)}\n`,
{ encoding: "utf8", mode: 0o600 },
);
await fs.writeFile(
authStorePath,
`${JSON.stringify(
{
version: 1,
profiles: {
"openai:default": {
type: "api_key",
provider: "openai",
keyRef: { source: "file", provider: "default", id: "/providers/openai/apiKey" },
},
},
},
null,
2,
)}\n`,
{ encoding: "utf8", mode: 0o600 },
);
const { secretFile, agentDir } = await createOpenAIFileRuntimeFixture(home);
const prepared = await prepareSecretsRuntimeSnapshot({
config: asConfig({
secrets: {
providers: {
default: {
source: "file",
path: secretFile,
mode: "json",
...(allowInsecureTempSecretFile ? { allowInsecurePath: true } : {}),
},
},
},
models: {
providers: {
openai: {
baseUrl: "https://api.openai.com/v1",
apiKey: { source: "file", provider: "default", id: "/providers/openai/apiKey" },
models: [],
},
},
},
}),
config: createOpenAIFileRuntimeConfig(secretFile),
agentDirs: [agentDir],
});
activateSecretsRuntimeSnapshot(prepared);
expect(loadConfig().models?.providers?.openai?.apiKey).toBe("sk-file-runtime");
expect(ensureAuthProfileStore(agentDir).profiles["openai:default"]).toMatchObject({
type: "api_key",
key: "sk-file-runtime",
});
expectResolvedOpenAIRuntime(agentDir);
await writeConfigFile({
...loadConfig(),
@@ -175,11 +199,7 @@ describe("secrets runtime snapshot integration", () => {
});
expect(loadConfig().gateway?.auth).toEqual({ mode: "token" });
expect(loadConfig().models?.providers?.openai?.apiKey).toBe("sk-file-runtime");
expect(ensureAuthProfileStore(agentDir).profiles["openai:default"]).toMatchObject({
type: "api_key",
key: "sk-file-runtime",
});
expectResolvedOpenAIRuntime(agentDir);
});
});
@@ -188,35 +208,7 @@ describe("secrets runtime snapshot integration", () => {
return;
}
await withTempHome("openclaw-secrets-runtime-refresh-fail-", async (home) => {
const configDir = path.join(home, ".openclaw");
const secretFile = path.join(configDir, "secrets.json");
const agentDir = path.join(configDir, "agents", "main", "agent");
const authStorePath = path.join(agentDir, "auth-profiles.json");
await fs.mkdir(agentDir, { recursive: true });
await fs.chmod(configDir, 0o700).catch(() => {});
await fs.writeFile(
secretFile,
`${JSON.stringify({ providers: { openai: { apiKey: "sk-file-runtime" } } }, null, 2)}\n`,
{ encoding: "utf8", mode: 0o600 },
);
await fs.writeFile(
authStorePath,
`${JSON.stringify(
{
version: 1,
profiles: {
"openai:default": {
type: "api_key",
provider: "openai",
keyRef: { source: "file", provider: "default", id: "/providers/openai/apiKey" },
},
},
},
null,
2,
)}\n`,
{ encoding: "utf8", mode: 0o600 },
);
const { secretFile, agentDir } = await createOpenAIFileRuntimeFixture(home);
let loadAuthStoreCalls = 0;
const loadAuthStore = () => {
@@ -228,33 +220,13 @@ describe("secrets runtime snapshot integration", () => {
"openai:default": {
type: "api_key",
provider: "openai",
keyRef: { source: "file", provider: "default", id: "/providers/openai/apiKey" },
keyRef: OPENAI_FILE_KEY_REF,
},
});
};
const prepared = await prepareSecretsRuntimeSnapshot({
config: asConfig({
secrets: {
providers: {
default: {
source: "file",
path: secretFile,
mode: "json",
...(allowInsecureTempSecretFile ? { allowInsecurePath: true } : {}),
},
},
},
models: {
providers: {
openai: {
baseUrl: "https://api.openai.com/v1",
apiKey: { source: "file", provider: "default", id: "/providers/openai/apiKey" },
models: [],
},
},
},
}),
config: createOpenAIFileRuntimeConfig(secretFile),
agentDirs: [agentDir],
loadAuthStore,
});
@@ -273,16 +245,10 @@ describe("secrets runtime snapshot integration", () => {
const activeAfterFailure = getActiveSecretsRuntimeSnapshot();
expect(activeAfterFailure).not.toBeNull();
expect(loadConfig().gateway?.auth).toBeUndefined();
expect(loadConfig().models?.providers?.openai?.apiKey).toBe("sk-file-runtime");
expect(activeAfterFailure?.sourceConfig.models?.providers?.openai?.apiKey).toEqual({
source: "file",
provider: "default",
id: "/providers/openai/apiKey",
});
expect(ensureAuthProfileStore(agentDir).profiles["openai:default"]).toMatchObject({
type: "api_key",
key: "sk-file-runtime",
});
expectResolvedOpenAIRuntime(agentDir);
expect(activeAfterFailure?.sourceConfig.models?.providers?.openai?.apiKey).toEqual(
OPENAI_FILE_KEY_REF,
);
});
});