






















@@ -106,6 +106,46 @@ describe("config io write", () => {
106106return persisted.commands;
107107};
108108109+const requireRecord = (value: unknown, label: string): Record<string, unknown> => {
110+if (!value || typeof value !== "object" || Array.isArray(value)) {
111+throw new Error(`expected ${label} to be a record`);
112+}
113+return value as Record<string, unknown>;
114+};
115+116+const requireArray = (value: unknown, label: string): unknown[] => {
117+if (!Array.isArray(value)) {
118+throw new Error(`expected ${label} to be an array`);
119+}
120+return value;
121+};
122+123+const expectInstallRecord = (
124+record: unknown,
125+expected: { source: string; spec: string; installPath: string },
126+) => {
127+const actual = requireRecord(record, "plugin install record");
128+expect(actual.source).toBe(expected.source);
129+expect(actual.spec).toBe(expected.spec);
130+expect(actual.installPath).toBe(expected.installPath);
131+};
132+133+const expectConfigWriteRejected = async (promise: Promise<unknown>) => {
134+try {
135+await promise;
136+} catch (error) {
137+expect(requireRecord(error, "config write rejection").code).toBe("CONFIG_WRITE_REJECTED");
138+return;
139+}
140+throw new Error("expected config write rejection");
141+};
142+143+const expectPersistedHashResult = (result: unknown) => {
144+const persistedHash = requireRecord(result, "config write result").persistedHash;
145+expect(typeof persistedHash).toBe("string");
146+expect(persistedHash).not.toBe("");
147+};
148+109149const createFastConfigIO = (home: string) =>
110150createConfigIO({
111151env: { OPENCLAW_TEST_FAST: "1" } as NodeJS.ProcessEnv,
@@ -155,8 +195,9 @@ describe("config io write", () => {
155195logger: { warn, error: vi.fn() },
156196});
157197158-await expect(io.readConfigFileSnapshot()).resolves.toMatchObject({ exists: true });
159-expect(io.loadConfig()).toMatchObject({ gateway: { mode: "local" } });
198+const snapshot = await io.readConfigFileSnapshot();
199+expect(snapshot.exists).toBe(true);
200+expect(io.loadConfig().gateway).toEqual({ mode: "local" });
160201161202expect(warn).toHaveBeenCalledWith(
162203expect.stringContaining(
@@ -256,18 +297,18 @@ describe("config io write", () => {
256297const initialRaw = await fs.readFile(configPath, "utf-8");
257298const cfg = io.loadConfig();
258299259-expect(cfg.plugins?.installs?.demo).toMatchObject({
300+expectInstallRecord(cfg.plugins?.installs?.demo, {
260301source: "npm",
261302spec: "demo@1.0.0",
262303installPath: pluginDir,
263304});
264305const snapshot = await io.readConfigFileSnapshot();
265-expect(snapshot.sourceConfig.plugins?.installs?.demo).toMatchObject({
306+expectInstallRecord(snapshot.sourceConfig.plugins?.installs?.demo, {
266307source: "npm",
267308spec: "demo@1.0.0",
268309installPath: pluginDir,
269310});
270-expect(snapshot.runtimeConfig.plugins?.installs?.demo).toMatchObject({
311+expectInstallRecord(snapshot.runtimeConfig.plugins?.installs?.demo, {
271312source: "npm",
272313spec: "demo@1.0.0",
273314installPath: pluginDir,
@@ -350,25 +391,22 @@ describe("config io write", () => {
350391},
351392});
352393353-await expect(
354-readPersistedInstalledPluginIndex({
394+const index = requireRecord(
395+await readPersistedInstalledPluginIndex({
355396stateDir: path.join(home, ".openclaw"),
356397}),
357-).resolves.toMatchObject({
358-installRecords: {
359-demo: {
360-source: "npm",
361-spec: "demo@1.0.0",
362-installPath: pluginDir,
363-},
364-},
365-plugins: [
366-expect.objectContaining({
367-pluginId: "demo",
368-installRecordHash: expect.stringMatching(/^[a-f0-9]{64}$/u),
369-}),
370-],
398+"persisted plugin index",
399+);
400+expectInstallRecord(requireRecord(index.installRecords, "install records").demo, {
401+source: "npm",
402+spec: "demo@1.0.0",
403+installPath: pluginDir,
371404});
405+const plugins = requireArray(index.plugins, "plugin index plugins");
406+expect(plugins).toHaveLength(1);
407+const indexedPlugin = requireRecord(plugins[0], "indexed plugin");
408+expect(indexedPlugin.pluginId).toBe("demo");
409+expect(indexedPlugin.installRecordHash).toMatch(/^[a-f0-9]{64}$/u);
372410const persistedConfig = JSON.parse(await fs.readFile(configPath, "utf-8")) as {
373411plugins?: { installs?: unknown };
374412};
@@ -415,20 +453,18 @@ describe("config io write", () => {
415453},
416454});
417455418-await expect(
419-readPersistedInstalledPluginIndex({
456+const index = requireRecord(
457+await readPersistedInstalledPluginIndex({
420458stateDir: path.join(home, ".openclaw"),
421459}),
422-).resolves.toMatchObject({
423-installRecords: {
424-missing: {
425-source: "npm",
426-spec: "missing-plugin@1.0.0",
427-installPath: pluginDir,
428-},
429-},
430-plugins: [],
460+"persisted plugin index",
461+);
462+expectInstallRecord(requireRecord(index.installRecords, "install records").missing, {
463+source: "npm",
464+spec: "missing-plugin@1.0.0",
465+installPath: pluginDir,
431466});
467+expect(index.plugins).toEqual([]);
432468const persistedConfig = JSON.parse(await fs.readFile(configPath, "utf-8")) as {
433469plugins?: { installs?: unknown };
434470};
@@ -464,7 +500,7 @@ describe("config io write", () => {
464500await fs.writeFile(path.join(unwritableStatePath, "plugins"), "not a directory", "utf-8");
465501466502const loadedConfig = io.loadConfig();
467-expect(loadedConfig.plugins?.installs?.demo).toMatchObject({
503+expectInstallRecord(loadedConfig.plugins?.installs?.demo, {
468504source: "npm",
469505spec: "demo@1.0.0",
470506installPath: pluginDir,
@@ -478,7 +514,7 @@ describe("config io write", () => {
478514);
479515480516const persisted = JSON.parse(await fs.readFile(configPath, "utf-8")) as typeof original;
481-expect(persisted.plugins.installs.demo).toMatchObject({
517+expectInstallRecord(persisted.plugins.installs.demo, {
482518source: "npm",
483519spec: "demo@1.0.0",
484520installPath: pluginDir,
@@ -512,7 +548,7 @@ describe("config io write", () => {
512548);
513549514550const persistedConfig = JSON.parse(await fs.readFile(configPath, "utf-8")) as typeof original;
515-expect(persistedConfig.plugins.installs.demo).toMatchObject({
551+expectInstallRecord(persistedConfig.plugins.installs.demo, {
516552source: "npm",
517553spec: "demo@1.0.0",
518554installPath: pluginDir,
@@ -816,16 +852,14 @@ describe("config io write", () => {
816852legacyIssues: [],
817853} satisfies ConfigFileSnapshot;
818854819-await expect(
855+await expectConfigWriteRejected(
820856io.writeConfigFile(
821857{ update: { channel: "beta" } },
822858{
823859 baseSnapshot,
824860},
825861),
826-).rejects.toMatchObject({
827-code: "CONFIG_WRITE_REJECTED",
828-});
862+);
829863830864await expect(fs.readFile(configPath, "utf-8")).resolves.toBe(originalRaw);
831865const entries = await fs.readdir(path.dirname(configPath));
@@ -872,29 +906,24 @@ describe("config io write", () => {
872906legacyIssues: [],
873907} satisfies ConfigFileSnapshot;
874908875-await expect(
876-io.writeConfigFile(
877-{ meta: original.meta, gateway: { mode: "local" } },
878-{
879-allowConfigSizeDrop: true,
880- baseSnapshot,
881-},
882-),
883-).resolves.toMatchObject({
884-persistedConfig: expect.objectContaining({ gateway: { mode: "local" } }),
885-});
909+const acceptedWrite = await io.writeConfigFile(
910+{ meta: original.meta, gateway: { mode: "local" } },
911+{
912+allowConfigSizeDrop: true,
913+ baseSnapshot,
914+},
915+);
916+expect(acceptedWrite.persistedConfig.gateway).toEqual({ mode: "local" });
886917887-await expect(
918+await expectConfigWriteRejected(
888919io.writeConfigFile(
889920{ meta: original.meta },
890921{
891922allowConfigSizeDrop: true,
892923 baseSnapshot,
893924},
894925),
895-).rejects.toMatchObject({
896-code: "CONFIG_WRITE_REJECTED",
897-});
926+);
898927});
899928});
900929@@ -1059,8 +1088,8 @@ describe("config io write", () => {
10591088logger: silentLogger,
10601089});
106110901062-await expect(
1063-io.writeConfigFile({
1091+expectPersistedHashResult(
1092+await io.writeConfigFile({
10641093agents: { list: [{ id: "main", default: true }] },
10651094plugins: {
10661095entries: {
@@ -1070,7 +1099,7 @@ describe("config io write", () => {
10701099},
10711100},
10721101}),
1073-).resolves.toMatchObject({ persistedHash: expect.any(String) });
1102+);
10741103});
1075110410761105mockLoadPluginManifestRegistry.mockReturnValue({
@@ -1147,7 +1176,10 @@ describe("config io write", () => {
11471176},
11481177});
114911781150-expect(JSON.parse(await fs.readFile(configPath, "utf-8"))).toEqual({
1179+const persisted = JSON.parse(await fs.readFile(configPath, "utf-8")) as {
1180+meta?: Record<string, unknown>;
1181+};
1182+expect(persisted).toEqual({
11511183gateway: { mode: "local", port: 18789 },
11521184models: {
11531185providers: {
@@ -1159,10 +1191,12 @@ describe("config io write", () => {
11591191},
11601192},
11611193meta: {
1162-lastTouchedAt: expect.any(String),
1163-lastTouchedVersion: expect.any(String),
1194+lastTouchedAt: persisted.meta?.lastTouchedAt,
1195+lastTouchedVersion: persisted.meta?.lastTouchedVersion,
11641196},
11651197});
1198+expect(typeof persisted.meta?.lastTouchedAt).toBe("string");
1199+expect(typeof persisted.meta?.lastTouchedVersion).toBe("string");
11661200} finally {
11671201if (previousConfigPath === undefined) {
11681202delete process.env.OPENCLAW_CONFIG_PATH;
@@ -1227,24 +1261,21 @@ describe("config io write", () => {
12271261agents: { defaults: { model: { primary: "openrouter/anthropic/claude-sonnet-4.6" } } },
12281262});
122912631230-expect(JSON.parse(await fs.readFile(configPath, "utf-8"))).toMatchObject({
1231-gateway: {
1232-auth: { token: "${OPENCLAW_GATEWAY_TOKEN}" },
1264+const persisted = JSON.parse(await fs.readFile(configPath, "utf-8")) as {
1265+gateway?: { auth?: { token?: string } };
1266+};
1267+expect(persisted.gateway?.auth?.token).toBe("${OPENCLAW_GATEWAY_TOKEN}");
1268+expect(observedSources).toHaveLength(1);
1269+const observedSource = requireRecord(observedSources[0], "observed source config");
1270+expect(observedSource.gateway).toEqual({
1271+mode: "local",
1272+auth: { mode: "token", token: "gateway-token-runtime" },
1273+});
1274+expect(observedSource.agents).toEqual({
1275+defaults: {
1276+model: { primary: "openrouter/anthropic/claude-sonnet-4.6" },
12331277},
12341278});
1235-expect(observedSources).toEqual([
1236-expect.objectContaining({
1237-gateway: {
1238-mode: "local",
1239-auth: { mode: "token", token: "gateway-token-runtime" },
1240-},
1241-agents: {
1242-defaults: {
1243-model: { primary: "openrouter/anthropic/claude-sonnet-4.6" },
1244-},
1245-},
1246-}),
1247-]);
12481279} finally {
12491280unsubscribe();
12501281if (previousConfigPath === undefined) {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。