test-workflow-parallel-codegen.js 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631
  1. import fs from 'fs/promises';
  2. import path from 'path';
  3. import { WorkflowExecutor } from './src/vl/workflow-executor.js';
  4. let passed = 0;
  5. let failed = 0;
  6. function test(name, fn) {
  7. return Promise.resolve()
  8. .then(fn)
  9. .then(() => {
  10. console.log(` ✓ ${name}`);
  11. passed++;
  12. })
  13. .catch((err) => {
  14. console.log(` ✗ ${name}: ${err.message}`);
  15. failed++;
  16. });
  17. }
  18. function assert(cond, msg) {
  19. if (!cond) throw new Error(msg || 'Assertion failed');
  20. }
  21. function assertIncludes(list, value, msg) {
  22. if (!list.includes(value)) {
  23. throw new Error(msg || `Expected ${JSON.stringify(value)} in ${JSON.stringify(list)}`);
  24. }
  25. }
  26. function sleep(ms) {
  27. return new Promise((resolve) => setTimeout(resolve, ms));
  28. }
  29. function json(obj) {
  30. return JSON.stringify(obj);
  31. }
  32. function normalizeMessages(messages = []) {
  33. return messages.map((msg) => ({
  34. role: msg.role,
  35. content: msg.content,
  36. text: typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content),
  37. }));
  38. }
  39. function findObjectMessage(messages, predicate) {
  40. for (const msg of messages) {
  41. const value = msg.content;
  42. if (value && typeof value === 'object' && !Array.isArray(value) && predicate(value)) {
  43. return value;
  44. }
  45. }
  46. return null;
  47. }
  48. function createFixture(projectName = 'SmokeApp') {
  49. const services = [
  50. {
  51. id: 'ItemService',
  52. domainId: 'ItemService',
  53. filePath: 'Services/ItemService.vs',
  54. path: 'Services/ItemService.vs',
  55. methods: [{ id: 'listItems', isPublic: true, params: '', returnType: '[OBJECT]' }],
  56. },
  57. {
  58. id: 'OrderService',
  59. domainId: 'OrderService',
  60. filePath: 'Services/OrderService.vs',
  61. path: 'Services/OrderService.vs',
  62. methods: [{ id: 'listOrders', isPublic: true, params: '', returnType: '[OBJECT]' }],
  63. },
  64. ];
  65. const components = [
  66. { id: 'ItemCard', filePath: 'ExtComponents/ItemCard.cp', path: 'ExtComponents/ItemCard.cp' },
  67. { id: 'OrderBadge', filePath: 'ExtComponents/OrderBadge.cp', path: 'ExtComponents/OrderBadge.cp' },
  68. ];
  69. const sections = [
  70. {
  71. id: 'HomePage',
  72. filePath: 'Sections/HomePage.sc',
  73. path: 'Sections/HomePage.sc',
  74. consumesServices: ['ItemService'],
  75. usesComponents: ['ItemCard'],
  76. },
  77. {
  78. id: 'OrdersPage',
  79. filePath: 'Sections/OrdersPage.sc',
  80. path: 'Sections/OrdersPage.sc',
  81. consumesServices: ['OrderService'],
  82. usesComponents: ['OrderBadge'],
  83. },
  84. ];
  85. const apps = [
  86. { id: 'MainApp', appId: 'MainApp', filePath: 'Apps/MainApp.vx', path: 'Apps/MainApp.vx', routes: [{ path: '/', section: 'HomePage' }] },
  87. { id: 'AdminApp', appId: 'AdminApp', filePath: 'Apps/AdminPortalShell.vx', path: 'Apps/AdminPortalShell.vx', routes: [{ path: '/orders', section: 'OrdersPage' }] },
  88. ];
  89. const fileManifest = {
  90. databaseFile: { id: 'MainDB', path: `Database/${projectName}.vdb`, type: 'vdb', sourceId: 'MainDB' },
  91. themeFile: { id: 'Theme', path: 'Theme/Theme.vth', type: 'vth', sourceId: 'Theme' },
  92. serviceFiles: services.map(({ id, path: filePath }) => ({ id, path: filePath, type: 'vs', sourceId: id })),
  93. componentFiles: components.map(({ id, path: filePath }) => ({ id, path: filePath, type: 'cp', sourceId: id })),
  94. sectionFiles: sections.map(({ id, path: filePath }) => ({ id, path: filePath, type: 'sc', sourceId: id })),
  95. appFiles: apps.map(({ id, appId, path: filePath }) => ({ id, appId, path: filePath, type: 'vx', sourceId: id })),
  96. };
  97. fileManifest.files = [
  98. fileManifest.databaseFile,
  99. fileManifest.themeFile,
  100. ...fileManifest.serviceFiles,
  101. ...fileManifest.componentFiles,
  102. ...fileManifest.sectionFiles,
  103. ...fileManifest.appFiles,
  104. ];
  105. fileManifest.references = [
  106. { from: 'Sections/HomePage.sc', to: 'Services/ItemService.vs', refType: 'consumes' },
  107. { from: 'Sections/OrdersPage.sc', to: 'Services/OrderService.vs', refType: 'consumes' },
  108. { from: 'Apps/MainApp.vx', to: 'Sections/HomePage.sc', refType: 'route' },
  109. { from: 'Apps/AdminApp.vx', to: 'Sections/OrdersPage.sc', refType: 'route' },
  110. ];
  111. return {
  112. prdJson: {
  113. projectName,
  114. summary: 'parallel smoke test',
  115. targetLang: 'zh-CN',
  116. },
  117. projectMeta: {
  118. specVersion: 'ProjectMeta/1.0',
  119. projectName,
  120. projectDescription: 'parallel smoke test',
  121. vlVersion: '3.5',
  122. config: {
  123. defaultDevice: 'Phone',
  124. defaultResolution: '375x812',
  125. themeFile: 'Theme/Theme.vth',
  126. colorScheme: 'light',
  127. },
  128. database: {
  129. file: `Database/${projectName}.vdb`,
  130. },
  131. theme: {
  132. file: 'Theme/Theme.vth',
  133. },
  134. dataSchema: {
  135. tables: [
  136. { id: 'Item', fields: [{ name: 'id', type: 'INT', constraints: ['PK'] }, { name: 'title', type: 'STRING' }] },
  137. { id: 'Order', fields: [{ name: 'id', type: 'INT', constraints: ['PK'] }, { name: 'status', type: 'STRING' }] },
  138. ],
  139. relations: [],
  140. },
  141. services: services.map(({ path: filePath, ...rest }) => ({ ...rest, filePath })),
  142. components: components.map(({ path: filePath, ...rest }) => ({ ...rest, filePath })),
  143. sections: sections.map(({ path: filePath, ...rest }) => ({ ...rest, filePath })),
  144. apps: apps.map(({ path: filePath, ...rest }) => ({ ...rest, filePath })),
  145. },
  146. blueprint: {
  147. projectName,
  148. valueDomains: { orderStatus: ['new', 'paid'] },
  149. roles: [{ id: 'Admin' }, { id: 'User' }],
  150. entities: [{ id: 'Item' }, { id: 'Order' }],
  151. apps: apps.map(({ appId, routes }) => ({ appId, pages: routes.map((route) => ({ id: route.section, sectionId: route.section })) })),
  152. features: ['list-items', 'list-orders'],
  153. },
  154. dataSchema: {
  155. tables: [
  156. { id: 'Item', fields: [{ name: 'id', type: 'INT' }, { name: 'title', type: 'STRING' }], indexes: [], testData: [] },
  157. { id: 'Order', fields: [{ name: 'id', type: 'INT' }, { name: 'status', type: 'STRING' }], indexes: [], testData: [] },
  158. ],
  159. relations: [],
  160. },
  161. layoutPlan: {
  162. apps: apps.map(({ appId, routes, path: filePath }) => ({
  163. appId,
  164. filePath,
  165. routes,
  166. layoutTree: { type: 'stack', items: routes.map((route) => route.section) },
  167. })),
  168. sections: sections.map(({ id }) => ({ id, structure: ['Header', 'List'] })),
  169. components: components.map(({ id }) => ({ id, previewSize: 'medium' })),
  170. },
  171. contractSheet: {
  172. serviceDomains: services.map(({ path: filePath, ...rest }) => ({ ...rest, filePath })),
  173. sectionContracts: sections.map(({ path: filePath, ...rest }) => ({ ...rest, filePath })),
  174. componentContracts: components.map(({ path: filePath, ...rest }) => ({ ...rest, filePath })),
  175. },
  176. stateBehavior: {
  177. appStates: apps.map(({ appId }) => ({ appId, globalVars: ['$ready(BOOL)'], initSequence: ['load'] })),
  178. sectionBehaviors: sections.map(({ id }) => ({ id, keyStates: ['$items([OBJECT])'], serviceBindings: [], initBehavior: [], internalMethods: [] })),
  179. eventWiring: [],
  180. modalTriggers: [],
  181. },
  182. intentSpec: {
  183. projectName,
  184. summary: 'parallel smoke test',
  185. domains: ['Item', 'Order'],
  186. project: { customTheme: true },
  187. apps: apps.map(({ appId }) => ({ appId })),
  188. },
  189. domainSchema: {
  190. entities: [
  191. { id: 'Item', fields: [{ name: 'id', type: 'INT' }, { name: 'title', type: 'STRING' }] },
  192. { id: 'Order', fields: [{ name: 'id', type: 'INT' }, { name: 'status', type: 'STRING' }] },
  193. ],
  194. relations: [],
  195. },
  196. serviceContractMap: {
  197. services: services.map(({ id, domainId, path: filePath }) => ({ id, domainId, path: filePath, filePath })),
  198. },
  199. serviceMapJson: {
  200. serviceDomains: services.map(({ id, domainId, path: filePath }) => ({ id, domainId, filePath })),
  201. },
  202. uiContractMap: {
  203. components: components.map(({ id, path: filePath }) => ({ id, path: filePath, filePath })),
  204. sections: sections.map(({ id, path: filePath }) => ({ id, path: filePath, filePath })),
  205. apps: apps.map(({ appId, path: filePath }) => ({ appId, path: filePath })),
  206. },
  207. uiMapJson: {
  208. components: components.map(({ id, path: filePath }) => ({ id, filePath })),
  209. sections: sections.map(({ id, path: filePath }) => ({ id, filePath })),
  210. apps: apps.map(({ id, appId, path: filePath }) => ({ id, appId, filePath })),
  211. },
  212. moduleManifest: {
  213. databaseFile: { id: 'MainDB', path: `Database/${projectName}.vdb`, type: 'vdb' },
  214. themeFile: { id: `${projectName}Theme`, path: `Theme/${projectName}.vth`, type: 'vth' },
  215. serviceFiles: services.map(({ id, domainId, path: filePath }) => ({ id, domainId, path: filePath, type: 'vs' })),
  216. componentFiles: components.map(({ id, path: filePath }) => ({ id, path: filePath, type: 'cp' })),
  217. sectionFiles: sections.map(({ id, path: filePath }) => ({ id, path: filePath, type: 'sc' })),
  218. appFiles: apps.map(({ id, appId, path: filePath }) => ({ id, appId, path: filePath, type: 'vx' })),
  219. files: fileManifest.files,
  220. references: fileManifest.references,
  221. },
  222. dependencyGraph: {
  223. nodes: [
  224. { id: 'ItemService', type: 'service' },
  225. { id: 'OrderService', type: 'service' },
  226. { id: 'HomePage', type: 'section' },
  227. { id: 'OrdersPage', type: 'section' },
  228. ],
  229. edges: [
  230. { from: 'HomePage', to: 'ItemService' },
  231. { from: 'OrdersPage', to: 'OrderService' },
  232. ],
  233. },
  234. qualityGateSpec: {
  235. rules: [{ id: 'no-missing-files', severity: 'error' }],
  236. },
  237. executionPlan: {
  238. tasks: [
  239. { id: 'gen-db', type: 'database' },
  240. { id: 'gen-theme', type: 'theme' },
  241. { id: 'gen-services', type: 'parallel' },
  242. ],
  243. parallelGroups: [['gen-db', 'gen-theme', 'gen-services']],
  244. },
  245. fileManifest,
  246. vdbCode: '// VL_VERSION:3.5\nDATABASE main\nTABLE Item { id INT, title STRING }\nTABLE Order { id INT, status STRING }\n',
  247. vthCode: `// VL_VERSION:3.5\nTHEME ${projectName} {\n COLOR primary = #006B5F\n}\n`,
  248. };
  249. }
  250. function serviceCode(id) {
  251. return `// VL_VERSION:3.5\nSERVICE ${id} {\n METHOD list(): [OBJECT]\n}\n`;
  252. }
  253. function componentCode(id) {
  254. return `// VL_VERSION:3.5\nCOMPONENT ${id} {\n VIEW { Text("${id}") }\n}\n`;
  255. }
  256. function sectionCode(id) {
  257. return `// VL_VERSION:3.5\nSECTION ${id} {\n INIT { }\n}\n`;
  258. }
  259. function appCode(id) {
  260. return `// VL_VERSION:3.5\nAPP ${id} {\n ROUTES { }\n}\n`;
  261. }
  262. function createFakeLLM(fixture, probe, options = {}) {
  263. return {
  264. async call(params) {
  265. const messages = normalizeMessages(params.messages);
  266. const allText = messages.map((msg) => msg.text).join('\n');
  267. const target = findObjectMessage(messages, (value) => (
  268. value.domainId || value.filePath || value.path || value.id || value.appId
  269. ));
  270. probe.active++;
  271. probe.maxActive = Math.max(probe.maxActive, probe.active);
  272. try {
  273. await sleep(options.delayMs || 80);
  274. const failId = options.failId;
  275. const targetId = target?.domainId || target?.id || target?.appId || target?.path || target?.filePath || '';
  276. if (failId && targetId.includes(failId)) {
  277. throw new Error(`forced failure for ${failId}`);
  278. }
  279. if (allText.includes('Generate the complete ProjectMeta JSON')) {
  280. return { content: json(fixture.projectMeta), model: 'fake-parallel-smoke', usage: {} };
  281. }
  282. if (allText.includes('Requirement:') && allText.includes('Target Language:')) {
  283. return { content: json(fixture.prdJson), model: 'fake-parallel-smoke', usage: {} };
  284. }
  285. if (allText.includes('Generate Blueprint.json')) {
  286. return { content: json(fixture.blueprint), model: 'fake-parallel-smoke', usage: {} };
  287. }
  288. if (allText.includes('Generate DataSchema.json')) {
  289. return { content: json(fixture.dataSchema), model: 'fake-parallel-smoke', usage: {} };
  290. }
  291. if (allText.includes('Generate LayoutPlan.json')) {
  292. return { content: json(fixture.layoutPlan), model: 'fake-parallel-smoke', usage: {} };
  293. }
  294. if (allText.includes('Generate FileManifest.json')) {
  295. return { content: json(fixture.fileManifest), model: 'fake-parallel-smoke', usage: {} };
  296. }
  297. if (allText.includes('Generate ContractSheet.json')) {
  298. return { content: json(fixture.contractSheet), model: 'fake-parallel-smoke', usage: {} };
  299. }
  300. if (allText.includes('Generate StateBehavior.json')) {
  301. return { content: json(fixture.stateBehavior), model: 'fake-parallel-smoke', usage: {} };
  302. }
  303. if (allText.includes('Generate Process/Specs/IntentSpec.json')) {
  304. return { content: json(fixture.intentSpec), model: 'fake-parallel-smoke', usage: {} };
  305. }
  306. if (allText.includes('Generate Process/Specs/DomainSchema.json')) {
  307. return { content: json(fixture.domainSchema), model: 'fake-parallel-smoke', usage: {} };
  308. }
  309. if (allText.includes('Generate Process/Specs/ServiceContractMap.json')) {
  310. return { content: json(fixture.serviceContractMap), model: 'fake-parallel-smoke', usage: {} };
  311. }
  312. if (allText.includes('Generate Process/Specs/QualityGateSpec.json')) {
  313. return { content: json(fixture.qualityGateSpec), model: 'fake-parallel-smoke', usage: {} };
  314. }
  315. if (allText.includes('Generate Process/Specs/UIContractMap.json')) {
  316. return { content: json(fixture.uiContractMap), model: 'fake-parallel-smoke', usage: {} };
  317. }
  318. if (allText.includes('Generate Process/Specs/ModuleManifest.json')) {
  319. return { content: json(fixture.moduleManifest), model: 'fake-parallel-smoke', usage: {} };
  320. }
  321. if (allText.includes('Generate Process/Specs/DependencyGraph.json')) {
  322. return { content: json(fixture.dependencyGraph), model: 'fake-parallel-smoke', usage: {} };
  323. }
  324. if (allText.includes('Generate Process/Run/ExecutionPlan.json')) {
  325. return { content: json(fixture.executionPlan), model: 'fake-parallel-smoke', usage: {} };
  326. }
  327. if (allText.includes('PRD:') && !allText.includes('VDB:') && !allText.includes('ServiceMap:')) {
  328. return { content: fixture.vdbCode, model: 'fake-parallel-smoke', usage: {} };
  329. }
  330. if (allText.includes('PRD:') && allText.includes('VDB:')) {
  331. return { content: json(fixture.serviceMapJson), model: 'fake-parallel-smoke', usage: {} };
  332. }
  333. if (allText.includes('PRD:') && allText.includes('ServiceMap:')) {
  334. return { content: json(fixture.uiMapJson), model: 'fake-parallel-smoke', usage: {} };
  335. }
  336. if (allText.includes('Generate the .vdb database schema file') || allText.includes('Generate complete VL .vdb source code')) {
  337. return { content: fixture.vdbCode, model: 'fake-parallel-smoke', usage: {} };
  338. }
  339. if (
  340. allText.includes('Generate the .vth theme file') ||
  341. allText.includes('Generate complete VL .vth source code') ||
  342. allText.includes('Generate complete VL .vth theme file')
  343. ) {
  344. return { content: fixture.vthCode, model: 'fake-parallel-smoke', usage: {} };
  345. }
  346. if (allText.includes('Generate the .vs service domain file for:') || allText.includes('Generate complete VL .vs source code')) {
  347. return { content: serviceCode(target?.domainId || target?.id || 'UnknownService'), model: 'fake-parallel-smoke', usage: {} };
  348. }
  349. if (allText.includes('DomainItem:')) {
  350. return { content: serviceCode(target?.domainId || target?.id || 'UnknownService'), model: 'fake-parallel-smoke', usage: {} };
  351. }
  352. if (allText.includes('Generate the .cp component file for:') || allText.includes('Generate complete VL .cp source code')) {
  353. return { content: componentCode(target?.id || 'UnknownComponent'), model: 'fake-parallel-smoke', usage: {} };
  354. }
  355. if (allText.includes('ComponentItem:')) {
  356. return { content: componentCode(target?.id || 'UnknownComponent'), model: 'fake-parallel-smoke', usage: {} };
  357. }
  358. if (allText.includes('Generate complete VL .sc source code') || allText.includes('Generate the .sc section file for:')) {
  359. return { content: sectionCode(target?.id || 'UnknownSection'), model: 'fake-parallel-smoke', usage: {} };
  360. }
  361. if (allText.includes('SectionItem:')) {
  362. return { content: sectionCode(target?.id || 'UnknownSection'), model: 'fake-parallel-smoke', usage: {} };
  363. }
  364. if (allText.includes('Generate complete VL .vx source code') || allText.includes('Generate the .vx app file for:')) {
  365. return { content: appCode(target?.appId || target?.id || 'UnknownApp'), model: 'fake-parallel-smoke', usage: {} };
  366. }
  367. if (allText.includes('AppItem:')) {
  368. return { content: appCode(target?.appId || target?.id || 'UnknownApp'), model: 'fake-parallel-smoke', usage: {} };
  369. }
  370. throw new Error(`Unhandled fake LLM prompt: ${allText.slice(0, 200)}`);
  371. } finally {
  372. probe.active--;
  373. }
  374. },
  375. };
  376. }
  377. async function collectFiles(dir, prefix = '') {
  378. const entries = await fs.readdir(path.join(dir, prefix), { withFileTypes: true }).catch(() => []);
  379. const files = [];
  380. for (const entry of entries) {
  381. const rel = path.join(prefix, entry.name);
  382. if (entry.isDirectory()) {
  383. files.push(...await collectFiles(dir, rel));
  384. } else {
  385. files.push(rel);
  386. }
  387. }
  388. return files.sort();
  389. }
  390. async function runExistingWorkflow(file, options = {}) {
  391. const workflowPath = path.join(process.cwd(), '.vl-code', 'workflows', file);
  392. const workflow = JSON.parse(await fs.readFile(workflowPath, 'utf8'));
  393. const workDir = path.join('/tmp/vlcode-lite-parallel-workflow-test', `${file.replace(/\.json$/, '')}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`);
  394. const fixture = createFixture(options.projectName);
  395. const probe = options.sharedProbe || { active: 0, maxActive: 0 };
  396. const errors = [];
  397. const done = [];
  398. await fs.rm(workDir, { recursive: true, force: true });
  399. await fs.mkdir(workDir, { recursive: true });
  400. const executor = new WorkflowExecutor({ workDir, model: 'fake-parallel-smoke' });
  401. executor._resolveDocCenterDocs = async () => {};
  402. executor._buildLLMAdapter = function buildLLMAdapter() {
  403. return createFakeLLM(fixture, probe, options);
  404. };
  405. await executor.execute(workflow, {
  406. userRequest: 'Generate a small inventory app',
  407. userRequirement: 'Generate a small inventory app',
  408. targetLang: 'zh-CN',
  409. }, {
  410. onNodeDone: (evt) => done.push(evt.nodeId),
  411. onNodeError: (evt) => errors.push(evt.nodeId),
  412. });
  413. return {
  414. workDir,
  415. files: await collectFiles(workDir),
  416. errors,
  417. done,
  418. maxActive: probe.maxActive,
  419. };
  420. }
  421. const concurrentWorkflow = {
  422. version: '3.16',
  423. name: 'ConcurrentWorkflowSmoke',
  424. steps: [
  425. {
  426. id: 'LLM_First',
  427. in: {
  428. stream: true,
  429. messages: [{ role: 'user', content: 'Generate the .vdb database schema file based on the databases defined in ProjectMeta.' }],
  430. },
  431. out: { '$first': '=_result' },
  432. next: 'LLM_Second',
  433. },
  434. {
  435. id: 'LLM_Second',
  436. in: {
  437. stream: true,
  438. messages: [{ role: 'user', content: 'Generate the .vdb database schema file based on the databases defined in ProjectMeta.' }],
  439. },
  440. out: { '$second': '=_result' },
  441. next: 'Stop_End',
  442. },
  443. { id: 'Stop_End' },
  444. ],
  445. };
  446. async function runConcurrentInstance(workDir, sharedProbe) {
  447. const fixture = createFixture('ConcurrentApp');
  448. const executor = new WorkflowExecutor({ workDir, model: 'fake-parallel-smoke' });
  449. executor._resolveDocCenterDocs = async () => {};
  450. executor._buildLLMAdapter = function buildLLMAdapter() {
  451. return createFakeLLM(fixture, sharedProbe, { delayMs: 120 });
  452. };
  453. await fs.rm(workDir, { recursive: true, force: true });
  454. await fs.mkdir(workDir, { recursive: true });
  455. await executor.execute(concurrentWorkflow, {}, {});
  456. }
  457. console.log('\n── Parallel Workflow Codegen Smoke ──');
  458. await test('parallel-codegen has real internal concurrency and writes all code files', async () => {
  459. const result = await runExistingWorkflow('parallel-codegen.json');
  460. assert(result.maxActive >= 4, `Expected internal concurrency >=4, got ${result.maxActive}`);
  461. assert(result.errors.length === 0, `Unexpected errors: ${result.errors.join(', ')}`);
  462. for (const rel of [
  463. '.vl-code/ProjectMeta.json',
  464. 'Process/ProjectMeta.json',
  465. 'Database/SmokeApp.vdb',
  466. 'Theme/Theme.vth',
  467. 'Services/ItemService.vs',
  468. 'Services/OrderService.vs',
  469. 'ExtComponents/ItemCard.cp',
  470. 'ExtComponents/OrderBadge.cp',
  471. 'Sections/HomePage.sc',
  472. 'Sections/OrdersPage.sc',
  473. 'Apps/MainApp.vx',
  474. 'Apps/AdminPortalShell.vx',
  475. ]) {
  476. assertIncludes(result.files, rel);
  477. }
  478. });
  479. await test('meta-direct-codegen keeps DB and Theme fanout working', async () => {
  480. const result = await runExistingWorkflow('meta-direct-codegen.json');
  481. assert(result.maxActive >= 2, `Expected concurrency >=2, got ${result.maxActive}`);
  482. assert(result.errors.length === 0, `Unexpected errors: ${result.errors.join(', ')}`);
  483. for (const rel of [
  484. '.vl-code/ProjectMeta.json',
  485. 'Database/SmokeApp.vdb',
  486. 'Theme/Theme.vth',
  487. 'Services/ItemService.vs',
  488. 'Services/OrderService.vs',
  489. 'ExtComponents/ItemCard.cp',
  490. 'ExtComponents/OrderBadge.cp',
  491. 'Sections/HomePage.sc',
  492. 'Sections/OrdersPage.sc',
  493. 'Apps/MainApp.vx',
  494. 'Apps/AdminPortalShell.vx',
  495. ]) {
  496. assertIncludes(result.files, rel);
  497. }
  498. });
  499. await test('3-file-codegen generates auxiliary specs and target VL files', async () => {
  500. const result = await runExistingWorkflow('3-file-codegen.json');
  501. assert(result.errors.length === 0, `Unexpected errors: ${result.errors.join(', ')}`);
  502. for (const rel of [
  503. 'Process/Artifacts/PRD.json',
  504. 'Process/Artifacts/ServiceMap.json',
  505. 'Process/Artifacts/UIMap.json',
  506. 'Database/SmokeApp.vdb',
  507. 'Theme/Theme.vth',
  508. 'Services/ItemService.vs',
  509. 'Services/OrderService.vs',
  510. 'ExtComponents/ItemCard.cp',
  511. 'ExtComponents/OrderBadge.cp',
  512. 'Sections/HomePage.sc',
  513. 'Sections/OrdersPage.sc',
  514. 'Apps/MainApp.vx',
  515. 'Apps/AdminPortalShell.vx',
  516. ]) {
  517. assertIncludes(result.files, rel);
  518. }
  519. });
  520. await test('6-file-codegen runs parallel spec fanout and parallel code generation', async () => {
  521. const result = await runExistingWorkflow('6-file-codegen.json');
  522. assert(result.maxActive >= 4, `Expected concurrency >=4, got ${result.maxActive}`);
  523. assert(result.errors.length === 0, `Unexpected errors: ${result.errors.join(', ')}`);
  524. for (const rel of [
  525. 'Process/Specs/Blueprint.json',
  526. 'Process/Specs/DataSchema.json',
  527. 'Process/Specs/LayoutPlan.json',
  528. 'Process/Specs/FileManifest.json',
  529. 'Process/Specs/ContractSheet.json',
  530. 'Process/Specs/StateBehavior.json',
  531. 'Database/SmokeApp.vdb',
  532. 'Theme/Theme.vth',
  533. 'Services/ItemService.vs',
  534. 'Services/OrderService.vs',
  535. 'ExtComponents/ItemCard.cp',
  536. 'ExtComponents/OrderBadge.cp',
  537. 'Sections/HomePage.sc',
  538. 'Sections/OrdersPage.sc',
  539. 'Apps/MainApp.vx',
  540. 'Apps/AdminPortalShell.vx',
  541. ]) {
  542. assertIncludes(result.files, rel);
  543. }
  544. });
  545. await test('9-file-codegen runs full parallel generation fanout', async () => {
  546. const result = await runExistingWorkflow('9-file-codegen.json');
  547. assert(result.maxActive >= 2, `Expected concurrency >=2, got ${result.maxActive}`);
  548. assert(result.errors.length === 0, `Unexpected errors: ${result.errors.join(', ')}`);
  549. for (const rel of [
  550. 'Process/Specs/IntentSpec.json',
  551. 'Process/Specs/DomainSchema.json',
  552. 'Process/Specs/ServiceContractMap.json',
  553. 'Process/Specs/QualityGateSpec.json',
  554. 'Process/Specs/UIContractMap.json',
  555. 'Process/Specs/ModuleManifest.json',
  556. 'Process/Specs/DependencyGraph.json',
  557. 'Process/Run/ExecutionPlan.json',
  558. 'Database/SmokeApp.vdb',
  559. 'Theme/Theme.vth',
  560. 'Services/ItemService.vs',
  561. 'Services/OrderService.vs',
  562. 'ExtComponents/ItemCard.cp',
  563. 'ExtComponents/OrderBadge.cp',
  564. 'Sections/HomePage.sc',
  565. 'Sections/OrdersPage.sc',
  566. 'Apps/MainApp.vx',
  567. 'Apps/AdminPortalShell.vx',
  568. ]) {
  569. assertIncludes(result.files, rel);
  570. }
  571. });
  572. await test('parallel component branch failure does not abort sibling code generation', async () => {
  573. const result = await runExistingWorkflow('parallel-codegen.json', { failId: 'OrderBadge' });
  574. assert(result.errors.length >= 1, 'Expected one skipped branch error');
  575. assertIncludes(result.files, 'ExtComponents/ItemCard.cp');
  576. assert(!result.files.includes('ExtComponents/OrderBadge.cp'), 'Failed branch file should be absent');
  577. assertIncludes(result.files, 'Services/ItemService.vs');
  578. assertIncludes(result.files, 'Apps/MainApp.vx');
  579. });
  580. await test('two executor instances can execute concurrently without state bleed', async () => {
  581. const sharedProbe = { active: 0, maxActive: 0 };
  582. const dirA = '/tmp/vlcode-lite-parallel-workflow-test/concurrent-a';
  583. const dirB = '/tmp/vlcode-lite-parallel-workflow-test/concurrent-b';
  584. await Promise.all([
  585. runConcurrentInstance(dirA, sharedProbe),
  586. runConcurrentInstance(dirB, sharedProbe),
  587. ]);
  588. assert(sharedProbe.maxActive >= 2, `Expected cross-instance concurrency >=2, got ${sharedProbe.maxActive}`);
  589. });
  590. console.log(`\n── Results ──\n\n ${passed} passed, ${failed} failed\n`);
  591. process.exit(failed > 0 ? 1 : 0);