test-modules.js 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639
  1. /**
  2. * Module import & export verification for VLCode Lite
  3. * Tests: import succeeds, exports exist, basic function calls work
  4. */
  5. import { fileURLToPath } from 'url';
  6. import path from 'path';
  7. const __dirname = path.dirname(fileURLToPath(import.meta.url));
  8. const results = [];
  9. let passCount = 0, failCount = 0, warnCount = 0;
  10. function ok(mod, msg) { passCount++; results.push(` ✅ ${mod}: ${msg}`); }
  11. function fail(mod, msg) { failCount++; results.push(` ❌ ${mod}: ${msg}`); }
  12. function warn(mod, msg) { warnCount++; results.push(` ⚠️ ${mod}: ${msg}`); }
  13. // ─── 1. Utils ────────────────────────────────────────────────────────
  14. async function testUtils() {
  15. console.log('\n═══ utils/ ═══');
  16. try {
  17. const { loadConfig, APP_VERSION } = await import('./src/utils/config.js');
  18. if (typeof loadConfig !== 'function') throw new Error('loadConfig not a function');
  19. if (!APP_VERSION) throw new Error('APP_VERSION is empty');
  20. const cfg = loadConfig(['--web', '--port', '9999']);
  21. if (cfg.port !== 9999) throw new Error(`port should be 9999, got ${cfg.port}`);
  22. if (!['cli', 'api-key'].includes(cfg.llmProvider)) {
  23. throw new Error(`llmProvider should be cli or api-key, got ${cfg.llmProvider}`);
  24. }
  25. ok('config.js', `v${APP_VERSION}, loadConfig() works, port=9999, llmProvider=${cfg.llmProvider}`);
  26. } catch (e) { fail('config.js', e.message); }
  27. try {
  28. const { extractZip, extractZipToDir, createZipFromDir } = await import('./src/utils/zip-extract.js');
  29. if (typeof extractZip !== 'function') throw new Error('extractZip not a function');
  30. if (typeof extractZipToDir !== 'function') throw new Error('extractZipToDir not a function');
  31. if (typeof createZipFromDir !== 'function') throw new Error('createZipFromDir not a function');
  32. const fns = ['extractZip', 'extractZipToDir', 'createZipFromDir'];
  33. ok('zip-extract.js', `exports: ${fns.join(', ')}`);
  34. } catch (e) { fail('zip-extract.js', e.message); }
  35. }
  36. // ─── 2. Data ─────────────────────────────────────────────────────────
  37. async function testData() {
  38. console.log('\n═══ data/ ═══');
  39. try {
  40. const { VL_VERSION, THEME_VERSION, PARSEVL_URL } = await import('./src/data/versions.js');
  41. if (!VL_VERSION || !THEME_VERSION || !PARSEVL_URL) throw new Error('missing constants');
  42. ok('versions.js', `VL=${VL_VERSION}, Theme=${THEME_VERSION}`);
  43. } catch (e) { fail('versions.js', e.message); }
  44. try {
  45. const { DOCCENTER_API_URL, DOC_REGISTRY, pathFor, docIdFor, resolveDocRef } = await import('./src/data/doc-paths.js');
  46. if (!DOCCENTER_API_URL) throw new Error('DOCCENTER_API_URL missing');
  47. if (typeof pathFor !== 'function') throw new Error('pathFor not a function');
  48. const p = pathFor('vlSyntax');
  49. if (p !== 1) throw new Error(`pathFor('vlSyntax') should be 1, got ${p}`);
  50. ok('doc-paths.js', `API=${DOCCENTER_API_URL.substring(0,30)}..., ${Object.keys(DOC_REGISTRY).length} entries`);
  51. } catch (e) { fail('doc-paths.js', e.message); }
  52. try {
  53. const mod = await import('./src/data/default-theme.js');
  54. ok('default-theme.js', `exports: ${Object.keys(mod).join(', ').substring(0, 60)}`);
  55. } catch (e) { fail('default-theme.js', e.message); }
  56. }
  57. // ─── 3. Core ─────────────────────────────────────────────────────────
  58. async function testCore() {
  59. console.log('\n═══ core/ ═══');
  60. // tool-registry
  61. try {
  62. const { ToolRegistry } = await import('./src/core/tool-registry.js');
  63. const reg = new ToolRegistry();
  64. reg.register('TestTool', { description: 'test', parameters: {}, execute: async () => 'ok' });
  65. if (!reg.has('TestTool')) throw new Error('has() failed');
  66. const result = await reg.execute('TestTool', {});
  67. if (!result || result.result !== 'ok') throw new Error(`execute returned ${JSON.stringify(result)}`);
  68. const schemas = reg.getToolSchemas();
  69. if (schemas.length !== 1) throw new Error(`schemas.length should be 1, got ${schemas.length}`);
  70. reg.clear();
  71. if (reg.listTools().length !== 0) throw new Error('clear() failed');
  72. ok('tool-registry.js', 'register/has/execute/getToolSchemas/clear all work');
  73. } catch (e) { fail('tool-registry.js', e.message); }
  74. // context-manager
  75. try {
  76. const { ContextManager } = await import('./src/core/context-manager.js');
  77. const cm = new ContextManager({ maxOutputTokens: 4096 });
  78. cm.addUserMessage('hello');
  79. cm.addAssistantMessage('hi there');
  80. const msgs = cm.getMessages();
  81. if (msgs.length !== 2) throw new Error(`expected 2 msgs, got ${msgs.length}`);
  82. const usage = cm.getUsage();
  83. if (typeof usage.usedTokens !== 'number') throw new Error('getUsage() missing usedTokens');
  84. ok('context-manager.js', `addUser/addAssistant/getMessages/getUsage work, ${msgs.length} msgs, ${usage.usedTokens} tokens`);
  85. } catch (e) { fail('context-manager.js', e.message); }
  86. // session
  87. try {
  88. const { SessionManager } = await import('./src/core/session.js');
  89. const sm = new SessionManager('/tmp/vlcode-lite-test');
  90. await sm.init();
  91. const { resumed } = await sm.startOrResume();
  92. ok('session.js', `init/startOrResume work, resumed=${resumed}`);
  93. } catch (e) { fail('session.js', e.message); }
  94. // session-pool
  95. try {
  96. const { SessionPool } = await import('./src/core/session-pool.js');
  97. // SessionPool needs shared modules, just verify import
  98. ok('session-pool.js', 'import OK (needs shared modules to construct)');
  99. } catch (e) { fail('session-pool.js', e.message); }
  100. // llm-provider
  101. try {
  102. const { CLIProvider, createProvider, isCLIAvailable } = await import('./src/core/llm-provider.js');
  103. if (typeof CLIProvider !== 'function') throw new Error('CLIProvider not a class');
  104. if (typeof createProvider !== 'function') throw new Error('createProvider not a function');
  105. if (typeof isCLIAvailable !== 'function') throw new Error('isCLIAvailable not a function');
  106. const available = await isCLIAvailable();
  107. const provider = createProvider({ model: 'claude-opus-4-6', port: 4002, llmProvider: available ? 'cli' : 'api-key', cliAvailable: available, apiKey: available ? undefined : 'test-key' });
  108. if (!['cli', 'api-key'].includes(provider.name)) {
  109. throw new Error(`provider.name should be cli or api-key, got ${provider.name}`);
  110. }
  111. ok('llm-provider.js', `Provider OK (${provider.name}), claude CLI available=${available}`);
  112. } catch (e) { fail('llm-provider.js', e.message); }
  113. // prompt-assembler
  114. try {
  115. const { PromptAssembler } = await import('./src/core/prompt-assembler.js');
  116. const pa = new PromptAssembler(
  117. { workDir: '/tmp', model: 'test' },
  118. { isVLProject: () => false, getSummary: () => ({}), getVLConfig: () => '', getProjectMd: () => '', getRules: () => [] }
  119. );
  120. const manual = pa.buildInstructionManual();
  121. if (!manual || manual.length === 0) throw new Error('buildInstructionManual returned empty');
  122. ok('prompt-assembler.js', `buildInstructionManual() returns ${manual.length} chars`);
  123. } catch (e) { fail('prompt-assembler.js', e.message); }
  124. // orchestrator (import only — needs full wiring)
  125. try {
  126. const { AgentOrchestrator } = await import('./src/core/orchestrator.js');
  127. if (typeof AgentOrchestrator !== 'function') throw new Error('not a class');
  128. ok('orchestrator.js', 'import OK (needs full wiring to construct)');
  129. } catch (e) { fail('orchestrator.js', e.message); }
  130. // cli
  131. try {
  132. const { CLIInterface } = await import('./src/core/cli.js');
  133. if (typeof CLIInterface !== 'function') throw new Error('not a class');
  134. ok('cli.js', 'import OK');
  135. } catch (e) { fail('cli.js', e.message); }
  136. // hooks
  137. try {
  138. const { HooksManager } = await import('./src/core/hooks.js');
  139. const hm = new HooksManager('/tmp/no-hooks');
  140. if (typeof hm.hasHooks !== 'function') throw new Error('hasHooks not a function');
  141. if (hm.hasHooks()) throw new Error('should have no hooks');
  142. ok('hooks.js', 'HooksManager works, hasHooks()=false for empty dir');
  143. } catch (e) { fail('hooks.js', e.message); }
  144. }
  145. // ─── 4. VL ───────────────────────────────────────────────────────────
  146. async function testVL() {
  147. console.log('\n═══ vl/ ═══');
  148. try {
  149. const { VLProjectContext } = await import('./src/vl/project-context.js');
  150. const ctx = new VLProjectContext('/tmp/empty-vl-test');
  151. await ctx.load();
  152. if (ctx.isVLProject()) throw new Error('empty dir should not be VL project');
  153. const summary = ctx.getSummary();
  154. if (summary !== null) throw new Error('getSummary should return null for non-VL project');
  155. ok('project-context.js', `load/isVLProject/getSummary work, non-VL returns null`);
  156. } catch (e) { fail('project-context.js', e.message); }
  157. try {
  158. const { VLSymbolIndex } = await import('./src/vl/symbol-index.js');
  159. const { VLProjectContext } = await import('./src/vl/project-context.js');
  160. const ctx = new VLProjectContext('/tmp/empty-vl-test');
  161. await ctx.load();
  162. const si = new VLSymbolIndex(ctx);
  163. await si.build();
  164. const stats = si.getStats();
  165. if (typeof stats.totalSymbols !== 'number') throw new Error('missing totalSymbols');
  166. ok('symbol-index.js', `build/getStats work, symbols=${stats.totalSymbols}`);
  167. } catch (e) { fail('symbol-index.js', e.message); }
  168. try {
  169. const { SmartContextLoader } = await import('./src/vl/smart-context.js');
  170. ok('smart-context.js', 'import OK');
  171. } catch (e) { fail('smart-context.js', e.message); }
  172. try {
  173. const { BlueprintContext } = await import('./src/vl/blueprint-context.js');
  174. const { VLProjectContext } = await import('./src/vl/project-context.js');
  175. const ctx = new VLProjectContext('/tmp/empty-vl-test');
  176. await ctx.load();
  177. const bp = new BlueprintContext(ctx);
  178. await bp.load();
  179. if (bp.hasBlueprints()) throw new Error('empty project should have no blueprints');
  180. ok('blueprint-context.js', 'load/hasBlueprints work');
  181. } catch (e) { fail('blueprint-context.js', e.message); }
  182. try {
  183. const { ImpactAnalyzer } = await import('./src/vl/impact-analyzer.js');
  184. ok('impact-analyzer.js', 'import OK');
  185. } catch (e) { fail('impact-analyzer.js', e.message); }
  186. try {
  187. const { VLAutoFix } = await import('./src/vl/auto-fix.js');
  188. ok('auto-fix.js', 'import OK');
  189. } catch (e) { fail('auto-fix.js', e.message); }
  190. try {
  191. const { FileCache } = await import('./src/vl/file-cache.js');
  192. const fc = new FileCache('/tmp');
  193. const stats = fc.getStats();
  194. if (typeof stats.hits !== 'number') throw new Error('getStats missing hits');
  195. ok('file-cache.js', `FileCache works, hitRate=${stats.hitRate}`);
  196. } catch (e) { fail('file-cache.js', e.message); }
  197. try {
  198. const { FileWatcher } = await import('./src/vl/file-watcher.js');
  199. ok('file-watcher.js', 'import OK');
  200. } catch (e) { fail('file-watcher.js', e.message); }
  201. try {
  202. const mod = await import('./src/vl/metadata-extractor.js');
  203. const fns = Object.keys(mod);
  204. ok('metadata-extractor.js', `exports: ${fns.join(', ')}`);
  205. } catch (e) { fail('metadata-extractor.js', e.message); }
  206. try {
  207. const { computeMetaDiff } = await import('./src/vl/meta-diff.js');
  208. if (typeof computeMetaDiff !== 'function') throw new Error('not a function');
  209. const diff = computeMetaDiff({ apps: [], sections: [] }, { apps: [], sections: [] });
  210. if (!diff.summary && diff.summary !== '') throw new Error('missing summary');
  211. ok('meta-diff.js', 'computeMetaDiff() works on empty metas');
  212. } catch (e) { fail('meta-diff.js', e.message); }
  213. try {
  214. const { parseSections, sectionDiff, mergeSections, smartMerge } = await import('./src/vl/section-diff.js');
  215. const sections = parseSections('# Header\nline1\n# Body\nline2');
  216. if (!(sections instanceof Map)) throw new Error('parseSections should return Map');
  217. if (sections.size < 2) throw new Error(`expected >=2 sections, got ${sections.size}`);
  218. const diff = sectionDiff('# A\nfoo', '# A\nbar\n# B\nnew');
  219. if (!diff.modified && !diff.added) throw new Error('diff should detect changes');
  220. ok('section-diff.js', `parseSections=${sections.size} sections, sectionDiff/mergeSections/smartMerge exported`);
  221. } catch (e) { fail('section-diff.js', e.message); }
  222. try {
  223. const { GenerationPipeline } = await import('./src/vl/generation-pipeline.js');
  224. ok('generation-pipeline.js', 'import OK');
  225. } catch (e) { fail('generation-pipeline.js', e.message); }
  226. try {
  227. const { WorkflowGenerationPipeline } = await import('./src/vl/workflow-generation-pipeline.js');
  228. ok('workflow-generation-pipeline.js', 'import OK');
  229. } catch (e) { fail('workflow-generation-pipeline.js', e.message); }
  230. try {
  231. const { WorkflowExecutor } = await import('./src/vl/workflow-executor.js');
  232. ok('workflow-executor.js', 'import OK');
  233. } catch (e) { fail('workflow-executor.js', e.message); }
  234. try {
  235. const { generateTestSkeleton } = await import('./src/vl/meta-test-generator.js');
  236. if (typeof generateTestSkeleton !== 'function') throw new Error('not a function');
  237. const result = generateTestSkeleton({ apps: [], sections: [], serviceDomains: [] }, {});
  238. if (!Array.isArray(result.testCases)) throw new Error('should return testCases array');
  239. ok('meta-test-generator.js', `generateTestSkeleton() returns ${result.testCases.length} cases for empty meta`);
  240. } catch (e) { fail('meta-test-generator.js', e.message); }
  241. }
  242. // ─── 5. Engine ───────────────────────────────────────────────────────
  243. async function testEngine() {
  244. console.log('\n═══ engine/ ═══');
  245. try {
  246. const { LocalWorkspace } = await import('./src/engine/local-workspace.js');
  247. const lw = new LocalWorkspace('/tmp/vlcode-lite-ws-test');
  248. await lw.createFile({ gid: 'test', path: 'hello.txt', content: 'world' });
  249. const read = await lw.readFile({ gid: 'test', path: 'hello.txt' });
  250. if (!read.content && read.content !== 'world') throw new Error(`readFile failed: ${JSON.stringify(read)}`);
  251. await lw.deleteFile({ gid: 'test', path: 'hello.txt' });
  252. ok('local-workspace.js', 'createFile/readFile/deleteFile work');
  253. } catch (e) { fail('local-workspace.js', e.message); }
  254. try {
  255. const { parseSSEChunk, mapEngineEvent } = await import('./src/engine/sse-parser.js');
  256. if (typeof parseSSEChunk !== 'function') throw new Error('parseSSEChunk not a function');
  257. const events = parseSSEChunk('event: test\ndata: {"foo":"bar"}\n\n');
  258. if (!Array.isArray(events)) throw new Error('should return array');
  259. ok('sse-parser.js', `parseSSEChunk works, parsed ${events.length} events`);
  260. } catch (e) { fail('sse-parser.js', e.message); }
  261. try {
  262. const { WorkflowEngineManager } = await import('./src/engine/workflow-engine-manager.js');
  263. ok('workflow-engine-manager.js', 'import OK');
  264. } catch (e) { fail('workflow-engine-manager.js', e.message); }
  265. }
  266. // ─── 6. Cloud / MCP ─────────────────────────────────────────────────
  267. async function testCloudMcp() {
  268. console.log('\n═══ cloud/ + mcp/ ═══');
  269. try {
  270. const { CloudAPI } = await import('./src/cloud/cloud-api.js');
  271. const api = new CloudAPI({ cookie: 'test' });
  272. if (typeof api.login !== 'function') throw new Error('login not a function');
  273. if (typeof api.listApps !== 'function') throw new Error('listApps not a function');
  274. if (typeof api.compileWorkspace !== 'function') throw new Error('compileWorkspace not a function');
  275. if (typeof api.syncPush !== 'function') throw new Error('syncPush not a function');
  276. ok('cloud-api.js', 'CloudAPI class OK, all key methods present');
  277. } catch (e) { fail('cloud-api.js', e.message); }
  278. // MCP server is a stdio process, just verify import
  279. try {
  280. // Can't import directly (it starts listening on stdin), just check file exists
  281. const fs = await import('fs');
  282. if (!fs.existsSync('./src/mcp/mcp-server.js')) throw new Error('file missing');
  283. ok('mcp-server.js', 'file exists (stdio process, cannot import test)');
  284. } catch (e) { fail('mcp-server.js', e.message); }
  285. }
  286. // ─── 7. Tools ────────────────────────────────────────────────────────
  287. async function testTools() {
  288. console.log('\n═══ tools/ ═══');
  289. const config = {
  290. workDir: '/tmp/vlcode-lite-tool-test',
  291. model: 'claude-opus-4-6',
  292. port: 3300,
  293. maxOutputTokens: 32000,
  294. llmProvider: 'cli',
  295. workspaceApiUrl: 'https://editor.visuallogic.ai/ih5/editor/workspace',
  296. autotest: { headless: true, parallelWorkers: 1, maxCases: 3, useWorkflowEngine: true },
  297. };
  298. const toolFiles = [
  299. ['read-file.js', 'createReadFileTool', config],
  300. ['edit-file.js', 'createEditFileTool', config],
  301. ['write-file.js', 'createWriteFileTool', config],
  302. ['bash.js', 'createBashTool', config],
  303. ['grep.js', 'createGrepTool', config],
  304. ['glob.js', 'createGlobTool', config],
  305. ['todo-write.js', 'createTodoWriteTool', null],
  306. ['sub-agent.js', 'createSubAgentTool', null],
  307. ['ask-user.js', 'createAskUserTool', null],
  308. ['tool-search.js', 'createToolSearchTool', null],
  309. ['memory.js', 'createMemoryTool', config],
  310. ['doc-center.js', 'createDocCenterTool', config],
  311. ['component-fetch.js', 'createComponentFetchTool', config],
  312. ['browser-inspect.js', 'createBrowserInspectTool', config],
  313. ['vl-compile.js', 'createVLCompileTool', config],
  314. ['vl-lint.js', 'createVLLintTool', config],
  315. ['vl-parse.js', 'createVLParseTool', config],
  316. ['vl-syntax-ref.js', 'createVLSyntaxRefTool', config],
  317. ['vl-validate.js', 'createVLValidateTool', config],
  318. ['vl-metadata.js', 'createVLMetadataTool', config],
  319. ['vl-component-test.js', 'createVLComponentTestTool', config],
  320. ['vl-generate.js', 'createVLGenerateTool', config],
  321. ['vl-adjust.js', 'createVLAdjustTool', config],
  322. ['vl-edit-section.js', 'createVLEditSectionTool', config],
  323. ['vl-autofix.js', 'createVLAutoFixTool', config],
  324. ['vl-cascade-edit.js', 'createVLCascadeEditTool', config],
  325. ['vl-symbols.js', 'createVLSymbolsTool', config],
  326. ['vl-impact.js', 'createVLImpactTool', config],
  327. ['meta-diff.js', 'createMetaDiffTool', config],
  328. ['section-diff.js', 'createSectionDiffTool', config],
  329. ['vl-meta-test.js', 'createVLMetaTestTool', config],
  330. ['workflow-run.js', 'createWorkflowRunTool', config],
  331. ['workspace-api.js', 'createWorkspaceAPITool', config],
  332. ['autotest-pipeline.js', 'createAutoTestPipelineTool', config],
  333. ];
  334. for (const [file, fnName, arg] of toolFiles) {
  335. try {
  336. const mod = await import(`./src/tools/${file}`);
  337. const fn = mod[fnName] || mod.default;
  338. if (!fn) {
  339. // Check what's actually exported
  340. const exports = Object.keys(mod);
  341. throw new Error(`${fnName} not found. Exports: [${exports.join(', ')}]`);
  342. }
  343. if (typeof fn !== 'function') throw new Error(`${fnName} is ${typeof fn}, not function`);
  344. let tool;
  345. if (arg === null) {
  346. tool = fn();
  347. } else if (file === 'workspace-api.js') {
  348. const { LocalWorkspace } = await import('./src/engine/local-workspace.js');
  349. tool = fn(config, new LocalWorkspace('/tmp'));
  350. } else if (file === 'autotest-pipeline.js') {
  351. const { VLProjectContext } = await import('./src/vl/project-context.js');
  352. const ctx = new VLProjectContext('/tmp');
  353. tool = fn(config, ctx);
  354. } else {
  355. tool = fn(arg);
  356. }
  357. if (!tool) throw new Error('factory returned null/undefined');
  358. if (!tool.description) throw new Error('missing description');
  359. if (!tool.parameters) throw new Error('missing parameters');
  360. if (typeof tool.execute !== 'function') throw new Error('missing execute function');
  361. ok(file, `${fnName}() → tool with description + parameters + execute`);
  362. } catch (e) {
  363. fail(file, e.message);
  364. }
  365. }
  366. // Test tool registration integration
  367. try {
  368. const { ToolRegistry } = await import('./src/core/tool-registry.js');
  369. const { registerAllTools } = await import('./src/tools/index.js');
  370. const { VLProjectContext } = await import('./src/vl/project-context.js');
  371. const { LocalWorkspace } = await import('./src/engine/local-workspace.js');
  372. const reg = new ToolRegistry();
  373. const ctx = new VLProjectContext('/tmp');
  374. await ctx.load();
  375. registerAllTools(reg, config, ctx, { localWorkspace: new LocalWorkspace('/tmp') });
  376. const tools = reg.listTools();
  377. ok('index.js', `registerAllTools() registered ${tools.length} tools: ${tools.join(', ')}`);
  378. } catch (e) { fail('index.js (registerAllTools)', e.message); }
  379. }
  380. // ─── 8. Server ───────────────────────────────────────────────────────
  381. async function testServer() {
  382. console.log('\n═══ server/ ═══');
  383. try {
  384. const { WebServer } = await import('./src/server/server.js');
  385. if (typeof WebServer !== 'function') throw new Error('not a class');
  386. ok('server.js', 'WebServer class import OK');
  387. } catch (e) { fail('server.js', e.message); }
  388. try {
  389. const mod = await import('./src/server/sse.js');
  390. if (typeof mod.setupSSE !== 'function') throw new Error('setupSSE not a function');
  391. ok('sse.js', 'setupSSE function OK');
  392. } catch (e) { fail('sse.js', e.message); }
  393. try {
  394. const mod = await import('./src/server/helpers.js');
  395. const fns = ['getCookie', 'clearCookie', 'saveCookie', 'loadWorkspaces', 'saveWorkspaces',
  396. 'ensureVLBible', 'hasVLFiles', 'ensureProjectProfile', 'ensureVLCodeHome'];
  397. const missing = fns.filter(f => typeof mod[f] !== 'function');
  398. if (missing.length > 0) throw new Error(`missing functions: ${missing.join(', ')}`);
  399. ok('helpers.js', `all ${fns.length} helper functions exported`);
  400. } catch (e) { fail('helpers.js', e.message); }
  401. try {
  402. const { WebServer } = await import('./src/server/index.js');
  403. if (typeof WebServer !== 'function') throw new Error('re-export failed');
  404. ok('index.js', 'WebServer re-export OK');
  405. } catch (e) { fail('server/index.js', e.message); }
  406. // Route modules — verify they export setup functions
  407. const routes = [
  408. 'chat.js', 'files.js', 'project.js', 'workspace.js', 'compile.js',
  409. 'workflow.js', 'cloud.js', 'intelligence.js', 'tools.js', 'conversation.js', 'misc.js'
  410. ];
  411. const setupFnNames = {
  412. 'chat.js': 'setupChatRoutes',
  413. 'files.js': 'setupFileRoutes',
  414. 'project.js': 'setupProjectRoutes',
  415. 'workspace.js': 'setupWorkspaceRoutes',
  416. 'compile.js': 'setupCompileRoutes',
  417. 'workflow.js': 'setupWorkflowRoutes',
  418. 'cloud.js': 'setupCloudRoutes',
  419. 'intelligence.js': 'setupIntelligenceRoutes',
  420. 'tools.js': 'setupToolRoutes',
  421. 'conversation.js': 'setupConversationRoutes',
  422. 'misc.js': 'setupMiscRoutes',
  423. };
  424. for (const route of routes) {
  425. try {
  426. const mod = await import(`./src/server/routes/${route}`);
  427. const expectedFn = setupFnNames[route];
  428. if (expectedFn && typeof mod[expectedFn] !== 'function') {
  429. throw new Error(`${expectedFn} not found. Exports: [${Object.keys(mod).join(', ')}]`);
  430. }
  431. ok(`routes/${route}`, `${expectedFn}() exported`);
  432. } catch (e) {
  433. fail(`routes/${route}`, e.message);
  434. }
  435. }
  436. }
  437. // ─── 9. Autotest sub-modules ─────────────────────────────────────────
  438. async function testAutotest() {
  439. console.log('\n═══ tools/autotest/ ═══');
  440. const files = [
  441. 'test-generator.js', 'test-runner.js', 'test-evaluator.js',
  442. 'auto-fixer.js', 'case-parser.js', 'report-generator.js',
  443. 'workflow-builder.js', 'workflow-runner.js'
  444. ];
  445. for (const file of files) {
  446. try {
  447. const mod = await import(`./src/tools/autotest/${file}`);
  448. const exports = Object.keys(mod);
  449. if (exports.length === 0) throw new Error('no exports');
  450. ok(`autotest/${file}`, `exports: ${exports.join(', ')}`);
  451. } catch (e) {
  452. fail(`autotest/${file}`, e.message);
  453. }
  454. }
  455. }
  456. // ─── 10. Functional tests (deeper) ──────────────────────────────────
  457. async function testFunctional() {
  458. console.log('\n═══ Functional Tests ═══');
  459. // ReadFile tool — actually read a file
  460. try {
  461. const { createReadFileTool } = await import('./src/tools/read-file.js');
  462. const tool = createReadFileTool({ workDir: '/tmp' });
  463. const result = await tool.execute({ file_path: path.join(__dirname, 'package.json') });
  464. const text = result.result || result;
  465. if (!text.includes('vlcode-lite')) throw new Error('ReadFile did not return package.json content');
  466. ok('ReadFile.execute', 'successfully reads package.json');
  467. } catch (e) { fail('ReadFile.execute', e.message); }
  468. // WriteFile + EditFile + ReadFile round-trip
  469. try {
  470. const { createWriteFileTool } = await import('./src/tools/write-file.js');
  471. const { createEditFileTool } = await import('./src/tools/edit-file.js');
  472. const { createReadFileTool } = await import('./src/tools/read-file.js');
  473. const cfg = { workDir: '/tmp' };
  474. const testFile = '/tmp/vlcode-lite-test-roundtrip.txt';
  475. const wt = createWriteFileTool(cfg);
  476. await wt.execute({ file_path: testFile, content: 'hello world' });
  477. const et = createEditFileTool(cfg);
  478. await et.execute({ file_path: testFile, old_string: 'hello', new_string: 'goodbye' });
  479. const rt = createReadFileTool(cfg);
  480. const readResult = await rt.execute({ file_path: testFile });
  481. const content = readResult.result || readResult;
  482. if (!content.includes('goodbye world')) throw new Error(`expected 'goodbye world', got: ${String(content).substring(0, 100)}`);
  483. ok('Write→Edit→Read roundtrip', 'file operations work correctly');
  484. } catch (e) { fail('Write→Edit→Read roundtrip', e.message); }
  485. // Bash tool
  486. try {
  487. const { createBashTool } = await import('./src/tools/bash.js');
  488. const tool = createBashTool({ workDir: '/tmp' });
  489. const result = await tool.execute({ command: 'echo "vlcode-lite-test"' });
  490. if (!result.includes('vlcode-lite-test')) throw new Error(`expected echo output, got: ${result}`);
  491. ok('Bash.execute', 'echo command works');
  492. } catch (e) { fail('Bash.execute', e.message); }
  493. // Grep tool
  494. try {
  495. const { createGrepTool } = await import('./src/tools/grep.js');
  496. const tool = createGrepTool({ workDir: __dirname });
  497. const result = await tool.execute({ pattern: 'vlcode-lite', path: path.join(__dirname, 'package.json') });
  498. if (!result) throw new Error('grep returned empty');
  499. ok('Grep.execute', 'finds "vlcode-lite" in package.json');
  500. } catch (e) { fail('Grep.execute', e.message); }
  501. // Glob tool
  502. try {
  503. const { createGlobTool } = await import('./src/tools/glob.js');
  504. const tool = createGlobTool({ workDir: __dirname });
  505. const result = await tool.execute({ pattern: 'src/core/*.js', path: __dirname });
  506. if (!result.includes('orchestrator.js')) throw new Error(`glob did not find orchestrator.js: ${result.substring(0, 200)}`);
  507. ok('Glob.execute', 'finds src/core/*.js files');
  508. } catch (e) { fail('Glob.execute', e.message); }
  509. // Memory tool
  510. try {
  511. const { createMemoryTool } = await import('./src/tools/memory.js');
  512. const tool = createMemoryTool({ workDir: '/tmp/vlcode-lite-mem-test' });
  513. await tool.execute({ operation: 'write', scope: 'project', key: 'test-key', value: 'test-value-123' });
  514. const readResult = await tool.execute({ operation: 'read', scope: 'project', key: 'test-key' });
  515. if (!readResult.includes('test-value-123')) throw new Error(`memory read failed: ${readResult}`);
  516. await tool.execute({ operation: 'delete', scope: 'project', key: 'test-key' });
  517. ok('Memory.execute', 'write/read/delete cycle works');
  518. } catch (e) { fail('Memory.execute', e.message); }
  519. // LocalWorkspace advanced
  520. try {
  521. const { LocalWorkspace } = await import('./src/engine/local-workspace.js');
  522. const lw = new LocalWorkspace('/tmp/vlcode-lite-lw-test');
  523. await lw.writeFiles({ gid: 'test', files: [
  524. { path: 'a.txt', content: 'aaa' },
  525. { path: 'sub/b.txt', content: 'bbb' },
  526. ]});
  527. const list = await lw.listFile({ gid: 'test' });
  528. if (!list.files || list.files.length < 2) throw new Error(`listFile should find >=2 files, got ${JSON.stringify(list)}`);
  529. const reads = await lw.readFiles({ gid: 'test', paths: ['a.txt', 'sub/b.txt'] });
  530. if (!reads.files || reads.files.length !== 2) throw new Error(`readFiles should return 2, got ${JSON.stringify(reads)}`);
  531. ok('LocalWorkspace batch', 'writeFiles/listFile/readFiles work');
  532. } catch (e) { fail('LocalWorkspace batch', e.message); }
  533. }
  534. // ─── Run all ─────────────────────────────────────────────────────────
  535. async function main() {
  536. console.log('╔══════════════════════════════════════════════════╗');
  537. console.log('║ VLCode Lite — Module Verification Suite ║');
  538. console.log('╚══════════════════════════════════════════════════╝');
  539. await testUtils();
  540. await testData();
  541. await testCore();
  542. await testVL();
  543. await testEngine();
  544. await testCloudMcp();
  545. await testTools();
  546. await testServer();
  547. await testAutotest();
  548. await testFunctional();
  549. console.log('\n══════════════════════════════════════════════════');
  550. console.log(` Results: ${passCount} passed, ${failCount} failed, ${warnCount} warnings`);
  551. console.log('══════════════════════════════════════════════════');
  552. if (failCount > 0) {
  553. console.log('\n❌ FAILURES:');
  554. results.filter(r => r.includes('❌')).forEach(r => console.log(r));
  555. }
  556. if (warnCount > 0) {
  557. console.log('\n⚠️ WARNINGS:');
  558. results.filter(r => r.includes('⚠️')).forEach(r => console.log(r));
  559. }
  560. console.log('\n✅ PASSED:');
  561. results.filter(r => r.includes('✅')).forEach(r => console.log(r));
  562. process.exit(failCount > 0 ? 1 : 0);
  563. }
  564. main().catch(e => { console.error('FATAL:', e); process.exit(2); });