chore: Migrate agent runtime lint to Oxlint (no-changelog) (#39971)

This commit is contained in:
Matsu 2026-09-30 11:46:43 +00:00 committed by GitHub
parent cd9d1ac669
commit 63c01bee24
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 303 additions and 43 deletions

View File

@ -14,28 +14,16 @@ export default defineConfig(
],
},
backendConfig,
{
rules: {
'@typescript-eslint/naming-convention': [
'error',
{
selector: 'enumMember',
format: ['UPPER_CASE', 'PascalCase'],
},
],
},
},
{
files: ['src/**/*.ts'],
ignores: ['src/**/__tests__/**/*.ts'],
rules: {
'@typescript-eslint/no-restricted-imports': [
'n8n-local-rules/no-static-runtime-import': [
'error',
{
paths: [
{
name: 'ai',
allowTypeImports: true,
message: AI_SDK_LAZY_IMPORT_MESSAGE,
},
],

View File

@ -0,0 +1,45 @@
import { backendConfig } from '@n8n/oxlint-config/backend';
import { defineConfig } from 'oxlint';
const AI_SDK_LAZY_IMPORT_MESSAGE =
"Import runtime values from 'ai' through the lazy loader in src/runtime/lazy-ai.ts or a dynamic import at the call site, so @n8n/agents stays light at boot.";
export default defineConfig({
extends: [backendConfig],
options: { typeAware: true },
ignorePatterns: [
'examples/**',
'vitest.integration.config.*',
'vitest.integration.setup.ts',
'src/__tests__/fixtures/**',
],
overrides: [
{
files: ['src/**/*.ts'],
rules: {
'n8n-local-rules/no-static-runtime-import': [
'error',
{
paths: [
{
name: 'ai',
message: AI_SDK_LAZY_IMPORT_MESSAGE,
},
],
},
],
},
},
{
files: ['src/**/__tests__/**/*.ts'],
rules: { 'n8n-local-rules/no-static-runtime-import': 'off' },
},
{
files: ['src/__tests__/integration/**/*.ts'],
rules: {
'typescript/require-await': 'off',
'n8n-local-rules/no-uncaught-json-parse': 'off',
},
},
],
});

View File

@ -79,8 +79,8 @@
"build:unchecked": "rimraf dist && tsc -p tsconfig.build.json --noCheck",
"format": "biome format --write src examples",
"format:check": "biome ci src examples",
"lint": "eslint . --quiet",
"lint:fix": "eslint . --fix",
"lint": "oxlint --quiet",
"lint:fix": "oxlint --fix",
"watch": "tsc -p tsconfig.build.json --watch",
"test": "vitest run",
"test:unit": "vitest run",
@ -159,6 +159,7 @@
}
},
"devDependencies": {
"@n8n/oxlint-config": "workspace:*",
"@n8n/typescript-config": "workspace:*",
"@n8n/vitest-config": "workspace:*",
"@opentelemetry/api": "catalog:",
@ -172,6 +173,8 @@
"dotenv": "catalog:",
"cross-env": "catalog:",
"nock": "catalog:",
"oxlint": "catalog:",
"oxlint-tsgolint": "catalog:",
"pg": "catalog:",
"tsx": "catalog:",
"typescript": "catalog:typescript",

View File

@ -1569,7 +1569,8 @@ describe('AgentRuntime — reasoning-only turn', () => {
const error = chunks.find(
(chunk): chunk is StreamChunk & { type: 'error' } => chunk.type === 'error',
);
expect(String((error?.error as Error).message)).toContain('no output');
if (!error) throw new Error('Expected an error chunk');
expect(String((error.error as Error).message)).toContain('no output');
});
it('fails a reasoning-only length turn without retrying or persisting it', async () => {

View File

@ -76,7 +76,8 @@ describe('buildCallPromptCacheOptions — OpenAI', () => {
const result = buildCallPromptCacheOptions({ enabled: true }, 'openai/gpt-5.1', context);
expect(result?.openai).toMatchObject({ promptCacheRetention: '24h' });
expect(typeof (result?.openai as { promptCacheKey: string }).promptCacheKey).toBe('string');
if (!result?.openai) throw new Error('Expected OpenAI prompt cache options');
expect(typeof (result.openai as { promptCacheKey: string }).promptCacheKey).toBe('string');
});
it('passes through an explicit promptCacheKey and promptCacheRetention unchanged', () => {
@ -120,7 +121,8 @@ describe('createOpenAIPromptCacheKey (via buildCallPromptCacheOptions)', () => {
...context,
...overrides,
});
return (result?.openai as { promptCacheKey: string }).promptCacheKey;
if (!result?.openai) throw new Error('Expected OpenAI prompt cache options');
return (result.openai as { promptCacheKey: string }).promptCacheKey;
}
it('is deterministic for the same agent, model, and instructions', () => {

View File

@ -33,6 +33,7 @@ import { RequireEscapedQueryValuesRule } from './require-escaped-query-values.js
import { NoOnLeaderTakeoverRule } from './no-on-leader-takeover.js';
import { NoEncryptionGuardrailDisableRule } from './no-encryption-guardrail-disable.js';
import { NoRawEnumRule } from './no-raw-enum.js';
import { NoStaticRuntimeImportRule } from './no-static-runtime-import.js';
export const rules = {
'no-uncaught-json-parse': NoUncaughtJsonParseRule,
@ -69,4 +70,5 @@ export const rules = {
'no-on-leader-takeover': NoOnLeaderTakeoverRule,
'no-encryption-guardrail-disable': NoEncryptionGuardrailDisableRule,
'no-raw-enum': NoRawEnumRule,
'no-static-runtime-import': NoStaticRuntimeImportRule,
} satisfies Record<string, AnyRuleModule>;

View File

@ -0,0 +1,46 @@
import { RuleTester } from '@typescript-eslint/rule-tester';
import { NoStaticRuntimeImportRule } from './no-static-runtime-import.js';
const ruleTester = new RuleTester();
const options: [{ paths: Array<{ name: string; message: string }> }] = [
{ paths: [{ name: 'heavy-sdk', message: 'Load this module lazily.' }] },
];
ruleTester.run('no-static-runtime-import', NoStaticRuntimeImportRule, {
valid: [
{ code: "import type { Client } from 'heavy-sdk';", options },
{ code: "import { type Client } from 'heavy-sdk';", options },
{ code: "const { Client } = await import('heavy-sdk');", options },
{ code: "import { Client } from 'other-sdk';", options },
{ code: "export type { Client } from 'heavy-sdk';", options },
{ code: "export { type Client } from 'heavy-sdk';", options },
{ code: "export type * from 'heavy-sdk';", options },
],
invalid: [
{
code: "import { Client } from 'heavy-sdk';",
options,
errors: [{ messageId: 'restrictedImport' }],
},
{
code: "import HeavySdk from 'heavy-sdk';",
options,
errors: [{ messageId: 'restrictedImport' }],
},
{
code: "import 'heavy-sdk';",
options,
errors: [{ messageId: 'restrictedImport' }],
},
{
code: "export { Client } from 'heavy-sdk';",
options,
errors: [{ messageId: 'restrictedImport' }],
},
{
code: "export * from 'heavy-sdk';",
options,
errors: [{ messageId: 'restrictedImport' }],
},
],
});

View File

@ -0,0 +1,74 @@
import { ESLintUtils, type TSESTree } from '@typescript-eslint/utils';
type Options = [{ paths: Array<{ name: string; message: string }> }];
type MessageIds = 'restrictedImport';
export const NoStaticRuntimeImportRule = ESLintUtils.RuleCreator.withoutDocs<Options, MessageIds>({
meta: {
type: 'problem',
docs: {
description: 'Disallow static runtime imports from configured modules.',
},
messages: {
restrictedImport: '{{ message }}',
},
schema: [
{
type: 'object',
additionalProperties: false,
required: ['paths'],
properties: {
paths: {
type: 'array',
items: {
type: 'object',
additionalProperties: false,
required: ['name', 'message'],
properties: {
name: { type: 'string' },
message: { type: 'string' },
},
},
},
},
},
],
},
defaultOptions: [{ paths: [] }],
create(context, [options]) {
const restrictedPaths = new Map(options.paths.map(({ name, message }) => [name, message]));
const reportRestrictedExport = (
node: TSESTree.ExportAllDeclaration | TSESTree.ExportNamedDeclaration,
) => {
if (!node.source || node.exportKind === 'type') return;
const message = restrictedPaths.get(node.source.value);
if (!message) return;
if (node.type === 'ExportNamedDeclaration') {
const hasRuntimeSpecifier = node.specifiers.some(
(specifier) => specifier.exportKind !== 'type',
);
if (node.specifiers.length > 0 && !hasRuntimeSpecifier) return;
}
context.report({ node: node.source, messageId: 'restrictedImport', data: { message } });
};
return {
ImportDeclaration(node) {
const message = restrictedPaths.get(node.source.value);
if (!message || node.importKind === 'type') return;
const hasRuntimeSpecifier = node.specifiers.some(
(specifier) => specifier.type !== 'ImportSpecifier' || specifier.importKind !== 'type',
);
if (node.specifiers.length > 0 && !hasRuntimeSpecifier) return;
context.report({ node: node.source, messageId: 'restrictedImport', data: { message } });
},
ExportAllDeclaration: reportRestrictedExport,
ExportNamedDeclaration: reportRestrictedExport,
};
},
});

View File

@ -13,7 +13,6 @@ const restrictedLazyRuntimeImports = [
'turndown',
].map((name) => ({
name,
allowTypeImports: true,
message: LAZY_RUNTIME_IMPORT_MESSAGE,
}));
@ -33,25 +32,11 @@ export default defineConfig(
'evaluations/cli/pairwise.ts',
],
},
{
rules: {
// Tool names may be kebab-case identifiers (e.g. 'list-workflows'), which
// require quotes in object literals. Skip naming checks for those.
'@typescript-eslint/naming-convention': [
'error',
{
selector: 'objectLiteralProperty',
modifiers: ['requiresQuotes'],
format: null,
},
],
},
},
{
files: ['src/**/*.ts'],
ignores: ['src/**/__tests__/**/*.ts'],
rules: {
'@typescript-eslint/no-restricted-imports': [
'n8n-local-rules/no-static-runtime-import': [
'error',
{ paths: restrictedLazyRuntimeImports },
],
@ -93,13 +78,6 @@ export default defineConfig(
'@typescript-eslint/no-unsafe-call': 'off',
},
},
{
// Debt: the base layer enforces kebab-case filenames and this package has
// 6 files that predate it. Rename them, then delete this block.
rules: {
'unicorn/filename-case': 'off',
},
},
{
files: ['evaluations/clients/n8n-client.ts'],
// An evaluation harness that talks to a local instance it started

View File

@ -0,0 +1,82 @@
import { backendConfig } from '@n8n/oxlint-config/backend';
import { defineConfig } from 'oxlint';
const LAZY_RUNTIME_IMPORT_MESSAGE =
'Use an existing lazy loader, or add one near first use. Static runtime imports of this dependency undo the idle-memory guardrail.';
const restrictedLazyRuntimeImports = [
'@daytona/sdk',
'csv-parse/sync',
'linkedom',
'pdf-parse',
'psl',
'turndown',
].map((name) => ({
name,
message: LAZY_RUNTIME_IMPORT_MESSAGE,
}));
export default defineConfig({
extends: [backendConfig],
options: { typeAware: true },
ignorePatterns: [
'scripts/**/*.cjs',
'skills/**/*.mjs',
'.data/**',
'evaluations/.data/**',
'evaluations/.output/**',
'.output/**',
'evaluations/cli/pairwise.ts',
],
overrides: [
{
files: ['src/**/*.ts'],
rules: {
'n8n-local-rules/no-static-runtime-import': [
'error',
{ paths: restrictedLazyRuntimeImports },
],
},
},
{
files: ['src/**/__tests__/**/*.ts'],
rules: { 'n8n-local-rules/no-static-runtime-import': 'off' },
},
{
files: ['src/tools/__tests__/**/*.test.ts'],
rules: {
'typescript/no-unsafe-assignment': 'off',
'typescript/no-unsafe-member-access': 'off',
'typescript/no-unsafe-argument': 'off',
},
},
{
files: ['evaluations/**/*.ts'],
jsPlugins: ['@n8n/oxlint-config/import-x-alias'],
rules: {
'import-x-alias/no-extraneous-dependencies': ['error', { devDependencies: true }],
},
},
{
files: ['evaluations/computer-use/report-html.ts'],
rules: {
'typescript/no-unsafe-assignment': 'off',
'typescript/no-unsafe-member-access': 'off',
'typescript/no-unsafe-argument': 'off',
'typescript/no-unsafe-call': 'off',
},
},
{
files: ['assets/workflow-diagnostics.mts'],
rules: {
'import-x-alias/no-extraneous-dependencies': 'off',
},
},
{
files: ['evaluations/clients/n8n-client.ts'],
rules: {
'n8n-local-rules/no-uncentralized-http': 'off',
},
},
],
});

View File

@ -11,8 +11,8 @@
"test": "vitest run",
"test:unit": "vitest run",
"test:dev": "vitest --silent=false",
"lint": "eslint . --quiet",
"lint:fix": "eslint . --fix",
"lint": "oxlint --quiet",
"lint:fix": "oxlint --fix",
"eval:instance-ai": "tsx evaluations/cli/index.ts",
"eval:agents": "tsx evaluations/cli/index.ts --source langtracer --suite agents --tier agents",
"eval:build-mcp-manifest": "tsx evaluations/cli/build-mcp-manifest.ts",
@ -100,6 +100,7 @@
"devDependencies": {
"@langchain/anthropic": "catalog:",
"@langchain/core": "catalog:",
"@n8n/oxlint-config": "workspace:*",
"@n8n/typescript-config": "workspace:*",
"@n8n/vitest-config": "workspace:*",
"@types/luxon": "catalog:",
@ -109,6 +110,8 @@
"fast-glob": "catalog:",
"p-limit": "catalog:p-limit-v7",
"playwright-core": "catalog:",
"oxlint": "catalog:",
"oxlint-tsgolint": "catalog:",
"tsx": "catalog:",
"typescript": "catalog:typescript",
"undici": "catalog:undici-v7",

View File

@ -6,8 +6,13 @@
"compilerOptions": {
"target": "es2023",
"lib": ["es2023"],
"module": "NodeNext",
"moduleResolution": "NodeNext",
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"paths": {
"@/*": ["../src/*"]
},
"types": ["node"]
},
"include": ["**/*.ts"]

View File

@ -1255,6 +1255,9 @@ importers:
specifier: 'catalog:'
version: 3.23.3(zod@3.25.76)
devDependencies:
'@n8n/oxlint-config':
specifier: workspace:*
version: link:../oxlint-config
'@n8n/typescript-config':
specifier: workspace:*
version: link:../typescript-config
@ -1294,6 +1297,12 @@ importers:
nock:
specifier: 'catalog:'
version: 14.0.14
oxlint:
specifier: 'catalog:'
version: 1.78.0(oxlint-tsgolint@0.21.1)
oxlint-tsgolint:
specifier: 'catalog:'
version: 0.21.1
pg:
specifier: 8.21.0
version: 8.21.0(pg-native@3.8.0)
@ -3150,6 +3159,9 @@ importers:
'@langchain/core':
specifier: 'catalog:'
version: 1.2.8(@opentelemetry/api@1.9.1)(@opentelemetry/exporter-trace-otlp-proto@0.221.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.10.0(@opentelemetry/api@1.9.1))(openai@6.46.0(@aws-sdk/credential-provider-node@3.972.80)(@smithy/signature-v4@5.7.2)(ws@8.21.1(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76))(ws@8.21.1(bufferutil@4.0.9)(utf-8-validate@5.0.10))
'@n8n/oxlint-config':
specifier: workspace:*
version: link:../oxlint-config
'@n8n/typescript-config':
specifier: workspace:*
version: link:../typescript-config
@ -3171,6 +3183,12 @@ importers:
fast-glob:
specifier: 'catalog:'
version: 3.3.3
oxlint:
specifier: 'catalog:'
version: 1.78.0(oxlint-tsgolint@0.21.1)
oxlint-tsgolint:
specifier: 'catalog:'
version: 0.21.1
p-limit:
specifier: catalog:p-limit-v7
version: 7.3.1

View File

@ -41,6 +41,13 @@ Possible approaches:
Audit every package configuration before conversion. Do not copy the full generic naming rule into a new custom implementation.
Phase 3 decisions:
- `@n8n/agents`: retire the enum-member casing selector. It enforces style only.
- `@n8n/instance-ai`: retire the quoted object-property selector. It only exempts names that require quotes and protects no API contract.
- `@n8n/instance-ai`: remove the stale package-wide filename exception. The package no longer contains files that need it.
- `@n8n/instance-ai`: override the removed Node 10 module resolution in the scripts tsconfig so tsgolint can keep linting `scripts/**/*.ts`.
### Testing infrastructure decisions
- `@n8n/rules-engine`, `@n8n/code-health`, and `@n8n/playwright-janitor`: retire the exemption-only rule-ID selectors. They enforce no positive naming contract.

View File

@ -62,6 +62,9 @@
"@n8n/ai-node-sdk#lint": {
"dependsOn": ["^build", "@n8n/oxlint-config#build"]
},
"@n8n/agents#lint": {
"dependsOn": ["^build", "@n8n/oxlint-config#build"]
},
"@n8n/ai-utilities#lint": {
"dependsOn": ["^build", "@n8n/oxlint-config#build"]
},
@ -113,6 +116,9 @@
"@n8n/imap#lint": {
"dependsOn": ["^build", "@n8n/oxlint-config#build"]
},
"@n8n/instance-ai#lint": {
"dependsOn": ["^build", "@n8n/oxlint-config#build"]
},
"@n8n/local-gateway#lint": {
"dependsOn": ["^build", "@n8n/oxlint-config#build"]
},