[object Object]

← 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

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 →