@weldr/runr 0.3.1 → 0.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/CHANGELOG.md +150 -1
  2. package/README.md +124 -111
  3. package/dist/audit/classifier.js +331 -0
  4. package/dist/cli.js +593 -282
  5. package/dist/commands/audit.js +259 -0
  6. package/dist/commands/bundle.js +180 -0
  7. package/dist/commands/continue.js +276 -0
  8. package/dist/commands/doctor.js +430 -45
  9. package/dist/commands/hooks.js +352 -0
  10. package/dist/commands/init.js +368 -8
  11. package/dist/commands/intervene.js +109 -0
  12. package/dist/commands/journal.js +167 -0
  13. package/dist/commands/meta.js +245 -0
  14. package/dist/commands/mode.js +157 -0
  15. package/dist/commands/orchestrate.js +29 -0
  16. package/dist/commands/packs.js +47 -0
  17. package/dist/commands/preflight.js +8 -5
  18. package/dist/commands/resume.js +421 -3
  19. package/dist/commands/run.js +63 -4
  20. package/dist/commands/status.js +47 -0
  21. package/dist/commands/submit.js +374 -0
  22. package/dist/config/schema.js +61 -1
  23. package/dist/diagnosis/analyzer.js +86 -1
  24. package/dist/diagnosis/formatter.js +3 -0
  25. package/dist/diagnosis/index.js +1 -0
  26. package/dist/diagnosis/stop-explainer.js +267 -0
  27. package/dist/diagnostics/stop-explainer.js +267 -0
  28. package/dist/guards/checkpoint.js +119 -0
  29. package/dist/journal/builder.js +497 -0
  30. package/dist/journal/redactor.js +68 -0
  31. package/dist/journal/renderer.js +220 -0
  32. package/dist/journal/types.js +7 -0
  33. package/dist/orchestrator/artifacts.js +17 -2
  34. package/dist/orchestrator/receipt.js +304 -0
  35. package/dist/output/stop-footer.js +185 -0
  36. package/dist/packs/actions.js +176 -0
  37. package/dist/packs/loader.js +200 -0
  38. package/dist/packs/renderer.js +46 -0
  39. package/dist/receipt/intervention.js +465 -0
  40. package/dist/receipt/writer.js +296 -0
  41. package/dist/redaction/redactor.js +95 -0
  42. package/dist/repo/context.js +147 -20
  43. package/dist/review/check-parser.js +211 -0
  44. package/dist/store/checkpoint-metadata.js +111 -0
  45. package/dist/store/run-store.js +21 -0
  46. package/dist/supervisor/runner.js +161 -10
  47. package/dist/tasks/task-metadata.js +74 -1
  48. package/dist/ux/brain.js +528 -0
  49. package/dist/ux/render.js +123 -0
  50. package/dist/ux/safe-commands.js +133 -0
  51. package/dist/ux/state.js +193 -0
  52. package/dist/ux/telemetry.js +110 -0
  53. package/package.json +5 -1
  54. package/packs/pr/pack.json +50 -0
  55. package/packs/pr/templates/AGENTS.md.tmpl +120 -0
  56. package/packs/pr/templates/CLAUDE.md.tmpl +101 -0
  57. package/packs/pr/templates/bundle.md.tmpl +27 -0
  58. package/packs/solo/pack.json +82 -0
  59. package/packs/solo/templates/AGENTS.md.tmpl +80 -0
  60. package/packs/solo/templates/CLAUDE.md.tmpl +126 -0
  61. package/packs/solo/templates/bundle.md.tmpl +27 -0
  62. package/packs/solo/templates/claude-cmd-bundle.md.tmpl +40 -0
  63. package/packs/solo/templates/claude-cmd-resume.md.tmpl +43 -0
  64. package/packs/solo/templates/claude-cmd-submit.md.tmpl +51 -0
  65. package/packs/solo/templates/claude-skill.md.tmpl +96 -0
  66. package/packs/trunk/pack.json +50 -0
  67. package/packs/trunk/templates/AGENTS.md.tmpl +87 -0
  68. package/packs/trunk/templates/CLAUDE.md.tmpl +126 -0
  69. package/packs/trunk/templates/bundle.md.tmpl +27 -0
  70. package/dist/commands/__tests__/report.test.js +0 -202
  71. package/dist/config/__tests__/presets.test.js +0 -104
  72. package/dist/context/__tests__/artifact.test.js +0 -130
  73. package/dist/context/__tests__/pack.test.js +0 -191
  74. package/dist/env/__tests__/fingerprint.test.js +0 -116
  75. package/dist/orchestrator/__tests__/policy.test.js +0 -185
  76. package/dist/orchestrator/__tests__/schema-version.test.js +0 -65
  77. package/dist/supervisor/__tests__/evidence-gate.test.js +0 -111
  78. package/dist/supervisor/__tests__/ownership.test.js +0 -103
  79. package/dist/supervisor/__tests__/state-machine.test.js +0 -290
  80. package/dist/workers/__tests__/claude.test.js +0 -88
  81. package/dist/workers/__tests__/codex.test.js +0 -81
