mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-31 12:33:31 +00:00
Merged via squash.
Prepared head SHA: 69e1861abf
Co-authored-by: bbblending <122739024+bbblending@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
This commit is contained in:
@@ -7,6 +7,7 @@ import {
|
||||
clearRuntimeConfigSnapshot,
|
||||
getRuntimeConfigSourceSnapshot,
|
||||
loadConfig,
|
||||
setRuntimeConfigSnapshotRefreshHandler,
|
||||
setRuntimeConfigSnapshot,
|
||||
writeConfigFile,
|
||||
} from "./io.js";
|
||||
@@ -41,6 +42,7 @@ function createRuntimeConfig(): OpenClawConfig {
|
||||
}
|
||||
|
||||
function resetRuntimeConfigState(): void {
|
||||
setRuntimeConfigSnapshotRefreshHandler(null);
|
||||
clearRuntimeConfigSnapshot();
|
||||
clearConfigCache();
|
||||
}
|
||||
@@ -96,4 +98,117 @@ describe("runtime config snapshot writes", () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("refreshes the runtime snapshot after writes so follow-up reads see persisted changes", async () => {
|
||||
await withTempHome("openclaw-config-runtime-write-refresh-", async (home) => {
|
||||
const configPath = path.join(home, ".openclaw", "openclaw.json");
|
||||
const sourceConfig: OpenClawConfig = {
|
||||
models: {
|
||||
providers: {
|
||||
openai: {
|
||||
baseUrl: "https://api.openai.com/v1",
|
||||
apiKey: { source: "env", provider: "default", id: "OPENAI_API_KEY" },
|
||||
models: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const runtimeConfig: OpenClawConfig = {
|
||||
models: {
|
||||
providers: {
|
||||
openai: {
|
||||
baseUrl: "https://api.openai.com/v1",
|
||||
apiKey: "sk-runtime-resolved", // pragma: allowlist secret
|
||||
models: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const nextRuntimeConfig: OpenClawConfig = {
|
||||
...runtimeConfig,
|
||||
gateway: { auth: { mode: "token" as const } },
|
||||
};
|
||||
|
||||
await fs.mkdir(path.dirname(configPath), { recursive: true });
|
||||
await fs.writeFile(configPath, `${JSON.stringify(sourceConfig, null, 2)}\n`, "utf8");
|
||||
|
||||
try {
|
||||
setRuntimeConfigSnapshot(runtimeConfig, sourceConfig);
|
||||
expect(loadConfig().gateway?.auth).toBeUndefined();
|
||||
|
||||
await writeConfigFile(nextRuntimeConfig);
|
||||
|
||||
expect(loadConfig().gateway?.auth).toEqual({ mode: "token" });
|
||||
expect(loadConfig().models?.providers?.openai?.apiKey).toBeDefined();
|
||||
|
||||
let persisted = JSON.parse(await fs.readFile(configPath, "utf8")) as {
|
||||
gateway?: { auth?: unknown };
|
||||
models?: { providers?: { openai?: { apiKey?: unknown } } };
|
||||
};
|
||||
expect(persisted.gateway?.auth).toEqual({ mode: "token" });
|
||||
// Post-write secret-ref: apiKey must stay as source ref (not plaintext).
|
||||
expect(persisted.models?.providers?.openai?.apiKey).toEqual({
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "OPENAI_API_KEY",
|
||||
});
|
||||
|
||||
// Follow-up write: runtimeConfigSourceSnapshot must be restored so second write
|
||||
// still runs secret-preservation merge-patch and keeps apiKey as ref (not plaintext).
|
||||
await writeConfigFile(loadConfig());
|
||||
persisted = JSON.parse(await fs.readFile(configPath, "utf8")) as {
|
||||
gateway?: { auth?: unknown };
|
||||
models?: { providers?: { openai?: { apiKey?: unknown } } };
|
||||
};
|
||||
expect(persisted.models?.providers?.openai?.apiKey).toEqual({
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "OPENAI_API_KEY",
|
||||
});
|
||||
} finally {
|
||||
clearRuntimeConfigSnapshot();
|
||||
clearConfigCache();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps the last-known-good runtime snapshot active while a specialized refresh is pending", async () => {
|
||||
await withTempHome("openclaw-config-runtime-refresh-pending-", async (home) => {
|
||||
const configPath = path.join(home, ".openclaw", "openclaw.json");
|
||||
const sourceConfig = createSourceConfig();
|
||||
const runtimeConfig = createRuntimeConfig();
|
||||
const nextRuntimeConfig: OpenClawConfig = {
|
||||
...runtimeConfig,
|
||||
gateway: { auth: { mode: "token" as const } },
|
||||
};
|
||||
|
||||
await fs.mkdir(path.dirname(configPath), { recursive: true });
|
||||
await fs.writeFile(configPath, `${JSON.stringify(sourceConfig, null, 2)}\n`, "utf8");
|
||||
|
||||
let releaseRefresh!: () => void;
|
||||
const refreshPending = new Promise<boolean>((resolve) => {
|
||||
releaseRefresh = () => resolve(true);
|
||||
});
|
||||
|
||||
try {
|
||||
setRuntimeConfigSnapshot(runtimeConfig, sourceConfig);
|
||||
setRuntimeConfigSnapshotRefreshHandler({
|
||||
refresh: async ({ sourceConfig: refreshedSource }) => {
|
||||
expect(refreshedSource.gateway?.auth).toEqual({ mode: "token" });
|
||||
expect(loadConfig().gateway?.auth).toBeUndefined();
|
||||
return await refreshPending;
|
||||
},
|
||||
});
|
||||
|
||||
const writePromise = writeConfigFile(nextRuntimeConfig);
|
||||
await Promise.resolve();
|
||||
|
||||
expect(loadConfig().gateway?.auth).toBeUndefined();
|
||||
releaseRefresh();
|
||||
await writePromise;
|
||||
} finally {
|
||||
resetRuntimeConfigState();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user