| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295 |
- 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);
|