Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
139 changes: 1 addition & 138 deletions src/cli/commands/execute.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { mkdirSync, rmSync, writeFileSync, existsSync, readFileSync, readdirSync
import { tmpdir } from 'node:os';
import { randomUUID } from 'node:crypto';
import { Command } from 'commander';
import { registerExecuteCommands, listSavedKatas, loadSavedKata, saveSavedKata, deleteSavedKata } from './execute.js';
import { registerExecuteCommands } from './execute.js';
import { CycleManager } from '@domain/services/cycle-manager.js';
import { JsonStore } from '@infra/persistence/json-store.js';
import { SessionExecutionBridge } from '@infra/execution/session-bridge.js';
Expand Down Expand Up @@ -2137,140 +2137,3 @@ describe('registerExecuteCommands', () => {
});
});

// ---------------------------------------------------------------------------
// Saved kata CRUD — direct function tests for mutation hardening
// ---------------------------------------------------------------------------

describe('saved kata CRUD functions', () => {
let tmpBase: string;

beforeEach(() => {
tmpBase = join(tmpdir(), `kata-crud-${randomUUID()}`);
mkdirSync(tmpBase, { recursive: true });
});

afterEach(() => {
rmSync(tmpBase, { recursive: true, force: true });
});

describe('listSavedKatas', () => {
it('returns empty when katas dir does not exist', () => {
expect(listSavedKatas(tmpBase)).toEqual([]);
});

it('returns valid katas and skips non-json files', () => {
const katasDir = join(tmpBase, 'katas');
mkdirSync(katasDir, { recursive: true });
writeFileSync(join(katasDir, 'valid.json'), JSON.stringify({ name: 'valid', stages: ['build'] }));
writeFileSync(join(katasDir, 'notes.txt'), 'not json');
writeFileSync(join(katasDir, 'broken.json'), '{ broken }');

const result = listSavedKatas(tmpBase);
expect(result).toHaveLength(1);
expect(result[0]!.name).toBe('valid');
expect(result[0]!.stages).toEqual(['build']);
});

it('skips files with invalid schema data', () => {
const katasDir = join(tmpBase, 'katas');
mkdirSync(katasDir, { recursive: true });
writeFileSync(join(katasDir, 'bad-schema.json'), JSON.stringify({ name: 123 }));

expect(listSavedKatas(tmpBase)).toEqual([]);
});
});

describe('loadSavedKata', () => {
it('loads a valid saved kata', () => {
const katasDir = join(tmpBase, 'katas');
mkdirSync(katasDir, { recursive: true });
writeFileSync(join(katasDir, 'my-kata.json'), JSON.stringify({
name: 'my-kata', stages: ['research', 'build'],
}));

const result = loadSavedKata(tmpBase, 'my-kata');
expect(result.stages).toEqual(['research', 'build']);
});

it('throws when kata does not exist', () => {
expect(() => loadSavedKata(tmpBase, 'missing')).toThrow(
'Kata "missing" not found.',
);
});

it('throws for invalid JSON content with cause', () => {
const katasDir = join(tmpBase, 'katas');
mkdirSync(katasDir, { recursive: true });
writeFileSync(join(katasDir, 'broken.json'), '{ broken }');

try {
loadSavedKata(tmpBase, 'broken');
expect.fail('Should have thrown');
} catch (err) {
expect(err).toBeInstanceOf(Error);
expect((err as Error).message).toContain('Kata "broken" has invalid JSON:');
expect((err as Error).cause).toBeDefined();
}
});

it('throws for valid JSON with invalid schema and includes cause', () => {
const katasDir = join(tmpBase, 'katas');
mkdirSync(katasDir, { recursive: true });
writeFileSync(join(katasDir, 'bad.json'), JSON.stringify({ name: 123 }));

try {
loadSavedKata(tmpBase, 'bad');
expect.fail('Should have thrown');
} catch (err) {
expect(err).toBeInstanceOf(Error);
expect((err as Error).message).toContain('Kata "bad" has invalid structure.');
expect((err as Error).cause).toBeDefined();
}
});
});

describe('saveSavedKata', () => {
it('creates the katas directory and writes the file', () => {
saveSavedKata(tmpBase, 'new-kata', ['build', 'review']);

const filePath = join(tmpBase, 'katas', 'new-kata.json');
expect(existsSync(filePath)).toBe(true);
const data = JSON.parse(readFileSync(filePath, 'utf-8'));
expect(data.name).toBe('new-kata');
expect(data.stages).toEqual(['build', 'review']);
});
});

describe('deleteSavedKata', () => {
it('deletes an existing saved kata', () => {
saveSavedKata(tmpBase, 'del-kata', ['build']);
const filePath = join(tmpBase, 'katas', 'del-kata.json');
expect(existsSync(filePath)).toBe(true);

deleteSavedKata(tmpBase, 'del-kata');
expect(existsSync(filePath)).toBe(false);
});

it('throws when kata does not exist', () => {
expect(() => deleteSavedKata(tmpBase, 'nonexistent')).toThrow(
'Kata "nonexistent" not found.',
);
});
});

describe('listSavedKatas with non-json files', () => {
it('filters out non-json files from the katas directory', () => {
const dir = join(tmpBase, 'katas');
mkdirSync(dir, { recursive: true });
writeFileSync(join(dir, 'readme.txt'), 'not a kata');
writeFileSync(join(dir, 'valid.json'), JSON.stringify({
name: 'valid',
stages: ['build'],
}));

const katas = listSavedKatas(tmpBase);
expect(katas).toHaveLength(1);
expect(katas[0]!.name).toBe('valid');
});
});
});
137 changes: 137 additions & 0 deletions src/cli/commands/saved-kata-store.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import { join } from 'node:path';
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { randomUUID } from 'node:crypto';
import { listSavedKatas, loadSavedKata, saveSavedKata, deleteSavedKata } from '@cli/commands/saved-kata-store.js';

