e071084ebe
govulncheck / govulncheck (push) Has been cancelled
Lint / golangci-lint (push) Has been cancelled
Run Tests / Unit Tests (push) Has been cancelled
Run Tests / Etcd Integration Tests (push) Has been cancelled
Harness (E2E) / Harnesses (mock LLM) (push) Has been cancelled
Harness (E2E) / Provider harnesses (live LLM conformance) (push) Has been cancelled
419 lines
11 KiB
Go
419 lines
11 KiB
Go
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)
|
|
}
|
|
})
|
|
}
|
|
}
|