feat(dev): add api test-gen command

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Virgil 2026-04-01 05:10:56 +00:00
parent 129199a5e0
commit b7d70883e9
5 changed files with 192 additions and 5 deletions

View file

@ -1,8 +1,8 @@
package dev
import (
"forge.lthn.ai/core/cli/pkg/cli"
"dappco.re/go/core/i18n"
"forge.lthn.ai/core/cli/pkg/cli"
)
// addAPICommands adds the 'api' command and its subcommands to the given parent command.
@ -17,6 +17,6 @@ func addAPICommands(parent *cli.Command) {
// Add the 'sync' command to 'api'
addSyncCommand(apiCmd)
// TODO: Add the 'test-gen' command to 'api'
// addTestGenCommand(apiCmd)
// Add the 'test-gen' command to 'api'
addTestGenCommand(apiCmd)
}

112
cmd/dev/cmd_api_testgen.go Normal file
View file

@ -0,0 +1,112 @@
package dev
import (
"bytes"
"path/filepath"
"text/template"
"dappco.re/go/core/i18n"
coreio "dappco.re/go/core/io"
"forge.lthn.ai/core/cli/pkg/cli"
)
func addTestGenCommand(parent *cli.Command) {
testGenCmd := &cli.Command{
Use: "test-gen",
Short: i18n.T("cmd.dev.api.test_gen.short"),
Long: i18n.T("cmd.dev.api.test_gen.long"),
RunE: func(cmd *cli.Command, args []string) error {
if err := runTestGen(); err != nil {
return cli.Wrap(err, i18n.Label("error"))
}
cli.Text(i18n.T("i18n.done.sync", "public API tests"))
return nil
},
}
parent.AddCommand(testGenCmd)
}
func runTestGen() error {
pkgDir := "pkg"
internalDirs, err := coreio.Local.List(pkgDir)
if err != nil {
return cli.Wrap(err, "failed to read pkg directory")
}
for _, dir := range internalDirs {
if !dir.IsDir() || dir.Name() == "core" {
continue
}
serviceName := dir.Name()
internalFile := filepath.Join(pkgDir, serviceName, serviceName+".go")
publicDir := serviceName
publicTestFile := filepath.Join(publicDir, serviceName+"_test.go")
if !coreio.Local.IsFile(internalFile) {
continue
}
symbols, err := getExportedSymbols(internalFile)
if err != nil {
return cli.Wrap(err, cli.Sprintf("error getting symbols for service '%s'", serviceName))
}
if len(symbols) == 0 {
continue
}
if err := generatePublicAPITestFile(publicDir, publicTestFile, serviceName, symbols); err != nil {
return cli.Wrap(err, cli.Sprintf("error generating public API test file for service '%s'", serviceName))
}
}
return nil
}
const publicAPITestTemplate = `// Code generated by "core dev api test-gen"; DO NOT EDIT.
package {{.ServiceName}}
import (
impl "forge.lthn.ai/core/cli/{{.ServiceName}}"
)
{{range .Symbols}}
{{- if eq .Kind "type"}}
type _ = impl.{{.Name}}
{{- else if eq .Kind "const"}}
const _ = impl.{{.Name}}
{{- else if eq .Kind "var"}}
var _ = impl.{{.Name}}
{{- else if eq .Kind "func"}}
var _ = impl.{{.Name}}
{{- end}}
{{end}}
`
func generatePublicAPITestFile(dir, path, serviceName string, symbols []symbolInfo) error {
if err := coreio.Local.EnsureDir(dir); err != nil {
return err
}
tmpl, err := template.New("publicAPITest").Parse(publicAPITestTemplate)
if err != nil {
return err
}
data := struct {
ServiceName string
Symbols []symbolInfo
}{
ServiceName: serviceName,
Symbols: symbols,
}
var buf bytes.Buffer
if err := tmpl.Execute(&buf, data); err != nil {
return err
}
return coreio.Local.Write(path, buf.String())
}

View file

@ -0,0 +1,70 @@
package dev
import (
"os"
"path/filepath"
"strings"
"testing"
"github.com/stretchr/testify/require"
"dappco.re/go/core/io"
)
func TestRunTestGen_Good(t *testing.T) {
tmpDir := t.TempDir()
originalWD, err := os.Getwd()
require.NoError(t, err)
t.Cleanup(func() {
_ = os.Chdir(originalWD)
})
require.NoError(t, os.Chdir(tmpDir))
serviceDir := filepath.Join(tmpDir, "pkg", "demo")
require.NoError(t, io.Local.EnsureDir(serviceDir))
require.NoError(t, io.Local.Write(filepath.Join(serviceDir, "demo.go"), `package demo
type Example struct{}
const Answer = 42
var Value = Example{}
func Run() {}
`))
require.NoError(t, runTestGen())
generatedPath := filepath.Join(tmpDir, "demo", "demo_test.go")
content, err := io.Local.Read(generatedPath)
require.NoError(t, err)
require.Contains(t, content, `// Code generated by "core dev api test-gen"; DO NOT EDIT.`)
require.Contains(t, content, `package demo`)
require.Contains(t, content, `impl "forge.lthn.ai/core/cli/demo"`)
require.Contains(t, content, `type _ = impl.Example`)
require.Contains(t, content, `const _ = impl.Answer`)
require.Contains(t, content, `var _ = impl.Value`)
require.Contains(t, content, `var _ = impl.Run`)
}
func TestGeneratePublicAPITestFile_Good(t *testing.T) {
tmpDir := t.TempDir()
require.NoError(t, generatePublicAPITestFile(
filepath.Join(tmpDir, "demo"),
filepath.Join(tmpDir, "demo", "demo_test.go"),
"demo",
[]symbolInfo{
{Name: "Example", Kind: "type"},
{Name: "Answer", Kind: "const"},
},
))
content, err := io.Local.Read(filepath.Join(tmpDir, "demo", "demo_test.go"))
require.NoError(t, err)
require.True(t, strings.Contains(content, `type _ = impl.Example`))
require.True(t, strings.Contains(content, `const _ = impl.Answer`))
}

View file

@ -19,6 +19,7 @@
//
// API Tools:
// - api sync: Synchronize public service APIs
// - api test-gen: Generate compile-time API test stubs
//
// Dev Environment (VM management):
// - install: Download dev environment image
@ -33,8 +34,8 @@
package dev
import (
"forge.lthn.ai/core/cli/pkg/cli"
"dappco.re/go/core/i18n"
"forge.lthn.ai/core/cli/pkg/cli"
_ "dappco.re/go/core/devops/locales"
)

View file

@ -8,7 +8,11 @@
"short": "Multi-repo development workflows",
"long": "Development workflow commands for managing multiple repositories.\n\nIncludes git operations, forge integration, CI status, and dev environment management.",
"api": {
"short": "API synchronisation tools"
"short": "API synchronisation tools",
"test_gen": {
"short": "Generate public API test stubs",
"long": "Scan internal service packages and generate compile-time tests for their public API wrappers."
}
},
"health": {
"short": "Quick health check across all repos",