From 0314641b45235c245cf48ffc1308384b90236926 Mon Sep 17 00:00:00 2001 From: GitHub Copilot Date: Thu, 19 Feb 2026 09:25:21 +0000 Subject: [PATCH 1/2] refactor(cli): apply functional/immutability improvements to pkg/cli - sanitizeValidationResults: use sliceutil.Map for outer and inner loops, extract pure helper function sanitizeCompileValidationError - buildAuditData: replace var+append jobs loop with sliceutil.Map - generateFailureAnalysis: replace var+append failedJobs loop with sliceutil.FilterMap These changes eliminate mutable intermediate state and make data transformations more declarative, improving clarity and reducing mutation surface area. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- pkg/cli/audit_report.go | 8 +++---- pkg/cli/audit_report_analysis.go | 13 ++++++----- pkg/cli/compile_config.go | 38 ++++++++++++-------------------- 3 files changed, 25 insertions(+), 34 deletions(-) diff --git a/pkg/cli/audit_report.go b/pkg/cli/audit_report.go index 3476817735a..ddbae273c24 100644 --- a/pkg/cli/audit_report.go +++ b/pkg/cli/audit_report.go @@ -8,6 +8,7 @@ import ( "github.com/github/gh-aw/pkg/constants" "github.com/github/gh-aw/pkg/logger" + "github.com/github/gh-aw/pkg/sliceutil" "github.com/github/gh-aw/pkg/timeutil" "github.com/github/gh-aw/pkg/workflow" ) @@ -228,8 +229,7 @@ func buildAuditData(processedRun ProcessedRun, metrics LogMetrics, mcpToolUsage } // Build job data - var jobs []JobData - for _, jobDetail := range processedRun.JobDetails { + jobs := sliceutil.Map(processedRun.JobDetails, func(jobDetail JobInfoWithDuration) JobData { job := JobData{ Name: jobDetail.Name, Status: jobDetail.Status, @@ -238,8 +238,8 @@ func buildAuditData(processedRun ProcessedRun, metrics LogMetrics, mcpToolUsage if jobDetail.Duration > 0 { job.Duration = timeutil.FormatDuration(jobDetail.Duration) } - jobs = append(jobs, job) - } + return job + }) // Build downloaded files list downloadedFiles := extractDownloadedFiles(run.LogsPath) diff --git a/pkg/cli/audit_report_analysis.go b/pkg/cli/audit_report_analysis.go index 45993fa2d72..8b53dd3f42d 100644 --- a/pkg/cli/audit_report_analysis.go +++ b/pkg/cli/audit_report_analysis.go @@ -254,12 +254,13 @@ func generateFailureAnalysis(processedRun ProcessedRun, errors []ErrorInfo) *Fai } // Collect failed job names - var failedJobs []string - for _, job := range processedRun.JobDetails { - if job.Conclusion == "failure" || job.Conclusion == "timed_out" || job.Conclusion == "cancelled" { - failedJobs = append(failedJobs, job.Name) - } - } + failedJobs := sliceutil.FilterMap( + processedRun.JobDetails, + func(job JobInfoWithDuration) bool { + return job.Conclusion == "failure" || job.Conclusion == "timed_out" || job.Conclusion == "cancelled" + }, + func(job JobInfoWithDuration) string { return job.Name }, + ) // Generate error summary errorSummary := "No specific errors identified" diff --git a/pkg/cli/compile_config.go b/pkg/cli/compile_config.go index 9fa358deba9..5b75c4b882e 100644 --- a/pkg/cli/compile_config.go +++ b/pkg/cli/compile_config.go @@ -2,6 +2,7 @@ package cli import ( "github.com/github/gh-aw/pkg/logger" + "github.com/github/gh-aw/pkg/sliceutil" "github.com/github/gh-aw/pkg/stringutil" ) @@ -78,34 +79,23 @@ func sanitizeValidationResults(results []ValidationResult) []ValidationResult { compileConfigLog.Printf("Sanitizing validation results: workflow_count=%d", len(results)) - sanitized := make([]ValidationResult, len(results)) - for i, result := range results { - sanitized[i] = ValidationResult{ + return sliceutil.Map(results, func(result ValidationResult) ValidationResult { + return ValidationResult{ Workflow: result.Workflow, Valid: result.Valid, CompiledFile: result.CompiledFile, - Errors: make([]CompileValidationError, len(result.Errors)), - Warnings: make([]CompileValidationError, len(result.Warnings)), - } - - // Sanitize all error messages - for j, err := range result.Errors { - sanitized[i].Errors[j] = CompileValidationError{ - Type: err.Type, - Message: stringutil.SanitizeErrorMessage(err.Message), - Line: err.Line, - } + Errors: sliceutil.Map(result.Errors, sanitizeCompileValidationError), + Warnings: sliceutil.Map(result.Warnings, sanitizeCompileValidationError), } + }) +} - // Sanitize all warning messages - for j, warn := range result.Warnings { - sanitized[i].Warnings[j] = CompileValidationError{ - Type: warn.Type, - Message: stringutil.SanitizeErrorMessage(warn.Message), - Line: warn.Line, - } - } +// sanitizeCompileValidationError returns a copy of the error with its message sanitized +// to remove potential secret key names. +func sanitizeCompileValidationError(err CompileValidationError) CompileValidationError { + return CompileValidationError{ + Type: err.Type, + Message: stringutil.SanitizeErrorMessage(err.Message), + Line: err.Line, } - - return sanitized } From 0ea2284791ef580605308a802ae95bc45798c6b5 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Feb 2026 05:29:27 -0800 Subject: [PATCH 2/2] Merge main into fp-enhancer/pkg-cli-immutability (#16812) --- .github/aw/actions-lock.json | 25 ----- .github/workflows/docs-noob-tester.lock.yml | 2 +- .../workflows/slide-deck-maintainer.lock.yml | 2 +- .../workflows/stale-repo-identifier.lock.yml | 2 +- docs/src/content/docs/reference/glossary.md | 4 + pkg/workflow/data/action_pins.json | 25 ----- pkg/workflow/domains.go | 33 ++++++ pkg/workflow/http_mcp_domains_test.go | 104 ++++++++++++++++++ 8 files changed, 144 insertions(+), 53 deletions(-) diff --git a/.github/aw/actions-lock.json b/.github/aw/actions-lock.json index 8ea5eab0fc6..a0eb86d0c3f 100644 --- a/.github/aw/actions-lock.json +++ b/.github/aw/actions-lock.json @@ -165,31 +165,6 @@ "version": "v3", "sha": "3477b6488008d9411aaf22a0924ec7c1f6a69980" }, - "github/stale-repos@v3.0.2": { - "repo": "github/stale-repos", - "version": "v3.0.2", - "sha": "a21e55567b83cf3c3f3f9085d3038dc6cee02598" - }, - "haskell-actions/setup@v2.10.3": { - "repo": "haskell-actions/setup", - "version": "v2.10.3", - "sha": "9cd1b7bf3f36d5a3c3b17abc3545bfb5481912ea" - }, - "oven-sh/setup-bun@v2.1.2": { - "repo": "oven-sh/setup-bun", - "version": "v2.1.2", - "sha": "3d267786b128fe76c2f16a390aa2448b815359f3" - }, - "ruby/setup-ruby@v1.288.0": { - "repo": "ruby/setup-ruby", - "version": "v1.288.0", - "sha": "09a7688d3b55cf0e976497ff046b70949eeaccfd" - }, - "super-linter/super-linter@v8.2.1": { - "repo": "super-linter/super-linter", - "version": "v8.2.1", - "sha": "2bdd90ed3262e023ac84bf8fe35dc480721fc1f2" - }, "super-linter/super-linter@v8.5.0": { "repo": "super-linter/super-linter", "version": "v8.5.0", diff --git a/.github/workflows/docs-noob-tester.lock.yml b/.github/workflows/docs-noob-tester.lock.yml index dceec067501..af3b27d10cd 100644 --- a/.github/workflows/docs-noob-tester.lock.yml +++ b/.github/workflows/docs-noob-tester.lock.yml @@ -694,7 +694,7 @@ jobs: timeout-minutes: 30 run: | set -o pipefail - sudo -E awf --env-all --container-workdir "${GITHUB_WORKSPACE}" --allow-domains '*.jsr.io,api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.npms.io,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,bun.sh,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,deb.nodesource.com,deno.land,get.pnpm.io,github.com,host.docker.internal,json-schema.org,json.schemastore.org,jsr.io,keyserver.ubuntu.com,nodejs.org,npm.pkg.github.com,npmjs.com,npmjs.org,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.bower.io,registry.npmjs.com,registry.npmjs.org,registry.yarnpkg.com,repo.yarnpkg.com,s.symcb.com,s.symcd.com,security.ubuntu.com,skimdb.npmjs.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.npmjs.com,www.npmjs.org,yarnpkg.com' --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --enable-host-access --image-tag 0.20.1 --skip-pull --enable-api-proxy \ + sudo -E awf --env-all --container-workdir "${GITHUB_WORKSPACE}" --allow-domains '*.jsr.io,api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.npms.io,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,bun.sh,cdn.playwright.dev,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,deb.nodesource.com,deno.land,get.pnpm.io,github.com,host.docker.internal,json-schema.org,json.schemastore.org,jsr.io,keyserver.ubuntu.com,nodejs.org,npm.pkg.github.com,npmjs.com,npmjs.org,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,playwright.download.prss.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.bower.io,registry.npmjs.com,registry.npmjs.org,registry.yarnpkg.com,repo.yarnpkg.com,s.symcb.com,s.symcd.com,security.ubuntu.com,skimdb.npmjs.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.npmjs.com,www.npmjs.org,yarnpkg.com' --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --enable-host-access --image-tag 0.20.1 --skip-pull --enable-api-proxy \ -- /bin/bash -c '/usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --add-dir "${GITHUB_WORKSPACE}" --disable-builtin-mcps --allow-all-tools --allow-all-paths --share /tmp/gh-aw/sandbox/agent/logs/conversation.md --prompt "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)"${GH_AW_MODEL_AGENT_COPILOT:+ --model "$GH_AW_MODEL_AGENT_COPILOT"}' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log env: COPILOT_AGENT_RUNNER_TYPE: STANDALONE diff --git a/.github/workflows/slide-deck-maintainer.lock.yml b/.github/workflows/slide-deck-maintainer.lock.yml index 210294c7339..a2c918d0092 100644 --- a/.github/workflows/slide-deck-maintainer.lock.yml +++ b/.github/workflows/slide-deck-maintainer.lock.yml @@ -755,7 +755,7 @@ jobs: timeout-minutes: 45 run: | set -o pipefail - sudo -E awf --env-all --container-workdir "${GITHUB_WORKSPACE}" --allow-domains '*.jsr.io,api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.npms.io,bun.sh,deb.nodesource.com,deno.land,get.pnpm.io,github.com,host.docker.internal,jsr.io,nodejs.org,npm.pkg.github.com,npmjs.com,npmjs.org,raw.githubusercontent.com,registry.bower.io,registry.npmjs.com,registry.npmjs.org,registry.yarnpkg.com,repo.yarnpkg.com,skimdb.npmjs.com,telemetry.enterprise.githubcopilot.com,www.npmjs.com,www.npmjs.org,yarnpkg.com' --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --enable-host-access --image-tag 0.20.1 --skip-pull --enable-api-proxy \ + sudo -E awf --env-all --container-workdir "${GITHUB_WORKSPACE}" --allow-domains '*.jsr.io,api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.npms.io,bun.sh,cdn.playwright.dev,deb.nodesource.com,deno.land,get.pnpm.io,github.com,host.docker.internal,jsr.io,nodejs.org,npm.pkg.github.com,npmjs.com,npmjs.org,playwright.download.prss.microsoft.com,raw.githubusercontent.com,registry.bower.io,registry.npmjs.com,registry.npmjs.org,registry.yarnpkg.com,repo.yarnpkg.com,skimdb.npmjs.com,telemetry.enterprise.githubcopilot.com,www.npmjs.com,www.npmjs.org,yarnpkg.com' --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --enable-host-access --image-tag 0.20.1 --skip-pull --enable-api-proxy \ -- /bin/bash -c '/usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --add-dir "${GITHUB_WORKSPACE}" --disable-builtin-mcps --allow-tool github --allow-tool safeoutputs --allow-tool '\''shell(cat)'\'' --allow-tool '\''shell(cat*)'\'' --allow-tool '\''shell(cd*)'\'' --allow-tool '\''shell(curl*)'\'' --allow-tool '\''shell(date)'\'' --allow-tool '\''shell(echo)'\'' --allow-tool '\''shell(find*)'\'' --allow-tool '\''shell(git add:*)'\'' --allow-tool '\''shell(git branch:*)'\'' --allow-tool '\''shell(git checkout:*)'\'' --allow-tool '\''shell(git commit:*)'\'' --allow-tool '\''shell(git merge:*)'\'' --allow-tool '\''shell(git rm:*)'\'' --allow-tool '\''shell(git status)'\'' --allow-tool '\''shell(git switch:*)'\'' --allow-tool '\''shell(git)'\'' --allow-tool '\''shell(grep)'\'' --allow-tool '\''shell(grep*)'\'' --allow-tool '\''shell(head)'\'' --allow-tool '\''shell(head*)'\'' --allow-tool '\''shell(kill*)'\'' --allow-tool '\''shell(ls)'\'' --allow-tool '\''shell(ls*)'\'' --allow-tool '\''shell(lsof*)'\'' --allow-tool '\''shell(npm ci*)'\'' --allow-tool '\''shell(npm install*)'\'' --allow-tool '\''shell(npm run*)'\'' --allow-tool '\''shell(npx @marp-team/marp-cli*)'\'' --allow-tool '\''shell(npx http-server*)'\'' --allow-tool '\''shell(pwd)'\'' --allow-tool '\''shell(pwd*)'\'' --allow-tool '\''shell(sort)'\'' --allow-tool '\''shell(tail)'\'' --allow-tool '\''shell(tail*)'\'' --allow-tool '\''shell(uniq)'\'' --allow-tool '\''shell(wc)'\'' --allow-tool '\''shell(yq)'\'' --allow-tool write --add-dir /tmp/gh-aw/cache-memory/ --allow-all-paths --share /tmp/gh-aw/sandbox/agent/logs/conversation.md --prompt "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)"${GH_AW_MODEL_AGENT_COPILOT:+ --model "$GH_AW_MODEL_AGENT_COPILOT"}' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log env: COPILOT_AGENT_RUNNER_TYPE: STANDALONE diff --git a/.github/workflows/stale-repo-identifier.lock.yml b/.github/workflows/stale-repo-identifier.lock.yml index 60ac793c9b1..f3485f4c66f 100644 --- a/.github/workflows/stale-repo-identifier.lock.yml +++ b/.github/workflows/stale-repo-identifier.lock.yml @@ -368,7 +368,7 @@ jobs: ORGANIZATION: ${{ env.ORGANIZATION }} id: stale-repos name: Run stale_repos tool - uses: github/stale-repos@a21e55567b83cf3c3f3f9085d3038dc6cee02598 # v3.0.2 + uses: github/stale-repos@3477b6488008d9411aaf22a0924ec7c1f6a69980 # v3.0.2 - env: INACTIVE_REPOS: ${{ steps.stale-repos.outputs.inactiveRepos }} name: Save stale repos output diff --git a/docs/src/content/docs/reference/glossary.md b/docs/src/content/docs/reference/glossary.md index b607021deb1..a0dbe39ec66 100644 --- a/docs/src/content/docs/reference/glossary.md +++ b/docs/src/content/docs/reference/glossary.md @@ -187,6 +187,10 @@ A GitHub Personal Access Token with granular permission control, specifying exac The `gh-aw` extension for GitHub CLI providing commands for managing agentic workflows: compile, run, status, logs, add, and project management. +### Playground + +An interactive web-based editor for authoring, compiling, and previewing agentic workflows without local installation. The Playground runs the gh-aw compiler in the browser using [WebAssembly](#webassembly-wasm) and auto-saves editor content to `localStorage` so work is preserved across sessions. Available at `/gh-aw/editor/`. + ### Validation Checking workflow files for errors, security issues, and best practices. Occurs during compilation and can be enhanced with strict mode and security scanners. diff --git a/pkg/workflow/data/action_pins.json b/pkg/workflow/data/action_pins.json index 8ea5eab0fc6..a0eb86d0c3f 100644 --- a/pkg/workflow/data/action_pins.json +++ b/pkg/workflow/data/action_pins.json @@ -165,31 +165,6 @@ "version": "v3", "sha": "3477b6488008d9411aaf22a0924ec7c1f6a69980" }, - "github/stale-repos@v3.0.2": { - "repo": "github/stale-repos", - "version": "v3.0.2", - "sha": "a21e55567b83cf3c3f3f9085d3038dc6cee02598" - }, - "haskell-actions/setup@v2.10.3": { - "repo": "haskell-actions/setup", - "version": "v2.10.3", - "sha": "9cd1b7bf3f36d5a3c3b17abc3545bfb5481912ea" - }, - "oven-sh/setup-bun@v2.1.2": { - "repo": "oven-sh/setup-bun", - "version": "v2.1.2", - "sha": "3d267786b128fe76c2f16a390aa2448b815359f3" - }, - "ruby/setup-ruby@v1.288.0": { - "repo": "ruby/setup-ruby", - "version": "v1.288.0", - "sha": "09a7688d3b55cf0e976497ff046b70949eeaccfd" - }, - "super-linter/super-linter@v8.2.1": { - "repo": "super-linter/super-linter", - "version": "v8.2.1", - "sha": "2bdd90ed3262e023ac84bf8fe35dc480721fc1f2" - }, "super-linter/super-linter@v8.5.0": { "repo": "super-linter/super-linter", "version": "v8.5.0", diff --git a/pkg/workflow/domains.go b/pkg/workflow/domains.go index 49581bc877a..389571ff502 100644 --- a/pkg/workflow/domains.go +++ b/pkg/workflow/domains.go @@ -99,6 +99,13 @@ var ClaudeDefaultDomains = []string{ "ts-ocsp.ws.symantec.com", } +// PlaywrightDomains are the domains required for Playwright browser downloads +// These domains are needed when Playwright MCP server initializes in the Docker container +var PlaywrightDomains = []string{ + "cdn.playwright.dev", + "playwright.download.prss.microsoft.com", +} + // init loads the ecosystem domains from the embedded JSON func init() { domainsLog.Print("Loading ecosystem domains from embedded JSON") @@ -349,6 +356,23 @@ func extractHTTPMCPDomains(tools map[string]any) []string { return domains } +// extractPlaywrightDomains returns Playwright domains when Playwright tool is configured +// Returns a slice of domain names required for Playwright browser downloads +// These domains are needed when Playwright MCP server initializes in the Docker container +func extractPlaywrightDomains(tools map[string]any) []string { + if tools == nil { + return []string{} + } + + // Check if Playwright tool is configured + if _, hasPlaywright := tools["playwright"]; hasPlaywright { + domainsLog.Printf("Detected Playwright tool, adding %d domains for browser downloads", len(PlaywrightDomains)) + return PlaywrightDomains + } + + return []string{} +} + // mergeDomainsWithNetwork combines default domains with NetworkPermissions allowed domains // Returns a deduplicated, sorted, comma-separated string suitable for AWF's --allow-domains flag func mergeDomainsWithNetwork(defaultDomains []string, network *NetworkPermissions) string { @@ -388,6 +412,15 @@ func mergeDomainsWithNetworkToolsAndRuntimes(defaultDomains []string, network *N } } + // Add Playwright ecosystem domains (if Playwright tool is specified) + // This ensures browser binaries can be downloaded when Playwright initializes + if tools != nil { + playwrightDomains := extractPlaywrightDomains(tools) + for _, domain := range playwrightDomains { + domainMap[domain] = true + } + } + // Add runtime ecosystem domains (if runtimes are specified) if runtimes != nil { runtimeDomains := getDomainsFromRuntimes(runtimes) diff --git a/pkg/workflow/http_mcp_domains_test.go b/pkg/workflow/http_mcp_domains_test.go index dc9600cea06..78c2746b770 100644 --- a/pkg/workflow/http_mcp_domains_test.go +++ b/pkg/workflow/http_mcp_domains_test.go @@ -232,3 +232,107 @@ func TestGetClaudeAllowedDomainsWithTools(t *testing.T) { require.Contains(t, result, "anthropic.com", "Should include Claude defaults") require.Contains(t, result, "registry.npmjs.org", "Should include Node ecosystem") } + +// TestExtractPlaywrightDomains tests extraction of Playwright ecosystem domains when Playwright tool is configured +func TestExtractPlaywrightDomains(t *testing.T) { + tests := []struct { + name string + tools map[string]any + expected []string + }{ + { + name: "playwright tool configured", + tools: map[string]any{ + "playwright": map[string]any{ + "allowed_domains": []string{"github.com"}, + }, + }, + expected: []string{"playwright.download.prss.microsoft.com", "cdn.playwright.dev"}, + }, + { + name: "playwright tool with empty config", + tools: map[string]any{ + "playwright": map[string]any{}, + }, + expected: []string{"playwright.download.prss.microsoft.com", "cdn.playwright.dev"}, + }, + { + name: "playwright tool with null config", + tools: map[string]any{ + "playwright": nil, + }, + expected: []string{"playwright.download.prss.microsoft.com", "cdn.playwright.dev"}, + }, + { + name: "no playwright tool", + tools: map[string]any{ + "github": map[string]any{ + "mode": "local", + }, + }, + expected: []string{}, + }, + { + name: "empty tools", + tools: map[string]any{}, + expected: []string{}, + }, + { + name: "nil tools", + tools: nil, + expected: []string{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := extractPlaywrightDomains(tt.tools) + + // Sort both slices for comparison + SortStrings(result) + SortStrings(tt.expected) + + assert.Equal(t, tt.expected, result, "Extracted Playwright domains should match expected") + }) + } +} + +// TestGetCopilotAllowedDomainsWithPlaywright tests that Playwright domains are automatically included for Copilot engine +func TestGetCopilotAllowedDomainsWithPlaywright(t *testing.T) { + network := &NetworkPermissions{ + Allowed: []string{"defaults"}, + } + + tools := map[string]any{ + "playwright": map[string]any{ + "allowed_domains": []string{"github.com"}, + }, + } + + result := GetCopilotAllowedDomainsWithTools(network, tools) + + // Should include Copilot defaults and Playwright ecosystem domains + require.Contains(t, result, "playwright.download.prss.microsoft.com", "Should include Playwright download domain") + require.Contains(t, result, "cdn.playwright.dev", "Should include Playwright CDN domain") + require.Contains(t, result, "api.githubcopilot.com", "Should include Copilot defaults") +} + +// TestGetCodexAllowedDomainsWithPlaywright tests that Playwright domains are automatically included for Codex engine +func TestGetCodexAllowedDomainsWithPlaywright(t *testing.T) { + network := &NetworkPermissions{ + Allowed: []string{"defaults"}, + } + + tools := map[string]any{ + "playwright": map[string]any{ + "allowed_domains": []string{"example.com"}, + }, + } + + result := GetCodexAllowedDomainsWithTools(network, tools) + + // Should include Codex defaults and Playwright ecosystem domains + require.Contains(t, result, "playwright.download.prss.microsoft.com", "Should include Playwright download domain") + require.Contains(t, result, "cdn.playwright.dev", "Should include Playwright CDN domain") + require.Contains(t, result, "api.openai.com", "Should include Codex defaults") +}