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
Original file line number Diff line number Diff line change
Expand Up @@ -467,5 +467,76 @@ Active step.
expect(normalizeOutputPath(result.stdout)).toMatch(/^\.work\/\d{4}-\d{2}-\d{2}-plan\.json$/);
expect(result.stderr).not.toContain('Invalid id');
});

it('soft-fails legacy session ownership format when RD_WORK_PATH is set', async () => {
// SessionService.loadSession throws 'Legacy session ownership format
// detected' when session.json contains 'ownedRunbooks' (or
// 'stashedRunbookOwnership'). isRecoverableActiveStateLookupError must
// recognize this so rdpath assembles a path without a context segment.
await fs.mkdir(path.join(testDir, '.rundown'), { recursive: true });
await fs.writeFile(
path.join(testDir, '.rundown', 'session.json'),
JSON.stringify({ ownedRunbooks: { 'wf-old-run': { runbookPath: 'foo.md' } } }, null, 2),
);

const result = await runRdpath(
['--file', 'plan.json'],
{
RD_WORK_PATH: '.work',
RD_CONTEXT_ID: undefined,
},
testDir,
);

expect(result.exitCode).toBe(0);
expect(normalizeOutputPath(result.stdout)).toMatch(/^\.work\/\d{4}-\d{2}-\d{2}-plan\.json$/);
expect(result.stderr).toBe('');
});

it('soft-fails legacy stacks session format when RD_WORK_PATH is set', async () => {
// Second variant of the legacy ownership branch: 'stacks' key triggers
// the same recoverable error path.
await fs.mkdir(path.join(testDir, '.rundown'), { recursive: true });
await fs.writeFile(
path.join(testDir, '.rundown', 'session.json'),
JSON.stringify({ stacks: { default: [] } }, null, 2),
);

const result = await runRdpath(
['--file', 'plan.json'],
{
RD_WORK_PATH: '.work',
RD_CONTEXT_ID: undefined,
},
testDir,
);

expect(result.exitCode).toBe(0);
expect(normalizeOutputPath(result.stdout)).toMatch(/^\.work\/\d{4}-\d{2}-\d{2}-plan\.json$/);
expect(result.stderr).toBe('');
});

it('soft-fails session.json that fails schema validation when RD_WORK_PATH is set', async () => {
// 'Session file contains invalid runbook targeting data' is thrown when
// session.json parses as JSON but fails SessionDataSchema validation.
await fs.mkdir(path.join(testDir, '.rundown'), { recursive: true });
await fs.writeFile(
path.join(testDir, '.rundown', 'session.json'),
JSON.stringify({ defaultStack: [{ not: 'a-string' }] }, null, 2),
);

const result = await runRdpath(
['--file', 'plan.json'],
{
RD_WORK_PATH: '.work',
RD_CONTEXT_ID: undefined,
},
testDir,
);

expect(result.exitCode).toBe(0);
expect(normalizeOutputPath(result.stdout)).toMatch(/^\.work\/\d{4}-\d{2}-\d{2}-plan\.json$/);
expect(result.stderr).toBe('');
});
});
});
120 changes: 120 additions & 0 deletions packages/core/__tests__/runbook/run-id.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { describe, it, expect } from '@jest/globals';
import { RUN_ID_PATTERN, RUN_ID_PREFIX, assertRunId, isRunId } from '../../src/runbook/run-id.js';

describe('isRunId', () => {
it('accepts canonical rd_ + 32 lowercase hex chars', () => {
expect(isRunId('rd_4b7f0c2d9e1a4b7f0c2d9e1a4b7f0c2d')).toBe(true);
});

it('accepts all-zero hex segment', () => {
expect(isRunId('rd_00000000000000000000000000000000')).toBe(true);
});

it('accepts all-f hex segment', () => {
expect(isRunId('rd_ffffffffffffffffffffffffffffffff')).toBe(true);
});

it('rejects uppercase hex characters', () => {
expect(isRunId('rd_ABCDEF00000000000000000000000000')).toBe(false);
});

it('rejects uppercase RD_ prefix', () => {
expect(isRunId('RD_00000000000000000000000000000000')).toBe(false);
});

it('rejects missing prefix', () => {
expect(isRunId('00000000000000000000000000000000')).toBe(false);
});

it('rejects 31-char hex segment', () => {
expect(isRunId('rd_0000000000000000000000000000000')).toBe(false);
});

it('rejects 33-char hex segment', () => {
expect(isRunId('rd_000000000000000000000000000000000')).toBe(false);
});

it('rejects non-hex character in segment', () => {
expect(isRunId('rd_g0000000000000000000000000000000')).toBe(false);
});

it('rejects hyphenated UUID-style body', () => {
expect(isRunId('rd_550e8400-e29b-41d4-a716-446655440000')).toBe(false);
});

it('rejects empty string', () => {
expect(isRunId('')).toBe(false);
});

it('rejects bare prefix with no body', () => {
expect(isRunId('rd_')).toBe(false);
});

it('rejects legacy wf-YYYY-MM-DD-... format', () => {
expect(isRunId('wf-2024-01-07-abc123')).toBe(false);
});

it('rejects non-string values', () => {
expect(isRunId(undefined)).toBe(false);
expect(isRunId(null)).toBe(false);
expect(isRunId(123)).toBe(false);
expect(isRunId({})).toBe(false);
});
});

describe('assertRunId', () => {
it('returns the input when valid', () => {
const raw = 'rd_4b7f0c2d9e1a4b7f0c2d9e1a4b7f0c2d';
expect(assertRunId(raw)).toBe(raw);
});

it('throws on invalid id with a message naming the expected format', () => {
expect(() => assertRunId('not-a-run-id')).toThrow(/rd_/);
});

it('throws on uppercase hex', () => {
expect(() => assertRunId('rd_ABCDEF00000000000000000000000000')).toThrow();
});

it('throws on empty string', () => {
expect(() => assertRunId('')).toThrow();
});

it('throws on bare prefix', () => {
expect(() => assertRunId('rd_')).toThrow();
});

it('is idempotent for valid ids', () => {
const raw = 'rd_22222222222222222222222222222222';
expect(assertRunId(assertRunId(raw))).toBe(raw);
});
});

describe('RUN_ID_PATTERN / RUN_ID_PREFIX', () => {
it('exports the rd_ prefix constant', () => {
expect(RUN_ID_PREFIX).toBe('rd_');
});

it('exposes a pattern that anchors both ends', () => {
expect(RUN_ID_PATTERN.source.startsWith('^')).toBe(true);
expect(RUN_ID_PATTERN.source.endsWith('$')).toBe(true);
});

it('matches values accepted by isRunId', () => {
const id = 'rd_11111111111111111111111111111111';
expect(RUN_ID_PATTERN.test(id)).toBe(true);
expect(isRunId(id)).toBe(true);
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.

it.each([
['missing rd_ prefix', '00000000000000000000000000000000'],
['uppercase hex', 'rd_ABCDEF00000000000000000000000000'],
['31-char body (too short)', 'rd_0000000000000000000000000000000'],
['33-char body (too long)', 'rd_000000000000000000000000000000000'],
['non-hex character in body', 'rd_g0000000000000000000000000000000'],
['hyphenated UUID body', 'rd_550e8400-e29b-41d4-a716-446655440000'],
])('rejects %s in both RUN_ID_PATTERN and isRunId', (_label, value) => {
expect(RUN_ID_PATTERN.test(value)).toBe(false);
expect(isRunId(value)).toBe(false);
});
});
Loading