n8n/packages/@n8n/node-cli/src/commands/lint.test.ts

201 lines
6.0 KiB
TypeScript

import { cancel } from '@clack/prompts';
import fs from 'node:fs/promises';
import { CommandTester } from '../test-utils/command-tester';
import { stripAnsiCodes } from '../test-utils/matchers';
import { mockSpawn } from '../test-utils/mock-child-process';
import { setupTestPackage } from '../test-utils/package-setup';
import { tmpdirTest } from '../test-utils/temp-fs';
describe('lint command', () => {
const mockProcessStdout = vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
beforeEach(() => {
vi.clearAllMocks();
});
tmpdirTest('successful lint - runs eslint with correct arguments', async ({ tmpdir }) => {
await setupTestPackage(tmpdir, {
eslintConfig: true,
});
mockSpawn('pnpm', ['exec', '--', 'eslint', '.'], { exitCode: 0 });
const result = await CommandTester.run('lint');
expect(result).toBeDefined();
});
tmpdirTest('successful lint with warnings - shows warnings in output', async ({ tmpdir }) => {
await setupTestPackage(tmpdir, {
eslintConfig: true,
});
const eslintWarnings = `
/tmp/project/src/index.ts
10:5 warning Unused variable 'unusedVar' @typescript-eslint/no-unused-vars
15:3 warning Missing return type @typescript-eslint/explicit-function-return-type
✖ 2 problems (0 errors, 2 warnings)
`;
mockSpawn('pnpm', ['exec', '--', 'eslint', '.'], {
exitCode: 0,
stdout: eslintWarnings,
});
const result = await CommandTester.run('lint');
expect(result).toBeDefined();
const stdoutCalls = mockProcessStdout.mock.calls.flat();
const allOutput = stripAnsiCodes(
stdoutCalls.map((call) => (Buffer.isBuffer(call) ? call.toString() : String(call))).join(''),
);
expect(allOutput).toContain('Unused variable');
});
tmpdirTest('lint with fix flag - passes --fix to eslint', async ({ tmpdir }) => {
await setupTestPackage(tmpdir, {
eslintConfig: true,
});
mockSpawn('pnpm', ['exec', '--', 'eslint', '.', '--fix'], { exitCode: 0 });
const result = await CommandTester.run('lint --fix');
expect(result).toBeDefined();
});
tmpdirTest('eslint failure - exits with error code', async ({ tmpdir }) => {
await setupTestPackage(tmpdir, {
eslintConfig: true,
});
mockSpawn('pnpm', ['exec', '--', 'eslint', '.'], {
exitCode: 1,
stderr: 'ESLint found 3 errors',
});
await expect(CommandTester.run('lint')).rejects.toThrow('EEXIT: 1');
});
tmpdirTest('eslint spawn error - handles process errors', async ({ tmpdir }) => {
await setupTestPackage(tmpdir, {
eslintConfig: true,
});
mockSpawn('pnpm', ['exec', '--', 'eslint', '.'], {
error: 'ENOENT: no such file or directory, spawn eslint',
});
await expect(CommandTester.run('lint')).rejects.toThrow();
});
tmpdirTest('invalid package - not an n8n node package', async ({ tmpdir }) => {
await fs.writeFile(
`${tmpdir}/package.json`,
JSON.stringify({
name: 'regular-package',
version: '1.0.0',
// No n8n field - this makes it an invalid n8n package
}),
);
await expect(CommandTester.run('lint')).rejects.toThrow('EEXIT: 1');
expect(cancel).toHaveBeenCalledWith('lint can only be run in an n8n node package');
});
tmpdirTest('strict mode with default config - passes validation', async ({ tmpdir }) => {
await setupTestPackage(tmpdir, {
packageJson: { n8n: { strict: true } },
eslintConfig: true,
});
mockSpawn('pnpm', ['exec', '--', 'eslint', '.'], { exitCode: 0 });
const result = await CommandTester.run('lint');
expect(result).toBeDefined();
});
tmpdirTest('cloud-only lint errors - suggests disabling cloud support', async ({ tmpdir }) => {
await setupTestPackage(tmpdir, {
eslintConfig: true,
});
mockSpawn('pnpm', ['exec', '--', 'eslint', '.'], {
exitCode: 1,
stderr: 'Error: @n8n/community-nodes/no-restricted-globals rule failed',
});
await expect(CommandTester.run('lint')).rejects.toThrow('EEXIT: 1');
const stdoutCalls = mockProcessStdout.mock.calls.flat();
const hasCloudMessage = stdoutCalls.some(
(call) =>
typeof call === 'string' && call.includes('n8n Cloud compatibility issues detected'),
);
expect(hasCloudMessage).toBe(true);
});
tmpdirTest('regular lint errors - no cloud suggestion', async ({ tmpdir }) => {
await setupTestPackage(tmpdir, {
eslintConfig: true,
});
mockSpawn('pnpm', ['exec', '--', 'eslint', '.'], {
exitCode: 1,
stderr: 'Error: Unexpected token',
});
await expect(CommandTester.run('lint')).rejects.toThrow('EEXIT: 1');
const stdoutCalls = mockProcessStdout.mock.calls.flat();
const hasCloudMessage = stdoutCalls.some(
(call) => typeof call === 'string' && call.includes('n8n Cloud compatibility'),
);
expect(hasCloudMessage).toBe(false);
});
tmpdirTest('strict mode with modified config - fails validation', async ({ tmpdir }) => {
await setupTestPackage(tmpdir, {
packageJson: { n8n: { strict: true } },
eslintConfig:
"import { config } from '@n8n/node-cli/eslint';\n\n// Custom modification\nexport default config;\n",
});
await fs.writeFile(`${tmpdir}/pnpm-lock.yaml`, 'lockfileVersion: 5.4\n');
await expect(CommandTester.run('lint')).rejects.toThrow('EEXIT: 1');
const stdoutCalls = mockProcessStdout.mock.calls.flat();
const hasStrictModeError = stdoutCalls.some(
(call) => typeof call === 'string' && call.includes('Strict mode violation:'),
);
expect(hasStrictModeError).toBe(true);
});
tmpdirTest('strict mode with missing config - fails validation', async ({ tmpdir }) => {
await setupTestPackage(tmpdir, {
packageJson: { n8n: { strict: true } },
});
await fs.writeFile(`${tmpdir}/pnpm-lock.yaml`, 'lockfileVersion: 5.4\n');
// Don't create eslint.config.mjs file (it will be missing)
await expect(CommandTester.run('lint')).rejects.toThrow('EEXIT: 1');
const stdoutCalls = mockProcessStdout.mock.calls.flat();
const stdout = stdoutCalls
.filter((call) => typeof call === 'string')
.map(stripAnsiCodes)
.join('\n');
expect(stdout).toContain('eslint.config.mjs not found');
});
});