package schemaguard import ( "encoding/json" "os" "path/filepath" "testing" ) // repoRoot walks up from the package directory to the module root. func repoRoot(t *testing.T) string { t.Helper() return filepath.Join("..", "..") } // TestPublishedExamplesMatchTheirSchemas is the check approval-engine suggested // after the same defect class hit three repositories: a published example that // contradicts the schema it claims gets implemented in preference to the prose. func TestPublishedExamplesMatchTheirSchemas(t *testing.T) { root := repoRoot(t) cases := []struct { schema string example string }{ {"schemas/decision_envelope.schema.json", "examples/caring/decision_envelope.json"}, {"schemas/decision_envelope.schema.json", "examples/secrets-engine/replay/decision_rotate.json"}, {"schemas/decision_envelope.schema.json", "examples/secrets-engine/replay/decision_destroy_dual_control.json"}, {"schemas/action_authorization.schema.json", "examples/caring/action_authorization.json"}, } for _, tc := range cases { t.Run(tc.example, func(t *testing.T) { s, err := Load(filepath.Join(root, tc.schema)) if err != nil { t.Fatalf("load schema: %v", err) } problems, err := s.ValidateFile(filepath.Join(root, tc.example)) if err != nil { t.Fatalf("read example: %v", err) } for _, p := range problems { t.Errorf("%s: %s", tc.example, p) } }) } } // TestEveryAllowExampleStatesItsLifetime pins the §9.7.1 obligation that // FLEX-WP-0019 closed. An allow without a lifetime is the gap G3 named, and an // example missing it teaches consumers the pre-WP-0019 shape. func TestEveryAllowExampleStatesItsLifetime(t *testing.T) { root := repoRoot(t) matches, err := filepath.Glob(filepath.Join(root, "examples", "*", "replay", "*.json")) if err != nil { t.Fatal(err) } extra := filepath.Join(root, "examples", "caring", "decision_envelope.json") if _, err := os.Stat(extra); err == nil { matches = append(matches, extra) } if len(matches) == 0 { t.Fatal("no decision examples found — this test would pass vacuously") } for _, path := range matches { raw, err := os.ReadFile(path) if err != nil { t.Fatalf("%s: %v", path, err) } var doc map[string]any if err := json.Unmarshal(raw, &doc); err != nil { t.Fatalf("%s: %v", path, err) } if doc["effect"] != "allow" { continue } if _, ok := doc["lifetime"].(map[string]any); !ok { t.Errorf("%s: allow carries no lifetime (security-layer-model v0.7 §9.7.1)", path) } } } // TestApprovalClaimsInFixturesAreComplete validates the approval-claims embedded // in this repo's check requests against approval-engine's published schema. // // It is skipped when the sibling repository is not checked out, because a // cross-repo path is not a dependency this module can assert. It is worth // running where it can: the destroy rule was first written against an invented // claim shape, and a partial claim in a fixture is how a consumer learns one. func TestApprovalClaimsInFixturesAreComplete(t *testing.T) { schemaPath := filepath.Join(os.Getenv("HOME"), "approval-engine", "schemas", "approval_claim.schema.json") if _, err := os.Stat(schemaPath); err != nil { t.Skipf("approval-engine not checked out at %s", schemaPath) } s, err := Load(schemaPath) if err != nil { t.Fatalf("load claim schema: %v", err) } root := repoRoot(t) matches, err := filepath.Glob(filepath.Join(root, "examples", "*", "check_request_*.json")) if err != nil { t.Fatal(err) } checked := 0 for _, path := range matches { raw, err := os.ReadFile(path) if err != nil { t.Fatalf("%s: %v", path, err) } var doc map[string]any if err := json.Unmarshal(raw, &doc); err != nil { t.Fatalf("%s: %v", path, err) } ctx, _ := doc["context"].(map[string]any) claim, ok := ctx["approval"] if !ok { continue } checked++ for _, p := range s.Validate(claim) { t.Errorf("%s: context.approval: %s", path, p) } } if checked == 0 { t.Skip("no check request carries context.approval") } }