Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/aw/cli-commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -250,13 +250,14 @@ Example file:

```yaml
default_max_ai_credits: "1000"
default_max_cache_misses: "5"
default_detection_max_ai_credits: "400"
default_max_turns: "12"
default_model_copilot: "gpt-5-mini"
default_model_codex: null # delete this variable
```

Recognized keys include `default_max_ai_credits`, `default_detection_max_ai_credits`, `default_max_daily_ai_credits`, `default_timeout_minutes`, `default_max_turns`, `default_detection_model`, `default_utc`, `default_model_copilot`, `default_model_claude`, `default_model_codex`. The compiler resolves model selection as `GH_AW_MODEL_*` → `GH_AW_DEFAULT_MODEL_*` → built-in engine fallback.
Recognized keys include `default_max_ai_credits`, `default_max_cache_misses`, `default_detection_max_ai_credits`, `default_max_daily_ai_credits`, `default_timeout_minutes`, `default_max_turns`, `default_detection_model`, `default_utc`, `default_model_copilot`, `default_model_claude`, `default_model_codex`. The compiler resolves model selection as `GH_AW_MODEL_*` → `GH_AW_DEFAULT_MODEL_*` → built-in engine fallback.

**MCP equivalent**: Not available — run from a local terminal.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Use these variables to set organization- or repository-wide defaults without edi
| Variable | Source | Purpose | Applies when |
| --- | --- | --- | --- |
| `GH_AW_DEFAULT_MAX_AI_CREDITS` | GitHub Actions `vars.*` at runtime | Default AWF `apiProxy.maxAiCredits` budget | `max-ai-credits` is not set in frontmatter or any imported workflow |
| `GH_AW_DEFAULT_MAX_CACHE_MISSES` | Compiler process environment | Default AWF `apiProxy.maxCacheMisses` guardrail | `max-cache-misses` is not set in frontmatter or any imported workflow |
| `GH_AW_DEFAULT_DETECTION_MAX_AI_CREDITS` | GitHub Actions `vars.*` at runtime | Default threat-detection AWF `apiProxy.maxAiCredits` budget | `safe-outputs.threat-detection.max-ai-credits` is not set |
| `GH_AW_DEFAULT_MAX_DAILY_AI_CREDITS` | GitHub Actions `vars.*` at runtime | Default `max-daily-ai-credits` guardrail threshold | `max-daily-ai-credits` is not set in frontmatter or any imported workflow |
| `GH_AW_DEFAULT_MAX_TURNS` | Compiler process environment | Default top-level `max-turns` | `max-turns` is not set in frontmatter and the selected engine supports max-turns |
Expand All @@ -24,7 +25,7 @@ Use these variables to set organization- or repository-wide defaults without edi

Use `gh aw env get` and `gh aw env update` to manage these
variables in batch at repo, org, or enterprise scope. The defaults file uses
`default_`-prefixed keys such as `default_max_ai_credits`, `default_detection_max_ai_credits`, `default_max_daily_ai_credits`, `default_timeout_minutes`,
`default_`-prefixed keys such as `default_max_ai_credits`, `default_max_cache_misses`, `default_detection_max_ai_credits`, `default_max_daily_ai_credits`, `default_timeout_minutes`,
`default_model_copilot`, and `default_utc`.

## Project Timezone
Expand Down Expand Up @@ -63,6 +64,14 @@ For max AI credits, precedence is:

The compiler emits `${{ vars.GH_AW_DEFAULT_MAX_AI_CREDITS || '1000' }}` in a runtime patch script when no frontmatter or imported value is set, so the organization variable is resolved at workflow run time by the GitHub Actions runner — not at compile time. A value of `-1` disables AWF budget steering at runtime. Positive values accept `K`/`M` suffixes such as `100M`.

For max cache misses, precedence is:

1. `max-cache-misses` in workflow frontmatter
2. `GH_AW_DEFAULT_MAX_CACHE_MISSES`
3. Built-in constant default: `5`

The compiler emits `apiProxy.maxCacheMisses` directly in the AWF config JSON. When `max-cache-misses` is omitted, the compiler reads `GH_AW_DEFAULT_MAX_CACHE_MISSES` from its process environment and falls back to `5` if the variable is unset or invalid.