@@ -1,111 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { validateNoChangesEvidence, formatEvidenceErrors } from '../evidence-gate.js';
3
- describe('validateNoChangesEvidence', () => {
4
- const allowlist = ['src/game/**', 'src/utils/**'];
5
- describe('when no evidence provided', () => {
6
- it('should fail with no evidence', () => {
7
- const result = validateNoChangesEvidence(undefined, allowlist);
8
- expect(result.ok).toBe(false);
9
- expect(result.errors).toContain('No evidence provided for no_changes_needed claim');
10
- });
11
- it('should fail with empty evidence object', () => {
12
- const result = validateNoChangesEvidence({}, allowlist);
13
- expect(result.ok).toBe(false);
14
- expect(result.errors.length).toBeGreaterThan(0);
15
- });
16
- });
17
- describe('files_checked evidence', () => {
18
- it('should pass with files_checked in scope', () => {
19
- const result = validateNoChangesEvidence({ files_checked: ['src/game/combat.ts', 'src/game/utils.ts'] }, allowlist);
20
- expect(result.ok).toBe(true);
21
- expect(result.satisfied_by).toBe('files_checked');
22
- });
23
- it('should fail with files_checked out of scope', () => {
24
- const result = validateNoChangesEvidence({ files_checked: ['package.json', 'src/game/combat.ts'] }, allowlist);
25
- expect(result.ok).toBe(false);
26
- expect(result.errors.some(e => e.includes('outside scope'))).toBe(true);
27
- });
28
- it('should fail with empty files_checked array', () => {
29
- const result = validateNoChangesEvidence({ files_checked: [] }, allowlist);
30
- expect(result.ok).toBe(false);
31
- });
32
- });
33
- describe('grep_output evidence', () => {
34
- it('should pass with non-empty grep_output', () => {
35
- const result = validateNoChangesEvidence({ grep_output: 'src/game/combat.ts:42: function alreadyImplemented()' }, allowlist);
36
- expect(result.ok).toBe(true);
37
- expect(result.satisfied_by).toBe('grep_output');
38
- });
39
- it('should fail with empty grep_output', () => {
40
- const result = validateNoChangesEvidence({ grep_output: '' }, allowlist);
41
- expect(result.ok).toBe(false);
42
- });
43
- it('should fail with whitespace-only grep_output', () => {
44
- const result = validateNoChangesEvidence({ grep_output: ' \n\t ' }, allowlist);
45
- expect(result.ok).toBe(false);
46
- });
47
- });
48
- describe('commands_run evidence', () => {
49
- it('should pass with commands_run all exit_code 0', () => {
50
- const result = validateNoChangesEvidence({
51
- commands_run: [
52
- { command: 'grep -r "feature" src/', exit_code: 0 },
53
- { command: 'test -f src/feature.ts', exit_code: 0 }
54
- ]
55
- }, allowlist);
56
- expect(result.ok).toBe(true);
57
- expect(result.satisfied_by).toBe('commands_run');
58
- });
59
- it('should fail with commands_run containing non-zero exit_code', () => {
60
- const result = validateNoChangesEvidence({
61
- commands_run: [
62
- { command: 'grep -r "feature" src/', exit_code: 1 }
63
- ]
64
- }, allowlist);
65
- expect(result.ok).toBe(false);
66
- expect(result.errors.some(e => e.includes('failed commands'))).toBe(true);
67
- });
68
- it('should fail with empty commands_run array', () => {
69
- const result = validateNoChangesEvidence({ commands_run: [] }, allowlist);
70
- expect(result.ok).toBe(false);
71
- });
72
- });
73
- describe('priority order', () => {
74
- it('should prefer files_checked over grep_output', () => {
75
- const result = validateNoChangesEvidence({
76
- files_checked: ['src/game/combat.ts'],
77
- grep_output: 'some output'
78
- }, allowlist);
79
- expect(result.ok).toBe(true);
80
- expect(result.satisfied_by).toBe('files_checked');
81
- });
82
- it('should fall back to grep_output if files_checked is out of scope', () => {
83
- const result = validateNoChangesEvidence({
84
- files_checked: ['package.json'],
85
- grep_output: 'some valid output'
86
- }, allowlist);
87
- expect(result.ok).toBe(true);
88
- expect(result.satisfied_by).toBe('grep_output');
89
- });
90
- });
91
- });
92
- describe('formatEvidenceErrors', () => {
93
- it('should return empty string for ok result', () => {
94
- const result = formatEvidenceErrors({
95
- ok: true,
96
- errors: [],
97
- satisfied_by: 'files_checked'
98
- });
99
- expect(result).toBe('');
100
- });
101
- it('should format errors for failed result', () => {
102
- const result = formatEvidenceErrors({
103
- ok: false,
104
- errors: ['No evidence provided', 'files_checked is empty']
105
- });
106
- expect(result).toContain('Insufficient evidence');
107
- expect(result).toContain('No evidence provided');
108
- expect(result).toContain('files_checked is empty');
109
- expect(result).toContain('Required:');
110
- });
111
- });
@@ -1,103 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { checkOwnership } from '../runner.js';
3
- describe('checkOwnership', () => {
4
- describe('when no ownership declared', () => {
5
- it('does not enforce - always returns ok', () => {
6
- const result = checkOwnership(['README.md', 'src/index.ts', 'package.json'], [], // no ownership declared
7
- ['node_modules/**']);
8
- expect(result.ok).toBe(true);
9
- expect(result.owned_paths).toEqual([]);
10
- expect(result.semantic_changed).toEqual([]);
11
- expect(result.violating_files).toEqual([]);
12
- });
13
- it('does not enforce even with changes outside would-be owns', () => {
14
- // This is the key test: tasks without owns should continue to work unchanged
15
- const result = checkOwnership(['README.md', 'docs/guide.md', 'src/unrelated.ts'], [], // no ownership - no enforcement
16
- []);
17
- expect(result.ok).toBe(true);
18
- expect(result.violating_files).toEqual([]);
19
- });
20
- });
21
- describe('when ownership declared', () => {
22
- it('allows changes within owned paths', () => {
23
- const result = checkOwnership(['courses/a/lesson1.md', 'courses/a/lesson2.md'], ['courses/a/**'], []);
24
- expect(result.ok).toBe(true);
25
- expect(result.owned_paths).toEqual(['courses/a/**']);
26
- expect(result.semantic_changed).toEqual(['courses/a/lesson1.md', 'courses/a/lesson2.md']);
27
- expect(result.violating_files).toEqual([]);
28
- });
29
- it('detects violation when changing files outside owned paths', () => {
30
- const result = checkOwnership(['courses/a/lesson1.md', 'README.md'], ['courses/a/**'], []);
31
- expect(result.ok).toBe(false);
32
- expect(result.owned_paths).toEqual(['courses/a/**']);
33
- expect(result.semantic_changed).toEqual(['courses/a/lesson1.md', 'README.md']);
34
- expect(result.violating_files).toEqual(['README.md']);
35
- });
36
- it('reports all violating files', () => {
37
- const result = checkOwnership(['courses/a/lesson.md', 'README.md', 'docs/guide.md', 'src/index.ts'], ['courses/a/**'], []);
38
- expect(result.ok).toBe(false);
39
- expect(result.violating_files).toEqual(['README.md', 'docs/guide.md', 'src/index.ts']);
40
- });
41
- it('supports multiple owned paths', () => {
42
- const result = checkOwnership(['courses/a/lesson.md', 'docs/a.md'], ['courses/a/**', 'docs/**'], []);
43
- expect(result.ok).toBe(true);
44
- expect(result.violating_files).toEqual([]);
45
- });
46
- });
47
- describe('env artifact filtering', () => {
48
- it('excludes env artifacts from ownership check', () => {
49
- const result = checkOwnership(['courses/a/lesson.md', 'node_modules/foo/bar.js'], ['courses/a/**'], ['node_modules/**']);
50
- expect(result.ok).toBe(true);
51
- expect(result.semantic_changed).toEqual(['courses/a/lesson.md']);
52
- expect(result.violating_files).toEqual([]);
53
- });
54
- it('only checks semantic changes, not env noise', () => {
55
- // If all changes are env artifacts, no enforcement needed
56
- const result = checkOwnership(['node_modules/foo/bar.js', '.next/cache/file'], ['courses/a/**'], ['node_modules/**', '.next/**']);
57
- expect(result.ok).toBe(true);
58
- expect(result.semantic_changed).toEqual([]);
59
- expect(result.violating_files).toEqual([]);
60
- });
61
- });
62
- describe('rename handling (when both paths are included)', () => {
63
- it('blocks rename from outside into owned paths', () => {
64
- // Simulates: git mv README.md courses/a/README.md
65
- // listChangedFiles now returns BOTH paths
66
- const result = checkOwnership(['README.md', 'courses/a/README.md'], ['courses/a/**'], []);
67
- expect(result.ok).toBe(false);
68
- expect(result.violating_files).toEqual(['README.md']);
69
- });
70
- it('allows rename within owned paths', () => {
71
- // Simulates: git mv courses/a/old.md courses/a/new.md
72
- const result = checkOwnership(['courses/a/old.md', 'courses/a/new.md'], ['courses/a/**'], []);
73
- expect(result.ok).toBe(true);
74
- expect(result.violating_files).toEqual([]);
75
- });
76
- it('blocks rename from owned to outside', () => {
77
- // Simulates: git mv courses/a/file.md README.md
78
- const result = checkOwnership(['courses/a/file.md', 'README.md'], ['courses/a/**'], []);
79
- expect(result.ok).toBe(false);
80
- expect(result.violating_files).toEqual(['README.md']);
81
- });
82
- });
83
- describe('defensive normalization', () => {
84
- it('normalizes raw directory paths to glob patterns', () => {
85
- // Caller passes raw "courses/a/" instead of normalized "courses/a/**"
86
- const result = checkOwnership(['courses/a/lesson.md'], ['courses/a/'], // raw, not normalized
87
- []);
88
- expect(result.ok).toBe(true);
89
- expect(result.owned_paths).toEqual(['courses/a/**']); // normalized in output
90
- });
91
- it('normalizes Windows-style paths', () => {
92
- const result = checkOwnership(['courses/a/lesson.md'], ['courses\\a\\'], // Windows-style
93
- []);
94
- expect(result.ok).toBe(true);
95
- expect(result.owned_paths).toEqual(['courses/a/**']);
96
- });
97
- it('handles ./prefix in ownership patterns', () => {
98
- const result = checkOwnership(['courses/a/lesson.md'], ['./courses/a/'], []);
99
- expect(result.ok).toBe(true);
100
- expect(result.owned_paths).toEqual(['courses/a/**']);
101
- });
102
- });
103
- });
@@ -1,290 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { createInitialState, updatePhase, stopRun, computeResumeTargetPhase, prepareForResume } from '../state-machine.js';
3
- describe('createInitialState', () => {
4
- it('creates state with INIT phase', () => {
5
- const state = createInitialState({
6
- run_id: 'test-run-123',
7
- repo_path: '/test/repo',
8
- task_text: 'Test task',
9
- allowlist: ['src/**'],
10
- denylist: ['node_modules/**']
11
- });
12
- expect(state.run_id).toBe('test-run-123');
13
- expect(state.repo_path).toBe('/test/repo');
14
- expect(state.phase).toBe('INIT');
15
- expect(state.milestone_index).toBe(0);
16
- expect(state.scope_lock.allowlist).toEqual(['src/**']);
17
- expect(state.scope_lock.denylist).toEqual(['node_modules/**']);
18
- expect(state.risk_score).toBe(0);
19
- expect(state.retries).toBe(0);
20
- });
21
- it('parses task text into milestones', () => {
22
- const state = createInitialState({
23
- run_id: 'test',
24
- repo_path: '/test',
25
- task_text: 'Add a new feature\n\nMore details here',
26
- allowlist: [],
27
- denylist: []
28
- });
29
- expect(state.milestones.length).toBeGreaterThan(0);
30
- expect(state.milestones[0].goal).toBe('Add a new feature');
31
- });
32
- });
33
- describe('updatePhase', () => {
34
- it('updates phase and tracks last_successful_phase as previous phase', () => {
35
- const initial = createInitialState({
36
- run_id: 'test',
37
- repo_path: '/test',
38
- task_text: 'Test',
39
- allowlist: [],
40
- denylist: []
41
- });
42
- const updated = updatePhase(initial, 'PLAN');
43
- expect(updated.phase).toBe('PLAN');
44
- // last_successful_phase tracks the previous phase (INIT in this case)
45
- expect(updated.last_successful_phase).toBe('INIT');
46
- // updated_at is set (may be same as initial if test runs in same ms)
47
- expect(typeof updated.updated_at).toBe('string');
48
- expect(updated.updated_at.length).toBeGreaterThan(0);
49
- });
50
- it('preserves other state properties', () => {
51
- const initial = createInitialState({
52
- run_id: 'preserve-test',
53
- repo_path: '/preserve',
54
- task_text: 'Preserve task',
55
- allowlist: ['src/**'],
56
- denylist: ['dist/**']
57
- });
58
- const updated = updatePhase(initial, 'IMPLEMENT');
59
- expect(updated.run_id).toBe('preserve-test');
60
- expect(updated.repo_path).toBe('/preserve');
61
- expect(updated.scope_lock.allowlist).toEqual(['src/**']);
62
- });
63
- });
64
- describe('stopRun', () => {
65
- it('sets phase to STOPPED and records reason', () => {
66
- const initial = createInitialState({
67
- run_id: 'stop-test',
68
- repo_path: '/test',
69
- task_text: 'Test',
70
- allowlist: [],
71
- denylist: []
72
- });
73
- const stopped = stopRun(initial, 'verification_failed');
74
- expect(stopped.phase).toBe('STOPPED');
75
- expect(stopped.stop_reason).toBe('verification_failed');
76
- });
77
- it('preserves state when stopping', () => {
78
- let state = createInitialState({
79
- run_id: 'stop-preserve',
80
- repo_path: '/test',
81
- task_text: 'Test',
82
- allowlist: ['src/**'],
83
- denylist: []
84
- });
85
- state = updatePhase(state, 'IMPLEMENT');
86
- state = { ...state, milestone_index: 2 };
87
- const stopped = stopRun(state, 'complete');
88
- expect(stopped.milestone_index).toBe(2);
89
- expect(stopped.scope_lock.allowlist).toEqual(['src/**']);
90
- });
91
- });
92
- describe('phase ordering', () => {
93
- it('follows expected phase progression for success path', () => {
94
- const phases = [];
95
- let state = createInitialState({
96
- run_id: 'phase-order',
97
- repo_path: '/test',
98
- task_text: 'Test',
99
- allowlist: [],
100
- denylist: []
101
- });
102
- phases.push(state.phase);
103
- state = updatePhase(state, 'PLAN');
104
- phases.push(state.phase);
105
- state = updatePhase(state, 'IMPLEMENT');
106
- phases.push(state.phase);
107
- state = updatePhase(state, 'VERIFY');
108
- phases.push(state.phase);
109
- state = updatePhase(state, 'REVIEW');
110
- phases.push(state.phase);
111
- state = updatePhase(state, 'CHECKPOINT');
112
- phases.push(state.phase);
113
- state = updatePhase(state, 'FINALIZE');
114
- phases.push(state.phase);
115
- expect(phases).toEqual([
116
- 'INIT',
117
- 'PLAN',
118
- 'IMPLEMENT',
119
- 'VERIFY',
120
- 'REVIEW',
121
- 'CHECKPOINT',
122
- 'FINALIZE'
123
- ]);
124
- });
125
- it('can loop back to IMPLEMENT after CHECKPOINT for multiple milestones', () => {
126
- let state = createInitialState({
127
- run_id: 'loop-test',
128
- repo_path: '/test',
129
- task_text: 'Multi milestone task',
130
- allowlist: [],
131
- denylist: []
132
- });
133
- // First milestone
134
- state = updatePhase(state, 'PLAN');
135
- state = updatePhase(state, 'IMPLEMENT');
136
- state = updatePhase(state, 'VERIFY');
137
- state = updatePhase(state, 'REVIEW');
138
- state = updatePhase(state, 'CHECKPOINT');
139
- state = { ...state, milestone_index: 1 };
140
- // Second milestone - loops back to IMPLEMENT
141
- state = updatePhase(state, 'IMPLEMENT');
142
- expect(state.phase).toBe('IMPLEMENT');
143
- expect(state.milestone_index).toBe(1);
144
- state = updatePhase(state, 'VERIFY');
145
- state = updatePhase(state, 'REVIEW');
146
- state = updatePhase(state, 'CHECKPOINT');
147
- state = { ...state, milestone_index: 2 };
148
- // Finalize after all milestones
149
- state = updatePhase(state, 'FINALIZE');
150
- expect(state.phase).toBe('FINALIZE');
151
- expect(state.milestone_index).toBe(2);
152
- });
153
- it('stops on verification failure', () => {
154
- let state = createInitialState({
155
- run_id: 'verify-fail',
156
- repo_path: '/test',
157
- task_text: 'Test',
158
- allowlist: [],
159
- denylist: []
160
- });
161
- state = updatePhase(state, 'PLAN');
162
- state = updatePhase(state, 'IMPLEMENT');
163
- state = updatePhase(state, 'VERIFY');
164
- // Verification fails - stop run
165
- state = stopRun(state, 'verification_failed');
166
- expect(state.phase).toBe('STOPPED');
167
- expect(state.stop_reason).toBe('verification_failed');
168
- // last_successful_phase is set by updatePhase (previous phase), not stopRun
169
- expect(state.last_successful_phase).toBe('IMPLEMENT');
170
- });
171
- });
172
- describe('computeResumeTargetPhase', () => {
173
- it('returns current phase if not STOPPED', () => {
174
- const state = createInitialState({
175
- run_id: 'test',
176
- repo_path: '/test',
177
- task_text: 'Test',
178
- allowlist: [],
179
- denylist: []
180
- });
181
- const updated = updatePhase(state, 'IMPLEMENT');
182
- expect(computeResumeTargetPhase(updated)).toBe('IMPLEMENT');
183
- });
184
- it('returns next phase after last_successful_phase when STOPPED', () => {
185
- let state = createInitialState({
186
- run_id: 'test',
187
- repo_path: '/test',
188
- task_text: 'Test',
189
- allowlist: [],
190
- denylist: []
191
- });
192
- state = updatePhase(state, 'PLAN');
193
- state = updatePhase(state, 'IMPLEMENT');
194
- state = updatePhase(state, 'VERIFY');
195
- state = stopRun(state, 'stalled_timeout');
196
- // last_successful_phase is IMPLEMENT (from the VERIFY transition)
197
- expect(state.last_successful_phase).toBe('IMPLEMENT');
198
- expect(computeResumeTargetPhase(state)).toBe('VERIFY');
199
- });
200
- it('returns INIT when STOPPED with no last_successful_phase', () => {
201
- const state = createInitialState({
202
- run_id: 'test',
203
- repo_path: '/test',
204
- task_text: 'Test',
205
- allowlist: [],
206
- denylist: []
207
- });
208
- const stopped = stopRun(state, 'guard_violation');
209
- expect(computeResumeTargetPhase(stopped)).toBe('INIT');
210
- });
211
- });
212
- describe('prepareForResume', () => {
213
- it('clears stop state and sets resume phase', () => {
214
- let state = createInitialState({
215
- run_id: 'test',
216
- repo_path: '/test',
217
- task_text: 'Test',
218
- allowlist: [],
219
- denylist: []
220
- });
221
- // Simulate normal phase progression: INIT -> PLAN -> IMPLEMENT -> VERIFY
222
- // Each updatePhase sets last_successful_phase to the PREVIOUS phase
223
- state = updatePhase(state, 'PLAN'); // last_successful_phase = 'INIT'
224
- state = updatePhase(state, 'IMPLEMENT'); // last_successful_phase = 'PLAN'
225
- state = updatePhase(state, 'VERIFY'); // last_successful_phase = 'IMPLEMENT'
226
- state = stopRun(state, 'stalled_timeout');
227
- state = { ...state, last_error: 'Some error' };
228
- const resumed = prepareForResume(state);
229
- // Resume from phase after last_successful_phase ('IMPLEMENT' -> 'VERIFY')
230
- expect(resumed.phase).toBe('VERIFY');
231
- expect(resumed.stop_reason).toBeUndefined();
232
- expect(resumed.last_error).toBeUndefined();
233
- expect(resumed.resume_token).toBe('test');
234
- });
235
- it('increments auto_resume_count when requested', () => {
236
- let state = createInitialState({
237
- run_id: 'test',
238
- repo_path: '/test',
239
- task_text: 'Test',
240
- allowlist: [],
241
- denylist: []
242
- });
243
- state = stopRun(state, 'stalled_timeout');
244
- const resumed = prepareForResume(state, { incrementAutoResumeCount: true });
245
- expect(resumed.auto_resume_count).toBe(1);
246
- // Resume again
247
- const stoppedAgain = stopRun(resumed, 'worker_call_timeout');
248
- const resumedAgain = prepareForResume(stoppedAgain, { incrementAutoResumeCount: true });
249
- expect(resumedAgain.auto_resume_count).toBe(2);
250
- });
251
- it('preserves auto_resume_count when not incrementing', () => {
252
- let state = createInitialState({
253
- run_id: 'test',
254
- repo_path: '/test',
255
- task_text: 'Test',
256
- allowlist: [],
257
- denylist: []
258
- });
259
- state = { ...state, auto_resume_count: 3 };
260
- state = stopRun(state, 'stalled_timeout');
261
- const resumed = prepareForResume(state);
262
- expect(resumed.auto_resume_count).toBe(3);
263
- });
264
- it('handles missing auto_resume_count (migration-safe)', () => {
265
- let state = createInitialState({
266
- run_id: 'test',
267
- repo_path: '/test',
268
- task_text: 'Test',
269
- allowlist: [],
270
- denylist: []
271
- });
272
- // Simulate old state without auto_resume_count
273
- delete state.auto_resume_count;
274
- state = stopRun(state, 'stalled_timeout');
275
- const resumed = prepareForResume(state, { incrementAutoResumeCount: true });
276
- expect(resumed.auto_resume_count).toBe(1);
277
- });
278
- it('uses custom resumeToken when provided', () => {
279
- let state = createInitialState({
280
- run_id: 'original-run',
281
- repo_path: '/test',
282
- task_text: 'Test',
283
- allowlist: [],
284
- denylist: []
285
- });
286
- state = stopRun(state, 'stalled_timeout');
287
- const resumed = prepareForResume(state, { resumeToken: 'custom-token' });
288
- expect(resumed.resume_token).toBe('custom-token');
289
- });
290
- });
@@ -1,88 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- function extractTextFromClaudeJson(output) {
3
- try {
4
- const parsed = JSON.parse(output);
5
- return parsed.result || parsed.content || parsed.message || output;
6
- }
7
- catch {
8
- // If not valid JSON, return raw output
9
- return output;
10
- }
11
- }
12
- describe('extractTextFromClaudeJson', () => {
13
- it('extracts result field from valid JSON', () => {
14
- const input = '{"result":"Hello World"}';
15
- expect(extractTextFromClaudeJson(input)).toBe('Hello World');
16
- });
17
- it('extracts content field when result is missing', () => {
18
- const input = '{"content":"Content text","other":"ignored"}';
19
- expect(extractTextFromClaudeJson(input)).toBe('Content text');
20
- });
21
- it('extracts message field as fallback', () => {
22
- const input = '{"message":"Message text"}';
23
- expect(extractTextFromClaudeJson(input)).toBe('Message text');
24
- });
25
- it('returns raw output when not valid JSON', () => {
26
- const input = 'This is plain text, not JSON';
27
- expect(extractTextFromClaudeJson(input)).toBe('This is plain text, not JSON');
28
- });
29
- it('returns raw output for truncated JSON', () => {
30
- const input = '{"result":"trun';
31
- expect(extractTextFromClaudeJson(input)).toBe('{"result":"trun');
32
- });
33
- it('returns raw output for empty object with no known fields', () => {
34
- const input = '{"unknown":"field"}';
35
- // Returns the original input since result/content/message are all falsy
36
- expect(extractTextFromClaudeJson(input)).toBe('{"unknown":"field"}');
37
- });
38
- it('handles empty string result', () => {
39
- const input = '{"result":"","content":"fallback"}';
40
- // Empty string is falsy, so falls through to content
41
- expect(extractTextFromClaudeJson(input)).toBe('fallback');
42
- });
43
- it('handles null values', () => {
44
- const input = '{"result":null,"content":"actual content"}';
45
- expect(extractTextFromClaudeJson(input)).toBe('actual content');
46
- });
47
- it('handles nested JSON in result', () => {
48
- const input = '{"result":"{\\"nested\\":\\"json\\"}"}';
49
- expect(extractTextFromClaudeJson(input)).toBe('{"nested":"json"}');
50
- });
51
- it('handles array response gracefully', () => {
52
- const input = '["array","response"]';
53
- // Arrays don't have result/content/message, returns original
54
- expect(extractTextFromClaudeJson(input)).toBe('["array","response"]');
55
- });
56
- it('handles error field in response', () => {
57
- const input = '{"error":"Something went wrong","result":""}';
58
- // Empty result is falsy, but we don't currently use error field
59
- // This returns the raw input since result/content/message are empty/missing
60
- expect(extractTextFromClaudeJson(input)).toBe('{"error":"Something went wrong","result":""}');
61
- });
62
- it('handles whitespace in output', () => {
63
- const input = ' {"result":"with spaces"} ';
64
- // JSON.parse handles leading/trailing whitespace
65
- expect(extractTextFromClaudeJson(input)).toBe('with spaces');
66
- });
67
- it('handles newlines in result text', () => {
68
- const input = '{"result":"line1\\nline2\\nline3"}';
69
- expect(extractTextFromClaudeJson(input)).toBe('line1\nline2\nline3');
70
- });
71
- });
72
- describe('Claude JSON error handling', () => {
73
- it('fails loud on completely invalid input', () => {
74
- // Current implementation returns the raw output, which is reasonable
75
- // but we might want to distinguish "valid text" from "parse error" later
76
- const input = '}{invalid';
77
- const result = extractTextFromClaudeJson(input);
78
- // Currently returns raw - this is acceptable behavior
79
- expect(result).toBe('}{invalid');
80
- });
81
- it('handles BOM characters', () => {
82
- const input = '\uFEFF{"result":"with BOM"}';
83
- // JSON.parse may fail with BOM prefix
84
- const result = extractTextFromClaudeJson(input);
85
- // Should either parse successfully or return raw
86
- expect(result.includes('with BOM') || result.includes('\uFEFF')).toBe(true);
87
- });
88
- });