10ff755649
Closes the 9 pre-existing name@source drifts that prevented strict mode: settings.json lists each marketplace plugin by machine name (e.g. "frontend-design@claude-plugins-official"), while Tooling Прил. Н describes them under a human/group name (e.g. "Frontend Design plugin", "Trail of Bits Skills" — single row #39 for 8 sub-plugins). Mechanism: - tools/.l1-watcher-aliases.txt — settings_name=tooling_substring map. - detectDrift(settings, tooling, aliases): direct match first, then alias-substring fallback. Settings name considered formalized if Tooling text includes either the name itself or aliases[name]. - parseAliases(raw) exported — line-based KV parser with #-comments and split-on-first-= semantics (values may contain "="). TDD: 6 new tests (3 detectDrift + 4 parseAliases). 12/12 GREEN. Smoke: node tools/l1-watcher.mjs -> exit 0, "OK — 0 drift". Known cosmetic baseline issue (pre-existing, not introduced here): the missingInSettings WARN list is noisy — regex /#\d+\s+([\w-]+(?:@[\w-]+)?)/g captures the first \w+ after "#NN" even when it is a plain word (e.g. "#1 PostgreSQL MCP" -> "PostgreSQL"), producing ~190 WARN entries. WARN is non-blocking, so strict mode flip in Phase 3 is unaffected; a follow-up filter on names containing "@" would silence this without behavioural change. Refs: ADR-011 brain governance §6.1 (C1 L1-watcher detector for the "plugin in settings.json without Tooling formalization" L1 pattern). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
113 lines
4.1 KiB
JavaScript
113 lines
4.1 KiB
JavaScript
import { describe, it, expect } from 'vitest';
|
|
import { detectDrift, parseAliases } from './l1-watcher.mjs';
|
|
|
|
describe('detectDrift', () => {
|
|
it('finds plugins in settings but not in tooling', () => {
|
|
const settings = { enabledPlugins: { 'foo@org': true, 'bar@org': true } };
|
|
const tooling = 'Описание #56 foo@org интегрирован.';
|
|
const drift = detectDrift(settings, tooling);
|
|
expect(drift.missingInTooling).toEqual(['bar@org']);
|
|
expect(drift.missingInSettings).toEqual([]);
|
|
});
|
|
|
|
it('finds plugins in tooling but not in settings', () => {
|
|
const settings = { enabledPlugins: { 'foo@org': true } };
|
|
const tooling = '#56 foo@org. #57 baz@org включён.';
|
|
const drift = detectDrift(settings, tooling);
|
|
expect(drift.missingInSettings).toEqual(['baz@org']);
|
|
});
|
|
|
|
it('returns empty arrays when in sync', () => {
|
|
const settings = { enabledPlugins: { 'foo@org': true } };
|
|
const tooling = '#56 foo@org описан.';
|
|
const drift = detectDrift(settings, tooling);
|
|
expect(drift.missingInTooling).toEqual([]);
|
|
expect(drift.missingInSettings).toEqual([]);
|
|
});
|
|
|
|
it('handles disabled plugins (value false)', () => {
|
|
const settings = { enabledPlugins: { 'foo@org': false, 'bar@org': true } };
|
|
const tooling = '#56 bar@org.';
|
|
const drift = detectDrift(settings, tooling);
|
|
expect(drift.missingInTooling).toEqual([]);
|
|
});
|
|
|
|
it('uses alias to match plugin under group/human name in tooling', () => {
|
|
const settings = {
|
|
enabledPlugins: { 'frontend-design@claude-plugins-official': true },
|
|
};
|
|
const tooling = '#30 Frontend Design plugin (paired stack)';
|
|
const aliases = {
|
|
'frontend-design@claude-plugins-official': 'Frontend Design plugin',
|
|
};
|
|
const drift = detectDrift(settings, tooling, aliases);
|
|
expect(drift.missingInTooling).toEqual([]);
|
|
});
|
|
|
|
it('several plugins share one alias (Trail of Bits group)', () => {
|
|
const settings = {
|
|
enabledPlugins: {
|
|
'differential-review@trailofbits': true,
|
|
'sharp-edges@trailofbits': true,
|
|
},
|
|
};
|
|
const tooling = '#39 Trail of Bits Skills (субсет 8 audit-плагинов)';
|
|
const aliases = {
|
|
'differential-review@trailofbits': 'Trail of Bits Skills',
|
|
'sharp-edges@trailofbits': 'Trail of Bits Skills',
|
|
};
|
|
const drift = detectDrift(settings, tooling, aliases);
|
|
expect(drift.missingInTooling).toEqual([]);
|
|
});
|
|
|
|
it('falls back to direct match when no alias defined', () => {
|
|
const settings = { enabledPlugins: { 'foo@org': true } };
|
|
const tooling = 'no foo here';
|
|
const drift = detectDrift(settings, tooling, {});
|
|
expect(drift.missingInTooling).toEqual(['foo@org']);
|
|
});
|
|
|
|
it('alias does NOT save a plugin when alias substring is also absent', () => {
|
|
const settings = { enabledPlugins: { 'foo@org': true } };
|
|
const tooling = 'unrelated text';
|
|
const aliases = { 'foo@org': 'Some Group Name' };
|
|
const drift = detectDrift(settings, tooling, aliases);
|
|
expect(drift.missingInTooling).toEqual(['foo@org']);
|
|
});
|
|
});
|
|
|
|
describe('parseAliases', () => {
|
|
it('parses key=value lines and ignores comments and blanks', () => {
|
|
const raw = [
|
|
'# header comment',
|
|
'',
|
|
'frontend-design@claude-plugins-official=Frontend Design plugin',
|
|
' ',
|
|
'# another comment',
|
|
'sharp-edges@trailofbits=Trail of Bits Skills',
|
|
'',
|
|
].join('\n');
|
|
const aliases = parseAliases(raw);
|
|
expect(aliases).toEqual({
|
|
'frontend-design@claude-plugins-official': 'Frontend Design plugin',
|
|
'sharp-edges@trailofbits': 'Trail of Bits Skills',
|
|
});
|
|
});
|
|
|
|
it('returns empty object on empty/null input', () => {
|
|
expect(parseAliases('')).toEqual({});
|
|
expect(parseAliases(null)).toEqual({});
|
|
expect(parseAliases(undefined)).toEqual({});
|
|
});
|
|
|
|
it('trims whitespace around key and value', () => {
|
|
const raw = ' foo@org = Bar Name ';
|
|
expect(parseAliases(raw)).toEqual({ 'foo@org': 'Bar Name' });
|
|
});
|
|
|
|
it('handles values that contain = sign (only splits on first =)', () => {
|
|
const raw = 'a@b=v1=v2';
|
|
expect(parseAliases(raw)).toEqual({ 'a@b': 'v1=v2' });
|
|
});
|
|
});
|