anvil-dev-framework 0.1.6 → 0.1.7
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 +13 -12
- package/VERSION +1 -1
- package/docs/INSTALLATION.md +18 -0
- package/docs/command-reference.md +1 -1
- package/global/commands/orient.md +29 -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__/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__/git_utils.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/github_service.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/handoff_generator.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__/orient_fast.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/quality_service.cpython-314.pyc +0 -0
- package/global/lib/__pycache__/ralph_prompt_generator.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/git_utils.py +267 -0
- package/global/lib/issue_models.py +28 -0
- package/global/lib/linear_provider.py +7 -0
- package/global/lib/orient_fast.py +24 -1
- package/global/tests/test_git_utils.py +160 -0
- package/global/tests/test_issue_models.py +40 -0
- package/global/tests/test_linear_provider.py +86 -0
- package/global/tools/anvil-memory/src/__tests__/commands.test.ts +238 -1
- package/global/tools/anvil-memory/src/commands/ralph-iteration.ts +249 -0
- package/global/tools/anvil-memory/src/index.ts +2 -8
- package/package.json +1 -1
- package/scripts/anvil +7 -2
- package/global/tools/anvil-memory/src/__tests__/ccs/context-monitor.test.ts +0 -535
- package/global/tools/anvil-memory/src/__tests__/ccs/edge-cases.test.ts +0 -645
- package/global/tools/anvil-memory/src/__tests__/ccs/fixtures.ts +0 -363
- package/global/tools/anvil-memory/src/__tests__/ccs/index.ts +0 -8
- package/global/tools/anvil-memory/src/__tests__/ccs/integration.test.ts +0 -417
- package/global/tools/anvil-memory/src/__tests__/ccs/prompt-generator.test.ts +0 -571
- package/global/tools/anvil-memory/src/__tests__/ccs/ralph-stop.test.ts +0 -440
- package/global/tools/anvil-memory/src/__tests__/ccs/test-utils.ts +0 -252
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Unit tests for git_utils.py
|
|
4
|
+
|
|
5
|
+
Run with: python3 -m pytest global/tests/test_git_utils.py -v
|
|
6
|
+
Or: python3 global/tests/test_git_utils.py
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import sys
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
# Add parent directory to path for imports
|
|
13
|
+
sys.path.insert(0, str(Path(__file__).parent.parent / "lib"))
|
|
14
|
+
|
|
15
|
+
from git_utils import (
|
|
16
|
+
get_current_branch,
|
|
17
|
+
get_repo_status,
|
|
18
|
+
get_divergence_status,
|
|
19
|
+
list_stashes,
|
|
20
|
+
run_git,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class TestRunGit:
|
|
25
|
+
"""Tests for the run_git helper."""
|
|
26
|
+
|
|
27
|
+
def test_run_git_returns_tuple(self):
|
|
28
|
+
code, stdout, stderr = run_git('--version')
|
|
29
|
+
assert isinstance(code, int)
|
|
30
|
+
assert isinstance(stdout, str)
|
|
31
|
+
assert isinstance(stderr, str)
|
|
32
|
+
|
|
33
|
+
def test_run_git_version_succeeds(self):
|
|
34
|
+
code, stdout, _ = run_git('--version')
|
|
35
|
+
assert code == 0
|
|
36
|
+
assert 'git version' in stdout
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class TestGetCurrentBranch:
|
|
40
|
+
"""Tests for get_current_branch()."""
|
|
41
|
+
|
|
42
|
+
def test_returns_string_in_git_repo(self):
|
|
43
|
+
branch = get_current_branch()
|
|
44
|
+
# Should return a non-empty string when in a git repo
|
|
45
|
+
assert isinstance(branch, str)
|
|
46
|
+
assert len(branch) > 0
|
|
47
|
+
|
|
48
|
+
def test_returns_expected_format(self):
|
|
49
|
+
branch = get_current_branch()
|
|
50
|
+
# Branch names shouldn't have newlines
|
|
51
|
+
assert '\n' not in branch
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class TestGetRepoStatus:
|
|
55
|
+
"""Tests for get_repo_status()."""
|
|
56
|
+
|
|
57
|
+
def test_returns_dict_with_expected_keys(self):
|
|
58
|
+
status = get_repo_status()
|
|
59
|
+
expected_keys = [
|
|
60
|
+
'has_changes',
|
|
61
|
+
'has_staged',
|
|
62
|
+
'has_unstaged',
|
|
63
|
+
'has_untracked',
|
|
64
|
+
'changed_files',
|
|
65
|
+
'untracked_files',
|
|
66
|
+
'current_branch',
|
|
67
|
+
]
|
|
68
|
+
for key in expected_keys:
|
|
69
|
+
assert key in status, f"Missing key: {key}"
|
|
70
|
+
|
|
71
|
+
def test_has_changes_is_boolean(self):
|
|
72
|
+
status = get_repo_status()
|
|
73
|
+
assert isinstance(status['has_changes'], bool)
|
|
74
|
+
|
|
75
|
+
def test_changed_files_is_list(self):
|
|
76
|
+
status = get_repo_status()
|
|
77
|
+
assert isinstance(status['changed_files'], list)
|
|
78
|
+
|
|
79
|
+
def test_current_branch_matches_get_current_branch(self):
|
|
80
|
+
status = get_repo_status()
|
|
81
|
+
branch = get_current_branch()
|
|
82
|
+
assert status['current_branch'] == branch
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class TestGetDivergenceStatus:
|
|
86
|
+
"""Tests for get_divergence_status()."""
|
|
87
|
+
|
|
88
|
+
def test_returns_dict_with_expected_keys(self):
|
|
89
|
+
div = get_divergence_status()
|
|
90
|
+
expected_keys = ['ahead', 'behind', 'diverged', 'remote_branch']
|
|
91
|
+
for key in expected_keys:
|
|
92
|
+
assert key in div, f"Missing key: {key}"
|
|
93
|
+
|
|
94
|
+
def test_ahead_behind_are_integers(self):
|
|
95
|
+
div = get_divergence_status()
|
|
96
|
+
assert isinstance(div['ahead'], int)
|
|
97
|
+
assert isinstance(div['behind'], int)
|
|
98
|
+
|
|
99
|
+
def test_diverged_is_boolean(self):
|
|
100
|
+
div = get_divergence_status()
|
|
101
|
+
assert isinstance(div['diverged'], bool)
|
|
102
|
+
|
|
103
|
+
def test_diverged_logic_is_correct(self):
|
|
104
|
+
div = get_divergence_status()
|
|
105
|
+
# diverged should be True only if both ahead > 0 and behind > 0
|
|
106
|
+
expected = div['ahead'] > 0 and div['behind'] > 0
|
|
107
|
+
assert div['diverged'] == expected
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class TestListStashes:
|
|
111
|
+
"""Tests for list_stashes()."""
|
|
112
|
+
|
|
113
|
+
def test_returns_list(self):
|
|
114
|
+
stashes = list_stashes()
|
|
115
|
+
assert isinstance(stashes, list)
|
|
116
|
+
|
|
117
|
+
def test_stash_items_have_expected_keys(self):
|
|
118
|
+
stashes = list_stashes()
|
|
119
|
+
if stashes: # Only test if there are stashes
|
|
120
|
+
stash = stashes[0]
|
|
121
|
+
assert 'index' in stash
|
|
122
|
+
assert 'branch' in stash
|
|
123
|
+
assert 'message' in stash
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def run_tests():
|
|
127
|
+
"""Run all tests manually if pytest not available."""
|
|
128
|
+
test_classes = [
|
|
129
|
+
TestRunGit,
|
|
130
|
+
TestGetCurrentBranch,
|
|
131
|
+
TestGetRepoStatus,
|
|
132
|
+
TestGetDivergenceStatus,
|
|
133
|
+
TestListStashes,
|
|
134
|
+
]
|
|
135
|
+
passed = 0
|
|
136
|
+
failed = 0
|
|
137
|
+
|
|
138
|
+
for test_class in test_classes:
|
|
139
|
+
instance = test_class()
|
|
140
|
+
for method_name in dir(instance):
|
|
141
|
+
if method_name.startswith("test_"):
|
|
142
|
+
try:
|
|
143
|
+
getattr(instance, method_name)()
|
|
144
|
+
print(f" ✓ {test_class.__name__}.{method_name}")
|
|
145
|
+
passed += 1
|
|
146
|
+
except AssertionError as e:
|
|
147
|
+
print(f" ✗ {test_class.__name__}.{method_name}: {e}")
|
|
148
|
+
failed += 1
|
|
149
|
+
except Exception as e:
|
|
150
|
+
print(f" ✗ {test_class.__name__}.{method_name}: {type(e).__name__}: {e}")
|
|
151
|
+
failed += 1
|
|
152
|
+
|
|
153
|
+
print(f"\nResults: {passed} passed, {failed} failed")
|
|
154
|
+
return failed == 0
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
if __name__ == "__main__":
|
|
158
|
+
print("Running git_utils tests...\n")
|
|
159
|
+
success = run_tests()
|
|
160
|
+
sys.exit(0 if success else 1)
|
|
@@ -267,6 +267,46 @@ class TestIssue:
|
|
|
267
267
|
)
|
|
268
268
|
assert "[agent-xyz]" in str(issue)
|
|
269
269
|
|
|
270
|
+
# Dict-style access tests (ANV-260)
|
|
271
|
+
def test_getitem_returns_field_value(self):
|
|
272
|
+
issue = Issue(id="1", identifier="TEST-001", title="Test")
|
|
273
|
+
assert issue["identifier"] == "TEST-001"
|
|
274
|
+
assert issue["title"] == "Test"
|
|
275
|
+
assert issue["status"] == IssueStatus.TODO
|
|
276
|
+
|
|
277
|
+
def test_getitem_raises_keyerror_for_unknown_field(self):
|
|
278
|
+
issue = Issue(id="1", identifier="TEST-001", title="Test")
|
|
279
|
+
try:
|
|
280
|
+
_ = issue["nonexistent"]
|
|
281
|
+
raise AssertionError("Should have raised KeyError")
|
|
282
|
+
except KeyError as e:
|
|
283
|
+
assert "nonexistent" in str(e)
|
|
284
|
+
|
|
285
|
+
def test_get_returns_field_value(self):
|
|
286
|
+
issue = Issue(id="1", identifier="TEST-001", title="Test")
|
|
287
|
+
assert issue.get("identifier") == "TEST-001"
|
|
288
|
+
assert issue.get("title") == "Test"
|
|
289
|
+
|
|
290
|
+
def test_get_returns_default_for_unknown_field(self):
|
|
291
|
+
issue = Issue(id="1", identifier="TEST-001", title="Test")
|
|
292
|
+
assert issue.get("nonexistent") is None
|
|
293
|
+
assert issue.get("nonexistent", "default") == "default"
|
|
294
|
+
|
|
295
|
+
def test_dict_style_access_matches_attribute_access(self):
|
|
296
|
+
issue = Issue(
|
|
297
|
+
id="1",
|
|
298
|
+
identifier="TEST-001",
|
|
299
|
+
title="Test",
|
|
300
|
+
status=IssueStatus.IN_PROGRESS,
|
|
301
|
+
priority=Priority.HIGH
|
|
302
|
+
)
|
|
303
|
+
# All fields should be accessible both ways
|
|
304
|
+
assert issue["id"] == issue.id
|
|
305
|
+
assert issue["identifier"] == issue.identifier
|
|
306
|
+
assert issue["title"] == issue.title
|
|
307
|
+
assert issue["status"] == issue.status
|
|
308
|
+
assert issue["priority"] == issue.priority
|
|
309
|
+
|
|
270
310
|
|
|
271
311
|
def run_tests():
|
|
272
312
|
"""Run all tests manually if pytest not available."""
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Unit tests for linear_provider.py
|
|
4
|
+
|
|
5
|
+
Run with: python3 -m pytest global/tests/test_linear_provider.py -v
|
|
6
|
+
Or: python3 global/tests/test_linear_provider.py
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import sys
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
# Add parent directory to path for imports
|
|
13
|
+
sys.path.insert(0, str(Path(__file__).parent.parent / "lib"))
|
|
14
|
+
|
|
15
|
+
from linear_provider import LinearProvider
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class TestLinearProviderInit:
|
|
19
|
+
"""Tests for LinearProvider initialization validation (ANV-260)."""
|
|
20
|
+
|
|
21
|
+
def test_init_without_team_raises_valueerror(self):
|
|
22
|
+
"""LinearProvider() without team_key or team_id should fail fast."""
|
|
23
|
+
try:
|
|
24
|
+
LinearProvider()
|
|
25
|
+
raise AssertionError("Should have raised ValueError")
|
|
26
|
+
except ValueError as e:
|
|
27
|
+
assert "team_key or team_id" in str(e)
|
|
28
|
+
assert "LinearProvider(team_key='ANV')" in str(e)
|
|
29
|
+
|
|
30
|
+
def test_init_with_team_key_succeeds(self):
|
|
31
|
+
"""LinearProvider(team_key='ANV') should work."""
|
|
32
|
+
lp = LinearProvider(team_key='ANV')
|
|
33
|
+
assert lp.team_key == 'ANV'
|
|
34
|
+
|
|
35
|
+
def test_init_with_team_id_succeeds(self):
|
|
36
|
+
"""LinearProvider(team_id='...') should work."""
|
|
37
|
+
lp = LinearProvider(team_id='some-uuid-123')
|
|
38
|
+
assert lp._team_id == 'some-uuid-123'
|
|
39
|
+
|
|
40
|
+
def test_init_with_both_succeeds(self):
|
|
41
|
+
"""LinearProvider with both team_key and team_id should work."""
|
|
42
|
+
lp = LinearProvider(team_key='ANV', team_id='uuid-123')
|
|
43
|
+
assert lp.team_key == 'ANV'
|
|
44
|
+
assert lp._team_id == 'uuid-123'
|
|
45
|
+
|
|
46
|
+
def test_error_message_is_helpful(self):
|
|
47
|
+
"""Error message should include example usage."""
|
|
48
|
+
try:
|
|
49
|
+
LinearProvider()
|
|
50
|
+
except ValueError as e:
|
|
51
|
+
error_msg = str(e)
|
|
52
|
+
# Should mention what's required
|
|
53
|
+
assert "requires" in error_msg.lower()
|
|
54
|
+
# Should give an example
|
|
55
|
+
assert "team_key='ANV'" in error_msg
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def run_tests():
|
|
59
|
+
"""Run all tests manually if pytest not available."""
|
|
60
|
+
test_classes = [TestLinearProviderInit]
|
|
61
|
+
passed = 0
|
|
62
|
+
failed = 0
|
|
63
|
+
|
|
64
|
+
for test_class in test_classes:
|
|
65
|
+
instance = test_class()
|
|
66
|
+
for method_name in dir(instance):
|
|
67
|
+
if method_name.startswith("test_"):
|
|
68
|
+
try:
|
|
69
|
+
getattr(instance, method_name)()
|
|
70
|
+
print(f" ✓ {test_class.__name__}.{method_name}")
|
|
71
|
+
passed += 1
|
|
72
|
+
except AssertionError as e:
|
|
73
|
+
print(f" ✗ {test_class.__name__}.{method_name}: {e}")
|
|
74
|
+
failed += 1
|
|
75
|
+
except Exception as e:
|
|
76
|
+
print(f" ✗ {test_class.__name__}.{method_name}: {type(e).__name__}: {e}")
|
|
77
|
+
failed += 1
|
|
78
|
+
|
|
79
|
+
print(f"\nResults: {passed} passed, {failed} failed")
|
|
80
|
+
return failed == 0
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
if __name__ == "__main__":
|
|
84
|
+
print("Running linear_provider tests...\n")
|
|
85
|
+
success = run_tests()
|
|
86
|
+
sys.exit(0 if success else 1)
|
|
@@ -15,8 +15,9 @@ import { handleObserve, parseObserveArgs } from '../commands/observe';
|
|
|
15
15
|
import { handleSearch, parseSearchArgs } from '../commands/search';
|
|
16
16
|
import { handleGet, parseGetArgs } from '../commands/get';
|
|
17
17
|
import { handleCheckpoint, parseCheckpointArgs } from '../commands/checkpoint';
|
|
18
|
+
import { handleRalphIteration, parseRalphIterationArgs } from '../commands/ralph-iteration';
|
|
18
19
|
import { AnvilMemoryDb } from '../db';
|
|
19
|
-
import type { Observation, Checkpoint } from '../types';
|
|
20
|
+
import type { Observation, Session, Checkpoint, RalphIteration } from '../types';
|
|
20
21
|
|
|
21
22
|
// Type definitions for command result data
|
|
22
23
|
interface InitResultData {
|
|
@@ -51,6 +52,11 @@ interface CheckpointResultData {
|
|
|
51
52
|
observation?: Observation;
|
|
52
53
|
}
|
|
53
54
|
|
|
55
|
+
interface RalphIterationResultData {
|
|
56
|
+
ralph_iteration?: RalphIteration;
|
|
57
|
+
observation?: Observation;
|
|
58
|
+
}
|
|
59
|
+
|
|
54
60
|
// Test database path
|
|
55
61
|
const TEST_DB_DIR = join(tmpdir(), 'anvil-memory-cmd-tests');
|
|
56
62
|
const getTestDbPath = () => join(TEST_DB_DIR, `cmd-test-${Date.now()}-${Math.random().toString(36).slice(2)}.db`);
|
|
@@ -629,6 +635,237 @@ describe('checkpoint command', () => {
|
|
|
629
635
|
});
|
|
630
636
|
});
|
|
631
637
|
|
|
638
|
+
describe('ralph-iteration command', () => {
|
|
639
|
+
let testDbPath: string;
|
|
640
|
+
|
|
641
|
+
beforeAll(async () => {
|
|
642
|
+
if (!existsSync(TEST_DB_DIR)) {
|
|
643
|
+
mkdirSync(TEST_DB_DIR, { recursive: true });
|
|
644
|
+
}
|
|
645
|
+
testDbPath = getTestDbPath();
|
|
646
|
+
await handleInit(['--path', testDbPath]);
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
afterAll(() => {
|
|
650
|
+
cleanupDb(testDbPath);
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
test('parseRalphIterationArgs parses required options', () => {
|
|
654
|
+
const opts = parseRalphIterationArgs(['--session', 'ralph-abc123', '--iteration', '5', '--status', 'running']);
|
|
655
|
+
expect(opts.session).toBe('ralph-abc123');
|
|
656
|
+
expect(opts.iteration).toBe('5');
|
|
657
|
+
expect(opts.status).toBe('running');
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
test('parseRalphIterationArgs parses short options', () => {
|
|
661
|
+
const opts = parseRalphIterationArgs(['-s', 'ralph-xyz', '-i', '10', '-t', 'completed']);
|
|
662
|
+
expect(opts.session).toBe('ralph-xyz');
|
|
663
|
+
expect(opts.iteration).toBe('10');
|
|
664
|
+
expect(opts.status).toBe('completed');
|
|
665
|
+
});
|
|
666
|
+
|
|
667
|
+
test('parseRalphIterationArgs parses optional fields', () => {
|
|
668
|
+
const opts = parseRalphIterationArgs([
|
|
669
|
+
'--session', 'ralph-test',
|
|
670
|
+
'--iteration', '3',
|
|
671
|
+
'--status', 'checkpointed',
|
|
672
|
+
'--items-completed', '8',
|
|
673
|
+
'--items-total', '10',
|
|
674
|
+
'--checkpoint-id', '42',
|
|
675
|
+
'--linear', 'ANV-203',
|
|
676
|
+
]);
|
|
677
|
+
expect(opts.itemsCompleted).toBe('8');
|
|
678
|
+
expect(opts.itemsTotal).toBe('10');
|
|
679
|
+
expect(opts.checkpointId).toBe('42');
|
|
680
|
+
expect(opts.linear).toBe('ANV-203');
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
test('handleRalphIteration requires --session', async () => {
|
|
684
|
+
const result = await handleRalphIteration(['--iteration', '1', '--status', 'running', '--path', testDbPath]);
|
|
685
|
+
|
|
686
|
+
expect(result.success).toBe(false);
|
|
687
|
+
expect(result.error).toContain('--session');
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
test('handleRalphIteration requires --iteration', async () => {
|
|
691
|
+
const result = await handleRalphIteration(['--session', 'ralph-test', '--status', 'running', '--path', testDbPath]);
|
|
692
|
+
|
|
693
|
+
expect(result.success).toBe(false);
|
|
694
|
+
expect(result.error).toContain('--iteration');
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
test('handleRalphIteration requires --status', async () => {
|
|
698
|
+
const result = await handleRalphIteration(['--session', 'ralph-test', '--iteration', '1', '--path', testDbPath]);
|
|
699
|
+
|
|
700
|
+
expect(result.success).toBe(false);
|
|
701
|
+
expect(result.error).toContain('--status');
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
test('handleRalphIteration validates status', async () => {
|
|
705
|
+
const result = await handleRalphIteration([
|
|
706
|
+
'--session', 'ralph-test',
|
|
707
|
+
'--iteration', '1',
|
|
708
|
+
'--status', 'invalid-status',
|
|
709
|
+
'--path', testDbPath,
|
|
710
|
+
]);
|
|
711
|
+
|
|
712
|
+
expect(result.success).toBe(false);
|
|
713
|
+
expect(result.error).toContain('Invalid status');
|
|
714
|
+
});
|
|
715
|
+
|
|
716
|
+
test('handleRalphIteration validates iteration is numeric', async () => {
|
|
717
|
+
const result = await handleRalphIteration([
|
|
718
|
+
'--session', 'ralph-test',
|
|
719
|
+
'--iteration', 'not-a-number',
|
|
720
|
+
'--status', 'running',
|
|
721
|
+
'--path', testDbPath,
|
|
722
|
+
]);
|
|
723
|
+
|
|
724
|
+
expect(result.success).toBe(false);
|
|
725
|
+
expect(result.error).toContain('Invalid iteration');
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
test('handleRalphIteration validates items-completed is numeric', async () => {
|
|
729
|
+
const result = await handleRalphIteration([
|
|
730
|
+
'--session', 'ralph-test',
|
|
731
|
+
'--iteration', '1',
|
|
732
|
+
'--status', 'running',
|
|
733
|
+
'--items-completed', 'abc',
|
|
734
|
+
'--path', testDbPath,
|
|
735
|
+
]);
|
|
736
|
+
|
|
737
|
+
expect(result.success).toBe(false);
|
|
738
|
+
expect(result.error).toContain('Invalid items-completed');
|
|
739
|
+
});
|
|
740
|
+
|
|
741
|
+
test('handleRalphIteration validates items-total is numeric', async () => {
|
|
742
|
+
const result = await handleRalphIteration([
|
|
743
|
+
'--session', 'ralph-test',
|
|
744
|
+
'--iteration', '1',
|
|
745
|
+
'--status', 'running',
|
|
746
|
+
'--items-total', 'xyz',
|
|
747
|
+
'--path', testDbPath,
|
|
748
|
+
]);
|
|
749
|
+
|
|
750
|
+
expect(result.success).toBe(false);
|
|
751
|
+
expect(result.error).toContain('Invalid items-total');
|
|
752
|
+
});
|
|
753
|
+
|
|
754
|
+
test('handleRalphIteration rejects negative iteration', async () => {
|
|
755
|
+
const result = await handleRalphIteration([
|
|
756
|
+
'--session', 'ralph-test',
|
|
757
|
+
'--iteration', '-1',
|
|
758
|
+
'--status', 'running',
|
|
759
|
+
'--path', testDbPath,
|
|
760
|
+
]);
|
|
761
|
+
|
|
762
|
+
expect(result.success).toBe(false);
|
|
763
|
+
expect(result.error).toContain('Invalid iteration');
|
|
764
|
+
});
|
|
765
|
+
|
|
766
|
+
test('handleRalphIteration creates ralph iteration record', async () => {
|
|
767
|
+
const result = await handleRalphIteration([
|
|
768
|
+
'--session', 'ralph-abc123',
|
|
769
|
+
'--iteration', '5',
|
|
770
|
+
'--status', 'running',
|
|
771
|
+
'--path', testDbPath,
|
|
772
|
+
]);
|
|
773
|
+
const data = result.data as RalphIterationResultData;
|
|
774
|
+
|
|
775
|
+
expect(result.success).toBe(true);
|
|
776
|
+
expect(result.message).toContain('iteration 5');
|
|
777
|
+
expect(result.message).toContain('running');
|
|
778
|
+
expect(data?.ralph_iteration?.session_id).toBe('ralph-abc123');
|
|
779
|
+
expect(data?.ralph_iteration?.iteration).toBe(5);
|
|
780
|
+
expect(data?.ralph_iteration?.status).toBe('running');
|
|
781
|
+
});
|
|
782
|
+
|
|
783
|
+
test('handleRalphIteration handles all valid statuses', async () => {
|
|
784
|
+
const statuses = ['running', 'completed', 'failed', 'checkpointed'] as const;
|
|
785
|
+
|
|
786
|
+
for (const status of statuses) {
|
|
787
|
+
const result = await handleRalphIteration([
|
|
788
|
+
'--session', `ralph-${status}`,
|
|
789
|
+
'--iteration', '1',
|
|
790
|
+
'--status', status,
|
|
791
|
+
'--path', testDbPath,
|
|
792
|
+
]);
|
|
793
|
+
const data = result.data as RalphIterationResultData;
|
|
794
|
+
expect(result.success).toBe(true);
|
|
795
|
+
expect(data?.ralph_iteration?.status).toBe(status);
|
|
796
|
+
}
|
|
797
|
+
});
|
|
798
|
+
|
|
799
|
+
test('handleRalphIteration stores items-completed and items-total', async () => {
|
|
800
|
+
const result = await handleRalphIteration([
|
|
801
|
+
'--session', 'ralph-items-test',
|
|
802
|
+
'--iteration', '3',
|
|
803
|
+
'--status', 'completed',
|
|
804
|
+
'--items-completed', '8',
|
|
805
|
+
'--items-total', '10',
|
|
806
|
+
'--path', testDbPath,
|
|
807
|
+
]);
|
|
808
|
+
const data = result.data as RalphIterationResultData;
|
|
809
|
+
|
|
810
|
+
expect(result.success).toBe(true);
|
|
811
|
+
expect(data?.ralph_iteration?.items_completed).toBe(8);
|
|
812
|
+
expect(data?.ralph_iteration?.items_total).toBe(10);
|
|
813
|
+
});
|
|
814
|
+
|
|
815
|
+
test('handleRalphIteration stores checkpoint-id', async () => {
|
|
816
|
+
// First create a checkpoint to link to
|
|
817
|
+
const checkpointResult = await handleCheckpoint([
|
|
818
|
+
'--level', 'L2',
|
|
819
|
+
'--percent', '90',
|
|
820
|
+
'--path', testDbPath,
|
|
821
|
+
]);
|
|
822
|
+
const checkpointData = checkpointResult.data as CheckpointResultData;
|
|
823
|
+
const checkpointId = checkpointData?.checkpoint?.id;
|
|
824
|
+
|
|
825
|
+
const result = await handleRalphIteration([
|
|
826
|
+
'--session', 'ralph-checkpoint-test',
|
|
827
|
+
'--iteration', '7',
|
|
828
|
+
'--status', 'checkpointed',
|
|
829
|
+
'--checkpoint-id', String(checkpointId),
|
|
830
|
+
'--path', testDbPath,
|
|
831
|
+
]);
|
|
832
|
+
const data = result.data as RalphIterationResultData;
|
|
833
|
+
|
|
834
|
+
expect(result.success).toBe(true);
|
|
835
|
+
expect(data?.ralph_iteration?.checkpoint_id).toBe(checkpointId);
|
|
836
|
+
});
|
|
837
|
+
|
|
838
|
+
test('handleRalphIteration creates linked observation with --linear', async () => {
|
|
839
|
+
const result = await handleRalphIteration([
|
|
840
|
+
'--session', 'ralph-observation-test',
|
|
841
|
+
'--iteration', '2',
|
|
842
|
+
'--status', 'completed',
|
|
843
|
+
'--items-completed', '5',
|
|
844
|
+
'--items-total', '5',
|
|
845
|
+
'--linear', 'ANV-203',
|
|
846
|
+
'--path', testDbPath,
|
|
847
|
+
]);
|
|
848
|
+
const data = result.data as RalphIterationResultData;
|
|
849
|
+
|
|
850
|
+
expect(result.success).toBe(true);
|
|
851
|
+
expect(data?.observation).toBeDefined();
|
|
852
|
+
expect(data?.observation?.type).toBe('ralph_iteration');
|
|
853
|
+
expect(data?.observation?.linear_issue).toBe('ANV-203');
|
|
854
|
+
});
|
|
855
|
+
|
|
856
|
+
test('handleRalphIteration fails when database does not exist', async () => {
|
|
857
|
+
const result = await handleRalphIteration([
|
|
858
|
+
'--session', 'ralph-test',
|
|
859
|
+
'--iteration', '1',
|
|
860
|
+
'--status', 'running',
|
|
861
|
+
'--path', '/tmp/nonexistent-db-12345.db',
|
|
862
|
+
]);
|
|
863
|
+
|
|
864
|
+
expect(result.success).toBe(false);
|
|
865
|
+
expect(result.error).toContain('Database not found');
|
|
866
|
+
});
|
|
867
|
+
});
|
|
868
|
+
|
|
632
869
|
describe('Command error handling', () => {
|
|
633
870
|
test('observe fails when database does not exist', async () => {
|
|
634
871
|
const result = await handleObserve([
|