Files
Dragan Spiridonov ca0c6e7c5e fix: use CLAUDE_PROJECT_DIR for hook paths and remove ruflo permissions from aqe init
Hook commands using relative paths (node ./dist/...) or git rev-parse
broke when invoked from subfolders. Adopted the CLAUDE_PROJECT_DIR
pattern (sh -c 'exec node "${CLAUDE_PROJECT_DIR:-.}/..."') which
Claude Code sets to the project root.

Also: removed ruflo/claude-flow permissions from _aqePermissions
injected by aqe init — only AQE-specific entries belong in user
settings. Fixed init-wizard-hooks.ts missing union-merge for
permissions (#362 parity).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 08:04:04 +00:00

284 lines
11 KiB
TypeScript

/**
* Test: Settings Merge Utilities
*
* Verifies that aqe init --auto correctly handles:
* - Fresh installs (no existing settings)
* - Updating existing AQE hooks (replace, don't duplicate)
* - Preserving non-AQE hooks from user config
* - Handling mixed old/new AQE command variants
*/
import { describe, it, expect } from 'vitest';
import {
isAqeHookEntry,
mergeHooksSmart,
generateAqeEnvVars,
generateV3SettingsSections,
} from '../../../src/init/settings-merge.js';
describe('Settings Merge Utilities', () => {
describe('isAqeHookEntry', () => {
it('should detect "aqe hooks" commands', () => {
const entry = {
matcher: '^Bash$',
hooks: [{ type: 'command', command: 'aqe hooks pre-command --command "$TOOL_INPUT_command"' }],
};
expect(isAqeHookEntry(entry)).toBe(true);
});
it('should detect "npx agentic-qe" commands', () => {
const entry = {
matcher: '^Task$',
hooks: [{ type: 'command', command: 'npx agentic-qe hooks pre-task --description "$TOOL_INPUT_prompt" --json' }],
};
expect(isAqeHookEntry(entry)).toBe(true);
});
it('should detect "npx @anthropics/agentic-qe" commands', () => {
const entry = {
hooks: [{ type: 'command', command: 'npx @anthropics/agentic-qe hooks route --task "$PROMPT"' }],
};
expect(isAqeHookEntry(entry)).toBe(true);
});
it('should NOT detect unrelated hooks', () => {
const entry = {
matcher: '^Bash$',
hooks: [{ type: 'command', command: 'eslint --fix "$TOOL_INPUT_file_path"' }],
};
expect(isAqeHookEntry(entry)).toBe(false);
});
it('should detect brain-checkpoint.cjs commands', () => {
const entry = {
hooks: [{ type: 'command', command: 'node .claude/helpers/brain-checkpoint.cjs verify --json' }],
};
expect(isAqeHookEntry(entry)).toBe(true);
});
it('should detect .claude/helpers/ commands', () => {
const entry = {
hooks: [{ type: 'command', command: 'node .claude/helpers/statusline-v3.cjs 2>/dev/null' }],
};
expect(isAqeHookEntry(entry)).toBe(true);
});
it('should handle entries without hooks array', () => {
expect(isAqeHookEntry({})).toBe(false);
expect(isAqeHookEntry(null)).toBe(false);
expect(isAqeHookEntry({ hooks: 'not-an-array' })).toBe(false);
});
});
describe('mergeHooksSmart', () => {
const newAqeHooks: Record<string, unknown[]> = {
PreToolUse: [
{
matcher: '^(Write|Edit|MultiEdit)$',
hooks: [{ type: 'command', command: 'npx agentic-qe hooks guard --file "$TOOL_INPUT_file_path" --json' }],
},
],
PostToolUse: [
{
matcher: '^(Write|Edit|MultiEdit)$',
hooks: [{ type: 'command', command: 'npx agentic-qe hooks post-edit --file "$TOOL_INPUT_file_path" --success --json' }],
},
],
SessionStart: [
{
hooks: [{ type: 'command', command: 'npx agentic-qe hooks session-start --session-id "$SESSION_ID" --json' }],
},
],
};
it('should add hooks to empty existing config', () => {
const result = mergeHooksSmart({}, newAqeHooks);
expect(result.PreToolUse).toHaveLength(1);
expect(result.PostToolUse).toHaveLength(1);
expect(result.SessionStart).toHaveLength(1);
});
it('should replace old AQE hooks with new ones', () => {
const existingHooks: Record<string, unknown[]> = {
PreToolUse: [
{
matcher: '^(Write|Edit|MultiEdit)$',
hooks: [{ type: 'command', command: 'aqe hooks pre-edit --file "$TOOL_INPUT_file_path"' }],
},
],
PostToolUse: [
{
matcher: '^(Write|Edit|MultiEdit)$',
hooks: [{ type: 'command', command: 'aqe hooks post-edit --file "$TOOL_INPUT_file_path" --success "$TOOL_SUCCESS"' }],
},
],
};
const result = mergeHooksSmart(existingHooks, newAqeHooks);
// Old hooks should be gone, new ones in place
expect(result.PreToolUse).toHaveLength(1);
const preCmd = (result.PreToolUse[0] as any).hooks[0].command;
expect(preCmd).toContain('npx agentic-qe hooks guard');
expect(result.PostToolUse).toHaveLength(1);
const postCmd = (result.PostToolUse[0] as any).hooks[0].command;
expect(postCmd).toContain('npx agentic-qe hooks post-edit');
});
it('should preserve non-AQE hooks alongside new AQE hooks', () => {
const existingHooks: Record<string, unknown[]> = {
PreToolUse: [
// User's custom eslint hook
{
matcher: '^(Write|Edit)$',
hooks: [{ type: 'command', command: 'eslint --fix "$TOOL_INPUT_file_path"' }],
},
// Old AQE hook that should be replaced
{
matcher: '^(Write|Edit|MultiEdit)$',
hooks: [{ type: 'command', command: 'aqe hooks pre-edit --file "$TOOL_INPUT_file_path"' }],
},
],
};
const result = mergeHooksSmart(existingHooks, newAqeHooks);
// Should have 2: new AQE guard + preserved eslint
expect(result.PreToolUse).toHaveLength(2);
const commands = result.PreToolUse.map(
(e: any) => e.hooks[0].command,
);
expect(commands).toContain('eslint --fix "$TOOL_INPUT_file_path"');
expect(commands).toContain('npx agentic-qe hooks guard --file "$TOOL_INPUT_file_path" --json');
});
it('should preserve user-defined hook types not managed by AQE', () => {
const existingHooks: Record<string, unknown[]> = {
NotificationSend: [
{ hooks: [{ type: 'command', command: 'notify-send "done"' }] },
],
};
const result = mergeHooksSmart(existingHooks, newAqeHooks);
expect(result.NotificationSend).toHaveLength(1);
expect((result.NotificationSend[0] as any).hooks[0].command).toBe('notify-send "done"');
});
it('should not duplicate AQE hooks when run multiple times', () => {
// First run
const result1 = mergeHooksSmart({}, newAqeHooks);
// Second run (simulating aqe init --auto again)
const result2 = mergeHooksSmart(result1, newAqeHooks);
// Third run
const result3 = mergeHooksSmart(result2, newAqeHooks);
expect(result3.PreToolUse).toHaveLength(1);
expect(result3.PostToolUse).toHaveLength(1);
expect(result3.SessionStart).toHaveLength(1);
});
it('should not duplicate brain-checkpoint hooks when run multiple times', () => {
const hooksWithBrainCheckpoint: Record<string, unknown[]> = {
...newAqeHooks,
SessionStart: [
...newAqeHooks.SessionStart,
{
hooks: [{ type: 'command', command: 'node .claude/helpers/brain-checkpoint.cjs verify --json', timeout: 5000, continueOnError: true }],
},
],
Stop: [
{
hooks: [{ type: 'command', command: 'node .claude/helpers/brain-checkpoint.cjs export --json', timeout: 60000, continueOnError: true }],
},
],
};
const result1 = mergeHooksSmart({}, hooksWithBrainCheckpoint);
const result2 = mergeHooksSmart(result1, hooksWithBrainCheckpoint);
const result3 = mergeHooksSmart(result2, hooksWithBrainCheckpoint);
expect(result3.SessionStart).toHaveLength(2); // session-start + brain-checkpoint verify
expect(result3.Stop).toHaveLength(1); // brain-checkpoint export
});
it('should handle mixed old aqe + npx agentic-qe commands', () => {
const existingHooks: Record<string, unknown[]> = {
PreToolUse: [
// Very old format
{ matcher: '^Bash$', hooks: [{ type: 'command', command: 'aqe hooks pre-command --command "$TOOL_INPUT_command"' }] },
// Slightly newer format
{ matcher: '^Task$', hooks: [{ type: 'command', command: 'npx agentic-qe hooks pre-task --description "$TOOL_INPUT_prompt"' }] },
],
SessionStart: [
{ hooks: [{ type: 'command', command: 'aqe hooks session-start --session-id "$SESSION_ID"' }] },
],
};
const result = mergeHooksSmart(existingHooks, newAqeHooks);
// All old AQE hooks should be removed, only new ones remain
expect(result.PreToolUse).toHaveLength(1);
expect(result.SessionStart).toHaveLength(1);
});
});
describe('generateAqeEnvVars', () => {
it('should generate full env vars from config', () => {
const config = {
learning: { enabled: true, hnswConfig: { M: 8 }, promotionThreshold: 3, qualityThreshold: 0.7 },
domains: { enabled: ['test-generation', 'coverage-analysis'] },
agents: { maxConcurrent: 15 },
} as any;
const env = generateAqeEnvVars(config);
expect(env.AQE_MEMORY_PATH).toBe('.agentic-qe/memory.db');
expect(env.AQE_V3_MODE).toBe('true');
expect(env.AQE_V3_HNSW_ENABLED).toBe('true');
expect(env.AQE_V3_DOMAINS).toBe('test-generation,coverage-analysis');
expect(env.AQE_V3_SWARM_SIZE).toBe('15');
});
it('should handle missing optional fields', () => {
const config = {
learning: { enabled: false },
domains: { enabled: [] },
} as any;
const env = generateAqeEnvVars(config);
expect(env.AQE_LEARNING_ENABLED).toBe('false');
expect(env.AQE_V3_DOMAINS).toBe('');
expect(env.AQE_V3_SWARM_SIZE).toBe('15'); // default
});
});
describe('generateV3SettingsSections', () => {
it('should generate statusLine, _aqePermissions, v3Configuration, v3Learning', () => {
const config = {
learning: { enabled: true, hnswConfig: { M: 8 }, promotionThreshold: 3, qualityThreshold: 0.7 },
domains: { enabled: ['test-generation'] },
agents: { maxConcurrent: 10 },
} as any;
const sections = generateV3SettingsSections(config);
expect(sections.statusLine).toBeDefined();
expect((sections.statusLine as any).enabled).toBe(true);
expect(sections._aqePermissions).toBeDefined();
expect(sections._aqePermissions).toContain('mcp__agentic-qe__*');
expect(sections.v3Configuration).toBeDefined();
expect((sections.v3Configuration as any).domains.total).toBe(1);
expect(sections.v3Learning).toBeDefined();
expect((sections.v3Learning as any).enabled).toBe(true);
expect((sections.v3Learning as any).patternPromotion.threshold).toBe(3);
});
});
});