209 lines
6.8 KiB
JavaScript
209 lines
6.8 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import { access, mkdir, readFile, readdir, rm, writeFile } from 'node:fs/promises';
|
|
import path from 'node:path';
|
|
import test from 'node:test';
|
|
|
|
import { installHooks } from './install-hooks.mjs';
|
|
|
|
const fixtureRoot = path.join(process.cwd(), '.mosaic-test-work', `hooks-${process.pid}`);
|
|
const quarantineRoot = path.join(fixtureRoot, 'quarantine');
|
|
|
|
async function fixture(name) {
|
|
const root = path.join(fixtureRoot, name);
|
|
await mkdir(path.join(root, '.husky'), { recursive: true });
|
|
return root;
|
|
}
|
|
|
|
async function exists(target) {
|
|
try {
|
|
await access(target);
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
test.after(async () => {
|
|
await rm(fixtureRoot, { recursive: true, force: true });
|
|
});
|
|
|
|
test('an interrupted install quarantines the partial active hook set and fails loudly', async () => {
|
|
const root = await fixture('interrupted');
|
|
let restoredHooksPath = 'not-called';
|
|
|
|
await assert.rejects(
|
|
installHooks({
|
|
root,
|
|
quarantineRoot,
|
|
runHusky: async (stagingHooks) => {
|
|
await mkdir(path.join(stagingHooks, '_'), { recursive: true });
|
|
await writeFile(path.join(stagingHooks, '_', 'h'), 'partial');
|
|
throw new Error('simulated interruption');
|
|
},
|
|
activateHooks: async () => {},
|
|
readHooksPath: async () => null,
|
|
restoreHooksPath: async (value) => {
|
|
restoredHooksPath = value;
|
|
},
|
|
}),
|
|
(error) => {
|
|
assert.match(error.message, /Hook installation failed/);
|
|
assert.match(error.message, /pnpm install --frozen-lockfile/);
|
|
return true;
|
|
},
|
|
);
|
|
|
|
assert.equal(await exists(path.join(root, '.husky', '_')), false);
|
|
assert.equal(restoredHooksPath, 'not-called');
|
|
const quarantined = await readdir(quarantineRoot);
|
|
assert.equal(quarantined.length, 1);
|
|
});
|
|
|
|
test('a failed replacement restores a previously complete active hook set', async () => {
|
|
const root = await fixture('rollback');
|
|
const activeShim = path.join(root, '.husky', '_', 'h');
|
|
await mkdir(path.dirname(activeShim), { recursive: true });
|
|
await writeFile(activeShim, 'previous-complete');
|
|
let previousRemainedActiveDuringStaging = false;
|
|
|
|
await assert.rejects(
|
|
installHooks({
|
|
root,
|
|
quarantineRoot: path.join(fixtureRoot, 'rollback-quarantine'),
|
|
runHusky: async () => {
|
|
previousRemainedActiveDuringStaging =
|
|
(await readFile(activeShim, 'utf8')) === 'previous-complete';
|
|
throw new Error('simulated replacement failure');
|
|
},
|
|
activateHooks: async () => {},
|
|
readHooksPath: async () => '.husky/_',
|
|
restoreHooksPath: async () => {},
|
|
}),
|
|
/Hook installation failed/,
|
|
);
|
|
|
|
assert.equal(previousRemainedActiveDuringStaging, true);
|
|
assert.equal(await readFile(activeShim, 'utf8'), 'previous-complete');
|
|
});
|
|
|
|
test('a mismatched complete hook set fails loudly instead of reporting a stale install as current', async () => {
|
|
const root = await fixture('mismatch');
|
|
const activeShim = path.join(root, '.husky', '_', 'h');
|
|
await mkdir(path.dirname(activeShim), { recursive: true });
|
|
await writeFile(activeShim, 'old-complete');
|
|
|
|
await assert.rejects(
|
|
installHooks({
|
|
root,
|
|
quarantineRoot: path.join(fixtureRoot, 'mismatch-quarantine'),
|
|
runHusky: async (stagingHooks) => {
|
|
await mkdir(path.join(stagingHooks, '_'), { recursive: true });
|
|
await writeFile(path.join(stagingHooks, '_', 'h'), 'new-complete');
|
|
},
|
|
activateHooks: async () => {},
|
|
readHooksPath: async () => '.husky/_',
|
|
restoreHooksPath: async () => {},
|
|
}),
|
|
/Hook installation failed.*pnpm install --frozen-lockfile/,
|
|
);
|
|
|
|
assert.equal(await readFile(activeShim, 'utf8'), 'old-complete');
|
|
});
|
|
|
|
test('a competing successful installer is not removed by the losing process', async () => {
|
|
const root = await fixture('concurrent');
|
|
const activeShim = path.join(root, '.husky', '_', 'h');
|
|
let restored = false;
|
|
|
|
await assert.rejects(
|
|
installHooks({
|
|
root,
|
|
quarantineRoot: path.join(fixtureRoot, 'concurrent-quarantine'),
|
|
runHusky: async (stagingHooks) => {
|
|
await mkdir(path.join(stagingHooks, '_'), { recursive: true });
|
|
await writeFile(path.join(stagingHooks, '_', 'h'), 'ours');
|
|
await mkdir(path.dirname(activeShim), { recursive: true });
|
|
await writeFile(activeShim, 'peer');
|
|
},
|
|
activateHooks: async () => {},
|
|
readHooksPath: async () => null,
|
|
restoreHooksPath: async () => {
|
|
restored = true;
|
|
},
|
|
}),
|
|
/Hook installation failed/,
|
|
);
|
|
|
|
assert.equal(await readFile(activeShim, 'utf8'), 'peer');
|
|
assert.equal(restored, false);
|
|
});
|
|
|
|
test("a competing installer that replaces this installer's active set is preserved", async () => {
|
|
const root = await fixture('concurrent-after-rename');
|
|
const active = path.join(root, '.husky', '_');
|
|
const activeShim = path.join(active, 'h');
|
|
let restored = false;
|
|
|
|
await assert.rejects(
|
|
installHooks({
|
|
root,
|
|
quarantineRoot: path.join(fixtureRoot, 'concurrent-after-rename-quarantine'),
|
|
runHusky: async (stagingHooks) => {
|
|
await mkdir(path.join(stagingHooks, '_'), { recursive: true });
|
|
await writeFile(path.join(stagingHooks, '_', 'h'), 'ours');
|
|
},
|
|
activateHooks: async () => {
|
|
await rm(active, { recursive: true, force: true });
|
|
await mkdir(active, { recursive: true });
|
|
await writeFile(activeShim, 'peer');
|
|
throw new Error('our activation lost to peer');
|
|
},
|
|
readHooksPath: async () => null,
|
|
restoreHooksPath: async () => {
|
|
restored = true;
|
|
},
|
|
}),
|
|
/Hook installation failed/,
|
|
);
|
|
|
|
assert.equal(await readFile(activeShim, 'utf8'), 'peer');
|
|
assert.equal(restored, false);
|
|
});
|
|
|
|
test('an explicit interactive HUSKY=0 opt-out preserves existing hooks without running installer', async () => {
|
|
const root = await fixture('disabled');
|
|
const activeShim = path.join(root, '.husky', '_', 'h');
|
|
await mkdir(path.dirname(activeShim), { recursive: true });
|
|
await writeFile(activeShim, 'preserved');
|
|
let ran = false;
|
|
|
|
await installHooks({
|
|
root,
|
|
disabled: true,
|
|
runHusky: async () => {
|
|
ran = true;
|
|
},
|
|
});
|
|
|
|
assert.equal(ran, false);
|
|
assert.equal(await readFile(activeShim, 'utf8'), 'preserved');
|
|
});
|
|
|
|
test('a successful install leaves a complete active hook set', async () => {
|
|
const root = await fixture('success');
|
|
|
|
await installHooks({
|
|
root,
|
|
quarantineRoot,
|
|
runHusky: async (stagingHooks) => {
|
|
await mkdir(path.join(stagingHooks, '_'), { recursive: true });
|
|
await writeFile(path.join(stagingHooks, '_', 'h'), 'complete');
|
|
},
|
|
activateHooks: async () => {},
|
|
readHooksPath: async () => null,
|
|
restoreHooksPath: async () => {},
|
|
});
|
|
|
|
assert.equal(await exists(path.join(root, '.husky', '_', 'h')), true);
|
|
});
|