mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-10 01:09:43 +02:00
fix: formatting and async test bug in hooks-config
- Fix Prettier formatting in ralph-tracker.ts and respawn-controller.ts (whitespace drift from Phase 2/4 refactoring) - Add missing `await` to writeHooksConfig() calls in hooks-config.test.ts (async function was called without await, causing ENOENT race condition) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -33,7 +33,13 @@ import {
|
|||||||
PlanTaskStatus,
|
PlanTaskStatus,
|
||||||
TddPhase,
|
TddPhase,
|
||||||
} from './types.js';
|
} from './types.js';
|
||||||
import { ANSI_ESCAPE_PATTERN_SIMPLE, fuzzyPhraseMatch, todoContentHash, stringSimilarity, Debouncer } from './utils/index.js';
|
import {
|
||||||
|
ANSI_ESCAPE_PATTERN_SIMPLE,
|
||||||
|
fuzzyPhraseMatch,
|
||||||
|
todoContentHash,
|
||||||
|
stringSimilarity,
|
||||||
|
Debouncer,
|
||||||
|
} from './utils/index.js';
|
||||||
import { MAX_LINE_BUFFER_SIZE } from './config/buffer-limits.js';
|
import { MAX_LINE_BUFFER_SIZE } from './config/buffer-limits.js';
|
||||||
import { MAX_TODOS_PER_SESSION } from './config/map-limits.js';
|
import { MAX_TODOS_PER_SESSION } from './config/map-limits.js';
|
||||||
import { RalphPlanTracker } from './ralph-plan-tracker.js';
|
import { RalphPlanTracker } from './ralph-plan-tracker.js';
|
||||||
|
|||||||
+18
-33
@@ -1092,7 +1092,8 @@ export class RespawnController extends EventEmitter {
|
|||||||
this.stopDetectionUpdates();
|
this.stopDetectionUpdates();
|
||||||
if (this._state === 'stopped') return;
|
if (this._state === 'stopped') return;
|
||||||
this.lastEmittedDetectionKey = '';
|
this.lastEmittedDetectionKey = '';
|
||||||
const id = this.cleanup.setInterval(() => {
|
const id = this.cleanup.setInterval(
|
||||||
|
() => {
|
||||||
try {
|
try {
|
||||||
if (this._state !== 'stopped') {
|
if (this._state !== 'stopped') {
|
||||||
const status = this.getDetectionStatus();
|
const status = this.getDetectionStatus();
|
||||||
@@ -1107,7 +1108,10 @@ export class RespawnController extends EventEmitter {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`[RespawnController] Error in detectionUpdateTimer:`, err);
|
console.error(`[RespawnController] Error in detectionUpdateTimer:`, err);
|
||||||
}
|
}
|
||||||
}, 2000, { description: 'detection-update' });
|
},
|
||||||
|
2000,
|
||||||
|
{ description: 'detection-update' }
|
||||||
|
);
|
||||||
this.timerIds.set('detection-update', id);
|
this.timerIds.set('detection-update', id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1406,7 +1410,6 @@ export class RespawnController extends EventEmitter {
|
|||||||
// Cancel hook confirmation timer if running
|
// Cancel hook confirmation timer if running
|
||||||
this.cancelTrackedTimer('hook-confirm', 'working patterns detected');
|
this.cancelTrackedTimer('hook-confirm', 'working patterns detected');
|
||||||
|
|
||||||
|
|
||||||
// Cancel any pending completion confirmation
|
// Cancel any pending completion confirmation
|
||||||
this.cancelCompletionConfirm();
|
this.cancelCompletionConfirm();
|
||||||
|
|
||||||
@@ -1590,7 +1593,6 @@ export class RespawnController extends EventEmitter {
|
|||||||
'init-monitor',
|
'init-monitor',
|
||||||
3000,
|
3000,
|
||||||
() => {
|
() => {
|
||||||
|
|
||||||
// If still in monitoring state and no work detected, consider it idle
|
// If still in monitoring state and no work detected, consider it idle
|
||||||
if (this._state === 'monitoring_init' && !this.workingDetected) {
|
if (this._state === 'monitoring_init' && !this.workingDetected) {
|
||||||
this.checkMonitoringInitIdle();
|
this.checkMonitoringInitIdle();
|
||||||
@@ -1625,7 +1627,6 @@ export class RespawnController extends EventEmitter {
|
|||||||
'step-delay',
|
'step-delay',
|
||||||
this.config.interStepDelayMs,
|
this.config.interStepDelayMs,
|
||||||
async () => {
|
async () => {
|
||||||
|
|
||||||
if (this._state === 'stopped') return;
|
if (this._state === 'stopped') return;
|
||||||
const prompt = this.config.kickstartPrompt!;
|
const prompt = this.config.kickstartPrompt!;
|
||||||
this.logAction('command', `Sending kickstart: "${prompt.substring(0, 40)}..."`);
|
this.logAction('command', `Sending kickstart: "${prompt.substring(0, 40)}..."`);
|
||||||
@@ -1677,13 +1678,17 @@ export class RespawnController extends EventEmitter {
|
|||||||
// Check interval for stuck state
|
// Check interval for stuck state
|
||||||
const checkIntervalMs = Math.min(this.config.stuckStateWarningMs, 60000); // Check every minute max
|
const checkIntervalMs = Math.min(this.config.stuckStateWarningMs, 60000); // Check every minute max
|
||||||
|
|
||||||
const id = this.cleanup.setInterval(() => {
|
const id = this.cleanup.setInterval(
|
||||||
|
() => {
|
||||||
try {
|
try {
|
||||||
this.checkStuckState();
|
this.checkStuckState();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`[RespawnController] Error in stuckStateTimer:`, err);
|
console.error(`[RespawnController] Error in stuckStateTimer:`, err);
|
||||||
}
|
}
|
||||||
}, checkIntervalMs, { description: 'stuck-state' });
|
},
|
||||||
|
checkIntervalMs,
|
||||||
|
{ description: 'stuck-state' }
|
||||||
|
);
|
||||||
this.timerIds.set('stuck-state', id);
|
this.timerIds.set('stuck-state', id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1836,12 +1841,16 @@ export class RespawnController extends EventEmitter {
|
|||||||
this.emit('timerStarted', { name, durationMs, endsAt, reason });
|
this.emit('timerStarted', { name, durationMs, endsAt, reason });
|
||||||
this.logAction('timer', `Started ${name}: ${Math.round(durationMs / 1000)}s${reason ? ` (${reason})` : ''}`);
|
this.logAction('timer', `Started ${name}: ${Math.round(durationMs / 1000)}s${reason ? ` (${reason})` : ''}`);
|
||||||
|
|
||||||
const id = this.cleanup.setTimeout(() => {
|
const id = this.cleanup.setTimeout(
|
||||||
|
() => {
|
||||||
this.timerIds.delete(name);
|
this.timerIds.delete(name);
|
||||||
this.activeTimers.delete(name);
|
this.activeTimers.delete(name);
|
||||||
this.emit('timerCompleted', name);
|
this.emit('timerCompleted', name);
|
||||||
callback();
|
callback();
|
||||||
}, durationMs, { description: name });
|
},
|
||||||
|
durationMs,
|
||||||
|
{ description: name }
|
||||||
|
);
|
||||||
this.timerIds.set(name, id);
|
this.timerIds.set(name, id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1927,12 +1936,10 @@ export class RespawnController extends EventEmitter {
|
|||||||
private startNoOutputTimer(): void {
|
private startNoOutputTimer(): void {
|
||||||
this.cancelTrackedTimer('no-output-fallback', 'restarting');
|
this.cancelTrackedTimer('no-output-fallback', 'restarting');
|
||||||
|
|
||||||
|
|
||||||
this.startTrackedTimer(
|
this.startTrackedTimer(
|
||||||
'no-output-fallback',
|
'no-output-fallback',
|
||||||
this.config.noOutputTimeoutMs,
|
this.config.noOutputTimeoutMs,
|
||||||
() => {
|
() => {
|
||||||
|
|
||||||
if (this._state === 'watching' || this._state === 'confirming_idle') {
|
if (this._state === 'watching' || this._state === 'confirming_idle') {
|
||||||
const msSinceOutput = Date.now() - this.lastOutputTime;
|
const msSinceOutput = Date.now() - this.lastOutputTime;
|
||||||
this.logAction('detection', `No-output fallback: ${Math.round(msSinceOutput / 1000)}s silence`);
|
this.logAction('detection', `No-output fallback: ${Math.round(msSinceOutput / 1000)}s silence`);
|
||||||
@@ -1967,7 +1974,6 @@ export class RespawnController extends EventEmitter {
|
|||||||
private startPreFilterTimer(): void {
|
private startPreFilterTimer(): void {
|
||||||
this.cancelTrackedTimer('pre-filter', 'restarting');
|
this.cancelTrackedTimer('pre-filter', 'restarting');
|
||||||
|
|
||||||
|
|
||||||
// Only set up pre-filter when AI check is enabled
|
// Only set up pre-filter when AI check is enabled
|
||||||
if (!this.config.aiIdleCheckEnabled) return;
|
if (!this.config.aiIdleCheckEnabled) return;
|
||||||
|
|
||||||
@@ -1975,7 +1981,6 @@ export class RespawnController extends EventEmitter {
|
|||||||
'pre-filter',
|
'pre-filter',
|
||||||
this.config.completionConfirmMs,
|
this.config.completionConfirmMs,
|
||||||
() => {
|
() => {
|
||||||
|
|
||||||
if (this._state === 'watching') {
|
if (this._state === 'watching') {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const msSinceOutput = now - this.lastOutputTime;
|
const msSinceOutput = now - this.lastOutputTime;
|
||||||
@@ -2084,7 +2089,6 @@ export class RespawnController extends EventEmitter {
|
|||||||
|
|
||||||
this.cancelTrackedTimer('pre-filter', 'AI verdict: IDLE');
|
this.cancelTrackedTimer('pre-filter', 'AI verdict: IDLE');
|
||||||
|
|
||||||
|
|
||||||
this.logAction('ai-check', `Verdict: IDLE - ${result.reasoning}`);
|
this.logAction('ai-check', `Verdict: IDLE - ${result.reasoning}`);
|
||||||
this.emit('aiCheckCompleted', result);
|
this.emit('aiCheckCompleted', result);
|
||||||
this.onIdleConfirmed(`ai-check: idle (${result.reasoning})`);
|
this.onIdleConfirmed(`ai-check: idle (${result.reasoning})`);
|
||||||
@@ -2092,7 +2096,6 @@ export class RespawnController extends EventEmitter {
|
|||||||
// Cancel timers and go to cooldown
|
// Cancel timers and go to cooldown
|
||||||
this.cancelTrackedTimer('completion-confirm', 'AI verdict: WORKING');
|
this.cancelTrackedTimer('completion-confirm', 'AI verdict: WORKING');
|
||||||
|
|
||||||
|
|
||||||
this.logAction('ai-check', `Verdict: WORKING - ${result.reasoning}`);
|
this.logAction('ai-check', `Verdict: WORKING - ${result.reasoning}`);
|
||||||
this.emit('aiCheckCompleted', result);
|
this.emit('aiCheckCompleted', result);
|
||||||
this.setState('watching');
|
this.setState('watching');
|
||||||
@@ -2149,12 +2152,10 @@ export class RespawnController extends EventEmitter {
|
|||||||
private startAutoAcceptTimer(): void {
|
private startAutoAcceptTimer(): void {
|
||||||
this.cancelTrackedTimer('auto-accept', 'restarting');
|
this.cancelTrackedTimer('auto-accept', 'restarting');
|
||||||
|
|
||||||
|
|
||||||
this.startTrackedTimer(
|
this.startTrackedTimer(
|
||||||
'auto-accept',
|
'auto-accept',
|
||||||
this.config.autoAcceptDelayMs,
|
this.config.autoAcceptDelayMs,
|
||||||
() => {
|
() => {
|
||||||
|
|
||||||
this.tryAutoAccept();
|
this.tryAutoAccept();
|
||||||
},
|
},
|
||||||
'plan mode detection'
|
'plan mode detection'
|
||||||
@@ -2167,7 +2168,6 @@ export class RespawnController extends EventEmitter {
|
|||||||
*/
|
*/
|
||||||
private cancelAutoAcceptTimer(): void {
|
private cancelAutoAcceptTimer(): void {
|
||||||
this.cancelTrackedTimer('auto-accept', 'cancelled');
|
this.cancelTrackedTimer('auto-accept', 'cancelled');
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -2385,11 +2385,9 @@ export class RespawnController extends EventEmitter {
|
|||||||
// Cancel completion confirm timer - hook takes precedence
|
// Cancel completion confirm timer - hook takes precedence
|
||||||
this.cancelTrackedTimer('completion-confirm', 'Stop hook received');
|
this.cancelTrackedTimer('completion-confirm', 'Stop hook received');
|
||||||
|
|
||||||
|
|
||||||
// Cancel pre-filter timer - hook takes precedence
|
// Cancel pre-filter timer - hook takes precedence
|
||||||
this.cancelTrackedTimer('pre-filter', 'Stop hook received');
|
this.cancelTrackedTimer('pre-filter', 'Stop hook received');
|
||||||
|
|
||||||
|
|
||||||
// Start short confirmation timer to handle race conditions
|
// Start short confirmation timer to handle race conditions
|
||||||
// (e.g., Stop hook arrives but Claude immediately starts new work)
|
// (e.g., Stop hook arrives but Claude immediately starts new work)
|
||||||
this.startHookConfirmTimer('stop');
|
this.startHookConfirmTimer('stop');
|
||||||
@@ -2428,7 +2426,6 @@ export class RespawnController extends EventEmitter {
|
|||||||
|
|
||||||
this.cancelTrackedTimer('no-output-fallback', 'idle_prompt received');
|
this.cancelTrackedTimer('no-output-fallback', 'idle_prompt received');
|
||||||
|
|
||||||
|
|
||||||
// idle_prompt is an even stronger signal than Stop hook (60s+ idle)
|
// idle_prompt is an even stronger signal than Stop hook (60s+ idle)
|
||||||
// Skip confirmation and go directly to idle
|
// Skip confirmation and go directly to idle
|
||||||
this.onIdleConfirmed('idle_prompt hook (60s+ idle)');
|
this.onIdleConfirmed('idle_prompt hook (60s+ idle)');
|
||||||
@@ -2443,13 +2440,10 @@ export class RespawnController extends EventEmitter {
|
|||||||
private startHookConfirmTimer(hookType: 'stop' | 'idle_prompt'): void {
|
private startHookConfirmTimer(hookType: 'stop' | 'idle_prompt'): void {
|
||||||
this.cancelTrackedTimer('hook-confirm', 'restarting');
|
this.cancelTrackedTimer('hook-confirm', 'restarting');
|
||||||
|
|
||||||
|
|
||||||
this.startTrackedTimer(
|
this.startTrackedTimer(
|
||||||
'hook-confirm',
|
'hook-confirm',
|
||||||
RespawnController.HOOK_CONFIRM_DELAY_MS,
|
RespawnController.HOOK_CONFIRM_DELAY_MS,
|
||||||
() => {
|
() => {
|
||||||
|
|
||||||
|
|
||||||
// Verify we haven't received new output since the hook arrived
|
// Verify we haven't received new output since the hook arrived
|
||||||
const hookTime = hookType === 'stop' ? this.stopHookTime : this.idlePromptTime;
|
const hookTime = hookType === 'stop' ? this.stopHookTime : this.idlePromptTime;
|
||||||
if (hookTime && this.lastOutputTime > hookTime) {
|
if (hookTime && this.lastOutputTime > hookTime) {
|
||||||
@@ -2524,7 +2518,6 @@ export class RespawnController extends EventEmitter {
|
|||||||
private startCompletionConfirmTimer(): void {
|
private startCompletionConfirmTimer(): void {
|
||||||
this.cancelTrackedTimer('completion-confirm', 'restarting');
|
this.cancelTrackedTimer('completion-confirm', 'restarting');
|
||||||
|
|
||||||
|
|
||||||
this.setState('confirming_idle');
|
this.setState('confirming_idle');
|
||||||
this.logAction('detection', 'Completion message found in output');
|
this.logAction('detection', 'Completion message found in output');
|
||||||
|
|
||||||
@@ -2532,7 +2525,6 @@ export class RespawnController extends EventEmitter {
|
|||||||
'completion-confirm',
|
'completion-confirm',
|
||||||
this.config.completionConfirmMs,
|
this.config.completionConfirmMs,
|
||||||
() => {
|
() => {
|
||||||
|
|
||||||
if (this._state === 'stopped') return;
|
if (this._state === 'stopped') return;
|
||||||
const msSinceOutput = Date.now() - this.lastOutputTime;
|
const msSinceOutput = Date.now() - this.lastOutputTime;
|
||||||
if (msSinceOutput >= this.config.completionConfirmMs) {
|
if (msSinceOutput >= this.config.completionConfirmMs) {
|
||||||
@@ -2569,12 +2561,10 @@ export class RespawnController extends EventEmitter {
|
|||||||
private startStepConfirmTimer(step: 'update' | 'init' | 'kickstart'): void {
|
private startStepConfirmTimer(step: 'update' | 'init' | 'kickstart'): void {
|
||||||
this.cancelTrackedTimer('step-confirm', 'restarting');
|
this.cancelTrackedTimer('step-confirm', 'restarting');
|
||||||
|
|
||||||
|
|
||||||
this.startTrackedTimer(
|
this.startTrackedTimer(
|
||||||
'step-confirm',
|
'step-confirm',
|
||||||
this.config.completionConfirmMs,
|
this.config.completionConfirmMs,
|
||||||
() => {
|
() => {
|
||||||
|
|
||||||
if (this._state === 'stopped') return;
|
if (this._state === 'stopped') return;
|
||||||
const msSinceOutput = Date.now() - this.lastOutputTime;
|
const msSinceOutput = Date.now() - this.lastOutputTime;
|
||||||
|
|
||||||
@@ -2608,7 +2598,6 @@ export class RespawnController extends EventEmitter {
|
|||||||
*/
|
*/
|
||||||
private cancelStepConfirm(): void {
|
private cancelStepConfirm(): void {
|
||||||
this.cancelTrackedTimer('step-confirm', 'working detected');
|
this.cancelTrackedTimer('step-confirm', 'working detected');
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -2774,7 +2763,6 @@ export class RespawnController extends EventEmitter {
|
|||||||
'step-delay',
|
'step-delay',
|
||||||
this.config.interStepDelayMs,
|
this.config.interStepDelayMs,
|
||||||
async () => {
|
async () => {
|
||||||
|
|
||||||
if (this._state === 'stopped') return;
|
if (this._state === 'stopped') return;
|
||||||
|
|
||||||
// Use RALPH_STATUS RECOMMENDATION if available, otherwise fall back to config
|
// Use RALPH_STATUS RECOMMENDATION if available, otherwise fall back to config
|
||||||
@@ -2815,7 +2803,6 @@ export class RespawnController extends EventEmitter {
|
|||||||
'step-delay',
|
'step-delay',
|
||||||
this.config.interStepDelayMs,
|
this.config.interStepDelayMs,
|
||||||
async () => {
|
async () => {
|
||||||
|
|
||||||
if (this._state === 'stopped') return;
|
if (this._state === 'stopped') return;
|
||||||
this.logAction('command', 'Sending: /clear');
|
this.logAction('command', 'Sending: /clear');
|
||||||
await this.session.writeViaMux('/clear\r'); // \r triggers Enter in Ink/Claude CLI
|
await this.session.writeViaMux('/clear\r'); // \r triggers Enter in Ink/Claude CLI
|
||||||
@@ -2828,7 +2815,6 @@ export class RespawnController extends EventEmitter {
|
|||||||
'clear-fallback',
|
'clear-fallback',
|
||||||
RespawnController.CLEAR_FALLBACK_TIMEOUT_MS,
|
RespawnController.CLEAR_FALLBACK_TIMEOUT_MS,
|
||||||
() => {
|
() => {
|
||||||
|
|
||||||
if (this._state === 'waiting_clear') {
|
if (this._state === 'waiting_clear') {
|
||||||
this.logAction('step', '/clear fallback: proceeding to /init');
|
this.logAction('step', '/clear fallback: proceeding to /init');
|
||||||
this.emit('stepCompleted', 'clear');
|
this.emit('stepCompleted', 'clear');
|
||||||
@@ -2859,7 +2845,6 @@ export class RespawnController extends EventEmitter {
|
|||||||
'step-delay',
|
'step-delay',
|
||||||
this.config.interStepDelayMs,
|
this.config.interStepDelayMs,
|
||||||
async () => {
|
async () => {
|
||||||
|
|
||||||
if (this._state === 'stopped') return;
|
if (this._state === 'stopped') return;
|
||||||
this.logAction('command', 'Sending: /init');
|
this.logAction('command', 'Sending: /init');
|
||||||
await this.session.writeViaMux('/init\r'); // \r triggers Enter in Ink/Claude CLI
|
await this.session.writeViaMux('/init\r'); // \r triggers Enter in Ink/Claude CLI
|
||||||
|
|||||||
+16
-16
@@ -111,19 +111,19 @@ describe('writeHooksConfig', () => {
|
|||||||
rmSync(testDir, { recursive: true, force: true });
|
rmSync(testDir, { recursive: true, force: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should create .claude directory if it does not exist', () => {
|
it('should create .claude directory if it does not exist', async () => {
|
||||||
writeHooksConfig(testDir);
|
await writeHooksConfig(testDir);
|
||||||
expect(existsSync(join(testDir, '.claude'))).toBe(true);
|
expect(existsSync(join(testDir, '.claude'))).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should create settings.local.json', () => {
|
it('should create settings.local.json', async () => {
|
||||||
writeHooksConfig(testDir);
|
await writeHooksConfig(testDir);
|
||||||
const settingsPath = join(testDir, '.claude', 'settings.local.json');
|
const settingsPath = join(testDir, '.claude', 'settings.local.json');
|
||||||
expect(existsSync(settingsPath)).toBe(true);
|
expect(existsSync(settingsPath)).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should write valid JSON', () => {
|
it('should write valid JSON', async () => {
|
||||||
writeHooksConfig(testDir);
|
await writeHooksConfig(testDir);
|
||||||
const settingsPath = join(testDir, '.claude', 'settings.local.json');
|
const settingsPath = join(testDir, '.claude', 'settings.local.json');
|
||||||
const content = readFileSync(settingsPath, 'utf-8');
|
const content = readFileSync(settingsPath, 'utf-8');
|
||||||
const parsed = JSON.parse(content);
|
const parsed = JSON.parse(content);
|
||||||
@@ -132,8 +132,8 @@ describe('writeHooksConfig', () => {
|
|||||||
expect(parsed.hooks).toHaveProperty('Stop');
|
expect(parsed.hooks).toHaveProperty('Stop');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should include hooks config in output', () => {
|
it('should include hooks config in output', async () => {
|
||||||
writeHooksConfig(testDir);
|
await writeHooksConfig(testDir);
|
||||||
const settingsPath = join(testDir, '.claude', 'settings.local.json');
|
const settingsPath = join(testDir, '.claude', 'settings.local.json');
|
||||||
const parsed = JSON.parse(readFileSync(settingsPath, 'utf-8'));
|
const parsed = JSON.parse(readFileSync(settingsPath, 'utf-8'));
|
||||||
expect(parsed.hooks).toBeDefined();
|
expect(parsed.hooks).toBeDefined();
|
||||||
@@ -141,7 +141,7 @@ describe('writeHooksConfig', () => {
|
|||||||
expect(parsed.hooks.Stop).toHaveLength(1);
|
expect(parsed.hooks.Stop).toHaveLength(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should merge with existing settings.local.json', () => {
|
it('should merge with existing settings.local.json', async () => {
|
||||||
const claudeDir = join(testDir, '.claude');
|
const claudeDir = join(testDir, '.claude');
|
||||||
mkdirSync(claudeDir, { recursive: true });
|
mkdirSync(claudeDir, { recursive: true });
|
||||||
writeFileSync(
|
writeFileSync(
|
||||||
@@ -149,7 +149,7 @@ describe('writeHooksConfig', () => {
|
|||||||
JSON.stringify({ existingKey: 'existingValue', permissions: { allow: ['Read'] } }, null, 2),
|
JSON.stringify({ existingKey: 'existingValue', permissions: { allow: ['Read'] } }, null, 2),
|
||||||
);
|
);
|
||||||
|
|
||||||
writeHooksConfig(testDir);
|
await writeHooksConfig(testDir);
|
||||||
|
|
||||||
const parsed = JSON.parse(readFileSync(join(claudeDir, 'settings.local.json'), 'utf-8'));
|
const parsed = JSON.parse(readFileSync(join(claudeDir, 'settings.local.json'), 'utf-8'));
|
||||||
expect(parsed.existingKey).toBe('existingValue');
|
expect(parsed.existingKey).toBe('existingValue');
|
||||||
@@ -157,7 +157,7 @@ describe('writeHooksConfig', () => {
|
|||||||
expect(parsed.hooks).toBeDefined();
|
expect(parsed.hooks).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should overwrite existing hooks key', () => {
|
it('should overwrite existing hooks key', async () => {
|
||||||
const claudeDir = join(testDir, '.claude');
|
const claudeDir = join(testDir, '.claude');
|
||||||
mkdirSync(claudeDir, { recursive: true });
|
mkdirSync(claudeDir, { recursive: true });
|
||||||
writeFileSync(
|
writeFileSync(
|
||||||
@@ -165,26 +165,26 @@ describe('writeHooksConfig', () => {
|
|||||||
JSON.stringify({ hooks: { oldHook: [] } }, null, 2),
|
JSON.stringify({ hooks: { oldHook: [] } }, null, 2),
|
||||||
);
|
);
|
||||||
|
|
||||||
writeHooksConfig(testDir);
|
await writeHooksConfig(testDir);
|
||||||
|
|
||||||
const parsed = JSON.parse(readFileSync(join(claudeDir, 'settings.local.json'), 'utf-8'));
|
const parsed = JSON.parse(readFileSync(join(claudeDir, 'settings.local.json'), 'utf-8'));
|
||||||
expect(parsed.hooks.oldHook).toBeUndefined();
|
expect(parsed.hooks.oldHook).toBeUndefined();
|
||||||
expect(parsed.hooks.Notification).toBeDefined();
|
expect(parsed.hooks.Notification).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle malformed existing settings.local.json', () => {
|
it('should handle malformed existing settings.local.json', async () => {
|
||||||
const claudeDir = join(testDir, '.claude');
|
const claudeDir = join(testDir, '.claude');
|
||||||
mkdirSync(claudeDir, { recursive: true });
|
mkdirSync(claudeDir, { recursive: true });
|
||||||
writeFileSync(join(claudeDir, 'settings.local.json'), 'not valid json{{{');
|
writeFileSync(join(claudeDir, 'settings.local.json'), 'not valid json{{{');
|
||||||
|
|
||||||
writeHooksConfig(testDir);
|
await writeHooksConfig(testDir);
|
||||||
|
|
||||||
const parsed = JSON.parse(readFileSync(join(claudeDir, 'settings.local.json'), 'utf-8'));
|
const parsed = JSON.parse(readFileSync(join(claudeDir, 'settings.local.json'), 'utf-8'));
|
||||||
expect(parsed.hooks).toBeDefined();
|
expect(parsed.hooks).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should end file with newline', () => {
|
it('should end file with newline', async () => {
|
||||||
writeHooksConfig(testDir);
|
await writeHooksConfig(testDir);
|
||||||
const content = readFileSync(join(testDir, '.claude', 'settings.local.json'), 'utf-8');
|
const content = readFileSync(join(testDir, '.claude', 'settings.local.json'), 'utf-8');
|
||||||
expect(content.endsWith('\n')).toBe(true);
|
expect(content.endsWith('\n')).toBe(true);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user