diff --git a/src/verification.test.ts b/src/verification.test.ts index 54cda99..4d1ba94 100644 --- a/src/verification.test.ts +++ b/src/verification.test.ts @@ -10,6 +10,7 @@ import { isVerificationProfile, runVerificationRequest, } from './verification.js'; +import { hasInstalledNodeModules } from './workspace-package-manager.js'; describe('verification helpers', () => { it('recognizes only fixed verification profiles', () => { @@ -82,16 +83,18 @@ describe('verification helpers', () => { expect(third).not.toBe(first); }); - it('fails verification when node_modules contains only cache noise', async () => { + it('backfills legacy node_modules state before verification', async () => { const repoDir = fs.mkdtempSync( - path.join(os.tmpdir(), 'ejclaw-verification-noise-'), + path.join(os.tmpdir(), 'ejclaw-verification-legacy-'), ); - fs.mkdirSync(path.join(repoDir, 'node_modules', '.vite'), { + fs.mkdirSync(path.join(repoDir, 'node_modules', '.bin'), { recursive: true, }); fs.writeFileSync( path.join(repoDir, 'package.json'), JSON.stringify({ + name: 'verification-legacy', + packageManager: 'bun@1.3.11', scripts: { test: 'vitest run', typecheck: 'tsc --noEmit', @@ -99,16 +102,22 @@ describe('verification helpers', () => { }, }), ); + fs.writeFileSync(path.join(repoDir, 'bun.lock'), ''); + fs.writeFileSync(path.join(repoDir, 'node_modules', '.bin', 'tsc'), ''); + + expect(hasInstalledNodeModules(repoDir)).toBe(false); const result = await runVerificationRequest( { - requestId: 'req-noise', + requestId: 'req-legacy-node-modules', profile: 'typecheck', + expectedSnapshotId: 'fs:mismatch', }, { repoDir }, ); + expect(hasInstalledNodeModules(repoDir)).toBe(true); expect(result.ok).toBe(false); - expect(result.error).toContain('installed node_modules tree'); + expect(result.error).toContain('Snapshot mismatch before verification'); }); }); diff --git a/src/verification.ts b/src/verification.ts index af2efa0..51a37ad 100644 --- a/src/verification.ts +++ b/src/verification.ts @@ -15,6 +15,7 @@ import { resolveGroupIpcPath } from './group-folder.js'; import { buildWorkspaceScriptCommand, detectPnpmStorePath, + ensureWorkspaceDependenciesInstalled, hasInstalledNodeModules, } from './workspace-package-manager.js'; import { @@ -271,6 +272,27 @@ export async function runVerificationRequest( }; } + if (!hasInstalledNodeModules(repoDir)) { + try { + ensureWorkspaceDependenciesInstalled(repoDir); + } catch (error) { + return { + ok: false, + profile: request.profile, + command: command.commandText, + stdout: '', + stderr: '', + exitCode: 1, + snapshotId: 'unknown', + runtimeVersion, + error: + error instanceof Error + ? error.message + : String(error), + }; + } + } + if (!hasInstalledNodeModules(repoDir)) { return { ok: false, diff --git a/src/workspace-package-manager.test.ts b/src/workspace-package-manager.test.ts index 406b606..70e3dd4 100644 --- a/src/workspace-package-manager.test.ts +++ b/src/workspace-package-manager.test.ts @@ -181,4 +181,32 @@ describe('workspace package manager helpers', () => { ); expect(hasInstalledNodeModules(repoDir)).toBe(false); }); + + it('backfills install state for a legacy runnable node_modules tree', () => { + const repoDir = path.join(tempRoot, 'legacy'); + fs.mkdirSync(path.join(repoDir, 'node_modules', '.bin'), { + recursive: true, + }); + fs.writeFileSync( + path.join(repoDir, 'package.json'), + JSON.stringify({ + name: 'legacy', + packageManager: 'bun@1.3.11', + scripts: { test: 'vitest run' }, + }), + ); + fs.writeFileSync(path.join(repoDir, 'bun.lock'), ''); + fs.writeFileSync(path.join(repoDir, 'node_modules', '.bin', 'vitest'), ''); + + expect(hasInstalledNodeModules(repoDir)).toBe(false); + + const result = ensureWorkspaceDependenciesInstalled(repoDir); + + expect(result).toMatchObject({ + installed: false, + packageManager: 'bun', + }); + expect(hasInstalledNodeModules(repoDir)).toBe(true); + expect(execFileSyncMock).not.toHaveBeenCalled(); + }); }); diff --git a/src/workspace-package-manager.ts b/src/workspace-package-manager.ts index 208ae44..0a55ba7 100644 --- a/src/workspace-package-manager.ts +++ b/src/workspace-package-manager.ts @@ -194,6 +194,27 @@ function hasRunnableNodeModulesTree(repoDir: string): boolean { } } +function backfillInstallFingerprintIfPossible( + repoDir: string, + packageManager: WorkspacePackageManager, +): boolean { + if (!hasRunnableNodeModulesTree(repoDir)) { + return false; + } + + if (readInstallFingerprint(repoDir)) { + return false; + } + + const expectedFingerprint = computeInstallFingerprint(repoDir); + if (!expectedFingerprint) { + return false; + } + + writeInstallFingerprint(repoDir, packageManager, expectedFingerprint); + return true; +} + export function detectWorkspacePackageManager( repoDir: string, ): WorkspacePackageManager | null { @@ -326,6 +347,10 @@ export function ensureWorkspaceDependenciesInstalled( return { installed: false, packageManager }; } + if (backfillInstallFingerprintIfPossible(repoDir, packageManager)) { + return { installed: false, packageManager }; + } + const command = resolveWorkspaceInstallCommand(repoDir); if (!command) { return { installed: false, packageManager };