import assert from 'node:assert/strict'; import { mkdir, mkdtemp, readFile, rm, symlink, writeFile, } from 'node:fs/promises'; import os from 'node:os'; import path from 'node:path'; import test from 'node:test'; import { activeRustBuildProcesses, auditRustBuildCache, cleanIncrementalCaches, parseArguments, parseWindowsTaskList, } from './rust-build-cache.mjs'; async function createFixture(t) { const root = await mkdtemp(path.join(os.tmpdir(), 'genarrative-rust-cache-')); t.after(() => rm(root, { recursive: true, force: true })); await mkdir(path.join(root, 'server-rs', 'target', 'debug', 'incremental'), { recursive: true, }); await mkdir(path.join(root, 'server-rs', 'target', 'debug', 'deps'), { recursive: true, }); await mkdir( path.join( root, 'apps', 'ai-game-creator-shell', 'src-tauri', 'target', 'debug', 'incremental', ), { recursive: true }, ); await mkdir( path.join( root, 'apps', 'ai-game-creator-shell', 'src-tauri', 'target', 'release', ), { recursive: true }, ); await writeFile(path.join(root, 'package.json'), '{}'); await writeFile(path.join(root, 'server-rs', 'Cargo.toml'), '[workspace]'); await writeFile( path.join(root, 'apps', 'ai-game-creator-shell', 'src-tauri', 'Cargo.toml'), '[package]\nname="fixture"\nversion="0.0.0"', ); await writeFile( path.join( root, 'server-rs', 'target', 'debug', 'incremental', 'server.bin', ), Buffer.alloc(17), ); await writeFile( path.join(root, 'server-rs', 'target', 'debug', 'deps', 'keep.rlib'), Buffer.alloc(5), ); await writeFile( path.join( root, 'apps', 'ai-game-creator-shell', 'src-tauri', 'target', 'debug', 'incremental', 'agc.bin', ), Buffer.alloc(23), ); await writeFile( path.join( root, 'apps', 'ai-game-creator-shell', 'src-tauri', 'target', 'release', 'keep.exe', ), Buffer.alloc(7), ); return root; } test('audit is read-only and clean requires explicit apply', async (t) => { const root = await createFixture(t); const audit = await auditRustBuildCache({ repoRoot: root, maxBytes: 1 }); assert.equal(audit.incrementalBytes, 40); assert.equal(audit.warning, true); const dryRun = await cleanIncrementalCaches({ repoRoot: root }); assert.equal(dryRun.applied, false); assert.equal(dryRun.plannedBytes, 40); assert.equal( ( await readFile( path.join( root, 'server-rs', 'target', 'debug', 'incremental', 'server.bin', ), ) ).length, 17, ); }); test('apply removes only fixed incremental directories and is idempotent', async (t) => { const root = await createFixture(t); const result = await cleanIncrementalCaches({ repoRoot: root, apply: true, processNames: [], }); assert.equal(result.applied, true); assert.equal(result.auditAfter.incrementalBytes, 0); assert.equal( ( await readFile( path.join(root, 'server-rs', 'target', 'debug', 'deps', 'keep.rlib'), ) ).length, 5, ); assert.equal( ( await readFile( path.join( root, 'apps', 'ai-game-creator-shell', 'src-tauri', 'target', 'release', 'keep.exe', ), ) ).length, 7, ); const replay = await cleanIncrementalCaches({ repoRoot: root, apply: true, processNames: [], }); assert.equal(replay.freedBytes, 0); }); test('apply fails closed while cargo or rustc is active', async (t) => { const root = await createFixture(t); await assert.rejects( cleanIncrementalCaches({ repoRoot: root, apply: true, processNames: ['cargo.exe', 'node.exe'], }), /活跃 Rust 构建进程/u, ); assert.equal( ( await readFile( path.join( root, 'server-rs', 'target', 'debug', 'incremental', 'server.bin', ), ) ).length, 17, ); }); test('managed paths reject a linked target directory', async (t) => { const root = await createFixture(t); const incremental = path.join( root, 'apps', 'ai-game-creator-shell', 'src-tauri', 'target', 'debug', 'incremental', ); const outside = await mkdtemp( path.join(os.tmpdir(), 'genarrative-rust-cache-outside-'), ); t.after(() => rm(outside, { recursive: true, force: true })); await rm(incremental, { recursive: true }); try { await symlink( outside, incremental, process.platform === 'win32' ? 'junction' : 'dir', ); } catch (error) { if (error?.code === 'EPERM') { t.skip('current Windows token cannot create a junction fixture'); return; } throw error; } await assert.rejects( auditRustBuildCache({ repoRoot: root }), /符号链接或 junction/u, ); }); test('argument and process parsing keep the destructive boundary explicit', () => { assert.deepEqual(parseArguments([]), { cleanIncremental: false, apply: false, json: false, maxGiB: 120, }); assert.equal(parseArguments(['--clean-incremental', '--apply']).apply, true); assert.throws(() => parseArguments(['--apply']), /只能与/u); assert.throws(() => parseArguments(['--root', 'C:\\']), /未知参数/u); assert.deepEqual( activeRustBuildProcesses(['Cargo.EXE', 'node.exe', 'rustc.exe']), ['cargo.exe', 'rustc.exe'], ); assert.deepEqual( parseWindowsTaskList( '"cargo.exe","1","Console","1","10 K"\r\n"node.exe","2"', ), ['cargo.exe', 'node.exe'], ); });