Files
brain/tools/graphify-safe-update.test.mjs

117 lines
4.0 KiB
JavaScript

// tools/graphify-safe-update.test.mjs
// Pure-helper tests for the post-commit safe-update wrapper.
// Covers scope filtering + code/doc partitioning — the parts that gated
// the spike-bloat incident (ADR-017 § "Стратегия обновлений").
import { describe, it, expect } from 'vitest';
import { filterInScope, partitionByExtension } from './graphify-safe-update.mjs';
const ALLOWED_SCOPES = ['docs/', '.claude/', 'app/'];
const SCAN_EXCLUDE_DIRS = ['node_modules/', 'vendor/', '__pycache__/', '.git/'];
const CODE_EXTS = new Set(['.php', '.ts', '.js', '.vue', '.mjs', '.cjs', '.py', '.go']);
describe('filterInScope', () => {
it('keeps files inside allowed scopes', () => {
const input = ['docs/foo.md', '.claude/skills/x/SKILL.md', 'app/Models/Deal.php'];
expect(filterInScope(input, ALLOWED_SCOPES, SCAN_EXCLUDE_DIRS)).toEqual(input);
});
it('rejects files outside allowed scopes (tools/, root .md, bin/)', () => {
const input = [
'tools/graphify-safe-update.mjs',
'CHANGELOG.md',
'bin/something.exe',
'package.json',
'README.md',
];
expect(filterInScope(input, ALLOWED_SCOPES, SCAN_EXCLUDE_DIRS)).toEqual([]);
});
it('rejects vendor/node_modules even if nominally under app/', () => {
const input = [
'app/vendor/laravel/framework/Foo.php',
'app/node_modules/foo/bar.js',
'app/__pycache__/x.pyc',
'app/.git/HEAD',
];
expect(filterInScope(input, ALLOWED_SCOPES, SCAN_EXCLUDE_DIRS)).toEqual([]);
});
it('keeps real app/ files alongside vendor exclusions', () => {
const input = [
'app/app/Services/LedgerService.php',
'app/vendor/laravel/framework/Container.php',
'app/resources/js/views/Dashboard.vue',
];
expect(filterInScope(input, ALLOWED_SCOPES, SCAN_EXCLUDE_DIRS)).toEqual([
'app/app/Services/LedgerService.php',
'app/resources/js/views/Dashboard.vue',
]);
});
it('returns empty array on empty input', () => {
expect(filterInScope([], ALLOWED_SCOPES, SCAN_EXCLUDE_DIRS)).toEqual([]);
});
it('handles mixed in-scope and out-of-scope', () => {
const input = [
'docs/CLAUDE.md',
'tools/x.mjs',
'app/Models/Deal.php',
'CHANGELOG.md',
'.claude/skills/y/SKILL.md',
];
expect(filterInScope(input, ALLOWED_SCOPES, SCAN_EXCLUDE_DIRS)).toEqual([
'docs/CLAUDE.md',
'app/Models/Deal.php',
'.claude/skills/y/SKILL.md',
]);
});
});
describe('partitionByExtension', () => {
it('puts .php/.ts/.vue/.mjs into code', () => {
const input = ['app/Foo.php', 'app/bar.ts', 'app/baz.vue', 'app/qux.mjs'];
const { codeFiles, docFiles } = partitionByExtension(input, CODE_EXTS);
expect(codeFiles).toEqual(input);
expect(docFiles).toEqual([]);
});
it('puts .md/.txt into doc', () => {
const input = ['docs/foo.md', '.claude/bar.md', 'app/README.txt'];
const { codeFiles, docFiles } = partitionByExtension(input, CODE_EXTS);
expect(codeFiles).toEqual([]);
expect(docFiles).toEqual(input);
});
it('partitions mixed list correctly', () => {
const input = [
'app/Models/Deal.php',
'docs/spec.md',
'app/resources/js/utils.ts',
'.claude/skills/x/SKILL.md',
'app/composer.json',
];
const { codeFiles, docFiles } = partitionByExtension(input, CODE_EXTS);
expect(codeFiles).toEqual(['app/Models/Deal.php', 'app/resources/js/utils.ts']);
expect(docFiles).toEqual([
'docs/spec.md',
'.claude/skills/x/SKILL.md',
'app/composer.json',
]);
});
it('is case-insensitive on extension', () => {
const input = ['app/Foo.PHP', 'app/Bar.TS', 'docs/README.MD'];
const { codeFiles, docFiles } = partitionByExtension(input, CODE_EXTS);
expect(codeFiles).toEqual(['app/Foo.PHP', 'app/Bar.TS']);
expect(docFiles).toEqual(['docs/README.MD']);
});
it('returns empty arrays on empty input', () => {
const { codeFiles, docFiles } = partitionByExtension([], CODE_EXTS);
expect(codeFiles).toEqual([]);
expect(docFiles).toEqual([]);
});
});