n8n/.github/scripts/owners.test.mjs
Matsu d74dc0f4e5
ci: Add reviewer recommendations based on CODEOWNERS (#30690)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 11:17:59 +00:00

174 lines
4.9 KiB
JavaScript

import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import {
assignOwnership,
ownershipsToAllocations,
parseOwnersContent,
parseOwnersFile,
} from './owners.mjs';
/**
* Run these tests by running
*
* node --test ./.github/scripts/owners.test.mjs
* */
describe('parseOwnersContent', () => {
it('parses well-formed OWNERS lines into Owner records', () => {
const content = [
'# header comment',
'',
'* @n8n-io/catalysts',
'packages/cli/ @n8n-io/cli-team',
'packages/foo/bar.ts @n8n-io/some-team',
].join('\n');
assert.deepEqual(parseOwnersContent(content), [
{ filepath: '*', team: '@n8n-io/catalysts' },
{ filepath: 'packages/cli/', team: '@n8n-io/cli-team' },
{ filepath: 'packages/foo/bar.ts', team: '@n8n-io/some-team' },
]);
});
it('skips lines without an @n8n-io team', () => {
const content = [
'# comment',
'* @other-org/team',
'pkg/ @n8n-io/keepers',
'',
].join('\n');
assert.deepEqual(parseOwnersContent(content), [
{ filepath: 'pkg/', team: '@n8n-io/keepers' },
]);
});
it('returns an empty array when no lines reference @n8n-io', () => {
assert.deepEqual(parseOwnersContent('# nothing here\n* @someone-else'), []);
});
});
describe('parseOwnersFile', () => {
it('reads the real OWNERS file into well-formed Owner records', () => {
const owners = parseOwnersFile();
assert.ok(owners.length > 0, 'OWNERS file should not be empty');
assert.ok(
owners.every(o => o.team && o.filepath),
'every parsed entry should have both team and filepath',
);
assert.ok(
owners.every(o => o.team?.startsWith('@n8n-io/')),
'every parsed team should belong to the @n8n-io org',
);
});
});
describe('assignOwnership', () => {
it('assigns every file to the catch-all team when only `*` is defined', () => {
const files = new Set(['a.ts', 'packages/cli/src/index.ts', 'docs/readme.md']);
const owners = [{ filepath: '*', team: '@n8n-io/catalysts' }];
const result = assignOwnership(files, owners);
assert.deepEqual(
result.get('@n8n-io/catalysts')?.sort(),
[...files].sort(),
);
assert.equal(result.size, 1);
});
it('applies last-match-wins: a later specific rule overrides the catch-all', () => {
const files = new Set([
'README.md',
'packages/cli/src/index.ts',
'packages/cli/src/lib/foo.ts',
]);
const owners = [
{ filepath: '*', team: '@n8n-io/catalysts' },
{ filepath: 'packages/cli/', team: '@n8n-io/cli-team' },
];
const result = assignOwnership(files, owners);
assert.deepEqual(result.get('@n8n-io/catalysts'), ['README.md']);
assert.deepEqual(
result.get('@n8n-io/cli-team')?.sort(),
['packages/cli/src/index.ts', 'packages/cli/src/lib/foo.ts'].sort(),
);
});
it('matches a directory pattern recursively', () => {
const files = new Set([
'packages/cli/src/deep/nested/file.ts',
'packages/cli/package.json',
]);
const owners = [{ filepath: 'packages/cli/', team: '@n8n-io/cli-team' }];
const result = assignOwnership(files, owners);
assert.deepEqual(
result.get('@n8n-io/cli-team')?.sort(),
[...files].sort(),
);
});
it('matches an exact file pattern only against that file', () => {
const files = new Set([
'packages/cli/src/controllers/ai.controller.ts',
'packages/cli/src/controllers/other.controller.ts',
]);
const owners = [
{
filepath: 'packages/cli/src/controllers/ai.controller.ts',
team: '@n8n-io/ai-team',
},
];
const result = assignOwnership(files, owners);
assert.deepEqual(result.get('@n8n-io/ai-team'), [
'packages/cli/src/controllers/ai.controller.ts',
]);
// the other controller matched no rule, so it must be omitted entirely
assert.equal(result.size, 1);
});
it('omits files that match no rule (no catch-all present)', () => {
const files = new Set(['unowned/file.ts', 'packages/cli/src/x.ts']);
const owners = [{ filepath: 'packages/cli/', team: '@n8n-io/cli-team' }];
const result = assignOwnership(files, owners);
assert.deepEqual(result.get('@n8n-io/cli-team'), ['packages/cli/src/x.ts']);
assert.equal(result.size, 1);
});
it('returns an empty Map when there are no changed files', () => {
const owners = [{ filepath: '*', team: '@n8n-io/catalysts' }];
const result = assignOwnership(new Set(), owners);
assert.equal(result.size, 0);
});
});
describe('ownershipsToAllocations', () => {
it('converts a Map of team -> files into Allocation[] with fileCount', () => {
const ownerships = new Map([
['@n8n-io/cli-team', ['a.ts', 'b.ts', 'c.ts']],
['@n8n-io/catalysts', ['README.md']],
]);
const result = ownershipsToAllocations(ownerships);
assert.deepEqual(result, [
{ team: '@n8n-io/cli-team', fileCount: 3 },
{ team: '@n8n-io/catalysts', fileCount: 1 },
]);
});
it('returns an empty array for an empty Map', () => {
assert.deepEqual(ownershipsToAllocations(new Map()), []);
});
});