From 29a5cbae4ca7bd815f12c3380023592222b2f261 Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Sun, 29 Mar 2026 17:59:44 +0300 Subject: [PATCH] feat(skill-tests): platform integration tests via .v8-project.json MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Runner reads v8path from .v8-project.json, skips platform tests if 1cv8.exe unavailable. Placeholders: {v8path}, {v8exe}, {dbPath}, etc. - platform-config: cf-init → meta-compile → db-create → load → update - platform-epf: epf-init → epf-build → db-create → epf-dump (roundtrip) - platform-cfe: config + extension → db-create → load both → update both All 6 integration tests green (3 file-only + 3 platform). Co-Authored-By: Claude Opus 4.6 (1M context) --- .../skills/integration/platform-cfe.test.mjs | 93 +++++++++++++++++++ .../integration/platform-config.test.mjs | 74 +++++++++++++++ .../skills/integration/platform-epf.test.mjs | 46 +++++++++ tests/skills/runner.mjs | 65 +++++++++++-- 4 files changed, 270 insertions(+), 8 deletions(-) create mode 100644 tests/skills/integration/platform-cfe.test.mjs create mode 100644 tests/skills/integration/platform-config.test.mjs create mode 100644 tests/skills/integration/platform-epf.test.mjs diff --git a/tests/skills/integration/platform-cfe.test.mjs b/tests/skills/integration/platform-cfe.test.mjs new file mode 100644 index 00000000..4439bb0c --- /dev/null +++ b/tests/skills/integration/platform-cfe.test.mjs @@ -0,0 +1,93 @@ +// platform-cfe.test.mjs — Integration test: load CFE extension into 1C platform +// Requires: 1C platform (1cv8.exe) via .v8-project.json +// Steps: build config → build extension → db-create → load config → load extension → update + +export const name = 'Загрузка расширения в базу с конфигурацией'; +export const setup = 'none'; +export const requiresPlatform = true; + +export const steps = [ + // ── 1. Build minimal base config ── + { + name: 'cf-init: базовая конфигурация', + script: 'cf-init/scripts/cf-init', + args: { '-Name': 'БазаДляРасширения', '-OutputDir': '{workDir}/config' }, + }, + { + name: 'meta-compile: Справочник Контрагенты', + script: 'meta-compile/scripts/meta-compile', + input: { type: 'Catalog', name: 'Контрагенты', codeLength: 9, descriptionLength: 100 }, + args: { '-JsonPath': '{inputFile}', '-OutputDir': '{workDir}/config' }, + }, + { + name: 'cf-edit: регистрация + совместимость интерфейса', + script: 'cf-edit/scripts/cf-edit', + input: [ + { operation: 'add-childObject', value: 'Catalog.Контрагенты' }, + { operation: 'modify-property', value: 'InterfaceCompatibilityMode=TaxiEnableVersion8_2' }, + ], + args: { '-ConfigPath': '{workDir}/config', '-DefinitionFile': '{inputFile}' }, + }, + + // ── 2. Build extension (borrow without forms) ── + { + name: 'cfe-init: расширение', + script: 'cfe-init/scripts/cfe-init', + args: { + '-Name': 'ТестРасширение', + '-OutputDir': '{workDir}/ext', + '-ConfigPath': '{workDir}/config', + }, + }, + { + name: 'cfe-borrow: заимствование Catalog.Контрагенты', + script: 'cfe-borrow/scripts/cfe-borrow', + args: { + '-ExtensionPath': '{workDir}/ext', + '-ConfigPath': '{workDir}/config', + '-Object': 'Catalog.Контрагенты', + }, + }, + + // ── 3. Create DB, load config ── + { + name: 'db-create: создание ИБ', + script: 'db-create/scripts/db-create', + args: { '-V8Path': '{v8path}', '-InfoBasePath': '{workDir}/testdb' }, + }, + { + name: 'db-load-xml: загрузка конфигурации', + script: 'db-load-xml/scripts/db-load-xml', + args: { + '-V8Path': '{v8path}', + '-InfoBasePath': '{workDir}/testdb', + '-ConfigDir': '{workDir}/config', + }, + }, + { + name: 'db-update: обновление БД (конфигурация)', + script: 'db-update/scripts/db-update', + args: { '-V8Path': '{v8path}', '-InfoBasePath': '{workDir}/testdb' }, + }, + + // ── 4. Load extension ── + { + name: 'db-load-xml: загрузка расширения', + script: 'db-load-xml/scripts/db-load-xml', + args: { + '-V8Path': '{v8path}', + '-InfoBasePath': '{workDir}/testdb', + '-ConfigDir': '{workDir}/ext', + '-Extension': 'ТестРасширение', + }, + }, + { + name: 'db-update: обновление БД (расширение)', + script: 'db-update/scripts/db-update', + args: { + '-V8Path': '{v8path}', + '-InfoBasePath': '{workDir}/testdb', + '-Extension': 'ТестРасширение', + }, + }, +]; diff --git a/tests/skills/integration/platform-config.test.mjs b/tests/skills/integration/platform-config.test.mjs new file mode 100644 index 00000000..114a66d4 --- /dev/null +++ b/tests/skills/integration/platform-config.test.mjs @@ -0,0 +1,74 @@ +// platform-config.test.mjs — Integration test: load config into 1C platform +// Requires: 1C platform (1cv8.exe) via .v8-project.json +// Steps: cf-init → meta-compile (objects without forms) → cf-edit → db-create → db-load-xml → db-update + +export const name = 'Загрузка конфигурации в платформу 1С'; +export const setup = 'none'; +export const requiresPlatform = true; + +export const steps = [ + // ── 1. Build minimal config (no forms — avoids ExtendedPresentation issue) ── + { + name: 'cf-init: пустая конфигурация', + script: 'cf-init/scripts/cf-init', + args: { '-Name': 'ПлатформенныйТест', '-OutputDir': '{workDir}/config' }, + }, + { + name: 'meta-compile: Справочник', + script: 'meta-compile/scripts/meta-compile', + input: { type: 'Catalog', name: 'Товары', codeLength: 9, descriptionLength: 100 }, + args: { '-JsonPath': '{inputFile}', '-OutputDir': '{workDir}/config' }, + }, + { + name: 'meta-compile: Документ', + script: 'meta-compile/scripts/meta-compile', + input: { + type: 'Document', name: 'Приход', + attributes: [{ name: 'Склад', type: 'String', length: 50 }], + }, + args: { '-JsonPath': '{inputFile}', '-OutputDir': '{workDir}/config' }, + }, + { + name: 'meta-compile: Перечисление', + script: 'meta-compile/scripts/meta-compile', + input: { type: 'Enum', name: 'Статусы', values: ['Новый', 'Выполнен'] }, + args: { '-JsonPath': '{inputFile}', '-OutputDir': '{workDir}/config' }, + }, + { + name: 'cf-edit: регистрация объектов', + script: 'cf-edit/scripts/cf-edit', + input: [ + { operation: 'add-childObject', value: 'Catalog.Товары' }, + { operation: 'add-childObject', value: 'Document.Приход' }, + { operation: 'add-childObject', value: 'Enum.Статусы' }, + ], + args: { '-ConfigPath': '{workDir}/config', '-DefinitionFile': '{inputFile}' }, + }, + + // ── 2. Create DB and load ── + { + name: 'db-create: создание файловой ИБ', + script: 'db-create/scripts/db-create', + args: { + '-V8Path': '{v8path}', + '-InfoBasePath': '{workDir}/testdb', + }, + }, + { + name: 'db-load-xml: загрузка конфигурации', + script: 'db-load-xml/scripts/db-load-xml', + args: { + '-V8Path': '{v8path}', + '-InfoBasePath': '{workDir}/testdb', + '-ConfigDir': '{workDir}/config', + }, + }, + { + name: 'db-update: обновление БД', + script: 'db-update/scripts/db-update', + args: { + '-V8Path': '{v8path}', + '-InfoBasePath': '{workDir}/testdb', + }, + }, +]; diff --git a/tests/skills/integration/platform-epf.test.mjs b/tests/skills/integration/platform-epf.test.mjs new file mode 100644 index 00000000..e93354df --- /dev/null +++ b/tests/skills/integration/platform-epf.test.mjs @@ -0,0 +1,46 @@ +// platform-epf.test.mjs — Integration test: EPF build/dump roundtrip +// Requires: 1C platform (1cv8.exe) via .v8-project.json +// Steps: epf-init (no forms) → epf-build → epf-dump + +export const name = 'Сборка и разборка внешней обработки (roundtrip)'; +export const setup = 'none'; +export const requiresPlatform = true; + +export const steps = [ + // ── 1. Create EPF without forms (avoids ExtendedPresentation issue) ── + { + name: 'epf-init: пустая обработка', + script: 'epf-init/scripts/init', + args: { '-Name': 'RoundtripТест', '-SrcDir': '{workDir}' }, + }, + + // ── 2. Build EPF binary ── + { + name: 'epf-build: сборка EPF', + script: 'epf-build/scripts/epf-build', + args: { + '-V8Path': '{v8path}', + '-SourceFile': '{workDir}/RoundtripТест.xml', + '-OutputFile': '{workDir}/RoundtripТест.epf', + }, + }, + + // ── 3. Create temp DB for dump (epf-dump requires database connection) ── + { + name: 'db-create: временная ИБ для разборки', + script: 'db-create/scripts/db-create', + args: { '-V8Path': '{v8path}', '-InfoBasePath': '{workDir}/tmpdb' }, + }, + + // ── 4. Dump EPF back to XML ── + { + name: 'epf-dump: разборка EPF в XML', + script: 'epf-dump/scripts/epf-dump', + args: { + '-V8Path': '{v8path}', + '-InputFile': '{workDir}/RoundtripТест.epf', + '-OutputDir': '{workDir}/roundtrip-dump', + '-InfoBasePath': '{workDir}/tmpdb', + }, + }, +]; diff --git a/tests/skills/runner.mjs b/tests/skills/runner.mjs index 3785aa69..18ffec03 100644 --- a/tests/skills/runner.mjs +++ b/tests/skills/runner.mjs @@ -841,6 +841,29 @@ async function runPool(cases, opts) { const INTEGRATION = resolve(ROOT, 'integration'); +// ─── Platform context (.v8-project.json) ───────────────────────────────────── + +function loadV8Context() { + const projectFile = join(REPO_ROOT, '.v8-project.json'); + if (!existsSync(projectFile)) return null; + try { + const proj = JSON.parse(readFileSync(projectFile, 'utf8')); + const v8bin = proj.v8path; + const v8exe = v8bin ? (existsSync(join(v8bin, '1cv8.exe')) ? join(v8bin, '1cv8.exe') : null) : null; + if (!v8exe) return null; + const defaultDb = proj.databases?.find(d => d.id === proj.default) || proj.databases?.[0]; + return { + v8path: v8bin, + v8exe, + dbPath: defaultDb?.path || '', + dbUser: defaultDb?.user || '', + dbPassword: defaultDb?.password || '', + configSrc: defaultDb?.configSrc || '', + databases: proj.databases || [], + }; + } catch { return null; } +} + async function discoverIntegration(filter) { if (!existsSync(INTEGRATION)) return []; const results = []; @@ -850,7 +873,7 @@ async function discoverIntegration(filter) { const id = `integration/${testName}`; if (filter && !id.startsWith(filter) && !id.includes(filter)) continue; const mod = await import(`file://${join(INTEGRATION, file).replace(/\\/g, '/')}`); - results.push({ id, name: mod.name || testName, steps: mod.steps || [], file, cache: mod.cache, setup: mod.setup || 'empty-config' }); + results.push({ id, name: mod.name || testName, steps: mod.steps || [], file, cache: mod.cache, setup: mod.setup || 'empty-config', requiresPlatform: !!mod.requiresPlatform }); } return results; } @@ -860,12 +883,34 @@ async function runIntegrationTest(test, opts) { const stepResults = []; let workspace = null; + // Skip platform-dependent tests if platform unavailable + if (test.requiresPlatform && !opts.v8ctx) { + const elapsed = ((performance.now() - t0) / 1000).toFixed(1); + return { id: test.id, name: test.name, passed: true, skipped: true, steps: [], elapsed: `${elapsed}s`, errors: [] }; + } + try { // Start from configured fixture or empty workspace const fixturePath = test.setup === 'none' ? null : ensureSetup(test.setup, opts.runtime, CASES); + if (fixturePath === SKIP) { + const elapsed = ((performance.now() - t0) / 1000).toFixed(1); + return { id: test.id, name: test.name, passed: true, skipped: true, steps: [], elapsed: `${elapsed}s`, errors: [] }; + } workspace = createWorkspace(fixturePath, false); const workDir = workspace.path; + // Platform placeholders + const v8 = opts.v8ctx || {}; + const replacePlaceholders = (s) => s + .replace('{workDir}', workDir) + .replace('{inputFile}', '') + .replace('{v8path}', v8.v8path || '') + .replace('{v8exe}', v8.v8exe || '') + .replace('{dbPath}', v8.dbPath || '') + .replace('{dbUser}', v8.dbUser || '') + .replace('{dbPassword}', v8.dbPassword || '') + .replace('{configSrc}', v8.configSrc || ''); + for (let i = 0; i < test.steps.length; i++) { const step = test.steps[i]; const stepT0 = performance.now(); @@ -877,15 +922,15 @@ async function runIntegrationTest(test, opts) { writeFileSync(inputFile, JSON.stringify(step.input, null, 2), 'utf8'); } - // Resolve args: replace {workDir} and {inputFile} + // Resolve args: replace placeholders const script = resolveScript(step.script, opts.runtime); const args = []; for (const [flag, value] of Object.entries(step.args || {})) { args.push(flag); if (value === true) continue; // switch - args.push(String(value) - .replace('{workDir}', workDir) - .replace('{inputFile}', inputFile || '')); + let resolved = String(value).replace('{inputFile}', inputFile || ''); + resolved = replacePlaceholders(resolved); + args.push(resolved); } // Execute @@ -894,7 +939,7 @@ async function runIntegrationTest(test, opts) { stdout = await execSkillAsync(opts.runtime, script, args); } catch (e) { const detail = e.stderr?.trim() || e.stdout?.trim() || e.message; - stepResults.push({ name: step.name, passed: false, error: `Step ${i + 1} failed: ${detail.substring(0, 500)}` }); + stepResults.push({ name: step.name, passed: false, error: `Step ${i + 1} failed: ${detail.substring(0, 1000)}` }); break; // stop on first failure } @@ -942,8 +987,9 @@ async function runIntegrationTest(test, opts) { function printIntegrationReport(results, opts) { console.log(''); for (const r of results) { - const icon = r.passed ? '\u2713' : '\u2717'; - console.log(` ${icon} ${r.name} (${r.elapsed}) ${r.id}`); + const icon = r.skipped ? '\u25CB' : r.passed ? '\u2713' : '\u2717'; + const suffix = r.skipped ? ' [skipped — no platform]' : ''; + console.log(` ${icon} ${r.name} (${r.elapsed}) ${r.id}${suffix}`); for (const step of r.steps) { const sIcon = step.passed ? '\u2713' : '\u2717'; console.log(` ${sIcon} ${step.name}${step.elapsed ? ` (${step.elapsed})` : ''}`); @@ -968,6 +1014,9 @@ async function main() { const opts = parseArgs(process.argv); mkdirSync(CACHE, { recursive: true }); + // Load platform context for platform-dependent tests + opts.v8ctx = loadV8Context(); + const isIntegrationFilter = opts.filter && opts.filter.startsWith('integration'); // Run integration tests if filter matches or no filter (run both)