← back to Cli Printing Press
fix(cli): query generic resources_fts from search empty-type branch (#1462)
35e515c12d6ca7dbb3366ba926086915997043ad · 2026-05-15 12:37:22 -0700 · Trevin Chow
* fix(cli): query generic resources_fts from search empty-type branch
The empty-type branch of search.go iterated only typed FTS tables, so
rows indexed in resources_fts but not in any typed FTS table returned
zero from <cli> search "<query>" (no --type). Confirmed on Asana CLI
where direct SELECT FROM resources_fts MATCH ... returned 249 hits but
search "" reported zero.
Append a db.Search(query, limit) call to the existing seen-map dedup
loop so the generic index is queried alongside the typed Search<X>
helpers. Test pins both the call and the contextual error message.
Closes #1390
* test(cli): anchor search-template default: slice on indentation
Greptile noted the strings.Index search for "default:" inside the
empty-type branch is fragile: a future comment containing "default:"
would shift the slice and silently under-test the assertions. Anchor
on "\n\t\t\tdefault:" so the boundary lands on the actual switch arm.
Refs #1390
Files touched
M internal/generator/generator_test.goM internal/generator/templates/search.go.tmpl
Diff
commit 35e515c12d6ca7dbb3366ba926086915997043ad
Author: Trevin Chow <trevin@trevinchow.com>
Date: Fri May 15 12:37:22 2026 -0700
fix(cli): query generic resources_fts from search empty-type branch (#1462)
* fix(cli): query generic resources_fts from search empty-type branch
The empty-type branch of search.go iterated only typed FTS tables, so
rows indexed in resources_fts but not in any typed FTS table returned
zero from <cli> search "<query>" (no --type). Confirmed on Asana CLI
where direct SELECT FROM resources_fts MATCH ... returned 249 hits but
search "" reported zero.
Append a db.Search(query, limit) call to the existing seen-map dedup
loop so the generic index is queried alongside the typed Search<X>
helpers. Test pins both the call and the contextual error message.
Closes #1390
* test(cli): anchor search-template default: slice on indentation
Greptile noted the strings.Index search for "default:" inside the
empty-type branch is fragile: a future comment containing "default:"
would shift the slice and silently under-test the assertions. Anchor
on "\n\t\t\tdefault:" so the boundary lands on the actual switch arm.
Refs #1390
---
internal/generator/generator_test.go | 27 +++++++++++++++++++++++++++
internal/generator/templates/search.go.tmpl | 20 +++++++++++++++++++-
2 files changed, 46 insertions(+), 1 deletion(-)
diff --git a/internal/generator/generator_test.go b/internal/generator/generator_test.go
index d41f0486..c5c933eb 100644
--- a/internal/generator/generator_test.go
+++ b/internal/generator/generator_test.go
@@ -10189,6 +10189,33 @@ func TestProjectManagementWorkflowsEmitReadOnlyAnnotations(t *testing.T) {
}
}
+// TestSearchTemplateEmptyTypeQueriesGenericFTS pins #1390 — the
+// no-type branch must include a generic db.Search(query, limit) call
+// alongside the per-table typed Search<Resource> loop. Rows indexed
+// only in resources_fts (not in any typed FTS table) otherwise return
+// zero on the default search path.
+func TestSearchTemplateEmptyTypeQueriesGenericFTS(t *testing.T) {
+ t.Parallel()
+ data, err := os.ReadFile(filepath.Join("templates", "search.go.tmpl"))
+ require.NoError(t, err)
+ body := string(data)
+
+ caseEmptyIdx := strings.Index(body, `case "":`)
+ require.GreaterOrEqual(t, caseEmptyIdx, 0, "search.go.tmpl must have a case \"\": branch")
+
+ // Anchor on the column-zero `default:` that closes the empty-type
+ // branch, so a stray "default:" inside a comment can't shift the
+ // slice boundary.
+ defaultIdx := strings.Index(body[caseEmptyIdx:], "\n\t\t\tdefault:")
+ require.GreaterOrEqual(t, defaultIdx, 0, "search.go.tmpl must have a tab-indented default: after case \"\":")
+ emptyBranch := body[caseEmptyIdx : caseEmptyIdx+defaultIdx]
+
+ assert.Contains(t, emptyBranch, "db.Search(query, limit)",
+ "case \"\": must call db.Search(query, limit) so rows indexed only in resources_fts (not in a typed FTS table) are returned")
+ assert.Contains(t, emptyBranch, `"search resources_fts failed:`,
+ "the generic-search error path must mention resources_fts so the failure is debuggable")
+}
+
// TestSearchTemplateEmitsEmptyJSONEnvelope pins the contract: the
// generated `search` command in --json (or piped) mode must always emit
// a valid JSON envelope, including on no matches. Agents pipe stdout
diff --git a/internal/generator/templates/search.go.tmpl b/internal/generator/templates/search.go.tmpl
index f785e685..805d14be 100644
--- a/internal/generator/templates/search.go.tmpl
+++ b/internal/generator/templates/search.go.tmpl
@@ -176,7 +176,12 @@ In local mode: searches locally synced data only.`,
{{- end}}
{{- end}}
case "":
- // Search all FTS-enabled tables individually to avoid duplicates.
+ // Search every FTS-enabled source — typed per-resource tables
+ // AND the generic resources_fts — and dedup by raw JSON so a
+ // row indexed in multiple FTS sources appears once. Without
+ // the generic-search call, rows that landed in resources_fts
+ // but not in any typed FTS table (e.g., a resource whose sync
+ // populated only the generic index) silently return zero.
seen := make(map[string]bool)
_ = seen // prevent unused error when no FTS tables exist
{{- range .Tables}}
@@ -196,6 +201,19 @@ In local mode: searches locally synced data only.`,
}
{{- end}}
{{- end}}
+ {
+ partial, searchErr := db.Search(query, limit)
+ if searchErr != nil {
+ return fmt.Errorf("search resources_fts failed: %w", searchErr)
+ }
+ for _, r := range partial {
+ key := string(r)
+ if !seen[key] {
+ seen[key] = true
+ results = append(results, r)
+ }
+ }
+ }
default:
// Unrecognized type — fall back to generic search
results, err = db.Search(query, limit)
← bb5e2def fix(cli): validate &&-chained narrative recipes segment-by-s
·
back to Cli Printing Press
·
fix(cli): drop unused client import from param-less endpoint 72adfefd →