Skip to content

Commit 946507e

Browse files
LeadGoEngineerPaperclip-Paperclip
andcommitted
feat(compat): add Runner.SkipDataPath for credentialless-CI exporters
The three §4 data-path subtests (JSONIsArray, CSVHasHeader, DefaultIsMarkdown) invoke the binary with a real --format value and expect a clean exit. For exporters whose data path requires credentials that the CI environment does not have — crono's env-var auth, liftoff's stored OAuth token at ~/.config/liftoff-export/auth.json, withings' OAuth refresh token — a vanilla `--format json` invocation under compat's default empty env exits non-zero with "not logged in" before the JSON-array check fires. Today the only escape route is SupportedFormats: []string{}, which semantically claims "we implement no codecs" — misleading, since liftoff/crono do implement markdown+json, just not from a credentialless CI. SkipDataPath is the explicit escape hatch the QUA-15 PR review specifically proposed: when true, the three data-path subtests skip with reason `data-path subtests disabled via Runner.SkipDataPath`, while the parse-level subtests (HelpDocumentsFormatFlag, UnknownFormatFails, FlagValidationIsHermetic) still run because they only exercise flag parsing and expect non-zero exit on the bad value. Composable with SupportedFormats — nil SupportedFormats + SkipDataPath: true is the typical liftoff/crono shape today. Flipping the bool back to false later — once auth is mockable in CI or secrets are provisioned — promotes the codec rows from human- attested to machine-attested per the CONTRACT.md Status table flip plan, without losing the parse-level attestation we get for free today. Self-test (TestRunContract_SkipDataPath_GatesDataSubtests) is adversarial: builds the stub with STUBCLI_FORMATS=__never__ so every --format value is rejected, then runs the full bundle with SkipDataPath: true. Parse-level subtests pass against the rejection side; data-path subtests skip via the guard. Delete the guard in jsonIsArray / csvHasHeader / defaultIsMarkdown and the test fails because the data-path subtests run against a stub that rejects every codec. Unblocks QUA-16 (liftoff onboarding as second consumer of compat/formats) without forcing the misleading SupportedFormats: []string{} workaround. Co-Authored-By: Paperclip <[email protected]>
1 parent 62c4c66 commit 946507e

4 files changed

Lines changed: 93 additions & 6 deletions

File tree

compat/README.md

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ When `compat.Runner.Subcommands` is set, every row above runs once per declared
102102
| `CSVHasHeader` | §4 | `--format csv` exits 0 and emits at least one non-empty line on stdout (the header row, present even on an empty result). |
103103
| `DefaultIsMarkdown` | §4 | No `--format` flag produces byte-identical stdout to `--format markdown`. |
104104

105-
`JSONIsArray`, `CSVHasHeader`, and `DefaultIsMarkdown` invoke the data path with no extra args beyond `--format`. Integrators whose data path needs credentials or other env to succeed pass them via `compat.Runner.Env`. As with `dates`, `subcommand=NAME/...` subtest groups fire when `Subcommands` is set.
105+
`JSONIsArray`, `CSVHasHeader`, and `DefaultIsMarkdown` invoke the data path with no extra args beyond `--format`. Integrators whose data path needs credentials or other env to succeed pass them via `compat.Runner.Env`; integrators whose data path is not runnable in CI at all today set `compat.Runner.SkipDataPath: true` (see below). As with `dates`, `subcommand=NAME/...` subtest groups fire when `Subcommands` is set.
106106

107107
### Partial-codec exporters: `Runner.SupportedFormats`
108108

@@ -123,6 +123,21 @@ Semantics:
123123

124124
**Per-CLI flip plan.** When a consumer wires the bundle into its CI with `SupportedFormats` matching its actual surface, the CONTRACT.md Status table's `--format` row for that CLI flips to **machine** for the codecs it declares. Codecs the CLI does not implement remain human-attested per-cell until the writer lands. Today the Status table has one cell per (CLI, codec) so the flip is per-cell, not per-row.
125125

