Add checkpoint identity and manifest types
This commit is contained in:
242
internal/core/workspace/identity_test.go
Normal file
242
internal/core/workspace/identity_test.go
Normal file
@@ -0,0 +1,242 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user