/** * Test script for adjustment workflows * Tests: workflow loading, auto-selection, param mapping, and workflow JSON validity */ import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const WORKFLOWS_DIR = path.join(__dirname, '.vl-code', 'workflows'); const TEST_PROJECT = path.join(process.env.HOME, 'Documents/VLProjects/_tests/AdjustTest'); let passed = 0; let failed = 0; function test(name, fn) { try { fn(); console.log(` ✅ ${name}`); passed++; } catch (err) { console.log(` ❌ ${name}: ${err.message}`); failed++; } } function assert(condition, msg) { if (!condition) throw new Error(msg || 'Assertion failed'); } // ========== 1. Workflow JSON Validity ========== console.log('\n📋 1. Workflow JSON Validity'); const adjustWorkflows = [ 'feature-adjust.json', 'data-adjust.json', 'batch-adjust.json', 'add-page.json', 'add-service.json', 'theme-customize.json', 'incremental-update.json', ]; for (const wfFile of adjustWorkflows) { test(`${wfFile} is valid JSON with required fields`, () => { const fp = path.join(WORKFLOWS_DIR, wfFile); assert(fs.existsSync(fp), `File not found: ${fp}`); const wf = JSON.parse(fs.readFileSync(fp, 'utf-8')); assert(wf.version, 'Missing version'); assert(wf.name, 'Missing name'); assert(wf.registry, 'Missing registry'); assert(wf.registry.params, 'Missing registry.params'); assert(wf.registry.vars, 'Missing registry.vars'); assert(Array.isArray(wf.steps), 'steps must be an array'); assert(wf.steps.length > 0, 'steps must not be empty'); }); } // ========== 2. New Workflow Structure ========== console.log('\n📋 2. New Workflow Structure'); test('feature-adjust.json has Pause node for developer review', () => { const wf = JSON.parse(fs.readFileSync(path.join(WORKFLOWS_DIR, 'feature-adjust.json'), 'utf-8')); const pauseNodes = wf.steps.filter(s => s.id.startsWith('Pause_')); assert(pauseNodes.length > 0, 'Must have at least one Pause node'); assert(pauseNodes[0].resumeResultTarget, 'Pause node must have resumeResultTarget'); }); test('feature-adjust.json has Fork for parallel execution', () => { const wf = JSON.parse(fs.readFileSync(path.join(WORKFLOWS_DIR, 'feature-adjust.json'), 'utf-8')); const forkNodes = wf.steps.filter(s => s.id.startsWith('Fork_')); assert(forkNodes.length > 0, 'Must have at least one Fork node'); assert(forkNodes[0].children.length > 0, 'Fork must have children'); }); test('data-adjust.json has cascade flow: DB → Services → Sections', () => { const wf = JSON.parse(fs.readFileSync(path.join(WORKFLOWS_DIR, 'data-adjust.json'), 'utf-8')); const stepIds = wf.steps.map(s => s.id); assert(stepIds.some(id => id.includes('Database')), 'Must have DB edit step'); assert(stepIds.some(id => id.includes('CascadeServices')), 'Must have service cascade'); assert(stepIds.some(id => id.includes('CascadeSections')), 'Must have section cascade'); }); test('batch-adjust.json has decompose + loop pattern', () => { const wf = JSON.parse(fs.readFileSync(path.join(WORKFLOWS_DIR, 'batch-adjust.json'), 'utf-8')); const stepIds = wf.steps.map(s => s.id); assert(stepIds.some(id => id.includes('Decompose')), 'Must have decompose step'); assert(stepIds.some(id => id.includes('Loop')), 'Must have loop step'); }); test('add-page.json writes the updated app back to the planned target app file', () => { const wf = JSON.parse(fs.readFileSync(path.join(WORKFLOWS_DIR, 'add-page.json'), 'utf-8')); const appUpdate = wf.steps.find(s => s.id === 'LLM_050_UpdateApp'); assert(appUpdate, 'Missing LLM_050_UpdateApp step'); assert(appUpdate.out && Object.keys(appUpdate.out).includes("/{$targetAppFilePath || ('Apps/' + $targetApp + '.vx')}"), 'App update must write to dynamic target app file path'); }); test('add-service.json updates the actual database file path from ProjectMeta', () => { const wf = JSON.parse(fs.readFileSync(path.join(WORKFLOWS_DIR, 'add-service.json'), 'utf-8')); const dbStep = wf.steps.find(s => s.id === 'LLM_020_UpdateDB'); assert(dbStep, 'Missing LLM_020_UpdateDB step'); assert(dbStep.out && Object.keys(dbStep.out).includes("/{currentMeta.database.file || ('Database/' + currentMeta.projectName + '.vdb')}"), 'DB update must write to the ProjectMeta database file or project-name fallback'); }); test('theme-customize.json writes cascade updates back to affected files', () => { const wf = JSON.parse(fs.readFileSync(path.join(WORKFLOWS_DIR, 'theme-customize.json'), 'utf-8')); const updateStep = wf.steps.find(s => s.id === 'LLM_030_UpdateFile'); assert(updateStep, 'Missing LLM_030_UpdateFile step'); assert(updateStep.out && Object.keys(updateStep.out).includes('/{_item.file}'), 'Cascade update must write the updated file'); }); test('incremental-update.json no longer depends on missing DocCenter prompt paths', () => { const wf = JSON.parse(fs.readFileSync(path.join(WORKFLOWS_DIR, 'incremental-update.json'), 'utf-8')); assert(Object.keys(wf.registry.docs).sort().join(',') === '1', 'Incremental update should only depend on the syntax doc until dedicated prompts exist'); }); // ========== 3. Auto-Selection Logic ========== console.log('\n📋 3. Auto-Selection Logic (import vl-adjust)'); // We can't directly import the function because it's not exported, // so we test by pattern matching the same regexes const featurePatterns = [ /add\s*(a\s+)?(?:new\s+)?(?:button|form|field|chart|dialog|modal|table|tab|card|filter|sort|export|import|upload|download)/i, /(?:添加|增加|加)\s*(?:一个?\s*)?(?:按钮|表单|字段|图表|弹窗|对话框|表格|标签页|卡片|筛选|排序|导出|导入|上传|下载)/, /add\s*(a\s+)?feature/i, /(?:添加|增加)\s*功能/, /给.*(?:加|添加|增加)\s*(?:一个?)?\s*(?:按钮|功能|字段|表单|图表)/, ]; const dataPatterns = [ /add\s*(a\s+)?(?:new\s+)?(?:field|column|table|index)/i, /(?:添加|增加|新增)\s*(?:字段|列|表|索引)/, /(?:modify|change|update)\s*(?:the\s+)?(?:database|schema|data\s*model|field\s*type)/i, /(?:修改|变更)\s*(?:数据库|数据模型|字段类型|表结构)/, ]; const batchPatterns = [ /(?:batch|multiple|several|一批|批量|多个)\s*(?:change|update|fix|修改|调整|更新)/i, /(?:change|update|fix|修改|调整|更新)\s*(?:batch|multiple|several|一批|批量|多个)/i, /同时.*(?:修改|调整|更新|添加)/, ]; function matchesAny(text, patterns) { return patterns.some(p => p.test(text)); } test('Detects "add a button" as add-feature', () => { assert(matchesAny('add a button to the home page', featurePatterns), 'Should match add-feature'); }); test('Detects "添加按钮" as add-feature', () => { assert(matchesAny('给首页添加一个按钮', featurePatterns), 'Should match add-feature (Chinese)'); }); test('Detects "add a chart" as add-feature', () => { assert(matchesAny('add a chart to show statistics', featurePatterns), 'Should match add-feature'); }); test('Detects "add feature" as add-feature', () => { assert(matchesAny('add a feature for export', featurePatterns), 'Should match add-feature'); }); test('Detects "add a field" as modify-data', () => { assert(matchesAny('add a field called price to Items table', dataPatterns), 'Should match modify-data'); }); test('Detects "添加字段" as modify-data', () => { assert(matchesAny('添加字段 description 到 Items 表', dataPatterns), 'Should match modify-data'); }); test('Detects "modify database schema" as modify-data', () => { assert(matchesAny('modify the database schema to add a new column', dataPatterns), 'Should match modify-data'); }); test('Detects "batch changes" as batch', () => { assert(matchesAny('batch update multiple files', batchPatterns), 'Should match batch'); }); test('Detects "批量修改" as batch', () => { assert(matchesAny('批量修改多个页面', batchPatterns), 'Should match batch'); }); // ========== 4. Test Project Validation ========== console.log('\n📋 4. Test Project (AdjustTest) Validation'); test('Test project directory exists', () => { assert(fs.existsSync(TEST_PROJECT), 'AdjustTest directory should exist'); }); test('ProjectMeta.json is valid', () => { const metaPath = path.join(TEST_PROJECT, '.vl-code', 'ProjectMeta.json'); const meta = JSON.parse(fs.readFileSync(metaPath, 'utf-8')); assert(meta.projectName === 'AdjustTest', 'projectName should be AdjustTest'); assert(meta.apps.length > 0, 'Should have at least one app'); assert(meta.sections.length > 0, 'Should have at least one section'); assert(meta.services.length > 0, 'Should have at least one service'); assert(meta.database, 'Should have database'); }); test('All VL files referenced in meta exist', () => { const meta = JSON.parse(fs.readFileSync(path.join(TEST_PROJECT, '.vl-code', 'ProjectMeta.json'), 'utf-8')); for (const app of meta.apps) { assert(fs.existsSync(path.join(TEST_PROJECT, app.file)), `App file missing: ${app.file}`); } for (const section of meta.sections) { assert(fs.existsSync(path.join(TEST_PROJECT, section.file)), `Section file missing: ${section.file}`); } for (const service of meta.services) { assert(fs.existsSync(path.join(TEST_PROJECT, service.file)), `Service file missing: ${service.file}`); } assert(fs.existsSync(path.join(TEST_PROJECT, meta.database.file)), `Database file missing: ${meta.database.file}`); assert(fs.existsSync(path.join(TEST_PROJECT, meta.theme.file)), `Theme file missing: ${meta.theme.file}`); }); test('VL files have correct version declaration', () => { const vlFiles = [ 'Apps/MainApp.vx', 'Sections/HomePage.sc', 'Sections/ItemList.sc', 'Services/ItemService.vs', 'Database/AppDB.vdb', 'Theme/DefaultTheme.vth', ]; for (const f of vlFiles) { const content = fs.readFileSync(path.join(TEST_PROJECT, f), 'utf-8'); assert(content.startsWith('// VL_VERSION:3.5'), `${f} must start with VL_VERSION:3.5`); } }); // ========== 5. Workflow-Param Mapping ========== console.log('\n📋 5. Workflow-Param Mapping'); test('feature-adjust expects featureRequest param', () => { const wf = JSON.parse(fs.readFileSync(path.join(WORKFLOWS_DIR, 'feature-adjust.json'), 'utf-8')); assert(wf.registry.params.includes('featureRequest(STRING)'), 'Must have featureRequest param'); assert(wf.registry.params.includes('currentMeta(OBJECT)'), 'Must have currentMeta param'); }); test('data-adjust expects dataRequest param', () => { const wf = JSON.parse(fs.readFileSync(path.join(WORKFLOWS_DIR, 'data-adjust.json'), 'utf-8')); assert(wf.registry.params.includes('dataRequest(STRING)'), 'Must have dataRequest param'); }); test('batch-adjust expects batchRequest param', () => { const wf = JSON.parse(fs.readFileSync(path.join(WORKFLOWS_DIR, 'batch-adjust.json'), 'utf-8')); assert(wf.registry.params.includes('batchRequest(STRING)'), 'Must have batchRequest param'); }); // ========== 6. Seed Workflows Sync ========== console.log('\n📋 6. Seed Workflows Sync'); const seedDir = path.join(__dirname, 'public', 'seed-workflows'); for (const wfFile of ['feature-adjust.json', 'data-adjust.json', 'batch-adjust.json']) { test(`${wfFile} exists in seed-workflows`, () => { assert(fs.existsSync(path.join(seedDir, wfFile)), `Missing in seed-workflows: ${wfFile}`); }); test(`${wfFile} matches between .vl-code/workflows and seed-workflows`, () => { const a = fs.readFileSync(path.join(WORKFLOWS_DIR, wfFile), 'utf-8'); const b = fs.readFileSync(path.join(seedDir, wfFile), 'utf-8'); assert(a === b, `Content mismatch for ${wfFile}`); }); } // ========== Summary ========== console.log(`\n${'='.repeat(50)}`); console.log(`Results: ${passed} passed, ${failed} failed, ${passed + failed} total`); console.log(`${'='.repeat(50)}\n`); process.exit(failed > 0 ? 1 : 0);