package main import ( "os" "path/filepath" "testing" ) // makeRPCDir creates a webSrcDir with optional lib/query-client.ts for the // global mutation handler check. func makeRPCDir(t *testing.T) string { t.Helper() dir := t.TempDir() return dir } func writeRPCComponent(t *testing.T, webSrcDir, subpath, content string) { t.Helper() fullPath := filepath.Join(webSrcDir, subpath) if err := os.MkdirAll(filepath.Dir(fullPath), 0755); err != nil { t.Fatalf("mkdir %s: %v", filepath.Dir(fullPath), err) } if err := os.WriteFile(fullPath, []byte(content), 0644); err != nil { t.Fatalf("write %s: %v", fullPath, err) } } func writeGlobalMutationHandler(t *testing.T, webSrcDir string) { t.Helper() libDir := filepath.Join(webSrcDir, "lib") if err := os.MkdirAll(libDir, 0755); err != nil { t.Fatalf("mkdir lib: %v", err) } if err := os.WriteFile(filepath.Join(libDir, "query-client.ts"), []byte(` import { MutationCache, QueryClient } from '@tanstack/react-query' export const queryClient = new QueryClient({ mutationCache: new MutationCache({ onError: (error) => { console.error('Global mutation error', error) }, }), }) `), 0644); err != nil { t.Fatalf("write query-client.ts: %v", err) } } // ───────────────────────────────────────────── // checkRPCErrors – query-no-error warning // ───────────────────────────────────────────── func TestCheckRPCErrors_QueryWithoutError_Warning(t *testing.T) { dir := makeRPCDir(t) writeRPCComponent(t, dir, "components/my-component.tsx", ` import { useQuery } from '@block-ninja/api/queries' export function MyComponent() { const { data, isLoading } = useQuery(myQuery, {}) return
{data}
} `) violations, warnings := checkRPCErrors(dir) if len(warnings) == 0 { t.Fatal("expected at least 1 warning for useQuery without error destructuring") } found := false for _, w := range warnings { if w.rule == "query-no-error" { found = true } } if !found { t.Fatalf("expected rule 'query-no-error' in warnings, got: %#v", warnings) } // Should not be a hard violation if len(violations) != 0 { t.Fatalf("expected 0 hard violations for missing query error, got %d: %#v", len(violations), violations) } } func TestCheckRPCErrors_QueryWithError_NoWarning(t *testing.T) { dir := makeRPCDir(t) writeRPCComponent(t, dir, "components/my-component.tsx", ` import { useQuery } from '@block-ninja/api/queries' export function MyComponent() { const { data, isLoading, error } = useQuery(myQuery, {}) if (error) return
Error!
return
{data}
} `) _, warnings := checkRPCErrors(dir) if len(warnings) != 0 { t.Fatalf("expected 0 warnings when error is destructured, got %d: %#v", len(warnings), warnings) } } // ───────────────────────────────────────────── // checkRPCErrors – mutation-no-error-handler violation // ───────────────────────────────────────────── func TestCheckRPCErrors_MutationWithNoErrorHandler_Violation(t *testing.T) { dir := makeRPCDir(t) // No global handler, no onError, no try/catch, no toast.error writeRPCComponent(t, dir, "components/editor.tsx", ` import { useMutation } from '@block-ninja/api/queries' export function Editor() { const saveMutation = useMutation(saveBlock) function handleSave() { saveMutation.mutate({ content: 'hello' }) } return } `) violations, _ := checkRPCErrors(dir) if len(violations) == 0 { t.Fatal("expected violation for mutation without error handler") } found := false for _, v := range violations { if v.rule == "mutation-no-error-handler" { found = true } } if !found { t.Fatalf("expected rule 'mutation-no-error-handler', got: %#v", violations) } } func TestCheckRPCErrors_MutationWithOnError_Clean(t *testing.T) { dir := makeRPCDir(t) writeRPCComponent(t, dir, "components/editor.tsx", ` import { useMutation } from '@block-ninja/api/queries' import { toast } from 'sonner' export function Editor() { const saveMutation = useMutation(saveBlock, { onError: (err) => toast.error('Save failed', { description: err.message }), onSuccess: () => toast.success('Saved'), }) return } `) violations, _ := checkRPCErrors(dir) if len(violations) != 0 { t.Fatalf("expected 0 violations for mutation with onError, got %d: %#v", len(violations), violations) } } func TestCheckRPCErrors_MutationWithTryCatch_Clean(t *testing.T) { dir := makeRPCDir(t) writeRPCComponent(t, dir, "components/editor.tsx", ` import { useMutation } from '@block-ninja/api/queries' export function Editor() { const saveMutation = useMutation(saveBlock) async function handleSave() { try { await saveMutation.mutateAsync({}) } catch (err) { console.error(err) } } return } `) violations, _ := checkRPCErrors(dir) if len(violations) != 0 { t.Fatalf("expected 0 violations when try/catch is present, got %d: %#v", len(violations), violations) } } func TestCheckRPCErrors_MutationWithToastError_Clean(t *testing.T) { dir := makeRPCDir(t) writeRPCComponent(t, dir, "components/editor.tsx", ` import { useMutation } from '@block-ninja/api/queries' import { toast } from 'sonner' export function Editor() { const saveMutation = useMutation(saveBlock) function handleSave() { saveMutation.mutate({}, { onSuccess: () => toast.success('Done'), }) // fallback: toast.error happens elsewhere } function handleError(err) { toast.error('Failed: ' + err.message) } return } `) violations, _ := checkRPCErrors(dir) if len(violations) != 0 { t.Fatalf("expected 0 violations when toast.error is present in file, got %d: %#v", len(violations), violations) } } func TestCheckRPCErrors_GlobalHandlerSuppressesViolation(t *testing.T) { dir := makeRPCDir(t) writeGlobalMutationHandler(t, dir) // Mutation with no per-mutation error handling — suppressed by global handler writeRPCComponent(t, dir, "components/editor.tsx", ` import { useMutation } from '@block-ninja/api/queries' export function Editor() { const saveMutation = useMutation(saveBlock) return } `) violations, _ := checkRPCErrors(dir) if len(violations) != 0 { t.Fatalf("expected 0 violations when global mutation handler is present, got %d: %#v", len(violations), violations) } } // ───────────────────────────────────────────── // checkRPCErrors – file path scoping // ───────────────────────────────────────────── func TestCheckRPCErrors_SkipsFilesOutsideComponentsAndRoutes(t *testing.T) { dir := makeRPCDir(t) // File is in lib/ — not components/ or routes/, should be skipped writeRPCComponent(t, dir, "lib/utils.tsx", ` import { useMutation } from '@block-ninja/api/queries' export function useHelperMutation() { return useMutation(doSomething) } `) violations, warnings := checkRPCErrors(dir) if len(violations) != 0 || len(warnings) != 0 { t.Fatalf("expected 0 violations and 0 warnings for file outside scoped dirs, got v=%d w=%d", len(violations), len(warnings)) } } func TestCheckRPCErrors_ChecksRouteFiles(t *testing.T) { dir := makeRPCDir(t) // File in routes/ should be checked — mutation with no handler writeRPCComponent(t, dir, "routes/dashboard.tsx", ` import { useMutation } from '@block-ninja/api/queries' export function Dashboard() { const doThing = useMutation(action) return } `) violations, _ := checkRPCErrors(dir) if len(violations) == 0 { t.Fatal("expected violation for mutation in routes/ without error handler") } } // ───────────────────────────────────────────── // checkRPCErrors – comment lines skipped // ───────────────────────────────────────────── func TestCheckRPCErrors_CommentedMutationSkipped(t *testing.T) { dir := makeRPCDir(t) writeRPCComponent(t, dir, "components/thing.tsx", ` export function Thing() { // const doThing = useMutation(action) return
} `) violations, _ := checkRPCErrors(dir) if len(violations) != 0 { t.Fatalf("expected 0 violations for commented-out mutation, got %d: %#v", len(violations), violations) } } // ───────────────────────────────────────────── // hasGlobalMutationErrorHandler // ───────────────────────────────────────────── func TestHasGlobalMutationErrorHandler_Present(t *testing.T) { dir := makeRPCDir(t) writeGlobalMutationHandler(t, dir) if !hasGlobalMutationErrorHandler(dir) { t.Fatal("expected hasGlobalMutationErrorHandler to return true when handler is present") } } func TestHasGlobalMutationErrorHandler_Missing(t *testing.T) { dir := makeRPCDir(t) if hasGlobalMutationErrorHandler(dir) { t.Fatal("expected hasGlobalMutationErrorHandler to return false when file is absent") } } func TestHasGlobalMutationErrorHandler_PartialContent(t *testing.T) { dir := makeRPCDir(t) libDir := filepath.Join(dir, "lib") if err := os.MkdirAll(libDir, 0755); err != nil { t.Fatalf("mkdir lib: %v", err) } // Has MutationCache but no onError if err := os.WriteFile(filepath.Join(libDir, "query-client.ts"), []byte(` import { MutationCache } from '@tanstack/react-query' const cache = new MutationCache({}) `), 0644); err != nil { t.Fatalf("write query-client.ts: %v", err) } if hasGlobalMutationErrorHandler(dir) { t.Fatal("expected false when MutationCache present but onError absent") } }