From edb71cf172dabbac976ba9172be16930d1f698e1 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 15 Jun 2026 13:40:01 +0000 Subject: [PATCH] Enforce specifications for actionpins, workflow Add spec-derived tests for previously-uncovered README sections. actionpins: - TestSpec_Constants_ResolutionErrorType: documented constant values - TestSpec_Types_ResolutionFailure: documented struct fields - TestSpec_PublicAPI_RecordResolutionFailure: auditing callback behavior workflow: - TestSpec_Engine_GlobalRegistrySingleton: singleton thread-safety contract - TestSpec_Sandbox_Constants: SandboxTypeAWF/Default values - TestSpec_MCPScripts_Constants: MCPScriptsModeHTTP/Directory - TestSpec_ActionPinning_ActionMode: documents sha/tag/local vs dev/release/script/action spec mismatch All tests derived from README.md specifications, not implementation source. Co-Authored-By: Claude Opus 4.8 (1M context) --- pkg/actionpins/spec_test.go | 47 ++++++++++++++++++++++++++++++++ pkg/workflow/spec_test.go | 53 +++++++++++++++++++++++++++++++++++++ 2 files changed, 100 insertions(+) diff --git a/pkg/actionpins/spec_test.go b/pkg/actionpins/spec_test.go index dae4f990348..c8bec3e7572 100644 --- a/pkg/actionpins/spec_test.go +++ b/pkg/actionpins/spec_test.go @@ -348,6 +348,53 @@ func TestSpec_Types_ContainerPin(t *testing.T) { assert.Equal(t, "ghcr.io/some/image@sha256:abc123", pin.PinnedImage, "ContainerPin.PinnedImage field") } +// TestSpec_Constants_ResolutionErrorType validates the documented ResolutionErrorType constant values. +// Spec table: ResolutionErrorTypeDynamicResolutionFailed="dynamic_resolution_failed", +// ResolutionErrorTypePinNotFound="pin_not_found". +func TestSpec_Constants_ResolutionErrorType(t *testing.T) { + assert.Equal(t, "dynamic_resolution_failed", string(actionpins.ResolutionErrorTypeDynamicResolutionFailed), + "ResolutionErrorTypeDynamicResolutionFailed should equal the documented value") + assert.Equal(t, "pin_not_found", string(actionpins.ResolutionErrorTypePinNotFound), + "ResolutionErrorTypePinNotFound should equal the documented value") +} + +// TestSpec_Types_ResolutionFailure validates the documented ResolutionFailure type structure. +// Spec: "Captures an unresolved action-ref pinning event (repo, ref, error type)". +func TestSpec_Types_ResolutionFailure(t *testing.T) { + failure := actionpins.ResolutionFailure{ + Repo: "unknown/action", + Ref: "v1", + ErrorType: actionpins.ResolutionErrorTypePinNotFound, + } + assert.Equal(t, "unknown/action", failure.Repo, "ResolutionFailure.Repo field") + assert.Equal(t, "v1", failure.Ref, "ResolutionFailure.Ref field") + assert.Equal(t, actionpins.ResolutionErrorTypePinNotFound, failure.ErrorType, "ResolutionFailure.ErrorType field") +} + +// TestSpec_PublicAPI_RecordResolutionFailure validates the documented auditing behavior: +// PinContext.RecordResolutionFailure collects ResolutionFailure events for unresolved pins, +// classified with ResolutionErrorTypePinNotFound when no usable pin is found. +// Spec section "Auditing Resolution Failures". +func TestSpec_PublicAPI_RecordResolutionFailure(t *testing.T) { + var failures []actionpins.ResolutionFailure + ctx := &actionpins.PinContext{ + Warnings: make(map[string]bool), + RecordResolutionFailure: func(f actionpins.ResolutionFailure) { + failures = append(failures, f) + }, + } + + _, err := actionpins.ResolveActionPin("does-not-exist/unknown-action-xyzzy", "v1", ctx) + require.NoError(t, err, "ResolveActionPin should not error even when the pin is unresolved") + + require.Len(t, failures, 1, "RecordResolutionFailure should be invoked once for an unresolved pin") + assert.Equal(t, actionpins.ResolutionErrorTypePinNotFound, failures[0].ErrorType, + "unresolved pin with no resolver should be classified as pin_not_found") + assert.Equal(t, "does-not-exist/unknown-action-xyzzy", failures[0].Repo, + "recorded failure should carry the queried repo") + assert.Equal(t, "v1", failures[0].Ref, "recorded failure should carry the queried ref") +} + // 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/workflow/spec_test.go b/pkg/workflow/spec_test.go index 23cf7cbac49..fb1e03dfd4a 100644 --- a/pkg/workflow/spec_test.go +++ b/pkg/workflow/spec_test.go @@ -294,3 +294,56 @@ func TestSpec_Engine_DocumentedEnginesRegistered(t *testing.T) { }) } } + +// TestSpec_Engine_GlobalRegistrySingleton validates the documented thread-safety contract that +// GetGlobalEngineRegistry returns a singleton initialized once at startup. +// Spec ("Thread Safety"): "The GetGlobalEngineRegistry() singleton is initialized once at startup +// and is safe for concurrent reads thereafter." +func TestSpec_Engine_GlobalRegistrySingleton(t *testing.T) { + first := workflow.GetGlobalEngineRegistry() + second := workflow.GetGlobalEngineRegistry() + require.NotNil(t, first, "GetGlobalEngineRegistry() must return a non-nil registry") + assert.Same(t, first, second, + "GetGlobalEngineRegistry() must return the same singleton instance on repeated calls") +} + +// TestSpec_Sandbox_Constants validates the documented SandboxType constant values. +// Spec ("Sandbox Constants"): SandboxTypeAWF = "awf", SandboxTypeDefault = "default" (alias for AWF). +func TestSpec_Sandbox_Constants(t *testing.T) { + assert.Equal(t, "awf", string(workflow.SandboxTypeAWF), + "SandboxTypeAWF must equal the documented value") + assert.Equal(t, "default", string(workflow.SandboxTypeDefault), + "SandboxTypeDefault must equal the documented value") +} + +// TestSpec_MCPScripts_Constants validates the documented MCP Scripts constants. +// Spec ("MCP Scripts Constants"): MCPScriptsModeHTTP = "http" is the only supported transport mode; +// MCPScriptsDirectory is the runtime directory where MCP scripts files are generated. +func TestSpec_MCPScripts_Constants(t *testing.T) { + assert.Equal(t, "http", workflow.MCPScriptsModeHTTP, + "MCPScriptsModeHTTP must be the documented http transport mode") + assert.NotEmpty(t, workflow.MCPScriptsDirectory, + "MCPScriptsDirectory must be a non-empty runtime directory path") +} + +// TestSpec_ActionPinning_ActionMode validates the documented ActionMode alias and DetectActionMode. +// Spec ("Action Pinning"): ActionMode is an "Action reference mode" with DetectActionMode detecting it. +// +// SPEC_MISMATCH: The README documents ActionMode values as `sha`, `tag`, `local`, but the +// implementation defines them as `dev`, `release`, `script`, and `action`. The README also +// describes DetectActionMode as detecting the "action reference mode" from a version string, but +// the implementation ignores the version parameter and instead detects from build/release context +// (the GH_AW_ACTION_MODE override, the release build flag, and GitHub Actions ref/event context). +// This test exercises the actual API and documents the divergence. +func TestSpec_ActionPinning_ActionMode(t *testing.T) { + // SPEC_MISMATCH: documented values (sha/tag/local) do not exist; the real values are these. + assert.Equal(t, "dev", string(workflow.ActionModeDev), "ActionModeDev value") + assert.Equal(t, "release", string(workflow.ActionModeRelease), "ActionModeRelease value") + + // DetectActionMode honors the GH_AW_ACTION_MODE override deterministically, independent of the + // (unused) version argument. + t.Setenv("GH_AW_ACTION_MODE", string(workflow.ActionModeRelease)) + mode := workflow.DetectActionMode("ignored-version") + assert.Equal(t, workflow.ActionModeRelease, mode, + "DetectActionMode must honor the GH_AW_ACTION_MODE override regardless of the version arg") +}