describe('saved-kata-store', () => {
let tmpBase: string;

beforeEach(() => {
tmpBase = join(tmpdir(), `kata-saved-store-${randomUUID()}`);
mkdirSync(tmpBase, { recursive: true });
});

afterEach(() => {
rmSync(tmpBase, { recursive: true, force: true });
});

describe('listSavedKatas', () => {
it('returns empty when katas dir does not exist', () => {
expect(listSavedKatas(tmpBase)).toEqual([]);
});

it('returns valid katas and skips non-json files', () => {
const katasDir = join(tmpBase, 'katas');
mkdirSync(katasDir, { recursive: true });
writeFileSync(join(katasDir, 'valid.json'), JSON.stringify({ name: 'valid', stages: ['build'] }));
writeFileSync(join(katasDir, 'notes.txt'), 'not json');
writeFileSync(join(katasDir, 'broken.json'), '{ broken }');

const result = listSavedKatas(tmpBase);
expect(result).toHaveLength(1);
expect(result[0]!.name).toBe('valid');
expect(result[0]!.stages).toEqual(['build']);
});

it('skips files with invalid schema data', () => {
const katasDir = join(tmpBase, 'katas');
mkdirSync(katasDir, { recursive: true });
writeFileSync(join(katasDir, 'bad-schema.json'), JSON.stringify({ name: 123 }));

expect(listSavedKatas(tmpBase)).toEqual([]);
});

it('filters out non-json files from the katas directory', () => {
const dir = join(tmpBase, 'katas');
mkdirSync(dir, { recursive: true });
writeFileSync(join(dir, 'readme.txt'), 'not a kata');
writeFileSync(join(dir, 'valid.json'), JSON.stringify({
name: 'valid',
stages: ['build'],
}));

const katas = listSavedKatas(tmpBase);
expect(katas).toHaveLength(1);
expect(katas[0]!.name).toBe('valid');
});
});

describe('loadSavedKata', () => {
it('loads a valid saved kata', () => {
const katasDir = join(tmpBase, 'katas');
mkdirSync(katasDir, { recursive: true });
writeFileSync(join(katasDir, 'my-kata.json'), JSON.stringify({
name: 'my-kata', stages: ['research', 'build'],
}));

const result = loadSavedKata(tmpBase, 'my-kata');
expect(result.stages).toEqual(['research', 'build']);
});

it('throws when kata does not exist', () => {
expect(() => loadSavedKata(tmpBase, 'missing')).toThrow(
'Kata "missing" not found.',
);
});

it('throws for invalid JSON content with cause', () => {
const katasDir = join(tmpBase, 'katas');
mkdirSync(katasDir, { recursive: true });
writeFileSync(join(katasDir, 'broken.json'), '{ broken }');

try {
loadSavedKata(tmpBase, 'broken');
expect.fail('Should have thrown');
} catch (err) {
expect(err).toBeInstanceOf(Error);
expect((err as Error).message).toContain('Kata "broken" has invalid JSON:');
expect((err as Error).cause).toBeDefined();
}
});

it('throws for valid JSON with invalid schema and includes cause', () => {
const katasDir = join(tmpBase, 'katas');
mkdirSync(katasDir, { recursive: true });
writeFileSync(join(katasDir, 'bad.json'), JSON.stringify({ name: 123 }));

try {
loadSavedKata(tmpBase, 'bad');
expect.fail('Should have thrown');
} catch (err) {
expect(err).toBeInstanceOf(Error);
expect((err as Error).message).toContain('Kata "bad" has invalid structure.');
expect((err as Error).cause).toBeDefined();
}
});
});

describe('saveSavedKata', () => {
it('creates the katas directory and writes the file', () => {
saveSavedKata(tmpBase, 'new-kata', ['build', 'review']);

const filePath = join(tmpBase, 'katas', 'new-kata.json');
expect(existsSync(filePath)).toBe(true);
const data = JSON.parse(readFileSync(filePath, 'utf-8'));
expect(data.name).toBe('new-kata');
expect(data.stages).toEqual(['build', 'review']);
});
});

describe('deleteSavedKata', () => {
it('deletes an existing saved kata', () => {
saveSavedKata(tmpBase, 'del-kata', ['build']);
const filePath = join(tmpBase, 'katas', 'del-kata.json');
expect(existsSync(filePath)).toBe(true);

deleteSavedKata(tmpBase, 'del-kata');
expect(existsSync(filePath)).toBe(false);
});

it('throws when kata does not exist', () => {
expect(() => deleteSavedKata(tmpBase, 'nonexistent')).toThrow(
'Kata "nonexistent" not found.',
);
});
});
});
20 changes: 0 additions & 20 deletions src/features/cycle-management/cooldown-session.helpers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,7 @@ import {
mapBridgeRunStatusToSyncedOutcome,
resolveAppliedProposalIds,
selectEffectiveBetOutcomes,
shouldRecordBetOutcomes,
shouldWarnOnIncompleteRuns,
shouldWriteDojoDiary,
shouldWriteDojoSession,
} from './cooldown-session.helpers.js';

