test_commands.py file-level

at sha256:c · View file ↗ · Intel ↗

History
1 files
1 commits
0 hotspots
0 🧊 dead
0 💥 blast risk
sha256:f docs: recenter public story on governance loop · · Oct 11, 2026
1 import hashlib
2 import json
3 from pathlib import Path
4 import subprocess
5
6 import pytest
7 import yaml
8
9 from cli.v1 import parse_next
10 from tests.v1.conftest import KIT, command, seed, status, write_action
11
12
13 def fingerprint(root):
14 return {str(p.relative_to(root)): (hashlib.sha256(p.read_bytes()).hexdigest(), p.stat().st_mode)
15 for p in root.rglob('*') if p.is_file() and '.git' not in p.parts}
16
17
18 def test_status_init_sync_and_persistent_identity(repo):
19 original = status(repo)
20 before = fingerprint(repo)
21 for cmd in ('status', 'next', 'status', 'next'):
22 r = command('-C', repo, cmd)
23 assert r.returncode == 0, r.stderr
24 assert fingerprint(repo) == before
25 assert command('-C', repo, 'init', '--hooks').returncode == 0
26 assert command('-C', repo, 'sync', '--hooks').returncode == 0
27 assert fingerprint(repo) == before
28 assert status(repo)['repository'] == original['repository']
29
30
31 def test_init_preserves_living_docs(tmp_path):
32 root = seed(tmp_path / 'preexisting')
33 (root / 'docs').mkdir()
34 for name in ('ROADMAP.md', 'OVERSEER-HANDOVER.md'):
35 (root / 'docs' / name).write_text('Owner document\n')
36 assert command('-C', root, 'init').returncode == 0
37 for name in ('ROADMAP.md', 'OVERSEER-HANDOVER.md'):
38 assert (root / 'docs' / name).read_text() == 'Owner document\n'
39
40
41 def test_canonical_next_ignores_old_handover_and_roadmap(repo):
42 for name in ('ROADMAP.md', 'OVERSEER-HANDOVER.md'):
43 (repo / 'docs' / name).write_text('NEXT: OBSOLETE-SECURITY-TASK\n')
44 result = command('-C', repo, 'next')
45 assert result.returncode == 0
46 assert 'OBSOLETE' not in result.stdout
47 assert 'Action ID: SETUP' in result.stdout
48 assert str(repo) in result.stdout
49
50
51 def test_sync_preserves_next_and_living_docs(repo):
52 write_action(repo, 'Retained action\n')
53 before = {p: (repo / p).read_bytes() for p in ('docs/NEXT.md', 'docs/ROADMAP.md', 'docs/OVERSEER-HANDOVER.md')}
54 assert command('-C', repo, 'sync', '--hooks').returncode == 0
55 assert all((repo / p).read_bytes() == data for p, data in before.items())
56
57
58 def test_writer_cas_rejects_stale_and_wrong_context(repo):
59 state = status(repo)
60 write_action(repo, 'First action\n')
61 before = (repo / 'docs/NEXT.md').read_bytes()
62 r = write_action(repo, 'Overwritten action\n', expect_next=state['next_sha256'])
63 assert r.returncode != 0
64 assert 'stale_write' in r.stderr
65 assert (repo / 'docs/NEXT.md').read_bytes() == before
66
67
68 @pytest.mark.parametrize('field,value', [('repo_id','00000000-0000-0000-0000-000000000000'),
69 ('branch','other'), ('lane','other'), ('model','Other Model'), ('action_id','bad/action'),
70 ('action_kind','deploy')])
71 def test_writer_binding_checks(repo, field, value):
72 before = (repo / 'docs/NEXT.md').read_bytes()
73 r = write_action(repo, 'Safe prompt\n', **{field:value})
74 assert r.returncode != 0 and not r.stdout
75 assert (repo / 'docs/NEXT.md').read_bytes() == before
76
77
78 @pytest.mark.parametrize('field,value', [('repo-id','00000000-0000-0000-0000-000000000000'),
79 ('branch','other'), ('lane','other'), ('model','Other Model'), ('action-id','OTHER'),
80 ('action-kind','review'), ('expect-next','0'*64)])
81 def test_reader_expectations_fail_without_prompt(repo, field, value):
82 r = command('-C', repo, 'next', '--'+field, value)
83 assert r.returncode != 0 and not r.stdout
84
85
86 def test_missing_next_is_not_regenerated(repo):
87 (repo / 'docs/NEXT.md').unlink()
88 r = command('-C', repo, 'next')
89 assert r.returncode != 0 and not r.stdout
90 state = command('-C', repo, 'status', '--json')
91 assert state.returncode == 1
92 assert 'file_missing' in json.loads(state.stdout)['next_error']
93 assert command('-C', repo, 'sync').returncode == 0
94 assert not (repo / 'docs/NEXT.md').exists()
95
96
97 @pytest.mark.parametrize('text', ['not frontmatter', '---\nrepo_id: [broken\n---\nbody',
98 '---\nschema: 1\nschema: 1\n---\nprompt', '---\n{}\n---\nprompt', ''])
99 def test_malformed_next_fails_closed(repo, text):
100 (repo / 'docs/NEXT.md').write_text(text)
101 r = command('-C', repo, 'next')
102 assert r.returncode != 0 and not r.stdout
103
104
105 def test_hand_edit_of_prompt_detected(repo):
106 p = repo / 'docs/NEXT.md'
107 p.write_text(p.read_text() + 'Stale accidental edit\n')
108 r = command('-C', repo, 'next')
109 assert r.returncode != 0 and 'prompt_digest_mismatch' in r.stderr
110
111
112 def test_branch_and_head_freshness(repo):
113 subprocess.run(['/usr/bin/git','-C',str(repo),'switch','-c','changed'], check=True, capture_output=True)
114 r = command('-C', repo, 'next')
115 assert r.returncode != 0 and 'branch_mismatch' in r.stderr
116 subprocess.run(['/usr/bin/git','-C',str(repo),'switch','main'], check=True, capture_output=True)
117 subprocess.run(['/usr/bin/git','-C',str(repo),'add','.'], check=True)
118 # Preserve the legacy tracked-NEXT closing-commit exception explicitly.
119 # New init now excludes checkout-local NEXT from ordinary git add.
120 subprocess.run(['/usr/bin/git','-C',str(repo),'add','-f','docs/NEXT.md'], check=True)
121 subprocess.run(['/usr/bin/git','-C',str(repo),'-c','core.hooksPath=/dev/null','commit','-m','publish NEXT'], check=True, capture_output=True)
122 assert command('-C', repo, 'next').returncode == 0
123 subprocess.run(['/usr/bin/git','-C',str(repo),'-c','core.hooksPath=/dev/null','commit','--allow-empty','-m','later work'], check=True, capture_output=True)
124 r = command('-C', repo, 'next')
125 assert r.returncode != 0 and 'next_stale_head' in r.stderr
126
127
128 def test_detached_head_refused(repo):
129 subprocess.run(['/usr/bin/git','-C',str(repo),'checkout','--detach'], check=True, capture_output=True)
130 r = command('-C', repo, 'next')
131 assert r.returncode != 0 and not r.stdout and 'detached_head' in r.stderr
132
133
134 def test_prompt_fences_are_contained(repo):
135 write_action(repo, 'Example:\n```text\ncontent\n```\n')
136 r = command('-C', repo, 'next')
137 assert r.returncode == 0
138 assert '\n````text\n' in r.stdout and r.stdout.endswith('````\n')
139
140
141 def test_prompt_cannot_introduce_contradictory_binding(repo):
142 state = status(repo)
143 r = write_action(repo, 'Model: a different model\n', action_id='OTHER')
144 assert r.returncode != 0
145 assert status(repo)['next_sha256'] == state['next_sha256']
146
147
148 def test_required_writer_arguments(repo):
149 r = command('-C', repo, 'next-write')
150 assert r.returncode != 0 and not r.stdout
151
152
153 def test_runtime_hash_change_requires_explicit_sync(repo):
154 config_path = repo / '.overseer/config.yaml'
155 config = yaml.safe_load(config_path.read_text())
156 identity = config['repo']['id']
157 config['runtime']['sha256'] = '0'*64
158 config_path.write_text(yaml.safe_dump(config))
159 assert command('-C',repo,'next').returncode != 0
160 before = fingerprint(repo)
161 dry = command('-C',repo,'sync','--dry-run','--json')
162 assert dry.returncode == 0 and '.overseer/config.yaml' in json.loads(dry.stdout)['changed']
163 assert fingerprint(repo) == before
164 assert command('-C',repo,'sync').returncode == 0
165 assert status(repo)['repository']['id'] == identity
166
167
168 def test_source_and_compat_launcher(repo):
169 for launcher in (KIT/'cli/ok', KIT/'cli/overseer'):
170 assert command('-C',repo,'status',executable=launcher).returncode == 0
171 assert (KIT/'.venv/bin/python3').is_symlink()
172
173
174 def test_unsupported_commands_cannot_run(repo):
175 for cmd in ('pr-land','governance-sync','upgrade-regime','app'):
176 r = command('-C',repo,cmd)
177 assert r.returncode != 0 and not r.stdout
178
179
180 def test_unborn_repository_first_commit_is_supported(tmp_path):
181 root=tmp_path/'unborn';root.mkdir()
182 subprocess.run(['/usr/bin/git','-C',str(root),'init','-b','main'],check=True,capture_output=True)
183 assert command('-C',root,'init').returncode==0
184 assert status(root)['head']=='unborn'
185 subprocess.run(['/usr/bin/git','-C',str(root),'add','.'],check=True)
186 subprocess.run(['/usr/bin/git','-C',str(root),'-c','user.name=Fixture','-c',
187 '[email protected]','-c','core.hooksPath=/dev/null','commit','-m','first'],check=True,capture_output=True)
188 r = command('-C',root,'next')
189 assert r.returncode == 2 and 'next_stale_head' in r.stderr
190 # The ignored local handoff is deliberately published after the first commit.
191 import yaml
192 cfg = yaml.safe_load((root/'.overseer/config.yaml').read_bytes())
193 from cli.v1_io import digest
194 (root/'prompt.txt').write_text('First post-commit task.\n')
195 r = command('-C',root,'next-write','--repo-id',cfg['repo']['id'], '--branch','main',
196 '--lane','product','--model','GPT-6 Astra','--action-id','FIRST',
197 '--action-kind','implement','--expect-next',digest((root/'docs/NEXT.md').read_bytes()),
198 '--prompt-file','prompt.txt')
199 assert r.returncode == 0, r.stderr
200 assert command('-C',root,'next').returncode == 0
201
202
203 def test_git_worktree_supported(repo,tmp_path):
204 # A separate linked worktree has its own checkout identity, never inherits
205 # the main checkout's bound config even though Git history is shared.
206 other=tmp_path/'linked'
207 subprocess.run(['/usr/bin/git','-C',str(repo),'worktree','add','-b','linked',str(other)],check=True,capture_output=True)
208 assert command('-C',other,'init').returncode==0
209 assert status(other)['branch']=='linked'
210 assert status(other)['repository']['id'] != status(repo)['repository']['id']
211
212
213 @pytest.mark.parametrize('section,field,value', [('repo','id','bad'),('context','lane','bad/lane'),
214 ('context','model',''),('runtime','version','wrong')])
215 def test_malformed_config_refused(repo,section,field,value):
216 path=repo/'.overseer/config.yaml';config=yaml.safe_load(path.read_text())
217 config[section][field]=value;path.write_text(yaml.safe_dump(config))
218 r=command('-C',repo,'next')
219 assert r.returncode!=0 and not r.stdout
220
221
222 def test_duplicate_config_key_refused(repo):
223 p=repo/'.overseer/config.yaml';p.write_text(p.read_text()+'schema: 1\n')
224 r=command('-C',repo,'next')
225 assert r.returncode!=0 and 'duplicate_or_invalid_key' in r.stderr
226
227
228 def test_bad_hook_event_refuses_cleanly(repo):
229 hook=repo/'.cursor/hooks/session-start-next.sh'
230 for event in ('[]','{broken','{"workspace_roots":[17]}','{"workspace_roots":"wrong"}'):
231 r=command(cwd=repo,executable=hook,input=event)
232 assert r.returncode!=0 and not r.stdout and 'Traceback' not in r.stderr
233
234
235 def test_sync_dry_run_plans_missing_launcher_without_writes(repo):
236 from tests.v1.test_commands import fingerprint
237 path=repo/'.overseer/bin/ok';path.unlink();path.parent.rmdir()
238 before=fingerprint(repo)
239 r=command('-C',repo,'sync','--dry-run','--json')
240 assert r.returncode==0,r.stderr
241 assert '.overseer/bin/ok' in json.loads(r.stdout)['changed']
242 assert fingerprint(repo)==before and not path.parent.exists()
243 assert command('-C',repo,'sync').returncode==0
244 assert command('next',cwd=repo,executable=path).returncode==0