Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 138 additions & 0 deletions builder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package codemode_test

import (
"context"
"encoding/json"
"math"
"os/exec"
"strings"
Expand Down Expand Up @@ -32,6 +33,44 @@ type invalidBuilderInput struct {
Count int32 `json:"count"`
}

// interfaceBuilderOutput is an unsupported interface/any nested output.
type interfaceBuilderOutput struct {
// Value is intentionally an unconstrained interface.
Value any `json:"value"`
}

// rawMessageBuilderOutput is an unsupported [json.RawMessage] nested output.
type rawMessageBuilderOutput struct {
// Value is intentionally opaque JSON.
Value json.RawMessage `json:"value"`
}

// cyclicBuilderOutput is an unsupported cyclic nested output.
type cyclicBuilderOutput struct {
// Next is a self-referential pointer that must fail registration.
Next *cyclicBuilderOutput `json:"next"`
}

// marshalerBuilderValue is a custom JSON marshaler used only as a rejected fixture.
type marshalerBuilderValue struct{}

// MarshalJSON exists so registration rejects custom marshalers.
func (marshalerBuilderValue) MarshalJSON() ([]byte, error) {
return []byte(`""`), nil
}

// marshalerBuilderOutput is an unsupported custom-marshaler nested output.
type marshalerBuilderOutput struct {
// Value is intentionally a custom JSON marshaler.
Value marshalerBuilderValue `json:"value"`
}

// mapKeyBuilderOutput is an unsupported non-string map key nested output.
type mapKeyBuilderOutput struct {
// Value is intentionally keyed by integers.
Value map[int]string `json:"value"`
}

// nilPolicy is a typed-nil authorization implementation used to test Build validation.
type nilPolicy struct{}

Expand Down Expand Up @@ -133,6 +172,105 @@ func TestRegisterRejectsInvalidContractsBeforeRetention(t *testing.T) {
)), codemode.ErrInvalidRegistration)
}

// TestRegisterRejectsUnsupportedNestedOutputsBeforeRetention proves representative
// unsupportable output graphs never enter the builder.
func TestRegisterRejectsUnsupportedNestedOutputsBeforeRetention(t *testing.T) {
tests := []struct {
// name identifies the unsupported nested output.
name string

// register attempts one invalid public registration.
register func(*codemode.Builder) error
}{
{
name: "interface any",
register: func(builder *codemode.Builder) error {
return codemode.Register(builder, codemode.Capability[builderInput, interfaceBuilderOutput]{
ID: "cap.interface",
Name: "records.interface",
Summary: "Unsupported interface output.",
Description: "Rejected before retention.",
Handler: func(context.Context, authz.Subject, builderInput) (interfaceBuilderOutput, error) {
return interfaceBuilderOutput{}, nil
},
})
},
},
{
name: "json.RawMessage",
register: func(builder *codemode.Builder) error {
return codemode.Register(builder, codemode.Capability[builderInput, rawMessageBuilderOutput]{
ID: "cap.raw",
Name: "records.raw",
Summary: "Unsupported raw message output.",
Description: "Rejected before retention.",
Handler: func(context.Context, authz.Subject, builderInput) (rawMessageBuilderOutput, error) {
return rawMessageBuilderOutput{}, nil
},
})
},
},
{
name: "cyclic type",
register: func(builder *codemode.Builder) error {
return codemode.Register(builder, codemode.Capability[builderInput, cyclicBuilderOutput]{
ID: "cap.cycle",
Name: "records.cycle",
Summary: "Unsupported cyclic output.",
Description: "Rejected before retention.",
Handler: func(context.Context, authz.Subject, builderInput) (cyclicBuilderOutput, error) {
return cyclicBuilderOutput{}, nil
},
})
},
},
{
name: "custom marshaler",
register: func(builder *codemode.Builder) error {
return codemode.Register(builder, codemode.Capability[builderInput, marshalerBuilderOutput]{
ID: "cap.marshaler",
Name: "records.marshaler",
Summary: "Unsupported marshaler output.",
Description: "Rejected before retention.",
Handler: func(context.Context, authz.Subject, builderInput) (marshalerBuilderOutput, error) {
return marshalerBuilderOutput{}, nil
},
})
},
},
{
name: "non-string map key",
register: func(builder *codemode.Builder) error {
return codemode.Register(builder, codemode.Capability[builderInput, mapKeyBuilderOutput]{
ID: "cap.mapkey",
Name: "records.mapkey",
Summary: "Unsupported map key output.",
Description: "Rejected before retention.",
Handler: func(context.Context, authz.Subject, builderInput) (mapKeyBuilderOutput, error) {
return mapKeyBuilderOutput{}, nil
},
})
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
builder := codemode.New(codemode.Options{Authorizer: authz.AllowAll(), Limits: codemode.DefaultLimits()})

err := tt.register(builder)

require.ErrorIs(t, err, codemode.ErrInvalidRegistration)
require.NoError(t, codemode.Register(builder, validBuilderCapability("cap.retained", "records.retained")))
server, buildErr := builder.Build()
require.NoError(t, buildErr)
results, searchErr := server.Search("unsupported")
require.NoError(t, searchErr)
assert.Empty(t, results)
})
}
}

