| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270 |
- /**
- * 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);
|