126+
### Credentialless CI: `Runner.SkipDataPath`
127+
128+
The bundle's three data-path subtests (`JSONIsArray`, `CSVHasHeader`, `DefaultIsMarkdown`) invoke the CLI with a real `--format` value and expect a clean exit — which means the data path has to be runnable in the CI environment. For crono (env-var auth), liftoff (stored OAuth token), and withings (OAuth refresh token), that is not true today: a vanilla `--format json` invocation under `compat`'s default empty env exits non-zero with "not logged in" before the JSON-array check fires.
129+
130+
`compat.Runner.SkipDataPath` is the explicit escape hatch: when true, the data-path subtests skip with reason `data-path subtests disabled via Runner.SkipDataPath`. The parse-level subtests still run because they assert on flag parsing, not on a working data path. This lets an exporter wire the bundle in for parse-level attestation today, then flip the bool back to false later (once auth is mockable in CI or secrets are provisioned) to promote its codec rows from human to machine.
131+
132+
```go
133+
formats.RunContract(t, compat.Runner{
134+
Binary: os.Getenv("EXPORT_CLI_BIN"),
135+
SkipDataPath: true, // CI does not have upstream credentials
136+
})
137+
```
138+
139+
`SkipDataPath` and `SupportedFormats` compose: `nil` SupportedFormats + `SkipDataPath: true` is the typical liftoff/crono shape today (declare the full surface, but skip the data-path subtests until creds are available). Use `SupportedFormats: []string{...}` instead of `SkipDataPath: true` when the CLI structurally lacks a codec — those are two different gaps and should not be confused.
140+
126141
## What it does NOT cover yet
127142

128143
The actual local-midnight semantics of `--since 2026-04-15` (the harmonization that just landed across crono/liftoff/withings) is still **human-attested** in the status table. Asserting it black-box requires either:

