package workspace import ( "path/filepath" "strings" "testing" "gitea.maximumdirect.net/eric/notarius/internal/core/artifacts" "gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline" ) func TestCheckpointIdentityIsDeterministic(t *testing.T) { first := mustIdentity(t, identityInput()) second := mustIdentity(t, identityInput()) if first.Digest != second.Digest { t.Fatalf("digest changed for same input: %q != %q", first.Digest, second.Digest) } if !strings.HasPrefix(first.Digest, "sha256:") { t.Fatalf("digest = %q, want sha256 prefix", first.Digest) } } func TestCheckpointIdentityChangesWhenInputsChange(t *testing.T) { base := mustIdentity(t, identityInput()) tests := []struct { name string mutate func(CheckpointIdentityInput) CheckpointIdentityInput }{ { name: "pipeline digest", mutate: func(input CheckpointIdentityInput) CheckpointIdentityInput { input.Pipeline.Digest = "sha256:pipeline-b" return input }, }, { name: "raw input digest", mutate: func(input CheckpointIdentityInput) CheckpointIdentityInput { input.RawInputDigest = "sha256:raw-b" return input }, }, { name: "selected lanes", mutate: func(input CheckpointIdentityInput) CheckpointIdentityInput { input.SelectedLanes = []string{"items"} return input }, }, { name: "reference digest", mutate: func(input CheckpointIdentityInput) CheckpointIdentityInput { input.References[0].Digest = "sha256:reference-b" return input }, }, { name: "runtime override", mutate: func(input CheckpointIdentityInput) CheckpointIdentityInput { input.RuntimeOverrides = []Fingerprint{{Name: "llm_profile", Value: "careful"}} return input }, }, { name: "provenance fingerprint", mutate: func(input CheckpointIdentityInput) CheckpointIdentityInput { input.ProvenanceFingerprints = []Fingerprint{{Name: "prompt:dnd.spells", Value: "sha256:prompt-b"}} return input }, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { changed := mustIdentity(t, tc.mutate(identityInput())) if changed.Digest == base.Digest { t.Fatalf("digest did not change after %s mutation: %q", tc.name, changed.Digest) } }) } } func TestCheckpointIdentityNormalizesOrder(t *testing.T) { input := identityInput() input.SelectedLanes = []string{"spells", "items", "spells"} input.RuntimeOverrides = []Fingerprint{ {Name: "z", Value: "2"}, {Name: "a", Value: "1"}, } input.ProvenanceFingerprints = []Fingerprint{ {Name: "schema", Value: "sha256:schema"}, {Name: "prompt", Value: "sha256:prompt"}, } identity := mustIdentity(t, input) if got := strings.Join(identity.SelectedLanes, ","); got != "items,spells" { t.Fatalf("selected lanes = %q, want sorted unique values", got) } if identity.RuntimeOverrides[0].Name != "a" || identity.ProvenanceFingerprints[0].Name != "prompt" { t.Fatalf("fingerprints not sorted: runtime=%+v provenance=%+v", identity.RuntimeOverrides, identity.ProvenanceFingerprints) } } func TestCheckpointIdentityPathIsFilesystemSafe(t *testing.T) { input := identityInput() input.Pipeline.ID = "campaign/main" input.InputKey = "seriatim/input" input.SourceDigest = "sha256:abcdef0123456789ffffffff" input.Pipeline.Digest = "sha256:1234567890abcdefeeeeeeee" identity := mustIdentity(t, input) relative, err := identity.RelativePath() if err != nil { t.Fatalf("RelativePath: %v", err) } if strings.Contains(relative, `\`) || strings.Contains(relative, "..") { t.Fatalf("relative path is not filesystem safe: %q", relative) } if relative != "campaign~2fmain/seriatim~2finput-abcdef0123456789/1234567890abcdef" { t.Fatalf("relative path = %q", relative) } root := t.TempDir() settings := Settings{ CheckpointsRoot: filepath.Join(root, "checkpoints"), ResumeEnabled: true, } got, err := settings.CheckpointDirectory(identity) if err != nil { t.Fatalf("CheckpointDirectory: %v", err) } want := filepath.Join(root, "checkpoints", "campaign~2fmain", "seriatim~2finput-abcdef0123456789", "1234567890abcdef") if got != want { t.Fatalf("checkpoint directory = %q, want %q", got, want) } } func TestCheckpointDirectoryDisabledReturnsEmptyPath(t *testing.T) { settings := Settings{CheckpointsRoot: filepath.Join(t.TempDir(), "checkpoints")} got, err := settings.CheckpointDirectory(mustIdentity(t, identityInput())) if err != nil { t.Fatalf("CheckpointDirectory: %v", err) } if got != "" { t.Fatalf("CheckpointDirectory = %q, want empty path", got) } } func TestNewCheckpointIdentityRequiresCoreInputs(t *testing.T) { tests := []struct { name string mutate func(CheckpointIdentityInput) CheckpointIdentityInput want string }{ { name: "pipeline id", mutate: func(input CheckpointIdentityInput) CheckpointIdentityInput { input.Pipeline.ID = "" return input }, want: "pipeline id", }, { name: "pipeline digest", mutate: func(input CheckpointIdentityInput) CheckpointIdentityInput { input.Pipeline.Digest = "" return input }, want: "pipeline digest", }, { name: "input key", mutate: func(input CheckpointIdentityInput) CheckpointIdentityInput { input.InputKey = "" input.Pipeline.Input.Module = "" return input }, want: "input key", }, { name: "input digest", mutate: func(input CheckpointIdentityInput) CheckpointIdentityInput { input.RawInputDigest = "" input.SourceDigest = "" return input }, want: "raw input digest or source digest", }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { _, err := NewCheckpointIdentity(tc.mutate(identityInput())) if err == nil || !strings.Contains(err.Error(), tc.want) { t.Fatalf("expected error containing %q, got %v", tc.want, err) } }) } } func identityInput() CheckpointIdentityInput { return CheckpointIdentityInput{ Pipeline: pipeline.ResolvedPipeline{ ID: "dnd-session", Digest: "sha256:pipeline-a", Input: pipeline.Binding("seriatim"), ArtifactLanes: []pipeline.ResolvedArtifactLane{ {ID: "spells"}, {ID: "items"}, }, }, InputKey: "seriatim", RawInputDigest: "sha256:raw-a", SelectedLanes: []string{"spells"}, RuntimeOverrides: []Fingerprint{ {Name: "llm_profile", Value: "fast"}, }, References: []artifacts.ReferenceProvenance{ { Stage: "extract", LaneID: "spells", SlotName: "party", OriginURI: "file:///party.yml", Digest: "sha256:reference-a", }, }, ProvenanceFingerprints: []Fingerprint{ {Name: "prompt:dnd.spells", Value: "sha256:prompt-a"}, }, } } func mustIdentity(t *testing.T, input CheckpointIdentityInput) CheckpointIdentity { t.Helper() identity, err := NewCheckpointIdentity(input) if err != nil { t.Fatalf("NewCheckpointIdentity: %v", err) } return identity }