Skip to content

Commit 58fa23b

Browse files
committed
test: align fs-safe dependency expectations
1 parent a859638 commit 58fa23b

6 files changed

Lines changed: 186 additions & 130 deletions

File tree

src/infra/archive.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,7 @@ describe("archive utils", () => {
240240
} catch (error) {
241241
rejected = true;
242242
expect(error).toMatchObject({
243-
code: "destination-symlink-traversal",
243+
code: expect.stringMatching(/destination-symlink-traversal|not-file/),
244244
} satisfies Partial<ArchiveSecurityError>);
245245
}
246246

src/infra/fs-safe.test.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,9 @@ async function runWriteOpenRace(params: {
3838
await params.runWrite();
3939
} catch (err) {
4040
expect(err).toMatchObject({
41-
code: expect.stringMatching(/outside-workspace|path-mismatch|path-alias|invalid-path/),
41+
code: expect.stringMatching(
42+
/outside-workspace|path-mismatch|path-alias|invalid-path|not-file/,
43+
),
4244
});
4345
}
4446
},

src/infra/json-file.test.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -175,10 +175,9 @@ describe("json-file helpers", () => {
175175
},
176176
);
177177

178-
it("falls back to copy when rename-based overwrite fails", async () => {
178+
it("preserves payload when rename-based overwrite reports EPERM", async () => {
179179
await withJsonPath(({ root, pathname }) => {
180180
writeExistingJson(pathname);
181-
const copySpy = vi.spyOn(fs, "copyFileSync");
182181
const renameSpy = vi.spyOn(fs, "renameSync").mockImplementationOnce(() => {
183182
const err = new Error("EPERM") as NodeJS.ErrnoException;
184183
err.code = "EPERM";
@@ -187,8 +186,7 @@ describe("json-file helpers", () => {
187186

188187
saveJsonFile(pathname, SAVED_PAYLOAD);
189188

190-
expect(renameSpy).toHaveBeenCalledOnce();
191-
expect(copySpy).toHaveBeenCalledOnce();
189+
expect(renameSpy).toHaveBeenCalled();
192190
expect(loadJsonFile(pathname)).toEqual(SAVED_PAYLOAD);
193191
expect(fs.readdirSync(root)).toEqual(["config.json"]);
194192
});

src/infra/json-files.test.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -96,20 +96,18 @@ describe("json file helpers", () => {
9696
});
9797
});
9898

99-
it("falls back to copy-on-replace for Windows rename EPERM", async () => {
99+
it("preserves text when Windows rename reports EPERM", async () => {
100100
await withTempDir({ prefix: "openclaw-json-files-" }, async (base) => {
101101
const filePath = path.join(base, "state.json");
102102
await fs.writeFile(filePath, "old", "utf8");
103103

104104
Object.defineProperty(process, "platform", { value: "win32", configurable: true });
105105
const renameError = Object.assign(new Error("EPERM"), { code: "EPERM" });
106106
const renameSpy = vi.spyOn(fs, "rename").mockRejectedValueOnce(renameError);
107-
const copySpy = vi.spyOn(fs, "copyFile");
108107

109108
await writeTextAtomic(filePath, "new");
110109

111110
expect(renameSpy).toHaveBeenCalledOnce();
112-
expect(copySpy).toHaveBeenCalledOnce();
113111
await expect(fs.readFile(filePath, "utf8")).resolves.toBe("new");
114112
});
115113
});

src/media/store.test.ts

Lines changed: 89 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -52,30 +52,97 @@ describe("media store", () => {
5252
segment: string;
5353
run: (store: typeof import("./store.js"), home: string) => Promise<{ path: string }>;
5454
}) {
55-
await withTempStore(async (store, home) => {
56-
const originalWriteFile = fs.writeFile.bind(fs);
57-
let injectedEnoent = false;
58-
vi.spyOn(fs, "writeFile").mockImplementation(async (...args) => {
59-
const [filePath] = args;
60-
if (
61-
!injectedEnoent &&
62-
typeof filePath === "string" &&
63-
filePath.includes(`${path.sep}${params.segment}${path.sep}`)
64-
) {
65-
injectedEnoent = true;
66-
await fs.rm(path.dirname(filePath), { recursive: true, force: true });
67-
const err = new Error("missing dir") as NodeJS.ErrnoException;
68-
err.code = "ENOENT";
69-
throw err;
70-
}
71-
return await originalWriteFile(...args);
55+
const mockKey = `./store.js?scope=retry-pruned-write-${params.segment}-${Date.now()}-${Math.random().toString(36).slice(2)}`;
56+
let injectedEnoent = false;
57+
vi.doMock("../infra/file-store.js", async (importOriginal) => {
58+
const actual = await importOriginal<typeof import("../infra/file-store.js")>();
59+
return {
60+
...actual,
61+
fileStore: (options: Parameters<typeof actual.fileStore>[0]) => {
62+
const actualStore = actual.fileStore(options);
63+
return {
64+
...actualStore,
65+
write: async (...args: Parameters<typeof actualStore.write>) => {
66+
const [relativePath] = args;
67+
if (!injectedEnoent && relativePath.includes(`${params.segment}${path.sep}`)) {
68+
injectedEnoent = true;
69+
await fs.rm(path.dirname(actualStore.path(relativePath)), {
70+
recursive: true,
71+
force: true,
72+
});
73+
const err = new Error("missing dir") as NodeJS.ErrnoException;
74+
err.code = "ENOENT";
75+
throw err;
76+
}
77+
return await actualStore.write(...args);
78+
},
79+
};
80+
},
81+
};
82+
});
83+
84+
try {
85+
const storeWithMock = await importFreshModule<typeof import("./store.js")>(
86+
import.meta.url,
87+
mockKey,
88+
);
89+
await withTempStore(async (_store, home) => {
90+
const saved = await params.run(storeWithMock, home);
91+
const savedStat = await fs.stat(saved.path);
92+
expect(injectedEnoent).toBe(true);
93+
expect(savedStat.isFile()).toBe(true);
7294
});
95+
} finally {
96+
vi.doUnmock("../infra/file-store.js");
97+
}
98+
}
7399

74-
const saved = await params.run(store, home);
75-
const savedStat = await fs.stat(saved.path);
76-
expect(injectedEnoent).toBe(true);
77-
expect(savedStat.isFile()).toBe(true);
100+
async function expectFailedBufferWriteCase() {
101+
const mockKey = `./store.js?scope=failed-buffer-write-${Date.now()}-${Math.random().toString(36).slice(2)}`;
102+
const attemptedRelPaths: string[] = [];
103+
vi.doMock("../infra/file-store.js", async (importOriginal) => {
104+
const actual = await importOriginal<typeof import("../infra/file-store.js")>();
105+
return {
106+
...actual,
107+
fileStore: (options: Parameters<typeof actual.fileStore>[0]) => {
108+
const actualStore = actual.fileStore(options);
109+
return {
110+
...actualStore,
111+
write: async (...args: Parameters<typeof actualStore.write>) => {
112+
const [relativePath] = args;
113+
if (relativePath.includes(`failed-buffer${path.sep}`)) {
114+
attemptedRelPaths.push(relativePath);
115+
const err = new Error("no space left on device") as NodeJS.ErrnoException;
116+
err.code = "ENOSPC";
117+
throw err;
118+
}
119+
return await actualStore.write(...args);
120+
},
121+
};
122+
},
123+
};
78124
});
125+
126+
try {
127+
const storeWithMock = await importFreshModule<typeof import("./store.js")>(
128+
import.meta.url,
129+
mockKey,
130+
);
131+
await withTempStore(async (_store) => {
132+
const mediaDir = await storeWithMock.ensureMediaDir();
133+
await expect(
134+
storeWithMock.saveMediaBuffer(Buffer.from("voice"), "audio/ogg", "failed-buffer"),
135+
).rejects.toMatchObject({ code: "ENOSPC" });
136+
137+
const failedDir = path.join(mediaDir, "failed-buffer");
138+
const entries = await fs.readdir(failedDir).catch(() => []);
139+
expect(attemptedRelPaths).toHaveLength(1);
140+
expect(path.basename(attemptedRelPaths[0] ?? "")).toMatch(/^[^/\\]+\.ogg$/);
141+
expect(entries).toEqual([]);
142+
});
143+
} finally {
144+
vi.doUnmock("../infra/file-store.js");
145+
}
79146
}
80147

81148
async function expectSavedOriginalFilenameCase(params: {
@@ -310,35 +377,7 @@ describe("media store", () => {
310377
{
311378
name: "does not leave final media artifacts when buffer writes fail",
312379
run: async () => {
313-
await withTempStore(async (store) => {
314-
const mediaDir = await store.ensureMediaDir();
315-
const originalWriteFile = fs.writeFile.bind(fs);
316-
const attemptedPaths: string[] = [];
317-
vi.spyOn(fs, "writeFile").mockImplementation(async (...args) => {
318-
const [filePath] = args;
319-
if (
320-
typeof filePath === "string" &&
321-
filePath.includes(`${path.sep}failed-buffer${path.sep}`)
322-
) {
323-
attemptedPaths.push(filePath);
324-
await originalWriteFile(filePath, Buffer.alloc(0), args[2]);
325-
const err = new Error("no space left on device") as NodeJS.ErrnoException;
326-
err.code = "ENOSPC";
327-
throw err;
328-
}
329-
return await originalWriteFile(...args);
330-
});
331-
332-
await expect(
333-
store.saveMediaBuffer(Buffer.from("voice"), "audio/ogg", "failed-buffer"),
334-
).rejects.toMatchObject({ code: "ENOSPC" });
335-
336-
const failedDir = path.join(mediaDir, "failed-buffer");
337-
const entries = await fs.readdir(failedDir).catch(() => []);
338-
expect(attemptedPaths).toHaveLength(1);
339-
expect(path.basename(attemptedPaths[0] ?? "")).toMatch(/^\..+\.tmp$/);
340-
expect(entries).toEqual([]);
341-
});
380+
await expectFailedBufferWriteCase();
342381
},
343382
},
344383
{

0 commit comments

Comments
 (0)