syncade 0.6.2__py3-none-any.whl
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.
- syncade/__init__.py +3 -0
- syncade/__main__.py +6 -0
- syncade/adapters/__init__.py +0 -0
- syncade/adapters/anthropic.py +457 -0
- syncade/adapters/base.py +221 -0
- syncade/adapters/fake.py +73 -0
- syncade/adapters/fake_common.py +29 -0
- syncade/adapters/fake_producer_audit_draft.py +460 -0
- syncade/adapters/fake_reviewer_synth.py +310 -0
- syncade/adapters/openai.py +484 -0
- syncade/adapters/openai_parsing.py +119 -0
- syncade/adapters/producer.py +221 -0
- syncade/adapters/producer_anthropic.py +300 -0
- syncade/adapters/producer_openai.py +226 -0
- syncade/adapters/registry.py +81 -0
- syncade/auth_check.py +554 -0
- syncade/auth_preflight.py +342 -0
- syncade/base_resolution.py +214 -0
- syncade/billing.py +141 -0
- syncade/checks_config.py +113 -0
- syncade/cli/__init__.py +546 -0
- syncade/cli/auth_gate.py +59 -0
- syncade/cli/config_keys.py +135 -0
- syncade/cli/config_list.py +82 -0
- syncade/cli/config_menu_rows.py +166 -0
- syncade/cli/config_mode.py +609 -0
- syncade/cli/config_overrides.py +122 -0
- syncade/cli/config_tui.py +476 -0
- syncade/cli/doctor_mode.py +72 -0
- syncade/cli/gc_mode.py +109 -0
- syncade/cli/install_skill.py +514 -0
- syncade/cli/metrics_mode.py +363 -0
- syncade/cli/modes.py +573 -0
- syncade/cli/parser.py +450 -0
- syncade/cli/parser_types.py +137 -0
- syncade/cli/paths.py +38 -0
- syncade/cli/preflight_paths.py +90 -0
- syncade/cli/resolve.py +116 -0
- syncade/cli/resume_mode.py +324 -0
- syncade/cli/toml_writer.py +410 -0
- syncade/cli/validate.py +421 -0
- syncade/config.py +478 -0
- syncade/config_auth.py +310 -0
- syncade/config_cold.py +209 -0
- syncade/config_gc.py +55 -0
- syncade/config_loader.py +182 -0
- syncade/config_loop.py +282 -0
- syncade/config_producer.py +222 -0
- syncade/config_retry.py +49 -0
- syncade/config_types.py +59 -0
- syncade/diff_filter.py +437 -0
- syncade/dispatcher.py +571 -0
- syncade/doctor.py +425 -0
- syncade/doctor_env.py +218 -0
- syncade/doctor_preview.py +524 -0
- syncade/doctor_types.py +28 -0
- syncade/exit_codes.py +82 -0
- syncade/findings.py +242 -0
- syncade/findings_json.py +456 -0
- syncade/gc.py +211 -0
- syncade/gc_execute.py +372 -0
- syncade/gc_protection.py +129 -0
- syncade/gc_types.py +50 -0
- syncade/gc_worktrees.py +200 -0
- syncade/git_object_id.py +12 -0
- syncade/git_preconditions.py +389 -0
- syncade/logging.py +289 -0
- syncade/metrics/__init__.py +32 -0
- syncade/metrics/aggregate.py +550 -0
- syncade/metrics/schema.py +221 -0
- syncade/orchestrator/__init__.py +61 -0
- syncade/orchestrator/_runs_dir.py +24 -0
- syncade/orchestrator/branch_advance.py +165 -0
- syncade/orchestrator/branch_guard.py +98 -0
- syncade/orchestrator/budget.py +107 -0
- syncade/orchestrator/escalation_coverage.py +81 -0
- syncade/orchestrator/loop.py +611 -0
- syncade/orchestrator/loop_dispatch_check.py +112 -0
- syncade/orchestrator/loop_finalize.py +404 -0
- syncade/orchestrator/loop_preflight.py +131 -0
- syncade/orchestrator/loop_resume.py +91 -0
- syncade/orchestrator/loop_rmtree.py +70 -0
- syncade/orchestrator/loop_round_step.py +599 -0
- syncade/orchestrator/prior_round.py +336 -0
- syncade/orchestrator/producer_phase.py +169 -0
- syncade/orchestrator/results.py +306 -0
- syncade/orchestrator/resume.py +96 -0
- syncade/orchestrator/resume_load.py +483 -0
- syncade/orchestrator/resume_plan.py +554 -0
- syncade/orchestrator/resume_target.py +215 -0
- syncade/orchestrator/resume_types.py +182 -0
- syncade/orchestrator/reviewer_template_failure.py +99 -0
- syncade/orchestrator/round.py +573 -0
- syncade/orchestrator/round_checks.py +91 -0
- syncade/orchestrator/round_no_changes.py +369 -0
- syncade/orchestrator/round_predispatch.py +212 -0
- syncade/orchestrator/verdict.py +279 -0
- syncade/persistence/__init__.py +189 -0
- syncade/persistence/_atomic.py +33 -0
- syncade/persistence/_clusters.py +70 -0
- syncade/persistence/_findings_verdict.py +201 -0
- syncade/persistence/_markdown.py +286 -0
- syncade/persistence/_validation.py +37 -0
- syncade/persistence/checks.py +249 -0
- syncade/persistence/decision_needed.py +289 -0
- syncade/persistence/findings_md.py +389 -0
- syncade/persistence/handoff.py +389 -0
- syncade/persistence/handoff_classify.py +196 -0
- syncade/persistence/last_reviewed.py +67 -0
- syncade/persistence/loop_manifest.py +165 -0
- syncade/persistence/loop_summary.py +352 -0
- syncade/persistence/loop_summary_text.py +428 -0
- syncade/persistence/producer.py +250 -0
- syncade/persistence/reviewer.py +198 -0
- syncade/persistence/round_manifest.py +238 -0
- syncade/persistence/run_init.py +153 -0
- syncade/persistence/run_summary.py +585 -0
- syncade/persistence/run_summary_next_steps.py +443 -0
- syncade/persistence/synth.py +242 -0
- syncade/persistence/test_run.py +152 -0
- syncade/presets.py +36 -0
- syncade/pricing_config.py +72 -0
- syncade/process.py +600 -0
- syncade/producer.py +189 -0
- syncade/producer_attempt.py +463 -0
- syncade/producer_escalation.py +146 -0
- syncade/producer_git.py +199 -0
- syncade/producer_result.py +205 -0
- syncade/prompts.py +448 -0
- syncade/prompts_loader.py +238 -0
- syncade/retry.py +159 -0
- syncade/run_inputs.py +40 -0
- syncade/run_status.py +198 -0
- syncade/selfcheck.py +471 -0
- syncade/skills/claude/README.md +221 -0
- syncade/skills/claude/SKILL.md +625 -0
- syncade/skills/codex/README.md +116 -0
- syncade/skills/codex/SKILL.md +574 -0
- syncade/snapshot.py +598 -0
- syncade/spec_audit.py +437 -0
- syncade/spec_audit_schema.py +190 -0
- syncade/spec_draft.py +423 -0
- syncade/spec_source.py +135 -0
- syncade/synthesis.py +428 -0
- syncade/synthesis_clusters.py +203 -0
- syncade/synthesis_repair.py +230 -0
- syncade/synthesis_schema.py +65 -0
- syncade/synthesizer/__init__.py +38 -0
- syncade/synthesizer/constants.py +33 -0
- syncade/synthesizer/driver.py +531 -0
- syncade/synthesizer/rendering.py +63 -0
- syncade/synthesizer/result.py +73 -0
- syncade/synthesizer/validation.py +421 -0
- syncade/synthesizer/workspace.py +208 -0
- syncade/templates/presets/balanced.toml +13 -0
- syncade/templates/presets/cheap.toml +12 -0
- syncade/templates/presets/thorough.toml +9 -0
- syncade/templates/producer.md +231 -0
- syncade/templates/reviewer.md +279 -0
- syncade/templates/reviewer_adversarial.md +164 -0
- syncade/templates/reviewer_codex.md +165 -0
- syncade/templates/spec_audit.md +168 -0
- syncade/templates/spec_draft.md +62 -0
- syncade/templates/synthesizer.md +204 -0
- syncade/test_runner.py +476 -0
- syncade/test_runner_classify.py +98 -0
- syncade/transcript.py +150 -0
- syncade/usage.py +407 -0
- syncade/worktree.py +497 -0
- syncade/worktree_env.py +133 -0
- syncade/worktree_paths.py +139 -0
- syncade-0.6.2.dist-info/METADATA +314 -0
- syncade-0.6.2.dist-info/RECORD +177 -0
- syncade-0.6.2.dist-info/WHEEL +5 -0
- syncade-0.6.2.dist-info/entry_points.txt +2 -0
- syncade-0.6.2.dist-info/licenses/LICENSE +202 -0
- syncade-0.6.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
"""Reviewer + synthesizer test doubles.
|
|
2
|
+
|
|
3
|
+
:class:`FakeAdapter` and :class:`FakeSynthesizerAdapter` cover the reviewer and
|
|
4
|
+
synthesizer adapter surfaces, plus their zero-finding canned-output defaults.
|
|
5
|
+
Re-exported from ``fake.py``.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
import threading
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
from syncade.adapters.base import Invocation
|
|
15
|
+
from syncade.config import ReviewerConfig
|
|
16
|
+
from syncade.findings import ReviewerOutput
|
|
17
|
+
from syncade.process import SubprocessResult
|
|
18
|
+
from syncade.synthesis import SynthesizerOutput
|
|
19
|
+
from syncade.worktree_env import worktree_scoped_env
|
|
20
|
+
|
|
21
|
+
from .fake_common import _noop_argv
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _default_canned_output() -> ReviewerOutput:
|
|
25
|
+
"""The conventional zero-finding SHIP verdict used when callers don't
|
|
26
|
+
supply a ``canned_output``. Returned as a fresh instance each call so
|
|
27
|
+
mutation by one test doesn't leak into the next.
|
|
28
|
+
|
|
29
|
+
The narrative-surface fields (``summary``, ``priority_order``,
|
|
30
|
+
``coverage_gaps``, ``dismissed_concerns``) are required on
|
|
31
|
+
:class:`ReviewerOutput`, so the test default populates a minimal
|
|
32
|
+
valid combination: a non-empty ``summary``, an empty
|
|
33
|
+
``priority_order`` (correct for zero findings), and empty
|
|
34
|
+
coverage/dismissal lists.
|
|
35
|
+
"""
|
|
36
|
+
return ReviewerOutput(
|
|
37
|
+
verdict="SHIP",
|
|
38
|
+
findings=[],
|
|
39
|
+
summary="fake reviewer canned SHIP",
|
|
40
|
+
priority_order=[],
|
|
41
|
+
coverage_gaps=[],
|
|
42
|
+
dismissed_concerns=[],
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class FakeAdapter:
|
|
47
|
+
"""In-memory test double for :class:`ReviewerAdapter`.
|
|
48
|
+
|
|
49
|
+
Configured at construction time. After use, ``self.invocations``
|
|
50
|
+
holds every ``build_invocation`` call's arguments (if recording is
|
|
51
|
+
enabled), which dispatcher tests can inspect to assert correct
|
|
52
|
+
fan-out behavior.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
canned_output: The :class:`ReviewerOutput` to return from
|
|
56
|
+
``parse_output``. Defaults to a zero-finding SHIP verdict.
|
|
57
|
+
canned_exception: If supplied, ``parse_output`` raises this
|
|
58
|
+
instead of returning. Use to exercise dispatcher
|
|
59
|
+
failure-handling paths (for example reviewer invocation or output
|
|
60
|
+
parse errors).
|
|
61
|
+
record_invocations: If True (default), every ``build_invocation``
|
|
62
|
+
call is appended to ``self.invocations`` as a tuple of
|
|
63
|
+
``(reviewer_config, worktree_path, prompt)``. Disable to
|
|
64
|
+
save memory in tests that fan out heavily.
|
|
65
|
+
canned_auth_exception: If supplied, ``check_auth`` raises this.
|
|
66
|
+
Use to test dispatcher auth-fail-fast behavior without
|
|
67
|
+
spawning a real subprocess. ``check_auth`` always
|
|
68
|
+
increments ``self.check_auth_calls`` regardless, so
|
|
69
|
+
dispatcher tests can assert the pre-flight phase ran.
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
name = "fake"
|
|
73
|
+
|
|
74
|
+
def __init__(
|
|
75
|
+
self,
|
|
76
|
+
canned_output: ReviewerOutput | None = None,
|
|
77
|
+
canned_exception: Exception | None = None,
|
|
78
|
+
record_invocations: bool = True,
|
|
79
|
+
canned_auth_exception: Exception | None = None,
|
|
80
|
+
) -> None:
|
|
81
|
+
self.canned_output = (
|
|
82
|
+
canned_output if canned_output is not None else _default_canned_output()
|
|
83
|
+
)
|
|
84
|
+
self.canned_exception = canned_exception
|
|
85
|
+
self.canned_auth_exception = canned_auth_exception
|
|
86
|
+
self.record_invocations = record_invocations
|
|
87
|
+
self.invocations: list[tuple[ReviewerConfig, Path, str]] = []
|
|
88
|
+
self.check_auth_calls = 0
|
|
89
|
+
self.parse_output_calls = 0
|
|
90
|
+
# Guards check_auth_calls, parse_output_calls, and invocations
|
|
91
|
+
# against races when the dispatcher calls into this adapter
|
|
92
|
+
# from multiple worker threads. See the fake module docstring.
|
|
93
|
+
self._lock = threading.Lock()
|
|
94
|
+
|
|
95
|
+
def build_invocation(
|
|
96
|
+
self,
|
|
97
|
+
reviewer_config: ReviewerConfig,
|
|
98
|
+
worktree_path: Path,
|
|
99
|
+
prompt: str,
|
|
100
|
+
) -> Invocation:
|
|
101
|
+
"""Record the call and return a no-op shell :class:`Invocation`.
|
|
102
|
+
|
|
103
|
+
The returned Invocation has ``cwd = worktree_path`` and the
|
|
104
|
+
caller's environment so a dispatcher that actually executes it
|
|
105
|
+
still gets a clean exit-0 SubprocessResult on platforms where
|
|
106
|
+
``/bin/true`` (or ``cmd /c exit 0``) exists.
|
|
107
|
+
"""
|
|
108
|
+
if self.record_invocations:
|
|
109
|
+
with self._lock:
|
|
110
|
+
self.invocations.append((reviewer_config, worktree_path, prompt))
|
|
111
|
+
return Invocation(
|
|
112
|
+
argv=_noop_argv(),
|
|
113
|
+
cwd=worktree_path,
|
|
114
|
+
# mirror the real reviewer adapters' worktree-scoped env so
|
|
115
|
+
# this double stays faithful. (The synth/auditor fakes deliberately
|
|
116
|
+
# keep the bare env — their real counterparts run cold in isolated
|
|
117
|
+
# tempdirs, not worktrees.)
|
|
118
|
+
env=worktree_scoped_env(worktree_path),
|
|
119
|
+
stdin_text=None,
|
|
120
|
+
timeout_seconds=None,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
def parse_output(self, result: SubprocessResult) -> ReviewerOutput:
|
|
124
|
+
"""Return the canned output, or raise the canned exception.
|
|
125
|
+
|
|
126
|
+
Increments ``self.parse_output_calls`` so tests that need to
|
|
127
|
+
assert "this code path was (not) reached" — notably the
|
|
128
|
+
auth-fail-fast tests, which must verify that NO reviewer's
|
|
129
|
+
parse_output ran when pre-flight failed — can do so directly
|
|
130
|
+
rather than inferring it from ``invocations``.
|
|
131
|
+
|
|
132
|
+
The ``result`` argument is ignored entirely — that's the whole
|
|
133
|
+
point of the fake. If you need parse behavior that varies with
|
|
134
|
+
``result``, write a real adapter or a custom subclass.
|
|
135
|
+
"""
|
|
136
|
+
with self._lock:
|
|
137
|
+
self.parse_output_calls += 1
|
|
138
|
+
if self.canned_exception is not None:
|
|
139
|
+
raise self.canned_exception
|
|
140
|
+
return self.canned_output
|
|
141
|
+
|
|
142
|
+
def extract_final_text(
|
|
143
|
+
self,
|
|
144
|
+
result: SubprocessResult,
|
|
145
|
+
*,
|
|
146
|
+
empty_output_exception_class: type[Exception],
|
|
147
|
+
) -> str:
|
|
148
|
+
"""Return the canned output as JSON text, or raise the canned
|
|
149
|
+
exception.
|
|
150
|
+
|
|
151
|
+
Serializing via ``model_dump_json`` (rather than returning some
|
|
152
|
+
placeholder string) keeps the identity a real adapter guarantees:
|
|
153
|
+
``parse_output(r)`` == ``parse_reviewer_output(extract_final_text(r))``.
|
|
154
|
+
A fake that broke that identity would let a caller pass tests against
|
|
155
|
+
the fake and fail against ``claude``.
|
|
156
|
+
|
|
157
|
+
``empty_output_exception_class`` is accepted for protocol symmetry but
|
|
158
|
+
ignored — the fake never has empty output. To simulate that case, pass
|
|
159
|
+
the relevant error as ``canned_exception``.
|
|
160
|
+
|
|
161
|
+
Deliberately does NOT touch ``parse_output_calls``: the auth-fail-fast
|
|
162
|
+
dispatcher tests assert that counter is 0 to prove no reviewer parsed
|
|
163
|
+
anything, and a different method inflating it would quietly rot that
|
|
164
|
+
assertion.
|
|
165
|
+
"""
|
|
166
|
+
del result, empty_output_exception_class # explicitly unused
|
|
167
|
+
if self.canned_exception is not None:
|
|
168
|
+
raise self.canned_exception
|
|
169
|
+
return self.canned_output.model_dump_json()
|
|
170
|
+
|
|
171
|
+
def check_auth(self) -> None:
|
|
172
|
+
"""Record the call and raise the canned auth exception if set.
|
|
173
|
+
|
|
174
|
+
Increments ``self.check_auth_calls`` on every invocation so
|
|
175
|
+
dispatcher tests can assert the pre-flight phase ran exactly
|
|
176
|
+
once per adapter. If ``canned_auth_exception`` was supplied at
|
|
177
|
+
construction time, it's raised here — this is how dispatcher
|
|
178
|
+
tests simulate "one reviewer's auth is broken" without spawning
|
|
179
|
+
real subprocesses.
|
|
180
|
+
"""
|
|
181
|
+
with self._lock:
|
|
182
|
+
self.check_auth_calls += 1
|
|
183
|
+
if self.canned_auth_exception is not None:
|
|
184
|
+
raise self.canned_auth_exception
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
# ---------------------------------------------------------------------------
|
|
188
|
+
# Synthesizer test double
|
|
189
|
+
# ---------------------------------------------------------------------------
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _default_canned_synth_output() -> SynthesizerOutput:
|
|
193
|
+
"""The conventional zero-finding SynthesizerOutput used when
|
|
194
|
+
callers don't supply a ``canned_output``. Returned as a fresh
|
|
195
|
+
instance each call so mutation by one test doesn't leak into the
|
|
196
|
+
next.
|
|
197
|
+
"""
|
|
198
|
+
return SynthesizerOutput(
|
|
199
|
+
consolidated_findings=[],
|
|
200
|
+
synthesis_summary="fake synthesizer canned: nothing to consolidate",
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
class FakeSynthesizerAdapter:
|
|
205
|
+
"""In-memory test double for the synthesizer codex adapter shape.
|
|
206
|
+
|
|
207
|
+
The synthesizer module's adapter contract is narrower than the
|
|
208
|
+
full :class:`~syncade.adapters.base.ReviewerAdapter` Protocol —
|
|
209
|
+
it only calls :meth:`build_invocation` and
|
|
210
|
+
:meth:`extract_final_text`. This fake implements that subset.
|
|
211
|
+
|
|
212
|
+
The fake's ``extract_final_text`` produces text that
|
|
213
|
+
will round-trip through :func:`syncade.synthesis.parse_synthesizer_output`
|
|
214
|
+
to yield ``canned_output``. That keeps the test exercising the
|
|
215
|
+
real parser path (so a regression in
|
|
216
|
+
``parse_synthesizer_output`` surfaces in tests that USE
|
|
217
|
+
``FakeSynthesizerAdapter`` even though they aren't reaching for a
|
|
218
|
+
parser-specific assertion).
|
|
219
|
+
|
|
220
|
+
Args:
|
|
221
|
+
canned_output: The :class:`SynthesizerOutput` the fake produces
|
|
222
|
+
when the synthesizer runs successfully. Default: an empty
|
|
223
|
+
consolidation with a non-empty summary string.
|
|
224
|
+
canned_exception: If supplied,
|
|
225
|
+
``extract_final_text`` raises this instead of
|
|
226
|
+
returning text. Use to exercise the synthesizer module's
|
|
227
|
+
failure paths. Pass :class:`syncade.synthesis.SynthesizerOutputError`
|
|
228
|
+
to simulate a parse failure, or a reviewer invocation error to
|
|
229
|
+
simulate a codex subprocess failure.
|
|
230
|
+
record_invocations: If True (default), every
|
|
231
|
+
``build_invocation`` call's ``(reviewer_config,
|
|
232
|
+
worktree_path, prompt)`` tuple is appended to
|
|
233
|
+
``self.invocations`` for test inspection.
|
|
234
|
+
"""
|
|
235
|
+
|
|
236
|
+
name = "openai" # so CodexAdapter.build_invocation's provider validation passes
|
|
237
|
+
"""Matches the codex provider key used by the synthetic ReviewerConfig.
|
|
238
|
+
|
|
239
|
+
The fake adapter does not need this attribute itself; keeping it aligned
|
|
240
|
+
with ``CodexAdapter`` makes test debugging simpler.
|
|
241
|
+
"""
|
|
242
|
+
|
|
243
|
+
def __init__(
|
|
244
|
+
self,
|
|
245
|
+
canned_output: SynthesizerOutput | None = None,
|
|
246
|
+
canned_exception: Exception | None = None,
|
|
247
|
+
record_invocations: bool = True,
|
|
248
|
+
) -> None:
|
|
249
|
+
self.canned_output = (
|
|
250
|
+
canned_output if canned_output is not None else _default_canned_synth_output()
|
|
251
|
+
)
|
|
252
|
+
self.canned_exception = canned_exception
|
|
253
|
+
self.record_invocations = record_invocations
|
|
254
|
+
self.invocations: list[tuple[ReviewerConfig, Path, str]] = []
|
|
255
|
+
self.extract_calls = 0
|
|
256
|
+
|
|
257
|
+
def build_invocation(
|
|
258
|
+
self,
|
|
259
|
+
reviewer_config: ReviewerConfig,
|
|
260
|
+
worktree_path: Path,
|
|
261
|
+
prompt: str,
|
|
262
|
+
) -> Invocation:
|
|
263
|
+
"""Record the call and return an :class:`Invocation` pointing
|
|
264
|
+
at a no-op shell command so ``run_subprocess`` still produces a clean
|
|
265
|
+
exit-0 :class:`SubprocessResult`.
|
|
266
|
+
|
|
267
|
+
clarification: ``worktree_path`` is the **synthesizer's
|
|
268
|
+
cold-isolation tempdir workspace**, NOT ``repo_root``. As of
|
|
269
|
+
+ , ``run_synthesizer`` provisions an isolated
|
|
270
|
+
tempdir, copies the PR doc into it, git-inits it, and passes
|
|
271
|
+
the WORKSPACE path here. Tests asserting on the recorded
|
|
272
|
+
path should check the ``syncade-synth-`` tempdir prefix
|
|
273
|
+
(see ``test_synthesizer_runs_in_isolated_tempdir_workspace``).
|
|
274
|
+
"""
|
|
275
|
+
if self.record_invocations:
|
|
276
|
+
self.invocations.append((reviewer_config, worktree_path, prompt))
|
|
277
|
+
return Invocation(
|
|
278
|
+
argv=_noop_argv(),
|
|
279
|
+
cwd=worktree_path,
|
|
280
|
+
env=dict(os.environ),
|
|
281
|
+
stdin_text=None,
|
|
282
|
+
timeout_seconds=None,
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
def extract_final_text(
|
|
286
|
+
self,
|
|
287
|
+
result: SubprocessResult,
|
|
288
|
+
*,
|
|
289
|
+
empty_output_exception_class: type[Exception],
|
|
290
|
+
) -> str:
|
|
291
|
+
"""Return the canned output as JSON text, or raise the canned
|
|
292
|
+
exception.
|
|
293
|
+
|
|
294
|
+
The synthesizer module calls this immediately after
|
|
295
|
+
``run_subprocess``; the returned text is then fed to
|
|
296
|
+
:func:`syncade.synthesis.parse_synthesizer_output`. Producing the canned
|
|
297
|
+
output via ``model_dump_json`` exercises the parser end-to-end.
|
|
298
|
+
|
|
299
|
+
``empty_output_exception_class`` is accepted for protocol
|
|
300
|
+
symmetry with :meth:`CodexAdapter.extract_final_text`
|
|
301
|
+
but ignored — the fake never has an empty agent_message; if
|
|
302
|
+
the caller wants to simulate that case, they pass an instance
|
|
303
|
+
of :class:`SynthesizerOutputError` as ``canned_exception``
|
|
304
|
+
directly.
|
|
305
|
+
"""
|
|
306
|
+
del result, empty_output_exception_class # explicitly unused
|
|
307
|
+
self.extract_calls += 1
|
|
308
|
+
if self.canned_exception is not None:
|
|
309
|
+
raise self.canned_exception
|
|
310
|
+
return self.canned_output.model_dump_json()
|