From e63825ddc7a7593b93a904ec5d0474c991a44021 Mon Sep 17 00:00:00 2001 From: Copilot Date: Fri, 3 Apr 2026 21:42:51 +0300 Subject: [PATCH 1/5] feat(sdk): add scratchDir/scratchFile utilities for organized temp files (#790) - Add scratchDir() and scratchFile() to resolution.ts - Export from SDK barrel - Create .squad/.scratch/ during init, add to .gitignore - 8 new tests Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .changeset/scratch-dir-utility.md | 10 ++++ packages/squad-sdk/src/config/init.ts | 2 + packages/squad-sdk/src/index.ts | 2 +- packages/squad-sdk/src/resolution.ts | 43 ++++++++++++++++ test/scratch-dir.test.ts | 71 +++++++++++++++++++++++++++ 5 files changed, 127 insertions(+), 1 deletion(-) create mode 100644 .changeset/scratch-dir-utility.md create mode 100644 test/scratch-dir.test.ts diff --git a/.changeset/scratch-dir-utility.md b/.changeset/scratch-dir-utility.md new file mode 100644 index 000000000..9fd5d5769 --- /dev/null +++ b/.changeset/scratch-dir-utility.md @@ -0,0 +1,10 @@ +--- +"@bradygaster/squad-sdk": minor +--- + +Add `.squad/.scratch/` directory for organized temp file management (#790) + +- `scratchDir()` — resolve and optionally create the scratch directory +- `scratchFile()` — create named temp files inside `.scratch/` +- Init scaffolds `.squad/.scratch/` and adds it to `.gitignore` +- Agents and CLI should use these utilities instead of writing temp files to repo root diff --git a/packages/squad-sdk/src/config/init.ts b/packages/squad-sdk/src/config/init.ts index 8dd85cdef..23765af45 100644 --- a/packages/squad-sdk/src/config/init.ts +++ b/packages/squad-sdk/src/config/init.ts @@ -695,6 +695,7 @@ export async function initSquad(options: InitOptions, storage: StorageProvider = join(squadDir, 'identity'), join(squadDir, 'orchestration-log'), join(squadDir, 'log'), + join(squadDir, '.scratch'), ]; for (const dir of directories) { @@ -1011,6 +1012,7 @@ ${projectDescription ? `- **Description:** ${projectDescription}\n` : ''}- **Cre '.squad/log/', '.squad/decisions/inbox/', '.squad/sessions/', + '.squad/.scratch/', ]; let existingIgnore = ''; diff --git a/packages/squad-sdk/src/index.ts b/packages/squad-sdk/src/index.ts index 185a192a2..0ff8ba0f7 100644 --- a/packages/squad-sdk/src/index.ts +++ b/packages/squad-sdk/src/index.ts @@ -10,7 +10,7 @@ const pkg = require('../package.json'); export const VERSION: string = pkg.version; // Export public API -export { resolveSquad, resolveGlobalSquadPath, resolvePersonalSquadDir, ensurePersonalSquadDir, ensureSquadPath, ensureSquadPathTriple, loadDirConfig, isConsultMode } from './resolution.js'; +export { resolveSquad, resolveGlobalSquadPath, resolvePersonalSquadDir, ensurePersonalSquadDir, ensureSquadPath, ensureSquadPathTriple, loadDirConfig, isConsultMode, scratchDir, scratchFile } from './resolution.js'; export type { SquadDirConfig, ResolvedSquadPaths } from './resolution.js'; export * from './config/index.js'; export * from './agents/onboarding.js'; diff --git a/packages/squad-sdk/src/resolution.ts b/packages/squad-sdk/src/resolution.ts index fbffaf461..9e322a53d 100644 --- a/packages/squad-sdk/src/resolution.ts +++ b/packages/squad-sdk/src/resolution.ts @@ -471,3 +471,46 @@ export function ensureSquadPathTriple( export function ensureSquadPathResolved(filePath: string, paths: ResolvedSquadPaths): string { return ensureSquadPathDual(filePath, paths.projectDir, paths.teamDir); } + +/** + * Resolve the scratch directory for temporary files. + * + * Returns `{squadRoot}/.scratch/` — the canonical location for ephemeral files + * that Squad and its agents create during operations (prompt files, intermediate + * processing artifacts, commit message drafts, etc.). + * + * If `create` is true (default), the directory is created if it does not exist. + * + * @param squadRoot - Absolute path to the `.squad/` directory. + * @param create - Whether to create the directory if missing (default: true). + * @returns Absolute path to the scratch directory. + */ +export function scratchDir(squadRoot: string, create: boolean = true): string { + const dir = path.join(squadRoot, '.scratch'); + if (create && !storage.existsSync(dir)) { + storage.mkdirSync(dir, { recursive: true }); + } + return dir; +} + +/** + * Create a temporary file inside the scratch directory. + * + * Returns the absolute path to the file. The caller is responsible for + * deleting the file when done (or relying on the cleanup capability). + * + * @param squadRoot - Absolute path to the `.squad/` directory. + * @param prefix - Filename prefix (e.g. `"fleet-prompt"`). + * @param ext - File extension including dot (e.g. `".txt"`). Defaults to `".tmp"`. + * @param content - Optional content to write immediately. + * @returns Absolute path to the created temp file. + */ +export function scratchFile(squadRoot: string, prefix: string, ext: string = '.tmp', content?: string): string { + const dir = scratchDir(squadRoot); + const filename = `${prefix}-${Date.now()}${ext}`; + const filePath = path.join(dir, filename); + if (content !== undefined) { + storage.writeSync(filePath, content); + } + return filePath; +} diff --git a/test/scratch-dir.test.ts b/test/scratch-dir.test.ts new file mode 100644 index 000000000..65c47bf49 --- /dev/null +++ b/test/scratch-dir.test.ts @@ -0,0 +1,71 @@ +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { scratchDir, scratchFile } from '@bradygaster/squad-sdk'; +import { mkdirSync, rmSync, existsSync, readFileSync, readdirSync } from 'node:fs'; +import path from 'node:path'; +import os from 'node:os'; + +const TEST_ROOT = path.join(os.tmpdir(), `squad-scratch-test-${Date.now()}`); +const SQUAD_ROOT = path.join(TEST_ROOT, '.squad'); + +beforeEach(() => { + mkdirSync(SQUAD_ROOT, { recursive: true }); +}); + +afterEach(() => { + rmSync(TEST_ROOT, { recursive: true, force: true }); +}); + +describe('scratchDir', () => { + it('creates .scratch/ inside .squad/ when create=true (default)', () => { + const dir = scratchDir(SQUAD_ROOT); + expect(dir).toBe(path.join(SQUAD_ROOT, '.scratch')); + expect(existsSync(dir)).toBe(true); + }); + + it('returns path without creating when create=false', () => { + const dir = scratchDir(SQUAD_ROOT, false); + expect(dir).toBe(path.join(SQUAD_ROOT, '.scratch')); + expect(existsSync(dir)).toBe(false); + }); + + it('is idempotent — calling twice does not throw', () => { + scratchDir(SQUAD_ROOT); + scratchDir(SQUAD_ROOT); + expect(existsSync(path.join(SQUAD_ROOT, '.scratch'))).toBe(true); + }); +}); + +describe('scratchFile', () => { + it('creates a temp file with prefix and default .tmp extension', () => { + const filePath = scratchFile(SQUAD_ROOT, 'test-prompt'); + expect(filePath).toContain('.scratch'); + expect(filePath).toMatch(/test-prompt-\d+\.tmp$/); + // File exists only if content was provided — otherwise just returns path + expect(existsSync(path.dirname(filePath))).toBe(true); + }); + + it('creates a file with content when provided', () => { + const content = 'hello from scratch'; + const filePath = scratchFile(SQUAD_ROOT, 'msg', '.txt', content); + expect(existsSync(filePath)).toBe(true); + expect(readFileSync(filePath, 'utf-8')).toBe(content); + }); + + it('uses custom extension', () => { + const filePath = scratchFile(SQUAD_ROOT, 'fleet', '.md'); + expect(filePath).toMatch(/fleet-\d+\.md$/); + }); + + it('generates unique filenames on successive calls', () => { + const a = scratchFile(SQUAD_ROOT, 'dup', '.txt', 'a'); + // tiny delay to ensure different timestamp + const b = scratchFile(SQUAD_ROOT, 'dup', '.txt', 'b'); + expect(a).not.toBe(b); + }); + + it('creates the .scratch/ directory if missing', () => { + expect(existsSync(path.join(SQUAD_ROOT, '.scratch'))).toBe(false); + scratchFile(SQUAD_ROOT, 'auto-create', '.txt', 'data'); + expect(existsSync(path.join(SQUAD_ROOT, '.scratch'))).toBe(true); + }); +}); From c5fa8a1e035a5046484cf3b237f9b4b7f0599d03 Mon Sep 17 00:00:00 2001 From: Copilot Date: Fri, 3 Apr 2026 22:06:47 +0300 Subject: [PATCH 2/5] fix: add counter to scratchFile for uniqueness in fast CI environments Date.now() can return identical values on rapid successive calls. Added monotonic counter suffix to guarantee unique filenames. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- packages/squad-sdk/src/resolution.ts | 4 +++- test/scratch-dir.test.ts | 8 +++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/squad-sdk/src/resolution.ts b/packages/squad-sdk/src/resolution.ts index 9e322a53d..19f3f9226 100644 --- a/packages/squad-sdk/src/resolution.ts +++ b/packages/squad-sdk/src/resolution.ts @@ -505,9 +505,11 @@ export function scratchDir(squadRoot: string, create: boolean = true): string { * @param content - Optional content to write immediately. * @returns Absolute path to the created temp file. */ +let _scratchCounter = 0; + export function scratchFile(squadRoot: string, prefix: string, ext: string = '.tmp', content?: string): string { const dir = scratchDir(squadRoot); - const filename = `${prefix}-${Date.now()}${ext}`; + const filename = `${prefix}-${Date.now()}-${_scratchCounter++}${ext}`; const filePath = path.join(dir, filename); if (content !== undefined) { storage.writeSync(filePath, content); diff --git a/test/scratch-dir.test.ts b/test/scratch-dir.test.ts index 65c47bf49..c5ca3724e 100644 --- a/test/scratch-dir.test.ts +++ b/test/scratch-dir.test.ts @@ -39,7 +39,7 @@ describe('scratchFile', () => { it('creates a temp file with prefix and default .tmp extension', () => { const filePath = scratchFile(SQUAD_ROOT, 'test-prompt'); expect(filePath).toContain('.scratch'); - expect(filePath).toMatch(/test-prompt-\d+\.tmp$/); + expect(filePath).toMatch(/test-prompt-\d+-\d+\.tmp$/); // File exists only if content was provided — otherwise just returns path expect(existsSync(path.dirname(filePath))).toBe(true); }); @@ -53,14 +53,16 @@ describe('scratchFile', () => { it('uses custom extension', () => { const filePath = scratchFile(SQUAD_ROOT, 'fleet', '.md'); - expect(filePath).toMatch(/fleet-\d+\.md$/); + expect(filePath).toMatch(/fleet-\d+-\d+\.md$/); }); it('generates unique filenames on successive calls', () => { const a = scratchFile(SQUAD_ROOT, 'dup', '.txt', 'a'); - // tiny delay to ensure different timestamp const b = scratchFile(SQUAD_ROOT, 'dup', '.txt', 'b'); + // Counter suffix ensures uniqueness even when Date.now() returns the same value expect(a).not.toBe(b); + expect(existsSync(a)).toBe(true); + expect(existsSync(b)).toBe(true); }); it('creates the .scratch/ directory if missing', () => { From d1ae96eb8e48ddc14689879139cae4e2295043d2 Mon Sep 17 00:00:00 2001 From: Copilot Date: Sat, 4 Apr 2026 19:06:11 +0300 Subject: [PATCH 3/5] fix: address Copilot review comments on scratch-dir - Sanitize prefix/ext in scratchFile to prevent path traversal - Proper monotonic counter for sub-millisecond uniqueness - JSDoc clarifies caller writes content (not always auto-written) - Remove unused readdirSync import from test - Add comment explaining monotonic counter in uniqueness test Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- packages/squad-sdk/src/resolution.ts | 30 ++++++++++++++++++++++------ test/scratch-dir.test.ts | 7 ++++--- 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/packages/squad-sdk/src/resolution.ts b/packages/squad-sdk/src/resolution.ts index 19f3f9226..476efb98a 100644 --- a/packages/squad-sdk/src/resolution.ts +++ b/packages/squad-sdk/src/resolution.ts @@ -1,4 +1,4 @@ -/** +/** * Squad directory resolution — walk-up and global path algorithms. * * resolveSquad() — find .squad/ by walking up from startDir to .git boundary @@ -494,22 +494,40 @@ export function scratchDir(squadRoot: string, create: boolean = true): string { } /** - * Create a temporary file inside the scratch directory. + * Return a unique file path inside the scratch directory. * - * Returns the absolute path to the file. The caller is responsible for - * deleting the file when done (or relying on the cleanup capability). + * Returns the absolute path to the file. Caller is responsible for writing + * content to the returned path (unless `content` is provided, in which case + * it is written immediately). The caller is also responsible for deleting + * the file when done (or relying on the cleanup capability). * * @param squadRoot - Absolute path to the `.squad/` directory. * @param prefix - Filename prefix (e.g. `"fleet-prompt"`). * @param ext - File extension including dot (e.g. `".txt"`). Defaults to `".tmp"`. * @param content - Optional content to write immediately. - * @returns Absolute path to the created temp file. + * @returns Absolute path to the temp file. */ let _scratchCounter = 0; +let _scratchLastTs = 0; export function scratchFile(squadRoot: string, prefix: string, ext: string = '.tmp', content?: string): string { + // Sanitize prefix/ext to prevent path traversal via '../' sequences + const safePrefix = prefix.replace(/[\/\\]/g, '_'); + const safeExt = ext.replace(/[\/\\]/g, '_'); + const dir = scratchDir(squadRoot); - const filename = `${prefix}-${Date.now()}-${_scratchCounter++}${ext}`; + + // Monotonic counter: if two calls happen in the same millisecond, + // the counter increments to guarantee unique filenames. + const now = Date.now(); + if (now === _scratchLastTs) { + _scratchCounter++; + } else { + _scratchCounter = 0; + _scratchLastTs = now; + } + + const filename = `${safePrefix}-${now}-${_scratchCounter}${safeExt}`; const filePath = path.join(dir, filename); if (content !== undefined) { storage.writeSync(filePath, content); diff --git a/test/scratch-dir.test.ts b/test/scratch-dir.test.ts index c5ca3724e..812a8a8bc 100644 --- a/test/scratch-dir.test.ts +++ b/test/scratch-dir.test.ts @@ -1,6 +1,6 @@ -import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import { scratchDir, scratchFile } from '@bradygaster/squad-sdk'; -import { mkdirSync, rmSync, existsSync, readFileSync, readdirSync } from 'node:fs'; +import { mkdirSync, rmSync, existsSync, readFileSync } from 'node:fs'; import path from 'node:path'; import os from 'node:os'; @@ -59,7 +59,8 @@ describe('scratchFile', () => { it('generates unique filenames on successive calls', () => { const a = scratchFile(SQUAD_ROOT, 'dup', '.txt', 'a'); const b = scratchFile(SQUAD_ROOT, 'dup', '.txt', 'b'); - // Counter suffix ensures uniqueness even when Date.now() returns the same value + // No delay needed: the monotonic counter inside scratchFile guarantees + // unique filenames even when Date.now() returns the same millisecond value. expect(a).not.toBe(b); expect(existsSync(a)).toBe(true); expect(existsSync(b)).toBe(true); From 8e10d3c1860f448903205b8e35516b2489d675cd Mon Sep 17 00:00:00 2001 From: Copilot Date: Sat, 4 Apr 2026 23:34:07 +0300 Subject: [PATCH 4/5] fix: address remaining Copilot review comments on scratch-dir PR - Add ensureSquadPath guard after path construction to prevent traversal - Add crypto.randomUUID() for stronger uniqueness alongside monotonic counter - Update JSDoc to clarify file-creation vs path-only behavior per Copilot suggestion - Update test regex to match new filename format with UUID segment Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- packages/squad-sdk/src/resolution.ts | 23 +++++++++++++++-------- test/scratch-dir.test.ts | 4 ++-- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/packages/squad-sdk/src/resolution.ts b/packages/squad-sdk/src/resolution.ts index 476efb98a..f7be930d5 100644 --- a/packages/squad-sdk/src/resolution.ts +++ b/packages/squad-sdk/src/resolution.ts @@ -14,6 +14,7 @@ import path from 'node:path'; import os from 'node:os'; +import crypto from 'node:crypto'; import { FSStorageProvider } from './storage/fs-storage-provider.js'; const storage = new FSStorageProvider(); @@ -494,18 +495,20 @@ export function scratchDir(squadRoot: string, create: boolean = true): string { } /** - * Return a unique file path inside the scratch directory. + * Generate a temporary file path inside the scratch directory. * - * Returns the absolute path to the file. Caller is responsible for writing - * content to the returned path (unless `content` is provided, in which case - * it is written immediately). The caller is also responsible for deleting - * the file when done (or relying on the cleanup capability). + * Returns the absolute path to the file. If `content` is provided, the file is + * created immediately with that content. Otherwise, this function only returns + * a unique path and does not create the file. + * + * The caller is responsible for deleting the file when done (or relying on the + * cleanup capability). * * @param squadRoot - Absolute path to the `.squad/` directory. * @param prefix - Filename prefix (e.g. `"fleet-prompt"`). * @param ext - File extension including dot (e.g. `".txt"`). Defaults to `".tmp"`. - * @param content - Optional content to write immediately. - * @returns Absolute path to the temp file. + * @param content - Optional content; when provided, the file is written immediately. + * @returns Absolute path to the temp file, whether or not it has been created yet. */ let _scratchCounter = 0; let _scratchLastTs = 0; @@ -527,8 +530,12 @@ export function scratchFile(squadRoot: string, prefix: string, ext: string = '.t _scratchLastTs = now; } - const filename = `${safePrefix}-${now}-${_scratchCounter}${safeExt}`; + const filename = `${safePrefix}-${now}-${_scratchCounter}-${crypto.randomUUID().slice(0, 8)}${safeExt}`; const filePath = path.join(dir, filename); + + // Guard against path traversal — ensure the final path stays inside .squad/ + ensureSquadPath(filePath, squadRoot); + if (content !== undefined) { storage.writeSync(filePath, content); } diff --git a/test/scratch-dir.test.ts b/test/scratch-dir.test.ts index 812a8a8bc..60ffaea87 100644 --- a/test/scratch-dir.test.ts +++ b/test/scratch-dir.test.ts @@ -39,7 +39,7 @@ describe('scratchFile', () => { it('creates a temp file with prefix and default .tmp extension', () => { const filePath = scratchFile(SQUAD_ROOT, 'test-prompt'); expect(filePath).toContain('.scratch'); - expect(filePath).toMatch(/test-prompt-\d+-\d+\.tmp$/); + expect(filePath).toMatch(/test-prompt-\d+-\d+-[0-9a-f]{8}\.tmp$/); // File exists only if content was provided — otherwise just returns path expect(existsSync(path.dirname(filePath))).toBe(true); }); @@ -53,7 +53,7 @@ describe('scratchFile', () => { it('uses custom extension', () => { const filePath = scratchFile(SQUAD_ROOT, 'fleet', '.md'); - expect(filePath).toMatch(/fleet-\d+-\d+\.md$/); + expect(filePath).toMatch(/fleet-\d+-\d+-[0-9a-f]{8}\.md$/); }); it('generates unique filenames on successive calls', () => { From c253197f857e45b9e8b0a422ae997977de880421 Mon Sep 17 00:00:00 2001 From: Copilot Date: Sun, 5 Apr 2026 07:56:12 +0300 Subject: [PATCH 5/5] =?UTF-8?q?fix:=20address=20Copilot=20review=20?= =?UTF-8?q?=E2=80=94=20sanitize=20prefix,=20add=20random=20suffix,=20fix?= =?UTF-8?q?=20JSDoc=20+=20test?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Use path.basename(prefix) to prevent path traversal via ../ or absolute paths - Replace monotonic counter with crypto.randomBytes(4) hex suffix for collision-free filenames - Update JSDoc to accurately state: writes content if provided, otherwise returns path only - Update test regex patterns and comments to reflect new hex-based uniqueness mechanism Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- packages/squad-sdk/src/resolution.ts | 27 +++++++++------------------ test/scratch-dir.test.ts | 8 ++++---- 2 files changed, 13 insertions(+), 22 deletions(-) diff --git a/packages/squad-sdk/src/resolution.ts b/packages/squad-sdk/src/resolution.ts index b2ec13dd0..217a4f1ae 100644 --- a/packages/squad-sdk/src/resolution.ts +++ b/packages/squad-sdk/src/resolution.ts @@ -14,6 +14,7 @@ import path from 'node:path'; import os from 'node:os'; +import crypto from 'node:crypto'; import { FSStorageProvider } from './storage/fs-storage-provider.js'; const storage = new FSStorageProvider(); @@ -499,10 +500,10 @@ export function scratchDir(squadRoot: string, create: boolean = true): string { /** * Return a unique file path inside the scratch directory. * - * Returns the absolute path to the file. Caller is responsible for writing - * content to the returned path (unless `content` is provided, in which case - * it is written immediately). The caller is also responsible for deleting - * the file when done (or relying on the cleanup capability). + * Writes content to the file if `content` is provided; otherwise returns + * the path only and the caller is responsible for writing to it. + * The caller is also responsible for deleting the file when done + * (or relying on the cleanup capability). * * @param squadRoot - Absolute path to the `.squad/` directory. * @param prefix - Filename prefix (e.g. `"fleet-prompt"`). @@ -510,27 +511,17 @@ export function scratchDir(squadRoot: string, create: boolean = true): string { * @param content - Optional content to write immediately. * @returns Absolute path to the temp file. */ -let _scratchCounter = 0; -let _scratchLastTs = 0; - export function scratchFile(squadRoot: string, prefix: string, ext: string = '.tmp', content?: string): string { - // Sanitize prefix/ext to prevent path traversal via '../' sequences - const safePrefix = prefix.replace(/[\/\\]/g, '_'); + // Sanitize prefix to prevent path traversal — strip directory components + const safePrefix = path.basename(prefix); const safeExt = ext.replace(/[\/\\]/g, '_'); const dir = scratchDir(squadRoot); - // Monotonic counter: if two calls happen in the same millisecond, - // the counter increments to guarantee unique filenames. const now = Date.now(); - if (now === _scratchLastTs) { - _scratchCounter++; - } else { - _scratchCounter = 0; - _scratchLastTs = now; - } + const rand = crypto.randomBytes(4).toString('hex'); - const filename = `${safePrefix}-${now}-${_scratchCounter}${safeExt}`; + const filename = `${safePrefix}-${now}-${rand}${safeExt}`; const filePath = path.join(dir, filename); if (content !== undefined) { storage.writeSync(filePath, content); diff --git a/test/scratch-dir.test.ts b/test/scratch-dir.test.ts index 60ffaea87..e93d23cb9 100644 --- a/test/scratch-dir.test.ts +++ b/test/scratch-dir.test.ts @@ -39,7 +39,7 @@ describe('scratchFile', () => { it('creates a temp file with prefix and default .tmp extension', () => { const filePath = scratchFile(SQUAD_ROOT, 'test-prompt'); expect(filePath).toContain('.scratch'); - expect(filePath).toMatch(/test-prompt-\d+-\d+-[0-9a-f]{8}\.tmp$/); + expect(filePath).toMatch(/test-prompt-\d+-[0-9a-f]{8}\.tmp$/); // File exists only if content was provided — otherwise just returns path expect(existsSync(path.dirname(filePath))).toBe(true); }); @@ -53,14 +53,14 @@ describe('scratchFile', () => { it('uses custom extension', () => { const filePath = scratchFile(SQUAD_ROOT, 'fleet', '.md'); - expect(filePath).toMatch(/fleet-\d+-\d+-[0-9a-f]{8}\.md$/); + expect(filePath).toMatch(/fleet-\d+-[0-9a-f]{8}\.md$/); }); it('generates unique filenames on successive calls', () => { const a = scratchFile(SQUAD_ROOT, 'dup', '.txt', 'a'); const b = scratchFile(SQUAD_ROOT, 'dup', '.txt', 'b'); - // No delay needed: the monotonic counter inside scratchFile guarantees - // unique filenames even when Date.now() returns the same millisecond value. + // Random hex suffix from crypto.randomBytes guarantees uniqueness + // even when Date.now() returns the same millisecond value. expect(a).not.toBe(b); expect(existsSync(a)).toBe(true); expect(existsSync(b)).toBe(true);