119 lines
4.7 KiB
Go
119 lines
4.7 KiB
Go
package semanticreconcile
|
|
|
|
import (
|
|
"context"
|
|
"io/fs"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"gitea.maximumdirect.net/eric/notarius/internal/framework/llm"
|
|
"gitea.maximumdirect.net/eric/promptkit"
|
|
)
|
|
|
|
func TestRegisterAssetsPreparesGenericPromptOffline(t *testing.T) {
|
|
registry := llm.NewAssetRegistry()
|
|
if err := RegisterAssets(registry); err != nil {
|
|
t.Fatalf("RegisterAssets() error = %v, want nil", err)
|
|
}
|
|
options, err := registry.PromptKitOptions()
|
|
if err != nil {
|
|
t.Fatalf("PromptKitOptions() error = %v, want nil", err)
|
|
}
|
|
options = append(options, promptkit.WithProfiles(promptkit.OpenAICompatibleProfile(promptkit.OpenAICompatibleProfileConfig{
|
|
ID: "semantic-reconciliation-test", Endpoint: "http://127.0.0.1:1/v1", Model: "offline-test-model",
|
|
})))
|
|
engine, err := promptkit.NewEngine(promptkit.Config{Timeout: time.Second}, options...)
|
|
if err != nil {
|
|
t.Fatalf("NewEngine() error = %v, want nil", err)
|
|
}
|
|
prepared, err := engine.Prepare(context.Background(), promptkit.RunRequest{
|
|
PromptID: PromptID, PromptVersion: PromptVersion, ProfileID: "semantic-reconciliation-test",
|
|
Inputs: map[string]promptkit.ArtifactRef{
|
|
"candidates": promptkit.Inline(`{"candidates":[{"candidate_id":1,"label":"Mira"},{"candidate_id":2,"label":"Captain Mira"}]}`),
|
|
"transcript": promptkit.Inline(`{"windows":[{"units":[{"unit_id":7,"text":"Mira arrived."}]}]}`),
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Prepare() error = %v, want nil", err)
|
|
}
|
|
|
|
if prepared.PromptID != PromptID || prepared.PromptVersion != PromptVersion {
|
|
t.Fatalf("prepared prompt identity = %q %q, want %q %q", prepared.PromptID, prepared.PromptVersion, PromptID, PromptVersion)
|
|
}
|
|
if prepared.SelectedProfileID != "semantic-reconciliation-test" {
|
|
t.Fatalf("selected profile = %q, want explicit test profile", prepared.SelectedProfileID)
|
|
}
|
|
if contract := prepared.OutputContract; contract.SchemaPath != "semantic_reconciliation_llm.v1.json" || contract.RepairAttempts != 0 {
|
|
t.Fatalf("output contract = %#v, want generic schema without repair", contract)
|
|
}
|
|
if len(prepared.Messages) != 5 || prepared.Messages[0].Role != "system" {
|
|
t.Fatalf("prepared messages = %#v, want five ordered messages beginning with system", prepared.Messages)
|
|
}
|
|
if cache := prepared.Messages[2].CacheControl; cache == nil || cache.Type != promptkit.CacheControlEphemeral {
|
|
t.Fatalf("semantic policy cache control = %#v, want ephemeral", cache)
|
|
}
|
|
for _, index := range []int{0, 1, 3, 4} {
|
|
if prepared.Messages[index].CacheControl != nil {
|
|
t.Fatalf("message %d cache control = %#v, want nil", index, prepared.Messages[index].CacheControl)
|
|
}
|
|
}
|
|
protocol := prepared.Messages[1].Content
|
|
for _, requirement := range []string{"positive integer", "Return IDs only", "do not copy candidate names", "source ranges"} {
|
|
if !strings.Contains(protocol, requirement) {
|
|
t.Fatalf("protocol message = %q, want requirement %q", protocol, requirement)
|
|
}
|
|
}
|
|
if !strings.Contains(prepared.Messages[3].Content, `"candidate_id":1`) || strings.Contains(prepared.Messages[3].Content, `"windows"`) {
|
|
t.Fatalf("candidate message = %q, want only integer candidate material", prepared.Messages[3].Content)
|
|
}
|
|
if !strings.Contains(prepared.Messages[4].Content, `"windows"`) || strings.Contains(prepared.Messages[4].Content, `"candidate_id"`) {
|
|
t.Fatalf("transcript message = %q, want only transcript windows", prepared.Messages[4].Content)
|
|
}
|
|
}
|
|
|
|
func TestAssetHashesAreDeterministicAndComplete(t *testing.T) {
|
|
firstPrompt, err := PromptHash()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
secondPrompt, err := PromptHash()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
schemaHash, err := SchemaHash()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if firstPrompt == "" || firstPrompt != secondPrompt || schemaHash == "" || firstPrompt == schemaHash {
|
|
t.Fatalf("asset hashes = prompt %q/%q schema %q, want stable distinct hashes", firstPrompt, secondPrompt, schemaHash)
|
|
}
|
|
}
|
|
|
|
func TestSharedPromptFilesExposeOnlyReusableCoreAssets(t *testing.T) {
|
|
first, err := SharedPromptFiles()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
second, err := SharedPromptFiles()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
wantNames := []string{"protocol.md", "candidates.md", "transcript-windows.md"}
|
|
gotNames := make([]string, len(first))
|
|
for index, file := range first {
|
|
gotNames[index] = file.Name
|
|
if content, err := fs.ReadFile(file.FS, file.Path); err != nil || len(content) == 0 {
|
|
t.Fatalf("shared file %q = %q, %v; want readable content", file.Name, content, err)
|
|
}
|
|
}
|
|
if !reflect.DeepEqual(gotNames, wantNames) {
|
|
t.Fatalf("shared files = %#v, want narrow allowlist %#v", gotNames, wantNames)
|
|
}
|
|
first[0].Name = "changed.md"
|
|
if second[0].Name != "protocol.md" {
|
|
t.Fatalf("SharedPromptFiles() reused mutable descriptors: %#v", second)
|
|
}
|
|
}
|