test-adjust-workflows.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270
  1. /**
  2. * Test script for adjustment workflows
  3. * Tests: workflow loading, auto-selection, param mapping, and workflow JSON validity
  4. */
  5. import fs from 'fs';
  6. import path from 'path';
  7. import { fileURLToPath } from 'url';
  8. const __dirname = path.dirname(fileURLToPath(import.meta.url));
  9. const WORKFLOWS_DIR = path.join(__dirname, '.vl-code', 'workflows');
  10. const TEST_PROJECT = path.join(process.env.HOME, 'Documents/VLProjects/_tests/AdjustTest');
  11. let passed = 0;
  12. let failed = 0;
  13. function test(name, fn) {
  14. try {
  15. fn();
  16. console.log(` ✅ ${name}`);
  17. passed++;
  18. } catch (err) {
  19. console.log(` ❌ ${name}: ${err.message}`);
  20. failed++;
  21. }
  22. }
  23. function assert(condition, msg) {
  24. if (!condition) throw new Error(msg || 'Assertion failed');
  25. }
  26. // ========== 1. Workflow JSON Validity ==========
  27. console.log('\n📋 1. Workflow JSON Validity');
  28. const adjustWorkflows = [
  29. 'feature-adjust.json',
  30. 'data-adjust.json',
  31. 'batch-adjust.json',
  32. 'add-page.json',
  33. 'add-service.json',
  34. 'theme-customize.json',
  35. 'incremental-update.json',
  36. ];
  37. for (const wfFile of adjustWorkflows) {
  38. test(`${wfFile} is valid JSON with required fields`, () => {
  39. const fp = path.join(WORKFLOWS_DIR, wfFile);
  40. assert(fs.existsSync(fp), `File not found: ${fp}`);
  41. const wf = JSON.parse(fs.readFileSync(fp, 'utf-8'));
  42. assert(wf.version, 'Missing version');
  43. assert(wf.name, 'Missing name');
  44. assert(wf.registry, 'Missing registry');
  45. assert(wf.registry.params, 'Missing registry.params');
  46. assert(wf.registry.vars, 'Missing registry.vars');
  47. assert(Array.isArray(wf.steps), 'steps must be an array');
  48. assert(wf.steps.length > 0, 'steps must not be empty');
  49. });
  50. }
  51. // ========== 2. New Workflow Structure ==========
  52. console.log('\n📋 2. New Workflow Structure');
  53. test('feature-adjust.json has Pause node for developer review', () => {
  54. const wf = JSON.parse(fs.readFileSync(path.join(WORKFLOWS_DIR, 'feature-adjust.json'), 'utf-8'));
  55. const pauseNodes = wf.steps.filter(s => s.id.startsWith('Pause_'));
  56. assert(pauseNodes.length > 0, 'Must have at least one Pause node');
  57. assert(pauseNodes[0].resumeResultTarget, 'Pause node must have resumeResultTarget');
  58. });
  59. test('feature-adjust.json has Fork for parallel execution', () => {
  60. const wf = JSON.parse(fs.readFileSync(path.join(WORKFLOWS_DIR, 'feature-adjust.json'), 'utf-8'));
  61. const forkNodes = wf.steps.filter(s => s.id.startsWith('Fork_'));
  62. assert(forkNodes.length > 0, 'Must have at least one Fork node');
  63. assert(forkNodes[0].children.length > 0, 'Fork must have children');
  64. });
  65. test('data-adjust.json has cascade flow: DB → Services → Sections', () => {
  66. const wf = JSON.parse(fs.readFileSync(path.join(WORKFLOWS_DIR, 'data-adjust.json'), 'utf-8'));
  67. const stepIds = wf.steps.map(s => s.id);
  68. assert(stepIds.some(id => id.includes('Database')), 'Must have DB edit step');
  69. assert(stepIds.some(id => id.includes('CascadeServices')), 'Must have service cascade');
  70. assert(stepIds.some(id => id.includes('CascadeSections')), 'Must have section cascade');
  71. });
  72. test('batch-adjust.json has decompose + loop pattern', () => {
  73. const wf = JSON.parse(fs.readFileSync(path.join(WORKFLOWS_DIR, 'batch-adjust.json'), 'utf-8'));
  74. const stepIds = wf.steps.map(s => s.id);
  75. assert(stepIds.some(id => id.includes('Decompose')), 'Must have decompose step');
  76. assert(stepIds.some(id => id.includes('Loop')), 'Must have loop step');
  77. });
  78. test('add-page.json writes the updated app back to the planned target app file', () => {
  79. const wf = JSON.parse(fs.readFileSync(path.join(WORKFLOWS_DIR, 'add-page.json'), 'utf-8'));
  80. const appUpdate = wf.steps.find(s => s.id === 'LLM_050_UpdateApp');
  81. assert(appUpdate, 'Missing LLM_050_UpdateApp step');
  82. assert(appUpdate.out && Object.keys(appUpdate.out).includes("/{$targetAppFilePath || ('Apps/' + $targetApp + '.vx')}"), 'App update must write to dynamic target app file path');
  83. });
  84. test('add-service.json updates the actual database file path from ProjectMeta', () => {
  85. const wf = JSON.parse(fs.readFileSync(path.join(WORKFLOWS_DIR, 'add-service.json'), 'utf-8'));
  86. const dbStep = wf.steps.find(s => s.id === 'LLM_020_UpdateDB');
  87. assert(dbStep, 'Missing LLM_020_UpdateDB step');
  88. 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');
  89. });
  90. test('theme-customize.json writes cascade updates back to affected files', () => {
  91. const wf = JSON.parse(fs.readFileSync(path.join(WORKFLOWS_DIR, 'theme-customize.json'), 'utf-8'));
  92. const updateStep = wf.steps.find(s => s.id === 'LLM_030_UpdateFile');
  93. assert(updateStep, 'Missing LLM_030_UpdateFile step');
  94. assert(updateStep.out && Object.keys(updateStep.out).includes('/{_item.file}'), 'Cascade update must write the updated file');
  95. });
  96. test('incremental-update.json no longer depends on missing DocCenter prompt paths', () => {
  97. const wf = JSON.parse(fs.readFileSync(path.join(WORKFLOWS_DIR, 'incremental-update.json'), 'utf-8'));
  98. assert(Object.keys(wf.registry.docs).sort().join(',') === '1', 'Incremental update should only depend on the syntax doc until dedicated prompts exist');
  99. });
  100. // ========== 3. Auto-Selection Logic ==========
  101. console.log('\n📋 3. Auto-Selection Logic (import vl-adjust)');
  102. // We can't directly import the function because it's not exported,
  103. // so we test by pattern matching the same regexes
  104. const featurePatterns = [
  105. /add\s*(a\s+)?(?:new\s+)?(?:button|form|field|chart|dialog|modal|table|tab|card|filter|sort|export|import|upload|download)/i,
  106. /(?:添加|增加|加)\s*(?:一个?\s*)?(?:按钮|表单|字段|图表|弹窗|对话框|表格|标签页|卡片|筛选|排序|导出|导入|上传|下载)/,
  107. /add\s*(a\s+)?feature/i,
  108. /(?:添加|增加)\s*功能/,
  109. /给.*(?:加|添加|增加)\s*(?:一个?)?\s*(?:按钮|功能|字段|表单|图表)/,
  110. ];
  111. const dataPatterns = [
  112. /add\s*(a\s+)?(?:new\s+)?(?:field|column|table|index)/i,
  113. /(?:添加|增加|新增)\s*(?:字段|列|表|索引)/,
  114. /(?:modify|change|update)\s*(?:the\s+)?(?:database|schema|data\s*model|field\s*type)/i,
  115. /(?:修改|变更)\s*(?:数据库|数据模型|字段类型|表结构)/,
  116. ];
  117. const batchPatterns = [
  118. /(?:batch|multiple|several|一批|批量|多个)\s*(?:change|update|fix|修改|调整|更新)/i,
  119. /(?:change|update|fix|修改|调整|更新)\s*(?:batch|multiple|several|一批|批量|多个)/i,
  120. /同时.*(?:修改|调整|更新|添加)/,
  121. ];
  122. function matchesAny(text, patterns) {
  123. return patterns.some(p => p.test(text));
  124. }
  125. test('Detects "add a button" as add-feature', () => {
  126. assert(matchesAny('add a button to the home page', featurePatterns), 'Should match add-feature');
  127. });
  128. test('Detects "添加按钮" as add-feature', () => {
  129. assert(matchesAny('给首页添加一个按钮', featurePatterns), 'Should match add-feature (Chinese)');
  130. });
  131. test('Detects "add a chart" as add-feature', () => {
  132. assert(matchesAny('add a chart to show statistics', featurePatterns), 'Should match add-feature');
  133. });
  134. test('Detects "add feature" as add-feature', () => {
  135. assert(matchesAny('add a feature for export', featurePatterns), 'Should match add-feature');
  136. });
  137. test('Detects "add a field" as modify-data', () => {
  138. assert(matchesAny('add a field called price to Items table', dataPatterns), 'Should match modify-data');
  139. });
  140. test('Detects "添加字段" as modify-data', () => {
  141. assert(matchesAny('添加字段 description 到 Items 表', dataPatterns), 'Should match modify-data');
  142. });
  143. test('Detects "modify database schema" as modify-data', () => {
  144. assert(matchesAny('modify the database schema to add a new column', dataPatterns), 'Should match modify-data');
  145. });
  146. test('Detects "batch changes" as batch', () => {
  147. assert(matchesAny('batch update multiple files', batchPatterns), 'Should match batch');
  148. });
  149. test('Detects "批量修改" as batch', () => {
  150. assert(matchesAny('批量修改多个页面', batchPatterns), 'Should match batch');
  151. });
  152. // ========== 4. Test Project Validation ==========
  153. console.log('\n📋 4. Test Project (AdjustTest) Validation');
  154. test('Test project directory exists', () => {
  155. assert(fs.existsSync(TEST_PROJECT), 'AdjustTest directory should exist');
  156. });
  157. test('ProjectMeta.json is valid', () => {
  158. const metaPath = path.join(TEST_PROJECT, '.vl-code', 'ProjectMeta.json');
  159. const meta = JSON.parse(fs.readFileSync(metaPath, 'utf-8'));
  160. assert(meta.projectName === 'AdjustTest', 'projectName should be AdjustTest');
  161. assert(meta.apps.length > 0, 'Should have at least one app');
  162. assert(meta.sections.length > 0, 'Should have at least one section');
  163. assert(meta.services.length > 0, 'Should have at least one service');
  164. assert(meta.database, 'Should have database');
  165. });
  166. test('All VL files referenced in meta exist', () => {
  167. const meta = JSON.parse(fs.readFileSync(path.join(TEST_PROJECT, '.vl-code', 'ProjectMeta.json'), 'utf-8'));
  168. for (const app of meta.apps) {
  169. assert(fs.existsSync(path.join(TEST_PROJECT, app.file)), `App file missing: ${app.file}`);
  170. }
  171. for (const section of meta.sections) {
  172. assert(fs.existsSync(path.join(TEST_PROJECT, section.file)), `Section file missing: ${section.file}`);
  173. }
  174. for (const service of meta.services) {
  175. assert(fs.existsSync(path.join(TEST_PROJECT, service.file)), `Service file missing: ${service.file}`);
  176. }
  177. assert(fs.existsSync(path.join(TEST_PROJECT, meta.database.file)), `Database file missing: ${meta.database.file}`);
  178. assert(fs.existsSync(path.join(TEST_PROJECT, meta.theme.file)), `Theme file missing: ${meta.theme.file}`);
  179. });
  180. test('VL files have correct version declaration', () => {
  181. const vlFiles = [
  182. 'Apps/MainApp.vx',
  183. 'Sections/HomePage.sc',
  184. 'Sections/ItemList.sc',
  185. 'Services/ItemService.vs',
  186. 'Database/AppDB.vdb',
  187. 'Theme/DefaultTheme.vth',
  188. ];
  189. for (const f of vlFiles) {
  190. const content = fs.readFileSync(path.join(TEST_PROJECT, f), 'utf-8');
  191. assert(content.startsWith('// VL_VERSION:3.5'), `${f} must start with VL_VERSION:3.5`);
  192. }
  193. });
  194. // ========== 5. Workflow-Param Mapping ==========
  195. console.log('\n📋 5. Workflow-Param Mapping');
  196. test('feature-adjust expects featureRequest param', () => {
  197. const wf = JSON.parse(fs.readFileSync(path.join(WORKFLOWS_DIR, 'feature-adjust.json'), 'utf-8'));
  198. assert(wf.registry.params.includes('featureRequest(STRING)'), 'Must have featureRequest param');
  199. assert(wf.registry.params.includes('currentMeta(OBJECT)'), 'Must have currentMeta param');
  200. });
  201. test('data-adjust expects dataRequest param', () => {
  202. const wf = JSON.parse(fs.readFileSync(path.join(WORKFLOWS_DIR, 'data-adjust.json'), 'utf-8'));
  203. assert(wf.registry.params.includes('dataRequest(STRING)'), 'Must have dataRequest param');
  204. });
  205. test('batch-adjust expects batchRequest param', () => {
  206. const wf = JSON.parse(fs.readFileSync(path.join(WORKFLOWS_DIR, 'batch-adjust.json'), 'utf-8'));
  207. assert(wf.registry.params.includes('batchRequest(STRING)'), 'Must have batchRequest param');
  208. });
  209. // ========== 6. Seed Workflows Sync ==========
  210. console.log('\n📋 6. Seed Workflows Sync');
  211. const seedDir = path.join(__dirname, 'public', 'seed-workflows');
  212. for (const wfFile of ['feature-adjust.json', 'data-adjust.json', 'batch-adjust.json']) {
  213. test(`${wfFile} exists in seed-workflows`, () => {
  214. assert(fs.existsSync(path.join(seedDir, wfFile)), `Missing in seed-workflows: ${wfFile}`);
  215. });
  216. test(`${wfFile} matches between .vl-code/workflows and seed-workflows`, () => {
  217. const a = fs.readFileSync(path.join(WORKFLOWS_DIR, wfFile), 'utf-8');
  218. const b = fs.readFileSync(path.join(seedDir, wfFile), 'utf-8');
  219. assert(a === b, `Content mismatch for ${wfFile}`);
  220. });
  221. }
  222. // ========== Summary ==========
  223. console.log(`\n${'='.repeat(50)}`);
  224. console.log(`Results: ${passed} passed, ${failed} failed, ${passed + failed} total`);
  225. console.log(`${'='.repeat(50)}\n`);
  226. process.exit(failed > 0 ? 1 : 0);