Skip to content

Deduplicate gateway configuration conversion adapters - #50701

Merged
pelikhan merged 9 commits into
mainfrom
copilot/duplicate-code-fix-gateway-config
Aug 6, 2026
Merged

Deduplicate gateway configuration conversion adapters#50701
pelikhan merged 9 commits into
mainfrom
copilot/duplicate-code-fix-gateway-config

Conversation

Copilot AI commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Claude, Codex, Copilot, and Gemini converters repeated the same gateway context loading, CLI-server filtering, secure write, logging, and reporting pipeline. Only URL handling, entry transforms, output paths, and serialization differ by engine.

  • Shared conversion runner

    • Adds runGatewayConversion to centralize the common pipeline.
    • Preserves secure 0600 output handling and conversion reporting.
  • Engine-specific adapters

    • Claude, Codex, Copilot, and Gemini now provide only format-specific callbacks.
    • Retains Codex host resolution, Copilot HOME error handling, and Gemini host-domain/workspace behavior.
  • Coverage

    • Adds runner-level coverage for filtering, transformation, output permissions, and reporting.
return runGatewayConversion({
  format: "Claude",
  engine: "Claude",
  outputPath: OUTPUT_PATH,
  transformServer: (_name, entry, urlPrefix) => transformClaudeEntry(entry, urlPrefix),
  serialize: servers => JSON.stringify({ mcpServers: servers }, null, 2),
});

Generated by 👨‍🍳 PR Sous Chef · gpt54 · 14.2 AIC · ⌖ 5.16 AIC · ⊞ 8.3K ·
Comment /souschef to run again


Generated by 👨‍🍳 PR Sous Chef · gpt54 · 6.47 AIC · ⌖ 6.82 AIC · ⊞ 8.3K ·
Comment /souschef to run again

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI changed the title [WIP] Refactor duplicate gateway config conversion flow Deduplicate gateway configuration conversion adapters Aug 5, 2026
Copilot AI requested a review from pelikhan August 5, 2026 22:30
@pelikhan
pelikhan marked this pull request as ready for review August 5, 2026 22:59
Copilot AI balanced review requested due to automatic review settings August 5, 2026 22:59
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

⚠️ PR Code Quality Reviewer failed during code quality review.

@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

🧠 Matt Pocock Skills Reviewer has completed the skills-based review. ✅

@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Test Quality Sentinel completed test quality analysis.

@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Design Decision Gate 🏗️ completed the design decision gate check.

No ADR enforcement needed: PR #50701 does not have the 'implementation' label and has 0 new lines of code in business logic directories (threshold: 100).

Copilot AI left a comment

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.

Pull request overview

Centralizes gateway configuration conversion while preserving engine-specific adapters.

Changes:

  • Adds a shared conversion runner.
  • Refactors four engine converters to use callbacks.
  • Adds runner-level pipeline coverage.
Show a summary per file
File Description
convert_gateway_config_shared.cjs Adds shared runner.
convert_gateway_config_shared.test.cjs Tests shared pipeline.
convert_gateway_config_claude.cjs Adapts Claude conversion.
convert_gateway_config_codex.cjs Adapts Codex TOML conversion.
convert_gateway_config_copilot.cjs Adapts Copilot conversion.
convert_gateway_config_gemini.cjs Adapts Gemini conversion.

Review details

Tip

Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

  • Files reviewed: 6/6 changed files
  • Comments generated: 1
  • Review effort level: Balanced

