Improve structural test diagnostics

This commit is contained in:
2026-09-05 18:08:49 +00:00
parent 85663f5bc9
commit 204caf9d24
10 changed files with 64 additions and 36 deletions

View File

@@ -295,7 +295,7 @@ independently without save/restore choreography.
## Stage 5 — Improve Selected Large Structural Test Diagnostics
**Status:** Pending
**Status:** Completed
### Goal

1
go.mod
View File

@@ -7,6 +7,7 @@ require (
github.com/aws/aws-sdk-go-v2/credentials v1.19.16
github.com/aws/aws-sdk-go-v2/service/s3 v1.101.0
github.com/aws/smithy-go v1.25.1
github.com/google/go-cmp v0.7.0
golang.org/x/sync v0.22.0
golang.org/x/sys v0.47.0
gopkg.in/yaml.v3 v3.0.1

2
go.sum
View File

@@ -34,6 +34,8 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.42.1 h1:F/M5Y9I3nwr2IEpshZgh1GeHpOIt
github.com/aws/aws-sdk-go-v2/service/sts v1.42.1/go.mod h1:mTNxImtovCOEEuD65mKW7DCsL+2gjEH+RPEAexAzAio=
github.com/aws/smithy-go v1.25.1 h1:J8ERsGSU7d+aCmdQur5Txg6bVoYelvQJgtZehD12GkI=
github.com/aws/smithy-go v1.25.1/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=

View File

@@ -5,10 +5,12 @@ import (
"errors"
"os"
"path/filepath"
"reflect"
"slices"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"gitea.maximumdirect.net/eric/narratio/internal/adapters/scriptorium"
"gitea.maximumdirect.net/eric/narratio/internal/config"
"gitea.maximumdirect.net/eric/narratio/internal/manifest"
@@ -53,7 +55,7 @@ func TestExecuteStagesRetriesFromLastAnalyzeCheckpoint(t *testing.T) {
}); !errors.Is(err, failure) {
t.Fatalf("executeStages() error = %v, want quest-log failure", err)
}
if want := []string{"chronicle-revised", "encounters-revised", "quest-log-revised"}; !reflect.DeepEqual(failingRunner.Calls, want) {
if want := []string{"chronicle-revised", "encounters-revised", "quest-log-revised"}; !slices.Equal(failingRunner.Calls, want) {
t.Fatalf("failed-run calls = %#v, want %#v", failingRunner.Calls, want)
}
@@ -66,8 +68,9 @@ func TestExecuteStagesRetriesFromLastAnalyzeCheckpoint(t *testing.T) {
assertAnalyzeArtifactStatus(t, failedAnalyze, "encounters", manifest.AnalyzeArtifactCurrent)
assertAnalyzeArtifactStatus(t, failedAnalyze, "quest_log", manifest.AnalyzeArtifactFailed)
assertAnalyzeArtifactStatus(t, failedAnalyze, "player_handout", manifest.AnalyzeArtifactStale)
if got := failedAnalyze.AnalyzeArtifacts["unrelated"]; !reflect.DeepEqual(got, unrelated) {
t.Fatalf("unrelated artifact = %#v, want preserved %#v", got, unrelated)
gotUnrelated := failedAnalyze.AnalyzeArtifacts["unrelated"]
if diff := cmp.Diff(unrelated, gotUnrelated); diff != "" {
t.Fatalf("unrelated artifact changed (-want +got):\n%s", diff)
}
for _, name := range []string{"publish", "notify"} {
if got := failed.Stages[name]; got == nil || got.Status != manifest.StatusStale {
@@ -82,7 +85,7 @@ func TestExecuteStagesRetriesFromLastAnalyzeCheckpoint(t *testing.T) {
}); err != nil {
t.Fatalf("retry executeStages() error = %v", err)
}
if want := []string{"quest-log-revised", "player-handout-revised"}; !reflect.DeepEqual(retryRunner.Calls, want) {
if want := []string{"quest-log-revised", "player-handout-revised"}; !slices.Equal(retryRunner.Calls, want) {
t.Fatalf("retry calls = %#v, want %#v", retryRunner.Calls, want)
}
@@ -148,7 +151,7 @@ func TestExecuteStagesPersistsAnalyzeCheckpointFailureRecovery(t *testing.T) {
if !errors.Is(err, checkpointErr) {
t.Fatalf("executeStages() error = %v, want checkpoint failure", err)
}
if want := []string{"chronicle", "encounters"}; !reflect.DeepEqual(runner.Calls, want) {
if want := []string{"chronicle", "encounters"}; !slices.Equal(runner.Calls, want) {
t.Fatalf("adapter calls = %#v, want %#v", runner.Calls, want)
}
@@ -322,8 +325,12 @@ func cloneStageRecords(m *manifest.Manifest, names ...string) map[string]manifes
func assertStageRecordsUnchanged(t *testing.T, m *manifest.Manifest, want map[string]manifest.StageRecord) {
t.Helper()
for name, expected := range want {
if got := m.Stages[name]; got == nil || !reflect.DeepEqual(*got, expected) {
t.Fatalf("upstream stage %q = %#v, want unchanged %#v", name, got, expected)
got := m.Stages[name]
if got == nil {
t.Fatalf("upstream stage %q is missing", name)
}
if diff := cmp.Diff(expected, *got); diff != "" {
t.Fatalf("upstream stage %q changed (-want +got):\n%s", name, diff)
}
}
}

View File

@@ -4,7 +4,7 @@ import (
"context"
"os"
"path/filepath"
"reflect"
"slices"
"strings"
"testing"
@@ -32,7 +32,7 @@ func TestRestorePlanDefaultScope(t *testing.T) {
got := actionRelPaths(plan.Actions)
want := []string{"artifacts/session_recap.md", "manifest.json", "transcripts/full.json"}
if !reflect.DeepEqual(got, want) {
if !slices.Equal(got, want) {
t.Fatalf("action local paths = %#v, want %#v", got, want)
}
if plan.DownloadCount != 3 || plan.SkipSameCount != 0 || plan.ConflictCount != 0 {
@@ -54,7 +54,7 @@ func TestRestorePlanIncludeAudio(t *testing.T) {
}
got := actionRelPaths(plan.Actions)
want := []string{"audio/alice.flac", "manifest.json"}
if !reflect.DeepEqual(got, want) {
if !slices.Equal(got, want) {
t.Fatalf("action local paths = %#v, want %#v", got, want)
}
}
@@ -112,7 +112,7 @@ func TestRestorePlanIncludesPreviousCacheByDefault(t *testing.T) {
got := actionRelPaths(plan.Actions)
want := []string{"manifest.json", "previous/artifacts/session_recap.md", "previous/manifest.json"}
if !reflect.DeepEqual(got, want) {
if !slices.Equal(got, want) {
t.Fatalf("action local paths = %#v, want %#v", got, want)
}
}
@@ -133,7 +133,7 @@ func TestRestorePlanIgnoresCurrentSessionArchivedPreviousCache(t *testing.T) {
got := actionRelPaths(plan.Actions)
want := []string{"manifest.json"}
if !reflect.DeepEqual(got, want) {
if !slices.Equal(got, want) {
t.Fatalf("action local paths = %#v, want %#v", got, want)
}
}
@@ -154,7 +154,7 @@ func TestRestorePlanMissingOptionalPreviousCacheSkipsArtifact(t *testing.T) {
got := actionRelPaths(plan.Actions)
want := []string{"manifest.json", "previous/manifest.json"}
if !reflect.DeepEqual(got, want) {
if !slices.Equal(got, want) {
t.Fatalf("action local paths = %#v, want %#v", got, want)
}
}

View File

@@ -5,6 +5,7 @@ import (
"path/filepath"
"reflect"
"runtime"
"slices"
"strings"
"testing"
)
@@ -31,7 +32,7 @@ workspace:
if err != nil {
t.Fatalf("splitPipelineCompositionEnvelope() error = %v", err)
}
if !reflect.DeepEqual(document.root, originalRoot) || !reflect.DeepEqual(document.sources, originalSources) {
if !reflect.DeepEqual(document.root, originalRoot) || !slices.Equal(document.sources, originalSources) {
t.Fatalf("splitPipelineCompositionEnvelope() mutated its input")
}
if compositionFieldIndex(body.root.fields, "composition") >= 0 {
@@ -40,7 +41,7 @@ workspace:
if compositionFieldIndex(body.root.fields, "workspace") < 0 {
t.Fatalf("pipeline body lost ordinary root fields")
}
if want := []string{"first.yml", "second.yml"}; !reflect.DeepEqual(envelope.imports, want) {
if want := []string{"first.yml", "second.yml"}; !slices.Equal(envelope.imports, want) {
t.Fatalf("imports = %#v, want %#v", envelope.imports, want)
}
if len(envelope.profiles) != 2 || envelope.profiles[0].name != "production" || envelope.profiles[1].name != "testing" {
@@ -126,7 +127,7 @@ storage:
t.Fatal("pipeline resolution metadata = nil")
}
wantSources := []string{absolutePath(t, rootPath), absolutePath(t, platformPath), absolutePath(t, artifactsPath)}
if !reflect.DeepEqual(cfg.resolution.sources, wantSources) || !reflect.DeepEqual(cfg.resolution.imports, wantSources[1:]) {
if !slices.Equal(cfg.resolution.sources, wantSources) || !slices.Equal(cfg.resolution.imports, wantSources[1:]) {
t.Fatalf("resolution sources=%#v imports=%#v, want %#v / %#v", cfg.resolution.sources, cfg.resolution.imports, wantSources, wantSources[1:])
}
assertPipelineFieldOwner(t, cfg, "whisperx.transcribe_url", absolutePath(t, rootPath))
@@ -424,7 +425,7 @@ func assertPipelineFieldOwner(t *testing.T, cfg *PipelineConfig, path, source st
}
for _, ownership := range cfg.resolution.ownership {
if ownership.path == path {
if !reflect.DeepEqual(ownership.sources, []string{source}) {
if !slices.Equal(ownership.sources, []string{source}) {
t.Fatalf("owner of %s = %#v, want %q", path, ownership.sources, source)
}
return

View File

@@ -3,9 +3,11 @@ package config
import (
"os"
"path/filepath"
"reflect"
"slices"
"strings"
"testing"
"github.com/google/go-cmp/cmp"
)
func TestLoadPipelineProfilesSelectDefaultAndExplicitOverlay(t *testing.T) {
@@ -45,7 +47,7 @@ whisperx:
t.Fatalf("default profile result = workspace=%q language=%q", production.Workspace.Root, production.WhisperX.Language)
}
assertSelectedPipelineProfile(t, production, "production", "default", productionPath)
if want := []string{absolutePath(t, rootPath), absolutePath(t, importPath), absolutePath(t, productionPath)}; !reflect.DeepEqual(production.resolution.sources, want) {
if want := []string{absolutePath(t, rootPath), absolutePath(t, importPath), absolutePath(t, productionPath)}; !slices.Equal(production.resolution.sources, want) {
t.Fatalf("default sources = %#v, want %#v", production.resolution.sources, want)
}
assertPipelineFieldOwner(t, production, "workspace.root", absolutePath(t, productionPath))
@@ -350,8 +352,8 @@ whisperx:
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(monolithicValues, composedValues) {
t.Fatalf("equivalent effective values differ:\nmonolithic=%#v\ncomposed=%#v", monolithicValues, composedValues)
if diff := cmp.Diff(monolithicValues, composedValues); diff != "" {
t.Fatalf("effective values differ (-monolithic +composed):\n%s", diff)
}
}

View File

@@ -2,11 +2,13 @@ package stage
import (
"path/filepath"
"reflect"
"slices"
"strings"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"gitea.maximumdirect.net/eric/narratio/internal/artifactmodel"
"gitea.maximumdirect.net/eric/narratio/internal/artifactpolicy"
"gitea.maximumdirect.net/eric/narratio/internal/config"
@@ -151,7 +153,7 @@ func TestPlanAnalyzeWorkSelectionAndDependencyClosure(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(plan.ExplicitTargets, tt.wantTargets) {
if !slices.Equal(plan.ExplicitTargets, tt.wantTargets) {
t.Fatalf("targets = %#v, want %#v", plan.ExplicitTargets, tt.wantTargets)
}
assertAnalyzePlanKeys(t, "execution", plan.ExecutionOrder, tt.wantExecution)
@@ -163,7 +165,7 @@ func TestPlanAnalyzeWorkSelectionAndDependencyClosure(t *testing.T) {
forced = append(forced, item.Key)
}
}
if !(len(forced) == 0 && len(tt.wantForced) == 0) && !reflect.DeepEqual(forced, tt.wantForced) {
if !(len(forced) == 0 && len(tt.wantForced) == 0) && !slices.Equal(forced, tt.wantForced) {
t.Fatalf("forced = %#v, want %#v", forced, tt.wantForced)
}
})
@@ -263,8 +265,16 @@ func TestPlanAnalyzeWorkStartingRecordsPreserveOnlyValidConfiguredAuthority(t *t
}
assertAnalyzePlanKeys(t, "invalidated", plan.Invalidated, []string{"invalid_unselected"})
assertAnalyzePlanKeys(t, "removed", plan.Removed, []string{"removed"})
if !reflect.DeepEqual(before["unrelated"], *stored["unrelated"]) || !reflect.DeepEqual(before["invalid_unselected"], *stored["invalid_unselected"]) {
t.Fatal("planning mutated stored records")
gotUnchanged := map[string]manifest.AnalyzeArtifactRecord{
"unrelated": *stored["unrelated"],
"invalid_unselected": *stored["invalid_unselected"],
}
wantUnchanged := map[string]manifest.AnalyzeArtifactRecord{
"unrelated": before["unrelated"],
"invalid_unselected": before["invalid_unselected"],
}
if diff := cmp.Diff(wantUnchanged, gotUnchanged); diff != "" {
t.Fatalf("planning mutated stored records (-before +after):\n%s", diff)
}
}
@@ -306,7 +316,7 @@ func TestPlanAnalyzeWorkOrderIsDeterministicAcrossMapInsertion(t *testing.T) {
t.Fatal(err)
}
got := analyzePlanKeys(plan.ExecutionOrder)
if !reflect.DeepEqual(got, want) {
if !slices.Equal(got, want) {
t.Fatalf("attempt %d order = %#v, want %#v", attempt, got, want)
}
}
@@ -350,7 +360,7 @@ func assertAnalyzePlanKeys(t *testing.T, label string, items []analyzePlanItem,
if len(got) == 0 && len(want) == 0 {
return
}
if !reflect.DeepEqual(got, want) {
if !slices.Equal(got, want) {
t.Fatalf("%s = %#v, want %#v", label, got, want)
}
}

View File

@@ -2,9 +2,10 @@ package stage
import (
"path/filepath"
"reflect"
"testing"
"github.com/google/go-cmp/cmp"
"gitea.maximumdirect.net/eric/narratio/internal/artifacts"
"gitea.maximumdirect.net/eric/narratio/internal/config"
"gitea.maximumdirect.net/eric/narratio/internal/manifest"
@@ -164,8 +165,11 @@ func TestReconcileAnalyzeArtifactsIsReadOnly(t *testing.T) {
if _, err := reconcileAnalyzeArtifacts(env.Config.Pipeline.Scriptorium, newAnalyzeIdentityExecution(t, env, m)); err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(before, m.Stages["analyze"].AnalyzeArtifacts) || !m.UpdatedAt.Equal(beforeUpdatedAt) {
t.Fatal("reconciliation mutated manifest state")
if diff := cmp.Diff(before, m.Stages["analyze"].AnalyzeArtifacts); diff != "" {
t.Fatalf("reconciliation mutated artifact state (-before +after):\n%s", diff)
}
if !m.UpdatedAt.Equal(beforeUpdatedAt) {
t.Fatalf("reconciliation changed manifest update time: before=%s after=%s", beforeUpdatedAt, m.UpdatedAt)
}
}

View File

@@ -1,11 +1,12 @@
package stage
import (
"reflect"
"strings"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"gitea.maximumdirect.net/eric/narratio/internal/artifacts"
)
@@ -56,8 +57,8 @@ func TestBuildPublishOutputPlanModelsEveryOutcome(t *testing.T) {
t.Fatalf("outcomes[%d] = %#v, want source %q status %q", index, plan.Outcomes[index], wantSources[index], wantStatuses[index])
}
}
if !reflect.DeepEqual(plan.Outcomes[1].Declaration, carried) {
t.Fatalf("carried declaration = %#v, want exact %#v", plan.Outcomes[1].Declaration, carried)
if diff := cmp.Diff(carried, plan.Outcomes[1].Declaration); diff != "" {
t.Fatalf("carried declaration differs (-want +got):\n%s", diff)
}
if plan.Outcomes[1].Destination != "artifacts/beta-old.md" || plan.Outcomes[1].OriginRun != "20260512T010203Z-11223344" {
t.Fatalf("carried outcome = %#v", plan.Outcomes[1])