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
6 changes: 5 additions & 1 deletion src/automation/autonomousRunner.cancel.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,11 @@ describe('AutonomousRunner cancellation state sync', () => {
beforeEach(async () => {
vi.resetModules();
tempDir = mkdtempSync(join(tmpdir(), 'openswarm-cancel-sync-'));
vi.stubEnv('OPENSWARM_TASK_STATE_FILE', join(tempDir, 'task-state.json'));
vi.stubEnv('OPENSWARM_TASK_STATE_FILE', join(tempDir, 'task-state.json')); // canonical store (taskState/store)
vi.stubEnv('OPENSWARM_RUNNER_TASK_STATE_FILE', join(tempDir, 'runner-task-state.json')); // legacy runnerState
vi.stubEnv('OPENSWARM_RUNNER_REJECTION_STATE_FILE', join(tempDir, 'runner-rejection-state.json'));
vi.stubEnv('OPENSWARM_RUNNER_PIPELINE_HISTORY_FILE', join(tempDir, 'runner-pipeline-history.json'));
vi.stubEnv('OPENSWARM_RUNNER_DECOMPOSITION_STATE_FILE', join(tempDir, 'runner-decomposition-state.json'));
({ AutonomousRunner } = await import('./autonomousRunner.js'));
runnerExecution = await import('./runnerExecution.js');
taskStateStore = await import('../taskState/store.js');
Expand Down
6 changes: 5 additions & 1 deletion src/automation/autonomousRunner.infraError.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,11 @@ describe('AutonomousRunner infra_error handling (INT-2010)', () => {
beforeEach(async () => {
vi.resetModules();
tempDir = mkdtempSync(join(tmpdir(), 'openswarm-infra-'));
vi.stubEnv('OPENSWARM_TASK_STATE_FILE', join(tempDir, 'task-state.json'));
vi.stubEnv('OPENSWARM_TASK_STATE_FILE', join(tempDir, 'task-state.json')); // canonical store (taskState/store)
vi.stubEnv('OPENSWARM_RUNNER_TASK_STATE_FILE', join(tempDir, 'runner-task-state.json')); // legacy runnerState
vi.stubEnv('OPENSWARM_RUNNER_REJECTION_STATE_FILE', join(tempDir, 'runner-rejection-state.json'));
vi.stubEnv('OPENSWARM_RUNNER_PIPELINE_HISTORY_FILE', join(tempDir, 'runner-pipeline-history.json'));
vi.stubEnv('OPENSWARM_RUNNER_DECOMPOSITION_STATE_FILE', join(tempDir, 'runner-decomposition-state.json'));
({ AutonomousRunner } = await import('./autonomousRunner.js'));
runnerExecution = await import('./runnerExecution.js');
}, 30000);
Expand Down
60 changes: 60 additions & 0 deletions src/automation/runnerState.coverage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ async function loadFreshModule() {
tempHome = mkdtempSync(join(tmpdir(), 'openswarm-runner-state-'));
vi.stubEnv('HOME', tempHome);
vi.stubEnv('USERPROFILE', tempHome);
// Clear any RUNNER_* overrides inherited from the shell (e.g. CI) so every path
// derives from tempHome, independent of the ambient environment. (INT-2543)
for (const v of ['OPENSWARM_RUNNER_TASK_STATE_FILE', 'OPENSWARM_RUNNER_REJECTION_STATE_FILE',
'OPENSWARM_RUNNER_PIPELINE_HISTORY_FILE', 'OPENSWARM_RUNNER_DECOMPOSITION_STATE_FILE']) {
vi.stubEnv(v, '');
}
mod = await import('./runnerState.js');
}

Expand Down Expand Up @@ -206,3 +212,57 @@ it('pickFailureDetail skips locale noSummary fallbacks (INT-2504 follow-up)', ()
expect(mod.pickFailureDetail(['(no summary)', 'real feedback here'])).toBe('real feedback here');
expect(mod.pickFailureDetail(['(요약 없음)'])).toBeUndefined();
});

describe('state-file env overrides (INT-2543)', () => {
afterEach(() => vi.unstubAllEnvs());

it('honors the OPENSWARM_RUNNER_* overrides for all four state files (test isolation)', async () => {
vi.resetModules();
const taskPath = join(tmpdir(), 'ovr-int2543-task.json');
const rejPath = join(tmpdir(), 'ovr-int2543-rej.json');
const histPath = join(tmpdir(), 'ovr-int2543-hist.json');
const decompPath = join(tmpdir(), 'ovr-int2543-decomp.json');
vi.stubEnv('OPENSWARM_RUNNER_TASK_STATE_FILE', taskPath);
vi.stubEnv('OPENSWARM_RUNNER_REJECTION_STATE_FILE', rejPath);
vi.stubEnv('OPENSWARM_RUNNER_PIPELINE_HISTORY_FILE', histPath);
vi.stubEnv('OPENSWARM_RUNNER_DECOMPOSITION_STATE_FILE', decompPath);
const m = await import('./runnerState.js');
expect(m.TASK_STATE_FILE).toBe(taskPath);
expect(m.REJECTION_STATE_FILE).toBe(rejPath);
expect(m.PIPELINE_HISTORY_FILE).toBe(histPath);
expect(m.DECOMPOSITION_STATE_FILE).toBe(decompPath);
});

it('appendPipelineHistory writes ONLY to the overridden pipeline-history file', async () => {
vi.resetModules();
const histPath = join(tmpdir(), `ovr-int2543-hist-${process.pid}-write.json`);
if (existsSync(histPath)) rmSync(histPath);
vi.stubEnv('OPENSWARM_RUNNER_PIPELINE_HISTORY_FILE', histPath);
vi.stubEnv('HOME', mkdtempSync(join(tmpdir(), 'oh-'))); // guard: any leak lands here, not real ~/.claude
const m = await import('./runnerState.js');
m.appendPipelineHistory({ taskId: 't1', issueId: 'INT-X', title: 'x', finalStatus: 'approved', success: true, durationMs: 1, at: '2026-01-01T00:00:00Z' } as any);
expect(existsSync(histPath)).toBe(true); // the override path got the write
expect(JSON.parse(readFileSync(histPath, 'utf8')).length).toBe(1);
});

it('does NOT reuse the canonical store env var (would clobber taskState/store schema)', async () => {
vi.resetModules();
// Setting ONLY the canonical store's var must not move the legacy runner path —
// sharing it would let one store overwrite the other's differently-schema'd file.
vi.stubEnv('OPENSWARM_TASK_STATE_FILE', join(tmpdir(), 'canonical-store.json'));
vi.stubEnv('OPENSWARM_RUNNER_TASK_STATE_FILE', ''); // independent of ambient shell env
vi.stubEnv('HOME', '/tmp/fake-home-int2543');
vi.stubEnv('USERPROFILE', '/tmp/fake-home-int2543');
const m = await import('./runnerState.js');
expect(m.TASK_STATE_FILE).toBe('/tmp/fake-home-int2543/.claude/openswarm-task-state.json');
});

it('falls back to the ~/.claude default when the override is unset (backward compatible)', async () => {
vi.resetModules();
vi.stubEnv('OPENSWARM_RUNNER_TASK_STATE_FILE', ''); // empty → falsy → fallback
vi.stubEnv('HOME', '/tmp/fake-home-int2543');
vi.stubEnv('USERPROFILE', '/tmp/fake-home-int2543');
const m = await import('./runnerState.js');
expect(m.TASK_STATE_FILE).toBe('/tmp/fake-home-int2543/.claude/openswarm-task-state.json');
});
});
17 changes: 13 additions & 4 deletions src/automation/runnerState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,19 @@ export function isPathEnabled(resolvedPath: string, enabledProjects: Set<string>
return false;
}

export const TASK_STATE_FILE = join(homedir(), '.claude', 'openswarm-task-state.json');
export const PIPELINE_HISTORY_FILE = join(homedir(), '.claude', 'openswarm-pipeline-history.json');
export const REJECTION_STATE_FILE = join(homedir(), '.claude', 'openswarm-rejection-state.json');
export const DECOMPOSITION_STATE_FILE = join(homedir(), '.claude', 'openswarm-decomposition-state.json');
// State-file paths honor env overrides so tests (and alternate deployments) can
// redirect them off the real ~/.claude state — an unset override keeps the exact
// legacy path (backward compatible). Without this, every runner integration test
// wrote the LIVE daemon's state files (observed: ISSUE-1 accrued 184 phantom failures),
// which also made those tests flaky. Read at import; tests stub the env then re-import.
// NOTE: these use OPENSWARM_RUNNER_* names, deliberately DISTINCT from the canonical
// task store's OPENSWARM_TASK_STATE_FILE (src/taskState/store.ts) — this legacy runner
// state has a different schema ({completed,failed,retryTimes,…}), so sharing the env
// var would let one store silently overwrite the other's file. (INT-2543)
export const TASK_STATE_FILE = process.env.OPENSWARM_RUNNER_TASK_STATE_FILE || join(homedir(), '.claude', 'openswarm-task-state.json');
export const PIPELINE_HISTORY_FILE = process.env.OPENSWARM_RUNNER_PIPELINE_HISTORY_FILE || join(homedir(), '.claude', 'openswarm-pipeline-history.json');
export const REJECTION_STATE_FILE = process.env.OPENSWARM_RUNNER_REJECTION_STATE_FILE || join(homedir(), '.claude', 'openswarm-rejection-state.json');
export const DECOMPOSITION_STATE_FILE = process.env.OPENSWARM_RUNNER_DECOMPOSITION_STATE_FILE || join(homedir(), '.claude', 'openswarm-decomposition-state.json');
export const DAILY_PACE_FILE = join(homedir(), '.openswarm', 'daily-pace.json');
export const PROJECT_SELECTION_FILE = join(homedir(), '.openswarm', 'project-selection.json');
const MAX_PIPELINE_HISTORY = 100;
Expand Down
Loading