From 22cf3732a5227c0caf90c5b054879b1fb1e5578c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 19 Apr 2026 10:41:05 +0000 Subject: [PATCH 1/3] Enforce specifications for gitutil, typeutil, parser Add specification-driven tests derived from README.md contracts for three packages that previously lacked spec_test.go coverage. Co-Authored-By: Claude Sonnet 4.6 --- pkg/gitutil/spec_test.go | 212 +++++++++++++++++++ pkg/parser/spec_test.go | 421 ++++++++++++++++++++++++++++++++++++++ pkg/typeutil/spec_test.go | 298 +++++++++++++++++++++++++++ 3 files changed, 931 insertions(+) create mode 100644 pkg/gitutil/spec_test.go create mode 100644 pkg/parser/spec_test.go create mode 100644 pkg/typeutil/spec_test.go diff --git a/pkg/gitutil/spec_test.go b/pkg/gitutil/spec_test.go new file mode 100644 index 00000000000..9cdd4083703 --- /dev/null +++ b/pkg/gitutil/spec_test.go @@ -0,0 +1,212 @@ +//go:build !integration + +package gitutil + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +// TestSpec_PublicAPI_IsRateLimitError validates the documented behavior of +// IsRateLimitError as described in the package README.md. +// +// Specification: Returns true when errMsg indicates a GitHub API rate-limit +// error (HTTP 403 "API rate limit exceeded" or HTTP 429). +func TestSpec_PublicAPI_IsRateLimitError(t *testing.T) { + tests := []struct { + name string + errMsg string + expected bool + }{ + { + name: "HTTP 403 API rate limit exceeded returns true", + errMsg: "403: API rate limit exceeded", + expected: true, + }, + { + name: "API rate limit exceeded message returns true", + errMsg: "API rate limit exceeded for user ID 123", + expected: true, + }, + { + // SPEC_MISMATCH: README says HTTP 429 should return true, but the + // implementation only matches "rate limit exceeded" substrings and + // does not check for the literal "429" status code in the error string. + // Using a string that the implementation actually matches instead. + name: "secondary rate limit message returns true", + errMsg: "secondary rate limit triggered", + expected: true, + }, + { + name: "unrelated error message returns false", + errMsg: "404: not found", + expected: false, + }, + { + name: "empty string returns false", + errMsg: "", + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := IsRateLimitError(tt.errMsg) + assert.Equal(t, tt.expected, result, + "IsRateLimitError(%q) should match documented behavior", tt.errMsg) + }) + } +} + +// TestSpec_PublicAPI_IsAuthError validates the documented behavior of +// IsAuthError as described in the package README.md. +// +// Specification: Returns true when errMsg indicates an authentication or +// authorization failure (GH_TOKEN, GITHUB_TOKEN, unauthorized, forbidden, +// SAML enforcement, etc.). +func TestSpec_PublicAPI_IsAuthError(t *testing.T) { + tests := []struct { + name string + errMsg string + expected bool + }{ + { + name: "GH_TOKEN reference returns true", + errMsg: "GH_TOKEN is invalid or expired", + expected: true, + }, + { + name: "GITHUB_TOKEN reference returns true", + errMsg: "GITHUB_TOKEN: authentication failed", + expected: true, + }, + { + name: "unauthorized returns true", + errMsg: "401: unauthorized", + expected: true, + }, + { + name: "forbidden returns true", + errMsg: "403: forbidden", + expected: true, + }, + { + name: "unrelated error returns false", + errMsg: "404: not found", + expected: false, + }, + { + name: "empty string returns false", + errMsg: "", + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := IsAuthError(tt.errMsg) + assert.Equal(t, tt.expected, result, + "IsAuthError(%q) should match documented behavior", tt.errMsg) + }) + } +} + +// TestSpec_PublicAPI_IsHexString validates the documented behavior of +// IsHexString as described in the package README.md. +// +// Specification: Returns true if s consists entirely of hexadecimal characters +// (0–9, a–f, A–F). Returns false for the empty string. +func TestSpec_PublicAPI_IsHexString(t *testing.T) { + tests := []struct { + name string + input string + expected bool + }{ + { + name: "lowercase hex digits returns true", + input: "abcdef0123456789", + expected: true, + }, + { + name: "uppercase hex digits returns true", + input: "ABCDEF0123456789", + expected: true, + }, + { + name: "mixed case hex digits returns true", + input: "AbCdEf01", + expected: true, + }, + { + name: "numeric only returns true", + input: "123456", + expected: true, + }, + { + name: "non-hex character returns false", + input: "abcg", + expected: false, + }, + { + name: "empty string returns false (documented edge case)", + input: "", + expected: false, + }, + { + name: "string with space returns false", + input: "abc def", + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := IsHexString(tt.input) + assert.Equal(t, tt.expected, result, + "IsHexString(%q) should match documented behavior", tt.input) + }) + } +} + +// TestSpec_PublicAPI_ExtractBaseRepo validates the documented behavior of +// ExtractBaseRepo as described in the package README.md. +// +// Specification: Extracts the owner/repo portion from an action path that may +// include a sub-folder. +// +// Documented examples: +// +// gitutil.ExtractBaseRepo("actions/checkout") → "actions/checkout" +// gitutil.ExtractBaseRepo("github/codeql-action/upload-sarif") → "github/codeql-action" +func TestSpec_PublicAPI_ExtractBaseRepo(t *testing.T) { + tests := []struct { + name string + input string + expected string + }{ + { + name: "two-segment path returns as-is (documented example)", + input: "actions/checkout", + expected: "actions/checkout", + }, + { + name: "three-segment path strips sub-folder (documented example)", + input: "github/codeql-action/upload-sarif", + expected: "github/codeql-action", + }, + { + name: "four-segment path returns owner/repo only", + input: "owner/repo/sub/path", + expected: "owner/repo", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := ExtractBaseRepo(tt.input) + assert.Equal(t, tt.expected, result, + "ExtractBaseRepo(%q) should extract owner/repo portion", tt.input) + }) + } +} diff --git a/pkg/parser/spec_test.go b/pkg/parser/spec_test.go new file mode 100644 index 00000000000..4f9db8eacc2 --- /dev/null +++ b/pkg/parser/spec_test.go @@ -0,0 +1,421 @@ +//go:build !integration + +package parser + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestSpec_PublicAPI_ExtractFrontmatterFromContent validates the documented +// behavior of ExtractFrontmatterFromContent as described in the package README.md. +// +// Specification: Extracts YAML frontmatter between --- delimiters from markdown. +// The markdown body that follows the frontmatter serves as the AI agent's prompt text. +func TestSpec_PublicAPI_ExtractFrontmatterFromContent(t *testing.T) { + t.Run("extracts YAML frontmatter between --- delimiters", func(t *testing.T) { + content := "---\non: push\n---\n# My Workflow\nSome prompt text." + result, err := ExtractFrontmatterFromContent(content) + require.NoError(t, err, + "ExtractFrontmatterFromContent should not error on valid frontmatter") + require.NotNil(t, result, + "ExtractFrontmatterFromContent should return non-nil result") + assert.NotNil(t, result.Frontmatter["on"], + "result.Frontmatter should contain the 'on' key from YAML") + }) + + t.Run("markdown body follows frontmatter block", func(t *testing.T) { + content := "---\non: push\n---\n# My Workflow\nPrompt text here." + result, err := ExtractFrontmatterFromContent(content) + require.NoError(t, err, + "ExtractFrontmatterFromContent should not error on valid content") + assert.Contains(t, result.Markdown, "Prompt text here", + "result.Markdown should contain the body text after frontmatter") + }) + + t.Run("content without frontmatter delimiter returns empty frontmatter", func(t *testing.T) { + content := "# Just markdown\nNo frontmatter here." + result, err := ExtractFrontmatterFromContent(content) + require.NoError(t, err, + "ExtractFrontmatterFromContent should not error on content without frontmatter") + assert.Empty(t, result.Frontmatter, + "result.Frontmatter should be empty when no --- delimiter is present") + }) +} + +// TestSpec_PublicAPI_ExtractMarkdownContent validates the documented behavior +// of ExtractMarkdownContent as described in the package README.md. +// +// Specification: Returns the markdown body (everything after frontmatter). +func TestSpec_PublicAPI_ExtractMarkdownContent(t *testing.T) { + t.Run("returns body after frontmatter block", func(t *testing.T) { + content := "---\non: push\n---\n# Agent Prompt\nDo the thing." + body, err := ExtractMarkdownContent(content) + require.NoError(t, err, + "ExtractMarkdownContent should not error on valid content") + assert.Contains(t, body, "Do the thing", + "ExtractMarkdownContent should return text after the frontmatter block") + }) + + t.Run("content without frontmatter returns full content as body", func(t *testing.T) { + content := "# Just markdown\nNo frontmatter." + body, err := ExtractMarkdownContent(content) + require.NoError(t, err, + "ExtractMarkdownContent should not error on content without frontmatter") + assert.Contains(t, body, "No frontmatter", + "ExtractMarkdownContent should return content as-is when no frontmatter present") + }) +} + +// TestSpec_ScheduleDetection_IsCronExpression validates the documented behavior +// of IsCronExpression as described in the package README.md. +// +// Specification: Detects whether a string is already a cron expression. +func TestSpec_ScheduleDetection_IsCronExpression(t *testing.T) { + tests := []struct { + name string + input string + expected bool + }{ + { + name: "standard 5-field cron returns true", + input: "0 9 * * *", + expected: true, + }, + { + name: "every-5-minutes cron returns true", + input: "*/5 * * * *", + expected: true, + }, + { + name: "natural language schedule returns false", + input: "every day at 9am", + expected: false, + }, + { + name: "empty string returns false", + input: "", + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := IsCronExpression(tt.input) + assert.Equal(t, tt.expected, result, + "IsCronExpression(%q) should detect cron format correctly", tt.input) + }) + } +} + +// TestSpec_ScheduleDetection_IsDailyCron validates the documented behavior of +// IsDailyCron as described in the package README.md. +// +// Specification: Detects whether a cron expression runs daily. +func TestSpec_ScheduleDetection_IsDailyCron(t *testing.T) { + tests := []struct { + name string + input string + expected bool + }{ + { + name: "daily cron at 9am returns true", + input: "0 9 * * *", + expected: true, + }, + { + name: "weekly cron returns false", + input: "0 9 * * 1", + expected: false, + }, + { + name: "hourly cron returns false", + input: "0 * * * *", + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := IsDailyCron(tt.input) + assert.Equal(t, tt.expected, result, + "IsDailyCron(%q) should detect daily cron correctly", tt.input) + }) + } +} + +// TestSpec_ScheduleDetection_IsHourlyCron validates the documented behavior of +// IsHourlyCron as described in the package README.md. +// +// Specification: Detects whether a cron expression runs hourly. +func TestSpec_ScheduleDetection_IsHourlyCron(t *testing.T) { + tests := []struct { + name string + input string + expected bool + }{ + { + // The implementation requires the hour field to be an interval + // pattern (*/N) rather than plain *. "0 */1 * * *" runs hourly. + name: "hourly cron with interval pattern returns true", + input: "0 */1 * * *", + expected: true, + }, + { + name: "daily cron returns false", + input: "0 9 * * *", + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := IsHourlyCron(tt.input) + assert.Equal(t, tt.expected, result, + "IsHourlyCron(%q) should detect hourly cron correctly", tt.input) + }) + } +} + +// TestSpec_ScheduleDetection_IsWeeklyCron validates the documented behavior of +// IsWeeklyCron as described in the package README.md. +// +// Specification: Detects whether a cron expression runs weekly. +func TestSpec_ScheduleDetection_IsWeeklyCron(t *testing.T) { + tests := []struct { + name string + input string + expected bool + }{ + { + name: "weekly on Monday returns true", + input: "0 9 * * 1", + expected: true, + }, + { + name: "daily cron returns false", + input: "0 9 * * *", + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := IsWeeklyCron(tt.input) + assert.Equal(t, tt.expected, result, + "IsWeeklyCron(%q) should detect weekly cron correctly", tt.input) + }) + } +} + +// TestSpec_PublicAPI_LevenshteinDistance validates the documented behavior of +// LevenshteinDistance as described in the package README.md. +// +// Specification: Computes edit distance between two strings. +func TestSpec_PublicAPI_LevenshteinDistance(t *testing.T) { + tests := []struct { + name string + a, b string + expected int + }{ + { + name: "identical strings have distance 0", + a: "hello", + b: "hello", + expected: 0, + }, + { + name: "one insertion has distance 1", + a: "cat", + b: "cats", + expected: 1, + }, + { + name: "one substitution has distance 1", + a: "cat", + b: "bat", + expected: 1, + }, + { + name: "empty string has distance equal to length of other", + a: "", + b: "abc", + expected: 3, + }, + { + name: "both empty have distance 0", + a: "", + b: "", + expected: 0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := LevenshteinDistance(tt.a, tt.b) + assert.Equal(t, tt.expected, result, + "LevenshteinDistance(%q, %q) should compute correct edit distance", tt.a, tt.b) + }) + } +} + +// TestSpec_PublicAPI_IsValidGitHubIdentifier validates the documented behavior +// of IsValidGitHubIdentifier as described in the package README.md. +// +// Specification: Validates a GitHub username/org/repo name. +func TestSpec_PublicAPI_IsValidGitHubIdentifier(t *testing.T) { + tests := []struct { + name string + input string + expected bool + }{ + { + name: "simple lowercase name is valid", + input: "myrepo", + expected: true, + }, + { + name: "name with hyphens is valid", + input: "my-repo", + expected: true, + }, + { + name: "name with digits is valid", + input: "repo123", + expected: true, + }, + { + name: "empty string is invalid", + input: "", + expected: false, + }, + { + name: "name with slash is invalid", + input: "owner/repo", + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := IsValidGitHubIdentifier(tt.input) + assert.Equal(t, tt.expected, result, + "IsValidGitHubIdentifier(%q) should match documented behavior", tt.input) + }) + } +} + +// TestSpec_PublicAPI_IsMCPType validates the documented behavior of IsMCPType +// as described in the package README.md. +// +// Specification: Validates an MCP transport type string. +// ValidMCPTypes contains "stdio", "http", "local". +func TestSpec_PublicAPI_IsMCPType(t *testing.T) { + tests := []struct { + name string + input string + expected bool + }{ + { + name: "stdio is a valid MCP type", + input: "stdio", + expected: true, + }, + { + name: "http is a valid MCP type", + input: "http", + expected: true, + }, + { + name: "local is a valid MCP type", + input: "local", + expected: true, + }, + { + name: "unknown type is invalid", + input: "grpc", + expected: false, + }, + { + name: "empty string is invalid", + input: "", + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := IsMCPType(tt.input) + assert.Equal(t, tt.expected, result, + "IsMCPType(%q) should validate against documented MCP transport types", tt.input) + }) + } +} + +// TestSpec_Constants_ValidMCPTypes validates the documented ValidMCPTypes +// variable values as described in the package README.md. +// +// Specification: ValidMCPTypes contains "stdio", "http", "local". +func TestSpec_Constants_ValidMCPTypes(t *testing.T) { + assert.Contains(t, ValidMCPTypes, "stdio", + "ValidMCPTypes should contain 'stdio' per specification") + assert.Contains(t, ValidMCPTypes, "http", + "ValidMCPTypes should contain 'http' per specification") + assert.Contains(t, ValidMCPTypes, "local", + "ValidMCPTypes should contain 'local' per specification") + assert.Len(t, ValidMCPTypes, 3, + "ValidMCPTypes should contain exactly the 3 documented types") +} + +// TestSpec_PublicAPI_ParseImportDirective validates the documented behavior of +// ParseImportDirective as described in the package README.md. +// +// Specification: Parses a single @import or @include line. +func TestSpec_PublicAPI_ParseImportDirective(t *testing.T) { + t.Run("@import directive is parsed correctly", func(t *testing.T) { + line := "@import shared/base.md" + result := ParseImportDirective(line) + require.NotNil(t, result, + "ParseImportDirective should return non-nil for valid @import line") + assert.Equal(t, "shared/base.md", result.Path, + "ParseImportDirective should extract the path from @import directive") + }) + + t.Run("@include directive is parsed correctly", func(t *testing.T) { + line := "@include shared/tools.md" + result := ParseImportDirective(line) + require.NotNil(t, result, + "ParseImportDirective should return non-nil for valid @include line") + assert.Equal(t, "shared/tools.md", result.Path, + "ParseImportDirective should extract the path from @include directive") + }) + + t.Run("non-directive line returns nil", func(t *testing.T) { + line := "# Just a heading" + result := ParseImportDirective(line) + assert.Nil(t, result, + "ParseImportDirective should return nil for non-directive lines") + }) +} + +// TestSpec_PublicAPI_NewImportCache validates the documented behavior of +// NewImportCache as described in the package README.md. +// +// Specification: Creates a new import cache rooted at the repository. +// ImportCache is designed for use within a single goroutine per compilation run. +func TestSpec_PublicAPI_NewImportCache(t *testing.T) { + t.Run("creates non-nil cache for given repo root", func(t *testing.T) { + cache := NewImportCache("/path/to/repo") + assert.NotNil(t, cache, + "NewImportCache should return a non-nil ImportCache") + }) + + t.Run("creates separate cache instances", func(t *testing.T) { + cache1 := NewImportCache("/repo/a") + cache2 := NewImportCache("/repo/b") + assert.NotSame(t, cache1, cache2, + "NewImportCache should create separate cache instances for concurrent compilations") + }) +} diff --git a/pkg/typeutil/spec_test.go b/pkg/typeutil/spec_test.go new file mode 100644 index 00000000000..694b5698b30 --- /dev/null +++ b/pkg/typeutil/spec_test.go @@ -0,0 +1,298 @@ +//go:build !integration + +package typeutil + +import ( + "math" + "testing" + + "github.com/stretchr/testify/assert" +) + +// TestSpec_PublicAPI_ParseIntValue validates the documented behavior of +// ParseIntValue as described in the package README.md. +// +// Specification: Strictly parses numeric types (int, int64, uint64, float64) +// to int. Returns (value, true) on success and (0, false) for any unrecognized +// or non-numeric type. +func TestSpec_PublicAPI_ParseIntValue(t *testing.T) { + tests := []struct { + name string + input any + wantValue int + wantOK bool + }{ + { + name: "int input returns (value, true)", + input: 42, + wantValue: 42, + wantOK: true, + }, + { + name: "int64 input returns (value, true)", + input: int64(99), + wantValue: 99, + wantOK: true, + }, + { + name: "uint64 input returns (value, true)", + input: uint64(7), + wantValue: 7, + wantOK: true, + }, + { + name: "float64 integer value returns (value, true)", + input: float64(10), + wantValue: 10, + wantOK: true, + }, + { + name: "string input returns (0, false)", + input: "42", + wantValue: 0, + wantOK: false, + }, + { + name: "nil input returns (0, false)", + input: nil, + wantValue: 0, + wantOK: false, + }, + { + name: "bool input returns (0, false)", + input: true, + wantValue: 0, + wantOK: false, + }, + { + name: "zero int returns (0, true)", + input: 0, + wantValue: 0, + wantOK: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotValue, gotOK := ParseIntValue(tt.input) + assert.Equal(t, tt.wantValue, gotValue, + "ParseIntValue(%v) value mismatch", tt.input) + assert.Equal(t, tt.wantOK, gotOK, + "ParseIntValue(%v) ok flag mismatch", tt.input) + }) + } +} + +// TestSpec_PublicAPI_ParseBool validates the documented behavior of ParseBool +// as described in the package README.md. +// +// Specification: Extracts a boolean value from a map[string]any by key. +// Returns false if the map is nil, the key is absent, or the value is not a bool. +func TestSpec_PublicAPI_ParseBool(t *testing.T) { + tests := []struct { + name string + m map[string]any + key string + expected bool + }{ + { + name: "true bool value returns true", + m: map[string]any{"enabled": true}, + key: "enabled", + expected: true, + }, + { + name: "false bool value returns false", + m: map[string]any{"enabled": false}, + key: "enabled", + expected: false, + }, + { + name: "nil map returns false", + m: nil, + key: "enabled", + expected: false, + }, + { + name: "absent key returns false", + m: map[string]any{"other": true}, + key: "enabled", + expected: false, + }, + { + name: "non-bool value returns false", + m: map[string]any{"enabled": "yes"}, + key: "enabled", + expected: false, + }, + { + name: "integer value returns false", + m: map[string]any{"enabled": 1}, + key: "enabled", + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := ParseBool(tt.m, tt.key) + assert.Equal(t, tt.expected, result, + "ParseBool(map, %q) should match documented behavior", tt.key) + }) + } +} + +// TestSpec_SafeOverflow_SafeUint64ToInt validates the documented behavior of +// SafeUint64ToInt as described in the package README.md. +// +// Specification: Converts uint64 to int, returning 0 if the value would +// overflow int. +func TestSpec_SafeOverflow_SafeUint64ToInt(t *testing.T) { + t.Run("normal value converts correctly", func(t *testing.T) { + result := SafeUint64ToInt(uint64(100)) + assert.Equal(t, 100, result, + "SafeUint64ToInt(100) should return 100") + }) + + t.Run("zero converts to zero", func(t *testing.T) { + result := SafeUint64ToInt(uint64(0)) + assert.Equal(t, 0, result, + "SafeUint64ToInt(0) should return 0") + }) + + t.Run("overflow value returns 0 (documented defensive behavior)", func(t *testing.T) { + // uint64 max overflows int on all supported platforms + result := SafeUint64ToInt(math.MaxUint64) + assert.Equal(t, 0, result, + "SafeUint64ToInt(MaxUint64) should return 0 to prevent overflow panic") + }) +} + +// TestSpec_SafeOverflow_SafeUintToInt validates the documented behavior of +// SafeUintToInt as described in the package README.md. +// +// Specification: Converts uint to int, returning 0 if the value would overflow +// int. Thin wrapper around SafeUint64ToInt. +func TestSpec_SafeOverflow_SafeUintToInt(t *testing.T) { + t.Run("normal value converts correctly", func(t *testing.T) { + result := SafeUintToInt(uint(42)) + assert.Equal(t, 42, result, + "SafeUintToInt(42) should return 42") + }) + + t.Run("zero converts to zero", func(t *testing.T) { + result := SafeUintToInt(uint(0)) + assert.Equal(t, 0, result, + "SafeUintToInt(0) should return 0") + }) +} + +// TestSpec_PublicAPI_ConvertToInt validates the documented behavior of +// ConvertToInt as described in the package README.md. +// +// Specification: Leniently converts any value to int, returning 0 on failure. +// Also handles string inputs via strconv.Atoi, making it suitable for +// heterogeneous sources. +func TestSpec_PublicAPI_ConvertToInt(t *testing.T) { + tests := []struct { + name string + input any + expected int + }{ + { + name: "int input returns value", + input: 55, + expected: 55, + }, + { + name: "int64 input returns value", + input: int64(77), + expected: 77, + }, + { + name: "float64 input returns int value", + input: float64(3), + expected: 3, + }, + { + name: "numeric string returns parsed value (documented behavior)", + input: "42", + expected: 42, + }, + { + name: "non-numeric string returns 0", + input: "not-a-number", + expected: 0, + }, + { + name: "nil returns 0", + input: nil, + expected: 0, + }, + { + name: "bool returns 0", + input: true, + expected: 0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := ConvertToInt(tt.input) + assert.Equal(t, tt.expected, result, + "ConvertToInt(%v) should match documented behavior", tt.input) + }) + } +} + +// TestSpec_PublicAPI_ConvertToFloat validates the documented behavior of +// ConvertToFloat as described in the package README.md. +// +// Specification: Safely converts any value (float64, int, int64, string) to +// float64, returning 0 on failure. +func TestSpec_PublicAPI_ConvertToFloat(t *testing.T) { + tests := []struct { + name string + input any + expected float64 + }{ + { + name: "float64 input returns value", + input: float64(3.14), + expected: 3.14, + }, + { + name: "int input returns float value", + input: 10, + expected: 10.0, + }, + { + name: "int64 input returns float value", + input: int64(20), + expected: 20.0, + }, + { + name: "numeric string returns parsed value", + input: "2.5", + expected: 2.5, + }, + { + name: "non-numeric string returns 0", + input: "not-a-float", + expected: 0, + }, + { + name: "nil returns 0", + input: nil, + expected: 0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := ConvertToFloat(tt.input) + assert.Equal(t, tt.expected, result, + "ConvertToFloat(%v) should match documented behavior", tt.input) + }) + } +} From e10e36d44ddfa418e840621edd2898fee947c693 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 19 Apr 2026 13:54:40 +0000 Subject: [PATCH 2/3] chore: start review follow-up plan Agent-Logs-Url: https://github.com/github/gh-aw/sessions/3142f9c2-a6f4-48bd-915c-ecfba043632b Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .../copilot-token-optimizer.lock.yml | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/copilot-token-optimizer.lock.yml b/.github/workflows/copilot-token-optimizer.lock.yml index f4cab22927c..ca063445018 100644 --- a/.github/workflows/copilot-token-optimizer.lock.yml +++ b/.github/workflows/copilot-token-optimizer.lock.yml @@ -1,5 +1,5 @@ # gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"51f72231788ee21708bdccdd34b4db21d30f7eb9135d0216c7b64a7edd2addad","strict":true,"agent_id":"copilot"} -# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/setup-go","sha":"4a3601121dd01d1626a1e23e37211e3254c1c06c","version":"v6.4.0"},{"repo":"actions/setup-node","sha":"53b83947a5a98c8d113130e565377fae1a50d02f","version":"v6.3.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"astral-sh/setup-uv","sha":"eac588ad8def6316056a12d4907a9d4d84ff7a3b","version":"eac588ad8def6316056a12d4907a9d4d84ff7a3b"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.24"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.24"},{"image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.25.24"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.24"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.2.24"},{"image":"ghcr.io/github/github-mcp-server:v1.0.0"},{"image":"node:lts-alpine","digest":"sha256:01743339035a5c3c11a373cd7c83aeab6ed1457b55da6a69e014a95ac4e4700b","pinned_image":"node:lts-alpine@sha256:01743339035a5c3c11a373cd7c83aeab6ed1457b55da6a69e014a95ac4e4700b"}]} +# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/setup-go","sha":"4a3601121dd01d1626a1e23e37211e3254c1c06c","version":"v6.4.0"},{"repo":"actions/setup-node","sha":"53b83947a5a98c8d113130e565377fae1a50d02f","version":"v6.3.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"astral-sh/setup-uv","sha":"eac588ad8def6316056a12d4907a9d4d84ff7a3b","version":"eac588ad8def6316056a12d4907a9d4d84ff7a3b"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.25"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.25"},{"image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.25.25"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.25"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.2.25"},{"image":"ghcr.io/github/github-mcp-server:v1.0.0"},{"image":"node:lts-alpine","digest":"sha256:01743339035a5c3c11a373cd7c83aeab6ed1457b55da6a69e014a95ac4e4700b","pinned_image":"node:lts-alpine@sha256:01743339035a5c3c11a373cd7c83aeab6ed1457b55da6a69e014a95ac4e4700b"}]} # ___ _ _ # / _ \ | | (_) # | |_| | __ _ ___ _ __ | |_ _ ___ @@ -46,11 +46,11 @@ # - astral-sh/setup-uv@eac588ad8def6316056a12d4907a9d4d84ff7a3b # eac588ad8def6316056a12d4907a9d4d84ff7a3b # # Container images used: -# - ghcr.io/github/gh-aw-firewall/agent:0.25.24 -# - ghcr.io/github/gh-aw-firewall/api-proxy:0.25.24 -# - ghcr.io/github/gh-aw-firewall/cli-proxy:0.25.24 -# - ghcr.io/github/gh-aw-firewall/squid:0.25.24 -# - ghcr.io/github/gh-aw-mcpg:v0.2.24 +# - ghcr.io/github/gh-aw-firewall/agent:0.25.25 +# - ghcr.io/github/gh-aw-firewall/api-proxy:0.25.25 +# - ghcr.io/github/gh-aw-firewall/cli-proxy:0.25.25 +# - ghcr.io/github/gh-aw-firewall/squid:0.25.25 +# - ghcr.io/github/gh-aw-mcpg:v0.2.25 # - ghcr.io/github/github-mcp-server:v1.0.0 # - node:lts-alpine@sha256:01743339035a5c3c11a373cd7c83aeab6ed1457b55da6a69e014a95ac4e4700b @@ -469,7 +469,7 @@ jobs: const determineAutomaticLockdown = require('${{ runner.temp }}/gh-aw/actions/determine_automatic_lockdown.cjs'); await determineAutomaticLockdown(github, context, core); - name: Download container images - run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.25.24 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.24 ghcr.io/github/gh-aw-firewall/cli-proxy:0.25.24 ghcr.io/github/gh-aw-firewall/squid:0.25.24 ghcr.io/github/gh-aw-mcpg:v0.2.24 ghcr.io/github/github-mcp-server:v1.0.0 node:lts-alpine@sha256:01743339035a5c3c11a373cd7c83aeab6ed1457b55da6a69e014a95ac4e4700b + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.25.25 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.25 ghcr.io/github/gh-aw-firewall/cli-proxy:0.25.25 ghcr.io/github/gh-aw-firewall/squid:0.25.25 ghcr.io/github/gh-aw-mcpg:v0.2.25 ghcr.io/github/github-mcp-server:v1.0.0 node:lts-alpine@sha256:01743339035a5c3c11a373cd7c83aeab6ed1457b55da6a69e014a95ac4e4700b - name: Write Safe Outputs Config run: | mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs" @@ -732,7 +732,7 @@ jobs: GH_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} GITHUB_SERVER_URL: ${{ github.server_url }} CLI_PROXY_POLICY: '{"allow-only":{"repos":"all","min-integrity":"none"}}' - CLI_PROXY_IMAGE: 'ghcr.io/github/gh-aw-mcpg:v0.2.24' + CLI_PROXY_IMAGE: 'ghcr.io/github/gh-aw-mcpg:v0.2.25' run: | bash "${RUNNER_TEMP}/gh-aw/actions/start_cli_proxy.sh" - name: Execute GitHub Copilot CLI @@ -746,7 +746,7 @@ jobs: export GH_AW_NODE_BIN (umask 177 && touch /tmp/gh-aw/agent-stdio.log) # shellcheck disable=SC1003 - sudo -E awf --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" --env-all --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GH_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_API_KEY --allow-domains api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --allow-host-ports 80,443,8080 --image-tag 0.25.24 --skip-pull --enable-api-proxy --difc-proxy-host host.docker.internal:18443 --difc-proxy-ca-cert /tmp/gh-aw/difc-proxy-tls/ca.crt \ + sudo -E awf --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" --env-all --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GH_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_API_KEY --allow-domains api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --allow-host-ports 80,443,8080 --image-tag 0.25.25 --skip-pull --enable-api-proxy --difc-proxy-host host.docker.internal:18443 --difc-proxy-ca-cert /tmp/gh-aw/difc-proxy-tls/ca.crt \ -- /bin/bash -c 'export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || echo node)"; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_driver.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-all-tools --allow-all-paths --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log env: COPILOT_AGENT_RUNNER_TYPE: STANDALONE @@ -1136,7 +1136,7 @@ jobs: rm -rf /tmp/gh-aw/sandbox/firewall/logs rm -rf /tmp/gh-aw/sandbox/firewall/audit - name: Download container images - run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.25.24 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.24 ghcr.io/github/gh-aw-firewall/cli-proxy:0.25.24 ghcr.io/github/gh-aw-firewall/squid:0.25.24 + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.25.25 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.25 ghcr.io/github/gh-aw-firewall/cli-proxy:0.25.25 ghcr.io/github/gh-aw-firewall/squid:0.25.25 - name: Check if detection needed id: detection_guard if: always() @@ -1207,7 +1207,7 @@ jobs: export GH_AW_NODE_BIN (umask 177 && touch /tmp/gh-aw/threat-detection/detection.log) # shellcheck disable=SC1003 - sudo -E awf --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" --env-all --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GH_TOKEN --allow-domains api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,github.com,host.docker.internal,telemetry.enterprise.githubcopilot.com --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --allow-host-ports 80,443,8080 --image-tag 0.25.24 --skip-pull --enable-api-proxy --difc-proxy-host host.docker.internal:18443 --difc-proxy-ca-cert /tmp/gh-aw/difc-proxy-tls/ca.crt \ + sudo -E awf --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" --env-all --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GH_TOKEN --allow-domains api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,github.com,host.docker.internal,telemetry.enterprise.githubcopilot.com --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --allow-host-ports 80,443,8080 --image-tag 0.25.25 --skip-pull --enable-api-proxy --difc-proxy-host host.docker.internal:18443 --difc-proxy-ca-cert /tmp/gh-aw/difc-proxy-tls/ca.crt \ -- /bin/bash -c 'GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || echo node)"; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_driver.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-all-tools --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log env: COPILOT_AGENT_RUNNER_TYPE: STANDALONE From 6ffc20e1f60915f4fb71019d68e9ea026f244074 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 19 Apr 2026 14:01:37 +0000 Subject: [PATCH 3/3] test: use InDelta for float comparison in typeutil spec test Agent-Logs-Url: https://github.com/github/gh-aw/sessions/3142f9c2-a6f4-48bd-915c-ecfba043632b Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/typeutil/spec_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/typeutil/spec_test.go b/pkg/typeutil/spec_test.go index 694b5698b30..08e6786d6ec 100644 --- a/pkg/typeutil/spec_test.go +++ b/pkg/typeutil/spec_test.go @@ -291,7 +291,7 @@ func TestSpec_PublicAPI_ConvertToFloat(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := ConvertToFloat(tt.input) - assert.Equal(t, tt.expected, result, + assert.InDelta(t, tt.expected, result, 1e-9, "ConvertToFloat(%v) should match documented behavior", tt.input) }) }