mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-06-13 17:34:57 +03:00
feat(skill-tests): platform integration tests via .v8-project.json
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) <noreply@anthropic.com>
This commit is contained in:
@@ -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': 'ТестРасширение',
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
];
|
||||
+57
-8
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user