Skip to content

Commit 034f5bc

Browse files
committed
refactor(util): address review feedback on writeAtomically
- Add optional onCleanupError callback so callers can log orphaned temp files; cliCredentialManager passes its logger to restore the warn-on-cleanup behavior that existed before the extraction. - Document that writeAtomically requires the parent directory to exist. - Use suffix "temp" so the call matches the doc example and the cliManager convention. - Tighten the json writer's midstream-error test to assert the destination survives and no temp file is left behind. - Fix the asyncIterable helper's docstring: it exercises async iteration, not stream backpressure.
1 parent ce9851c commit 034f5bc

4 files changed

Lines changed: 21 additions & 7 deletions

File tree

src/core/cliCredentialManager.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -266,8 +266,11 @@ export class CliCredentialManager {
266266
content: string,
267267
): Promise<void> {
268268
await fs.mkdir(path.dirname(filePath), { recursive: true });
269-
await writeAtomically(filePath, (tempPath) =>
270-
fs.writeFile(tempPath, content, { mode: 0o600 }),
269+
await writeAtomically(
270+
filePath,
271+
(tempPath) => fs.writeFile(tempPath, content, { mode: 0o600 }),
272+
(rmErr, tempPath) =>
273+
this.logger.warn("Failed to delete temp file", tempPath, rmErr),
271274
);
272275
}
273276
}

src/util/fs.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,19 +58,26 @@ export function tempFilePath(basePath: string, suffix: string): string {
5858
* Writes `outputPath` atomically: `write` builds the content at a sibling
5959
* temp path, then we rename onto the destination. A failure mid-write never
6060
* touches the destination; the temp file is best-effort cleaned up.
61+
*
62+
* The parent directory of `outputPath` must already exist; if it doesn't,
63+
* the `write` callback will surface an ENOENT on the temp path. Provide
64+
* `onCleanupError` to be notified if the post-failure temp-file removal
65+
* itself fails (e.g. for Windows-lock diagnostics); the original error is
66+
* always rethrown regardless.
6167
*/
6268
export async function writeAtomically<T>(
6369
outputPath: string,
6470
write: (tempPath: string) => Promise<T>,
71+
onCleanupError?: (err: unknown, tempPath: string) => void,
6572
): Promise<T> {
66-
const tempPath = tempFilePath(outputPath, "tmp");
73+
const tempPath = tempFilePath(outputPath, "temp");
6774
try {
6875
const result = await write(tempPath);
6976
await renameWithRetry(fs.rename, tempPath, outputPath);
7077
return result;
7178
} catch (err) {
72-
await fs.rm(tempPath, { force: true }).catch(() => {
73-
// Surface the original failure, not the cleanup failure.
79+
await fs.rm(tempPath, { force: true }).catch((rmErr) => {
80+
onCleanupError?.(rmErr, tempPath);
7481
});
7582
throw err;
7683
}

test/mocks/asyncIterable.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/**
22
* Wraps a sync array as an `AsyncIterable` that yields one item per microtask,
3-
* so consumers exercise the same backpressure path they would in production.
3+
* so consumers exercise the same async iteration path they would in production.
44
*/
55
export async function* asyncIterable<T>(
66
values: readonly T[],

test/unit/telemetry/export/writers/json.test.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,13 +44,17 @@ describe("writeJsonArrayExport", () => {
4444
expect(readOut()).toEqual([]);
4545
});
4646

47-
it("propagates midstream errors", async () => {
47+
it("leaves the destination untouched on midstream errors", async () => {
48+
vol.writeFileSync(OUT, "previous");
4849
const failing = (async function* () {
4950
yield makeEvent();
5051
await Promise.resolve();
5152
throw new Error("boom");
5253
})();
5354

5455
await expect(writeJsonArrayExport(OUT, failing)).rejects.toThrow(/boom/);
56+
57+
expect(vol.readFileSync(OUT, "utf8")).toBe("previous");
58+
expect(vol.readdirSync("/exports")).toEqual(["telemetry.json"]);
5559
});
5660
});

0 commit comments

Comments
 (0)