Skip to content

Commit 80aa844

Browse files
lpcoxCopilotCopilot
authored
refactor: eliminate duplicate code patterns (#2681)
* refactor: eliminate duplicate code patterns (#2673, #2674, #2675) 1. Extract shared jest.mock('execa') factory into test-helpers/mock-execa.test-utils.ts and update 12 test files to use it (was copy-pasted verbatim in each). 2. Extract parseProviderBaseUrl() helper in copilot-api-resolver.ts to deduplicate the 7-line URL normalization prologue shared by two functions. 3. Extract cache_and_unset_token() helper in one-shot-token.c to deduplicate the 18-line cache-store/unsetenv/log/mark-accessed tail in getenv and secure_getenv. All tests pass. Build passes. Closes #2673 Closes #2674 Closes #2675 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: unused mock imports, jest.mock require lint, strdup null check --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
1 parent 5ee16b2 commit 80aa844

14 files changed

Lines changed: 99 additions & 136 deletions

containers/agent/one-shot-token/one-shot-token.c

Lines changed: 39 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,43 @@ static int get_token_index(const char *name) {
327327
*
328328
* For all other variables: passes through to real getenv
329329
*/
330+
331+
/**
332+
* Shared cache-and-unset logic for sensitive token interception.
333+
* Caches the value, removes it from the environment, logs if debug is on,
334+
* and marks the token as accessed. Must be called with token_mutex held.
335+
*/
336+
static char *cache_and_unset_token(int token_idx, const char *name, char *result, const char *via_suffix) {
337+
if (result != NULL) {
338+
/* Cache the value so subsequent reads succeed after unsetenv */
339+
/* Note: This memory is intentionally never freed - it must persist
340+
* for the lifetime of the process */
341+
char *cached = strdup(result);
342+
if (cached == NULL) {
343+
fprintf(stderr, "[one-shot-token] Failed to cache token %s: out of memory\n", name);
344+
/* Still unset and mark as accessed, but return NULL to signal failure */
345+
unsetenv(name);
346+
token_accessed[token_idx] = 1;
347+
return NULL;
348+
}
349+
token_cache[token_idx] = cached;
350+
351+
/* Unset the variable from the environment so /proc/self/environ is cleared */
352+
unsetenv(name);
353+
354+
if (debug_enabled) {
355+
fprintf(stderr, "[one-shot-token] Token %s accessed and cached (length: %zu)%s\n",
356+
name, strlen(token_cache[token_idx]), via_suffix);
357+
}
358+
359+
result = token_cache[token_idx];
360+
}
361+
362+
/* Mark as accessed even if NULL (prevents repeated log messages) */
363+
token_accessed[token_idx] = 1;
364+
return result;
365+
}
366+
330367
__attribute__((visibility("default")))
331368
char *getenv(const char *name) {
332369
ensure_real_getenv();
@@ -354,26 +391,7 @@ char *getenv(const char *name) {
354391
if (!token_accessed[token_idx]) {
355392
/* First access - get the real value and cache it */
356393
result = real_getenv(name);
357-
358-
if (result != NULL) {
359-
/* Cache the value so subsequent reads succeed after unsetenv */
360-
/* Note: This memory is intentionally never freed - it must persist
361-
* for the lifetime of the process */
362-
token_cache[token_idx] = strdup(result);
363-
364-
/* Unset the variable from the environment so /proc/self/environ is cleared */
365-
unsetenv(name);
366-
367-
if (debug_enabled) {
368-
fprintf(stderr, "[one-shot-token] Token %s accessed and cached (length: %zu)\n",
369-
name, strlen(token_cache[token_idx]));
370-
}
371-
372-
result = token_cache[token_idx];
373-
}
374-
375-
/* Mark as accessed even if NULL (prevents repeated log messages) */
376-
token_accessed[token_idx] = 1;
394+
result = cache_and_unset_token(token_idx, name, result, "");
377395
} else {
378396
/* Already accessed - return cached value */
379397
result = token_cache[token_idx];
@@ -428,26 +446,7 @@ char *secure_getenv(const char *name) {
428446
if (!token_accessed[token_idx]) {
429447
/* First access - get the real value using secure_getenv */
430448
result = real_secure_getenv(name);
431-
432-
if (result != NULL) {
433-
/* Cache the value so subsequent reads succeed after unsetenv */
434-
/* Note: This memory is intentionally never freed - it must persist
435-
* for the lifetime of the process */
436-
token_cache[token_idx] = strdup(result);
437-
438-
/* Unset the variable from the environment so /proc/self/environ is cleared */
439-
unsetenv(name);
440-
441-
if (debug_enabled) {
442-
fprintf(stderr, "[one-shot-token] Token %s accessed and cached (length: %zu) (via secure_getenv)\n",
443-
name, strlen(token_cache[token_idx]));
444-
}
445-
446-
result = token_cache[token_idx];
447-
}
448-
449-
/* Mark as accessed even if NULL (prevents repeated log messages) */
450-
token_accessed[token_idx] = 1;
449+
result = cache_and_unset_token(token_idx, name, result, " (via secure_getenv)");
451450
} else {
452451
/* Already accessed - return cached value */
453452
result = token_cache[token_idx];

src/compose-generator.test.ts

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,10 @@ import * as os from 'os';
66
import * as path from 'path';
77

88
// Create mock functions (must remain per-file — jest.mock() is hoisted before imports)
9-
const mockExecaFn = jest.fn();
10-
const mockExecaSync = jest.fn();
119

1210
// Mock execa module
13-
jest.mock('execa', () => {
14-
const fn = (...args: any[]) => mockExecaFn(...args);
15-
fn.sync = (...args: any[]) => mockExecaSync(...args);
16-
return fn;
17-
});
11+
// eslint-disable-next-line @typescript-eslint/no-require-imports
12+
jest.mock('execa', () => require('./test-helpers/mock-execa.test-utils').execaMockFactory());
1813

1914
let mockConfig: WrapperConfig;
2015

src/copilot-api-resolver.ts

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,10 @@ export function resolveCopilotApiKey(
99
}
1010

1111
/**
12-
* Derive a Copilot API target hostname from COPILOT_PROVIDER_BASE_URL.
13-
* Returns undefined when the value is empty or not a valid URL/host.
12+
* Parse a provider base URL into a URL object, handling missing schemes.
13+
* Returns undefined if the input is empty or unparseable.
1414
*/
15-
export function deriveCopilotApiTargetFromProviderBaseUrl(
16-
providerBaseUrl: string | undefined
17-
): string | undefined {
15+
function parseProviderBaseUrl(providerBaseUrl: string | undefined): URL | undefined {
1816
const trimmed = providerBaseUrl?.trim();
1917
if (!trimmed) return undefined;
2018

@@ -23,33 +21,35 @@ export function deriveCopilotApiTargetFromProviderBaseUrl(
2321
: `https://${trimmed}`;
2422

2523
try {
26-
return new URL(candidate).hostname || undefined;
24+
return new URL(candidate);
2725
} catch {
2826
return undefined;
2927
}
3028
}
3129

30+
/**
31+
* Derive a Copilot API target hostname from COPILOT_PROVIDER_BASE_URL.
32+
* Returns undefined when the value is empty or not a valid URL/host.
33+
*/
34+
export function deriveCopilotApiTargetFromProviderBaseUrl(
35+
providerBaseUrl: string | undefined
36+
): string | undefined {
37+
return parseProviderBaseUrl(providerBaseUrl)?.hostname || undefined;
38+
}
39+
3240
/**
3341
* Derive a Copilot API base-path prefix from COPILOT_PROVIDER_BASE_URL.
3442
* Returns undefined when the value is empty, invalid, or has no path.
3543
*/
3644
export function deriveCopilotApiBasePathFromProviderBaseUrl(
3745
providerBaseUrl: string | undefined
3846
): string | undefined {
39-
const trimmed = providerBaseUrl?.trim();
40-
if (!trimmed) return undefined;
41-
42-
const candidate = trimmed.includes('://')
43-
? trimmed
44-
: `https://${trimmed}`;
47+
const url = parseProviderBaseUrl(providerBaseUrl);
48+
if (!url) return undefined;
4549

46-
try {
47-
const pathname = new URL(candidate).pathname.replace(/\/+$/, '');
48-
if (!pathname || pathname === '/') return undefined;
49-
return pathname.startsWith('/') ? pathname : `/${pathname}`;
50-
} catch {
51-
return undefined;
52-
}
50+
const pathname = url.pathname.replace(/\/+$/, '');
51+
if (!pathname || pathname === '/') return undefined;
52+
return pathname.startsWith('/') ? pathname : `/${pathname}`;
5353
}
5454

5555
/**

src/docker-manager-cleanup.test.ts

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,11 @@ import * as path from 'path';
55
import * as os from 'os';
66

77
// Create mock functions
8-
const mockExecaFn = jest.fn();
9-
const mockExecaSync = jest.fn();
108

119
// Mock execa module
12-
jest.mock('execa', () => {
13-
const fn = (...args: any[]) => mockExecaFn(...args);
14-
fn.sync = (...args: any[]) => mockExecaSync(...args);
15-
return fn;
16-
});
10+
import { mockExecaFn, mockExecaSync } from './test-helpers/mock-execa.test-utils';
11+
// eslint-disable-next-line @typescript-eslint/no-require-imports
12+
jest.mock('execa', () => require('./test-helpers/mock-execa.test-utils').execaMockFactory());
1713

1814
describe('docker-manager writeConfigs and cleanup', () => {
1915
describe('writeConfigs', () => {

src/docker-manager-lifecycle.test.ts

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,11 @@ import * as path from 'path';
55
import * as os from 'os';
66

77
// Create mock functions
8-
const mockExecaFn = jest.fn();
9-
const mockExecaSync = jest.fn();
108

119
// Mock execa module
12-
jest.mock('execa', () => {
13-
const fn = (...args: any[]) => mockExecaFn(...args);
14-
fn.sync = (...args: any[]) => mockExecaSync(...args);
15-
return fn;
16-
});
10+
import { mockExecaFn } from './test-helpers/mock-execa.test-utils';
11+
// eslint-disable-next-line @typescript-eslint/no-require-imports
12+
jest.mock('execa', () => require('./test-helpers/mock-execa.test-utils').execaMockFactory());
1713

1814
describe('docker-manager lifecycle', () => {
1915
describe('startContainers', () => {

src/docker-manager-utils.test.ts

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,10 @@ import * as path from 'path';
44
import * as os from 'os';
55

66
// Create mock functions
7-
const mockExecaFn = jest.fn();
8-
const mockExecaSync = jest.fn();
97

108
// Mock execa module
11-
jest.mock('execa', () => {
12-
const fn = (...args: any[]) => mockExecaFn(...args);
13-
fn.sync = (...args: any[]) => mockExecaSync(...args);
14-
return fn;
15-
});
9+
// eslint-disable-next-line @typescript-eslint/no-require-imports
10+
jest.mock('execa', () => require('./test-helpers/mock-execa.test-utils').execaMockFactory());
1611

1712
describe('docker-manager utilities', () => {
1813
describe('subnetsOverlap', () => {

src/services/agent-environment.test.ts

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,10 @@ import * as path from 'path';
66
import * as os from 'os';
77

88
// Create mock functions (must remain per-file — jest.mock() is hoisted before imports)
9-
const mockExecaFn = jest.fn();
10-
const mockExecaSync = jest.fn();
119

1210
// Mock execa module
13-
jest.mock('execa', () => {
14-
const fn = (...args: any[]) => mockExecaFn(...args);
15-
fn.sync = (...args: any[]) => mockExecaSync(...args);
16-
return fn;
17-
});
11+
// eslint-disable-next-line @typescript-eslint/no-require-imports
12+
jest.mock('execa', () => require('../test-helpers/mock-execa.test-utils').execaMockFactory());
1813

1914
let mockConfig: WrapperConfig;
2015

src/services/agent-service.test.ts

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,10 @@ import * as path from 'path';
66
import * as os from 'os';
77

88
// Create mock functions (must remain per-file — jest.mock() is hoisted before imports)
9-
const mockExecaFn = jest.fn();
10-
const mockExecaSync = jest.fn();
119

1210
// Mock execa module
13-
jest.mock('execa', () => {
14-
const fn = (...args: any[]) => mockExecaFn(...args);
15-
fn.sync = (...args: any[]) => mockExecaSync(...args);
16-
return fn;
17-
});
11+
// eslint-disable-next-line @typescript-eslint/no-require-imports
12+
jest.mock('execa', () => require('../test-helpers/mock-execa.test-utils').execaMockFactory());
1813

1914
let mockConfig: WrapperConfig;
2015

src/services/agent-volumes.test.ts

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,11 @@ import * as path from 'path';
66
import * as os from 'os';
77

88
// Create mock functions (must remain per-file — jest.mock() is hoisted before imports)
9-
const mockExecaFn = jest.fn();
10-
const mockExecaSync = jest.fn();
119

1210
// Mock execa module
13-
jest.mock('execa', () => {
14-
const fn = (...args: any[]) => mockExecaFn(...args);
15-
fn.sync = (...args: any[]) => mockExecaSync(...args);
16-
return fn;
17-
});
11+
import { mockExecaSync } from '../test-helpers/mock-execa.test-utils';
12+
// eslint-disable-next-line @typescript-eslint/no-require-imports
13+
jest.mock('execa', () => require('../test-helpers/mock-execa.test-utils').execaMockFactory());
1814

1915
let mockConfig: WrapperConfig;
2016

src/services/api-proxy-service.test.ts

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,10 @@ import * as os from 'os';
66
import * as path from 'path';
77

88
// Create mock functions (must remain per-file — jest.mock() is hoisted before imports)
9-
const mockExecaFn = jest.fn();
10-
const mockExecaSync = jest.fn();
119

1210
// Mock execa module
13-
jest.mock('execa', () => {
14-
const fn = (...args: any[]) => mockExecaFn(...args);
15-
fn.sync = (...args: any[]) => mockExecaSync(...args);
16-
return fn;
17-
});
11+
// eslint-disable-next-line @typescript-eslint/no-require-imports
12+
jest.mock('execa', () => require('../test-helpers/mock-execa.test-utils').execaMockFactory());
1813

1914
let mockConfig: WrapperConfig;
2015

0 commit comments

Comments
 (0)