anvil-dev-framework 0.1.6
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.
- package/README.md +719 -0
- package/VERSION +1 -0
- package/docs/ANVIL-REPO-IMPLEMENTATION-PLAN.md +441 -0
- package/docs/FIRST-SKILL-TUTORIAL.md +408 -0
- package/docs/INSTALLATION-RETRO-NOTES.md +458 -0
- package/docs/INSTALLATION.md +984 -0
- package/docs/anvil-hud.md +469 -0
- package/docs/anvil-init.md +255 -0
- package/docs/anvil-state.md +210 -0
- package/docs/boris-cherny-ralph-wiggum-insights.md +608 -0
- package/docs/command-reference.md +2022 -0
- package/docs/hooks-tts.md +368 -0
- package/docs/implementation-guide.md +810 -0
- package/docs/linear-github-integration.md +247 -0
- package/docs/local-issues.md +677 -0
- package/docs/patterns/README.md +419 -0
- package/docs/planning-responsibilities.md +139 -0
- package/docs/session-workflow.md +573 -0
- package/docs/simplification-plan-template.md +297 -0
- package/docs/simplification-principles.md +129 -0
- package/docs/specifications/CCS-RALPH-INTEGRATION-DESIGN.md +633 -0
- package/docs/specifications/CCS-RESEARCH-REPORT.md +169 -0
- package/docs/specifications/PLAN-ANV-verification-ralph-wiggum.md +403 -0
- package/docs/specifications/PLAN-parallel-tracks-anvil-memory-ccs.md +494 -0
- package/docs/specifications/SPEC-ANV-VRW/component-01-verify.md +208 -0
- package/docs/specifications/SPEC-ANV-VRW/component-02-stop-gate.md +226 -0
- package/docs/specifications/SPEC-ANV-VRW/component-03-posttooluse.md +209 -0
- package/docs/specifications/SPEC-ANV-VRW/component-04-ralph-wiggum.md +604 -0
- package/docs/specifications/SPEC-ANV-VRW/component-05-atomic-actions.md +311 -0
- package/docs/specifications/SPEC-ANV-VRW/component-06-verify-subagent.md +264 -0
- package/docs/specifications/SPEC-ANV-VRW/component-07-claude-md.md +363 -0
- package/docs/specifications/SPEC-ANV-VRW/index.md +182 -0
- package/docs/specifications/SPEC-ANV-anvil-memory.md +573 -0
- package/docs/specifications/SPEC-ANV-context-checkpoints.md +781 -0
- package/docs/specifications/SPEC-ANV-verification-ralph-wiggum.md +789 -0
- package/docs/sync.md +122 -0
- package/global/CLAUDE.md +140 -0
- package/global/agents/verify-app.md +164 -0
- package/global/commands/anvil-settings.md +527 -0
- package/global/commands/anvil-sync.md +121 -0
- package/global/commands/change.md +197 -0
- package/global/commands/clarify.md +252 -0
- package/global/commands/cleanup.md +292 -0
- package/global/commands/commit-push-pr.md +207 -0
- package/global/commands/decay-review.md +127 -0
- package/global/commands/discover.md +158 -0
- package/global/commands/doc-coverage.md +122 -0
- package/global/commands/evidence.md +307 -0
- package/global/commands/explore.md +121 -0
- package/global/commands/force-exit.md +135 -0
- package/global/commands/handoff.md +191 -0
- package/global/commands/healthcheck.md +302 -0
- package/global/commands/hud.md +84 -0
- package/global/commands/insights.md +319 -0
- package/global/commands/linear-setup.md +184 -0
- package/global/commands/lint-fix.md +198 -0
- package/global/commands/orient.md +510 -0
- package/global/commands/plan.md +228 -0
- package/global/commands/ralph.md +346 -0
- package/global/commands/ready.md +182 -0
- package/global/commands/release.md +305 -0
- package/global/commands/retro.md +96 -0
- package/global/commands/shard.md +166 -0
- package/global/commands/spec.md +227 -0
- package/global/commands/sprint.md +184 -0
- package/global/commands/tasks.md +228 -0
- package/global/commands/test-and-commit.md +151 -0
- package/global/commands/validate.md +132 -0
- package/global/commands/verify.md +251 -0
- package/global/commands/weekly-review.md +156 -0
- package/global/hooks/__pycache__/ralph_context_monitor.cpython-314.pyc +0 -0
- package/global/hooks/__pycache__/statusline_agent_sync.cpython-314.pyc +0 -0
- package/global/hooks/anvil_memory_observe.ts +322 -0
- package/global/hooks/anvil_memory_session.ts +166 -0
- package/global/hooks/anvil_memory_stop.ts +187 -0
- package/global/hooks/parse_transcript.py +116 -0
- package/global/hooks/post_merge_cleanup.sh +132 -0
- package/global/hooks/post_tool_format.sh +215 -0
- package/global/hooks/ralph_context_monitor.py +240 -0
- package/global/hooks/ralph_stop.sh +502 -0
- package/global/hooks/statusline.sh +1110 -0
- package/global/hooks/statusline_agent_sync.py +224 -0
- package/global/hooks/stop_gate.sh +250 -0
- package/global/lib/.claude/anvil-state.json +21 -0
- package/global/lib/__pycache__/agent_registry.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/claim_service.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/coderabbit_service.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/config_service.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/coordination_service.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/doc_coverage_service.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/gate_logger.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/github_service.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/hygiene_service.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/issue_models.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/issue_provider.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/linear_data_service.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/linear_provider.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/local_provider.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/quality_service.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/ralph_state.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/state_manager.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/transcript_parser.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/verification_runner.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/verify_iteration.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/verify_subagent.cpython-314.pyc +0 -0
- package/global/lib/agent_registry.py +995 -0
- package/global/lib/anvil-state.sh +435 -0
- package/global/lib/claim_service.py +515 -0
- package/global/lib/coderabbit_service.py +314 -0
- package/global/lib/config_service.py +423 -0
- package/global/lib/coordination_service.py +331 -0
- package/global/lib/doc_coverage_service.py +1305 -0
- package/global/lib/gate_logger.py +316 -0
- package/global/lib/github_service.py +310 -0
- package/global/lib/handoff_generator.py +775 -0
- package/global/lib/hygiene_service.py +712 -0
- package/global/lib/issue_models.py +257 -0
- package/global/lib/issue_provider.py +339 -0
- package/global/lib/linear_data_service.py +210 -0
- package/global/lib/linear_provider.py +987 -0
- package/global/lib/linear_provider.py.backup +671 -0
- package/global/lib/local_provider.py +486 -0
- package/global/lib/orient_fast.py +457 -0
- package/global/lib/quality_service.py +470 -0
- package/global/lib/ralph_prompt_generator.py +563 -0
- package/global/lib/ralph_state.py +1202 -0
- package/global/lib/state_manager.py +417 -0
- package/global/lib/transcript_parser.py +597 -0
- package/global/lib/verification_runner.py +557 -0
- package/global/lib/verify_iteration.py +490 -0
- package/global/lib/verify_subagent.py +250 -0
- package/global/skills/README.md +155 -0
- package/global/skills/quality-gates/SKILL.md +252 -0
- package/global/skills/skill-template/SKILL.md +109 -0
- package/global/skills/testing-strategies/SKILL.md +337 -0
- package/global/templates/CHANGE-template.md +105 -0
- package/global/templates/HANDOFF-template.md +63 -0
- package/global/templates/PLAN-template.md +111 -0
- package/global/templates/SPEC-template.md +93 -0
- package/global/templates/ralph/PROMPT.md.template +89 -0
- package/global/templates/ralph/fix_plan.md.template +31 -0
- package/global/templates/ralph/progress.txt.template +23 -0
- package/global/tests/__pycache__/test_doc_coverage.cpython-314.pyc +0 -0
- package/global/tests/test_doc_coverage.py +520 -0
- package/global/tests/test_issue_models.py +299 -0
- package/global/tests/test_local_provider.py +323 -0
- package/global/tools/README.md +178 -0
- package/global/tools/__pycache__/anvil-hud.cpython-314.pyc +0 -0
- package/global/tools/anvil-hud.py +3622 -0
- package/global/tools/anvil-hud.py.bak +3318 -0
- package/global/tools/anvil-issue.py +432 -0
- package/global/tools/anvil-memory/CLAUDE.md +49 -0
- package/global/tools/anvil-memory/README.md +42 -0
- package/global/tools/anvil-memory/bun.lock +25 -0
- package/global/tools/anvil-memory/bunfig.toml +9 -0
- package/global/tools/anvil-memory/package.json +23 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/context-monitor.test.ts +535 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/edge-cases.test.ts +645 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/fixtures.ts +363 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/index.ts +8 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/integration.test.ts +417 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/prompt-generator.test.ts +571 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/ralph-stop.test.ts +440 -0
- package/global/tools/anvil-memory/src/__tests__/ccs/test-utils.ts +252 -0
- package/global/tools/anvil-memory/src/__tests__/commands.test.ts +657 -0
- package/global/tools/anvil-memory/src/__tests__/db.test.ts +641 -0
- package/global/tools/anvil-memory/src/__tests__/hooks.test.ts +272 -0
- package/global/tools/anvil-memory/src/__tests__/performance.test.ts +427 -0
- package/global/tools/anvil-memory/src/__tests__/test-utils.ts +113 -0
- package/global/tools/anvil-memory/src/commands/checkpoint.ts +197 -0
- package/global/tools/anvil-memory/src/commands/get.ts +115 -0
- package/global/tools/anvil-memory/src/commands/init.ts +94 -0
- package/global/tools/anvil-memory/src/commands/observe.ts +163 -0
- package/global/tools/anvil-memory/src/commands/search.ts +112 -0
- package/global/tools/anvil-memory/src/db.ts +638 -0
- package/global/tools/anvil-memory/src/index.ts +205 -0
- package/global/tools/anvil-memory/src/types.ts +122 -0
- package/global/tools/anvil-memory/tsconfig.json +29 -0
- package/global/tools/ralph-loop.sh +359 -0
- package/package.json +45 -0
- package/scripts/anvil +822 -0
- package/scripts/extract_patterns.py +222 -0
- package/scripts/init-project.sh +541 -0
- package/scripts/install.sh +229 -0
- package/scripts/postinstall.js +41 -0
- package/scripts/rollback.sh +188 -0
- package/scripts/sync.sh +623 -0
- package/scripts/test-statusline.sh +248 -0
- package/scripts/update_claude_md.py +224 -0
- package/scripts/verify.sh +255 -0
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ralph Stop Hook Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests for ralph_stop.sh checkpoint handling and loop control.
|
|
5
|
+
* Verifies iteration logic, completion detection, circuit breakers,
|
|
6
|
+
* and CCS checkpoint integration.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { describe, test, expect, beforeEach, afterEach, beforeAll } from 'bun:test';
|
|
10
|
+
import { existsSync } from 'fs';
|
|
11
|
+
import {
|
|
12
|
+
TestStateManager,
|
|
13
|
+
runRalphStop,
|
|
14
|
+
isJqAvailable,
|
|
15
|
+
} from './test-utils';
|
|
16
|
+
import {
|
|
17
|
+
createMinimalRalphState,
|
|
18
|
+
createFullRalphState,
|
|
19
|
+
createCheckpointedState,
|
|
20
|
+
createLinearIntegratedState,
|
|
21
|
+
TRANSCRIPT_COMPLETE,
|
|
22
|
+
TRANSCRIPT_FATAL,
|
|
23
|
+
TRANSCRIPT_STUCK,
|
|
24
|
+
} from './fixtures';
|
|
25
|
+
|
|
26
|
+
describe('Ralph Stop Hook', () => {
|
|
27
|
+
let stateManager: TestStateManager;
|
|
28
|
+
let jqAvailable: boolean;
|
|
29
|
+
|
|
30
|
+
beforeAll(async () => {
|
|
31
|
+
jqAvailable = await isJqAvailable();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
stateManager = new TestStateManager('ralph-stop');
|
|
36
|
+
stateManager.init();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
afterEach(() => {
|
|
40
|
+
stateManager.cleanup();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe('Non-Ralph Mode', () => {
|
|
44
|
+
test('exits silently without state file', async () => {
|
|
45
|
+
if (!jqAvailable) {
|
|
46
|
+
console.log('Skipping: jq not available');
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Don't create any state file
|
|
51
|
+
const result = await runRalphStop(stateManager);
|
|
52
|
+
|
|
53
|
+
expect(result.exitCode).toBe(0);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('does not create state file if not in Ralph mode', async () => {
|
|
57
|
+
if (!jqAvailable) {
|
|
58
|
+
console.log('Skipping: jq not available');
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
await runRalphStop(stateManager);
|
|
63
|
+
|
|
64
|
+
expect(stateManager.stateExists()).toBe(false);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe('Iteration Tracking', () => {
|
|
69
|
+
test('increments iteration counter', async () => {
|
|
70
|
+
if (!jqAvailable) {
|
|
71
|
+
console.log('Skipping: jq not available');
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
stateManager.writeState(createMinimalRalphState({ iteration: 5 }));
|
|
76
|
+
|
|
77
|
+
await runRalphStop(stateManager);
|
|
78
|
+
|
|
79
|
+
const state = stateManager.readState();
|
|
80
|
+
expect(state.iteration).toBe(6);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('initializes state if missing fields', async () => {
|
|
84
|
+
if (!jqAvailable) {
|
|
85
|
+
console.log('Skipping: jq not available');
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Write minimal state
|
|
90
|
+
stateManager.writeState({});
|
|
91
|
+
|
|
92
|
+
await runRalphStop(stateManager);
|
|
93
|
+
|
|
94
|
+
const state = stateManager.readState();
|
|
95
|
+
expect(state.iteration).toBeDefined();
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe('Max Iterations Safety', () => {
|
|
100
|
+
test('exits with code 0 at max iterations', async () => {
|
|
101
|
+
if (!jqAvailable) {
|
|
102
|
+
console.log('Skipping: jq not available');
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
stateManager.writeState(createMinimalRalphState({ iteration: 49 }));
|
|
107
|
+
|
|
108
|
+
const result = await runRalphStop(stateManager, { maxIterations: 50 });
|
|
109
|
+
|
|
110
|
+
expect(result.exitCode).toBe(0);
|
|
111
|
+
expect(result.stderr).toContain('Maximum iterations');
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('cleans up state at max iterations', async () => {
|
|
115
|
+
if (!jqAvailable) {
|
|
116
|
+
console.log('Skipping: jq not available');
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
stateManager.writeState(createMinimalRalphState({ iteration: 49 }));
|
|
121
|
+
|
|
122
|
+
await runRalphStop(stateManager, { maxIterations: 50 });
|
|
123
|
+
|
|
124
|
+
// State file should be cleaned up
|
|
125
|
+
expect(stateManager.stateExists()).toBe(false);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test('continues loop before max iterations', async () => {
|
|
129
|
+
if (!jqAvailable) {
|
|
130
|
+
console.log('Skipping: jq not available');
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
stateManager.writeState(createMinimalRalphState({ iteration: 10 }));
|
|
135
|
+
|
|
136
|
+
const result = await runRalphStop(stateManager, { maxIterations: 50 });
|
|
137
|
+
|
|
138
|
+
expect(result.exitCode).toBe(1); // Block exit
|
|
139
|
+
expect(result.stderr).toContain('restarting');
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test('respects custom max iterations', async () => {
|
|
143
|
+
if (!jqAvailable) {
|
|
144
|
+
console.log('Skipping: jq not available');
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
stateManager.writeState(createMinimalRalphState({ iteration: 9 }));
|
|
149
|
+
|
|
150
|
+
const result = await runRalphStop(stateManager, { maxIterations: 10 });
|
|
151
|
+
|
|
152
|
+
expect(result.exitCode).toBe(0);
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
describe('Completion Promise Detection', () => {
|
|
157
|
+
test('exits with code 0 on completion promise', async () => {
|
|
158
|
+
if (!jqAvailable) {
|
|
159
|
+
console.log('Skipping: jq not available');
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
stateManager.writeState(createMinimalRalphState({ iteration: 5 }));
|
|
164
|
+
|
|
165
|
+
const result = await runRalphStop(stateManager, {
|
|
166
|
+
transcript: TRANSCRIPT_COMPLETE,
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
expect(result.exitCode).toBe(0);
|
|
170
|
+
expect(result.stderr).toContain('complete');
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test('cleans up state on completion', async () => {
|
|
174
|
+
if (!jqAvailable) {
|
|
175
|
+
console.log('Skipping: jq not available');
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
stateManager.writeState(createMinimalRalphState({ iteration: 5 }));
|
|
180
|
+
|
|
181
|
+
await runRalphStop(stateManager, {
|
|
182
|
+
transcript: TRANSCRIPT_COMPLETE,
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
expect(stateManager.stateExists()).toBe(false);
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
describe('Fatal Error Detection', () => {
|
|
190
|
+
test('exits with code 0 on fatal error', async () => {
|
|
191
|
+
if (!jqAvailable) {
|
|
192
|
+
console.log('Skipping: jq not available');
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
stateManager.writeState(createMinimalRalphState({ iteration: 5 }));
|
|
197
|
+
|
|
198
|
+
const result = await runRalphStop(stateManager, {
|
|
199
|
+
transcript: TRANSCRIPT_FATAL,
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
expect(result.exitCode).toBe(0);
|
|
203
|
+
expect(result.stderr).toContain('error');
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test('cleans up state on fatal error', async () => {
|
|
207
|
+
if (!jqAvailable) {
|
|
208
|
+
console.log('Skipping: jq not available');
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
stateManager.writeState(createMinimalRalphState({ iteration: 5 }));
|
|
213
|
+
|
|
214
|
+
await runRalphStop(stateManager, {
|
|
215
|
+
transcript: TRANSCRIPT_FATAL,
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
expect(stateManager.stateExists()).toBe(false);
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
describe('Stuck Signal Detection', () => {
|
|
223
|
+
test('logs warning on stuck signals', async () => {
|
|
224
|
+
if (!jqAvailable) {
|
|
225
|
+
console.log('Skipping: jq not available');
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
stateManager.writeState(createMinimalRalphState({ iteration: 5 }));
|
|
230
|
+
|
|
231
|
+
const result = await runRalphStop(stateManager, {
|
|
232
|
+
transcript: TRANSCRIPT_STUCK,
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
// Should warn but not exit
|
|
236
|
+
expect(result.stderr).toContain('Stuck');
|
|
237
|
+
expect(result.exitCode).toBe(1); // Still continues
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
describe('CCS Checkpoint Integration', () => {
|
|
242
|
+
test('restarts with exit code 1 on active checkpoint', async () => {
|
|
243
|
+
if (!jqAvailable) {
|
|
244
|
+
console.log('Skipping: jq not available');
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
stateManager.writeState(createCheckpointedState('L2', 87));
|
|
249
|
+
|
|
250
|
+
const result = await runRalphStop(stateManager);
|
|
251
|
+
|
|
252
|
+
expect(result.exitCode).toBe(1); // Restart loop
|
|
253
|
+
expect(result.stderr).toContain('checkpoint');
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
test('detects active checkpoint in state', async () => {
|
|
257
|
+
if (!jqAvailable) {
|
|
258
|
+
console.log('Skipping: jq not available');
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
stateManager.writeState(createCheckpointedState('L2', 87));
|
|
263
|
+
|
|
264
|
+
const result = await runRalphStop(stateManager);
|
|
265
|
+
|
|
266
|
+
// The checkpoint is detected (log message confirms this)
|
|
267
|
+
expect(result.stderr).toContain('CCS checkpoint detected');
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
test('attempts PROMPT.md generation for checkpoint resume', async () => {
|
|
271
|
+
if (!jqAvailable) {
|
|
272
|
+
console.log('Skipping: jq not available');
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
stateManager.writeState(createCheckpointedState('L2', 87, {
|
|
277
|
+
task_name: 'Test Task',
|
|
278
|
+
todo_items: ['Item 1', 'Item 2'],
|
|
279
|
+
}));
|
|
280
|
+
|
|
281
|
+
const result = await runRalphStop(stateManager);
|
|
282
|
+
|
|
283
|
+
// Hook attempts to handle checkpoint (success depends on Python generator path)
|
|
284
|
+
expect(result.stderr).toContain('checkpoint');
|
|
285
|
+
// Note: PROMPT.md generation may fail in test env due to relative path resolution
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
test('handles L3 checkpoint same as L2', async () => {
|
|
289
|
+
if (!jqAvailable) {
|
|
290
|
+
console.log('Skipping: jq not available');
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
stateManager.writeState(createCheckpointedState('L3', 96));
|
|
295
|
+
|
|
296
|
+
const result = await runRalphStop(stateManager);
|
|
297
|
+
|
|
298
|
+
expect(result.exitCode).toBe(1);
|
|
299
|
+
expect(result.stderr).toContain('checkpoint');
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
describe('Circuit Breaker', () => {
|
|
304
|
+
// Note: Circuit breaker tests are limited in isolated test env because
|
|
305
|
+
// they rely on actual git diff hashes. These tests verify the logic
|
|
306
|
+
// runs without crashing; full circuit breaker testing requires a git repo.
|
|
307
|
+
|
|
308
|
+
test('does not crash when checking git diff in non-repo', async () => {
|
|
309
|
+
if (!jqAvailable) {
|
|
310
|
+
console.log('Skipping: jq not available');
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
stateManager.writeState(createMinimalRalphState({
|
|
315
|
+
iteration: 10,
|
|
316
|
+
no_change_count: 2,
|
|
317
|
+
last_diff_hash: 'abc123',
|
|
318
|
+
}));
|
|
319
|
+
|
|
320
|
+
const result = await runRalphStop(stateManager);
|
|
321
|
+
|
|
322
|
+
// Should complete without crashing
|
|
323
|
+
expect(typeof result.exitCode).toBe('number');
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
test('continues iteration when git is unavailable', async () => {
|
|
327
|
+
if (!jqAvailable) {
|
|
328
|
+
console.log('Skipping: jq not available');
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
stateManager.writeState(createMinimalRalphState({
|
|
333
|
+
iteration: 5,
|
|
334
|
+
no_change_count: 0,
|
|
335
|
+
}));
|
|
336
|
+
|
|
337
|
+
const result = await runRalphStop(stateManager);
|
|
338
|
+
|
|
339
|
+
// Should continue (exit code 1) since no completion/max iter/fatal
|
|
340
|
+
expect(result.exitCode).toBe(1);
|
|
341
|
+
});
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
describe('Linear Integration', () => {
|
|
345
|
+
test('handles state with Linear integration enabled', async () => {
|
|
346
|
+
if (!jqAvailable) {
|
|
347
|
+
console.log('Skipping: jq not available');
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
stateManager.writeState(createLinearIntegratedState());
|
|
352
|
+
|
|
353
|
+
const result = await runRalphStop(stateManager);
|
|
354
|
+
|
|
355
|
+
expect(result.exitCode).toBe(1); // Normal restart
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
test('does not crash with missing Linear fields', async () => {
|
|
359
|
+
if (!jqAvailable) {
|
|
360
|
+
console.log('Skipping: jq not available');
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
stateManager.writeState(createMinimalRalphState({
|
|
365
|
+
linear_integration: { enabled: true },
|
|
366
|
+
}));
|
|
367
|
+
|
|
368
|
+
const result = await runRalphStop(stateManager);
|
|
369
|
+
|
|
370
|
+
expect(result.exitCode).toBe(1); // Should still work
|
|
371
|
+
});
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
describe('Error Handling', () => {
|
|
375
|
+
test('handles corrupted state file gracefully', async () => {
|
|
376
|
+
if (!jqAvailable) {
|
|
377
|
+
console.log('Skipping: jq not available');
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Write invalid JSON
|
|
382
|
+
stateManager.writeFile('.claude/ralph-state.json', 'not valid json');
|
|
383
|
+
|
|
384
|
+
// Should not crash, but may have unexpected behavior
|
|
385
|
+
// The important thing is it doesn't hang
|
|
386
|
+
const result = await runRalphStop(stateManager);
|
|
387
|
+
expect(typeof result.exitCode).toBe('number');
|
|
388
|
+
});
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
describe('Logging', () => {
|
|
392
|
+
test('creates log entries when logging enabled', async () => {
|
|
393
|
+
if (!jqAvailable) {
|
|
394
|
+
console.log('Skipping: jq not available');
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
stateManager.writeState(createMinimalRalphState({ iteration: 5 }));
|
|
399
|
+
|
|
400
|
+
await runRalphStop(stateManager, { enableLogging: true });
|
|
401
|
+
|
|
402
|
+
// Check if log file was created
|
|
403
|
+
expect(stateManager.fileExists('.claude/logs/ralph.log')).toBe(true);
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
test('does not create logs when logging disabled', async () => {
|
|
407
|
+
if (!jqAvailable) {
|
|
408
|
+
console.log('Skipping: jq not available');
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
stateManager.writeState(createMinimalRalphState({ iteration: 5 }));
|
|
413
|
+
|
|
414
|
+
await runRalphStop(stateManager, { enableLogging: false });
|
|
415
|
+
|
|
416
|
+
expect(stateManager.fileExists('.claude/logs/ralph.log')).toBe(false);
|
|
417
|
+
});
|
|
418
|
+
});
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
describe('Ralph Stop Hook - jq Dependency', () => {
|
|
422
|
+
let stateManager: TestStateManager;
|
|
423
|
+
|
|
424
|
+
beforeEach(() => {
|
|
425
|
+
stateManager = new TestStateManager('ralph-stop-jq');
|
|
426
|
+
stateManager.init();
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
afterEach(() => {
|
|
430
|
+
stateManager.cleanup();
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
test('hook script exists', async () => {
|
|
434
|
+
const hookPath = '/Users/alexandercahiz/Projects/anvil-dev-framework/global/hooks/ralph_stop.sh';
|
|
435
|
+
expect(existsSync(hookPath)).toBe(true);
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
// Note: Testing missing jq is tricky because we can't easily uninstall it
|
|
439
|
+
// The hook handles this case by printing an error and exiting 0
|
|
440
|
+
});
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CCS Test Utilities
|
|
3
|
+
*
|
|
4
|
+
* Shared helpers for Context Checkpoint System (CCS) end-to-end testing.
|
|
5
|
+
* Provides subprocess execution for Python and Bash hooks, state file
|
|
6
|
+
* management, and context simulation.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { mkdirSync, rmSync, writeFileSync, readFileSync, existsSync } from 'fs';
|
|
10
|
+
import { join } from 'path';
|
|
11
|
+
import { tmpdir } from 'os';
|
|
12
|
+
|
|
13
|
+
// Project paths
|
|
14
|
+
export const PROJECT_ROOT = join(__dirname, '../../../../../..');
|
|
15
|
+
export const HOOKS_DIR = join(PROJECT_ROOT, 'global/hooks');
|
|
16
|
+
export const LIB_DIR = join(PROJECT_ROOT, 'global/lib');
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Result from running a hook subprocess
|
|
20
|
+
*/
|
|
21
|
+
export interface HookResult {
|
|
22
|
+
stdout: string;
|
|
23
|
+
stderr: string;
|
|
24
|
+
exitCode: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Test directory manager for isolated state files
|
|
29
|
+
*/
|
|
30
|
+
export class TestStateManager {
|
|
31
|
+
readonly baseDir: string;
|
|
32
|
+
readonly stateFile: string;
|
|
33
|
+
readonly claudeDir: string;
|
|
34
|
+
|
|
35
|
+
constructor(prefix = 'ccs-test') {
|
|
36
|
+
this.baseDir = join(tmpdir(), `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
37
|
+
this.claudeDir = join(this.baseDir, '.claude');
|
|
38
|
+
this.stateFile = join(this.claudeDir, 'ralph-state.json');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Initialize the test directory structure
|
|
43
|
+
*/
|
|
44
|
+
init(): void {
|
|
45
|
+
mkdirSync(this.claudeDir, { recursive: true });
|
|
46
|
+
mkdirSync(join(this.claudeDir, 'handoffs'), { recursive: true });
|
|
47
|
+
mkdirSync(join(this.claudeDir, 'logs'), { recursive: true });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Write a Ralph state file
|
|
52
|
+
*/
|
|
53
|
+
writeState(state: object): void {
|
|
54
|
+
writeFileSync(this.stateFile, JSON.stringify(state, null, 2));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Read the current Ralph state file
|
|
59
|
+
*/
|
|
60
|
+
readState(): Record<string, unknown> {
|
|
61
|
+
if (!existsSync(this.stateFile)) {
|
|
62
|
+
return {};
|
|
63
|
+
}
|
|
64
|
+
return JSON.parse(readFileSync(this.stateFile, 'utf-8'));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Check if state file exists
|
|
69
|
+
*/
|
|
70
|
+
stateExists(): boolean {
|
|
71
|
+
return existsSync(this.stateFile);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Clean up all test files
|
|
76
|
+
*/
|
|
77
|
+
cleanup(): void {
|
|
78
|
+
try {
|
|
79
|
+
rmSync(this.baseDir, { recursive: true, force: true });
|
|
80
|
+
} catch {
|
|
81
|
+
// Ignore cleanup errors
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Write a file relative to the test base directory
|
|
87
|
+
*/
|
|
88
|
+
writeFile(relativePath: string, content: string): void {
|
|
89
|
+
const fullPath = join(this.baseDir, relativePath);
|
|
90
|
+
mkdirSync(join(fullPath, '..'), { recursive: true });
|
|
91
|
+
writeFileSync(fullPath, content);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Read a file relative to the test base directory
|
|
96
|
+
*/
|
|
97
|
+
readFile(relativePath: string): string {
|
|
98
|
+
return readFileSync(join(this.baseDir, relativePath), 'utf-8');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Check if a file exists relative to the test base directory
|
|
103
|
+
*/
|
|
104
|
+
fileExists(relativePath: string): boolean {
|
|
105
|
+
return existsSync(join(this.baseDir, relativePath));
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Run a Python hook as a subprocess
|
|
111
|
+
*/
|
|
112
|
+
export async function runPythonHook(
|
|
113
|
+
hookPath: string,
|
|
114
|
+
input: object,
|
|
115
|
+
options: {
|
|
116
|
+
cwd?: string;
|
|
117
|
+
env?: Record<string, string>;
|
|
118
|
+
args?: string[];
|
|
119
|
+
} = {}
|
|
120
|
+
): Promise<HookResult> {
|
|
121
|
+
const inputJson = JSON.stringify(input);
|
|
122
|
+
const cwd = options.cwd ?? PROJECT_ROOT;
|
|
123
|
+
const args = options.args ?? [];
|
|
124
|
+
|
|
125
|
+
const proc = Bun.spawn(['python3', hookPath, ...args], {
|
|
126
|
+
stdin: new Blob([inputJson]),
|
|
127
|
+
stdout: 'pipe',
|
|
128
|
+
stderr: 'pipe',
|
|
129
|
+
cwd,
|
|
130
|
+
env: { ...process.env, ...options.env },
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const stdout = await new Response(proc.stdout).text();
|
|
134
|
+
const stderr = await new Response(proc.stderr).text();
|
|
135
|
+
await proc.exited;
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
stdout: stdout.trim(),
|
|
139
|
+
stderr: stderr.trim(),
|
|
140
|
+
exitCode: proc.exitCode ?? 0,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Run a Bash hook as a subprocess
|
|
146
|
+
*/
|
|
147
|
+
export async function runBashHook(
|
|
148
|
+
hookPath: string,
|
|
149
|
+
options: {
|
|
150
|
+
cwd?: string;
|
|
151
|
+
env?: Record<string, string>;
|
|
152
|
+
stdin?: string;
|
|
153
|
+
} = {}
|
|
154
|
+
): Promise<HookResult> {
|
|
155
|
+
const cwd = options.cwd ?? PROJECT_ROOT;
|
|
156
|
+
|
|
157
|
+
const proc = Bun.spawn(['bash', hookPath], {
|
|
158
|
+
stdin: options.stdin ? new Blob([options.stdin]) : undefined,
|
|
159
|
+
stdout: 'pipe',
|
|
160
|
+
stderr: 'pipe',
|
|
161
|
+
cwd,
|
|
162
|
+
env: { ...process.env, ...options.env },
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
const stdout = await new Response(proc.stdout).text();
|
|
166
|
+
const stderr = await new Response(proc.stderr).text();
|
|
167
|
+
await proc.exited;
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
stdout: stdout.trim(),
|
|
171
|
+
stderr: stderr.trim(),
|
|
172
|
+
exitCode: proc.exitCode ?? 0,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Run the context monitor Python hook
|
|
178
|
+
*/
|
|
179
|
+
export async function runContextMonitor(
|
|
180
|
+
input: object,
|
|
181
|
+
stateManager: TestStateManager
|
|
182
|
+
): Promise<HookResult> {
|
|
183
|
+
const hookPath = join(HOOKS_DIR, 'ralph_context_monitor.py');
|
|
184
|
+
return runPythonHook(hookPath, input, {
|
|
185
|
+
cwd: stateManager.baseDir,
|
|
186
|
+
env: {
|
|
187
|
+
PYTHONPATH: LIB_DIR,
|
|
188
|
+
},
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Run the ralph stop Bash hook
|
|
194
|
+
*/
|
|
195
|
+
export async function runRalphStop(
|
|
196
|
+
stateManager: TestStateManager,
|
|
197
|
+
options: {
|
|
198
|
+
maxIterations?: number;
|
|
199
|
+
transcript?: string;
|
|
200
|
+
enableLogging?: boolean;
|
|
201
|
+
} = {}
|
|
202
|
+
): Promise<HookResult> {
|
|
203
|
+
const hookPath = join(HOOKS_DIR, 'ralph_stop.sh');
|
|
204
|
+
const stateRelPath = '.claude/ralph-state.json';
|
|
205
|
+
|
|
206
|
+
return runBashHook(hookPath, {
|
|
207
|
+
cwd: stateManager.baseDir,
|
|
208
|
+
env: {
|
|
209
|
+
RALPH_STATE_FILE: stateRelPath,
|
|
210
|
+
RALPH_MAX_ITERATIONS: String(options.maxIterations ?? 50),
|
|
211
|
+
RALPH_ENABLE_LOGGING: options.enableLogging ? 'true' : 'false',
|
|
212
|
+
CLAUDE_TRANSCRIPT: options.transcript ?? '',
|
|
213
|
+
},
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Check if jq is available on the system
|
|
219
|
+
*/
|
|
220
|
+
export async function isJqAvailable(): Promise<boolean> {
|
|
221
|
+
try {
|
|
222
|
+
const proc = Bun.spawn(['which', 'jq'], {
|
|
223
|
+
stdout: 'pipe',
|
|
224
|
+
stderr: 'pipe',
|
|
225
|
+
});
|
|
226
|
+
await proc.exited;
|
|
227
|
+
return proc.exitCode === 0;
|
|
228
|
+
} catch {
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Check if Python 3 is available on the system
|
|
235
|
+
*/
|
|
236
|
+
export async function isPython3Available(): Promise<boolean> {
|
|
237
|
+
try {
|
|
238
|
+
const proc = Bun.spawn(['python3', '--version'], {
|
|
239
|
+
stdout: 'pipe',
|
|
240
|
+
stderr: 'pipe',
|
|
241
|
+
});
|
|
242
|
+
await proc.exited;
|
|
243
|
+
return proc.exitCode === 0;
|
|
244
|
+
} catch {
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Alias for isPython3Available for convenience
|
|
251
|
+
*/
|
|
252
|
+
export const isPythonAvailable = isPython3Available;
|