diff --git a/ccw/tests/file-utils.test.ts b/ccw/tests/file-utils.test.ts new file mode 100644 index 00000000..5996ece8 --- /dev/null +++ b/ccw/tests/file-utils.test.ts @@ -0,0 +1,115 @@ +/** + * Unit tests for file-utils utility module. + * + * Notes: + * - Targets the runtime implementation shipped in `ccw/dist`. + * - Uses in-memory fs stubs (no real file IO). + */ + +import { after, beforeEach, describe, it } from 'node:test'; +import assert from 'node:assert/strict'; +import path from 'node:path'; +import { createRequire } from 'node:module'; + +const require = createRequire(import.meta.url); +// eslint-disable-next-line @typescript-eslint/no-var-requires +const fs = require('node:fs') as typeof import('node:fs'); + +type FsState = { + existing: Set; + files: Map; + writeCalls: Array<{ path: string; content: string; encoding: string }>; +}; + +const state: FsState = { + existing: new Set(), + files: new Map(), + writeCalls: [], +}; + +function key(filePath: string): string { + return path.resolve(filePath).replace(/\\/g, '/').toLowerCase(); +} + +function setFile(filePath: string, content: string): void { + state.existing.add(key(filePath)); + state.files.set(key(filePath), content); +} + +const originalFs = { + existsSync: fs.existsSync, + readFileSync: fs.readFileSync, + writeFileSync: fs.writeFileSync, +}; + +fs.existsSync = ((filePath: string) => state.existing.has(key(filePath))) as any; +fs.readFileSync = ((filePath: string, encoding: string) => { + assert.equal(encoding, 'utf8'); + const content = state.files.get(key(filePath)); + if (content === undefined) { + throw new Error(`ENOENT: no such file or directory, open '${filePath}'`); + } + return content; +}) as any; +fs.writeFileSync = ((filePath: string, content: string, encoding: string) => { + state.writeCalls.push({ path: filePath, content: String(content), encoding: String(encoding) }); + setFile(filePath, String(content)); +}) as any; + +const fileUtilsUrl = new URL('../dist/utils/file-utils.js', import.meta.url).href; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +let mod: any; + +beforeEach(() => { + state.existing.clear(); + state.files.clear(); + state.writeCalls.length = 0; +}); + +describe('file-utils utility module', async () => { + mod = await import(fileUtilsUrl); + + it('readJsonFile parses valid JSON and returns null on failures', () => { + const jsonPath = 'C:\\tmp\\data.json'; + setFile(jsonPath, JSON.stringify({ ok: true, n: 1 })); + + assert.deepEqual(mod.readJsonFile(jsonPath), { ok: true, n: 1 }); + assert.equal(mod.readJsonFile('C:\\tmp\\missing.json'), null); + + const invalidPath = 'C:\\tmp\\invalid.json'; + setFile(invalidPath, '{'); + assert.equal(mod.readJsonFile(invalidPath), null); + }); + + it('readTextFile reads existing files and returns null when missing', () => { + const textPath = 'C:\\tmp\\note.txt'; + setFile(textPath, 'hello'); + + assert.equal(mod.readTextFile(textPath), 'hello'); + assert.equal(mod.readTextFile('C:\\tmp\\missing.txt'), null); + }); + + it('writeTextFile writes UTF-8 content', () => { + mod.writeTextFile('C:\\tmp\\out.txt', 'content'); + + assert.equal(state.writeCalls.length, 1); + assert.deepEqual(state.writeCalls[0], { + path: 'C:\\tmp\\out.txt', + content: 'content', + encoding: 'utf8', + }); + }); + + it('pathExists delegates to existsSync', () => { + assert.equal(mod.pathExists('C:\\tmp\\nope'), false); + setFile('C:\\tmp\\yes', '1'); + assert.equal(mod.pathExists('C:\\tmp\\yes'), true); + }); +}); + +after(() => { + fs.existsSync = originalFs.existsSync; + fs.readFileSync = originalFs.readFileSync; + fs.writeFileSync = originalFs.writeFileSync; +}); +