feat(hooks): wave-1 guards -- approval ask, plan gate, test lock, post-edit check
Wave-1 order 4 (P7, P12, the P9 test lock, the P3 plan-approval guard). All command hooks; none runs a model. - P12: pre-bash-executor answers permissionDecision "ask" before merge into main, push, tag/publish and destructive git (holds in auto mode). post-approval-log writes exactly what the person was asked to the K2 approval log: merge/push to main as `merge`, tag/publish as `release`, with a one-hour expiry; nothing when no person was asked. - P3: pre-plan-gate runs the K3 plan gate on a Voyage plan leaving plan mode and sends a refused plan back; "not measured" asks, never passes. - P9: lib/util/test-lock.mjs (K8). pre-write-executor refuses edits to locked test files; stop-test-lock refuses "done" for the locking session (stop_hook_active guarded). - P7: post-edit-check runs the target repo's own formatter/linter per edit, only during a Voyage plan run or after opt-in (.claude/voyage-guards.json); lib/verification/step-check.mjs (K9) fails the step on findings; no tool reads "not measured", never green. - STORM cap matcher also names `Agent` (the tool's name since 2.1.63). - Mod precondition stated in README, SECURITY.md and every PreToolUse stop guard: Voyage's guards are deterministic only when no installed mod handles `tool.check`. K2, K3 and K5 are stubbed in tests until orders 2 and 3 are merged. Two independent review rounds; every finding has a regression test. Suite 1600 (1598 pass / 0 fail / 2 skipped). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
2c89a6b580
commit
2ee8137e7c
31 changed files with 3278 additions and 107 deletions
71
tests/lib/guards-prose-pins.test.mjs
Normal file
71
tests/lib/guards-prose-pins.test.mjs
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
// tests/lib/guards-prose-pins.test.mjs
|
||||
// Prose pins owned by the guards process (wave-1 order 4). Counted as doc pins
|
||||
// by lib/util/test-census.mjs (PIN_FILE_RE matches `prose-pins.test.mjs`).
|
||||
//
|
||||
// The mod precondition: a Claude Code mod that handles `tool.check` answers
|
||||
// AFTER the rules and the PreToolUse hooks, and «its answer can replace
|
||||
// theirs» — it can approve a call a plugin PreToolUse hook blocked
|
||||
// (code.claude.com/docs/en/permissions § Extend permissions with hooks,
|
||||
// verified verbatim 06.10). So Voyage's stop guards are deterministic only
|
||||
// under a stated condition, and the condition is written where the guards are
|
||||
// described: README § Security hardening, SECURITY.md, and the head of every
|
||||
// PreToolUse stop guard.
|
||||
|
||||
import { test } from 'node:test';
|
||||
import { strict as assert } from 'node:assert';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { join, dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
||||
const read = (rel) => readFileSync(join(ROOT, rel), 'utf8');
|
||||
|
||||
export const MOD_PRECONDITION = "Voyage's guards are deterministic only when no installed mod handles `tool.check`.";
|
||||
|
||||
const STOP_GUARDS = [
|
||||
'hooks/scripts/pre-bash-executor.mjs',
|
||||
'hooks/scripts/pre-write-executor.mjs',
|
||||
'hooks/scripts/pre-agent-cap.mjs',
|
||||
'hooks/scripts/pre-plan-gate.mjs',
|
||||
];
|
||||
|
||||
test('mod precondition: README § Security hardening states it', () => {
|
||||
const md = read('README.md');
|
||||
const start = md.indexOf('### Security hardening');
|
||||
assert.ok(start !== -1);
|
||||
const end = md.indexOf('\n### ', start + 1);
|
||||
assert.ok(md.slice(start, end).includes(MOD_PRECONDITION), 'README § Security hardening must state the mod precondition');
|
||||
});
|
||||
|
||||
test('mod precondition: SECURITY.md states it', () => {
|
||||
assert.ok(read('SECURITY.md').includes(MOD_PRECONDITION));
|
||||
});
|
||||
|
||||
test('mod precondition: the head of every PreToolUse stop guard states it', () => {
|
||||
for (const rel of STOP_GUARDS) {
|
||||
const head = read(rel).split('\n').slice(0, 40).join('\n').replace(/^\/\/ ?/gm, '').replace(/\n/g, ' ');
|
||||
assert.ok(head.replace(/\s+/g, ' ').includes(MOD_PRECONDITION), `${rel}: the first 40 lines must state the mod precondition`);
|
||||
}
|
||||
});
|
||||
|
||||
test('mod precondition: the guards cannot see a mod; the docs say so where it is stated', () => {
|
||||
// Looked up 06.10: no hook input field names installed mods; the documented
|
||||
// ways to see them are `/plugin` and a CLI subcommand, both for a person.
|
||||
assert.match(read('SECURITY.md'), /cannot tell whether such a mod is installed/);
|
||||
});
|
||||
|
||||
test('P12 guard: README names the classes it asks about and that it holds in auto mode', () => {
|
||||
const md = read('README.md');
|
||||
const sec = md.slice(md.indexOf('### Security hardening'), md.indexOf('#### Recommended: disable Skill shell execution'));
|
||||
for (const w of ['merge into `main`', 'push', 'tag', 'destructive git', 'auto mode']) {
|
||||
assert.ok(sec.includes(w), `README § Security hardening must mention ${w}`);
|
||||
}
|
||||
});
|
||||
|
||||
test('borrowings are credited where the guards are described (PLAN fixed limit 3)', () => {
|
||||
const md = read('README.md');
|
||||
const sec = md.slice(md.indexOf('### Security hardening'), md.indexOf('#### Recommended: disable Skill shell execution'));
|
||||
for (const src of ['Superpowers', 'agent-skills', 'ai-native-sdlc', 'Pocock', 'Kiro', 'Cherny', 'Claude Code']) {
|
||||
assert.ok(sec.includes(src), `credit missing for ${src}`);
|
||||
}
|
||||
});
|
||||
96
tests/lib/repo-tools.test.mjs
Normal file
96
tests/lib/repo-tools.test.mjs
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
// tests/lib/repo-tools.test.mjs
|
||||
// lib/verification/repo-tools.mjs finds the target repo's OWN formatter,
|
||||
// linter and type checker. A tool counts only when the repo configures it AND
|
||||
// it is installed locally (node_modules/.bin) or on PATH; nothing is ever
|
||||
// downloaded. Exit codes are mapped per tool to clean / finding / error, and an
|
||||
// error is never read as clean.
|
||||
|
||||
import { test } from 'node:test';
|
||||
import { strict as assert } from 'node:assert';
|
||||
import { writeFileSync, mkdirSync, realpathSync } from 'node:fs';
|
||||
import { join, dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { makeRepo, fakeTool, tmp } from '../hooks/guard-helpers.mjs';
|
||||
|
||||
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||
const mod = await import(join(HERE, '..', '..', 'lib', 'verification', 'repo-tools.mjs'));
|
||||
const { resolveRepo, detectTools, toolsForFile, runTool } = mod;
|
||||
|
||||
test('resolveRepo: finds the root and git dir from a file path that does not exist yet', () => {
|
||||
const repo = makeRepo('main');
|
||||
const r = resolveRepo(join(repo, 'src', 'new', 'file.js'));
|
||||
assert.equal(r.root, realpathSync(repo));
|
||||
assert.equal(r.gitDir, realpathSync(join(repo, '.git')));
|
||||
});
|
||||
|
||||
test('resolveRepo: null outside a repository', () => {
|
||||
assert.equal(resolveRepo(tmp()), null);
|
||||
});
|
||||
|
||||
test('detectTools: prettier counts only with config AND a local binary', () => {
|
||||
const repo = makeRepo('main');
|
||||
writeFileSync(join(repo, '.prettierrc'), '{}');
|
||||
let tools = detectTools(repo, { env: { PATH: '/nonexistent' } });
|
||||
const p = tools.find((t) => t.name === 'prettier');
|
||||
assert.ok(p && !p.available, 'configured but not installed is listed as unavailable');
|
||||
fakeTool(join(repo, 'node_modules', '.bin'), 'prettier');
|
||||
tools = detectTools(repo, { env: { PATH: '/nonexistent' } });
|
||||
assert.ok(tools.find((t) => t.name === 'prettier').available);
|
||||
});
|
||||
|
||||
test('detectTools: a repo with nothing configured has no tools', () => {
|
||||
assert.deepEqual(detectTools(makeRepo('main'), { env: { PATH: '/nonexistent' } }), []);
|
||||
});
|
||||
|
||||
test('detectTools: eslint and tsc (repo tier) from config + local binaries', () => {
|
||||
const repo = makeRepo('main');
|
||||
writeFileSync(join(repo, 'eslint.config.js'), 'export default [];');
|
||||
writeFileSync(join(repo, 'tsconfig.json'), '{}');
|
||||
fakeTool(join(repo, 'node_modules', '.bin'), 'eslint');
|
||||
fakeTool(join(repo, 'node_modules', '.bin'), 'tsc');
|
||||
const tools = detectTools(repo, { env: { PATH: '/nonexistent' } });
|
||||
const eslint = tools.find((t) => t.name === 'eslint');
|
||||
const tsc = tools.find((t) => t.name === 'tsc');
|
||||
assert.equal(eslint.kind, 'lint');
|
||||
assert.equal(eslint.tier, 'file');
|
||||
assert.equal(tsc.kind, 'typecheck');
|
||||
assert.equal(tsc.tier, 'repo');
|
||||
});
|
||||
|
||||
test('detectTools: ruff from pyproject [tool.ruff] and a binary on PATH', () => {
|
||||
const repo = makeRepo('main');
|
||||
writeFileSync(join(repo, 'pyproject.toml'), '[tool.ruff]\nline-length = 100\n');
|
||||
const bin = join(tmp(), 'bin');
|
||||
fakeTool(bin, 'ruff');
|
||||
const tools = detectTools(repo, { env: { PATH: bin } });
|
||||
assert.ok(tools.some((t) => t.name === 'ruff-check' && t.available));
|
||||
assert.ok(tools.some((t) => t.name === 'ruff-format' && t.available));
|
||||
});
|
||||
|
||||
test('toolsForFile: by extension, file tier only', () => {
|
||||
const repo = makeRepo('main');
|
||||
writeFileSync(join(repo, '.prettierrc'), '{}');
|
||||
writeFileSync(join(repo, 'tsconfig.json'), '{}');
|
||||
fakeTool(join(repo, 'node_modules', '.bin'), 'prettier');
|
||||
fakeTool(join(repo, 'node_modules', '.bin'), 'tsc');
|
||||
const tools = detectTools(repo, { env: { PATH: '/nonexistent' } });
|
||||
assert.deepEqual(toolsForFile(tools, 'src/a.ts').map((t) => t.name), ['prettier']);
|
||||
assert.deepEqual(toolsForFile(tools, 'image.png').map((t) => t.name), []);
|
||||
});
|
||||
|
||||
test('runTool: clean, finding and error are three different answers', () => {
|
||||
const repo = makeRepo('main');
|
||||
writeFileSync(join(repo, '.prettierrc'), '{}');
|
||||
fakeTool(join(repo, 'node_modules', '.bin'), 'prettier');
|
||||
mkdirSync(join(repo, 'src'));
|
||||
writeFileSync(join(repo, 'src', 'good.js'), 'ok');
|
||||
writeFileSync(join(repo, 'src', 'bad.js'), 'BAD');
|
||||
const tool = detectTools(repo, { env: { PATH: '/nonexistent' } }).find((t) => t.name === 'prettier');
|
||||
assert.equal(runTool(tool, ['src/good.js'], { cwd: repo }).status, 'clean');
|
||||
const f = runTool(tool, ['src/bad.js'], { cwd: repo });
|
||||
assert.equal(f.status, 'finding');
|
||||
assert.match(f.output, /problem/);
|
||||
// exit 7 is neither clean nor a finding for prettier
|
||||
fakeTool(join(repo, 'node_modules', '.bin'), 'prettier', { findingExit: 7 });
|
||||
assert.equal(runTool(tool, ['src/bad.js'], { cwd: repo }).status, 'error');
|
||||
});
|
||||
106
tests/lib/step-check.test.mjs
Normal file
106
tests/lib/step-check.test.mjs
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
// tests/lib/step-check.test.mjs
|
||||
// K9 — the step check (lib/verification/step-check.mjs --cwd <dir>). At step
|
||||
// end it runs the repo's own tools over the changed files (and the repo-wide
|
||||
// ones, such as the type checker), reads the post-edit guard's log, and
|
||||
// answers 0 = green, 1 = findings, 2 = "not measured" (never green). JSON on
|
||||
// stdout. The architecture-rule runner (K5, wave-1 order 3) is optional: when
|
||||
// it is missing, it is listed as not measured.
|
||||
|
||||
import { test } from 'node:test';
|
||||
import { strict as assert } from 'node:assert';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { writeFileSync, mkdirSync, appendFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { ROOT, makeRepo, gitIn, fakeTool, tmp, pluginCopy } from '../hooks/guard-helpers.mjs';
|
||||
|
||||
const CLI = join(ROOT, 'lib', 'verification', 'step-check.mjs');
|
||||
|
||||
function run(cli, args, env = {}) {
|
||||
const r = spawnSync(process.execPath, [cli, ...args], { encoding: 'utf8', env: { ...process.env, PATH: '/usr/bin:/bin', ...env } });
|
||||
let json = null;
|
||||
try { json = JSON.parse(r.stdout); } catch { json = null; }
|
||||
return { code: r.status, stdout: r.stdout, stderr: r.stderr, json };
|
||||
}
|
||||
|
||||
function toolRepo() {
|
||||
const repo = makeRepo('main');
|
||||
writeFileSync(join(repo, '.prettierrc'), '{}');
|
||||
writeFileSync(join(repo, '.gitignore'), 'node_modules/\n');
|
||||
gitIn(repo, 'add', '.prettierrc', '.gitignore');
|
||||
gitIn(repo, 'commit', '-q', '-m', 'tools');
|
||||
fakeTool(join(repo, 'node_modules', '.bin'), 'prettier');
|
||||
return repo;
|
||||
}
|
||||
|
||||
test('K9 green (0): the changed files pass the repo\'s own tools', () => {
|
||||
const repo = toolRepo();
|
||||
writeFileSync(join(repo, 'a.js'), 'ok\n');
|
||||
const r = run(CLI, ['--cwd', repo]);
|
||||
assert.equal(r.code, 0, r.stdout + r.stderr);
|
||||
assert.equal(r.json.status, 'green');
|
||||
assert.ok(r.json.checks.some((c) => c.name === 'prettier' && c.status === 'clean'));
|
||||
});
|
||||
|
||||
test('K9 findings (1): a changed file the formatter rejects', () => {
|
||||
const repo = toolRepo();
|
||||
writeFileSync(join(repo, 'a.js'), 'BAD\n');
|
||||
const r = run(CLI, ['--cwd', repo]);
|
||||
assert.equal(r.code, 1, r.stdout);
|
||||
assert.equal(r.json?.status, 'findings');
|
||||
});
|
||||
|
||||
test('K9 findings (1): the post-edit log holds a finding for a changed file no tool can re-check', () => {
|
||||
const repo = makeRepo('main');
|
||||
writeFileSync(join(repo, 'a.js'), 'x\n');
|
||||
mkdirSync(join(repo, '.git', 'voyage'), { recursive: true });
|
||||
appendFileSync(join(repo, '.git', 'voyage', 'post-edit.jsonl'),
|
||||
JSON.stringify({ ts: new Date().toISOString(), file: 'a.js', status: 'finding', results: [{ name: 'prettier', status: 'finding' }] }) + '\n');
|
||||
const r = run(CLI, ['--cwd', repo]);
|
||||
assert.equal(r.code, 1, r.stdout);
|
||||
assert.equal(r.json?.status, 'findings', 'exit 1 must be a finding, not a crash');
|
||||
});
|
||||
|
||||
test('K9 not measured (2): a repo with no tools at all -> never green', () => {
|
||||
const repo = makeRepo('main');
|
||||
writeFileSync(join(repo, 'a.js'), 'BAD\n');
|
||||
const r = run(CLI, ['--cwd', repo]);
|
||||
assert.equal(r.code, 2, r.stdout);
|
||||
assert.equal(r.json.status, 'not_measured');
|
||||
});
|
||||
|
||||
test('K9 not measured (2): not a git repository', () => {
|
||||
const r = run(CLI, ['--cwd', tmp()]);
|
||||
assert.equal(r.code, 2);
|
||||
});
|
||||
|
||||
test('K9 usage error (2): no --cwd', () => {
|
||||
assert.equal(run(CLI, []).code, 2);
|
||||
});
|
||||
|
||||
test('K9 --full: checks tracked files, not only the diff', () => {
|
||||
const repo = toolRepo();
|
||||
writeFileSync(join(repo, 'old.js'), 'BAD\n');
|
||||
gitIn(repo, 'add', 'old.js');
|
||||
gitIn(repo, 'commit', '-q', '-m', 'old');
|
||||
assert.notEqual(run(CLI, ['--cwd', repo]).json.status, 'findings', 'old.js is not in the diff');
|
||||
assert.equal(run(CLI, ['--cwd', repo, '--full']).code, 1);
|
||||
});
|
||||
|
||||
test('K5 optional: arch-rules missing is listed as not measured and does not turn green into red', () => {
|
||||
const plugin = pluginCopy({ 'lib/verification/arch-rules.mjs': 'missing' });
|
||||
const repo = toolRepo();
|
||||
writeFileSync(join(repo, 'a.js'), 'ok\n');
|
||||
const r = run(join(plugin, 'lib', 'verification', 'step-check.mjs'), ['--cwd', repo]);
|
||||
assert.equal(r.code, 0, r.stdout);
|
||||
const arch = r.json.checks.find((c) => c.name === 'arch-rules');
|
||||
assert.equal(arch.status, 'not_measured');
|
||||
});
|
||||
|
||||
test('K5 optional: arch-rules stub reporting a breach (exit 1) makes the step red', () => {
|
||||
const plugin = pluginCopy({ 'lib/verification/arch-rules.mjs': 'stub' });
|
||||
const repo = toolRepo();
|
||||
writeFileSync(join(repo, 'a.js'), 'ok\n');
|
||||
const r = run(join(plugin, 'lib', 'verification', 'step-check.mjs'), ['--cwd', repo], { STUB_EXIT: '1' });
|
||||
assert.equal(r.code, 1, r.stdout);
|
||||
assert.equal(r.json?.checks.find((c) => c.name === 'arch-rules')?.status, 'finding');
|
||||
});
|
||||
104
tests/lib/test-lock.test.mjs
Normal file
104
tests/lib/test-lock.test.mjs
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
// tests/lib/test-lock.test.mjs
|
||||
// K8 — the test lock CLI (lib/util/test-lock.mjs). The executor (wave-1 order
|
||||
// 1) locks a step's test files once the test is written and seen failing, and
|
||||
// unlocks them when the test passes. The lock lives inside the repository's
|
||||
// git directory, so it can never be committed.
|
||||
|
||||
import { test } from 'node:test';
|
||||
import { strict as assert } from 'node:assert';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
||||
import { join, dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { makeRepo, gitIn, tmp } from '../hooks/guard-helpers.mjs';
|
||||
|
||||
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||
const CLI = join(HERE, '..', '..', 'lib', 'util', 'test-lock.mjs');
|
||||
|
||||
function run(cwd, args, env = {}) {
|
||||
const r = spawnSync(process.execPath, [CLI, ...args], { cwd, encoding: 'utf8', env: { ...process.env, ...env } });
|
||||
return { code: r.status, stdout: r.stdout, stderr: r.stderr };
|
||||
}
|
||||
|
||||
function repoWithTests() {
|
||||
const repo = makeRepo('main');
|
||||
mkdirSync(join(repo, 'tests'), { recursive: true });
|
||||
writeFileSync(join(repo, 'tests', 'a.test.js'), 'a\n');
|
||||
writeFileSync(join(repo, 'tests', 'b.test.js'), 'b\n');
|
||||
return repo;
|
||||
}
|
||||
|
||||
test('K8 lock: exit 0, status names the step and the files', () => {
|
||||
const repo = repoWithTests();
|
||||
const r = run(repo, ['lock', '--files', 'tests/a.test.js,tests/b.test.js', '--step', '3']);
|
||||
assert.equal(r.code, 0, r.stderr);
|
||||
const s = run(repo, ['status']);
|
||||
assert.equal(s.code, 0);
|
||||
const st = JSON.parse(s.stdout);
|
||||
assert.equal(st.locked, true);
|
||||
assert.equal(st.step, 3);
|
||||
assert.equal(st.files.length, 2);
|
||||
assert.ok(st.files.every((f) => f.endsWith('.test.js')));
|
||||
});
|
||||
|
||||
test('K8 lock: stored outside what can be committed (git status stays clean)', () => {
|
||||
const repo = repoWithTests();
|
||||
gitIn(repo, 'add', '-A');
|
||||
gitIn(repo, 'commit', '-q', '-m', 'tests');
|
||||
assert.equal(run(repo, ['lock', '--files', 'tests/a.test.js', '--step', '1']).code, 0);
|
||||
assert.equal(gitIn(repo, 'status', '--porcelain'), '', 'the lock must not appear as a change in the repo');
|
||||
const st = JSON.parse(run(repo, ['status']).stdout);
|
||||
assert.ok(st.lock_path.includes(`${'.git'}`), `lock path should be inside the git dir: ${st.lock_path}`);
|
||||
});
|
||||
|
||||
test('K8 lock records the Claude Code session it was taken in', () => {
|
||||
const repo = repoWithTests();
|
||||
run(repo, ['lock', '--files', 'tests/a.test.js', '--step', '2'], { CLAUDE_CODE_SESSION_ID: 'sess-123' });
|
||||
assert.equal(JSON.parse(run(repo, ['status']).stdout).session, 'sess-123');
|
||||
});
|
||||
|
||||
test('K8 unlock: exit 0, status unlocked', () => {
|
||||
const repo = repoWithTests();
|
||||
run(repo, ['lock', '--files', 'tests/a.test.js', '--step', '1']);
|
||||
assert.equal(run(repo, ['unlock']).code, 0);
|
||||
assert.equal(JSON.parse(run(repo, ['status']).stdout).locked, false);
|
||||
});
|
||||
|
||||
test('K8 lock refuses (exit 1) while another step holds the lock', () => {
|
||||
const repo = repoWithTests();
|
||||
run(repo, ['lock', '--files', 'tests/a.test.js', '--step', '1']);
|
||||
const r = run(repo, ['lock', '--files', 'tests/b.test.js', '--step', '2']);
|
||||
assert.equal(r.code, 1);
|
||||
assert.match(r.stderr, /step 1/);
|
||||
});
|
||||
|
||||
test('K8: --cwd points at another directory', () => {
|
||||
const repo = repoWithTests();
|
||||
assert.equal(run(tmp(), ['lock', '--cwd', repo, '--files', 'tests/a.test.js', '--step', '4']).code, 0);
|
||||
assert.equal(JSON.parse(run(tmp(), ['status', '--cwd', repo]).stdout).step, 4);
|
||||
});
|
||||
|
||||
test('K8 could not run (exit 2): not a git repository', () => {
|
||||
const r = run(tmp(), ['lock', '--files', 'x.test.js', '--step', '1']);
|
||||
assert.equal(r.code, 2);
|
||||
});
|
||||
|
||||
test('K8 could not run (exit 2): bad usage', () => {
|
||||
const repo = repoWithTests();
|
||||
assert.equal(run(repo, ['lock', '--step', '1']).code, 2, 'no --files');
|
||||
assert.equal(run(repo, ['lock', '--files', 'tests/a.test.js']).code, 2, 'no --step');
|
||||
assert.equal(run(repo, ['lock', '--files', 'tests/a.test.js', '--step', 'x']).code, 2, 'step not a number');
|
||||
assert.equal(run(repo, ['frobnicate']).code, 2);
|
||||
});
|
||||
|
||||
test('K8 status on a corrupt lock: exit 2, never "unlocked"', () => {
|
||||
const repo = repoWithTests();
|
||||
run(repo, ['lock', '--files', 'tests/a.test.js', '--step', '1']);
|
||||
const path = JSON.parse(run(repo, ['status']).stdout).lock_path;
|
||||
writeFileSync(path, '{not json');
|
||||
const s = run(repo, ['status']);
|
||||
assert.equal(s.code, 2);
|
||||
assert.ok(existsSync(path));
|
||||
assert.ok(!/"locked":\s*false/.test(s.stdout));
|
||||
assert.ok(readFileSync(path, 'utf8').startsWith('{not'), 'status never rewrites the lock');
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue