diff --git a/pkg/actionpins/spec_test.go b/pkg/actionpins/spec_test.go index 252531c752e..912e713fe09 100644 --- a/pkg/actionpins/spec_test.go +++ b/pkg/actionpins/spec_test.go @@ -256,6 +256,30 @@ func TestSpec_Types_ActionPinsData(t *testing.T) { assert.Equal(t, "actions/checkout", entry.Repo, "entry Repo should match") } +// TestSpec_PublicAPI_ResolveActionPin_EmbeddedMatch validates embedded-only pin resolution returns +// a formatted reference for a known repository. Spec: "Embedded-only lookup from bundled pin data" +func TestSpec_PublicAPI_ResolveActionPin_EmbeddedMatch(t *testing.T) { + known := "actions/checkout" + latestPin, ok := actionpins.GetActionPinByRepo(known) + require.True(t, ok, "prerequisite: known repo must be in embedded data") + + ctx := &actionpins.PinContext{StrictMode: false, Warnings: make(map[string]bool)} + result, err := actionpins.ResolveActionPin(known, latestPin.Version, ctx) + require.NoError(t, err, "embedded-only ResolveActionPin should not error for known pin") + assert.NotEmpty(t, result, "should return non-empty pinned reference for known embedded pin") + assert.Contains(t, result, latestPin.SHA, "resolved reference should contain the pin SHA") +} + +// TestSpec_PublicAPI_GetActionPins_SPEC_MISMATCH documents a spec-implementation gap. +// SPEC_MISMATCH: The README specifies GetActionPins() []ActionPin ("Returns all loaded pins") +// but this function is not implemented. Only GetActionPinsByRepo(repo string) is available. +// Proxy validation: verify embedded data is non-empty via the available API. +func TestSpec_PublicAPI_GetActionPins_SPEC_MISMATCH(t *testing.T) { + // SPEC_MISMATCH: GetActionPins() documented in README does not exist in the implementation. + pins := actionpins.GetActionPinsByRepo("actions/checkout") + assert.NotEmpty(t, pins, "embedded pin data should be non-empty (proxy for missing GetActionPins)") +} + // TestSpec_ThreadSafety_ConcurrentGetActionPinsByRepo validates that concurrent calls to GetActionPinsByRepo // are safe after initialization (sync.Once guarantee from the spec). func TestSpec_ThreadSafety_ConcurrentGetActionPinsByRepo(t *testing.T) { diff --git a/pkg/agentdrain/spec_test.go b/pkg/agentdrain/spec_test.go index fc7056085a9..8a8998a1918 100644 --- a/pkg/agentdrain/spec_test.go +++ b/pkg/agentdrain/spec_test.go @@ -444,6 +444,56 @@ func TestSpec_Types_AnomalyReport(t *testing.T) { assert.LessOrEqual(t, report.AnomalyScore, 1.0, "AnomalyReport.AnomalyScore should be in documented range [0, 1]") } +// TestSpec_PublicAPI_Coordinator_TrainEvent validates Coordinator.TrainEvent routes known-good +// events to the correct stage miner and returns a MatchResult. +// Spec: "Training phase — call for known-good events" on Coordinator +func TestSpec_PublicAPI_Coordinator_TrainEvent(t *testing.T) { + cfg := agentdrain.DefaultConfig() + stages := []string{"plan", "tool_call", "finish"} + coord, err := agentdrain.NewCoordinator(cfg, stages) + require.NoError(t, err) + + evt := agentdrain.AgentEvent{ + Stage: "plan", + Fields: map[string]string{"action": "start", "step": "1"}, + } + result, err := coord.TrainEvent(evt) + require.NoError(t, err, "Coordinator.TrainEvent should not error for valid event in registered stage") + assert.NotNil(t, result, "Coordinator.TrainEvent should return a non-nil MatchResult") + assert.Equal(t, "plan", result.Stage, "MatchResult.Stage should match the trained event stage") +} + +// TestSpec_PublicAPI_Coordinator_LoadDefaultWeights validates that LoadDefaultWeights +// does not error on a freshly constructed coordinator. +// Spec: "Call coord.LoadDefaultWeights() to initialize the coordinator with pre-trained cluster weights" +func TestSpec_PublicAPI_Coordinator_LoadDefaultWeights(t *testing.T) { + cfg := agentdrain.DefaultConfig() + stages := []string{"plan", "tool_call", "finish"} + coord, err := agentdrain.NewCoordinator(cfg, stages) + require.NoError(t, err) + + err = coord.LoadDefaultWeights() + require.NoError(t, err, "LoadDefaultWeights should not error (no-op when empty, loads otherwise)") +} + +// TestSpec_PublicAPI_Miner_ClusterCount_SPEC_MISMATCH documents a spec-implementation gap. +// SPEC_MISMATCH: The README shows miner.ClusterCount() as a method, but Miner only implements +// Clusters() []Cluster. Use len(miner.Clusters()) as the equivalent. +func TestSpec_PublicAPI_Miner_ClusterCount_SPEC_MISMATCH(t *testing.T) { + // SPEC_MISMATCH: ClusterCount() documented in README does not exist; use len(Clusters()). + cfg := agentdrain.DefaultConfig() + miner, err := agentdrain.NewMiner(cfg) + require.NoError(t, err) + + assert.Equal(t, 0, len(miner.Clusters()), "cluster count should be zero before training") + + evt := agentdrain.AgentEvent{Stage: "plan", Fields: map[string]string{"step": "init"}} + _, err = miner.TrainEvent(evt) + require.NoError(t, err) + + assert.Equal(t, 1, len(miner.Clusters()), "cluster count should be 1 after training one unique event") +} + // TestSpec_Types_Snapshot validates the documented Snapshot/SnapshotCluster type structures. // Spec: Snapshot{Config, Clusters []SnapshotCluster, NextID}, SnapshotCluster{ID, Template, Size, Stage}. func TestSpec_Types_Snapshot(t *testing.T) { diff --git a/pkg/cli/spec_test.go b/pkg/cli/spec_test.go index cfc4580b265..9a73eab94e4 100644 --- a/pkg/cli/spec_test.go +++ b/pkg/cli/spec_test.go @@ -1009,3 +1009,59 @@ func TestSpec_DesignDecision_StderrDiagnostics(t *testing.T) { names := ValidArtifactSetNames() assert.NotEmpty(t, names, "ValidArtifactSetNames returns data via return value, not stdout") } + +// TestSpec_PublicAPI_GetAllCodemods validates that GetAllCodemods returns at least one codemod +// with required fields populated. +// Spec: "Returns all available codemods" +func TestSpec_PublicAPI_GetAllCodemods(t *testing.T) { + codemods := GetAllCodemods() + require.NotEmpty(t, codemods, "GetAllCodemods should return at least one codemod") + for _, c := range codemods { + assert.NotEmpty(t, c.ID, "each Codemod should have a non-empty ID") + assert.NotEmpty(t, c.Name, "each Codemod should have a non-empty Name") + assert.NotEmpty(t, c.Description, "each Codemod should have a non-empty Description") + assert.NotNil(t, c.Apply, "each Codemod should have a non-nil Apply function") + } +} + +// TestSpec_PublicAPI_ResolveArtifactFilter validates that ResolveArtifactFilter expands +// artifact set aliases to concrete artifact names. +// Spec: "Expands artifact set aliases to concrete artifact names" +func TestSpec_PublicAPI_ResolveArtifactFilter(t *testing.T) { + t.Run("all returns nil meaning no filter applied", func(t *testing.T) { + result := ResolveArtifactFilter([]string{"all"}) + assert.Nil(t, result, "\"all\" should return nil (no filter — download all artifacts)") + }) + + t.Run("empty list returns nil meaning no filter applied", func(t *testing.T) { + result := ResolveArtifactFilter([]string{}) + assert.Nil(t, result, "empty input should return nil (no filter — download all artifacts)") + }) + + t.Run("non-all named set expands to concrete artifact list", func(t *testing.T) { + sets := ValidArtifactSetNames() + for _, s := range sets { + if s == "all" { + continue + } + result := ResolveArtifactFilter([]string{s}) + assert.NotNil(t, result, "artifact set %q should expand to a concrete list", s) + assert.NotEmpty(t, result, "artifact set %q should expand to at least one artifact name", s) + break + } + }) +} + +// TestSpec_PublicAPI_GroupRunsByWorkflow validates that a flat slice of runs is grouped by workflow name. +// Spec: "Groups a flat slice of runs by workflow name" +func TestSpec_PublicAPI_GroupRunsByWorkflow(t *testing.T) { + runs := []WorkflowRun{ + {WorkflowName: "workflow-a"}, + {WorkflowName: "workflow-b"}, + {WorkflowName: "workflow-a"}, + } + grouped := GroupRunsByWorkflow(runs) + assert.Len(t, grouped, 2, "should produce two groups for two distinct workflow names") + assert.Len(t, grouped["workflow-a"], 2, "workflow-a group should contain two runs") + assert.Len(t, grouped["workflow-b"], 1, "workflow-b group should contain one run") +}