// 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, ALLOWED_SCOPES, SCAN_EXCLUDE_DIRS } from './graphify-safe-update.mjs'; const CODE_EXTS = new Set(['.php', '.ts', '.js', '.vue', '.mjs', '.cjs', '.py', '.go']); describe('ALLOWED_SCOPES / SCAN_EXCLUDE_DIRS contract (post-split)', () => { it('includes tools/ and excludes the removed app/ scope', () => { expect(ALLOWED_SCOPES.includes('tools/')).toBe(true); expect(ALLOWED_SCOPES.includes('app/')).toBe(false); expect(ALLOWED_SCOPES).toEqual(['docs/', '.claude/', 'tools/']); }); it('excludes test files by the .test.mjs suffix', () => { expect(SCAN_EXCLUDE_DIRS.includes('.test.mjs')).toBe(true); }); }); describe('filterInScope', () => { it('keeps files inside allowed scopes (docs/.claude/tools)', () => { const input = ['docs/foo.md', '.claude/skills/x/SKILL.md', 'tools/floor-decide.mjs']; expect(filterInScope(input, ALLOWED_SCOPES, SCAN_EXCLUDE_DIRS)).toEqual(input); }); it('keeps tools/ production code', () => { const input = ['tools/enforce-supreme-gate.mjs', 'tools/plan-lock.mjs']; expect(filterInScope(input, ALLOWED_SCOPES, SCAN_EXCLUDE_DIRS)).toEqual(input); }); it('rejects tools/ test files (.test.mjs suffix)', () => { const input = ['tools/floor-decide.test.mjs', 'tools/enforce-supreme-gate.test.mjs']; expect(filterInScope(input, ALLOWED_SCOPES, SCAN_EXCLUDE_DIRS)).toEqual([]); }); it('keeps tools/ source but drops its sibling test file', () => { const input = ['tools/floor-decide.mjs', 'tools/floor-decide.test.mjs']; expect(filterInScope(input, ALLOWED_SCOPES, SCAN_EXCLUDE_DIRS)).toEqual(['tools/floor-decide.mjs']); }); it('rejects the removed app/ scope and other out-of-scope paths', () => { const input = [ 'app/Models/Deal.php', 'CHANGELOG.md', 'bin/something.exe', 'package.json', 'README.md', ]; expect(filterInScope(input, ALLOWED_SCOPES, SCAN_EXCLUDE_DIRS)).toEqual([]); }); it('rejects vendor/node_modules/.git even if nominally under an allowed scope', () => { const input = [ 'tools/vendor/foo/bar.mjs', 'docs/node_modules/foo/bar.js', 'tools/__pycache__/x.pyc', '.claude/.git/HEAD', ]; expect(filterInScope(input, ALLOWED_SCOPES, SCAN_EXCLUDE_DIRS)).toEqual([]); }); 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/floor-decide.mjs', 'tools/floor-decide.test.mjs', 'app/Models/Deal.php', 'CHANGELOG.md', '.claude/skills/y/SKILL.md', ]; expect(filterInScope(input, ALLOWED_SCOPES, SCAN_EXCLUDE_DIRS)).toEqual([ 'docs/CLAUDE.md', 'tools/floor-decide.mjs', '.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([]); }); });