For threat-detection max AI credits, precedence is:

1. `safe-outputs.threat-detection.max-ai-credits` in workflow frontmatter (compile-time literal)
Expand Down Expand Up @@ -139,6 +148,7 @@ Set compiler process defaults for timeout and max-turns:
```bash
export GH_AW_DEFAULT_TIMEOUT_MINUTES=30
export GH_AW_DEFAULT_MAX_TURNS=12
export GH_AW_DEFAULT_MAX_CACHE_MISSES=7
export GH_AW_DEFAULT_DETECTION_MODEL=gpt-5.5-mini
```

Expand Down
1 change: 1 addition & 0 deletions docs/src/content/docs/reference/environment-variables.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_CO
Use `gh aw env get` / `gh aw env update` to batch-manage
these `GH_AW_DEFAULT_*` variables at repo, org, or enterprise scope with
`default_`-prefixed YAML keys such as `default_max_ai_credits`,
`default_max_cache_misses`,
`default_detection_max_ai_credits`, `default_max_daily_ai_credits`, and `default_model_copilot`.

### Agent runs
Expand Down
3 changes: 3 additions & 0 deletions pkg/cli/env_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ const (

type defaultsFile struct {
DefaultMaxAICredits *string `yaml:"default_max_ai_credits"`
DefaultMaxCacheMisses *string `yaml:"default_max_cache_misses"`
DefaultDetectionMaxAICredits *string `yaml:"default_detection_max_ai_credits"`
DefaultMaxDailyAICredits *string `yaml:"default_max_daily_ai_credits"`
DefaultMaxTurns *string `yaml:"default_max_turns"`
Expand Down Expand Up @@ -94,6 +95,7 @@ func (e *defaultsGHError) Unwrap() error {

var defaultsBindings = []defaultsBinding{
{envName: compilerenv.DefaultMaxAICredits, fieldName: "default_max_ai_credits", get: func(f *defaultsFile) **string { return &f.DefaultMaxAICredits }},
{envName: compilerenv.DefaultMaxCacheMisses, fieldName: "default_max_cache_misses", get: func(f *defaultsFile) **string { return &f.DefaultMaxCacheMisses }},
{envName: compilerenv.DefaultDetectionMaxAICredits, fieldName: "default_detection_max_ai_credits", get: func(f *defaultsFile) **string { return &f.DefaultDetectionMaxAICredits }},
{envName: compilerenv.DefaultMaxDailyAICredits, fieldName: "default_max_daily_ai_credits", get: func(f *defaultsFile) **string { return &f.DefaultMaxDailyAICredits }},
{envName: compilerenv.DefaultMaxTurns, fieldName: "default_max_turns", get: func(f *defaultsFile) **string { return &f.DefaultMaxTurns }},
Expand Down Expand Up @@ -323,6 +325,7 @@ func defaultsValidateFile(file *defaultsFile) error {
}

validateNonZeroInt("default_max_ai_credits", file.DefaultMaxAICredits)
validatePositiveInt("default_max_cache_misses", file.DefaultMaxCacheMisses)
validateNonZeroInt("default_detection_max_ai_credits", file.DefaultDetectionMaxAICredits)
validateNonZeroInt("default_max_daily_ai_credits", file.DefaultMaxDailyAICredits)
validatePositiveInt("default_max_turns", file.DefaultMaxTurns)
Expand Down
6 changes: 6 additions & 0 deletions pkg/cli/env_command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ func TestResolveDefaultsTarget(t *testing.T) {
func TestDefaultsFileYAMLKeys(t *testing.T) {
file := defaultsFile{
DefaultMaxAICredits: new("1000"),
DefaultMaxCacheMisses: new("5"),
DefaultDetectionMaxAICredits: new("400"),
DefaultMaxDailyAICredits: new("500000"),
DefaultMaxTurns: new("42"),
Expand All @@ -99,6 +100,7 @@ func TestDefaultsFileYAMLKeys(t *testing.T) {

yml := string(data)
assert.Contains(t, yml, "default_max_ai_credits:")
assert.Contains(t, yml, "default_max_cache_misses:")
assert.Contains(t, yml, "default_detection_max_ai_credits:")
assert.Contains(t, yml, "default_max_daily_ai_credits:")
assert.Contains(t, yml, "default_max_turns:")
Expand Down Expand Up @@ -146,6 +148,7 @@ func TestDefaultsValidateFile(t *testing.T) {
t.Run("accepts valid values", func(t *testing.T) {
err := defaultsValidateFile(&defaultsFile{
DefaultMaxAICredits: new("1000"),
DefaultMaxCacheMisses: new("5"),
DefaultDetectionMaxAICredits: new("400"),
DefaultMaxDailyAICredits: new("500000"),
DefaultMaxTurns: new("12"),
Expand All @@ -169,6 +172,7 @@ func TestDefaultsValidateFile(t *testing.T) {
t.Run("rejects invalid numeric and empty model values", func(t *testing.T) {
err := defaultsValidateFile(&defaultsFile{
DefaultMaxAICredits: new("0"),
DefaultMaxCacheMisses: new("0"),
DefaultDetectionMaxAICredits: new("0"),
DefaultMaxDailyAICredits: new("0"),
DefaultMaxTurns: new("abc"),
Expand All @@ -178,6 +182,7 @@ func TestDefaultsValidateFile(t *testing.T) {
})
require.Error(t, err)
assert.Contains(t, err.Error(), "default_max_ai_credits must be a non-zero integer when set")
assert.Contains(t, err.Error(), "default_max_cache_misses must be a positive integer when set")
assert.Contains(t, err.Error(), "default_detection_max_ai_credits must be a non-zero integer when set")
assert.Contains(t, err.Error(), "default_max_daily_ai_credits must be a non-zero integer when set")
assert.Contains(t, err.Error(), "default_max_turns must be a positive integer when set")
Expand Down Expand Up @@ -212,6 +217,7 @@ func TestDefaultsBuildUpdateChanges(t *testing.T) {

for _, field := range []string{
"default_max_ai_credits",
"default_max_cache_misses",
"default_detection_max_ai_credits",
"default_max_daily_ai_credits",
"default_max_turns",
Expand Down
3 changes: 3 additions & 0 deletions pkg/constants/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,9 @@ const DefaultMaxDailyAICredits = "5000"
// DefaultMaxRuns is the default AWF invocation cap enforced by the AWF API proxy.
const DefaultMaxRuns = 500

// DefaultMaxCacheMisses is the default AWF consecutive cache-miss guardrail.
const DefaultMaxCacheMisses = 5

// MCPSessionTimeoutMin is the minimum allowed value for engine.mcp.session-timeout (5 minutes).
const MCPSessionTimeoutMin = 5 * time.Minute

Expand Down
3 changes: 3 additions & 0 deletions pkg/parser/import_field_extractor.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ type importAccumulator struct {
mergedMaxTurns string
mergedMaxToolDenials string
mergedMaxRuns string
mergedMaxCacheMisses string
mergedMaxAICredits string
mergedMaxDailyAICredits string
// Best-effort sub-agent frontmatter warnings collected during BFS traversal.
Expand Down Expand Up @@ -362,6 +363,7 @@ func (acc *importAccumulator) extractConfigFields(fm map[string]any, fullPath st
acc.extractFirstWinsJSONField(fm, fullPath, "max-turns", &acc.mergedMaxTurns)
acc.extractFirstWinsJSONField(fm, fullPath, "max-tool-denials", &acc.mergedMaxToolDenials)
acc.extractFirstWinsJSONField(fm, fullPath, "max-runs", &acc.mergedMaxRuns)
acc.extractFirstWinsJSONField(fm, fullPath, "max-cache-misses", &acc.mergedMaxCacheMisses)
acc.extractFirstWinsJSONField(fm, fullPath, "max-ai-credits", &acc.mergedMaxAICredits)
acc.extractFirstWinsJSONField(fm, fullPath, "max-daily-ai-credits", &acc.mergedMaxDailyAICredits)

Expand Down Expand Up @@ -751,6 +753,7 @@ func (acc *importAccumulator) toImportsResult(topologicalOrder []string) *Import
MergedMaxTurns: acc.mergedMaxTurns,
MergedMaxToolDenials: acc.mergedMaxToolDenials,
MergedMaxRuns: acc.mergedMaxRuns,
MergedMaxCacheMisses: acc.mergedMaxCacheMisses,
MergedMaxAICredits: acc.mergedMaxAICredits,
MergedMaxDailyAICredits: acc.mergedMaxDailyAICredits,
Warnings: acc.warnings,
Expand Down
3 changes: 3 additions & 0 deletions pkg/parser/import_field_extractor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -649,6 +649,7 @@ func TestExtractConfigFields_FirstWinsAndAccumulates(t *testing.T) {
"max-turns": 10,
"max-tool-denials": 5,
"max-runs": 3,
"max-cache-misses": 4,
"max-ai-credits": 1234,
"max-daily-ai-credits": 4096,
"mcp-servers": map[string]any{"server-a": map[string]any{"url": "https://a.example.com"}},
Expand All @@ -663,6 +664,7 @@ func TestExtractConfigFields_FirstWinsAndAccumulates(t *testing.T) {
"max-turns": 99,
"max-tool-denials": 11,
"max-runs": 88,
"max-cache-misses": 99,
"max-ai-credits": 55,
"max-daily-ai-credits": 66,
"mcp-servers": map[string]any{"server-b": map[string]any{"url": "https://b.example.com"}},
Expand All @@ -680,6 +682,7 @@ func TestExtractConfigFields_FirstWinsAndAccumulates(t *testing.T) {
assert.Equal(t, "10", acc.mergedMaxTurns, "max-turns should be first-wins")
assert.Equal(t, "5", acc.mergedMaxToolDenials, "max-tool-denials should be first-wins")
assert.Equal(t, "3", acc.mergedMaxRuns, "max-runs should be first-wins")
assert.Equal(t, "4", acc.mergedMaxCacheMisses, "max-cache-misses should be first-wins")
assert.Equal(t, "1234", acc.mergedMaxAICredits, "max-ai-credits should be first-wins")
assert.Equal(t, "4096", acc.mergedMaxDailyAICredits, "max-daily-ai-credits should be first-wins")

Expand Down
1 change: 1 addition & 0 deletions pkg/parser/import_processor.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ type ImportsResult struct {
MergedMaxTurns string // First max-turns value found across all imports (JSON-encoded, first-wins)
MergedMaxToolDenials string // First max-tool-denials value found across all imports (JSON-encoded, first-wins)
MergedMaxRuns string // First max-runs value found across all imports (JSON-encoded, first-wins)
MergedMaxCacheMisses string // First max-cache-misses value found across all imports (JSON-encoded, first-wins)
MergedMaxAICredits string // First max-ai-credits value found across all imports (JSON-encoded, first-wins)
MergedMaxDailyAICredits string // First max-daily-ai-credits value found across all imports (JSON-encoded, first-wins)
ImportedFiles []string // List of imported file paths (for manifest)
Expand Down
28 changes: 28 additions & 0 deletions pkg/parser/schema_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -635,6 +635,34 @@ func TestValidateMainWorkflowFrontmatterWithSchemaAndLocation_MaxAICreditsOtherN
}
}

func TestValidateMainWorkflowFrontmatterWithSchemaAndLocation_MaxCacheMissesPositiveAccepted(t *testing.T) {
t.Parallel()

validFrontmatter := map[string]any{
"on": "push",
"max-cache-misses": 5,
}

err := ValidateMainWorkflowFrontmatterWithSchemaAndLocation(validFrontmatter, "/tmp/gh-aw/max-cache-misses-positive-test.md")
if err != nil {
t.Fatalf("expected max-cache-misses=5 to pass schema validation, got: %v", err)
}
}

func TestValidateMainWorkflowFrontmatterWithSchemaAndLocation_MaxCacheMissesZeroRejected(t *testing.T) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/tdd] Tests cover max-cache-misses: 0 (rejected) and max-cache-misses: 5 (accepted), but not negative values. The schema defines minimum: 1, which should also reject -1, and that boundary isn't currently verified.

💡 Suggested test
func TestValidateMainWorkflowFrontmatterWithSchemaAndLocation_MaxCacheMissesNegativeRejected(t *testing.T) {
    t.Parallel()
    err := ValidateMainWorkflowFrontmatterWithSchemaAndLocation(map[string]any{
        "on":               "push",
        "max-cache-misses": -1,
    }, "/tmp/gh-aw/max-cache-misses-negative-test.md")
    if err == nil {
        t.Fatal("expected max-cache-misses=-1 to fail schema validation")
    }
}

Completing the three-point boundary coverage (negative / zero / positive) is a spec-readable guarantee that minimum: 1 is wired correctly.

t.Parallel()

invalidFrontmatter := map[string]any{
"on": "push",
"max-cache-misses": 0,
}

err := ValidateMainWorkflowFrontmatterWithSchemaAndLocation(invalidFrontmatter, "/tmp/gh-aw/max-cache-misses-zero-test.md")
if err == nil {
t.Fatal("expected max-cache-misses=0 to fail schema validation")
}
}

func TestValidateMainWorkflowFrontmatterWithSchemaAndLocation_MaxDailyAICreditsZeroInvalid(t *testing.T) {
t.Parallel()

Expand Down
6 changes: 6 additions & 0 deletions pkg/parser/schemas/main_workflow_schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -3712,6 +3712,12 @@
"default": 1000,
"description": "Per-run AI Credits budget control for firewall cost enforcement. Enabled by default at 1000 (1k) when omitted. Set to -1 to disable both budget enforcement and token steering. Supports GitHub Actions expressions."
},
"max-cache-misses": {
"type": "integer",
"minimum": 1,
"default": 5,
"description": "Maximum number of consecutive AWF cache misses allowed before the API proxy blocks further requests. Maps to `apiProxy.maxCacheMisses` and defaults to 5 when omitted."
},
"max-daily-ai-credits": {
"$ref": "#/$defs/max_daily_ai_credits_limit",
"description": "24-hour AI Credits guardrail for runs triggered by the same user. Omit the field to leave the guardrail disabled. Supports GitHub Actions expressions."
Expand Down
7 changes: 7 additions & 0 deletions pkg/workflow/awf_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ import (
"github.com/github/gh-aw/pkg/constants"
"github.com/github/gh-aw/pkg/jsonutil"
"github.com/github/gh-aw/pkg/logger"
"github.com/github/gh-aw/pkg/workflow/compilerenv"
"github.com/santhosh-tekuri/jsonschema/v6"
)

Expand Down Expand Up @@ -197,6 +198,9 @@ type AWFAPIProxyConfig struct {
// MaxRuns is the maximum number of LLM invocations allowed for a run.
MaxRuns int `json:"maxRuns,omitempty"`

// MaxCacheMisses is the maximum number of consecutive cache misses allowed for a run.
MaxCacheMisses int `json:"maxCacheMisses,omitempty"`

// MaxAICredits is the explicit per-run AI credits budget enforced by the API proxy.
MaxAICredits int64 `json:"maxAiCredits,omitempty"`

Expand Down Expand Up @@ -354,11 +358,13 @@ func BuildAWFConfigJSON(config AWFCommandConfig) (string, error) {
// BuildAWFCommand (see injectMaxAICreditsExpression in awf_helpers.go).
maxAICredits := int64(0)
maxRuns := constants.DefaultMaxRuns
maxCacheMisses := compilerenv.ResolveDefaultMaxCacheMisses(constants.DefaultMaxCacheMisses)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/improve-codebase-architecture] ResolveDefaultMaxCacheMisses is called unconditionally here, but its result is immediately overwritten by GetMaxCacheMisses() on line 367 whenever EngineConfig != nil. Since GetMaxCacheMisses() already handles a nil receiver, the pre-call is redundant.

💡 Simplification

Replace the pre-call + conditional block with a single nil-safe call:

var ec *EngineConfig
if config.WorkflowData != nil {
    ec = config.WorkflowData.EngineConfig
}
maxCacheMisses := ec.GetMaxCacheMisses()  // handles nil, env override, and built-in default

This matches the single-source-of-truth intent of GetMaxCacheMisses() and removes the double ResolveDefaultMaxCacheMisses call that happens today when EngineConfig != nil && MaxCacheMisses == 0.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant os.Getenv call: the pre-init at this line is overwritten immediately when EngineConfig != nil.

maxCacheMisses is assigned via ResolveDefaultMaxCacheMisses (which calls os.Getenv), then unconditionally overwritten by GetMaxCacheMisses() at line 367 — which also calls ResolveDefaultMaxCacheMisses internally when MaxCacheMisses <= 0. In the common non-nil EngineConfig / unset-field path, the env-var is read twice and the first result is thrown away.

Compare maxRuns: its pre-init is constants.DefaultMaxRuns (no syscall), deferring env-lookup entirely to GetMaxRuns(). maxCacheMisses should follow the same pattern.

💡 Suggested fix
// Before:
maxRuns := constants.DefaultMaxRuns
maxCacheMisses := compilerenv.ResolveDefaultMaxCacheMisses(constants.DefaultMaxCacheMisses)
if config.WorkflowData != nil && config.WorkflowData.EngineConfig != nil {
    maxRuns = config.WorkflowData.EngineConfig.GetMaxRuns()
    maxCacheMisses = config.WorkflowData.EngineConfig.GetMaxCacheMisses()
}

// After — GetMaxCacheMisses already handles the nil-receiver and fallback:
maxRuns := constants.DefaultMaxRuns
maxCacheMisses := 0
if config.WorkflowData != nil && config.WorkflowData.EngineConfig != nil {
    maxRuns = config.WorkflowData.EngineConfig.GetMaxRuns()
    maxCacheMisses = config.WorkflowData.EngineConfig.GetMaxCacheMisses()
} else {
    maxCacheMisses = compilerenv.ResolveDefaultMaxCacheMisses(constants.DefaultMaxCacheMisses)
}

Alternatively, since GetMaxCacheMisses() already handles e == nil, you can drop the guard entirely:

maxCacheMisses := (*EngineConfig)(nil).GetMaxCacheMisses()  // resolves env/built-in default via nil-safe method
if config.WorkflowData != nil && config.WorkflowData.EngineConfig != nil {
    maxCacheMisses = config.WorkflowData.EngineConfig.GetMaxCacheMisses()
}

if config.WorkflowData != nil && config.WorkflowData.EngineConfig != nil {
if config.WorkflowData.EngineConfig.MaxAICredits != 0 {
maxAICredits = config.WorkflowData.EngineConfig.MaxAICredits
}
maxRuns = config.WorkflowData.EngineConfig.GetMaxRuns()
maxCacheMisses = config.WorkflowData.EngineConfig.GetMaxCacheMisses()
}

// Token steering is enabled by default. Setting max-ai-credits to a negative
Expand All @@ -373,6 +379,7 @@ func BuildAWFConfigJSON(config AWFCommandConfig) (string, error) {
apiProxy := &AWFAPIProxyConfig{
Enabled: true,
MaxRuns: maxRuns,
MaxCacheMisses: maxCacheMisses,
MaxAICredits: maxAICredits,
EnableTokenSteering: enableTokenSteering && awfSupportsTokenSteering(firewallConfig),
}
Expand Down
61 changes: 61 additions & 0 deletions pkg/workflow/awf_config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ func TestBuildAWFConfigJSON(t *testing.T) {
assert.Contains(t, jsonStr, `"apiProxy"`, "should include apiProxy section")
assert.Contains(t, jsonStr, `"enabled":true`, "apiProxy should be enabled")
assert.Contains(t, jsonStr, fmt.Sprintf(`"maxRuns":%d`, constants.DefaultMaxRuns), "apiProxy should emit default maxRuns")
assert.Contains(t, jsonStr, fmt.Sprintf(`"maxCacheMisses":%d`, constants.DefaultMaxCacheMisses), "apiProxy should emit default maxCacheMisses")
assert.NotContains(t, jsonStr, `"maxEffectiveTokens"`, "apiProxy should omit maxEffectiveTokens when unset")

// container.imageTag
Expand Down Expand Up @@ -161,6 +162,66 @@ func TestBuildAWFConfigJSON(t *testing.T) {
assert.Contains(t, jsonStr, `"maxAiCredits":333`, "apiProxy should bake in frontmatter maxAiCredits (skipping runtime expression)")
})

t.Run("default max-cache-misses uses built-in default when unset", func(t *testing.T) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Environment-sensitive test will produce a false failure when GH_AW_DEFAULT_MAX_CACHE_MISSES is set in the runner: this subtest is named "default max-cache-misses uses built-in default when unset", but it never calls t.Setenv(compilerenv.DefaultMaxCacheMisses, "") to actually enforce the "unset" condition. If GH_AW_DEFAULT_MAX_CACHE_MISSES is present in the CI environment, ResolveDefaultMaxCacheMisses returns that value, BuildAWFConfigJSON emits it, and the assertion at line 181 ("maxCacheMisses":5) fails with a misleading error. The base fixture assertion at line 52 (fmt.Sprintf("maxCacheMisses:%d", constants.DefaultMaxCacheMisses)) has the same gap.

💡 Suggested fix

Add at the top of this subtest (and at the top of the base fixture subtest):

t.Setenv(compilerenv.DefaultMaxCacheMisses, "")  // guarantee the built-in default path

The sibling test "enterprise default max-cache-misses env var overrides built-in default" already calls t.Setenv(compilerenv.DefaultMaxCacheMisses, "9") correctly. The "unset" case needs the symmetric clearing call to actually test what its name claims.

config := AWFCommandConfig{
EngineName: "copilot",
AllowedDomains: "github.com",
WorkflowData: &WorkflowData{
EngineConfig: &EngineConfig{
ID: "copilot",
},
NetworkPermissions: &NetworkPermissions{
Firewall: &FirewallConfig{Enabled: true},
},
},
}

jsonStr, err := BuildAWFConfigJSON(config)
require.NoError(t, err)
assert.Contains(t, jsonStr, `"maxCacheMisses":5`, "apiProxy should emit built-in maxCacheMisses default when unset")
})

t.Run("enterprise default max-cache-misses env var overrides built-in default", func(t *testing.T) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/tdd] All three new max-cache-misses tests in BuildAWFConfigJSON use WorkflowData with a non-nil EngineConfig. The initial compilerenv.ResolveDefaultMaxCacheMisses call in BuildAWFConfigJSON is only the effective value when WorkflowData == nil or EngineConfig == nil — those paths aren't exercised for cache misses.

💡 Suggested test
t.Run("env default max-cache-misses applied when WorkflowData is nil", func(t *testing.T) {
    t.Setenv(compilerenv.DefaultMaxCacheMisses, "11")
    config := AWFCommandConfig{
        EngineName:     "copilot",
        AllowedDomains: "github.com",
        WorkflowData:   nil,
    }
    jsonStr, err := BuildAWFConfigJSON(config)
    require.NoError(t, err)
    assert.Contains(t, jsonStr, `"maxCacheMisses":11`, "should fall back to env default when WorkflowData is nil")
})

This pins the WorkflowData == nil branch and verifies the pre-call at line 361 actually does useful work.

t.Setenv(compilerenv.DefaultMaxCacheMisses, "9")
config := AWFCommandConfig{
EngineName: "copilot",
AllowedDomains: "github.com",
WorkflowData: &WorkflowData{
EngineConfig: &EngineConfig{
ID: "copilot",
},
NetworkPermissions: &NetworkPermissions{
Firewall: &FirewallConfig{Enabled: true},
},
},
}

jsonStr, err := BuildAWFConfigJSON(config)
require.NoError(t, err)
assert.Contains(t, jsonStr, `"maxCacheMisses":9`, "apiProxy should emit env-managed default maxCacheMisses when frontmatter is unset")
})

t.Run("frontmatter max-cache-misses takes precedence over env default", func(t *testing.T) {
t.Setenv(compilerenv.DefaultMaxCacheMisses, "9")
config := AWFCommandConfig{
EngineName: "copilot",
AllowedDomains: "github.com",
WorkflowData: &WorkflowData{
EngineConfig: &EngineConfig{
ID: "copilot",
MaxCacheMisses: 3,
},
NetworkPermissions: &NetworkPermissions{
Firewall: &FirewallConfig{Enabled: true},
},
},
}

jsonStr, err := BuildAWFConfigJSON(config)
require.NoError(t, err)
assert.Contains(t, jsonStr, `"maxCacheMisses":3`, "apiProxy should emit frontmatter maxCacheMisses ahead of env default")
})

// T-AIC-PR-007: Imported workflow max-ai-credits baked into AWF config JSON when no main
// frontmatter value is present; imported value takes precedence over the runtime default.
// The compiler_orchestrator_engine.go applies MergedMaxAICredits to EngineConfig when
Expand Down
Loading
Loading