compat/compat.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,37 @@ type Runner struct {
7878
// declare "all of §4".
7979
SupportedFormats []string
8080

81+
// SkipDataPath, when true, causes section bundles to skip every
82+
// subtest that requires the CLI's data path to succeed in CI —
83+
// i.e. the subtests that invoke the binary with a real --format
84+
// value and expect a clean exit. The parse-level subtests
85+
// (HelpDocumentsFormatFlag, UnknownFormatFails,
86+
// FlagValidationIsHermetic) still run because they only exercise
87+
// flag parsing and explicitly expect non-zero exit on the bad
88+
// value.
89+
//
90+
// This is the escape hatch for exporters whose data path requires
91+
// credentials that the CI environment does not have. Crono uses
92+
// env-var auth (CRONOMETER_USERNAME/PASSWORD); liftoff uses a
93+
// stored OAuth token at ~/.config/liftoff-export/auth.json;
94+
// withings uses an OAuth refresh token. None of these are present
95+
// in `compat`'s default empty-env invocation, so an unconditional
96+
// `--format json` run exits non-zero with "not logged in" before
97+
// the JSON-array check can run. Setting SkipDataPath: true lets
98+
// the exporter wire the bundle in for parse-level attestation
99+
// without a misleading SupportedFormats: []string{} workaround
100+
// that implies "we implement no codecs".
101+
//
102+
// Flipping it back to false later — once auth is mockable in CI
103+
// or secrets are provisioned — promotes the codec rows from
104+
// human-attested to machine-attested per the CONTRACT.md Status
105+
// table flip plan.
106+
//
107+
// Composable with SupportedFormats: when both are set, the
108+
// codec-specific gate runs first, so SupportedFormats: nil +
109+
// SkipDataPath: true is the typical liftoff/crono shape today.
110+
SkipDataPath bool
111+
81112
// subcommand, when non-empty, is prepended to args on every Run
82113
// call. Set via WithSubcommand; section bundles use it to dispatch
83114
// per-subcommand. Callers do not need to set it directly — set

compat/formats/formats.go

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -181,9 +181,13 @@ func flagValidationIsHermetic(t *testing.T, r compat.Runner) {
181181
// covered implicitly so long as the integrator's data path returns
182182
// successfully.
183183
//
184-
// Skipped (not failed) if "json" is not in Runner.SupportedFormats.
184+
// Skipped (not failed) if "json" is not in Runner.SupportedFormats,
185+
// or if Runner.SkipDataPath is true.
185186
func jsonIsArray(t *testing.T, r compat.Runner) {
186187
t.Helper()
188+
if r.SkipDataPath {
189+
t.Skipf("data-path subtests disabled via Runner.SkipDataPath")
190+
}
187191
if !r.SupportsFormat("json") {
188192
t.Skipf("--format json not declared in Runner.SupportedFormats")
189193
}
@@ -207,12 +211,16 @@ func jsonIsArray(t *testing.T, r compat.Runner) {
207211
// success with "no rows" for CSV — the header row is still required,
208212
// so even a zero-row CSV must have one line.
209213
//
210-
// Skipped (not failed) if "csv" is not in Runner.SupportedFormats.
214+
// Skipped (not failed) if "csv" is not in Runner.SupportedFormats,
215+
// or if Runner.SkipDataPath is true.
211216
// crono-export-cli and liftoff-export-cli are partial-codec exporters
212217
// today; the bundle becomes adoptable for them by declaring
213218
// SupportedFormats: []string{"markdown", "json"}.
214219
func csvHasHeader(t *testing.T, r compat.Runner) {
215220
t.Helper()
221+
if r.SkipDataPath {
222+
t.Skipf("data-path subtests disabled via Runner.SkipDataPath")
223+
}
216224
if !r.SupportsFormat("csv") {
217225
t.Skipf("--format csv not declared in Runner.SupportedFormats")
218226
}
@@ -232,11 +240,14 @@ func csvHasHeader(t *testing.T, r compat.Runner) {
232240
// available without parsing markdown.
233241
//
234242
// Skipped (not failed) if "markdown" is not in
235-
// Runner.SupportedFormats. A CLI that does not declare markdown
236-
// cannot be expected to default to it, and forcing the equality check
237-
// would just measure noise.
243+
// Runner.SupportedFormats, or if Runner.SkipDataPath is true. A CLI
244+
// that does not declare markdown cannot be expected to default to it,
245+
// and forcing the equality check would just measure noise.
238246
func defaultIsMarkdown(t *testing.T, r compat.Runner) {
239247
t.Helper()
248+
if r.SkipDataPath {
249+
t.Skipf("data-path subtests disabled via Runner.SkipDataPath")
250+
}
240251
if !r.SupportsFormat("markdown") {
241252
t.Skipf("--format markdown not declared in Runner.SupportedFormats")
242253
}

compat/formats/formats_test.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,36 @@ func TestRunContract_PartialCodec_SkipsCSV(t *testing.T) {
6464
formats.RunContract(t, r)
6565
}
6666

67+
// TestRunContract_SkipDataPath_GatesDataSubtests pins down the
68+
// Runner.SkipDataPath affordance: when set, the data-path subtests
69+
// (JSONIsArray / CSVHasHeader / DefaultIsMarkdown) skip even though
70+
// the codec is otherwise "supported". This is what lets liftoff /
71+
// crono / withings wire the bundle in for parse-level attestation
72+
// before their data paths are runnable in CI without credentials.
73+
//
74+
// The proof is adversarial. We build the stub with
75+
// STUBCLI_FORMATS=__never__ so every --format value (markdown, json,
76+
// csv, AND the unknown sentinel) is rejected with a non-zero exit.
77+
// The parse-level subtests still pass because they assert on the
78+
// rejection side. With SkipDataPath: true, the bundle is green.
79+
// Remove the SkipDataPath guard in jsonIsArray / csvHasHeader /
80+
// defaultIsMarkdown and this test fails because the data-path
81+
// subtests now run against a stub that rejects every codec.
82+
//
83+
// Critically, SupportedFormats is left nil so this exercises the
84+
// SkipDataPath gate independently of the codec-membership gate. The
85+
// pair compose: nil + SkipDataPath: true is the liftoff/crono shape
86+
// today.
87+
func TestRunContract_SkipDataPath_GatesDataSubtests(t *testing.T) {
88+
bin := buildStub(t)
89+
r := compat.Runner{
90+
Binary: bin,
91+
Env: []string{"STUBCLI_FORMATS=__never__"},
92+
SkipDataPath: true,
93+
}
94+
formats.RunContract(t, r)
95+
}
96+
6797
// TestSupportsFormat documents the nil-vs-empty-vs-subset behavior
6898
// of compat.Runner.SupportsFormat at the package level. The formats
6999
// bundle's skip guards rely on these semantics being stable.

0 commit comments

Comments
 (0)