243 lines
6.7 KiB
Go
243 lines
6.7 KiB
Go
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
|
|
}
|