mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-07-14 06:45:17 +03:00
test(meta-edit): guard-инвариант сохранения uuid существующих сущностей
check-uuid-invariant.mjs: компилирует объект (Catalog + реквизиты + ТЧ), фиксирует ВСЕ uuid (тип-элемент + GeneratedType TypeId/ValueId + реквизиты/ТЧ), применяет широкую правку (rename + смена типа + структурные свойства + свойства объекта + ТЧ + add + remove), проверяет что uuid существующих сущностей ЦЕЛЫ (удалённый — ушёл). Оба рантаймa. Закрывает пробел: снапшот-тесты нормализуют uuid позиционно и НЕ ловят перегенерацию id. Инвариант критичен (смена uuid рвёт ссылки/данные/состояние поддержки) и станет защитой для будущих операций над предопределёнными (их <Item id=GUID> тоже нельзя перегенерировать). Проверено: 20 uuid, ps1+py OK. Запуск после правок meta-edit (как check-enum-drift.mjs). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
#!/usr/bin/env node
|
||||
// Инвариант: НИ ОДНА операция meta-edit не меняет идентификатор существующей сущности —
|
||||
// ни объекта, ни реквизита/измерения/ресурса/ТЧ, ни GeneratedType (TypeId/ValueId).
|
||||
// Смена uuid рвёт ссылки/данные/состояние поддержки. Снапшот-тесты это НЕ ловят
|
||||
// (нормализуют uuid позиционно), поэтому нужен отдельный guard.
|
||||
//
|
||||
// Компилирует объект, фиксирует uuid'ы, применяет широкую правку (rename+type+структурные
|
||||
// свойства+свойства объекта+ТЧ+add+remove), сверяет что uuid существующих сущностей целы.
|
||||
// Прогоняет оба рантайма. Выход 1 при нарушении. Запуск: node tests/skills/check-uuid-invariant.mjs [--runtime python]
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { readFileSync, writeFileSync, mkdtempSync, rmSync, mkdirSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { tmpdir } from 'node:os';
|
||||
|
||||
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
||||
const runtimes = process.argv.includes('--runtime')
|
||||
? [process.argv[process.argv.indexOf('--runtime') + 1] === 'python' ? 'python' : 'powershell']
|
||||
: ['powershell', 'python'];
|
||||
|
||||
function skill(runtime, name, args, cwd) {
|
||||
const ext = runtime === 'python' ? '.py' : '.ps1';
|
||||
const p = join(ROOT, '.claude/skills', name, 'scripts', name + ext);
|
||||
if (runtime === 'python') {
|
||||
execFileSync(process.env.PYTHON || 'python', [p, ...args], { cwd, stdio: 'pipe' });
|
||||
} else {
|
||||
execFileSync('powershell.exe', ['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-File', p, ...args], { cwd, stdio: 'pipe' });
|
||||
}
|
||||
}
|
||||
|
||||
// Множество ВСЕХ идентификаторов объекта: каждый uuid="..." (тип-элемент, реквизиты, ТЧ,
|
||||
// формы, команды…) + GeneratedType TypeId/ValueId. Надёжнее, чем маппинг по имени
|
||||
// (ТЧ имеет InternalInfo между uuid и Properties).
|
||||
function collectUuids(xmlPath) {
|
||||
const s = readFileSync(xmlPath, 'utf8');
|
||||
const set = new Set([...s.matchAll(/\buuid="([0-9a-f-]{36})"/g)].map(m => m[1]));
|
||||
for (const m of s.matchAll(/<xr:(?:TypeId|ValueId)>([0-9a-f-]{36})</g)) set.add(m[1]);
|
||||
return set;
|
||||
}
|
||||
|
||||
// uuid реквизита по имени (для исключения намеренно удаляемого; реквизит имеет <Properties> сразу).
|
||||
function attrUuid(xmlPath, name) {
|
||||
const s = readFileSync(xmlPath, 'utf8');
|
||||
const m = s.match(new RegExp(`<Attribute uuid="([0-9a-f-]{36})">\\s*<Properties>\\s*<Name>${name}</Name>`));
|
||||
return m ? m[1] : null;
|
||||
}
|
||||
|
||||
let failures = 0;
|
||||
|
||||
for (const runtime of runtimes) {
|
||||
let work;
|
||||
try {
|
||||
work = mkdtempSync(join(tmpdir(), 'uuidinv-'));
|
||||
// 1. Конфигурация + объект
|
||||
skill(runtime, 'cf-init', ['-OutputDir', work, '-Name', 'Т'], work);
|
||||
const inp = join(work, 'c.json');
|
||||
writeFileSync(inp, JSON.stringify({
|
||||
type: 'Catalog', name: 'Спр',
|
||||
attributes: ['Комм: String(50)', 'Сумма: Number(15,2)', 'Удаляемый: String(10)'],
|
||||
tabularSections: { 'Товары': ['Цена: Number(15,2)', 'Кол: Number(15,3)'] },
|
||||
}), 'utf8');
|
||||
skill(runtime, 'meta-compile', ['-JsonPath', inp, '-OutputDir', work], work);
|
||||
const objXml = join(work, 'Catalogs', 'Спр.xml');
|
||||
|
||||
const before = collectUuids(objXml);
|
||||
const removedUuid = attrUuid(objXml, 'Удаляемый');
|
||||
|
||||
// 2. Широкая правка существующих сущностей + add + remove
|
||||
const edit = join(work, 'e.json');
|
||||
writeFileSync(edit, JSON.stringify({
|
||||
modify: {
|
||||
properties: { CodeLength: 15, DataLockFields: ['Сумма'] },
|
||||
attributes: {
|
||||
'Комм': { name: 'Комментарий', type: 'String(200)' },
|
||||
'Сумма': { MinValue: 0, Format: 'ЧЦ=15; ЧДЦ=2' },
|
||||
},
|
||||
tabularSections: { 'Товары': { modify: { 'Цена': { name: 'ЦенаНовая' } } } },
|
||||
},
|
||||
add: { attributes: ['НовыйРекв: Boolean'] },
|
||||
remove: { attributes: ['Удаляемый'] },
|
||||
}), 'utf8');
|
||||
skill(runtime, 'meta-edit', ['-ObjectPath', objXml, '-DefinitionFile', edit, '-NoValidate'], work);
|
||||
|
||||
const after = collectUuids(objXml);
|
||||
|
||||
// 3. Инвариант: каждый uuid, существовавший ДО правки (кроме намеренно удалённого),
|
||||
// должен присутствовать ПОСЛЕ (переименование/смена типа/структурные свойства НЕ меняют id).
|
||||
const fail = (msg) => { console.log(`[${runtime}] НАРУШЕНИЕ: ${msg}`); failures++; };
|
||||
let checked = 0;
|
||||
for (const uuid of before) {
|
||||
if (uuid === removedUuid) continue; // Attribute «Удаляемый» намеренно удалён
|
||||
checked++;
|
||||
if (!after.has(uuid)) fail(`uuid ${uuid} пропал после правки (перегенерирован?)`);
|
||||
}
|
||||
if (removedUuid && after.has(removedUuid)) fail(`uuid удалённого реквизита ${removedUuid} остался (не удалён?)`);
|
||||
console.log(`[${runtime}] проверено ${checked} uuid (объект + GeneratedType + реквизиты/ТЧ), удалённый: ${removedUuid ? 'ок' : 'НЕ НАЙДЕН'}`);
|
||||
} catch (e) {
|
||||
console.log(`[${runtime}] ОШИБКА прогона: ${(e.stderr || e.message || '').toString().slice(0, 300)}`);
|
||||
failures++;
|
||||
} finally {
|
||||
if (work) try { rmSync(work, { recursive: true, force: true }); } catch {}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(failures === 0
|
||||
? 'OK — инвариант сохранения uuid держится (объект/сущности/GeneratedType)'
|
||||
: `\n${failures} НАРУШЕНИЙ инварианта uuid.`);
|
||||
process.exit(failures ? 1 : 0);
|
||||
Reference in New Issue
Block a user