package generate import ( "os" "path/filepath" "strings" "testing" ) func TestToTitle(t *testing.T) { tests := []struct { in, want string }{ {"order-service", "OrderService"}, {"task", "Task"}, {"inventory_item", "InventoryItem"}, {"hello world", "HelloWorld"}, {"a-b-c", "ABC"}, {"already", "Already"}, } for _, tt := range tests { if got := toTitle(tt.in); got != tt.want { t.Errorf("toTitle(%q) = %q, want %q", tt.in, got, tt.want) } } } func TestProtoType(t *testing.T) { tests := []struct { in, want string }{ {"string", "string"}, {"int64", "int64"}, {"int32", "int32"}, {"bool", "bool"}, {"float64", "double"}, {"unknown", "string"}, {"", "string"}, } for _, tt := range tests { if got := protoType(tt.in); got != tt.want { t.Errorf("protoType(%q) = %q, want %q", tt.in, got, tt.want) } } } func TestFirstNonEmpty(t *testing.T) { if got := firstNonEmpty("", "", "c"); got != "c" { t.Errorf("got %q, want %q", got, "c") } if got := firstNonEmpty("a", "b"); got != "a" { t.Errorf("got %q, want %q", got, "a") } if got := firstNonEmpty("", ""); got != "" { t.Errorf("got %q, want %q", got, "") } } func TestExtractJSON(t *testing.T) { tests := []struct { name, in, want string }{ { "fenced json", "Here's the design:\n```json\n{\"services\": []}\n```\nDone.", `{"services": []}`, }, { "fenced no lang", "```\n{\"a\": 1}\n```", `{"a": 1}`, }, { "raw json", `some text {"key": "val"} trailing`, `{"key": "val"}`, }, { "nested braces", `{"a": {"b": 1}}`, `{"a": {"b": 1}}`, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := extractJSON(tt.in) if got != tt.want { t.Errorf("extractJSON() = %q, want %q", got, tt.want) } }) } } func TestExtractCode(t *testing.T) { tests := []struct { name, in string wantPrefix string }{ { "go fence", "Here:\n```go\npackage handler\n\nfunc Foo() {}\n```\nDone.", "package handler", }, { "generic fence", "```\npackage main\n```", "package main", }, { "raw code", "Sure, here's the code:\npackage handler\n\ntype X struct{}", "package handler", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := extractCode(tt.in) if !strings.HasPrefix(got, tt.wantPrefix) { t.Errorf("extractCode() = %q, want prefix %q", got, tt.wantPrefix) } }) } } func TestBuildProto(t *testing.T) { svc := ServiceSpec{ Name: "task-service", Description: "Manages tasks", Fields: []FieldSpec{ {Name: "id", Type: "string", Description: "Task ID"}, {Name: "title", Type: "string", Description: "Task title"}, {Name: "done", Type: "bool", Description: "Completion status"}, {Name: "created", Type: "int64", Description: "Created timestamp"}, {Name: "updated", Type: "int64", Description: "Updated timestamp"}, }, Endpoints: []EndpointSpec{ {Name: "Create", Description: "Create a task"}, {Name: "Read", Description: "Get a task"}, {Name: "Update", Description: "Update a task"}, {Name: "Delete", Description: "Delete a task"}, {Name: "List", Description: "List tasks"}, {Name: "ToggleComplete", Description: "Toggle completion"}, }, } proto := buildProto("taskservice", "TaskService", svc) checks := []string{ `syntax = "proto3"`, `package taskservice`, `service TaskService`, `rpc Create(CreateRequest) returns (CreateResponse)`, `rpc ToggleComplete(ToggleCompleteRequest) returns (ToggleCompleteResponse)`, `message TaskServiceRecord`, `string title = 2`, `bool done = 3`, `message CreateRequest`, `message ReadRequest`, `message DeleteRequest`, `message ListRequest`, `message ToggleCompleteRequest`, } for _, c := range checks { if !strings.Contains(proto, c) { t.Errorf("buildProto() missing %q", c) } } // Create should not include id, created, updated createIdx := strings.Index(proto, "message CreateRequest") createEnd := strings.Index(proto[createIdx:], "}") createBlock := proto[createIdx : createIdx+createEnd] for _, skip := range []string{"string id", "int64 created", "int64 updated"} { if strings.Contains(createBlock, skip) { t.Errorf("CreateRequest should not contain %q", skip) } } } func TestBuildMain(t *testing.T) { // New naming: no -service suffix main := buildMain("order", "Order") checks := []string{ `"order/handler"`, `pb "order/proto"`, `micro.NewService("order"`, `pb.RegisterOrderHandler`, `handler.New()`, } for _, c := range checks { if !strings.Contains(main, c) { t.Errorf("buildMain(order) missing %q", c) } } // Legacy naming: -service suffix stripped main = buildMain("order-service", "OrderService") checks = []string{ `"order-service/handler"`, `pb "order-service/proto"`, `micro.NewService("order"`, `pb.RegisterOrderServiceHandler`, `handler.New()`, } for _, c := range checks { if !strings.Contains(main, c) { t.Errorf("buildMain() missing %q", c) } } } func TestHandlerModifiedTracking(t *testing.T) { dir := t.TempDir() handlerDir := filepath.Join(dir, "handler") os.MkdirAll(handlerDir, 0755) handlerFile := filepath.Join(handlerDir, "test.go") // No .micro file → not modified os.WriteFile(handlerFile, []byte("package handler\n"), 0644) if handlerModified(dir, handlerFile) { t.Error("expected not modified when no .micro exists") } // Record hash → not modified recordHandlerHash(dir, handlerFile) if handlerModified(dir, handlerFile) { t.Error("expected not modified after recording hash") } // Edit the file → modified os.WriteFile(handlerFile, []byte("package handler\n\nfunc Foo() {}\n"), 0644) if !handlerModified(dir, handlerFile) { t.Error("expected modified after editing file") } // Re-record → not modified again recordHandlerHash(dir, handlerFile) if handlerModified(dir, handlerFile) { t.Error("expected not modified after re-recording hash") } } func TestMetaReadWrite(t *testing.T) { dir := t.TempDir() m := readMeta(dir) if len(m) != 0 { t.Error("expected empty meta for new dir") } m["handler_hash"] = "abc123" m["version"] = "1" writeMeta(dir, m) m2 := readMeta(dir) if m2["handler_hash"] != "abc123" || m2["version"] != "1" { t.Errorf("readMeta() = %v, want handler_hash=abc123, version=1", m2) } } func TestGenerateStructure(t *testing.T) { dir := t.TempDir() svcDir := filepath.Join(dir, "test-svc") svc := ServiceSpec{ Name: "test-svc", Description: "Test service", Fields: []FieldSpec{ {Name: "id", Type: "string"}, {Name: "name", Type: "string"}, }, Endpoints: []EndpointSpec{ {Name: "Create"}, {Name: "Read"}, }, } if err := generateStructure(svcDir, svc); err != nil { t.Fatal(err) } // Check files exist for _, f := range []string{ "proto/test-svc.proto", "handler/test-svc.go", "main.go", "go.mod", "Makefile", ".gitignore", } { if _, err := os.Stat(filepath.Join(svcDir, f)); err != nil { t.Errorf("missing %s: %v", f, err) } } // Check .micro was created with handler hash meta := readMeta(svcDir) if meta["handler_hash"] == "" { t.Error("expected handler_hash in .micro after generateStructure") } // Run again — should not overwrite main.go mainBefore, _ := os.ReadFile(filepath.Join(svcDir, "main.go")) os.WriteFile(filepath.Join(svcDir, "main.go"), []byte("// user edited\n"), 0644) if err := generateStructure(svcDir, svc); err != nil { t.Fatal(err) } mainAfter, _ := os.ReadFile(filepath.Join(svcDir, "main.go")) if string(mainAfter) == string(mainBefore) { t.Error("expected main.go to keep user edit on re-run") } // Proto should be protected if user modified it protoFile := filepath.Join(svcDir, "proto", "test-svc.proto") os.WriteFile(protoFile, []byte("// user-edited proto\n"), 0644) if err := generateStructure(svcDir, svc); err != nil { t.Fatal(err) } protoAfter, _ := os.ReadFile(protoFile) if string(protoAfter) != "// user-edited proto\n" { t.Error("expected proto to be preserved after user edit") } // Proto should regenerate if NOT modified recordFileHash(svcDir, "proto_hash", protoFile) if err := generateStructure(svcDir, svc); err != nil { t.Fatal(err) } protoAfter2, _ := os.ReadFile(protoFile) _ = protoAfter2 } func TestFileModified(t *testing.T) { dir := t.TempDir() f := filepath.Join(dir, "test.txt") os.WriteFile(f, []byte("original"), 0644) // No hash → not modified if fileModified(dir, "test_hash", f) { t.Error("expected not modified with no saved hash") } recordFileHash(dir, "test_hash", f) // Same content → not modified if fileModified(dir, "test_hash", f) { t.Error("expected not modified with matching hash") } // Changed content → modified os.WriteFile(f, []byte("changed"), 0644) if !fileModified(dir, "test_hash", f) { t.Error("expected modified after content change") } } func TestDiscoverExisting(t *testing.T) { dir := t.TempDir() // Empty directory → empty string if got := discoverExisting(dir); got != "" { t.Errorf("expected empty for empty dir, got %q", got) } // Non-service directory (no proto) → empty os.MkdirAll(filepath.Join(dir, "not-a-service"), 0755) if got := discoverExisting(dir); got != "" { t.Errorf("expected empty for dir without proto, got %q", got) } // Create a real service directory with proto svcDir := filepath.Join(dir, "order-service") os.MkdirAll(filepath.Join(svcDir, "proto"), 0755) os.WriteFile(filepath.Join(svcDir, "proto", "order-service.proto"), []byte("syntax = \"proto3\";\nservice OrderService {}"), 0644) got := discoverExisting(dir) if !strings.Contains(got, "order-service") { t.Errorf("expected to find order-service, got %q", got) } if !strings.Contains(got, "OrderService") { t.Errorf("expected to find proto content, got %q", got) } // Add a second service svc2Dir := filepath.Join(dir, "user-service") os.MkdirAll(filepath.Join(svc2Dir, "proto"), 0755) os.WriteFile(filepath.Join(svc2Dir, "proto", "user-service.proto"), []byte("syntax = \"proto3\";\nservice UserService {}"), 0644) got = discoverExisting(dir) if !strings.Contains(got, "order-service") || !strings.Contains(got, "user-service") { t.Errorf("expected both services, got %q", got) } } func TestIsTruncated(t *testing.T) { tests := []struct { name string code string want bool }{ {"complete", "package handler\n\nfunc New() *H { return &H{} }\n", false}, {"empty", "", true}, {"no closing brace", "package handler\n\nfunc Foo() {", true}, {"unbalanced", "package handler\n\nfunc Foo() {\n\tif true {", true}, {"balanced", "package handler\n\nfunc Foo() {\n\tif true {\n\t}\n}", false}, {"trailing whitespace ok", "package handler\n\ntype X struct{}\n\n", false}, {"mid-expression", "package handler\n\nfunc F() {\n\tx := 1 +", true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := isTruncated(tt.code); got != tt.want { t.Errorf("isTruncated() = %v, want %v", got, tt.want) } }) } }