Backfill verification install state

This commit is contained in:
ejclaw
2026-04-08 01:35:34 +09:00
parent a90159e5d4
commit 92d499c779
4 changed files with 89 additions and 5 deletions

View File

@@ -10,6 +10,7 @@ import {
isVerificationProfile, isVerificationProfile,
runVerificationRequest, runVerificationRequest,
} from './verification.js'; } from './verification.js';
import { hasInstalledNodeModules } from './workspace-package-manager.js';
describe('verification helpers', () => { describe('verification helpers', () => {
it('recognizes only fixed verification profiles', () => { it('recognizes only fixed verification profiles', () => {
@@ -82,16 +83,18 @@ describe('verification helpers', () => {
expect(third).not.toBe(first); 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( 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, recursive: true,
}); });
fs.writeFileSync( fs.writeFileSync(
path.join(repoDir, 'package.json'), path.join(repoDir, 'package.json'),
JSON.stringify({ JSON.stringify({
name: 'verification-legacy',
packageManager: 'bun@1.3.11',
scripts: { scripts: {
test: 'vitest run', test: 'vitest run',
typecheck: 'tsc --noEmit', 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( const result = await runVerificationRequest(
{ {
requestId: 'req-noise', requestId: 'req-legacy-node-modules',
profile: 'typecheck', profile: 'typecheck',
expectedSnapshotId: 'fs:mismatch',
}, },
{ repoDir }, { repoDir },
); );
expect(hasInstalledNodeModules(repoDir)).toBe(true);
expect(result.ok).toBe(false); expect(result.ok).toBe(false);
expect(result.error).toContain('installed node_modules tree'); expect(result.error).toContain('Snapshot mismatch before verification');
}); });
}); });

View File

@@ -15,6 +15,7 @@ import { resolveGroupIpcPath } from './group-folder.js';
import { import {
buildWorkspaceScriptCommand, buildWorkspaceScriptCommand,
detectPnpmStorePath, detectPnpmStorePath,
ensureWorkspaceDependenciesInstalled,
hasInstalledNodeModules, hasInstalledNodeModules,
} from './workspace-package-manager.js'; } from './workspace-package-manager.js';
import { 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)) { if (!hasInstalledNodeModules(repoDir)) {
return { return {
ok: false, ok: false,

View File

@@ -181,4 +181,32 @@ describe('workspace package manager helpers', () => {
); );
expect(hasInstalledNodeModules(repoDir)).toBe(false); 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();
});
}); });

View File

@@ -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( export function detectWorkspacePackageManager(
repoDir: string, repoDir: string,
): WorkspacePackageManager | null { ): WorkspacePackageManager | null {
@@ -326,6 +347,10 @@ export function ensureWorkspaceDependenciesInstalled(
return { installed: false, packageManager }; return { installed: false, packageManager };
} }
if (backfillInstallFingerprintIfPossible(repoDir, packageManager)) {
return { installed: false, packageManager };
}
const command = resolveWorkspaceInstallCommand(repoDir); const command = resolveWorkspaceInstallCommand(repoDir);
if (!command) { if (!command) {
return { installed: false, packageManager }; return { installed: false, packageManager };