Loading files
This file is pinned to the selected release. Download the original to open it in your design tool.
3aa468eadThis file is pinned to the selected release. Download the original to open it in your design tool.
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { mkdtemp, mkdir, rm, stat, utimes, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import path from 'node:path';
import { existsSync } from 'node:fs';
import { sweepStaleTempDirs, pruneHistoryDir, TEMP_PREFIX } from '../src/util/tmp.js';
// These exercise the I8 startup sweep. They create real `copperhead-*` dirs in
// the OS temp dir (the exact place the leak lives) and age them via utimes, so
// the age gate is tested against the same clock the sweep reads.
//
// Fixture mtimes are anchored to the *real* clock, never to the synthetic `now`
// injected into the sweep. The OS temp dir is shared, and `create` runs its own
// sweep at `Date.now()` on startup (src/commands/create.ts): a fixture aged
// against a synthetic epoch looks decades stale to that real-clock sweep, so a
// create test in a parallel worker deletes it mid-test and this file fails with
// an empty `removed`. Keeping fixtures real-clock-fresh and moving the injected
// `now` forward instead makes them stale only to the call under test.
describe('sweepStaleTempDirs (I8: reclaim leaked scratch dirs)', () => {
const made: string[] = [];
async function makeAged(suffix: string, ageMs: number, base: number): Promise<string> {
const dir = path.join(tmpdir(), `${TEMP_PREFIX}${suffix}-${process.pid}-${made.length}`);
await mkdir(dir, { recursive: true });
await writeFile(path.join(dir, 'erc.json'), '{}'); // non-empty, like a real leak
const when = new Date(base - ageMs);
await utimes(dir, when, when);
made.push(dir);
return dir;
}
afterEach(async () => {
for (const d of made.splice(0)) await rm(d, { recursive: true, force: true });
});
it('removes a stale dir but keeps a fresh one', async () => {
// Both dirs are minutes old by the real clock, so a concurrent real-clock
// sweep leaves them alone; the injected `now` is 100m ahead, which pushes
// only the older one past the 2h cutoff.
const base = Date.now();
const stale = await makeAged('erc-stale', 30 * 60 * 1000, base); // 130m to the sweep
const fresh = await makeAged('cc-fresh', 60 * 1000, base); // 101m to the sweep
const now = base + 100 * 60 * 1000;
const removed = await sweepStaleTempDirs(now); // default 2h cutoff
expect(removed).toContain(stale);
expect(removed).not.toContain(fresh);
expect(existsSync(stale)).toBe(false);
expect(existsSync(fresh)).toBe(true);
});
it('never touches non-copperhead temp dirs', async () => {
const now = 1_000_000_000_000;
const foreign = await mkdtemp(path.join(tmpdir(), 'someone-else-'));
made.push(foreign);
const when = new Date(now - 24 * 60 * 60 * 1000); // a day old
await utimes(foreign, when, when);
const removed = await sweepStaleTempDirs(now);
expect(removed).not.toContain(foreign);
expect(existsSync(foreign)).toBe(true);
});
it('honours a custom maxAge and returns [] when nothing is stale', async () => {
const now = Date.now(); // real clock: 90s old is fresh to the default 2h sweep
const recent = await makeAged('validate-recent', 90 * 1000, now); // 90s old
// 60s cutoff: the 90s-old dir is stale and swept.
expect(await sweepStaleTempDirs(now, 60 * 1000)).toContain(recent);
});
it('is best-effort: a missing temp root yields [] rather than throwing', async () => {
// stat of the sweep is guarded; a well-formed call on an empty result set
// must simply return [] (no dirs match the prefix at this instant is fine).
const removed = await sweepStaleTempDirs(0, 1);
expect(Array.isArray(removed)).toBe(true);
});
});
describe('pruneHistoryDir (4.1: cap .history/ growth)', () => {
let repo: string;
beforeEach(async () => {
repo = await mkdtemp(path.join(tmpdir(), 'copperhead-hist-test-'));
});
afterEach(async () => {
await rm(repo, { recursive: true, force: true });
});
it('keeps the newest N entries and removes older ones (recursively)', async () => {
const hist = path.join(repo, '.history', 'nested');
await mkdir(hist, { recursive: true });
const base = 1_000_000_000_000;
for (let i = 0; i < 10; i++) {
const f = path.join(hist, `snap-${i}.kicad_sch`);
await writeFile(f, `v${i}`);
const when = new Date(base + i * 1000); // ascending mtime: higher i = newer
await utimes(f, when, when);
}
const removed = await pruneHistoryDir(repo, 4);
expect(removed).toBe(6); // 10 - 4 kept
// the 4 newest (i=6..9) survive; the 6 oldest are gone
expect(existsSync(path.join(hist, 'snap-9.kicad_sch'))).toBe(true);
expect(existsSync(path.join(hist, 'snap-6.kicad_sch'))).toBe(true);
expect(existsSync(path.join(hist, 'snap-5.kicad_sch'))).toBe(false);
expect(existsSync(path.join(hist, 'snap-0.kicad_sch'))).toBe(false);
});
it('is a no-op when under the cap or the dir is absent', async () => {
expect(await pruneHistoryDir(repo, 200)).toBe(0); // no .history/ at all
await mkdir(path.join(repo, '.history'), { recursive: true });
await writeFile(path.join(repo, '.history', 'a'), 'x');
expect(await pruneHistoryDir(repo, 200)).toBe(0); // one file, under cap
expect(existsSync(path.join(repo, '.history', 'a'))).toBe(true);
});
});