mirror of https://github.com/garrytan/gstack.git
fix: stage legacy team hook deletion
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
parent
5f1e896f90
commit
7233eb0d60
|
|
@ -220,6 +220,9 @@ HOOK_EOF
|
||||||
# registered successfully. Other project hooks are left untouched.
|
# registered successfully. Other project hooks are left untouched.
|
||||||
if [ -e "$HOOKS_DIR/check-gstack.sh" ] || [ -L "$HOOKS_DIR/check-gstack.sh" ]; then
|
if [ -e "$HOOKS_DIR/check-gstack.sh" ] || [ -L "$HOOKS_DIR/check-gstack.sh" ]; then
|
||||||
rm "$HOOKS_DIR/check-gstack.sh"
|
rm "$HOOKS_DIR/check-gstack.sh"
|
||||||
|
if (cd "$REPO_ROOT" && git ls-files --error-unmatch -- .claude/hooks/check-gstack.sh >/dev/null 2>&1); then
|
||||||
|
GENERATED+=(".claude/hooks/check-gstack.sh")
|
||||||
|
fi
|
||||||
echo " - .claude/hooks/check-gstack.sh — removed legacy enforcement hook"
|
echo " - .claude/hooks/check-gstack.sh — removed legacy enforcement hook"
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@
|
||||||
<li>The generated hook resolves the user home in <code>HOME</code>, <code>USERPROFILE</code>, then <code>os.homedir()</code> order so POSIX, Git Bash, PowerShell, and cmd runners use the expected global install root.</li>
|
<li>The generated hook resolves the user home in <code>HOME</code>, <code>USERPROFILE</code>, then <code>os.homedir()</code> order so POSIX, Git Bash, PowerShell, and cmd runners use the expected global install root.</li>
|
||||||
<li>The hook contract follows <code>docs/spikes/claude-code-hook-mutation.md</code>: pass-through writes <code>{}</code>; denial writes a <code>hookSpecificOutput</code> object for <code>PreToolUse</code> with <code>permissionDecision: "deny"</code>.</li>
|
<li>The hook contract follows <code>docs/spikes/claude-code-hook-mutation.md</code>: pass-through writes <code>{}</code>; denial writes a <code>hookSpecificOutput</code> object for <code>PreToolUse</code> with <code>permissionDecision: "deny"</code>.</li>
|
||||||
<li>Missing or unreadable gstack is an intentional denial with exit code zero. Install instructions are present in stderr and <code>permissionDecisionReason</code>, so the runner does not report a hook execution error.</li>
|
<li>Missing or unreadable gstack is an intentional denial with exit code zero. Install instructions are present in stderr and <code>permissionDecisionReason</code>, so the runner does not report a hook execution error.</li>
|
||||||
<li>Rerunning required mode replaces a matching legacy <code>check-gstack.sh</code> registration, removes duplicate matching registrations, and deletes the obsolete Bash hook only after the CommonJS hook is generated and registered successfully. Unrelated project hooks remain unchanged.</li>
|
<li>Rerunning required mode replaces a matching legacy <code>check-gstack.sh</code> registration, removes duplicate matching registrations, and deletes the obsolete Bash hook only after the CommonJS hook is generated and registered successfully. If Git tracks the deleted hook, the printed <code>git add</code> command includes its path so the suggested migration commit stages the deletion; untracked removed hooks are omitted because they have no index change. Unrelated project hooks remain unchanged.</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<h2>Deviations</h2>
|
<h2>Deviations</h2>
|
||||||
|
|
|
||||||
|
|
@ -302,7 +302,7 @@ describe('gstack-team-init', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('required: creates enforcement hook', () => {
|
test('required: creates enforcement hook', () => {
|
||||||
run(`${TEAM_INIT} required`, { cwd: tmpDir });
|
const result = run(`${TEAM_INIT} required`, { cwd: tmpDir });
|
||||||
const hookPath = path.join(tmpDir, '.claude', 'hooks', 'check-gstack.cjs');
|
const hookPath = path.join(tmpDir, '.claude', 'hooks', 'check-gstack.cjs');
|
||||||
expect(fs.existsSync(hookPath)).toBe(true);
|
expect(fs.existsSync(hookPath)).toBe(true);
|
||||||
expect(
|
expect(
|
||||||
|
|
@ -319,6 +319,7 @@ describe('gstack-team-init', () => {
|
||||||
expect(hook).toContain("permissionDecision: 'deny'");
|
expect(hook).toContain("permissionDecision: 'deny'");
|
||||||
expect(hook).toContain('BLOCKED: gstack is not installed');
|
expect(hook).toContain('BLOCKED: gstack is not installed');
|
||||||
expect(hook).not.toContain('#!/bin/bash');
|
expect(hook).not.toContain('#!/bin/bash');
|
||||||
|
expect(result.stdout).not.toMatch(/git add .*check-gstack\.sh/);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('required: registers one shell-neutral project hook', () => {
|
test('required: registers one shell-neutral project hook', () => {
|
||||||
|
|
@ -534,22 +535,45 @@ describe('gstack-team-init', () => {
|
||||||
execSync('git add .claude', { cwd: tmpDir });
|
execSync('git add .claude', { cwd: tmpDir });
|
||||||
execSync('git commit -m "add legacy hook"', { cwd: tmpDir });
|
execSync('git commit -m "add legacy hook"', { cwd: tmpDir });
|
||||||
|
|
||||||
run(`${TEAM_INIT} required`, { cwd: tmpDir });
|
const migration = run(`${TEAM_INIT} required`, { cwd: tmpDir });
|
||||||
expect(fs.existsSync(legacyHook)).toBe(false);
|
expect(fs.existsSync(legacyHook)).toBe(false);
|
||||||
expect(fs.readFileSync(unrelatedHook, 'utf-8')).toBe(unrelatedHookContents);
|
expect(fs.readFileSync(unrelatedHook, 'utf-8')).toBe(unrelatedHookContents);
|
||||||
|
const suggestedGitAdd = migration.stdout
|
||||||
|
.split('\n')
|
||||||
|
.find(line => line.startsWith(' git add '))
|
||||||
|
?.trim();
|
||||||
|
if (!suggestedGitAdd) {
|
||||||
|
throw new Error('gstack-team-init did not print a git add command');
|
||||||
|
}
|
||||||
|
expect(suggestedGitAdd.split(/\s+/)).toContain(
|
||||||
|
'.claude/hooks/check-gstack.sh',
|
||||||
|
);
|
||||||
|
execSync(suggestedGitAdd, { cwd: tmpDir });
|
||||||
|
execSync('git commit -m "migrate required hook"', { cwd: tmpDir });
|
||||||
|
expect(
|
||||||
|
execSync('git status --short', { cwd: tmpDir, encoding: 'utf-8' }),
|
||||||
|
).toBe('');
|
||||||
|
|
||||||
run(`${TEAM_INIT} required`, { cwd: tmpDir });
|
const rerun = run(`${TEAM_INIT} required`, { cwd: tmpDir });
|
||||||
const settings = JSON.parse(
|
const settings = JSON.parse(
|
||||||
fs.readFileSync(path.join(tmpDir, '.claude', 'settings.json'), 'utf-8'),
|
fs.readFileSync(path.join(tmpDir, '.claude', 'settings.json'), 'utf-8'),
|
||||||
);
|
);
|
||||||
expect(fs.existsSync(legacyHook)).toBe(false);
|
expect(fs.existsSync(legacyHook)).toBe(false);
|
||||||
expect(fs.readFileSync(unrelatedHook, 'utf-8')).toBe(unrelatedHookContents);
|
expect(fs.readFileSync(unrelatedHook, 'utf-8')).toBe(unrelatedHookContents);
|
||||||
|
const rerunGitAdd = rerun.stdout
|
||||||
|
.split('\n')
|
||||||
|
.find(line => line.startsWith(' git add '))
|
||||||
|
?.trim();
|
||||||
|
if (!rerunGitAdd) {
|
||||||
|
throw new Error('gstack-team-init rerun did not print a git add command');
|
||||||
|
}
|
||||||
|
expect(rerunGitAdd.split(/\s+/)).not.toContain(
|
||||||
|
'.claude/hooks/check-gstack.sh',
|
||||||
|
);
|
||||||
|
execSync(rerunGitAdd, { cwd: tmpDir });
|
||||||
expect(
|
expect(
|
||||||
execSync('git status --short -- .claude/hooks/check-gstack.sh', {
|
execSync('git status --short', { cwd: tmpDir, encoding: 'utf-8' }),
|
||||||
cwd: tmpDir,
|
).toBe('');
|
||||||
encoding: 'utf-8',
|
|
||||||
}),
|
|
||||||
).toBe(' D .claude/hooks/check-gstack.sh\n');
|
|
||||||
expect(settings.hooks.PreToolUse).toHaveLength(1);
|
expect(settings.hooks.PreToolUse).toHaveLength(1);
|
||||||
expect(settings.hooks.PreToolUse[0].hooks).toHaveLength(1);
|
expect(settings.hooks.PreToolUse[0].hooks).toHaveLength(1);
|
||||||
expect(settings.hooks.PreToolUse[0].hooks[0].command).toContain(
|
expect(settings.hooks.PreToolUse[0].hooks[0].command).toContain(
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue