emit_test.go raw
1 package emit
2
3 import (
4 "fmt"
5 "go/parser"
6 "go/token"
7 "sort"
8 "strings"
9 "testing"
10
11 "git.mleku.dev/mleku/dendrite/pkg/axiom"
12 "git.mleku.dev/mleku/dendrite/pkg/enzyme"
13 "git.mleku.dev/mleku/dendrite/pkg/lattice"
14 )
15
16 type tagConstraint struct{ tag string }
17
18 func (c tagConstraint) Tag() string { return c.tag }
19 func (c tagConstraint) Admits(e axiom.Element) bool { return e.Type() == c.tag }
20
21 // bondAll ingests Go source into a lattice and bonds elements to matching sites.
22 func bondAll(t *testing.T, l *lattice.Lattice, src string) int {
23 t.Helper()
24 ge := enzyme.GoSource{}
25 elements := ge.Digest(strings.NewReader(src))
26 bonded := 0
27 for e := range elements {
28 for _, n := range l.Nodes() {
29 if !n.Occupied() && n.Admits(e) {
30 n.Bond(e)
31 bonded++
32 break
33 }
34 }
35 }
36 return bonded
37 }
38
39 // makeLattice creates a lattice with sites for all Go AST element types.
40 func makeLattice(t *testing.T) *lattice.Lattice {
41 t.Helper()
42 l := lattice.New()
43 tags := []string{
44 "package", "import", "type", "struct", "interface", "field",
45 "func", "method", "comment",
46 "assign", "return", "if", "for", "switch", "select",
47 "go", "send", "expr", "defer", "decl", "branch", "case", "comm",
48 // Declaration-level ident subtypes.
49 "ident:func-name", "ident:method-name", "ident:type-name",
50 "ident:field-name", "ident:param", "ident:result",
51 "ident:receiver", "ident:var-name",
52 }
53 for _, tag := range tags {
54 for range 20 {
55 n := l.AddNode([]axiom.Constraint{tagConstraint{tag}})
56 n.SetEnergy(true)
57 }
58 }
59 // Wire up neighbors.
60 nodes := l.Nodes()
61 for i := range nodes {
62 if i > 0 {
63 l.Connect(nodes[i-1], nodes[i])
64 }
65 }
66 return l
67 }
68
69 func TestHarvestAndEmit(t *testing.T) {
70 l := lattice.New()
71
72 tags := []string{"package", "import", "type", "struct", "field",
73 "func", "ident:var-name", "ident:func-name", "method", "return"}
74 nodes := make(map[string]*lattice.Node)
75 for _, tag := range tags {
76 n := l.AddNode([]axiom.Constraint{tagConstraint{tag}})
77 nodes[tag] = n
78 }
79 for range 5 {
80 l.AddNode([]axiom.Constraint{tagConstraint{"ident:var-name"}})
81 }
82
83 nodes["package"].Bond(enzyme.Elem("package", "main"))
84 nodes["import"].Bond(enzyme.Elem("import", `"fmt"`))
85 nodes["type"].Bond(enzyme.Elem("type", "MyType"))
86 nodes["struct"].Bond(enzyme.Elem("struct", "MyType"))
87 nodes["field"].Bond(enzyme.Elem("field", "MyType\x00Name string"))
88 nodes["func"].Bond(enzyme.Elem("func", "main()"))
89 nodes["ident:var-name"].Bond(enzyme.Elem("ident:var-name", "foo"))
90 nodes["ident:func-name"].Bond(enzyme.Elem("ident:func-name", "main"))
91 nodes["method"].Bond(enzyme.Elem("method", "MyType.Run()"))
92 nodes["return"].Bond(enzyme.Elem("return", ""))
93
94 files := Harvest(l)
95 frags := files[""]
96 t.Logf("harvested %d fragments", len(frags))
97
98 var buf strings.Builder
99 err := EmitGo(frags, &buf)
100 if err != nil {
101 t.Fatal(err)
102 }
103
104 src := buf.String()
105 t.Log("--- emitted source ---")
106 t.Log(src)
107
108 if !strings.Contains(src, "package main") {
109 t.Error("missing package declaration")
110 }
111 if !strings.Contains(src, "type MyType struct") {
112 t.Error("missing type declaration")
113 }
114 if !strings.Contains(src, "func main()") {
115 t.Error("missing func declaration")
116 }
117 }
118
119 func TestEmitFromSelfIngest(t *testing.T) {
120 l := makeLattice(t)
121
122 src := `package main
123
124 import "fmt"
125
126 type Thing struct {
127 Name string
128 }
129
130 func main() {
131 f := Thing{Name: "world"}
132 fmt.Println(f.Name)
133 }
134 `
135 bonded := bondAll(t, l, src)
136 t.Logf("bonded %d elements", bonded)
137
138 files := Harvest(l)
139 frags := files[""]
140
141 var buf strings.Builder
142 err := EmitGo(frags, &buf)
143 if err != nil {
144 t.Fatal(err)
145 }
146
147 emitted := buf.String()
148 t.Log("--- emitted from self-ingest ---")
149 t.Log(emitted)
150
151 if !strings.Contains(emitted, "package main") {
152 t.Error("missing package declaration")
153 }
154 if !strings.Contains(emitted, "func main()") {
155 t.Error("expected main function")
156 }
157 // Should have the assignment in main's body.
158 if !strings.Contains(emitted, "Thing{") {
159 t.Error("expected Thing struct literal in main body")
160 }
161 // Should have the fmt.Println call.
162 if !strings.Contains(emitted, "fmt.Println") {
163 t.Error("expected fmt.Println call in body")
164 }
165 // Should have fmt import.
166 if !strings.Contains(emitted, `"fmt"`) {
167 t.Error("expected fmt import")
168 }
169 }
170
171 func TestEmitReconstructsBody(t *testing.T) {
172 l := makeLattice(t)
173
174 src := `package main
175
176 import "fmt"
177
178 func greet(name string) string {
179 if name == "" {
180 return "hello, stranger"
181 }
182 return fmt.Sprintf("hello, %s", name)
183 }
184
185 func main() {
186 msg := greet("world")
187 fmt.Println(msg)
188 }
189 `
190 bonded := bondAll(t, l, src)
191 t.Logf("bonded %d elements", bonded)
192
193 files := Harvest(l)
194 frags := files[""]
195
196 var buf strings.Builder
197 err := EmitGo(frags, &buf)
198 if err != nil {
199 t.Fatal(err)
200 }
201
202 emitted := buf.String()
203 t.Log("--- emitted with body reconstruction ---")
204 t.Log(emitted)
205
206 // Check function declarations include signatures.
207 if !strings.Contains(emitted, "func greet(") {
208 t.Error("expected greet function with parameters")
209 }
210 // Check greet body has the if statement.
211 if !strings.Contains(emitted, `name == ""`) {
212 t.Error("expected if condition in greet body")
213 }
214 // Check return statement.
215 if !strings.Contains(emitted, `return fmt.Sprintf`) || !strings.Contains(emitted, `return "hello, stranger"`) {
216 t.Error("expected return statements in greet body")
217 }
218 // Check main body has assignment.
219 if !strings.Contains(emitted, `greet("world")`) {
220 t.Error("expected greet call in main body")
221 }
222 }
223
224 func TestEmitMethodReconstruction(t *testing.T) {
225 l := makeLattice(t)
226
227 src := `package main
228
229 import "fmt"
230
231 type Server struct {
232 Port int
233 }
234
235 func (s Server) Start() error {
236 fmt.Printf("listening on %d\n", s.Port)
237 return nil
238 }
239
240 func main() {
241 s := Server{Port: 8080}
242 s.Start()
243 }
244 `
245 bonded := bondAll(t, l, src)
246 t.Logf("bonded %d elements", bonded)
247
248 files := Harvest(l)
249 frags := files[""]
250
251 var buf strings.Builder
252 err := EmitGo(frags, &buf)
253 if err != nil {
254 t.Fatal(err)
255 }
256
257 emitted := buf.String()
258 t.Log("--- emitted with methods ---")
259 t.Log(emitted)
260
261 // Method should have receiver with original variable name.
262 if !strings.Contains(emitted, "func (s Server) Start()") {
263 t.Error("expected method with receiver")
264 }
265 // Method body should have the Printf call.
266 if !strings.Contains(emitted, "fmt.Printf") {
267 t.Error("expected fmt.Printf in method body")
268 }
269 // Struct declaration.
270 if !strings.Contains(emitted, "type Server struct") {
271 t.Error("expected Server struct")
272 }
273 }
274
275 func TestEmitCompilesClean(t *testing.T) {
276 l := makeLattice(t)
277
278 src := `package main
279
280 import "fmt"
281
282 type Score struct {
283 Value int
284 }
285
286 func compute() int {
287 return 42
288 }
289
290 func main() {
291 x := compute()
292 fmt.Printf("score: %d\n", x)
293 }
294 `
295 bondAll(t, l, src)
296
297 files := Harvest(l)
298 frags := files[""]
299
300 var buf strings.Builder
301 err := EmitGo(frags, &buf)
302 if err != nil {
303 t.Fatal(err)
304 }
305
306 emitted := buf.String()
307 t.Log("--- emitted source ---")
308 t.Log(emitted)
309
310 // Verify it parses as valid Go.
311 fset := token.NewFileSet()
312 _, parseErr := parser.ParseFile(fset, "emitted.go", emitted, parser.SkipObjectResolution)
313 if parseErr != nil {
314 t.Errorf("emitted source does not parse as valid Go: %v\nsource:\n%s", parseErr, emitted)
315 }
316 }
317
318 func TestEmitLegacyMode(t *testing.T) {
319 // Test that the legacy self-logging mode still works when only
320 // declarations and literals are present (no body elements).
321 l := lattice.New()
322
323 nodes := make(map[string]*lattice.Node)
324 for _, tag := range []string{"package", "type", "struct", "field", "func", "literal:string", "method"} {
325 n := l.AddNode([]axiom.Constraint{tagConstraint{tag}})
326 nodes[tag] = n
327 }
328
329 nodes["package"].Bond(enzyme.Elem("package", "main"))
330 nodes["type"].Bond(enzyme.Elem("type", "Score"))
331 nodes["struct"].Bond(enzyme.Elem("struct", "Score"))
332 nodes["field"].Bond(enzyme.Elem("field", "Score\x00Value int"))
333 nodes["func"].Bond(enzyme.Elem("func", "main()"))
334 nodes["literal:string"].Bond(enzyme.Elem("literal:string", `"score: %d nodes\n"`))
335 nodes["method"].Bond(enzyme.Elem("method", "Score.Compute()"))
336
337 files := Harvest(l)
338 frags := files[""]
339
340 var buf strings.Builder
341 err := EmitGo(frags, &buf)
342 if err != nil {
343 t.Fatal(err)
344 }
345
346 emitted := buf.String()
347 t.Log("--- legacy emitted source ---")
348 t.Log(emitted)
349
350 // Should have self-knowledge in legacy mode.
351 if !strings.Contains(emitted, "nTypes") {
352 t.Error("expected self-knowledge declaration in legacy mode")
353 }
354 if !strings.Contains(emitted, "fmt.Printf") {
355 t.Error("expected fmt.Printf for format strings in legacy mode")
356 }
357 }
358
359 func TestParseBodyValue(t *testing.T) {
360 parent, source := parseBodyValue("main\x00x := 42")
361 if parent != "main" {
362 t.Errorf("expected parent 'main', got %q", parent)
363 }
364 if source != "x := 42" {
365 t.Errorf("expected source 'x := 42', got %q", source)
366 }
367
368 // Legacy element with no separator.
369 parent, source = parseBodyValue("")
370 if parent != "" {
371 t.Errorf("expected empty parent, got %q", parent)
372 }
373 }
374
375 func TestParseFuncName(t *testing.T) {
376 tests := []struct {
377 val string
378 isMethod bool
379 want string
380 }{
381 {"main()", false, "main"},
382 {"greet(name string) string", false, "greet"},
383 {"Foo.Hello() string", true, "Hello"},
384 {"*Server.Start() error", true, "Start"},
385 }
386 for _, tt := range tests {
387 got := parseFuncName(tt.val, tt.isMethod)
388 if got != tt.want {
389 t.Errorf("parseFuncName(%q, %v) = %q, want %q", tt.val, tt.isMethod, got, tt.want)
390 }
391 }
392 }
393
394 func TestEmitMultipleStructs(t *testing.T) {
395 l := makeLattice(t)
396
397 src := `package main
398
399 import "fmt"
400
401 type Server struct {
402 Port int
403 Host string
404 }
405
406 type Client struct {
407 Name string
408 Timeout int
409 }
410
411 func main() {
412 s := Server{Port: 8080, Host: "localhost"}
413 c := Client{Name: "test", Timeout: 30}
414 fmt.Println(s, c)
415 }
416 `
417 bonded := bondAll(t, l, src)
418 t.Logf("bonded %d elements", bonded)
419
420 files := Harvest(l)
421 frags := files[""]
422
423 var buf strings.Builder
424 err := EmitGo(frags, &buf)
425 if err != nil {
426 t.Fatal(err)
427 }
428
429 emitted := buf.String()
430 t.Log("--- emitted with multiple structs ---")
431 t.Log(emitted)
432
433 // Server struct should have Port and Host but NOT Name or Timeout.
434 if !strings.Contains(emitted, "type Server struct") {
435 t.Error("expected Server struct")
436 }
437 if !strings.Contains(emitted, "type Client struct") {
438 t.Error("expected Client struct")
439 }
440
441 // Find Server struct block and verify its fields.
442 serverIdx := strings.Index(emitted, "type Server struct")
443 clientIdx := strings.Index(emitted, "type Client struct")
444 if serverIdx < 0 || clientIdx < 0 {
445 t.Fatal("missing struct declarations")
446 }
447
448 // Extract the Server struct block.
449 serverEnd := strings.Index(emitted[serverIdx:], "}\n")
450 serverBlock := emitted[serverIdx : serverIdx+serverEnd+2]
451 t.Logf("Server block: %q", serverBlock)
452
453 if !strings.Contains(serverBlock, "Port") {
454 t.Error("Server should contain Port field")
455 }
456 if !strings.Contains(serverBlock, "Host") {
457 t.Error("Server should contain Host field")
458 }
459 if strings.Contains(serverBlock, "Name") {
460 t.Error("Server should NOT contain Name field (belongs to Client)")
461 }
462 if strings.Contains(serverBlock, "Timeout") {
463 t.Error("Server should NOT contain Timeout field (belongs to Client)")
464 }
465
466 // Extract the Client struct block.
467 clientEnd := strings.Index(emitted[clientIdx:], "}\n")
468 clientBlock := emitted[clientIdx : clientIdx+clientEnd+2]
469 t.Logf("Client block: %q", clientBlock)
470
471 if !strings.Contains(clientBlock, "Name") {
472 t.Error("Client should contain Name field")
473 }
474 if !strings.Contains(clientBlock, "Timeout") {
475 t.Error("Client should contain Timeout field")
476 }
477 if strings.Contains(clientBlock, "Port") {
478 t.Error("Client should NOT contain Port field (belongs to Server)")
479 }
480
481 // Verify it parses as valid Go.
482 fset := token.NewFileSet()
483 _, parseErr := parser.ParseFile(fset, "emitted.go", emitted, parser.SkipObjectResolution)
484 if parseErr != nil {
485 t.Errorf("emitted source does not parse: %v\nsource:\n%s", parseErr, emitted)
486 }
487 }
488
489 func TestEmitDirective(t *testing.T) {
490 l := makeLattice(t)
491 // Add sites for directive and var element types.
492 for range 4 {
493 n := l.AddNode([]axiom.Constraint{tagConstraint{"directive"}})
494 n.SetEnergy(true)
495 }
496 for range 4 {
497 n := l.AddNode([]axiom.Constraint{tagConstraint{"var"}})
498 n.SetEnergy(true)
499 }
500
501 src := `package main
502
503 import "embed"
504
505 //go:embed hello.txt
506 var content string
507
508 func main() {
509 println(content)
510 }
511 `
512 bonded := bondAll(t, l, src)
513 t.Logf("bonded %d elements", bonded)
514
515 files := Harvest(l)
516 frags := files[""]
517
518 var buf strings.Builder
519 err := EmitGo(frags, &buf)
520 if err != nil {
521 t.Fatal(err)
522 }
523
524 emitted := buf.String()
525 t.Log("--- emitted with directive ---")
526 t.Log(emitted)
527
528 // The //go:embed directive should appear in the output.
529 if !strings.Contains(emitted, "//go:embed") {
530 t.Error("expected //go:embed directive in emitted source")
531 }
532
533 // The var declaration should appear.
534 if !strings.Contains(emitted, "var content") {
535 t.Error("expected var content declaration in emitted source")
536 }
537
538 // The directive should appear before the var declaration.
539 embedIdx := strings.Index(emitted, "//go:embed")
540 varIdx := strings.Index(emitted, "var content")
541 if embedIdx >= 0 && varIdx >= 0 && embedIdx > varIdx {
542 t.Error("//go:embed directive should appear before var declaration")
543 }
544 }
545
546 func TestImportPruning(t *testing.T) {
547 // Verify unused imports are pruned.
548 stmts := []bodyStmt{
549 {Source: `fmt.Println("hello")`},
550 }
551 imports := []Fragment{
552 {Type: "import", Value: `"fmt"`},
553 {Type: "import", Value: `"os"`}, // unused — should be pruned
554 {Type: "import", Value: `"strings"`}, // unused — should be pruned
555 }
556 result := inferImports(stmts, imports, nil, nil, nil)
557 if len(result) != 1 || result[0] != `"fmt"` {
558 t.Errorf("expected only fmt import, got %v", result)
559 }
560 }
561
562 func TestRejectUntrustedImport(t *testing.T) {
563 // External imports must be rejected by trust policy.
564 stmts := []bodyStmt{
565 {Source: `evil.DoSomething()`},
566 {Source: `axiom.NewElement()`},
567 }
568 imports := []Fragment{
569 {Type: "import", Value: `"github.com/evil/pkg"`}, // untrusted
570 {Type: "import", Value: `"git.mleku.dev/mleku/dendrite/pkg/axiom"`}, // trusted self-import
571 {Type: "import", Value: `"fmt"`}, // trusted stdlib (unused)
572 }
573 result := inferImports(stmts, imports, nil, nil, nil)
574
575 // Only the self-import should survive (used + trusted).
576 // fmt is trusted but unused, evil is used but untrusted.
577 found := make(map[string]bool)
578 for _, imp := range result {
579 found[imp] = true
580 }
581
582 if found[`"github.com/evil/pkg"`] {
583 t.Error("untrusted external import should have been rejected")
584 }
585 if !found[`"git.mleku.dev/mleku/dendrite/pkg/axiom"`] {
586 t.Error("trusted self-import should have been kept")
587 }
588 if found[`"fmt"`] {
589 t.Error("unused import should have been pruned")
590 }
591 }
592
593 func TestInferImportsFromSignatures(t *testing.T) {
594 // Imports should be inferred from function signatures and var declarations,
595 // not just body statements.
596 stmts := []bodyStmt{
597 {Source: `fmt.Println("hello")`},
598 }
599 funcs := []funcDecl{
600 {Name: "foo", Signature: "foo(s *spore.Spore) ratio.Ratio", IsMethod: false},
601 }
602 vars := []Fragment{
603 {Type: "var", Value: `var Name = reflect.TypeOf(Block{}).Name()`},
604 }
605 result := inferImports(stmts, nil, funcs, vars, nil)
606 found := make(map[string]bool)
607 for _, imp := range result {
608 found[imp] = true
609 }
610 if !found[`"fmt"`] {
611 t.Error("expected fmt import from body, got", result)
612 }
613 if !found[`"reflect"`] {
614 t.Error("expected reflect import from var declaration, got", result)
615 }
616 if !found[`"git.mleku.dev/mleku/dendrite/pkg/spore"`] {
617 t.Error("expected spore import from func signature, got", result)
618 }
619 if !found[`"git.mleku.dev/mleku/dendrite/pkg/ratio"`] {
620 t.Error("expected ratio import from func signature, got", result)
621 }
622 }
623
624 func TestDependencyOrdering(t *testing.T) {
625 // Strict dependency chain: x → y → Println.
626 // x must come before y, y before Println.
627 stmts := []bodyStmt{
628 {Source: `fmt.Println(y)`, Tag: "expr"},
629 {Source: `y := x + 1`, Tag: "assign"},
630 {Source: `x := 1`, Tag: "assign"},
631 }
632
633 for range 10 {
634 ordered := orderBody(stmts)
635 xIdx, yIdx, printIdx := -1, -1, -1
636 for i, s := range ordered {
637 switch {
638 case strings.Contains(s.Source, "x := 1"):
639 xIdx = i
640 case strings.Contains(s.Source, "y := x"):
641 yIdx = i
642 case strings.Contains(s.Source, "Println"):
643 printIdx = i
644 }
645 }
646 if xIdx > yIdx {
647 t.Errorf("x := 1 (idx %d) should come before y := x + 1 (idx %d)", xIdx, yIdx)
648 }
649 if yIdx > printIdx {
650 t.Errorf("y := x + 1 (idx %d) should come before Println(y) (idx %d)", yIdx, printIdx)
651 }
652 }
653 }
654
655 func TestNonDependentShuffling(t *testing.T) {
656 // Three independent assignments + one dependent Println.
657 // a, b, c have no dependencies on each other — should be shuffled.
658 // Println depends on all three — must come last.
659 stmts := []bodyStmt{
660 {Source: `a := 1`, Tag: "assign"},
661 {Source: `b := 2`, Tag: "assign"},
662 {Source: `c := 3`, Tag: "assign"},
663 {Source: `fmt.Println(a, b, c)`, Tag: "expr"},
664 }
665
666 orderings := make(map[string]bool)
667 for range 100 {
668 ordered := orderBody(stmts)
669
670 // Println must always be last.
671 last := ordered[len(ordered)-1]
672 if !strings.Contains(last.Source, "Println") {
673 t.Fatal("Println must always be last (depends on a, b, c)")
674 }
675
676 // Collect the order of a, b, c.
677 var order string
678 for _, s := range ordered[:3] {
679 switch {
680 case strings.Contains(s.Source, "a :="):
681 order += "a"
682 case strings.Contains(s.Source, "b :="):
683 order += "b"
684 case strings.Contains(s.Source, "c :="):
685 order += "c"
686 }
687 }
688 orderings[order] = true
689 }
690
691 if len(orderings) < 2 {
692 t.Errorf("expected at least 2 distinct orderings of independent statements, got %d: %v",
693 len(orderings), orderings)
694 }
695 t.Logf("observed %d distinct orderings: %v", len(orderings), orderings)
696 }
697
698 func TestEmitProject(t *testing.T) {
699 // Create fragments for two files: main package and a library package.
700 mainFrags := []Fragment{
701 {Type: "package", Value: "main"},
702 {Type: "import", Value: `"fmt"`},
703 {Type: "import", Value: `"git.mleku.dev/mleku/dendrite/pkg/axiom"`},
704 {Type: "func", Value: "main()"},
705 }
706 // Simulate body content so imports get resolved.
707 mainFrags = append(mainFrags, Fragment{Type: "expr", Value: "main\x00fmt.Println(axiom.New())"})
708
709 libFrags := []Fragment{
710 {Type: "package", Value: "axiom"},
711 {Type: "type", Value: "Element"},
712 {Type: "interface", Value: "Element"},
713 {Type: "func", Value: "New() Element"},
714 }
715
716 files := map[string][]Fragment{
717 "main.go": mainFrags,
718 "axiom/axiom.go": libFrags,
719 }
720
721 project := EmitProject(files, "git.mleku.dev/mleku/dendrite")
722
723 // Should have 3 files: main.go, axiom/axiom.go, go.mod.
724 if len(project) != 3 {
725 t.Errorf("expected 3 files, got %d: %v", len(project), keys(project))
726 }
727
728 // Check go.mod exists and has replace directive.
729 gomod := project["go.mod"]
730 if gomod == "" {
731 t.Fatal("missing go.mod")
732 }
733 t.Log("--- go.mod ---")
734 t.Log(gomod)
735
736 if !strings.Contains(gomod, "module git.mleku.dev/mleku/dendrite") {
737 t.Error("go.mod missing module declaration")
738 }
739 if !strings.Contains(gomod, "git.mleku.dev/mleku/dendrite/axiom => ./axiom") {
740 t.Error("go.mod missing replace directive for axiom")
741 }
742
743 // Check main.go has package main.
744 mainSrc := project["main.go"]
745 if !strings.Contains(mainSrc, "package main") {
746 t.Error("main.go missing package main")
747 }
748 t.Log("--- main.go ---")
749 t.Log(mainSrc)
750
751 // Check axiom/axiom.go has package axiom.
752 axiomSrc := project["axiom/axiom.go"]
753 if !strings.Contains(axiomSrc, "package axiom") {
754 t.Error("axiom/axiom.go missing package axiom")
755 }
756 t.Log("--- axiom/axiom.go ---")
757 t.Log(axiomSrc)
758 }
759
760 func TestGoModReplace(t *testing.T) {
761 internalPkgs := map[string]bool{
762 "axiom": true,
763 "emit": true,
764 "enzyme": true,
765 "lattice": true,
766 }
767 gomod := emitGoMod("git.mleku.dev/mleku/dendrite", internalPkgs)
768 t.Log(gomod)
769
770 if !strings.Contains(gomod, "module git.mleku.dev/mleku/dendrite") {
771 t.Error("missing module declaration")
772 }
773 if !strings.Contains(gomod, "go 1.24") {
774 t.Error("missing go version")
775 }
776
777 // All internal packages should have replace directives.
778 for pkg := range internalPkgs {
779 expected := fmt.Sprintf("git.mleku.dev/mleku/dendrite/%s => ./%s", pkg, pkg)
780 if !strings.Contains(gomod, expected) {
781 t.Errorf("missing replace directive for %s", pkg)
782 }
783 }
784 }
785
786 func keys(m map[string]string) []string {
787 var ks []string
788 for k := range m {
789 ks = append(ks, k)
790 }
791 sort.Strings(ks)
792 return ks
793 }
794
795 func TestEmitGoPackage(t *testing.T) {
796 // Verify non-main package emission doesn't force a main function.
797 frags := []Fragment{
798 {Type: "package", Value: "axiom"},
799 {Type: "type", Value: "Element"},
800 {Type: "interface", Value: "Element"},
801 }
802
803 var buf strings.Builder
804 err := EmitGoPackage(frags, &buf, "axiom")
805 if err != nil {
806 t.Fatal(err)
807 }
808
809 emitted := buf.String()
810 t.Log("--- non-main package ---")
811 t.Log(emitted)
812
813 if !strings.Contains(emitted, "package axiom") {
814 t.Error("expected package axiom")
815 }
816 if strings.Contains(emitted, "func main()") {
817 t.Error("non-main package should NOT have func main()")
818 }
819 }
820