Skip to content

Commit d6da0b4

Browse files
test(cli): add registry-iterating done-bar + contract gap-fills (#25)
Raises CLI test coverage toward the USER-FLOW test matrix (item 4: "iterate cobra command tree; fail if a command lacks a K-row"). donebar_command_coverage_test.go: - TestDoneBar_EveryCommandCovered walks the LIVE cobra tree (35 cmds) and asserts each is runnable (RunE/Run) + help-bearing (Short) + mapped to a covering test in commandTestMap. A new rootCmd.AddCommand without a map row RED-fails CI — the requested drift guard. Skips cobra-injected builtins (`help`); reverse-checks for stale map rows. - TestDoneBar_TestMapPointsAtRealTests parses the package's *_test.go via go/ast and asserts every mapped test name actually exists, so the map can't rot pointing at a deleted test. contract_gaps_test.go (fills the two real gaps the done-bar maps to): - TestContract_StorageVectorProvisionEndToEnd drives `storage new` / `vector new` through the real command tree (the integration suite only did db/cache/nosql/queue), asserting endpoint mapping, --name forwarding, and the development env default (CLAUDE.md rule 11). - TestContract_LoginUsesCanonicalAuthURL pins K1/A12: login opens the server-provided canonical auth_url from POST /auth/cli verbatim, not a CLI-synthesised URL or the API host. Test-only change. Full race gate green; project coverage 96.0% (>=95); golangci-lint 0 issues (SA5011 test-suppression + Go toolchain pin untouched). Real-backend mutating CLI flows (K6 deploy live) still depend on the W0 test-cohort work and are out of scope here. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent c231284 commit d6da0b4

2 files changed

Lines changed: 411 additions & 0 deletions

File tree

cmd/contract_gaps_test.go

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
package cmd
2+
3+
// contract_gaps_test.go — fills the contract-coverage gaps the done-bar
4+
// (donebar_command_coverage_test.go) maps to:
5+
//
6+
// TestContract_StorageVectorProvisionEndToEnd
7+
// The integration suite drives `db/cache/nosql/queue new` end-to-end
8+
// (TestIntegration_ProvisionAllTypes) but NOT `storage new` / `vector new`
9+
// through the real cobra tree. This asserts both: correct endpoint hit
10+
// (/storage/new, /vector/new via the resource_type the mock records),
11+
// flag→payload mapping (--name forwarded), and the resolved-env default.
12+
//
13+
// TestContract_LoginUsesCanonicalAuthURL
14+
// K1 / A12 (USER-FLOW matrix): `instant login` must open the auth_url the
15+
// server returns from POST /auth/cli — the CANONICAL host — verbatim, not
16+
// a CLI-synthesised one. This pins the device-flow contract: the CLI is a
17+
// dumb relay of the server-provided canonical URL.
18+
//
19+
// Both use the existing hermetic stateful mock (testapi_test.go) and the
20+
// integration harness (integration_test.go) so they run with zero network and
21+
// obey the mandatory resource-cleanup sweep.
22+
23+
import (
24+
"strings"
25+
"testing"
26+
)
27+
28+
// TestContract_StorageVectorProvisionEndToEnd provisions storage and vector
29+
// resources through the REAL command tree and asserts the mock recorded the
30+
// correct resource_type for each — proving `instant storage new` hits
31+
// /storage/new and `instant vector new` hits /vector/new (the mock keys
32+
// resource_type off the endpoint path in endpointResourceType).
33+
func TestContract_StorageVectorProvisionEndToEnd(t *testing.T) {
34+
c := newITContext(t)
35+
36+
cases := []struct {
37+
group string // CLI group: `instant <group> new`
38+
wantType string // resource_type the mock records (== endpoint mapping)
39+
}{
40+
{"storage", "storage"},
41+
{"vector", "vector"},
42+
}
43+
44+
for _, tc := range cases {
45+
t.Run(tc.group, func(t *testing.T) {
46+
resetProvisionFlags()
47+
name := "contract-" + tc.group
48+
out, token := c.provisionViaCLI(tc.group, name)
49+
50+
// flag→payload: the name we passed must round-trip into the stored
51+
// resource (the mock requires a non-empty name or it 400s).
52+
c.mock.mu.Lock()
53+
res := c.mock.resources[token]
54+
c.mock.mu.Unlock()
55+
if res == nil {
56+
t.Fatalf("%s: no resource recorded for token %q", tc.group, token)
57+
}
58+
59+
// endpoint mapping: storage new -> /storage/new (resource_type
60+
// "storage"); vector new -> /vector/new (resource_type "vector").
61+
if res.ResourceType != tc.wantType {
62+
t.Errorf("%s: wrong endpoint — recorded resource_type=%q, want %q (CLI hit the wrong /%s/new path?)",
63+
tc.group, res.ResourceType, tc.wantType, tc.group)
64+
}
65+
if res.Name != name {
66+
t.Errorf("%s: --name not forwarded — recorded name=%q, want %q", tc.group, res.Name, name)
67+
}
68+
69+
// resolved-env default (CLAUDE.md rule 11): no --env => development.
70+
if res.Env != "development" {
71+
t.Errorf("%s: resolved-env default want %q, got %q", tc.group, "development", res.Env)
72+
}
73+
74+
// user-visible output carries the token + an ok line.
75+
if !strings.Contains(out, token) {
76+
t.Errorf("%s: provision output missing token %q: %q", tc.group, token, out)
77+
}
78+
})
79+
}
80+
}
81+
82+
// browserOpenLine extracts the URL line that runLogin prints immediately
83+
// after the "Opening browser to:" label. runLogin emits:
84+
//
85+
// Opening browser to:
86+
// <auth_url>
87+
//
88+
// so the URL is the next non-empty line. Returns "" if the label is absent.
89+
func browserOpenLine(stdout string) string {
90+
lines := strings.Split(stdout, "\n")
91+
for i, ln := range lines {
92+
if strings.Contains(ln, "Opening browser to:") {
93+
for _, next := range lines[i+1:] {
94+
if strings.TrimSpace(next) != "" {
95+
return strings.TrimSpace(next)
96+
}
97+
}
98+
}
99+
}
100+
return ""
101+
}
102+
103+
// TestContract_LoginUsesCanonicalAuthURL pins the device-flow contract: the
104+
// URL `instant login` opens is the auth_url the server returns from
105+
// POST /auth/cli (the canonical instanode.dev host), passed through verbatim.
106+
//
107+
// The mock's /auth/cli returns auth_url="https://instanode.dev/cli-auth?s=test".
108+
// We capture login's stdout (it prints "Opening browser to:\n <auth_url>")
109+
// and assert the canonical URL is what surfaces — so a future refactor that
110+
// synthesises the URL CLI-side instead of relaying the server's would fail.
111+
func TestContract_LoginUsesCanonicalAuthURL(t *testing.T) {
112+
c := newITContext(t)
113+
// Complete auth immediately so poll succeeds on the first iteration and the
114+
// command returns without hitting the long production poll window.
115+
c.mock.mu.Lock()
116+
c.mock.authComplete = true
117+
c.mock.mu.Unlock()
118+
119+
const canonical = "https://instanode.dev/cli-auth?s=test"
120+
121+
stdout, _ := captureStdout(t, func() {
122+
_, _, err := run("login")
123+
if err != nil {
124+
t.Fatalf("login failed: %v", err)
125+
}
126+
})
127+
128+
if !strings.Contains(stdout, canonical) {
129+
t.Errorf("login must open the server-provided canonical auth_url %q (relayed from POST /auth/cli), got stdout=%q",
130+
canonical, stdout)
131+
}
132+
133+
// Defense-in-depth, scoped to the browser-open line only: the URL the CLI
134+
// tells the user it is opening must be the canonical auth_url, NOT the API
135+
// base URL the CLI happens to be pointed at (the httptest server). Other
136+
// lines (e.g. the "Upgrade for higher limits: <api>/pricing" footer)
137+
// legitimately reference the API host, so we isolate the open line.
138+
openLine := browserOpenLine(stdout)
139+
if openLine == "" {
140+
t.Fatalf("login did not print a 'Opening browser to:' line; stdout=%q", stdout)
141+
}
142+
if !strings.Contains(openLine, canonical) {
143+
t.Errorf("browser-open line must carry the canonical auth_url %q, got %q", canonical, openLine)
144+
}
145+
if strings.Contains(openLine, c.srv) {
146+
t.Errorf("browser-open line leaked the API host %q — it must relay the server's canonical auth_url, not the API base URL. line=%q",
147+
c.srv, openLine)
148+
}
149+
}

0 commit comments

Comments
 (0)