Automate worktree dependency installs
This commit is contained in:
178
src/workspace-package-manager.test.ts
Normal file
178
src/workspace-package-manager.test.ts
Normal file
@@ -0,0 +1,178 @@
|
||||
import fs from 'fs';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
const { execFileSyncMock } = vi.hoisted(() => ({
|
||||
execFileSyncMock: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('child_process', () => ({
|
||||
execFileSync: execFileSyncMock,
|
||||
}));
|
||||
|
||||
import {
|
||||
buildWorkspaceScriptCommand,
|
||||
detectWorkspacePackageManager,
|
||||
ensureWorkspaceDependenciesInstalled,
|
||||
hasInstalledNodeModules,
|
||||
resolveWorkspaceInstallCommand,
|
||||
} from './workspace-package-manager.js';
|
||||
|
||||
describe('workspace package manager helpers', () => {
|
||||
let tempRoot: string;
|
||||
|
||||
beforeEach(() => {
|
||||
tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ejclaw-pkgmgr-'));
|
||||
execFileSyncMock.mockReset();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(tempRoot, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('detects package managers from packageManager field and lockfiles', () => {
|
||||
const pnpmRepo = path.join(tempRoot, 'pnpm');
|
||||
fs.mkdirSync(pnpmRepo, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(pnpmRepo, 'package.json'),
|
||||
JSON.stringify({ packageManager: 'pnpm@10.11.0' }),
|
||||
);
|
||||
|
||||
const bunRepo = path.join(tempRoot, 'bun');
|
||||
fs.mkdirSync(bunRepo, { recursive: true });
|
||||
fs.writeFileSync(path.join(bunRepo, 'package.json'), JSON.stringify({}));
|
||||
fs.writeFileSync(path.join(bunRepo, 'bun.lock'), '');
|
||||
|
||||
const yarnRepo = path.join(tempRoot, 'yarn');
|
||||
fs.mkdirSync(yarnRepo, { recursive: true });
|
||||
fs.writeFileSync(path.join(yarnRepo, 'package.json'), JSON.stringify({}));
|
||||
fs.writeFileSync(path.join(yarnRepo, 'yarn.lock'), '');
|
||||
|
||||
const mixedRepo = path.join(tempRoot, 'mixed');
|
||||
fs.mkdirSync(mixedRepo, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(mixedRepo, 'package.json'),
|
||||
JSON.stringify({ packageManager: 'bun@1.3.11' }),
|
||||
);
|
||||
fs.writeFileSync(path.join(mixedRepo, 'bun.lock'), '');
|
||||
fs.writeFileSync(path.join(mixedRepo, 'pnpm-lock.yaml'), 'lockfileVersion: 9.0\n');
|
||||
|
||||
expect(detectWorkspacePackageManager(pnpmRepo)).toBe('pnpm');
|
||||
expect(detectWorkspacePackageManager(bunRepo)).toBe('bun');
|
||||
expect(detectWorkspacePackageManager(yarnRepo)).toBe('yarn');
|
||||
expect(detectWorkspacePackageManager(mixedRepo)).toBe('bun');
|
||||
});
|
||||
|
||||
it('fails fast when multiple lockfiles exist without a packageManager field', () => {
|
||||
const repoDir = path.join(tempRoot, 'ambiguous');
|
||||
fs.mkdirSync(repoDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(repoDir, 'package.json'), JSON.stringify({}));
|
||||
fs.writeFileSync(path.join(repoDir, 'bun.lock'), '');
|
||||
fs.writeFileSync(path.join(repoDir, 'package-lock.json'), '{}\n');
|
||||
|
||||
expect(() => detectWorkspacePackageManager(repoDir)).toThrow(
|
||||
/Ambiguous package manager/i,
|
||||
);
|
||||
});
|
||||
|
||||
it('builds script and install commands for non-npm workspaces', () => {
|
||||
const pnpmRepo = path.join(tempRoot, 'pnpm');
|
||||
fs.mkdirSync(pnpmRepo, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(pnpmRepo, 'package.json'),
|
||||
JSON.stringify({ packageManager: 'pnpm@10.11.0' }),
|
||||
);
|
||||
|
||||
const bunRepo = path.join(tempRoot, 'bun');
|
||||
fs.mkdirSync(bunRepo, { recursive: true });
|
||||
fs.writeFileSync(path.join(bunRepo, 'package.json'), JSON.stringify({}));
|
||||
fs.writeFileSync(path.join(bunRepo, 'bun.lock'), '');
|
||||
|
||||
expect(buildWorkspaceScriptCommand(pnpmRepo, 'typecheck')).toMatchObject({
|
||||
file: 'corepack',
|
||||
args: ['pnpm', 'run', 'typecheck'],
|
||||
commandText: 'corepack pnpm run typecheck',
|
||||
});
|
||||
expect(resolveWorkspaceInstallCommand(pnpmRepo)).toMatchObject({
|
||||
file: 'corepack',
|
||||
args: ['pnpm', 'install', '--frozen-lockfile'],
|
||||
commandText: 'corepack pnpm install --frozen-lockfile',
|
||||
});
|
||||
expect(buildWorkspaceScriptCommand(bunRepo, 'build')).toMatchObject({
|
||||
file: 'bun',
|
||||
args: ['run', 'build'],
|
||||
commandText: 'bun run build',
|
||||
});
|
||||
});
|
||||
|
||||
it('installs dependencies once and tracks install fingerprint', () => {
|
||||
const repoDir = path.join(tempRoot, 'repo');
|
||||
fs.mkdirSync(repoDir, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(repoDir, 'package.json'),
|
||||
JSON.stringify({
|
||||
name: 'repo',
|
||||
packageManager: 'pnpm@10.11.0',
|
||||
scripts: { typecheck: 'tsc --noEmit' },
|
||||
}),
|
||||
);
|
||||
fs.writeFileSync(path.join(repoDir, 'pnpm-lock.yaml'), 'lockfileVersion: 9.0\n');
|
||||
|
||||
execFileSyncMock.mockImplementation((_file, _args, options) => {
|
||||
const cwd = (options as { cwd: string }).cwd;
|
||||
fs.mkdirSync(path.join(cwd, 'node_modules', '.bin'), { recursive: true });
|
||||
fs.writeFileSync(path.join(cwd, 'node_modules', '.bin', 'tsc'), '');
|
||||
return '';
|
||||
});
|
||||
|
||||
const first = ensureWorkspaceDependenciesInstalled(repoDir);
|
||||
expect(first).toMatchObject({
|
||||
installed: true,
|
||||
packageManager: 'pnpm',
|
||||
commandText: 'corepack pnpm install --frozen-lockfile',
|
||||
});
|
||||
expect(hasInstalledNodeModules(repoDir)).toBe(true);
|
||||
|
||||
execFileSyncMock.mockClear();
|
||||
const second = ensureWorkspaceDependenciesInstalled(repoDir);
|
||||
expect(second).toMatchObject({
|
||||
installed: false,
|
||||
packageManager: 'pnpm',
|
||||
});
|
||||
expect(execFileSyncMock).not.toHaveBeenCalled();
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(repoDir, 'package.json'),
|
||||
JSON.stringify({
|
||||
name: 'repo',
|
||||
packageManager: 'pnpm@10.11.0',
|
||||
scripts: { typecheck: 'tsc --noEmit', test: 'vitest run' },
|
||||
}),
|
||||
);
|
||||
|
||||
expect(hasInstalledNodeModules(repoDir)).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects no-op installs that only leave the sentinel file behind', () => {
|
||||
const repoDir = path.join(tempRoot, 'noop');
|
||||
fs.mkdirSync(repoDir, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(repoDir, 'package.json'),
|
||||
JSON.stringify({
|
||||
name: 'noop',
|
||||
packageManager: 'npm@10.0.0',
|
||||
scripts: { typecheck: 'tsc --noEmit' },
|
||||
}),
|
||||
);
|
||||
fs.writeFileSync(path.join(repoDir, 'package-lock.json'), '{}\n');
|
||||
|
||||
execFileSyncMock.mockImplementation(() => '');
|
||||
|
||||
expect(() => ensureWorkspaceDependenciesInstalled(repoDir)).toThrow(
|
||||
/did not produce a usable node_modules tree/i,
|
||||
);
|
||||
expect(hasInstalledNodeModules(repoDir)).toBe(false);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user