// TestRegisterRejectsObviousDuplicates proves duplicate stable and model-facing identities fail immediately.
func TestRegisterRejectsObviousDuplicates(t *testing.T) {
builder := codemode.New(codemode.Options{Authorizer: authz.AllowAll(), Limits: codemode.DefaultLimits()})
Expand Down
75 changes: 72 additions & 3 deletions internal/catalog/catalog_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,24 @@ type ExportedOutput struct {
Name string `json:"name"`
}

// NamedItem is a nested catalog result whose Go identifier must stay host-local.
type NamedItem struct {
// ID is the nested row identifier.
ID string `json:"id"`

// Active reports whether the row survives filtering.
Active bool `json:"active"`

// Score is the nested finite floating-point field.
Score float64 `json:"score"`
}

// compositeTestOutput is a list-valued catalog result used to lock derived notation.
type compositeTestOutput struct {
// Items is the compiled list of nested objects.
Items []NamedItem `json:"items"`
}

// TestBuildValidatesEveryRegistrationBeforeFiltering proves disabled entries cannot hide invalid contracts.
func TestBuildValidatesEveryRegistrationBeforeFiltering(t *testing.T) {
registration := validRegistration("cap.bad", "records.bad", "Bad record")
Expand Down Expand Up @@ -163,12 +181,26 @@ func TestBuildFiltersOnceAndDerivesEverySurface(t *testing.T) {
validRegistration("cap.zeta", "teams.zeta", "Zeta team"),
validRegistration("cap.disabled", "records.disabled", "Disabled record"),
validRegistration("cap.alpha", "records.alpha", "Alpha record"),
{
ID: "cap.composite",
Name: "records.composite",
Summary: "Composite record",
Description: "Composite record full description.",
Plan: mustCompileCompositePlan(),
Invoke: func(context.Context, authz.Subject, any) (any, error) {
return compositeTestOutput{}, nil
},
},
}, testOptions("cap.disabled"))
require.NoError(t, err)

entries := catalog.Entries()
require.Len(t, entries, 2)
assert.Equal(t, []string{"records.alpha", "teams.zeta"}, []string{entries[0].Name, entries[1].Name})
require.Len(t, entries, 3)
assert.Equal(t, []string{"records.alpha", "records.composite", "teams.zeta"}, []string{
entries[0].Name,
entries[1].Name,
entries[2].Name,
})
_, foundByName := catalog.Lookup("records.disabled")
_, foundByID := catalog.LookupID("cap.disabled")
_, foundDisabledDescription := catalog.Describe("records.disabled")
Expand All @@ -179,8 +211,16 @@ func TestBuildFiltersOnceAndDerivesEverySurface(t *testing.T) {
assert.False(t, foundDisabledDescription)
assert.Empty(t, disabledSearch)

recordSearch, err := catalog.Search("record")
require.NoError(t, err)
require.Len(t, recordSearch, 2)
assert.Equal(t, []string{"records.alpha", "records.composite"}, []string{
recordSearch[0].Name,
recordSearch[1].Name,
})

bindings := catalog.NamespaceBindings()
require.Len(t, bindings, 2)
require.Len(t, bindings, 3)
assert.Equal(t, []string{"records"}, bindings[0].Segments)
assert.Equal(t, "alpha", bindings[0].Function)
assert.Same(t, entries[0].Plan, bindings[0].Capability.Plan)
Expand All @@ -201,6 +241,26 @@ func TestBuildFiltersOnceAndDerivesEverySurface(t *testing.T) {
assert.Equal(t, "name", description.Output[0].Name)
assert.Equal(t, "str", description.Output[0].Type)
assert.True(t, description.Output[0].Required)

compositeDescription, foundComposite := catalog.Describe("records.composite")
require.True(t, foundComposite)
assert.Equal(t, "records.composite(*, org: str, limit: int | None)", compositeDescription.Signature)
require.Len(t, compositeDescription.Output, 1)
assert.Equal(t, "items", compositeDescription.Output[0].Name)
assert.Equal(t, "list[{id: str, active: bool, score: float}]", compositeDescription.Output[0].Type)
assert.True(t, compositeDescription.Output[0].Required)
assertDescriptionOmitsOutputTypeNames(t, compositeDescription, "NamedItem", "compositeTestOutput")
compositeDescription.Input[0].Name = "mutated"
compositeDescription.Input[0].Type = "mutated"
compositeDescription.Output[0].Name = "mutated"
compositeDescription.Output[0].Type = "mutated"
freshComposite, foundComposite := catalog.Describe("records.composite")
require.True(t, foundComposite)
assert.Equal(t, "org", freshComposite.Input[0].Name)
assert.Equal(t, "str", freshComposite.Input[0].Type)
assert.Equal(t, "items", freshComposite.Output[0].Name)
assert.Equal(t, "list[{id: str, active: bool, score: float}]", freshComposite.Output[0].Type)

description.Input[0].Name = "mutated"
freshDescription, foundDescription := catalog.Describe("records.alpha")
require.True(t, foundDescription)
Expand Down Expand Up @@ -395,6 +455,15 @@ func mustCompileExportedOutputPlan() *binding.Plan {
return plan
}

// mustCompileCompositePlan compiles the catalog test input with a list-valued output.
func mustCompileCompositePlan() *binding.Plan {
plan, err := binding.CompileFor[testInput, compositeTestOutput]()
if err != nil {
panic(err)
}
return plan
}

// assertSearchOmitsOutputTypeNames requires a Search result to omit host Go output identifiers.
func assertSearchOmitsOutputTypeNames(t *testing.T, result SearchResult, forbidden ...string) {
t.Helper()
Expand Down
Loading