← back to Cli Printing Press
fix(cli): FTS trigger safety, envelope unwrapping, dogfood testing phase (#124)
bc3208460d267857b6caf13c965fa64292b5271b · 2026-04-04 15:10:43 -0700 · Trevin Chow
* fix(cli): FTS triggers only use content-sync when fields are extracted columns
When FTS5 fields (title, description) aren't actual extracted columns on
the table (they're inside the JSON data column), content-sync triggers
fail with "no such column". Now allFieldsAreColumns() validates FTS
fields against the table's real columns before enabling triggers.
When triggers aren't safe, the generator emits a standalone FTS table
(without content= binding) and populates it during upsert by extracting
fields from the JSON with lookupFieldValue(). This gives correct FTS
search without requiring column extraction for every searchable field.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(cli): response envelope unwrapping, CSV output, provenance counter
Move response envelope handling from client to display layer. The client
returns raw API responses (preserving pagination metadata, error bodies,
etc.). The new extractResponseData() helper in the promoted command
output path unwraps {"status":"success","data":...} envelopes before
passing to filterFields/compactFields/printAutoTable.
Safety: only unwraps when an explicit "status" field equals "success" or
"ok". APIs like Stripe where "data" is a regular list field (no status
wrapper) are not affected — the absence of a status field means no
unwrapping occurs.
Also fixes:
- --csv now works when output is piped (CSV check before JSON pipe path)
- Provenance counter handles single objects (me, profile) instead of
showing "0 results"
- Single-child sub-resource groups wire leaf commands directly to avoid
redundant nesting (e.g., "bookings cancel cancel <uid>" → "bookings
cancel <uid>")
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(skills): add structured dogfood testing phase
Replace the shallow Phase 5 "Optional Live Smoke" with a structured
dogfood testing protocol. Two depth levels:
- Quick check (5-10 min): read-only tests — doctor, list commands, sync
round-trip, search, output modes, one transcendence command
- Full dogfood (15-30 min): complete entity lifecycle — create, verify,
mutate, sync verification, output fidelity, error paths, incremental
sync
Key design decisions:
- User picks depth before testing begins
- Fixes happen inline as bugs are found (not accumulated)
- Each fix is tagged as CLI-specific or machine issue (feeds retro)
- Common failure patterns table helps diagnose quickly
- Mandatory when API key is available
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(cli): preserve built-in auth, quote FTS trigger column names
Fixes two P1 issues from Codex review:
1. Auth: always register the built-in auth command (login/token
management). Skip "auth" in the resource loop instead of suppressing
the built-in. The API's /auth endpoints remain accessible via the api
discovery command.
2. FTS triggers: added safeJoin template function that applies safeName
to each field before joining. Trigger INSERT bodies now use
{{safeJoin .FTS5Fields ", "}} so reserved words like "from", "order"
are properly quoted in the trigger SQL.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Files touched
M internal/generator/generator.goM internal/generator/schema_builder.goM internal/generator/templates/client.go.tmplM internal/generator/templates/command_promoted.go.tmplM internal/generator/templates/helpers.go.tmplM internal/generator/templates/root.go.tmplM internal/generator/templates/store.go.tmplM skills/printing-press/SKILL.mdA skills/printing-press/references/dogfood-testing.md
Diff
commit bc3208460d267857b6caf13c965fa64292b5271b
Author: Trevin Chow <trevin@trevinchow.com>
Date: Sat Apr 4 15:10:43 2026 -0700
fix(cli): FTS trigger safety, envelope unwrapping, dogfood testing phase (#124)
* fix(cli): FTS triggers only use content-sync when fields are extracted columns
When FTS5 fields (title, description) aren't actual extracted columns on
the table (they're inside the JSON data column), content-sync triggers
fail with "no such column". Now allFieldsAreColumns() validates FTS
fields against the table's real columns before enabling triggers.
When triggers aren't safe, the generator emits a standalone FTS table
(without content= binding) and populates it during upsert by extracting
fields from the JSON with lookupFieldValue(). This gives correct FTS
search without requiring column extraction for every searchable field.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(cli): response envelope unwrapping, CSV output, provenance counter
Move response envelope handling from client to display layer. The client
returns raw API responses (preserving pagination metadata, error bodies,
etc.). The new extractResponseData() helper in the promoted command
output path unwraps {"status":"success","data":...} envelopes before
passing to filterFields/compactFields/printAutoTable.
Safety: only unwraps when an explicit "status" field equals "success" or
"ok". APIs like Stripe where "data" is a regular list field (no status
wrapper) are not affected — the absence of a status field means no
unwrapping occurs.
Also fixes:
- --csv now works when output is piped (CSV check before JSON pipe path)
- Provenance counter handles single objects (me, profile) instead of
showing "0 results"
- Single-child sub-resource groups wire leaf commands directly to avoid
redundant nesting (e.g., "bookings cancel cancel <uid>" → "bookings
cancel <uid>")
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(skills): add structured dogfood testing phase
Replace the shallow Phase 5 "Optional Live Smoke" with a structured
dogfood testing protocol. Two depth levels:
- Quick check (5-10 min): read-only tests — doctor, list commands, sync
round-trip, search, output modes, one transcendence command
- Full dogfood (15-30 min): complete entity lifecycle — create, verify,
mutate, sync verification, output fidelity, error paths, incremental
sync
Key design decisions:
- User picks depth before testing begins
- Fixes happen inline as bugs are found (not accumulated)
- Each fix is tagged as CLI-specific or machine issue (feeds retro)
- Common failure patterns table helps diagnose quickly
- Mandatory when API key is available
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(cli): preserve built-in auth, quote FTS trigger column names
Fixes two P1 issues from Codex review:
1. Auth: always register the built-in auth command (login/token
management). Skip "auth" in the resource loop instead of suppressing
the built-in. The API's /auth endpoints remain accessible via the api
discovery command.
2. FTS triggers: added safeJoin template function that applies safeName
to each field before joining. Trigger INSERT bodies now use
{{safeJoin .FTS5Fields ", "}} so reserved words like "from", "order"
are properly quoted in the trigger SQL.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---
internal/generator/generator.go | 7 +
internal/generator/schema_builder.go | 21 ++-
internal/generator/templates/client.go.tmpl | 1 +
.../generator/templates/command_promoted.go.tmpl | 19 ++-
internal/generator/templates/helpers.go.tmpl | 28 ++++
internal/generator/templates/root.go.tmpl | 4 +-
internal/generator/templates/store.go.tmpl | 31 +++-
skills/printing-press/SKILL.md | 27 ++--
.../printing-press/references/dogfood-testing.md | 161 +++++++++++++++++++++
9 files changed, 280 insertions(+), 19 deletions(-)
diff --git a/internal/generator/generator.go b/internal/generator/generator.go
index fb700475..97327afb 100644
--- a/internal/generator/generator.go
+++ b/internal/generator/generator.go
@@ -113,6 +113,13 @@ func New(s *spec.APISpec, outputDir string) *Generator {
},
"envName": func(s string) string { return strings.ToUpper(strings.ReplaceAll(s, "-", "_")) },
"safeName": safeSQLName,
+ "safeJoin": func(fields []string, sep string) string {
+ safe := make([]string, len(fields))
+ for i, f := range fields {
+ safe[i] = safeSQLName(f)
+ }
+ return strings.Join(safe, sep)
+ },
"goLiteral": func(v any) string {
switch val := v.(type) {
case string:
diff --git a/internal/generator/schema_builder.go b/internal/generator/schema_builder.go
index db01e283..3daabcf4 100644
--- a/internal/generator/schema_builder.go
+++ b/internal/generator/schema_builder.go
@@ -82,7 +82,10 @@ func BuildSchema(s *spec.APISpec) []TableDef {
if len(textFields) >= 2 && gravity >= 6 {
table.FTS5 = true
table.FTS5Fields = textFields
- table.FTS5Triggers = true
+ // Only use content-sync triggers when ALL FTS fields are
+ // actual extracted columns on the table. Otherwise the
+ // triggers reference non-existent columns and fail.
+ table.FTS5Triggers = allFieldsAreColumns(textFields, table.Columns)
}
tables = append(tables, table)
@@ -339,6 +342,22 @@ func safeSQLName(name string) string {
return name
}
+// allFieldsAreColumns returns true if every field name exists as an extracted
+// column on the table. Used to decide whether FTS content-sync triggers are
+// safe (they reference new.field which only works for real columns).
+func allFieldsAreColumns(fields []string, columns []ColumnDef) bool {
+ colSet := make(map[string]bool, len(columns))
+ for _, col := range columns {
+ colSet[col.Name] = true
+ }
+ for _, f := range fields {
+ if !colSet[f] {
+ return false
+ }
+ }
+ return true
+}
+
// toSnakeCase converts camelCase, PascalCase, or kebab-case to snake_case.
func toSnakeCase(s string) string {
s = strings.ReplaceAll(s, "-", "_")
diff --git a/internal/generator/templates/client.go.tmpl b/internal/generator/templates/client.go.tmpl
index 96428f3c..efe0ba71 100644
--- a/internal/generator/templates/client.go.tmpl
+++ b/internal/generator/templates/client.go.tmpl
@@ -562,6 +562,7 @@ func sanitizeJSONResponse(body []byte) []byte {
return body
}
+
func truncateBody(b []byte) string {
s := string(b)
if len(s) > 200 {
diff --git a/internal/generator/templates/command_promoted.go.tmpl b/internal/generator/templates/command_promoted.go.tmpl
index ab5ab091..bda9d969 100644
--- a/internal/generator/templates/command_promoted.go.tmpl
+++ b/internal/generator/templates/command_promoted.go.tmpl
@@ -88,12 +88,23 @@ func new{{camel .PromotedName}}PromotedCmd(flags *rootFlags) *cobra.Command {
}
{{- if .HasStore}}
+ // Unwrap API response envelopes (e.g. {"status":"success","data":[...]})
+ // so output helpers see the inner data, not the wrapper.
+ data = extractResponseData(data)
+
// Print provenance to stderr
{
var countItems []json.RawMessage
- _ = json.Unmarshal(data, &countItems)
+ if json.Unmarshal(data, &countItems) != nil {
+ // Single object, not an array
+ countItems = []json.RawMessage{data}
+ }
printProvenance(cmd, len(countItems), prov)
}
+ // CSV bypasses JSON pipe path so --csv works when piped
+ if flags.csv {
+ return printOutputWithFlags(cmd.OutOrStdout(), data, flags)
+ }
// For JSON output, wrap with provenance envelope
if flags.asJSON || !isTerminal(cmd.OutOrStdout()) {
filtered := data
@@ -145,7 +156,13 @@ func new{{camel .PromotedName}}PromotedCmd(flags *rootFlags) *cobra.Command {
{{- end}}
{{- end}}
{{- range $subName, $sub := .Resource.SubResources}}
+{{- if eq (len $sub.Endpoints) 1}}
+{{- range $eName, $_ := $sub.Endpoints}}
+ cmd.AddCommand(new{{camel $.FuncPrefix}}{{camel $subName}}{{camel $eName}}Cmd(flags))
+{{- end}}
+{{- else}}
cmd.AddCommand(new{{camel $.FuncPrefix}}{{camel $subName}}Cmd(flags))
+{{- end}}
{{- end}}
return cmd
diff --git a/internal/generator/templates/helpers.go.tmpl b/internal/generator/templates/helpers.go.tmpl
index 893c2b5c..f39be82c 100644
--- a/internal/generator/templates/helpers.go.tmpl
+++ b/internal/generator/templates/helpers.go.tmpl
@@ -446,6 +446,34 @@ func printOutputWithFlags(w io.Writer, data json.RawMessage, flags *rootFlags) e
return printOutput(w, data, flags.asJSON)
}
+// extractResponseData unwraps common API response envelopes for display.
+// Many APIs return {"status":"success","data":[...]} instead of a bare array.
+// This extracts the inner data for output helpers (filterFields, compactFields,
+// printAutoTable) that expect arrays or flat objects.
+//
+// Only unwraps when a "status" field is present and indicates success — this
+// avoids false positives on APIs where "data" is a regular field (e.g., Stripe
+// returns {"data":[...],"has_more":true} where "data" is the list, not an
+// envelope wrapper).
+func extractResponseData(data json.RawMessage) json.RawMessage {
+ var envelope struct {
+ Status string `json:"status"`
+ Data json.RawMessage `json:"data"`
+ }
+ if err := json.Unmarshal(data, &envelope); err != nil {
+ return data
+ }
+ if envelope.Data == nil || envelope.Status == "" {
+ return data // No status field = not an envelope, might be regular "data" field
+ }
+ switch envelope.Status {
+ case "success", "ok", "OK", "Success":
+ return envelope.Data
+ default:
+ return data
+ }
+}
+
// compactFields keeps only the most important fields for agent consumption.
// For arrays: allowlist of high-gravity fields (no descriptions).
// For single objects: blocklist that strips known-verbose fields (descriptions, comments, etc.).
diff --git a/internal/generator/templates/root.go.tmpl b/internal/generator/templates/root.go.tmpl
index e0d605b2..5cae5eda 100644
--- a/internal/generator/templates/root.go.tmpl
+++ b/internal/generator/templates/root.go.tmpl
@@ -98,14 +98,12 @@ func Execute() error {
}
{{- range $name, $resource := .Resources}}
-{{- if not (index $.PromotedResourceNames $name)}}
+{{- if and (not (index $.PromotedResourceNames $name)) (ne $name "auth")}}
rootCmd.AddCommand(new{{camel $name}}Cmd(&flags))
{{- end}}
{{- end}}
rootCmd.AddCommand(newDoctorCmd(&flags))
-{{- if not (index .Resources "auth")}}
rootCmd.AddCommand(newAuthCmd(&flags))
-{{- end}}
{{- if .VisionSet.Export}}
rootCmd.AddCommand(newExportCmd(&flags))
{{- end}}
diff --git a/internal/generator/templates/store.go.tmpl b/internal/generator/templates/store.go.tmpl
index 79157564..75a4a0b7 100644
--- a/internal/generator/templates/store.go.tmpl
+++ b/internal/generator/templates/store.go.tmpl
@@ -88,6 +88,7 @@ func (s *Store) migrate() error {
`CREATE {{if .Unique}}UNIQUE {{end}}INDEX IF NOT EXISTS {{.Name}} ON {{safeName .TableName}}({{safeName .Columns}})`,
{{- end}}
{{- if .FTS5}}
+{{- if .FTS5Triggers}}
`CREATE VIRTUAL TABLE IF NOT EXISTS {{.Name}}_fts USING fts5(
{{- range $i, $field := .FTS5Fields}}
{{- if $i}},{{end}}
@@ -97,19 +98,28 @@ func (s *Store) migrate() error {
content_rowid='rowid'
)`,
`CREATE TRIGGER IF NOT EXISTS {{.Name}}_ai AFTER INSERT ON {{safeName .Name}} BEGIN
- INSERT INTO {{.Name}}_fts(rowid, {{join .FTS5Fields ", "}})
+ INSERT INTO {{.Name}}_fts(rowid, {{safeJoin .FTS5Fields ", "}})
VALUES (new.rowid, {{- range $i, $field := .FTS5Fields}}{{if $i}}, {{end}}new.{{safeName $field}}{{- end}});
END`,
`CREATE TRIGGER IF NOT EXISTS {{.Name}}_ad AFTER DELETE ON {{safeName .Name}} BEGIN
- INSERT INTO {{.Name}}_fts({{.Name}}_fts, rowid, {{join .FTS5Fields ", "}})
+ INSERT INTO {{.Name}}_fts({{.Name}}_fts, rowid, {{safeJoin .FTS5Fields ", "}})
VALUES ('delete', old.rowid, {{- range $i, $field := .FTS5Fields}}{{if $i}}, {{end}}old.{{safeName $field}}{{- end}});
END`,
`CREATE TRIGGER IF NOT EXISTS {{.Name}}_au AFTER UPDATE ON {{safeName .Name}} BEGIN
- INSERT INTO {{.Name}}_fts({{.Name}}_fts, rowid, {{join .FTS5Fields ", "}})
+ INSERT INTO {{.Name}}_fts({{.Name}}_fts, rowid, {{safeJoin .FTS5Fields ", "}})
VALUES ('delete', old.rowid, {{- range $i, $field := .FTS5Fields}}{{if $i}}, {{end}}old.{{safeName $field}}{{- end}});
- INSERT INTO {{.Name}}_fts(rowid, {{join .FTS5Fields ", "}})
+ INSERT INTO {{.Name}}_fts(rowid, {{safeJoin .FTS5Fields ", "}})
VALUES (new.rowid, {{- range $i, $field := .FTS5Fields}}{{if $i}}, {{end}}new.{{safeName $field}}{{- end}});
END`,
+{{- else}}
+ `CREATE VIRTUAL TABLE IF NOT EXISTS {{.Name}}_fts USING fts5(
+ id,
+{{- range $i, $field := .FTS5Fields}}
+ {{safeName $field}},
+{{- end}}
+ tokenize='porter unicode61'
+ )`,
+{{- end}}
{{- end}}
{{- end}}
{{- end}}
@@ -299,6 +309,19 @@ func (s *Store) Upsert{{pascal .Name}}(data json.RawMessage) error {
if err != nil {
return err
}
+{{- if and .FTS5 (not .FTS5Triggers)}}
+
+ // Standalone FTS: manually sync since content-sync triggers aren't used
+ // (FTS fields are inside the JSON data column, not extracted table columns)
+ _, _ = tx.Exec(`DELETE FROM {{.Name}}_fts WHERE id = ?`, id)
+ _, _ = tx.Exec(
+ `INSERT INTO {{.Name}}_fts (id, {{safeJoin .FTS5Fields ", "}}) VALUES (?{{- range .FTS5Fields}}, ?{{- end}})`,
+ id,
+{{- range .FTS5Fields}}
+ lookupFieldValue(obj, "{{.}}"),
+{{- end}}
+ )
+{{- end}}
return tx.Commit()
}
diff --git a/skills/printing-press/SKILL.md b/skills/printing-press/SKILL.md
index 0f8e6660..161eaccb 100644
--- a/skills/printing-press/SKILL.md
+++ b/skills/printing-press/SKILL.md
@@ -1252,22 +1252,29 @@ printing-press lock release --cli <api>-pp-cli
```
The working copy remains in `$CLI_WORK_DIR` for potential future retry. Proceed to Phase 5.5 to archive manuscripts (archiving still happens on hold).
-## Phase 5: Optional Live Smoke
+## Phase 5: Dogfood Testing
-Only run this if a token is available and the user agreed.
+**MANDATORY when an API key is available.** This is where you use the CLI as an
+actual user would — not just "does it start" but "does it produce correct, useful
+output for real workflows."
-Use read-only smoke tests:
-- `--help`
-- one or two representative GET/list commands
-- sync/search/health path if a local data layer exists
+Shipcheck verified the CLI builds and commands respond. Dogfood verifies it
+actually works.
-If live smoke finds bugs:
-- fix only the real bug
-- re-run the shipcheck block once
+Read and follow [references/dogfood-testing.md](references/dogfood-testing.md) for
+the complete dogfood protocol: depth selection (quick check vs full lifecycle),
+test plan execution, inline fix workflow, and reporting.
+
+**Key rules:**
+- Fix issues as you find them, not at the end. The value is discovering bugs in
+ context.
+- Note whether each fix is CLI-specific or a machine issue (feeds the retro).
+- If a fix changes the sync, data layer, or output pipeline, re-run the relevant
+ shipcheck tool (`verify` or `scorecard`) once after all dogfood fixes.
Write:
-`$PROOFS_DIR/<stamp>-fix-<api>-pp-cli-live-smoke.md`
+`$PROOFS_DIR/<stamp>-fix-<api>-pp-cli-dogfood.md`
## Phase 5.5: Promote and Archive
diff --git a/skills/printing-press/references/dogfood-testing.md b/skills/printing-press/references/dogfood-testing.md
new file mode 100644
index 00000000..f68b1a47
--- /dev/null
+++ b/skills/printing-press/references/dogfood-testing.md
@@ -0,0 +1,161 @@
+# Dogfood Testing Reference
+
+> **When to read:** Phase 4.5 of the printing-press skill, after shipcheck passes.
+> This replaces the old Phase 5 (Optional Live Smoke) with structured testing.
+
+## Overview
+
+Dogfood testing runs the CLI against the real API as an actual user would.
+Shipcheck verifies that commands start and return exit codes. Dogfood verifies
+that they produce correct, useful output for real workflows.
+
+The user chooses the depth before testing begins. Both levels require an API key.
+
+## Depth Levels
+
+### Quick Check (5-10 min)
+
+Verifies core commands produce correct output against the live API. No mutations.
+
+**What it tests:**
+1. **Auth**: `doctor` shows valid credentials
+2. **List endpoints**: The 3-5 highest-traffic list commands return data (not empty, not error)
+3. **Sync round-trip**: `sync` → `sql "SELECT count(*) FROM user_bookings"` → count > 0
+4. **Search**: `search "<term from synced data>"` → finds the result
+5. **Output modes**: Pick one list command and test `--json`, `--select <2 fields>`, `--compact`, `--csv`
+6. **Transcendence**: At least one transcendence command produces output from synced data (e.g., `health`, `today --date <date-with-data>`)
+
+**What it skips:**
+- Mutations (create, update, delete, cancel)
+- Full booking/entity lifecycle
+- Edge cases and error paths
+
+**Pass criteria:** All 6 checks produce non-empty, correctly-formatted output. Any failure → investigate and fix before proceeding.
+
+### Full Dogfood (15-30 min)
+
+Tests a complete entity lifecycle including mutations, sync verification, and
+all output modes. Requires explicit user permission since mutations affect their account.
+
+**What it tests:**
+
+Everything in Quick Check, plus:
+
+7. **Create**: Create a test entity via the API (booking, issue, record — whatever
+ the primary mutable entity is). Use obviously-test data ("Dogfood Test User",
+ "dogfood@test.example.com").
+
+8. **Verify creation**: List command shows the new entity. Get-by-ID returns full details.
+
+9. **Sync + search**: Re-sync. Search for the test entity by name. Verify it appears
+ in transcendence commands (`today --date`, `health`, `sql`).
+
+10. **Mutate**: Test 2-3 mutation subcommands on the test entity:
+ - Update/modify (reschedule, edit, etc.)
+ - Re-sync and verify the change is reflected
+ - Cancel/delete
+ - Re-sync and verify status change
+
+11. **Output fidelity**: For each tested command, verify:
+ - `--json` produces valid JSON (pipe through `python3 -c "import json,sys; json.load(sys.stdin)"`)
+ - `--select <fields>` returns only those fields
+ - `--csv` produces CSV with header row
+ - `--compact` returns fewer fields than full output
+ - Table output (default terminal) is readable
+
+12. **Error paths**: Test 2-3 expected error cases:
+ - Get a non-existent ID → meaningful error message (not stack trace)
+ - Missing required flag → help text shown
+ - Invalid filter value → clear error
+
+13. **Incremental sync**: Run `sync` twice. Second run should be faster (fewer items fetched).
+ Verify with: `sync` → note count → create entity via API → `sync` → count increased by 1.
+
+**Pass criteria:** All 13 checks pass. Mutations successfully execute against the real API.
+Entity lifecycle completes: create → verify → mutate → sync → verify change → delete → verify deletion.
+
+## Running the Dogfood
+
+### Before starting
+
+1. API key must be available (from the API Key Gate in Phase 0.5)
+2. Shipcheck must have passed (at least `ship-with-gaps`)
+3. CLI binary must be built and working
+
+### Step 1: Ask the user
+
+Present via `AskUserQuestion`:
+
+> "Shipcheck passed. How thoroughly should I test against the live API?"
+>
+> 1. **Quick check** — Read-only tests: list, sync, search, output modes (~5 min)
+> 2. **Full dogfood** — Complete lifecycle with mutations: create, modify, cancel, sync verification (~15-30 min). I'll create test entities on your account.
+> 3. **Skip testing** — Proceed to publish
+
+If the user selects "Full dogfood", confirm: "I'll create test data on your account (test bookings, test records, etc.) and clean up by cancelling/deleting them. OK to proceed?"
+
+### Step 2: Execute the test plan
+
+For each test, print the command being run and its result. Use a clear format:
+
+```
+[1/6] doctor
+ $ cal-com-pp-cli doctor
+ PASS: Config ok, API reachable, credentials valid
+
+[2/6] bookings list
+ $ cal-com-pp-cli bookings --no-cache --json | head -3
+ PASS: 3 bookings returned
+
+[3/6] sync + sql
+ $ cal-com-pp-cli sync --full
+ $ cal-com-pp-cli sql "SELECT count(*) as n FROM user_bookings"
+ PASS: 3 rows synced
+```
+
+### Step 3: Fix issues inline
+
+When a test fails:
+1. Diagnose the root cause immediately
+2. Fix it in the printed CLI
+3. Re-run the failing test
+4. Note whether the fix is CLI-specific or a machine issue (for the retro)
+
+Do NOT accumulate failures and fix them later. Fix each one as you find it.
+This is the value of dogfood — you discover issues in context and fix them
+while you understand the API's behavior.
+
+### Step 4: Report results
+
+After all tests complete, print a summary:
+
+```
+Dogfood Results: <cli-name>
+ Level: Quick Check / Full Dogfood
+ Tests: N/N passed
+ Fixes applied: M
+ - [list each fix with 1-line description]
+ Machine issues found: K
+ - [list issues for the retro]
+```
+
+### Common failure patterns
+
+| Symptom | Likely cause | Fix location |
+|---------|-------------|--------------|
+| All list commands return empty | Response envelope not unwrapped | Client or output helpers |
+| `--select` strips everything | filterFields can't parse envelope | Add extractResponseData call |
+| `--csv` shows JSON | CSV check after JSON pipe check | Promoted template output path |
+| `search` returns no results | FTS table not wired into search cmd | search.go switch statement |
+| `sync` gets 404 on some endpoints | API version header mismatch | Client header per-path |
+| Mutation command requires ugly name | operationId not cleaned up | Command Use: field |
+| `<cmd> --help` shows wrong example | Example field has placeholder values | Command Example: field |
+| `me` shows "0 results" | Provenance counter assumes array | Count single objects as 1 |
+
+## What NOT to test
+
+- Internal implementation details (store schema, migration order)
+- Performance benchmarks
+- Concurrent access
+- Edge cases that require specific account setup (team features, org hierarchy)
+- Endpoints the user doesn't have access to (org-level when user is individual)
← 289ac4bf fix(cli): SQL reserved word safety, promoted subcommands, ve
·
back to Cli Printing Press
·
fix(skills): auto-polish after dogfood testing when fixes we e7f756b2 →