diff --git a/docs/contracts.md b/docs/contracts.md new file mode 100644 index 0000000..4258b8c --- /dev/null +++ b/docs/contracts.md @@ -0,0 +1,80 @@ +# Machine Contracts + +Lathe's durable value is a small set of versioned, machine-readable contracts. +Generated CLIs, agents, and sibling tooling (installers, verifiers, registries) +integrate through these contracts, not through Lathe internals. Any new tool or +capability should answer one question first: which contract does it consume or +produce, and how is that contract versioned? + +Code is the source of truth. Each section below points at the defining source +file; when this page and the code disagree, trust the code and fix this page. + +## Generated code schema + +- Version constant: `runtime.SchemaVersion` in `pkg/runtime/spec.go`. +- Couples generated `[]runtime.CommandSpec` literals + (`internal/generated//_gen.go` in downstream repos) to the + runtime that executes them. +- Enforced by `runtime.AssertSchema` at mount time: a mismatch fails fast at + startup with an instruction to re-run codegen. +- Bump the constant whenever `CommandSpec` semantics or the generated mount + contract change in a way that requires regeneration. + +## Command catalog + +- Version field: `catalog_schema_version`, constant + `runtime.CatalogSchemaVersion` in `pkg/runtime/catalog.go`. +- Served by ` commands --json`, `commands show --json`, + `search "" --json`, and `commands schema --json`. +- This is the agent-facing discovery contract and the source of truth for + generated operation details: HTTP method and path template, auth + requirements, flags, body schema, output and pagination hints. +- Only generated API operation commands carry catalog entries. Framework + commands (`auth`, `commands`, `search`, `update`, `__lathe`) are discovered + through `--help`, not the catalog. +- Consumers: agents following the documented loop (search, then + `commands show`, then `auth status`, then execute), generated Skill files + (guidance and indexes only, never execution authority), and external + conformance tooling. + +## Verify report + +- Emitted by ` __lathe verify --json`; implemented in + `pkg/lathe/verify.go`. +- Shape: `{"ok": bool, "checks": [{"name": string, "ok": bool, "error": + string}]}`. The process exits non-zero when any check fails. +- This is the generated CLI's self-evidence: root help contract, catalog + schema and JSON round-trip, per-command flag consistency, and an isolated + unauthenticated `auth status` probe. +- The report is not independently versioned today; treat any change to its + shape or check names as a contract change and document it here. + +## Structured errors and exit codes + +- Defined in `pkg/runtime/errors.go`. +- Error codes: `general`, `usage`, `api_error`, `not_authenticated`. +- Exit codes: `0` OK, `1` general, `2` usage, `3` API error, + `4` not authenticated. +- Consumers: agents and scripts that branch on failure classes instead of + parsing prose. + +## Durable inputs + +- `cli.yaml`, parsed by `pkg/config.Load`, plus codegen-only keys + (`skill.root`, `skill.include`) parsed in `internal/lathecmd`. +- `specs/sources.yaml`, parsed by `internal/sourceconfig`, pinning upstream + spec refs per module. +- Optional overlay files, parsed by `internal/overlay`, merged at codegen time + only. +- Together these are the reproducibility contract: generated output must be + reproducible from these pinned inputs. Overlay concepts never reach the + runtime. + +## Rules + +- The runtime catalog is authoritative for operation details; generated Skill + files are guidance. +- Search results are discovery only; agents must inspect a command via + `commands show` before executing it. +- A change to any surface above is product behavior: prove it with focused + tests and treat the version fields as the compatibility gate. diff --git a/internal/codegen/app/app.go b/internal/codegen/app/app.go new file mode 100644 index 0000000..827d5d5 --- /dev/null +++ b/internal/codegen/app/app.go @@ -0,0 +1,65 @@ +// Package app models the generated CLI application that one codegen run is +// about to emit. Outputs are collected into an App first and written together, +// so a failing input never leaves partially generated output behind. The model +// is internal to codegen; it is not a stable extension API. +package app + +import ( + "github.com/lathe-cli/lathe/internal/codegen/render" + "github.com/lathe-cli/lathe/pkg/config" + "github.com/lathe-cli/lathe/pkg/runtime" +) + +// App is the complete set of generated outputs for one codegen run. +type App struct { + Manifest *config.Manifest + Modules []Module + Skill *Skill +} + +// Module is one generated command module and how it mounts on the root command. +type Module struct { + Source string + CLIName string + Flat bool + Specs []runtime.CommandSpec +} + +// Skill is the optional generated Skill directory output. +type Skill struct { + Dir string + Include render.SkillInclude + Modules []render.SkillModule +} + +// Validate rejects app compositions that would produce a conflicting root +// command tree. Flat modules are skipped: their module name is never mounted, +// and flat root conflicts are rejected by ResolveFlatCommandPath. +func (a *App) Validate() error { + names := make([]string, 0, len(a.Modules)) + for _, m := range a.Modules { + if m.Flat { + continue + } + names = append(names, m.CLIName) + } + return render.ValidateModuleNames(names) +} + +// Write renders every collected output. +func (a *App) Write() error { + mounts := make([]render.ModuleMount, 0, len(a.Modules)) + for _, m := range a.Modules { + if err := render.RenderModule(m.Source, m.CLIName, m.Specs, nil); err != nil { + return err + } + mounts = append(mounts, render.ModuleMount{Name: m.Source, Flat: m.Flat}) + } + if err := render.RenderModulesGen(mounts); err != nil { + return err + } + if a.Skill == nil { + return nil + } + return render.RenderSkillDirectoryWithInclude(a.Skill.Dir, a.Manifest, a.Skill.Modules, a.Skill.Include) +} diff --git a/internal/codegen/render/render.go b/internal/codegen/render/render.go index c4a9e0d..ea666c7 100644 --- a/internal/codegen/render/render.go +++ b/internal/codegen/render/render.go @@ -188,10 +188,27 @@ var reservedRootCommands = map[string]bool{ "auth": true, "commands": true, "help": true, + "login": true, "search": true, "update": true, } +// ValidateModuleNames rejects namespaced module mount names that would shadow +// a reserved root command or another module on the generated root command. +func ValidateModuleNames(names []string) error { + seen := map[string]bool{} + for _, name := range names { + if reservedRootCommands[name] { + return fmt.Errorf("module name %q conflicts with a reserved root command", name) + } + if seen[name] { + return fmt.Errorf("module name %q is mounted more than once", name) + } + seen[name] = true + } + return nil +} + func MergeOverlay(specs []runtime.CommandSpec, overrides map[string]overlay.Override) []runtime.CommandSpec { return MergeOverlayModule(specs, overlay.Module{Commands: overrides}) } @@ -537,8 +554,7 @@ func Mount(root *cobra.Command) error { if err := runtime.AssertSchema(generatedSchemaVersion); err != nil { return err } - runtime.Build(root, {{printf "%q" .CLIName}}, Specs) - return nil + return runtime.Build(root, {{printf "%q" .CLIName}}, Specs) } func MountFlat(root *cobra.Command) error { diff --git a/internal/codegen/render/render_test.go b/internal/codegen/render/render_test.go index 0967259..38809e4 100644 --- a/internal/codegen/render/render_test.go +++ b/internal/codegen/render/render_test.go @@ -102,6 +102,7 @@ func TestRenderModule_AppliesOverlay(t *testing.T) { `func Mount(root *cobra.Command) error`, `if err := runtime.AssertSchema(generatedSchemaVersion); err != nil`, `return err`, + `return runtime.Build(root, "demo", Specs)`, `Schema:`, `&runtime.SchemaSpec{`, `Properties: map[string]*runtime.SchemaSpec`, @@ -595,3 +596,19 @@ func TestRewriteCommandExamples_NormalizesMultiWordGroupPaths(t *testing.T) { t.Fatalf("namespaced structured example = %q", got[0].Examples[0].Command) } } + +func TestValidateModuleNames(t *testing.T) { + if err := ValidateModuleNames([]string{"pets", "billing"}); err != nil { + t.Fatalf("distinct module names should pass: %v", err) + } + for _, reserved := range []string{"__lathe", "auth", "commands", "help", "login", "search", "update"} { + err := ValidateModuleNames([]string{"pets", reserved}) + if err == nil || !strings.Contains(err.Error(), "reserved root command") { + t.Fatalf("module name %q should be rejected, got %v", reserved, err) + } + } + err := ValidateModuleNames([]string{"pets", "pets"}) + if err == nil || !strings.Contains(err.Error(), "mounted more than once") { + t.Fatalf("duplicate module names should be rejected, got %v", err) + } +} diff --git a/internal/lathecmd/lathecmd.go b/internal/lathecmd/lathecmd.go index 362a4d3..a1addf4 100644 --- a/internal/lathecmd/lathecmd.go +++ b/internal/lathecmd/lathecmd.go @@ -7,6 +7,7 @@ import ( "os" "path/filepath" + "github.com/lathe-cli/lathe/internal/codegen/app" "github.com/lathe-cli/lathe/internal/codegen/backends/graphql" "github.com/lathe-cli/lathe/internal/codegen/backends/openapi3" "github.com/lathe-cli/lathe/internal/codegen/backends/proto" @@ -163,6 +164,24 @@ func runCodegen(sourcesPath string, manifestPath string, cacheRoot string, overl return err } + generated, err := buildGeneratedApp(cfg, overlays, syncRoot, manifest, skillDir, skillInclude) + if err != nil { + return err + } + if err := generated.Validate(); err != nil { + return err + } + return generated.Write() +} + +// buildGeneratedApp parses and normalizes every configured source into the +// generated app model without writing any output. +func buildGeneratedApp(cfg *sourceconfig.Config, overlays map[string]overlay.Module, syncRoot string, manifest *config.Manifest, skillDir string, skillInclude render.SkillInclude) (*app.App, error) { + generated := &app.App{Manifest: manifest} + if skillDir != "" { + generated.Skill = &app.Skill{Dir: skillDir, Include: skillInclude} + } + ordered := cfg.Ordered() moduleNames := make([]string, 0, len(ordered)) for _, src := range ordered { @@ -172,22 +191,20 @@ func runCodegen(sourcesPath string, manifestPath string, cacheRoot string, overl } moduleNames = append(moduleNames, name) } - var mounts []render.ModuleMount - var skillModules []render.SkillModule var shortcutRootNames []string for i, src := range ordered { syncDir := filepath.Join(syncRoot, src.Name) if err := specsync.VerifyState(syncDir, src); err != nil { - return err + return nil, err } state, err := specsync.LoadState(syncDir) if err != nil { - return err + return nil, err } mod, err := parseSource(src, syncDir) if err != nil { - return err + return nil, err } specs := normalize.Normalize(mod) @@ -200,11 +217,11 @@ func runCodegen(sourcesPath string, manifestPath string, cacheRoot string, overl cliName := moduleNames[i] flat, err := render.ResolveFlatCommandPath(manifest.CLI.CommandPath, len(ordered), specs) if err != nil { - return err + return nil, err } validateRootNames := append(append([]string(nil), moduleNames...), shortcutRootNames...) if err := render.ValidateShortcuts(validateRootNames, specs, flat); err != nil { - return err + return nil, err } for _, spec := range specs { for _, shortcut := range spec.Shortcuts { @@ -212,23 +229,12 @@ func runCodegen(sourcesPath string, manifestPath string, cacheRoot string, overl } } specs = render.RewriteCommandExamples(manifest.CLI.Name, cliName, specs, flat) - if err := render.RenderModule(src.Name, cliName, specs, nil); err != nil { - return err - } - mounts = append(mounts, render.ModuleMount{Name: src.Name, Flat: flat}) - if skillDir != "" { - skillModules = append(skillModules, render.SkillModule{Source: src, State: state, Specs: specs}) - } - } - if err := render.RenderModulesGen(mounts); err != nil { - return err - } - if skillDir != "" { - if err := render.RenderSkillDirectoryWithInclude(skillDir, manifest, skillModules, skillInclude); err != nil { - return err + generated.Modules = append(generated.Modules, app.Module{Source: src.Name, CLIName: cliName, Flat: flat, Specs: specs}) + if generated.Skill != nil { + generated.Skill.Modules = append(generated.Skill.Modules, render.SkillModule{Source: src, State: state, Specs: specs}) } } - return nil + return generated, nil } func resolveSkillOutput(manifestPath string, flags skillFlagOptions) (*config.Manifest, string, render.SkillInclude, error) { diff --git a/internal/lathecmd/lathecmd_test.go b/internal/lathecmd/lathecmd_test.go index 476cf0f..5af98d7 100644 --- a/internal/lathecmd/lathecmd_test.go +++ b/internal/lathecmd/lathecmd_test.go @@ -479,6 +479,45 @@ paths: } } +func TestRunCodegen_RejectsReservedModuleNameBeforeWriting(t *testing.T) { + dir := t.TempDir() + t.Chdir(dir) + writeCodegenFile(t, "go.mod", "module example.com/fake\n\ngo 1.25\n") + writeCodegenFile(t, "cli.yaml", "cli:\n name: acmectl\n short: Acme CLI\n command_path: namespaced\n") + writeCodegenFile(t, "specs/sources.yaml", `sources: + auth: + repo_url: https://example.com/acme.git + pinned_tag: v1.0.0 + backend: openapi3 + openapi3: + files: [openapi.yaml] +`) + writeCodegenFile(t, ".cache/specs-sync/auth/sync-state.yaml", `source: auth +backend: openapi3 +synced_from: v1.0.0 +resolved_sha: "0000000000000000000000000000000000000000" +`) + writeCodegenFile(t, ".cache/specs-sync/auth/openapi.yaml", `openapi: "3.0.3" +paths: + /users: + get: + operationId: Users_List + tags: [Users] + summary: List users + responses: + "200": + description: OK +`) + + err := RunCodegen([]string{"-sources", "specs/sources.yaml", "-cache", ".cache"}, &bytes.Buffer{}) + if err == nil || !strings.Contains(err.Error(), "reserved root command") { + t.Fatalf("expected reserved module name error, got %v", err) + } + if _, err := os.Stat("internal/generated"); !os.IsNotExist(err) { + t.Fatalf("codegen should fail before writing generated code, stat err = %v", err) + } +} + func TestRunCodegen_MissingManifestFailsWhenSkillEnabled(t *testing.T) { dir := t.TempDir() t.Chdir(dir) diff --git a/pkg/lathe/catalog_test.go b/pkg/lathe/catalog_test.go index d3f6c3e..f142ea8 100644 --- a/pkg/lathe/catalog_test.go +++ b/pkg/lathe/catalog_test.go @@ -12,6 +12,13 @@ import ( "github.com/lathe-cli/lathe/pkg/runtime" ) +func mustBuild(t *testing.T, root *cobra.Command, service string, specs []runtime.CommandSpec) { + t.Helper() + if err := runtime.Build(root, service, specs); err != nil { + t.Fatalf("Build(%q): %v", service, err) + } +} + func TestCommandsJSON_EmptyCatalog(t *testing.T) { root := NewApp(testManifest()) out, err := execute(root, "commands", "--json") @@ -32,7 +39,7 @@ func TestCommandsJSON_EmptyCatalog(t *testing.T) { func TestCommandsShowAndSearchJSON(t *testing.T) { root := NewApp(testManifest()) - runtime.Build(root, "demo", []runtime.CommandSpec{{ + mustBuild(t, root, "demo", []runtime.CommandSpec{{ Group: "Users", Use: "get-user", Short: "Get a user", @@ -96,7 +103,7 @@ func TestCommandsShowAndSearchJSON(t *testing.T) { func TestCommandsShow_EnvelopeBody(t *testing.T) { root := NewApp(testManifest()) const tmpl = `{"query":"mutation CreateApp($name:String!){createApp(name:$name){id}}","variables":{}}` - runtime.Build(root, "demo", []runtime.CommandSpec{{ + mustBuild(t, root, "demo", []runtime.CommandSpec{{ Group: "Apps", Use: "create-app", Short: "Create an app", @@ -130,7 +137,7 @@ func TestCommandsShow_EnvelopeBody(t *testing.T) { func TestCommandsShow_EnvelopeVariableFlag(t *testing.T) { root := NewApp(testManifest()) const tmpl = `{"query":"mutation createApp($name: String!){createApp(name:$name){id}}","variables":{}}` - runtime.Build(root, "demo", []runtime.CommandSpec{{ + mustBuild(t, root, "demo", []runtime.CommandSpec{{ Group: "Apps", Use: "create-app", Short: "Create an app", @@ -184,7 +191,7 @@ func TestCommandsSchemaJSON(t *testing.T) { func TestSearchExcludesHiddenCommands(t *testing.T) { root := NewApp(testManifest()) - runtime.Build(root, "demo", []runtime.CommandSpec{{ + mustBuild(t, root, "demo", []runtime.CommandSpec{{ Group: "Users", Use: "delete-user", Short: "Delete a user", diff --git a/pkg/lathe/verify_test.go b/pkg/lathe/verify_test.go index 96c9731..8a4e67e 100644 --- a/pkg/lathe/verify_test.go +++ b/pkg/lathe/verify_test.go @@ -16,7 +16,7 @@ func TestRunVerifyGeneratedJSON(t *testing.T) { code := run(RunOptions{ Manifest: []byte("cli:\n name: myctl\n short: test cli\n"), Mount: func(root *cobra.Command) error { - runtime.Build(root, "demo", []runtime.CommandSpec{ + if err := runtime.Build(root, "demo", []runtime.CommandSpec{ { Group: "Users", Use: "get-user", @@ -39,7 +39,9 @@ func TestRunVerifyGeneratedJSON(t *testing.T) { PathTpl: "/users", RequestBody: &runtime.RequestBody{Required: true, MediaType: "application/json"}, }, - }) + }); err != nil { + return err + } skills := &cobra.Command{Use: "skills"} pkg := &cobra.Command{Use: "package"} pkg.Flags().String("file", "", "") diff --git a/pkg/runtime/build.go b/pkg/runtime/build.go index 1f94141..1869577 100644 --- a/pkg/runtime/build.go +++ b/pkg/runtime/build.go @@ -30,18 +30,19 @@ func AssertSchema(generated int) error { // Build mounts a service command tree under root, driven entirely by specs. // Replaces the previous per-operation function approach: every operation is // data, the execution path is a single function. -func Build(root *cobra.Command, service string, specs []CommandSpec) { +func Build(root *cobra.Command, service string, specs []CommandSpec) error { + if findChildCommand(root, service) != nil { + return fmt.Errorf("module command %q conflicts with an existing root command", service) + } svc := &cobra.Command{Use: service, Short: service + " API", GroupID: ModuleGroupID} for _, group := range buildGroups(service, specs) { svc.AddCommand(group) } if err := ValidateShortcuts(specs, rootCommandNames(root, svc)); err != nil { - panic(err) + return err } root.AddCommand(svc) - if err := mountShortcuts(root, specs); err != nil { - panic(err) - } + return mountShortcuts(root, specs) } func BuildFlat(root *cobra.Command, service string, specs []CommandSpec) error { diff --git a/pkg/runtime/build_test.go b/pkg/runtime/build_test.go index e98bd54..85b06f8 100644 --- a/pkg/runtime/build_test.go +++ b/pkg/runtime/build_test.go @@ -20,6 +20,26 @@ func newRootWithModuleGroup() *cobra.Command { return root } +func mustBuild(t *testing.T, root *cobra.Command, service string, specs []CommandSpec) { + t.Helper() + if err := Build(root, service, specs); err != nil { + t.Fatalf("Build(%q): %v", service, err) + } +} + +func TestBuild_RejectsExistingRootCommandConflict(t *testing.T) { + root := newRootWithModuleGroup() + root.AddCommand(&cobra.Command{Use: "auth"}) + + err := Build(root, "auth", nil) + if err == nil || !strings.Contains(err.Error(), `module command "auth" conflicts`) { + t.Fatalf("expected root conflict error, got %v", err) + } + if len(root.Commands()) != 1 { + t.Fatalf("conflicting module must not be mounted; root commands = %v", cmdNames(root.Commands())) + } +} + func TestBuild_PopulatesGroupAndOpTree(t *testing.T) { specs := []CommandSpec{ { @@ -46,7 +66,7 @@ func TestBuild_PopulatesGroupAndOpTree(t *testing.T) { } root := newRootWithModuleGroup() - Build(root, "demo", specs) + mustBuild(t, root, "demo", specs) svc := mustFindChild(t, root, "demo") usersGroup := mustFindChild(t, svc, "users") @@ -97,7 +117,7 @@ func TestAssertSchema_Mismatch(t *testing.T) { func TestBuild_EmptySpecsMountsEmptyService(t *testing.T) { root := newRootWithModuleGroup() - Build(root, "demo", nil) + mustBuild(t, root, "demo", nil) svc := mustFindChild(t, root, "demo") if len(svc.Commands()) != 0 { @@ -170,7 +190,7 @@ func TestBuild_BodyFlagsAttachedWhenHasBody(t *testing.T) { }} root := newRootWithModuleGroup() - Build(root, "demo", specs) + mustBuild(t, root, "demo", specs) svc := mustFindChild(t, root, "demo") users := mustFindChild(t, svc, "users") @@ -211,7 +231,7 @@ func TestBuild_SetStrSendsStringBodyFields(t *testing.T) { root := newRootWithModuleGroup() root.PersistentFlags().String("hostname", "", "") root.PersistentFlags().StringP("output", "o", "raw", "") - Build(root, "demo", specs) + mustBuild(t, root, "demo", specs) root.SetArgs([]string{ "--hostname", srv.URL, "demo", "users", "create-user", @@ -264,7 +284,7 @@ func TestBuild_FileSendsRequestBodyMediaType(t *testing.T) { root := newRootWithModuleGroup() root.PersistentFlags().String("hostname", "", "") root.PersistentFlags().StringP("output", "o", "raw", "") - Build(root, "demo", []CommandSpec{{ + mustBuild(t, root, "demo", []CommandSpec{{ Group: "Exports", Use: "create-export", Method: "POST", @@ -293,7 +313,7 @@ func TestBuild_NonJSONRequestBodyRequiresFile(t *testing.T) { root := newRootWithModuleGroup() root.PersistentFlags().String("hostname", "", "") root.PersistentFlags().StringP("output", "o", "raw", "") - Build(root, "demo", []CommandSpec{{ + mustBuild(t, root, "demo", []CommandSpec{{ Group: "Exports", Use: "create-export", Method: "POST", @@ -327,7 +347,7 @@ func TestBuild_DryRunPrintsResolvedRequestWithoutSending(t *testing.T) { root.SetErr(io.Discard) root.PersistentFlags().String("hostname", "", "") root.PersistentFlags().StringP("output", "o", "raw", "") - Build(root, "demo", []CommandSpec{{ + mustBuild(t, root, "demo", []CommandSpec{{ Group: "Users", Use: "create-user", Method: "POST", @@ -598,7 +618,7 @@ func newRecordingGraphQLRoot(t *testing.T, spec CommandSpec) (*cobra.Command, st root := newRootWithModuleGroup() root.PersistentFlags().String("hostname", "", "") root.PersistentFlags().StringP("output", "o", "raw", "") - Build(root, "demo", []CommandSpec{spec}) + mustBuild(t, root, "demo", []CommandSpec{spec}) return root, srv.URL, func() ([]byte, bool) { return rawBody, called } } @@ -634,7 +654,7 @@ func TestBuild_FloatVariableSentAsJSONNumber(t *testing.T) { root := newRootWithModuleGroup() root.PersistentFlags().String("hostname", "", "") root.PersistentFlags().StringP("output", "o", "raw", "") - Build(root, "demo", specs) + mustBuild(t, root, "demo", specs) root.SetArgs([]string{"--hostname", srv.URL, "demo", "apps", "set-weight", "--weight", "1.5"}) if err := root.Execute(); err != nil { t.Fatalf("Execute: %v", err) @@ -682,7 +702,7 @@ func TestBuild_IntListVariableSentAsJSONNumbers(t *testing.T) { root := newRootWithModuleGroup() root.PersistentFlags().String("hostname", "", "") root.PersistentFlags().StringP("output", "o", "raw", "") - Build(root, "demo", specs) + mustBuild(t, root, "demo", specs) root.SetArgs([]string{"--hostname", srv.URL, "demo", "apps", "set-ids", "--ids", "1", "--ids", "2"}) if err := root.Execute(); err != nil { t.Fatalf("Execute: %v", err) @@ -725,7 +745,7 @@ func TestBuild_RequiredQueryParamBlocksBeforeRequest(t *testing.T) { root := newRootWithModuleGroup() root.PersistentFlags().String("hostname", "", "") root.PersistentFlags().StringP("output", "o", "raw", "") - Build(root, "demo", specs) + mustBuild(t, root, "demo", specs) root.SetArgs([]string{"--hostname", srv.URL, "demo", "receivers", "get-receiver"}) err := root.Execute() @@ -752,7 +772,7 @@ func TestBuild_PaginationFlagsAttached(t *testing.T) { }} root := newRootWithModuleGroup() - Build(root, "demo", specs) + mustBuild(t, root, "demo", specs) svc := mustFindChild(t, root, "demo") items := mustFindChild(t, svc, "items") @@ -772,7 +792,7 @@ func TestBuild_WaitFlagOnMutating(t *testing.T) { } root := newRootWithModuleGroup() - Build(root, "demo", specs) + mustBuild(t, root, "demo", specs) svc := mustFindChild(t, root, "demo") resources := mustFindChild(t, svc, "resources") diff --git a/pkg/runtime/catalog_test.go b/pkg/runtime/catalog_test.go index 507b445..9ac3fac 100644 --- a/pkg/runtime/catalog_test.go +++ b/pkg/runtime/catalog_test.go @@ -9,7 +9,7 @@ import ( func TestBuildCatalog_UsesAttachedSpec(t *testing.T) { root := newRootWithModuleGroup() - Build(root, "demo", []CommandSpec{ + mustBuild(t, root, "demo", []CommandSpec{ { Group: "Users", Use: "get-user", @@ -138,7 +138,7 @@ func TestBuildCatalog_UsesAttachedSpec(t *testing.T) { func TestBuildCatalog_ProjectsLegacyExample(t *testing.T) { root := newRootWithModuleGroup() - Build(root, "demo", []CommandSpec{{ + mustBuild(t, root, "demo", []CommandSpec{{ Group: "Users", Use: "get-user", Short: "Get a user", @@ -163,7 +163,7 @@ func TestBuildCatalog_ProjectsLegacyExample(t *testing.T) { func TestBuildCatalog_RequestBodyEnvelope(t *testing.T) { root := newRootWithModuleGroup() const tmpl = `{"query":"mutation CreateApp($name:String!){createApp(name:$name){id}}","variables":{}}` - Build(root, "demo", []CommandSpec{{ + mustBuild(t, root, "demo", []CommandSpec{{ Group: "Apps", Use: "create-app", Short: "Create an app", @@ -216,7 +216,7 @@ func TestBuildCatalog_RequestBodyEnvelope(t *testing.T) { func TestBuildCatalog_SensitiveFlagInputModes(t *testing.T) { root := newRootWithModuleGroup() - Build(root, "demo", []CommandSpec{{ + mustBuild(t, root, "demo", []CommandSpec{{ Group: "Credentials", Use: "create-credential", Short: "Create credential", @@ -244,7 +244,7 @@ func TestBuildCatalog_SensitiveFlagInputModes(t *testing.T) { func TestBuildCatalog_HiddenCommands(t *testing.T) { root := newRootWithModuleGroup() - Build(root, "demo", []CommandSpec{ + mustBuild(t, root, "demo", []CommandSpec{ {Group: "Users", Use: "get-user", Short: "Get a user", Method: "GET", PathTpl: "/users/{id}"}, {Group: "Users", Use: "delete-user", Short: "Delete a user", Method: "DELETE", PathTpl: "/users/{id}", Hidden: true}, }) @@ -261,7 +261,7 @@ func TestBuildCatalog_HiddenCommands(t *testing.T) { func TestFindAndSearchCatalog(t *testing.T) { root := newRootWithModuleGroup() - Build(root, "demo", []CommandSpec{ + mustBuild(t, root, "demo", []CommandSpec{ { Group: "Users", Use: "get-user", @@ -309,7 +309,7 @@ func TestFindAndSearchCatalog(t *testing.T) { func TestSearchCatalog_SoftMatchesNoisyIntent(t *testing.T) { root := newRootWithModuleGroup() - Build(root, "demo", []CommandSpec{ + mustBuild(t, root, "demo", []CommandSpec{ { Group: "Users", Use: "get-user", @@ -347,7 +347,7 @@ func TestSearchCatalog_SoftMatchesNoisyIntent(t *testing.T) { func TestBuildCatalog_DefaultAuthRequired(t *testing.T) { root := newRootWithModuleGroup() - Build(root, "demo", []CommandSpec{{ + mustBuild(t, root, "demo", []CommandSpec{{ Group: "Users", Use: "get-user", Short: "Get a user", diff --git a/pkg/runtime/spec.go b/pkg/runtime/spec.go index 0ef3369..5c2d1bb 100644 --- a/pkg/runtime/spec.go +++ b/pkg/runtime/spec.go @@ -2,7 +2,7 @@ package runtime import "encoding/json" -const SchemaVersion = 8 +const SchemaVersion = 9 type CommandSpec struct { Group string