test-workflow-json-guard.js 2.7 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586
  1. import { WorkflowExecutor } from './src/vl/workflow-executor.js';
  2. let passed = 0;
  3. let failed = 0;
  4. function test(name, fn) {
  5. return Promise.resolve()
  6. .then(fn)
  7. .then(() => {
  8. console.log(` ✓ ${name}`);
  9. passed++;
  10. })
  11. .catch((err) => {
  12. console.log(` ✗ ${name}: ${err.message}`);
  13. failed++;
  14. });
  15. }
  16. function assert(cond, msg) {
  17. if (!cond) throw new Error(msg || 'Assertion failed');
  18. }
  19. console.log('\n── Workflow JSON Guard Regression ──');
  20. function createExecutorWithResponse(text) {
  21. const executor = new WorkflowExecutor({ workDir: '/tmp/vlcode-json-guard', model: 'claude-opus-4-6' });
  22. executor.client = {
  23. messages: {
  24. async create() {
  25. return {
  26. content: [{ type: 'text', text }],
  27. stop_reason: 'end_turn',
  28. usage: {},
  29. model: 'stub-model',
  30. };
  31. },
  32. },
  33. };
  34. return executor;
  35. }
  36. await test('critical PRD generation rejects empty JSON output', async () => {
  37. const executor = createExecutorWithResponse('{}');
  38. let threw = false;
  39. try {
  40. await executor._buildLLMAdapter().call({
  41. docs: ['31'],
  42. output_config: { format: { type: 'json_object' } },
  43. messages: [{ role: 'user', content: 'Requirement: test' }],
  44. });
  45. } catch (err) {
  46. threw = true;
  47. assert(err.message.includes('Critical workflow JSON step'), `unexpected error: ${err.message}`);
  48. }
  49. assert(threw, 'expected critical JSON step to throw');
  50. });
  51. await test('non-critical JSON steps still fall back to empty object for compatibility', async () => {
  52. const executor = createExecutorWithResponse('No JSON here.');
  53. const result = await executor._buildLLMAdapter().call({
  54. docs: ['999'],
  55. output_config: { format: { type: 'json_object' } },
  56. messages: [{ role: 'user', content: 'test' }],
  57. });
  58. assert(result.content === '{}', `expected compatibility fallback, got ${result.content}`);
  59. });
  60. await test('JSON repair removes superfluous closing braces from near-valid payloads', async () => {
  61. const malformed = '{"apps":[{"id":"Main"}],"sections":[{"id":"Home","internal":{"blocks":[]}}}],"components":[]}';
  62. const executor = createExecutorWithResponse(malformed);
  63. const result = await executor._buildLLMAdapter().call({
  64. docs: ['999'],
  65. output_config: { format: { type: 'json_object' } },
  66. messages: [{ role: 'user', content: 'test' }],
  67. });
  68. const parsed = JSON.parse(result.content);
  69. assert(Array.isArray(parsed.apps), 'apps should be preserved after repair');
  70. assert(Array.isArray(parsed.sections), 'sections should be preserved after repair');
  71. });
  72. console.log(`\n── Results ──\n\n ${passed} passed, ${failed} failed\n`);
  73. process.exit(failed > 0 ? 1 : 0);