Require explicit manifest schemas

This commit is contained in:
2026-09-02 11:45:40 +00:00
parent 9679117e94
commit 2cfca710ec
17 changed files with 307 additions and 123 deletions

View File

@@ -74,18 +74,19 @@ func TestLocalStoreCreateSaveLoadRoundTrip(t *testing.T) {
}
}
func TestLocalStoreArtifactMetadataCompatibility(t *testing.T) {
func TestLocalStorePreservesOptionalArtifactMetadata(t *testing.T) {
store := &LocalStore{}
ctx := context.Background()
dir := t.TempDir()
oldPath := filepath.Join(dir, "old-manifest.json")
oldJSON := `{
path := filepath.Join(dir, "manifest.json")
data := `{
"schema_version": "narratio.session-manifest.v1",
"session_id": "2026-05-03",
"created_at": "2026-05-03T10:00:00Z",
"updated_at": "2026-05-03T10:01:00Z",
"stages": {
"analyze": {
"name": "analyze",
"render": {
"name": "render",
"status": "succeeded",
"created_at": "2026-05-03T10:00:00Z",
"updated_at": "2026-05-03T10:01:00Z",
@@ -93,22 +94,22 @@ func TestLocalStoreArtifactMetadataCompatibility(t *testing.T) {
}
}
}`
if err := os.WriteFile(oldPath, []byte(oldJSON), 0o644); err != nil {
t.Fatalf("WriteFile() old manifest error = %v", err)
if err := os.WriteFile(path, []byte(data), 0o644); err != nil {
t.Fatalf("WriteFile() manifest error = %v", err)
}
loaded, err := store.Load(ctx, oldPath)
loaded, err := store.Load(ctx, path)
if err != nil {
t.Fatalf("Load() old manifest error = %v", err)
t.Fatalf("Load() manifest error = %v", err)
}
oldOutput := loaded.Stages["analyze"].Outputs[0]
oldOutput := loaded.Stages["render"].Outputs[0]
if oldOutput.Contract != nil || oldOutput.ExternalProvenance != nil {
t.Fatalf("old output metadata = %#v, %#v; want nil", oldOutput.Contract, oldOutput.ExternalProvenance)
}
if err := store.Save(ctx, oldPath, loaded); err != nil {
t.Fatalf("Save() old manifest error = %v", err)
if err := store.Save(ctx, path, loaded); err != nil {
t.Fatalf("Save() manifest error = %v", err)
}
roundTripped, err := os.ReadFile(oldPath)
roundTripped, err := os.ReadFile(path)
if err != nil {
t.Fatalf("ReadFile() round-tripped old manifest error = %v", err)
}
@@ -116,12 +117,12 @@ func TestLocalStoreArtifactMetadataCompatibility(t *testing.T) {
t.Fatalf("old manifest gained fabricated metadata:\n%s", roundTripped)
}
loaded.Stages["analyze"].Outputs[0].Contract = &artifactmodel.ContractMetadata{
loaded.Stages["render"].Outputs[0].Contract = &artifactmodel.ContractMetadata{
MediaType: "application/json",
SchemaID: "example.recap",
SchemaVersion: "v1",
}
loaded.Stages["analyze"].Outputs[0].ExternalProvenance = &artifactmodel.ExternalProvenance{
loaded.Stages["render"].Outputs[0].ExternalProvenance = &artifactmodel.ExternalProvenance{
System: "example",
RunID: "external-run",
PipelineID: "pipeline",
@@ -135,7 +136,7 @@ func TestLocalStoreArtifactMetadataCompatibility(t *testing.T) {
if err != nil {
t.Fatalf("Load() metadata manifest error = %v", err)
}
got := withMetadata.Stages["analyze"].Outputs[0]
got := withMetadata.Stages["render"].Outputs[0]
if got.Contract == nil || got.Contract.SchemaID != "example.recap" {
t.Fatalf("contract = %#v, want persisted contract", got.Contract)
}
@@ -242,7 +243,7 @@ func TestLoadRejectsInvalidManifest(t *testing.T) {
ctx := context.Background()
path := filepath.Join(t.TempDir(), "manifest.json")
if err := os.WriteFile(path, []byte(`{"updated_at":"2026-05-03T10:00:00Z"}`), 0o644); err != nil {
if err := os.WriteFile(path, []byte(`{"schema_version":"narratio.session-manifest.v1","updated_at":"2026-05-03T10:00:00Z","stages":{}}`), 0o644); err != nil {
t.Fatalf("WriteFile() error = %v", err)
}
@@ -319,7 +320,7 @@ func TestLoadRunRejectsInvalidManifest(t *testing.T) {
ctx := context.Background()
path := filepath.Join(t.TempDir(), "run-manifest.json")
if err := os.WriteFile(path, []byte(`{"session_id":"2026-05-03"}`), 0o644); err != nil {
if err := os.WriteFile(path, []byte(`{"schema_version":"narratio.run-manifest.v1","session_id":"2026-05-03","stages":{}}`), 0o644); err != nil {
t.Fatalf("WriteFile() error = %v", err)
}
@@ -331,3 +332,101 @@ func TestLoadRunRejectsInvalidManifest(t *testing.T) {
t.Fatalf("error = %q, want run_id validation", err.Error())
}
}
func TestLocalStoreRejectsUnsupportedSchemaVersions(t *testing.T) {
store := &LocalStore{}
now := time.Date(2026, 5, 3, 10, 0, 0, 0, time.UTC)
for _, test := range []struct {
name string
schema string
load func(*strings.Reader) error
save func() error
want string
}{
{
name: "missing session schema",
schema: `{"session_id":"session","created_at":"2026-05-03T10:00:00Z","updated_at":"2026-05-03T10:00:00Z","stages":{}}`,
load: func(source *strings.Reader) error {
_, err := store.LoadReader(context.Background(), source)
return err
},
save: func() error {
return store.Save(context.Background(), filepath.Join(t.TempDir(), "manifest.json"), &Manifest{SessionID: "session", CreatedAt: now, UpdatedAt: now, Stages: map[string]*StageRecord{}})
},
want: "session manifest schema_version is required",
},
{
name: "unknown session schema",
schema: `{"schema_version":"narratio.session-manifest.v99","session_id":"session","created_at":"2026-05-03T10:00:00Z","updated_at":"2026-05-03T10:00:00Z","stages":{}}`,
load: func(source *strings.Reader) error {
_, err := store.LoadReader(context.Background(), source)
return err
},
save: func() error {
m := New("session", now)
m.SchemaVersion = "narratio.session-manifest.v99"
return store.Save(context.Background(), filepath.Join(t.TempDir(), "manifest.json"), m)
},
want: "unsupported session manifest schema_version",
},
{
name: "missing run schema",
schema: `{"session_id":"session","run_id":"run","created_at":"2026-05-03T10:00:00Z","updated_at":"2026-05-03T10:00:00Z","stages":{}}`,
load: func(source *strings.Reader) error {
_, err := store.LoadRunReader(context.Background(), source)
return err
},
save: func() error {
return store.SaveRun(context.Background(), filepath.Join(t.TempDir(), "run.json"), &RunManifest{SessionID: "session", RunID: "run", CreatedAt: now, UpdatedAt: now, Stages: map[string]*RunStageRecord{}})
},
want: "run manifest schema_version is required",
},
{
name: "unknown run schema",
schema: `{"schema_version":"narratio.run-manifest.v99","session_id":"session","run_id":"run","created_at":"2026-05-03T10:00:00Z","updated_at":"2026-05-03T10:00:00Z","stages":{}}`,
load: func(source *strings.Reader) error {
_, err := store.LoadRunReader(context.Background(), source)
return err
},
save: func() error {
m := NewRun("session", "", "run", false, nil, now)
m.SchemaVersion = "narratio.run-manifest.v99"
return store.SaveRun(context.Background(), filepath.Join(t.TempDir(), "run.json"), m)
},
want: "unsupported run manifest schema_version",
},
} {
t.Run(test.name, func(t *testing.T) {
if err := test.load(strings.NewReader(test.schema)); err == nil || !strings.Contains(err.Error(), test.want) {
t.Fatalf("load error = %v, want %q", err, test.want)
}
if err := test.save(); err == nil || !strings.Contains(err.Error(), test.want) {
t.Fatalf("save error = %v, want %q", err, test.want)
}
})
}
}
func TestLocalStoreRejectsIncompleteStageIdentity(t *testing.T) {
now := time.Date(2026, 5, 3, 10, 0, 0, 0, time.UTC)
store := &LocalStore{}
for _, test := range []struct {
name string
stages map[string]*StageRecord
want string
}{
{name: "nil map", want: "stages is required"},
{name: "nil entry", stages: map[string]*StageRecord{"prepare": nil}, want: "stages.prepare is nil"},
{name: "missing name", stages: map[string]*StageRecord{"prepare": {CreatedAt: now, UpdatedAt: now}}, want: "stages.prepare.name is required"},
{name: "mismatched name", stages: map[string]*StageRecord{"prepare": {Name: "render", CreatedAt: now, UpdatedAt: now}}, want: "does not match map key"},
} {
t.Run(test.name, func(t *testing.T) {
m := New("session", now)
m.Stages = test.stages
if err := store.Save(context.Background(), filepath.Join(t.TempDir(), "manifest.json"), m); err == nil || !strings.Contains(err.Error(), test.want) {
t.Fatalf("Save() error = %v, want %q", err, test.want)
}
})
}
}