diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index 3c22d6d..b937bca 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -675,6 +675,12 @@ source-context tests and `internal/engine` configuration fixtures. Validation: run the configuration and internal-engine packages together when they compile. Correct production defects before adjusting meaningful assertions. +Completion: remaining adapter and internal-engine fixtures use identified lists; +coverage retains inventory ordering, null and metadata presence, prompt snapshot +ownership, syntax validation, and file origins. Added checks for version 2 in +string and integer forms, malformed headers, legacy mappings, and a headerless +imported item. Both configuration and internal-engine suites passed on Go 1.27.0. + ## Stage 15: Migrate Public Configuration, Inventory, And Scope Tests Scope: root construction, sources, inspection/diff, inheritance, profile diff --git a/internal/configuration/listing_test.go b/internal/configuration/listing_test.go index 78a2a3b..1be292e 100644 --- a/internal/configuration/listing_test.go +++ b/internal/configuration/listing_test.go @@ -12,14 +12,12 @@ import ( func TestLoadedListsSortedEffectiveIdentities(t *testing.T) { state, err := Load(mapping("identities", map[string]any{ - "backends": map[string]any{ - "local": map[string]any{"endpoint": "http://localhost:8080/v1"}, - }, + "backends": []any{map[string]any{"id": "local", "endpoint": "http://localhost:8080/v1"}}, "collections": map[string]any{ - "z": map[string]any{"profiles": map[string]any{"profile": map[string]any{"backend": "local", "model": "z-model"}}, "prompts": map[string]any{"prompt": listingPromptDefinition()}}, - "partial": map[string]any{"profiles": map[string]any{"profile": map[string]any{"backend": "local"}}}, - "a": map[string]any{"profiles": map[string]any{"profile": map[string]any{"backend": "local", "model": "a-model"}}, "prompts": map[string]any{"prompt": listingPromptDefinition()}}, - " z": map[string]any{"profiles": map[string]any{"profile ": map[string]any{"backend": "local", "model": "spaced-model"}}, "prompts": map[string]any{"prompt ": listingPromptDefinition()}}, + "z": map[string]any{"profiles": []any{map[string]any{"id": "profile", "backend": "local", "model": "z-model"}}, "prompts": []any{listingPromptDefinition("prompt")}}, + "partial": map[string]any{"profiles": []any{map[string]any{"id": "profile", "backend": "local"}}}, + "a": map[string]any{"profiles": []any{map[string]any{"id": "profile", "backend": "local", "model": "a-model"}}, "prompts": []any{listingPromptDefinition("prompt")}}, + " z": map[string]any{"profiles": []any{map[string]any{"id": "profile ", "backend": "local", "model": "spaced-model"}}, "prompts": []any{listingPromptDefinition("prompt ")}}, }, }), nil) if err != nil { @@ -59,9 +57,9 @@ func TestLoadedListsSortedEffectiveIdentities(t *testing.T) { func TestLoadedListsAreIndependent(t *testing.T) { state, err := Load(mapping("identities", map[string]any{ - "backends": map[string]any{"backend": map[string]any{"endpoint": "https://example.test/v1"}}, - "profiles": map[string]any{"profile": map[string]any{}}, - "prompts": map[string]any{"example": listingPromptDefinition()}, + "backends": []any{map[string]any{"id": "backend", "endpoint": "https://example.test/v1"}}, + "profiles": []any{map[string]any{"id": "profile"}}, + "prompts": []any{listingPromptDefinition("example")}, }), nil) if err != nil { t.Fatal(err) @@ -127,8 +125,8 @@ func TestLoadedListsEmptyInventory(t *testing.T) { func TestLoadedListsHonorCanceledContext(t *testing.T) { state, err := Load(mapping("identities", map[string]any{ - "profiles": map[string]any{"partial": map[string]any{}}, - "prompts": map[string]any{"example": listingPromptDefinition()}, + "profiles": []any{map[string]any{"id": "partial"}}, + "prompts": []any{listingPromptDefinition("example")}, }), nil) if err != nil { t.Fatal(err) @@ -150,9 +148,9 @@ func TestLoadedListsHonorCanceledContext(t *testing.T) { } } -func listingPromptDefinition() map[string]any { +func listingPromptDefinition(id string) map[string]any { return map[string]any{ - "version": "v1", + "id": id, "messages": []any{map[string]any{"role": "user", "content": "Hello."}}, "output": map[string]any{"format": "text", "validation_mode": "basic"}, } diff --git a/internal/configuration/load_test.go b/internal/configuration/load_test.go index fb64934..806ded7 100644 --- a/internal/configuration/load_test.go +++ b/internal/configuration/load_test.go @@ -300,11 +300,11 @@ func TestLoadChecksOptionalConfigurationVersion(t *testing.T) { include bool valid bool }{ - {name: "string one", version: "1", include: true, valid: true}, - {name: "integer one", version: int64(1), include: true, valid: true}, + {name: "string two", version: "2", include: true, valid: true}, + {name: "integer two", version: int64(2), include: true, valid: true}, {name: "omitted", valid: true}, {name: "explicit null", include: true}, - {name: "unsupported string", version: "2", include: true}, + {name: "unsupported string", version: "1", include: true}, } { t.Run(test.name, func(t *testing.T) { values := map[string]any{} @@ -316,8 +316,8 @@ func TestLoadChecksOptionalConfigurationVersion(t *testing.T) { if err != nil { t.Fatalf("load accepted version: %v", err) } - if test.include && (loaded.result.Config.Version == nil || *loaded.result.Config.Version != "1") { - t.Fatalf("typed version = %v, want 1", loaded.result.Config.Version) + if test.include && (loaded.result.Config.Version == nil || *loaded.result.Config.Version != "2") { + t.Fatalf("typed version = %v, want 2", loaded.result.Config.Version) } if !test.include && loaded.result.Config.Version != nil { t.Fatalf("omitted version = %q, want nil", *loaded.result.Config.Version) @@ -328,9 +328,6 @@ func TestLoadChecksOptionalConfigurationVersion(t *testing.T) { if err == nil || !errors.As(err, &loadErr) { t.Fatalf("load error = %v, want ConfigKit load error", err) } - if strings.Contains(err.Error(), "2") { - t.Fatalf("load error disclosed the unsupported value: %v", err) - } found := false for _, diagnostic := range loadErr.Diagnostics() { if diagnostic.Path.String() == "/version" && diagnostic.Message == "unsupported configuration specification version" { @@ -344,9 +341,28 @@ func TestLoadChecksOptionalConfigurationVersion(t *testing.T) { } } +func TestLoadRejectsMalformedHeadersAndLegacyResourceMappings(t *testing.T) { + for name, values := range map[string]map[string]any{ + "boolean header": {"version": true}, + "fractional header": {"version": 2.5}, + "sequence header": {"version": []any{2}}, + "backend mapping": {"backends": map[string]any{"local": map[string]any{"endpoint": "https://example.invalid/v1"}}}, + "profile mapping": {"profiles": map[string]any{"local": map[string]any{}}}, + "prompt mapping": {"prompts": map[string]any{"local": map[string]any{}}}, + } { + t.Run(name, func(t *testing.T) { + _, err := load(mapping(name, values), nil) + var loadErr *configkit.LoadError + if !errors.As(err, &loadErr) || len(loadErr.Diagnostics()) == 0 { + t.Fatalf("load error = %v, want structured rejection", err) + } + }) + } +} + func TestLoadConsumesAcceptedUnderlayVersionAndValidatesHeaderlessPayload(t *testing.T) { underlay := mapping("declared underlay", map[string]any{ - "version": int64(1), + "version": int64(2), }) loaded, err := load(mapping("headerless main", map[string]any{}), []configkit.Underlay{{Inputs: []configkit.Input{underlay}}}) if err != nil { diff --git a/internal/configuration/prompts_test.go b/internal/configuration/prompts_test.go index 1bc0a4c..a72cd6e 100644 --- a/internal/configuration/prompts_test.go +++ b/internal/configuration/prompts_test.go @@ -99,6 +99,23 @@ func TestLoadCapturesInheritedImportedContent(t *testing.T) { } } +func TestLoadHeaderlessImportedItem(t *testing.T) { + files := fstest.MapFS{ + "main.yaml": {Data: []byte("version: 2\nconfig:\n imports:\n - path: item.yaml\n destination: [prompts]\n placement: sequence-item\n required: true\n")}, + "item.yaml": {Data: []byte("id: example\nmessages:\n - role: user\n content: hello\noutput:\n format: text\n validation_mode: basic\n")}, + } + namespace := configkit.NewFilesystem("items", files) + state, err := load(configkit.Input{Source: namespace.File("main.yaml"), Imports: true, + Filesystem: &configkit.FilesystemContext{FS: &namespace, Root: "."}}, nil) + if err != nil { + t.Fatal(err) + } + definition, _, err := state.prompt("example") + if err != nil || definition.Templates[0].Content != "hello" { + t.Fatalf("headerless item = %+v, %v", definition, err) + } +} + func TestLoadUsesExplicitMappingContextAndClosesContent(t *testing.T) { filesystem := &countingFS{FS: fstest.MapFS{ "assets/body.md": {Data: []byte("From mapping")}, @@ -146,7 +163,7 @@ func TestLoadCapturesNativeContentFile(t *testing.T) { } func TestLoadRejectsMalformedStaticSyntax(t *testing.T) { - _, err := load(mapping("bad-template", map[string]any{"prompts": map[string]any{"example": map[string]any{ + _, err := load(mapping("bad-template", map[string]any{"prompts": []any{map[string]any{"id": "example", "messages": []any{map[string]any{"role": "user", "content": "{{if}}"}}, "output": map[string]any{"format": "text", "validation_mode": "basic"}, }}}), nil) @@ -168,11 +185,10 @@ func TestPromptInspectionLookupMatchesSnapshotSelectionAndOwnsInputs(t *testing. } } selectedPrompt := makePrompt("selected") + selectedPrompt["id"] = "selected" state, err := load(mapping("prompt inspection lookup", map[string]any{ - "profiles": map[string]any{"partial": map[string]any{}}, - "prompts": map[string]any{ - "selected": selectedPrompt, - }, + "profiles": []any{map[string]any{"id": "partial"}}, + "prompts": []any{selectedPrompt}, }), nil) if err != nil { t.Fatal(err) @@ -227,9 +243,8 @@ func TestPromptInspectionLookupMatchesSnapshotSelectionAndOwnsInputs(t *testing. } func TestLoadAcceptsEmptyInlineContentAndRejectsInvalidPromptFields(t *testing.T) { - state, err := load(mapping("empty-content", map[string]any{"prompts": map[string]any{" exact.id ": map[string]any{ - "version": "v1", - "inputs": []any{map[string]any{"name": " note ", "required": true}}, + state, err := load(mapping("empty-content", map[string]any{"prompts": []any{map[string]any{"id": " exact.id ", + "inputs": []any{map[string]any{"name": " note ", "required": true}}, "messages": []any{map[string]any{"role": " USER ", "content": "", "content_file": nil, "cache_control": map[string]any{"type": " ephemeral ", "ttl": "1h "}}}, "output": map[string]any{"format": "text", "validation_mode": "basic"}, @@ -249,7 +264,7 @@ func TestLoadAcceptsEmptyInlineContentAndRejectsInvalidPromptFields(t *testing.T if _, _, err := state.prompt("exact.id"); !errors.Is(err, errPromptNotFound) { t.Fatalf("trimmed prompt identity unexpectedly matched: %v", err) } - _, err = load(mapping("obsolete-field", map[string]any{"prompts": map[string]any{"example": map[string]any{ + _, err = load(mapping("obsolete-field", map[string]any{"prompts": []any{map[string]any{ "version": "v1", "id": "example", "messages": []any{map[string]any{"role": "user", "content": "ok"}}, "output": map[string]any{"format": "text", "validation_mode": "basic"}, @@ -258,7 +273,7 @@ func TestLoadAcceptsEmptyInlineContentAndRejectsInvalidPromptFields(t *testing.T if !errors.As(err, &loadError) { t.Fatalf("obsolete identity field should fail strict decoding: %v", err) } - _, err = load(mapping("null-prompt", map[string]any{"prompts": map[string]any{"example": nil}}), nil) + _, err = load(mapping("null-prompt", map[string]any{"prompts": []any{nil}}), nil) if err == nil { t.Fatal("null prompt collection was accepted") } @@ -266,8 +281,8 @@ func TestLoadAcceptsEmptyInlineContentAndRejectsInvalidPromptFields(t *testing.T func TestRequirementSnapshotCapturesMetadataPresenceAndNormalizedValues(t *testing.T) { state, err := load(mapping("requirement-metadata", map[string]any{ - "prompts": map[string]any{"example": map[string]any{ - "version": "v1", + "prompts": []any{map[string]any{ + "id": "example", "session_id": "{{.session}}", "inputs": []any{ map[string]any{"name": "absent"}, @@ -340,8 +355,8 @@ func TestInputMetadataPresenceDoesNotChangePromptHashOrInspection(t *testing.T) input[name] = value } state, err := load(mapping("hash-"+variant.name, map[string]any{ - "prompts": map[string]any{"same": map[string]any{ - "version": "v1", + "prompts": []any{map[string]any{ + "id": "same", "inputs": []any{input}, "messages": []any{map[string]any{"role": "user", "content": "hello"}}, "output": map[string]any{"format": "text", "validation_mode": "basic"}, diff --git a/internal/engine/smoke_test.go b/internal/engine/smoke_test.go index c9b970d..2d97b69 100644 --- a/internal/engine/smoke_test.go +++ b/internal/engine/smoke_test.go @@ -13,14 +13,14 @@ import ( func engineSmokeBackendInput() configkit.Input { return configkit.Input{Source: configkit.Mapping("smoke-test backend", map[string]any{ - "backends": map[string]any{"test-backend": map[string]any{ + "backends": []any{map[string]any{"id": "test-backend", "endpoint": "https://test-backend.example/v1", "smoke_tests": map[string]any{ "default_profile": "test-profile", "authentication": map[string]any{"kind": "none"}, }, }}, - "profiles": map[string]any{"test-profile": map[string]any{ + "profiles": []any{map[string]any{"id": "test-profile", "backend": "test-backend", "model": "synthetic-model", }},