feat(04-01): generate model, migration, and command artifacts

Add make:model, make:migration and make:command with plugin-id
inference, duplicate rejection, and a deterministic registry refresh
that leaves handwritten plugin.go untouched.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jakub Zych
2026-09-18 13:39:03 +02:00
parent 1edf9d7e4c
commit d8c56509f2
8 changed files with 1050 additions and 4 deletions

View File

@@ -410,6 +410,153 @@ func TestScaffoldPluginSmoke(t *testing.T) {
}
}
func TestScaffoldCoreArtifacts(t *testing.T) {
dir := copyHelloApp(t)
ctx := t.Context()
pluginDir, err := MakePlugin(ctx, dir, "golem15.demo")
if err != nil {
t.Fatal(err)
}
pluginBefore, err := os.ReadFile(filepath.Join(pluginDir, "plugin.go"))
if err != nil {
t.Fatal(err)
}
model, err := MakeModel(ctx, dir, "golem15.demo", "Album", false)
if err != nil {
t.Fatal(err)
}
if model.Hint != "" {
t.Fatalf("new plugin printed accessor hint: %s", model.Hint)
}
if len(model.Files) != 2 {
t.Fatalf("make:model files = %v", model.Files)
}
albumSrc, err := os.ReadFile(filepath.Join(pluginDir, "models", "album.go"))
if err != nil {
t.Fatal(err)
}
for _, want := range []string{
"type Album struct",
"CreatedAt",
"UpdatedAt",
`return "golem15_demo_albums"`,
} {
if !bytes.Contains(albumSrc, []byte(want)) {
t.Fatalf("album.go missing %s:\n%s", want, albumSrc)
}
}
createAlbum, err := filepath.Glob(filepath.Join(pluginDir, "updates", "*_create_golem15_demo_albums.go"))
if err != nil {
t.Fatal(err)
}
if len(createAlbum) != 1 {
t.Fatalf("create-table migration files = %v", createAlbum)
}
migSrc, err := os.ReadFile(createAlbum[0])
if err != nil {
t.Fatal(err)
}
for _, want := range []string{
"func CreateAlbums()",
"CREATE TABLE golem15_demo_albums",
"DROP TABLE IF EXISTS golem15_demo_albums",
} {
if !bytes.Contains(migSrc, []byte(want)) {
t.Fatalf("model migration missing %s:\n%s", want, migSrc)
}
}
widget, err := MakeModel(ctx, pluginDir, "", "Widget", true)
if err != nil {
t.Fatal(err)
}
if len(widget.Files) != 1 || !strings.HasSuffix(widget.Files[0], filepath.Join("models", "widget.go")) {
t.Fatalf("--no-migration files = %v", widget.Files)
}
widgetMigs, err := filepath.Glob(filepath.Join(pluginDir, "updates", "*widget*"))
if err != nil {
t.Fatal(err)
}
if len(widgetMigs) != 0 {
t.Fatalf("--no-migration wrote %v", widgetMigs)
}
if _, err := MakeMigration(ctx, dir, "golem15.demo", "AddWidgetIndex"); err != nil {
t.Fatal(err)
}
if _, err := MakeCommand(ctx, dir, "golem15.demo", "Purge"); err != nil {
t.Fatal(err)
}
cmdSrc, err := os.ReadFile(filepath.Join(pluginDir, "console", "purge.go"))
if err != nil {
t.Fatal(err)
}
if !bytes.Contains(cmdSrc, []byte(`"demo:purge"`)) {
t.Fatalf("command name missing:\n%s", cmdSrc)
}
if _, err := MakeModel(ctx, dir, "golem15.demo", "Album", false); err == nil {
t.Fatal("duplicate make:model want error")
}
reg1, err := os.ReadFile(filepath.Join(pluginDir, "registry.gen.go"))
if err != nil {
t.Fatal(err)
}
if _, err := MakeCommand(ctx, dir, "golem15.demo", "Purge"); err == nil {
t.Fatal("duplicate make:command want error")
}
if err := refreshRegistry(pluginDir); err != nil {
t.Fatal(err)
}
reg2, err := os.ReadFile(filepath.Join(pluginDir, "registry.gen.go"))
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(reg1, reg2) {
t.Fatal("repeated generation changed registry.gen.go")
}
for _, want := range []string{
"&models.Album{}",
"&models.Widget{}",
"updates.CreateAlbums()",
"updates.AddWidgetIndex()",
"console.PurgeCommand()",
} {
if !bytes.Contains(reg1, []byte(want)) {
t.Fatalf("registry.gen.go missing %s:\n%s", want, reg1)
}
}
pluginAfter, err := os.ReadFile(filepath.Join(pluginDir, "plugin.go"))
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(pluginBefore, pluginAfter) {
t.Fatal("handwritten plugin.go was rewritten")
}
greeter, err := MakeCommand(ctx, filepath.Join(dir, "plugins", "greeter"), "", "Wave")
if err != nil {
t.Fatal(err)
}
if greeter.Hint == "" || !strings.Contains(greeter.Hint, "generatedCommands()") {
t.Fatalf("want accessor hint for handwritten plugin, got %q", greeter.Hint)
}
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)
}
}
func assertScaffoldFiles(t *testing.T, pluginDir string) {
t.Helper()
assertWinterPluginShape(t, pluginDir)