describe('cooldown-session helpers', () => {
Expand All @@ -34,13 +31,6 @@ describe('cooldown-session helpers', () => {
});
});

describe('shouldRecordBetOutcomes', () => {
it('returns true only for non-empty explicit outcomes', () => {
expect(shouldRecordBetOutcomes([])).toBe(false);
expect(shouldRecordBetOutcomes([{ betId: 'bet-1', outcome: 'complete' }])).toBe(true);
});
});

describe('selectEffectiveBetOutcomes', () => {
it('prefers explicit outcomes over auto-synced outcomes', () => {
expect(selectEffectiveBetOutcomes(
Expand Down Expand Up @@ -70,16 +60,6 @@ describe('cooldown-session helpers', () => {
});
});

describe('dojo helpers', () => {
it('requires dojoDir to write diary and session outputs', () => {
expect(shouldWriteDojoDiary(undefined)).toBe(false);
expect(shouldWriteDojoDiary('/tmp/dojo')).toBe(true);
expect(shouldWriteDojoSession(undefined, { build: vi.fn() })).toBe(false);
expect(shouldWriteDojoSession('/tmp/dojo', undefined)).toBe(false);
expect(shouldWriteDojoSession('/tmp/dojo', { build: vi.fn() })).toBe(true);
});
});

describe('clampConfidenceWithDelta', () => {
it('clamps confidence updates to the valid 0..1 range', () => {
expect(clampConfidenceWithDelta(0.9, 0.2)).toBe(1);
Expand Down
12 changes: 0 additions & 12 deletions src/features/cycle-management/cooldown-session.helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,6 @@ export function shouldWarnOnIncompleteRuns(incompleteRunsCount: number, force: b
return incompleteRunsCount > 0 && !force;
}

export function shouldRecordBetOutcomes(outcomes: readonly CooldownHelperBetOutcome[]): boolean {
return outcomes.length > 0;
}

export function selectEffectiveBetOutcomes(
explicitBetOutcomes: readonly CooldownHelperBetOutcome[],
syncedBetOutcomes: readonly CooldownHelperBetOutcome[],
Expand All @@ -76,14 +72,6 @@ export function buildDiaryBetOutcomesFromCycleBets(bets: readonly CooldownDiaryB
}));
}

export function shouldWriteDojoDiary(dojoDir: string | undefined): boolean {
return Boolean(dojoDir);
}

export function shouldWriteDojoSession(dojoDir: string | undefined, dojoSessionBuilder: unknown): boolean {
return Boolean(dojoDir && dojoSessionBuilder);
}

export function clampConfidenceWithDelta(existingConfidence: number, confidenceDelta: number): number {
return Math.min(1, Math.max(0, existingConfidence + confidenceDelta));
}
Expand Down
Loading
Loading