Skip to content

Commit c39f1b2

Browse files
mrjfCopilot
andauthored
Require golden fixture cutover gates (#105)
* Require golden fixture cutover gates * Fix test file formatting for CI Co-authored-by: mrjf <180956+mrjf@users.noreply.github.com> * Fix parity coverage manifest for new Python tests Co-authored-by: mrjf <180956+mrjf@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: mrjf <180956+mrjf@users.noreply.github.com>
1 parent e0eaf25 commit c39f1b2

5 files changed

Lines changed: 214 additions & 106 deletions

File tree

.crane/scripts/score.go

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,9 @@ type CutoverGates struct {
6363
FunctionalContracts float64 `json:"functional_contracts"`
6464
StateDiffContracts float64 `json:"state_diff_contracts"`
6565
PythonBehaviorContracts float64 `json:"python_behavior_contracts"`
66+
GoldenFixtureCorpus string `json:"golden_fixture_corpus"`
67+
AllGoGoldenTests string `json:"all_go_golden_tests"`
68+
NoPythonRuntime string `json:"no_python_runtime_dependency"`
6669
KnownExceptions int `json:"known_exceptions"`
6770
GoTests string `json:"go_tests"`
6871
PythonTests string `json:"python_tests"`
@@ -99,6 +102,9 @@ type Score struct {
99102
PythonTestsPassing bool `json:"python_tests_passing"`
100103
GoTestsPassing bool `json:"go_tests_passing"`
101104
BenchmarksPassing bool `json:"benchmarks_passing"`
105+
GoldenFixtureCorpus bool `json:"golden_fixture_corpus"`
106+
AllGoGoldenTests bool `json:"all_go_golden_tests"`
107+
NoPythonRuntime bool `json:"no_python_runtime_dependency"`
102108
ParityPassing int `json:"parity_passing"`
103109
ParityTotal int `json:"parity_total"`
104110
SourceTestsPassing int `json:"source_tests_passing"`
@@ -143,6 +149,9 @@ func computeScore(input scanInput, getenv getenvFunc) (Score, error) {
143149
functional := RatioGate{}
144150
stateDiff := RatioGate{}
145151
behaviorContracts := RatioGate{}
152+
goldenFixtureCorpus := BoolGate{}
153+
allGoGoldenTests := BoolGate{}
154+
noPythonRuntime := BoolGate{}
146155

147156
for scanner.Scan() {
148157
line := scanner.Text()
@@ -151,7 +160,21 @@ func computeScore(input scanInput, getenv getenvFunc) (Score, error) {
151160
}
152161
if gate, ok := parseGateEvent(line); ok {
153162
eventsSeen++
154-
applyGateEvent(gate, &pythonReference, &surface, &help, &functional, &stateDiff, &behaviorContracts, &knownExceptions, &pythonTests, &benchmarks)
163+
applyGateEvent(
164+
gate,
165+
&pythonReference,
166+
&surface,
167+
&help,
168+
&functional,
169+
&stateDiff,
170+
&behaviorContracts,
171+
&goldenFixtureCorpus,
172+
&allGoGoldenTests,
173+
&noPythonRuntime,
174+
&knownExceptions,
175+
&pythonTests,
176+
&benchmarks,
177+
)
155178
continue
156179
}
157180

@@ -163,7 +186,21 @@ func computeScore(input scanInput, getenv getenvFunc) (Score, error) {
163186

164187
if ev.Output != "" {
165188
if gate, ok := parseGateEvent(ev.Output); ok {
166-
applyGateEvent(gate, &pythonReference, &surface, &help, &functional, &stateDiff, &behaviorContracts, &knownExceptions, &pythonTests, &benchmarks)
189+
applyGateEvent(
190+
gate,
191+
&pythonReference,
192+
&surface,
193+
&help,
194+
&functional,
195+
&stateDiff,
196+
&behaviorContracts,
197+
&goldenFixtureCorpus,
198+
&allGoGoldenTests,
199+
&noPythonRuntime,
200+
&knownExceptions,
201+
&pythonTests,
202+
&benchmarks,
203+
)
167204
}
168205
if n, ok := approvedExceptionCount(ev.Output); ok && n > knownExceptions {
169206
knownExceptions = n
@@ -253,6 +290,9 @@ func computeScore(input scanInput, getenv getenvFunc) (Score, error) {
253290
FunctionalContracts: functional.Percent(),
254291
StateDiffContracts: stateDiff.Percent(),
255292
PythonBehaviorContracts: behaviorContracts.Percent(),
293+
GoldenFixtureCorpus: passFail(goldenFixtureCorpus.OK()),
294+
AllGoGoldenTests: passFail(allGoGoldenTests.OK()),
295+
NoPythonRuntime: passFail(noPythonRuntime.OK()),
256296
KnownExceptions: knownExceptions,
257297
GoTests: passFail(goTestsPass),
258298
PythonTests: passFail(pythonTests.OK()),
@@ -275,6 +315,9 @@ func computeScore(input scanInput, getenv getenvFunc) (Score, error) {
275315
gates.FunctionalContracts == 1.0 &&
276316
gates.StateDiffContracts == 1.0 &&
277317
gates.PythonBehaviorContracts == 1.0 &&
318+
gates.GoldenFixtureCorpus == "pass" &&
319+
gates.AllGoGoldenTests == "pass" &&
320+
gates.NoPythonRuntime == "pass" &&
278321
gates.KnownExceptions == 0 &&
279322
gates.GoTests == "pass" &&
280323
gates.PythonTests == "pass" &&
@@ -315,6 +358,9 @@ func computeScore(input scanInput, getenv getenvFunc) (Score, error) {
315358
PythonTestsPassing: gates.PythonTests == "pass",
316359
GoTestsPassing: gates.GoTests == "pass",
317360
BenchmarksPassing: gates.Benchmarks == "pass",
361+
GoldenFixtureCorpus: gates.GoldenFixtureCorpus == "pass",
362+
AllGoGoldenTests: gates.AllGoGoldenTests == "pass",
363+
NoPythonRuntime: gates.NoPythonRuntime == "pass",
318364
ParityPassing: metrics.ParityPassing,
319365
ParityTotal: metrics.ParityTotal,
320366
SourceTestsPassing: metrics.SourceTestsPassing,
@@ -344,6 +390,9 @@ func applyGateEvent(
344390
functional *RatioGate,
345391
stateDiff *RatioGate,
346392
behaviorContracts *RatioGate,
393+
goldenFixtureCorpus *BoolGate,
394+
allGoGoldenTests *BoolGate,
395+
noPythonRuntime *BoolGate,
347396
knownExceptions *int,
348397
pythonTests *BoolGate,
349398
benchmarks *RatioGate,
@@ -361,6 +410,12 @@ func applyGateEvent(
361410
*stateDiff = RatioGate{Seen: true, Passing: gate.Passing, Total: gate.Total}
362411
case "python_behavior_contracts":
363412
*behaviorContracts = RatioGate{Seen: true, Passing: gate.Passing, Total: gate.Total}
413+
case "golden_fixture_corpus":
414+
*goldenFixtureCorpus = BoolGate{Seen: true, Passed: gate.Passed}
415+
case "all_go_golden_tests":
416+
*allGoGoldenTests = BoolGate{Seen: true, Passed: gate.Passed}
417+
case "no_python_runtime_dependency":
418+
*noPythonRuntime = BoolGate{Seen: true, Passed: gate.Passed}
364419
case "known_exceptions":
365420
*knownExceptions = gate.Count
366421
case "python_tests":
@@ -412,6 +467,9 @@ func gateResults(gates CutoverGates) []GateResult {
412467
{Name: "functional_contracts", Passing: gates.FunctionalContracts == 1.0},
413468
{Name: "state_diff_contracts", Passing: gates.StateDiffContracts == 1.0},
414469
{Name: "python_behavior_contracts", Passing: gates.PythonBehaviorContracts == 1.0},
470+
{Name: "golden_fixture_corpus", Passing: gates.GoldenFixtureCorpus == "pass"},
471+
{Name: "all_go_golden_tests", Passing: gates.AllGoGoldenTests == "pass"},
472+
{Name: "no_python_runtime_dependency", Passing: gates.NoPythonRuntime == "pass"},
415473
{Name: "python_tests_pass", Passing: gates.PythonTests == "pass"},
416474
{Name: "benchmarks_pass", Passing: gates.Benchmarks == "pass"},
417475
{Name: "no_known_exceptions", Passing: gates.KnownExceptions == 0},

cmd/apm/CUTOVER.md

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,12 @@ via PyInstaller packaging and `pip install apm-cli`.
1313
The Go CLI currently implements:
1414
- `apm --help` / `apm --version` (full parity with Python)
1515
- `apm init [--yes] [PROJECT_NAME]` (functional, creates apm.yml)
16-
- Per-command `--help` for all 26 commands (golden-file verified)
16+
- Per-command `--help` for all 26 commands (initial golden-file coverage)
17+
18+
The checked-in `cmd/apm/testdata/golden/` files are the start of the
19+
cutover corpus, not final completion proof. Final completion requires the
20+
full command matrix below to be represented as committed fixtures and replayed
21+
by Go without invoking the Python runtime.
1722

1823
Most remaining commands are wired at the CLI surface. That is not enough for
1924
cutover. A command that prints success without writing the expected files,
@@ -84,8 +89,18 @@ are true:
8489
4. Python-vs-Go parity tests pass for all commands in the matrix
8590
5. Migration benchmarks pass real fixture-backed command workloads and emit a
8691
passing counted `benchmarks` gate
87-
6. `go build ./cmd/apm` produces a single static binary
88-
7. CI passes on the crane PR branch (`crane/crane-migration-python-to-go-full-apm-cli-rewrite`)
92+
6. The final Python-reference parity run has been frozen into a committed,
93+
versioned golden fixture corpus. The corpus must include CLI inventory,
94+
help and usage output, error output, exit codes, generated files, lockfiles,
95+
config files, managed-file manifests, deterministic cache/config layout, and
96+
audit artifacts for the full command matrix.
97+
7. An all-Go golden replay passes against that corpus with no live Python
98+
oracle. The replay must build `cmd/apm` and compare only the Go binary
99+
against checked-in fixtures.
100+
8. A no-Python-runtime check passes: `APM_PYTHON_BIN` is unset, the Python CLI
101+
is hidden or unavailable to the replay, and the golden replay still passes.
102+
9. `go build ./cmd/apm` produces a single static binary
103+
10. CI passes on the crane PR branch (`crane/crane-migration-python-to-go-full-apm-cli-rewrite`)
89104

90105
## Cutover Steps
91106

@@ -102,13 +117,14 @@ When conditions are met:
102117

103118
## Python Compatibility Shim
104119

105-
Until all commands are implemented in Go, the Python CLI remains the
106-
authoritative `apm` command. The Go binary is available as `apm-go`
107-
for testing.
120+
Until all commands are implemented in Go and the golden replay gate passes, the
121+
Python CLI remains the authoritative `apm` command. The Go binary is available
122+
as `apm-go` for testing.
108123

109-
The shim removal plan: once the command matrix passes functional tests,
110-
the Python entrypoint is replaced by the Go binary in the same PR that
111-
passes the final parity tests.
124+
The shim removal plan: once the command matrix passes functional tests, the
125+
final Python-reference behavior is frozen into golden fixtures. Only after the
126+
all-Go replay passes without a Python runtime can the Python entrypoint be
127+
replaced by the Go binary.
112128

113129
## Timeline
114130

0 commit comments

Comments
 (0)