Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
52 changes: 52 additions & 0 deletions core/test/unit_setup_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

import {afterAll, beforeAll, describe, expect, it} from 'vitest';
import {
SCRUBBED_ENV_PREFIXES,
SCRUBBED_ENV_VARS,
} from '../../tests/unit_setup.js';

describe('unit:core test environment', () => {
it('removes every listed variable', () => {
for (const name of SCRUBBED_ENV_VARS) {
expect(process.env[name]).toBeUndefined();
}
});

it('removes every variable in a scrubbed family', () => {
const remaining = Object.keys(process.env).filter((name) =>
SCRUBBED_ENV_PREFIXES.some((prefix) => name.startsWith(prefix)),
);

expect(remaining).toEqual([]);
});

it('does not empty the environment', () => {
expect(Object.keys(process.env).length).toBeGreaterThan(0);
});

/**
* Pins the module-scope design: the scrub runs once, before the test file is
* imported. Converting it to a top-level `beforeEach` would delete this value
* between the `beforeAll` and the assertion -- and would break
* `core/test/models/apigee_llm_test.ts`, which sets three of the scrubbed
* variables in its own `beforeAll`.
*/
describe('a value set by the test file', () => {
beforeAll(() => {
process.env.GOOGLE_CLOUD_PROJECT = 'explicit-project';
});

afterAll(() => {
delete process.env.GOOGLE_CLOUD_PROJECT;
});

it('survives the scrub', () => {
expect(process.env.GOOGLE_CLOUD_PROJECT).toBe('explicit-project');
});
});
});
29 changes: 29 additions & 0 deletions dev/test/unit_setup_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

import {describe, expect, it} from 'vitest';
import {
SCRUBBED_ENV_PREFIXES,
SCRUBBED_ENV_VARS,
} from '../../tests/unit_setup.js';

// `setupFiles` is configured per Vitest project, so each unit project needs its
// own test to prove its own wiring.
describe('unit:dev test environment', () => {
it('removes every listed variable', () => {
for (const name of SCRUBBED_ENV_VARS) {
expect(process.env[name]).toBeUndefined();
}
});

it('removes every variable in a scrubbed family', () => {
const remaining = Object.keys(process.env).filter((name) =>
SCRUBBED_ENV_PREFIXES.some((prefix) => name.startsWith(prefix)),
);

expect(remaining).toEqual([]);
});
});
29 changes: 29 additions & 0 deletions integrations/test/unit_setup_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

import {describe, expect, it} from 'vitest';
import {
SCRUBBED_ENV_PREFIXES,
SCRUBBED_ENV_VARS,
} from '../../tests/unit_setup.js';

// `setupFiles` is configured per Vitest project, so each unit project needs its
// own test to prove its own wiring.
describe('unit:integrations test environment', () => {
it('removes every listed variable', () => {
for (const name of SCRUBBED_ENV_VARS) {
expect(process.env[name]).toBeUndefined();
}
});

it('removes every variable in a scrubbed family', () => {
const remaining = Object.keys(process.env).filter((name) =>
SCRUBBED_ENV_PREFIXES.some((prefix) => name.startsWith(prefix)),
);

expect(remaining).toEqual([]);
});
});
64 changes: 64 additions & 0 deletions tests/unit_setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

import {afterAll} from 'vitest';

/**
* Environment variables that ADK production code reads directly. Unit tests
* must not inherit them from the developer's shell: a unit test that needs a
* value sets it explicitly, so that the suite behaves identically on a
* contributor's machine and in CI, where none of these are set.
*
* Every name here has a reader under `core/src`, `dev/src` or
* `integrations/src`. Ambient credential variables that ADK never reads
* (`GOOGLE_APPLICATION_CREDENTIALS`, `CLOUDSDK_*`) are deliberately absent:
* deleting them would not make auth hermetic, because Application Default
* Credentials also resolve via the gcloud well-known file and the metadata
* server.
*/
export const SCRUBBED_ENV_VARS: readonly string[] = [
'APIGEE_PROXY_URL',
'DATABASE_URL',
'GEMINI_API_KEY',
'GOOGLE_API_KEY',
'GOOGLE_CLOUD_AGENT_ENGINE_ID',
'GOOGLE_CLOUD_LOCATION',
'GOOGLE_CLOUD_PROJECT',
'GOOGLE_GENAI_API_KEY',
'GOOGLE_GENAI_USE_VERTEXAI',
'OTEL_EXPORTER_OTLP_ENDPOINT',
'OTEL_EXPORTER_OTLP_LOGS_ENDPOINT',
'OTEL_EXPORTER_OTLP_METRICS_ENDPOINT',
'OTEL_EXPORTER_OTLP_TRACES_ENDPOINT',
];

/**
* Families scrubbed by prefix. Feature flags are read under names built at
* runtime (`ADK_ENABLE_${featureName}` in feature_registry.ts), so the ADK
* family cannot be enumerated.
*/
export const SCRUBBED_ENV_PREFIXES: readonly string[] = ['ADK_'];

function isScrubbed(name: string): boolean {
return (
SCRUBBED_ENV_VARS.includes(name) ||
SCRUBBED_ENV_PREFIXES.some((prefix) => name.startsWith(prefix))
);
}

const inheritedEnv: Record<string, string> = {};

for (const name of Object.keys(process.env)) {
const value = process.env[name];
if (value !== undefined && isScrubbed(name)) {
inheritedEnv[name] = value;
delete process.env[name];
}
}

afterAll(() => {
Object.assign(process.env, inheritedEnv);
});
11 changes: 11 additions & 0 deletions vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,14 @@ const INTEGRATION_HOOK_TIMEOUT_MS = 120000;
*/
const INTEGRATION_TEST_TIMEOUT_MS = 60000;

/**
* Setup module for the unit projects. It strips ADK-relevant environment
* variables so a developer's shell cannot change unit test behaviour. The
* `integration`, `e2e` and `cross-language` projects deliberately do not use
* it: they read real credentials.
*/
const UNIT_SETUP_FILE = path.resolve(__dirname, './tests/unit_setup.ts');

export default defineConfig({
test: {
poolOptions: {
Expand All @@ -35,6 +43,7 @@ export default defineConfig({
test: {
name: 'unit:core',
environment: 'node',
setupFiles: [UNIT_SETUP_FILE],
alias: {
'@google/adk': path.resolve(__dirname, './core/src'),
'@google/adk-integrations': path.resolve(
Expand All @@ -49,6 +58,7 @@ export default defineConfig({
test: {
name: 'unit:dev',
environment: 'node',
setupFiles: [UNIT_SETUP_FILE],
alias: {
'@google/adk': path.resolve(__dirname, './core/src'),
'@google/adk-integrations': path.resolve(
Expand All @@ -63,6 +73,7 @@ export default defineConfig({
test: {
name: 'unit:integrations',
environment: 'node',
setupFiles: [UNIT_SETUP_FILE],
alias: {
'@google/adk': path.resolve(__dirname, './core/src'),
'@google/adk-integrations': path.resolve(
Expand Down
Loading