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
7 changes: 1 addition & 6 deletions core/src/artifacts/file_artifact_service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -558,12 +558,7 @@ async function getCanonicalUri(
filename: string,
version: number,
): Promise<string> {
const artifactDir = await getArtifactDir(
rootDir,
userId,
sessionId,
filename,
);
const artifactDir = getArtifactDir(rootDir, userId, sessionId, filename);
const storedFilename = path.basename(artifactDir);
const versionsDir = getVersionsDir(artifactDir);
const payloadPath = path.join(
Expand Down
2 changes: 1 addition & 1 deletion core/src/models/google_llm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -323,7 +323,7 @@ export class Gemini extends BaseLlm {
messageQueue.push(message);
},
onerror: (error) => {
messageQueue.error(error);
messageQueue.error(new Error(error.message, {cause: error}));
},
onclose: () => {
messageQueue.close();
Expand Down
1 change: 0 additions & 1 deletion core/src/tools/openapi_tool/rest_api_tool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,6 @@ export class RestApiTool extends BaseTool {
const response = await globalThis.fetch(url, {
method,
headers,
// eslint-disable-next-line no-undef
body: body as BodyInit,
});

Expand Down
4 changes: 2 additions & 2 deletions core/src/utils/async_queue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export class AsyncQueue<T> implements AsyncIterable<T> {
reject: (reason?: unknown) => void;
}> = [];
private closed = false;
private errorVal?: unknown;
private errorVal?: Error;

push(value: T) {
if (this.closed) return;
Expand All @@ -26,7 +26,7 @@ export class AsyncQueue<T> implements AsyncIterable<T> {
}
}

error(err: unknown) {
error(err: Error) {
this.errorVal = err;
while (this.resolvers.length > 0) {
const {reject} = this.resolvers.shift()!;
Expand Down
44 changes: 44 additions & 0 deletions core/test/models/google_llm_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,18 @@ class TestGemini extends Gemini {
}
}

/** Node has no global ErrorEvent, which is what the live API reports errors as. */
class TestErrorEvent extends Event implements ErrorEvent {
readonly colno = 0;
readonly error: unknown = undefined;
readonly filename = '';
readonly lineno = 0;

constructor(readonly message: string) {
super('error');
}
}

describe('GoogleLlm', () => {
const clearEnv = () => {
delete process.env['GOOGLE_CLOUD_PROJECT'];
Expand Down Expand Up @@ -652,5 +664,37 @@ describe('GoogleLlm', () => {
}),
);
});

it('surfaces a live API error as an Error keeping the event as cause', async () => {
const llm = new TestGemini({apiKey: 'test-key'});
const connection = await llm.connect({
model: 'gemini-2.5-flash',
contents: [],
liveConnectConfig: {},
toolsDict: {},
});

const [params] = vi.mocked(llm.liveApiClient.live.connect).mock.calls[0];
const {onerror} = params.callbacks;
if (!onerror) {
expect.fail('connect() did not register an onerror callback');
}
const event = new TestErrorEvent('live socket failed');
onerror(event);

const failure = await connection
.receive()
.next()
.then(
() => expect.fail('receive() should have rejected'),
(error: unknown) => error,
);

if (!(failure instanceof Error)) {
expect.fail(`expected an Error, got ${typeof failure}`);
}
expect(failure.message).toBe('live socket failed');
expect(failure.cause).toBe(event);
});
});
});
31 changes: 31 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,37 @@ export default defineConfig([
ignores: ["**/dist/**", "dev/src/browser/**"],
},
tseslint.configs.recommended,
// Type-aware linting for the published source trees. This block must stay
// ahead of the "**/*.ts" block below: `recommendedTypeCheckedOnly` bundles
// typescript-eslint's `eslint-recommended`, which switches off 18 core rules
// (no-undef, no-const-assign, no-unreachable, ...) that "js/recommended"
// re-enables afterwards.
{
files: ["core/src/**/*.ts", "integrations/src/**/*.ts"],
extends: [tseslint.configs.recommendedTypeCheckedOnly],
languageOptions: {
parserOptions: {
project: ["./core/tsconfig.json", "./integrations/tsconfig.json"],
tsconfigRootDir: import.meta.dirname,
},
},
rules: {
// Pre-existing findings, deferred for staged adoption rather than
// suppressed: burn each count down and re-enable the rule.
"@typescript-eslint/no-base-to-string": "off", // 4 findings, 4 files
"@typescript-eslint/no-redundant-type-constituents": "off", // 4 findings, 4 files
"@typescript-eslint/no-unnecessary-type-assertion": "off", // 82 findings, 38 files
"@typescript-eslint/no-unsafe-argument": "off", // 3 findings, 3 files
"@typescript-eslint/no-unsafe-assignment": "off", // 34 findings, 11 files
"@typescript-eslint/no-unsafe-call": "off", // 6 findings, 4 files
"@typescript-eslint/no-unsafe-enum-comparison": "off", // 8 findings, 4 files
"@typescript-eslint/no-unsafe-member-access": "off", // 42 findings, 5 files
"@typescript-eslint/no-unsafe-return": "off", // 3 findings, 3 files
"@typescript-eslint/require-await": "off", // 69 findings, 37 files
"@typescript-eslint/restrict-plus-operands": "off", // 4 findings, 2 files
"@typescript-eslint/restrict-template-expressions": "off", // 26 findings, 14 files
},
},
{
files: ["**/*.ts"],
plugins: { js },
Expand Down
160 changes: 160 additions & 0 deletions tests/integration/lint_config/lint_config_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import {ESLint, type Linter} from 'eslint';
import {describe, expect, it} from 'vitest';

/** Source trees linted with type information. */
const TYPE_CHECKED_FILES = [
'core/src/runner/runner.ts',
'integrations/src/index.ts',
];

/**
* Trees deliberately left on the non-type-aware parse: the test trees are in no
* tsconfig `include`, and dev/src resolves `@google/adk` through core/dist, so
* its findings would depend on whether the tree happens to be built.
*/
const NON_TYPE_CHECKED_FILES = [
'core/test/utils/task_test.ts',
'dev/src/server/adk_api_server.ts',
];

/**
* Core rules that typescript-eslint's `eslint-recommended` switches off and
* `js/recommended` switches back on. They stay enabled only while the
* type-aware block is ordered ahead of the `**\/*.ts` block.
*/
const CORE_RULES_PRESERVED_BY_BLOCK_ORDER = [
'constructor-super',
'getter-return',
'no-class-assign',
'no-const-assign',
'no-dupe-args',
'no-dupe-class-members',
'no-dupe-keys',
'no-func-assign',
'no-import-assign',
'no-new-native-nonconstructor',
'no-obj-calls',
'no-redeclare',
'no-setter-return',
'no-this-before-super',
'no-undef',
'no-unreachable',
'no-unsafe-negation',
'no-with',
];

/**
* A rule that cannot run without type information: typescript-eslint throws
* rather than reporting when the parser produced no program for the file.
*/
const TYPE_INFO_REQUIRED_RULE =
'@typescript-eslint/no-unnecessary-type-assertion';

const ERROR = 2;
const OFF = 0;

const eslint = new ESLint();

function severityOf(
config: Awaited<ReturnType<ESLint['calculateConfigForFile']>>,
ruleId: string,
): Linter.RuleSeverity | undefined {
const entry: Linter.RuleEntry | undefined = config.rules?.[ruleId];
return Array.isArray(entry) ? entry[0] : entry;
}

function lintWithTypeInfoRequiredRule(): ESLint {
return new ESLint({
overrideConfig: {rules: {[TYPE_INFO_REQUIRED_RULE]: 'error'}},
});
}

describe('ESLint type-aware configuration', () => {
describe.each(TYPE_CHECKED_FILES)('%s', (file) => {
it('is parsed against the per-package TypeScript programs', async () => {
const config = await eslint.calculateConfigForFile(file);
const {project, tsconfigRootDir} = config.languageOptions.parserOptions;

expect(project).toEqual([
'./core/tsconfig.json',
'./integrations/tsconfig.json',
]);
expect(tsconfigRootDir).toBe(process.cwd());
});

it('enables the type-checked rules that reached zero findings', async () => {
const config = await eslint.calculateConfigForFile(file);

expect(
severityOf(config, '@typescript-eslint/no-floating-promises'),
).toBe(ERROR);
expect(severityOf(config, '@typescript-eslint/no-misused-promises')).toBe(
ERROR,
);
expect(severityOf(config, '@typescript-eslint/await-thenable')).toBe(
ERROR,
);
});

it('leaves the deferred rules off', async () => {
const config = await eslint.calculateConfigForFile(file);

expect(
severityOf(config, '@typescript-eslint/no-unsafe-assignment'),
).toBe(OFF);
expect(severityOf(config, '@typescript-eslint/require-await')).toBe(OFF);
});

it('keeps the core correctness rules the preset would disable', async () => {
const config = await eslint.calculateConfigForFile(file);

for (const rule of CORE_RULES_PRESERVED_BY_BLOCK_ORDER) {
expect(severityOf(config, rule), rule).toBe(ERROR);
}
});
});

describe.each(NON_TYPE_CHECKED_FILES)('%s', (file) => {
it('is not parsed with type information', async () => {
const config = await eslint.calculateConfigForFile(file);

expect(config.languageOptions.parserOptions.project).toBeUndefined();
});

it('does not enable the type-checked rules', async () => {
const config = await eslint.calculateConfigForFile(file);

expect(
severityOf(config, '@typescript-eslint/no-floating-promises'),
).toBeUndefined();
});
});

it('loads a TypeScript program and lints a source file cleanly', async () => {
const [result] = await eslint.lintFiles(['core/src/utils/task.ts']);

expect(result.messages).toEqual([]);
expect(result.errorCount).toBe(0);
});

it('runs a type-information-dependent rule over core/src', async () => {
const [result] = await lintWithTypeInfoRequiredRule().lintFiles([
'core/src/utils/task.ts',
]);

expect(result.fatalErrorCount).toBe(0);
});

it('has no type information to give the test tree', async () => {
await expect(
lintWithTypeInfoRequiredRule().lintFiles([
'core/test/utils/task_test.ts',
]),
).rejects.toThrow(/requires type information/);
});
});
Loading