import fs from 'fs/promises'; import path from 'path'; import { WorkflowExecutor } from './src/vl/workflow-executor.js'; let passed = 0; let failed = 0; function test(name, fn) { return Promise.resolve() .then(fn) .then(() => { console.log(` ✓ ${name}`); passed++; }) .catch((err) => { console.log(` ✗ ${name}: ${err.message}`); failed++; }); } function assert(cond, msg) { if (!cond) throw new Error(msg || 'Assertion failed'); } function json(value) { return JSON.stringify(value); } function normalizeMessages(messages = []) { return messages.map((msg) => ({ role: msg.role, text: typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content), })); } async function collectFiles(dir, prefix = '') { const entries = await fs.readdir(path.join(dir, prefix), { withFileTypes: true }).catch(() => []); const files = []; for (const entry of entries) { const rel = path.join(prefix, entry.name); if (entry.isDirectory()) files.push(...await collectFiles(dir, rel)); else files.push(rel); } return files.sort(); } function makeCurrentMeta() { return { specVersion: 'ProjectMeta/1.0', projectName: 'OpsSuite', vlVersion: '3.5', config: { defaultDevice: 'Phone', defaultResolution: '375x812', themeFile: 'Theme/BrandTheme.vth', colorScheme: 'light', }, database: { file: 'Database/AppDB.vdb' }, theme: { file: 'Theme/BrandTheme.vth' }, services: [ { id: 'ReportService', domainId: 'ReportService', file: 'Services/ReportService.vs', filePath: 'Services/ReportService.vs' }, ], components: [ { id: 'StatusCard', file: 'ExtComponents/StatusCard.cp', filePath: 'ExtComponents/StatusCard.cp' }, ], sections: [ { id: 'Dashboard', file: 'Sections/Dashboard.sc', filePath: 'Sections/Dashboard.sc' }, ], apps: [ { id: 'MainApp', file: 'Apps/MainApp.vx', filePath: 'Apps/MainApp.vx' }, { id: 'AdminApp', file: 'Apps/AdminPortalShell.vx', filePath: 'Apps/AdminPortalShell.vx' }, ], }; } async function seedProject(workDir) { const files = { '.vl-code/ProjectMeta.json': json(makeCurrentMeta()), 'Database/AppDB.vdb': '// VL_VERSION:3.5\nDATABASE AppDB { }\n', 'Theme/BrandTheme.vth': '// VL_VERSION:3.5\nTHEME BrandTheme {\n COLOR primary = #005B4F\n}\n', 'Services/ReportService.vs': '// VL_VERSION:3.5\nSERVICE ReportService {\n METHOD list(): [OBJECT]\n}\n', 'Sections/Dashboard.sc': '// VL_VERSION:3.5\nSECTION Dashboard {\n INIT { }\n}\n', 'ExtComponents/StatusCard.cp': '// VL_VERSION:3.5\nCOMPONENT StatusCard {\n VIEW { Text("status") }\n}\n', 'Apps/MainApp.vx': '// VL_VERSION:3.5\nAPP MainApp {\n ROUTES { }\n}\n', 'Apps/AdminPortalShell.vx': '// VL_VERSION:3.5\nAPP AdminApp {\n ROUTES { path:\"/dashboard\" -> Dashboard }\n}\n', }; for (const [rel, content] of Object.entries(files)) { const full = path.join(workDir, rel); await fs.mkdir(path.dirname(full), { recursive: true }); await fs.writeFile(full, content, 'utf8'); } } function createAdjustLLM() { let themeCascadeCount = 0; return { async call(params) { const allText = normalizeMessages(params.messages).map((msg) => msg.text).join('\n'); if (allText.includes('Plan the new page. Determine')) { const updatedMeta = makeCurrentMeta(); updatedMeta.sections = [ ...updatedMeta.sections, { id: 'AnalyticsPage', file: 'Sections/AnalyticsPage.sc', filePath: 'Sections/AnalyticsPage.sc', consumesServices: ['ReportService'], usesComponents: ['KpiCard'], }, ]; updatedMeta.components = [ ...updatedMeta.components, { id: 'KpiCard', file: 'ExtComponents/KpiCard.cp', filePath: 'ExtComponents/KpiCard.cp' }, ]; return { content: json({ section: { id: 'AnalyticsPage', filePath: 'Sections/AnalyticsPage.sc', description: 'Analytics dashboard page', consumesServices: ['ReportService'], usesComponents: ['KpiCard'], bindings: [], events: [], }, components: [ { id: 'KpiCard', name: 'KpiCard', filePath: 'ExtComponents/KpiCard.cp', description: 'Summary KPI card' }, ], service: { name: 'ReportService', domainId: 'ReportService', filePath: 'Services/ReportService.vs', methods: [{ id: 'getAnalytics' }], }, targetApp: 'AdminApp', targetAppFilePath: 'Apps/AdminPortalShell.vx', route: '/analytics', updatedMeta, }), model: 'fake-adjust', usage: {}, }; } if (allText.includes('Plan the new service domain. Determine')) { const updatedMeta = makeCurrentMeta(); updatedMeta.services = [ ...updatedMeta.services, { id: 'AuditService', domainId: 'AuditService', file: 'Services/AuditService.vs', filePath: 'Services/AuditService.vs' }, ]; return { content: json({ service: { name: 'AuditService', domainId: 'AuditService', filePath: 'Services/AuditService.vs', methods: [{ id: 'listAuditLogs' }], routes: [], }, dbChanges: { newTables: [{ id: 'AuditLog' }], modifiedTables: [], }, updatedMeta, }), model: 'fake-adjust', usage: {}, }; } if (allText.includes('Analyze the current theme and plan the changes')) { return { content: json({ tokenChanges: [{ token: 'primary', oldValue: '#005B4F', newValue: '#0A7A68' }], affectedFiles: [ { file: 'Sections/Dashboard.sc', reason: 'Uses primary theme token' }, { file: 'ExtComponents/StatusCard.cp', reason: 'Uses primary theme token' }, ], summary: 'Refresh primary accent color', }), model: 'fake-adjust', usage: {}, }; } if (allText.includes('Generate the .sc section file for this new section')) { return { content: '// VL_VERSION:3.5\nSECTION AnalyticsPage {\n INIT { }\n}\n', model: 'fake-adjust', usage: {} }; } if (allText.includes('Generate the .cp component file for:')) { return { content: '// VL_VERSION:3.5\nCOMPONENT KpiCard {\n VIEW { Text("kpi") }\n}\n', model: 'fake-adjust', usage: {} }; } if (allText.includes('Add or update the service domain with new methods for the page')) { return { content: '// VL_VERSION:3.5\nSERVICE ReportService {\n METHOD list(): [OBJECT]\n METHOD getAnalytics(): OBJECT\n}\n', model: 'fake-adjust', usage: {} }; } if (allText.includes('Update the app .vx file to include the new page route')) { return { content: '// VL_VERSION:3.5\nAPP AdminApp {\n ROUTES { path:"/dashboard" -> Dashboard, path:"/analytics" -> AnalyticsPage }\n}\n', model: 'fake-adjust', usage: {} }; } if (allText.includes('Update the .vdb database schema file')) { return { content: '// VL_VERSION:3.5\nDATABASE AppDB {\n TABLE AuditLog { id INT }\n}\n', model: 'fake-adjust', usage: {} }; } if (allText.includes('Generate the .vs service domain file for:')) { return { content: '// VL_VERSION:3.5\nSERVICE AuditService {\n METHOD listAuditLogs(): [OBJECT]\n}\n', model: 'fake-adjust', usage: {} }; } if (allText.includes('Generate the COMPLETE updated .vth theme file')) { return { content: '// VL_VERSION:3.5\nTHEME BrandTheme {\n COLOR primary = #0A7A68\n}\n', model: 'fake-adjust', usage: {} }; } if (allText.includes('Update this file to use the new theme tokens')) { themeCascadeCount += 1; if (themeCascadeCount === 1) { return { content: '// VL_VERSION:3.5\nSECTION Dashboard {\n INIT { }\n VIEW { Text("dashboard") }\n}\n', model: 'fake-adjust', usage: {} }; } return { content: '// VL_VERSION:3.5\nCOMPONENT StatusCard {\n VIEW { Text("status-updated") }\n}\n', model: 'fake-adjust', usage: {} }; } throw new Error(`Unhandled fake LLM prompt: ${allText.slice(0, 220)}`); }, }; } async function runWorkflow(fileName, params) { const workflow = JSON.parse(await fs.readFile(path.join(process.cwd(), '.vl-code', 'workflows', fileName), 'utf8')); const workDir = path.join('/tmp/vlcode-lite-adjust-workflows', `${fileName.replace(/\.json$/, '')}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`); await fs.rm(workDir, { recursive: true, force: true }); await fs.mkdir(workDir, { recursive: true }); await seedProject(workDir); const executor = new WorkflowExecutor({ workDir, model: 'fake-adjust' }); executor._resolveDocCenterDocs = async () => {}; executor._buildLLMAdapter = function buildLLMAdapter() { return createAdjustLLM(); }; const errors = []; await executor.execute(workflow, params, { onNodeError: (evt) => errors.push(evt.error || evt), onError: (msg) => errors.push(msg), }); return { workDir, files: await collectFiles(workDir), errors }; } console.log('\n── Adjust Workflow Execution Regression ──'); await test('add-page writes the planned target app file instead of dropping the route update', async () => { const currentMeta = makeCurrentMeta(); const result = await runWorkflow('add-page.json', { pageDescription: 'Add an analytics page to the admin app', currentMeta, approved: true, }); assert(result.errors.length === 0, `Unexpected errors: ${result.errors.join(', ')}`); assert(result.files.includes('Sections/AnalyticsPage.sc'), 'Expected new section file'); assert(result.files.includes('ExtComponents/KpiCard.cp'), 'Expected new component file'); assert(result.files.includes('Services/ReportService.vs'), 'Expected service file update'); assert(result.files.includes('Apps/AdminPortalShell.vx'), 'Expected target app file update'); }); await test('add-service respects currentMeta.database.file instead of hardcoding Database/main.vdb', async () => { const currentMeta = makeCurrentMeta(); const result = await runWorkflow('add-service.json', { serviceDescription: 'Add audit log backend service', currentMeta, }); assert(result.errors.length === 0, `Unexpected errors: ${result.errors.join(', ')}`); assert(result.files.includes('Database/AppDB.vdb'), 'Expected dynamic database path'); assert(!result.files.includes('Database/main.vdb'), 'Should not write hardcoded main.vdb'); assert(result.files.includes('Services/AuditService.vs'), 'Expected generated service file'); }); await test('theme-customize writes both the theme file and cascade-updated VL files', async () => { const currentMeta = makeCurrentMeta(); const result = await runWorkflow('theme-customize.json', { themeRequest: 'Refresh the brand color', currentMeta, }); assert(result.errors.length === 0, `Unexpected errors: ${result.errors.join(', ')}`); assert(result.files.includes('Theme/BrandTheme.vth'), 'Expected dynamic theme file path'); assert(result.files.includes('Sections/Dashboard.sc'), 'Expected updated section file'); assert(result.files.includes('ExtComponents/StatusCard.cp'), 'Expected updated component file'); }); console.log(`\n── Results ──\n\n ${passed} passed, ${failed} failed\n`); process.exit(failed > 0 ? 1 : 0);