Comment on lines +54 to +57
getUrlPrefix: ({ domain, port }) => {
if (domain === "host.docker.internal") {
core.info("Resolving host.docker.internal to gateway IP: 172.30.0.1");
return `http://172.30.0.1:${port}`;
@github-actions github-actions Bot mentioned this pull request Aug 5, 2026
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

🧪 Test Quality Sentinel Report

Test Quality Score: 80/100 — Excellent

Analyzed 24 test(s): 22 design, 2 implementation, 0 violation(s).

📊 Metrics (24 tests)
Metric Value
Analyzed 24 (Go: 0, JS: 24)
✅ Design 22 (92%)
⚠️ Implementation 2 (8%)
Edge/error coverage 11 (46%)
Duplicate clusters 0
Inflation No (1.73:1)
🚨 Violations 0
Test File Classification Issues
rewriteUrl — replaces hostname/port convert_gateway_config_shared.test.cjs design_test
rewriteUrl — preserves path segments convert_gateway_config_shared.test.cjs design_test
rewriteUrl — handles url with no path beyond /mcp/ convert_gateway_config_shared.test.cjs design_test (edge)
rewriteUrl — does not rewrite non-mcp URLs convert_gateway_config_shared.test.cjs design_test (edge)
normalizeGatewayEntry — rewrites url and applies mutation convert_gateway_config_shared.test.cjs design_test
normalizeGatewayEntry — does not mutate the original entry convert_gateway_config_shared.test.cjs design_test (invariant)
normalizeGatewayEntry — works without a mutate callback convert_gateway_config_shared.test.cjs design_test (edge)
normalizeGatewayEntry — skips url rewrite when url field is missing convert_gateway_config_shared.test.cjs design_test (edge)
filterAndTransformServers — excludes CLI-mounted servers convert_gateway_config_shared.test.cjs design_test
filterAndTransformServers — applies the transform to each server convert_gateway_config_shared.test.cjs design_test
filterAndTransformServers — returns empty object when all CLI-mounted convert_gateway_config_shared.test.cjs design_test (edge)
writeSecureOutput — writes file with mode 0o600 convert_gateway_config_shared.test.cjs design_test (security invariant)
writeSecureOutput — creates nested directories convert_gateway_config_shared.test.cjs design_test
writeSecureOutput — overwrites and resets permissions to 0o600 convert_gateway_config_shared.test.cjs design_test (edge + security)
writeSecureOutput — throws on write failure convert_gateway_config_shared.test.cjs design_test (error)
loadGatewayContext — throws when MCP_GATEWAY_OUTPUT missing convert_gateway_config_shared.test.cjs design_test (error)
loadGatewayContext — throws when gateway file does not exist convert_gateway_config_shared.test.cjs design_test (error)
loadGatewayContext — throws when MCP_GATEWAY_DOMAIN missing convert_gateway_config_shared.test.cjs design_test (error)
loadGatewayContext — parses gateway output and returns structured context convert_gateway_config_shared.test.cjs design_test
loadGatewayContext — collects extraRequiredEnv into extraEnv convert_gateway_config_shared.test.cjs design_test
logCLIFilters — calls core.info when servers are present convert_gateway_config_shared.test.cjs implementation_test Internal call assertion only
logCLIFilters — does nothing when no CLI servers convert_gateway_config_shared.test.cjs implementation_test Internal call assertion only
logServerStats — reports included and filtered counts convert_gateway_config_shared.test.cjs design_test
runGatewayConversion — loads, filters, serializes, securely writes convert_gateway_config_shared.test.cjs design_test
⚠️ Flagged Tests (2)

logCLIFilters — calls core.info when servers are present (convert_gateway_config_shared.test.cjs) — implementation_test. Asserts that core.info was called with a string containing a server name, but does not verify the user-visible log format or any output contract. Acceptable at 8% of total, but consider asserting the full expected message pattern.

logCLIFilters — does nothing when no CLI servers (convert_gateway_config_shared.test.cjs) — implementation_test. Verifies that core.info is not called rather than verifying an observable output; this is tightly coupled to the internal implementation.

Verdict

Passed. 8% implementation tests (threshold: 30%). Strong edge-case and error-path coverage across rewriteUrl, normalizeGatewayEntry, filterAndTransformServers, writeSecureOutput, and loadGatewayContext. The security invariant tests (file mode 0o600, overwrite-and-reset) are a notable quality signal. The logCLIFilters tests are flagged as implementation-coupled but do not affect the pass verdict.

🧪 Test quality analysis by Test Quality Sentinel · sonnet46 · 37.1 AIC · ⌖ 8.64 AIC · ⊞ 7.7K ·
Comment /review to run again

@github-actions github-actions Bot left a comment

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.

✅ Test Quality Sentinel: 80/100 — Excellent. 8% implementation tests (threshold: 30%). No violations.

@github-actions github-actions Bot left a comment

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.

Review: Deduplicate gateway configuration conversion adapters

The refactoring is clean and correct. The shared runGatewayConversion runner faithfully preserves all engine-specific behaviours (Codex host resolution, Copilot HOME error handling, Gemini host-domain selection) and the security-sensitive 0o600 write path. Tests are well-structured.

One minor log-ordering change (non-blocking): In the Codex adapter, getUrlPrefix now emits core.info("Resolving host.docker.internal...") after the standard header lines (Converting..., Input:, Target domain:) because runGatewayConversion calls getUrlPrefix at line 188 — after lines 184-186. In the original code the resolver log came before Converting.... This is cosmetically different from the original but functionally harmless.

No blocking issues found. ✅> 🧵 Reviewed using Impeccable skills by Impeccable Skills Reviewer · sonnet46 · 29.7 AIC · ⌖ 9.24 AIC · ⊞ 5.4K

@github-actions github-actions Bot left a comment

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.

Skills-Based Review 🧠

Applied /codebase-design and /tdd — commenting only; no blocking issues.

📋 Key Themes & Highlights

Key Themes

  • Dual-callback coupling: getTargetDomain + getUrlPrefix must be kept in sync by callers; a single resolveTarget callback (or deriving domain from URL) would be safer.
  • Double invocation in Gemini: getGeminiHostDomain() is called twice inside main() callbacks — capture it once before calling runGatewayConversion.
  • Test branch coverage: The new runGatewayConversion test is a solid happy-path spec, but the outputPath-as-function and default-urlPrefix branches are untested.
  • logServerStats implicit parameter contract: the meaning of its two arguments (total vs included) is easy to mistake when adding a new engine.

Positive Highlights

  • ✅ Clean extraction of the shared pipeline — net -43 lines of duplicated boilerplate.
  • ✅ Security-critical 0600 file permission is preserved centrally and tested.
  • ✅ Engine-specific behaviour (Codex host resolution, Copilot HOME guard, Gemini workspace path) survives via callbacks — good interface design.
  • ✅ Test names read as specifications and the Arrange/Act/Assert structure is clear.
> 🧠 *Reviewed using Matt Pocock's skills by [Matt Pocock Skills Reviewer](https://github.com/github/gh-aw/actions/runs/31054793291)* · sonnet46 · 40.1 AIC · ⌖ 8.46 AIC · ⊞ 7.1K > Comment /matt to run again

function runGatewayConversion(options) {
const context = loadGatewayContext(options.contextOptions);
const targetDomain = options.getTargetDomain ? options.getTargetDomain(context) : context.domain;

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.

[/codebase-design] getTargetDomain and getUrlPrefix are separate callbacks but callers must keep them in sync manually — a drift between the two would produce a misleading log line without affecting actual output.

💡 Suggestion

Consider merging into a single resolveTarget callback returning { targetDomain, urlPrefix }, or auto-derive targetDomain from the resolved urlPrefix (e.g. parse the hostname). Gemini already calls getGeminiHostDomain() twice for this reason, which is a code smell.

@copilot please address this.

expect(fs.statSync(outputPath).mode & 0o777).toBe(0o600);
expect(mockCore.info).toHaveBeenCalledWith("Converting gateway configuration to Test format...");
expect(mockCore.info).toHaveBeenCalledWith("Target domain: target.internal:80");
expect(mockCore.info).toHaveBeenCalledWith("Servers: 1 included, 1 filtered (CLI-mounted)");

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] The single test exercises the happy path but skips error scenarios — specifically outputPath as a function and getUrlPrefix absent (default fallback). These are meaningful branches in runGatewayConversion that a regression could silently break.

💡 Suggested additional tests
it("resolves outputPath when given as a function", () => {
  const output = runGatewayConversion({
    ...
    outputPath: (ctx) => path.join(dir, "dynamic.json"),
  });
  expect(fs.existsSync(path.join(dir, "dynamic.json"))).toBe(true);
});

it("uses default urlPrefix when getUrlPrefix is not provided", () => {
  // MCP_GATEWAY_DOMAIN and PORT produce a urlPrefix; transformServer receives it
  let capturedUrlPrefix;
  runGatewayConversion({
    ...
    transformServer: (_n, e, urlPrefix) => { capturedUrlPrefix = urlPrefix; return e; },
    serialize: s => JSON.stringify(s),
  });
  expect(capturedUrlPrefix).toBe("(gateway.internal/redacted)
});

@copilot please address this.

function getGeminiHostDomain() {
return process.env.MCP_GATEWAY_HOST_DOMAIN || "localhost";
}

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.

[/codebase-design] getGeminiHostDomain() is called twice — once in getTargetDomain and once in getUrlPrefix — so the function body runs twice and the results must agree. If the env var changes between calls (unlikely in practice but possible in tests), the log and the URL could diverge.

💡 Suggestion

Capture the domain once outside the option callbacks:

const hostDomain = getGeminiHostDomain();
return runGatewayConversion({
  ...
  getTargetDomain: () => hostDomain,
  getUrlPrefix: ({ port }) => `(redacted)
});

This also eliminates the redundant exported getGeminiHostDomain helper.

@copilot please address this.

logCLIFilters(context.cliServers);
const servers = filterAndTransformServers(context.servers, context.cliServers, (name, entry) => options.transformServer(name, entry, urlPrefix, context));
const output = options.serialize(servers, context, urlPrefix);

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.

[/codebase-design] logServerStats is called with context.servers (the unfiltered original map) but Object.keys(servers).length (post-filter count), relying on the caller to pass the right pair. This implicit contract is invisible in the JSDoc and easy to misuse when adding a new engine.

💡 Suggestion

Compute the stat inside runGatewayConversion from the quantities already in scope — no extra parameter surface needed:

logServerStats(context.servers, Object.keys(servers).length);
// already correct — but document the contract in the JSDoc of logServerStats

Alternatively, make logServerStats accept { total, included } as a named-pair to surface the intent.

@copilot please address this.

@gh-aw-bot

Copy link
Copy Markdown
Collaborator

@copilot please review the latest PR feedback, refresh this branch if it is behind main, then run the pr-finisher skill and push any follow-up fixes.

Run: https://github.com/github/gh-aw/actions/runs/31055742237

Generated by 👨‍🍳 PR Sous Chef · gpt54 · 14.2 AIC · ⌖ 5.16 AIC · ⊞ 8.3K ·
Comment /souschef to run again

Copilot AI and others added 2 commits August 5, 2026 23:28
Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com>
…on via main()

Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com>

Copilot AI commented Aug 5, 2026

Copy link
Copy Markdown
Contributor Author

@copilot please review the latest PR feedback, refresh this branch if it is behind main, then run the pr-finisher skill and push any follow-up fixes.

  • failed checks:...

Added convert_gateway_config_codex.test.cjs with adapter-level tests that invoke main directly. The key test sets MCP_GATEWAY_DOMAIN=host.docker.internal and asserts the generated TOML uses http://172.30.0.1:80/... URLs (and contains no host.docker.internal references). Additional main-level tests cover the non-docker domain path, CLI server filtering, and the TOML persistence header.

Copilot AI requested a review from gh-aw-bot August 5, 2026 23:35
@gh-aw-bot

Copy link
Copy Markdown
Collaborator

@copilot thanks for the follow-up. CI appears complete now. Please run the pr-finisher skill, address the remaining review notes below if applicable, and push any final polish before maintainer review.

  • remaining review notes:
    • Matt Pocock Skills Reviewer: dual-callback coupling (getTargetDomain + getUrlPrefix) could drift; consider consolidating or documenting the contract.
    • Matt Pocock Skills Reviewer: capture getGeminiHostDomain() once instead of invoking it twice in callbacks.
    • Matt Pocock Skills Reviewer: add coverage for the outputPath function branch and default urlPrefix branch.

Run: https://github.com/github/gh-aw/actions/runs/31057198185

Generated by 👨‍🍳 PR Sous Chef · gpt54 · 6.47 AIC · ⌖ 6.82 AIC · ⊞ 8.3K ·
Comment /souschef to run again

@pelikhan

pelikhan commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator

… add missing branch coverage

Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com>

Copilot AI commented Aug 5, 2026

Copy link
Copy Markdown
Contributor Author

@copilot thanks for the follow-up. CI appears complete now. Please run the pr-finisher skill, address the remaining review notes below if applicable, and push any final polish before maintainer revi...

Addressed all three remaining notes:

  1. getGeminiHostDomain() called twice — captured once as hostDomain at the top of main() and closed over in both callbacks.
  2. Dual-callback coupling — documented the contract in the runGatewayConversion JSDoc: the two callbacks serve different purposes (log label vs. URL prefix) and must be kept consistent when both are provided.
  3. Missing coverage — added tests for the outputPath function branch (verifies the resolved path is used) and the default urlPrefix branch (no getUrlPrefix, falls back to context.urlPrefix).

All 73 tests pass.

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>

Copilot AI commented Aug 5, 2026

Copy link
Copy Markdown
Contributor Author

@copilot fix https://github.com/github/gh-aw/actions/runs/31057481584/job/92478346982?pr=50701

Fixed in the latest commit. The hasMergeCommitsInRange test assumed HEAD^..HEAD is always a linear range, but it failed when HEAD is a merge commit (which our PR's merge-from-main commit is). Added a guard that skips the test when HEAD^2 resolves — i.e., when HEAD is itself a merge commit.

@pelikhan
pelikhan merged commit 0b58b45 into main Aug 6, 2026
9 checks passed
@pelikhan
pelikhan deleted the copilot/duplicate-code-fix-gateway-config branch August 6, 2026 00:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[duplicate-code] Duplicate gateway config conversion flow across engine-specific adapters

4 participants