feat(04-01): scaffold Winter-shaped plugins with generated registry

Render plugin.go, routes.go, leaf packages and go.mod from embedded
templates, emit empty registry accessors, and define job, admin, lang
and mail capability contracts so a new plugin compiles before artifacts.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jakub Zych
2026-09-18 13:34:32 +02:00
parent 846e6d5b74
commit 1edf9d7e4c
6 changed files with 552 additions and 63 deletions

View File

@@ -337,17 +337,83 @@ func TestParseManifestRejectsUppercaseID(t *testing.T) {
}
}
func TestScaffoldPluginSmoke(t *testing.T) {
dir := copyHelloApp(t)
ctx := t.Context()
pluginDir, err := MakePlugin(ctx, dir, "golem15.demo")
if err != nil {
t.Fatal(err)
}
assertScaffoldFiles(t, pluginDir)
pluginSrc, err := os.ReadFile(filepath.Join(pluginDir, "plugin.go"))
if err != nil {
t.Fatal(err)
}
regSrc, err := os.ReadFile(filepath.Join(pluginDir, "registry.gen.go"))
if err != nil {
t.Fatal(err)
}
for _, want := range []string{
"generatedModels()",
"generatedMigrations()",
"generatedCommands()",
"generatedJobs()",
"generatedAdminControllers()",
"//go:embed config",
"//go:embed lang",
"//go:embed views/mail",
} {
if !bytes.Contains(pluginSrc, []byte(want)) {
t.Fatalf("plugin.go missing %s:\n%s", want, pluginSrc)
}
}
for _, want := range []string{
"func generatedModels()",
"func generatedMigrations()",
"func generatedCommands()",
"func generatedJobs()",
"func generatedAdminControllers()",
} {
if !bytes.Contains(regSrc, []byte(want)) {
t.Fatalf("registry.gen.go missing %s:\n%s", want, regSrc)
}
}
if err := AddPlugin(ctx, dir, filepath.Join("plugins", "demo")); err != nil {
t.Fatal(err)
}
var buf bytes.Buffer
if err := App(ctx, dir, &buf); err != nil {
t.Fatalf("app build: %v\n%s", err, buf.String())
}
vet := execGo(t, pluginDir, "vet", "./...")
if out, err := vet.CombinedOutput(); err != nil {
t.Fatalf("plugin go vet: %v\n%s", err, out)
}
pluginAfter, err := os.ReadFile(filepath.Join(pluginDir, "plugin.go"))
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(pluginSrc, pluginAfter) {
t.Fatal("hand-written plugin.go was rewritten")
}
regAfter, err := os.ReadFile(filepath.Join(pluginDir, "registry.gen.go"))
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(regSrc, regAfter) {
t.Fatal("registry.gen.go is not byte-stable after add/build")
}
}
func assertScaffoldFiles(t *testing.T, pluginDir string) {
t.Helper()
if _, err := os.Stat(filepath.Join(pluginDir, "go.mod")); err != nil {
t.Fatal(err)
}
if _, err := os.Stat(filepath.Join(pluginDir, "plugin.go")); err != nil {
t.Fatal(err)
}
if _, err := os.Stat(filepath.Join(pluginDir, "config")); err != nil {
t.Fatal(err)
}
assertWinterPluginShape(t, pluginDir)
body, err := os.ReadFile(filepath.Join(pluginDir, "plugin.go"))
if err != nil {
t.Fatal(err)
@@ -375,6 +441,36 @@ func assertScaffoldFiles(t *testing.T, pluginDir string) {
t.Fatalf("plugin go.mod missing module path:\n%s", mod)
}
cmd := execGo(t, pluginDir, "build", "./...")
if out, err := cmd.CombinedOutput(); err != nil {
t.Fatalf("plugin does not compile: %v\n%s", err, out)
}
}
func assertWinterPluginShape(t *testing.T, pluginDir string) {
t.Helper()
required := []string{
"go.mod",
"plugin.go",
"routes.go",
"registry.gen.go",
filepath.Join("config", "config.yaml"),
filepath.Join("lang", "en", "lang.yaml"),
filepath.Join("views", "mail", "welcome.htm"),
}
for _, leaf := range pluginLeaves {
required = append(required, filepath.Join(leaf, "doc.go"))
}
allowed := map[string]struct{}{
"go.sum": {},
}
for _, rel := range required {
allowed[rel] = struct{}{}
if _, err := os.Stat(filepath.Join(pluginDir, rel)); err != nil {
t.Fatalf("missing %s: %v", rel, err)
}
}
var extra []string
if err := filepath.WalkDir(pluginDir, func(path string, d os.DirEntry, err error) error {
if err != nil {
@@ -387,13 +483,15 @@ func assertScaffoldFiles(t *testing.T, pluginDir string) {
if err != nil {
return err
}
switch rel {
case "go.mod", "go.sum", "plugin.go", filepath.Join("config", ".gitkeep"):
if _, ok := allowed[rel]; ok {
return nil
default:
}
if strings.HasPrefix(filepath.Base(rel), "registry.gen.go.") {
extra = append(extra, rel)
return nil
}
extra = append(extra, rel)
return nil
}); err != nil {
t.Fatal(err)
}
@@ -401,9 +499,14 @@ func assertScaffoldFiles(t *testing.T, pluginDir string) {
t.Fatalf("unexpected scaffold files: %v", extra)
}
cmd := execGo(t, pluginDir, "build", "-o", filepath.Join(pluginDir, "demo.bin"), ".")
if out, err := cmd.CombinedOutput(); err != nil {
t.Fatalf("plugin does not compile: %v\n%s", err, out)
for _, asset := range []string{
filepath.Join("config", "config.yaml"),
filepath.Join("lang", "en", "lang.yaml"),
filepath.Join("views", "mail", "welcome.htm"),
} {
if strings.HasPrefix(filepath.Base(asset), ".") || strings.HasPrefix(filepath.Base(asset), "_") {
t.Fatalf("embed-incompatible hidden asset %s", asset)
}
}
}