import { describe, it, expect, beforeEach, afterEach, afterAll } from 'vitest' import { z } from '@bpinternal/zui' import { getClient, getZai, metadata } from './utils' import { TableAdapter } from '../src/adapters/botpress-table' type File = { path: string name: string content: string } const simpleFile: File = { path: 'src/hello.ts', name: 'hello.ts', content: `function greet(name: string) { console.log('Hello, ' + name) } greet('World')`, } const configFile: File = { path: 'config.json', name: 'config.json', content: `{ "version": "1.0.0", "name": "test-app", "enabled": true }`, } const packageFile: File = { path: 'package.json', name: 'package.json', content: `{ "name": "my-app", "version": "0.1.0", "dependencies": { "react": "^18.0.0" } }`, } describe('zai.patch', { timeout: 60_000 }, () => { const zai = getZai() it('patches a single file with simple instruction and verifies syntax', async () => { const result = await zai.patch([simpleFile], 'change the greeting message to say "Hi" instead of "Hello"').result() expect(result.output).toHaveLength(1) expect(result.output[0].path).toBe('src/hello.ts') expect(result.output[0].name).toBe('hello.ts') expect(result.output[0].content).toContain('Hi') expect(result.output[0].content).not.toContain('Hello') expect(result.output[0].content).toContain('greet') expect(result.output[0].content).toMatchInlineSnapshot(` "function greet(name: string) { console.log('Hi, ' + name) } greet('World')" `) expect(result.output[0].patch).toMatchInlineSnapshot(`"◼︎=2| console.log('Hi, ' + name)"`) }) it('patches a single file by adding a line', async () => { const result = await zai .patch( [simpleFile], 'add a complete JSDoc comment block (/** ... */) above the greet function that describes what it does' ) .result() expect(result.output).toHaveLength(1) expect(result.output[0].content).toContain('/**') expect(result.output[0].content).toContain('*/') expect(result.output[0].content).toContain('function greet') expect(result.output[0].content).toMatchInlineSnapshot(` "/** * Greets the provided name. */ function greet(name: string) { console.log('Hello, ' + name) } greet('World')" `) expect(result.output[0].patch).toMatchInlineSnapshot(` "◼︎=1|/** * Greets the provided name. */ function greet(name: string) {" `) }) it('patches a single file by deleting a line', async () => { const result = await zai.patch([simpleFile], 'remove the greet function call at the bottom').result() expect(result.output).toHaveLength(1) expect(result.output[0].content).toContain('function greet') expect(result.output[0].content).not.toContain("greet('World')") expect(result.output[0].content).toMatchInlineSnapshot(` "function greet(name: string) { console.log('Hello, ' + name) } " `) expect(result.output[0].patch).toMatchInlineSnapshot(`"◼︎-005"`) }) it('patches multiple files with a single instruction', async () => { const files = [configFile, packageFile] const result = await zai.patch(files, 'change version to 2.0.0 in all files').result() expect(result.output).toHaveLength(2) expect(result.output.find((f) => f.name === 'config.json')?.content).toContain('"version": "2.0.0"') expect(result.output.find((f) => f.name === 'package.json')?.content).toContain('"version": "2.0.0"') expect(result.output.find((f) => f.name === 'config.json')?.patch).toMatchInlineSnapshot( `"◼︎=2| "version": "2.0.0","` ) expect(result.output.find((f) => f.name === 'package.json')?.patch).toMatchInlineSnapshot( `"◼︎=3| "version": "2.0.0","` ) }) it('patches JSON file correctly', async () => { const result = await zai .patch([configFile], 'add a new field "description": "A test application" after the name field') .result() expect(result.output).toHaveLength(1) expect(result.output[0].content).toContain('"description": "A test application"') expect(result.output[0].content).toContain('"name": "test-app"') // Verify it's still valid JSON structure expect(() => JSON.parse(result.output[0].content)).not.toThrow() expect(result.output[0].content).toMatchInlineSnapshot(` "{ "version": "1.0.0", "name": "test-app", "description": "A test application", "enabled": true }" `) expect(result.output[0].patch).toMatchInlineSnapshot(`"◼︎>003| "description": "A test application","`) }) it('handles complex multi-line changes', async () => { const result = await zai .patch( [simpleFile], 'refactor the greet function to accept an optional greeting parameter that defaults to "Hello"' ) .result() expect(result.output).toHaveLength(1) expect(result.output[0].content).toContain('greet') // Should have parameter with default or optional parameter const hasDefaultParam = result.output[0].content.includes('greeting = ') || result.output[0].content.includes('greeting?') || result.output[0].content.includes('greeting: string = ') expect(hasDefaultParam).toBe(true) expect(result.output[0].content).toMatchInlineSnapshot(` "function greet(name: string, greeting: string = "Hello") { console.log(greeting + ', ' + name) } greet('World')" `) expect(result.output[0].patch).toMatchInlineSnapshot(` "◼︎=001|function greet(name: string, greeting: string = "Hello") { ◼︎=002| console.log(greeting + ', ' + name)" `) }) it('leaves files unchanged when instruction says no changes needed', async () => { const result = await zai.patch([simpleFile], 'only modify files that use React hooks').result() expect(result.output).toHaveLength(1) // File should be mostly unchanged since it doesn't use React expect(result.output[0].path).toBe(simpleFile.path) expect(result.output[0].patch).toMatchInlineSnapshot(`""`) }) it('handles empty files array', async () => { const result = await zai.patch([], 'change something').result() expect(result.output).toHaveLength(0) }) it('preserves file metadata (path and name)', async () => { const files = [ { path: 'src/components/Button.tsx', name: 'Button.tsx', content: 'export const Button = () => null' }, { path: 'src/utils/helper.ts', name: 'helper.ts', content: 'export const helper = () => {}' }, ] const result = await zai.patch(files, 'add a comment at the top of each file').result() expect(result.output).toHaveLength(2) expect(result.output.find((f) => f.path === 'src/components/Button.tsx')).toBeDefined() expect(result.output.find((f) => f.path === 'src/utils/helper.ts')).toBeDefined() expect(result.output.find((f) => f.name === 'Button.tsx')).toBeDefined() expect(result.output.find((f) => f.name === 'helper.ts')).toBeDefined() expect(result.output.find((f) => f.name === 'Button.tsx')?.patch).toMatchInlineSnapshot(`"◼︎<001|//"`) expect(result.output.find((f) => f.name === 'helper.ts')?.patch).toMatchInlineSnapshot(`"◼︎<001|//"`) }) it('handles file with special characters in content', async () => { const specialFile: File = { path: 'test.md', name: 'test.md', content: `# Title\n\nContent with \`code\` and **bold** and [links](url)`, } const result = await zai.patch([specialFile], 'add a new section called "## Installation"').result() expect(result.output).toHaveLength(1) expect(result.output[0].content).toContain('## Installation') expect(result.output[0].content).toContain('# Title') expect(result.output[0].content).toMatchInlineSnapshot(` "# Title Content with \`code\` and **bold** and [links](url) ## Installation" `) expect(result.output[0].patch).toMatchInlineSnapshot(`"◼︎>3|## Installation"`) }) it('patches a markdown table by adding a row', async () => { const tableFile: File = { path: 'fruits.md', name: 'fruits.md', content: `# Fruits | Name | Color | Price | | ------ | ------ | ----- | | Apple | Red | 1.00 | | Banana | Yellow | 0.50 |`, } const result = await zai .patch([tableFile], 'add a new row to the table for "Grape" with color "Purple" and price "2.00"') .result() expect(result.output).toHaveLength(1) const patched = result.output[0].content // Grape row exists and is well-formed (no doubled pipes like "||") expect(patched).toContain('Grape') expect(patched).toContain('Purple') expect(patched).toContain('2.00') expect(patched).not.toMatch(/\|\|/) // Existing rows are preserved expect(patched).toContain('| Apple | Red | 1.00 |') expect(patched).toContain('| Banana | Yellow | 0.50 |') // Every table row starts with a single pipe (not a double pipe) const tableRows = patched.split('\n').filter((l) => l.trim().startsWith('|')) for (const row of tableRows) { expect(row.trimStart().startsWith('||')).toBe(false) } }) it('patches a markdown table by changing an existing row', async () => { const tableFile: File = { path: 'fruits.md', name: 'fruits.md', content: `# Fruits | Name | Color | Price | | ------ | ------ | ----- | | Apple | Red | 1.00 | | Banana | Yellow | 0.50 |`, } const result = await zai.patch([tableFile], 'change the price of Apple from 1.00 to 1.50').result() expect(result.output).toHaveLength(1) const patched = result.output[0].content expect(patched).toContain('1.50') expect(patched).not.toContain('| Apple | Red | 1.00 |') // No doubled pipes — guards against a bug where lines starting with `|` // get an extra leading pipe added by the patch protocol. expect(patched).not.toMatch(/\|\|/) // Banana row is untouched expect(patched).toContain('| Banana | Yellow | 0.50 |') // Every table row starts with a single pipe const tableRows = patched.split('\n').filter((l) => l.trim().startsWith('|')) for (const row of tableRows) { expect(row.trimStart().startsWith('||')).toBe(false) } }) it('can add multiple sections to different files', async () => { const file1: File = { path: 'file1.txt', name: 'file1.txt', content: 'Line 1\nLine 2' } const file2: File = { path: 'file2.txt', name: 'file2.txt', content: 'Content A\nContent B' } const result = await zai.patch([file1, file2], 'add "# Header" at the beginning of each file').result() expect(result.output).toHaveLength(2) expect(result.output[0].content).toContain('# Header') expect(result.output[1].content).toContain('# Header') expect(result.output[0].patch).toMatchInlineSnapshot(`"◼︎<1|# Header"`) expect(result.output[1].patch).toMatchInlineSnapshot(`"◼︎<1|# Header"`) }) it('replaces a range of lines', async () => { const multiLineFile: File = { path: 'example.ts', name: 'example.ts', content: `const a = 1 const b = 2 const c = 3 const d = 4 const e = 5`, } const result = await zai .patch( [multiLineFile], 'replace lines 2 to 4 (const b through const d) with a single line: const sum = 2 + 3 + 4' ) .result() expect(result.output).toHaveLength(1) expect(result.output[0].content).toContain('const sum') expect(result.output[0].content).toContain('const a = 1') expect(result.output[0].content).toContain('const e = 5') expect(result.output[0].content).not.toContain('const b = 2') expect(result.output[0].content).not.toContain('const c = 3') expect(result.output[0].content).not.toContain('const d = 4') expect(result.output[0].content).toMatchInlineSnapshot(` "const a = 1 const sum = 2 + 3 + 4 const e = 5" `) expect(result.output[0].patch).toMatchInlineSnapshot(`"◼︎=2-4|const sum = 2 + 3 + 4"`) }) it('ensures patched JSON is valid JSON', async () => { const jsonFile: File = { path: 'data.json', name: 'data.json', content: `{ "users": [ {"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"} ], "count": 2 }`, } const result = await zai .patch([jsonFile], 'add a new user {"id": 3, "name": "Charlie"} to the users array and update count to 3') .result() expect(result.output).toHaveLength(1) // Verify it's valid JSON expect(() => JSON.parse(result.output[0].content)).not.toThrow() const parsed = JSON.parse(result.output[0].content) expect(parsed.users).toHaveLength(3) expect(parsed.count).toBe(3) expect(parsed.users[2].name).toBe('Charlie') expect(result.output[0].content).toMatchInlineSnapshot(` "{ "users": [ {"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}, {"id": 3, "name": "Charlie"} ], "count": 3 }" `) expect(result.output[0].patch).toMatchInlineSnapshot(` "◼︎=4| {"id": 2, "name": "Bob"}, ◼︎<5| {"id": 3, "name": "Charlie"} ◼︎=6| "count": 3" `) }) it('handles large file with chunking', async () => { // Create a large file with many lines (but not TOO large) const largeContent = Array.from({ length: 1000 }, (_, i) => `const var${i} = ${i}`).join('\n') const largeFile: File = { path: 'large.ts', name: 'large.ts', content: largeContent, } const result = await zai.patch([largeFile], 'add a comment "// Variables" at the very top').result() expect(result.output).toHaveLength(1) expect(result.output[0].content).toContain('// Variables') expect(result.output[0].content).toContain('const var0 = 0') expect(result.output[0].patch).toMatchInlineSnapshot(`"◼︎<1|// Variables"`) }) it('patches code that requires understanding context from two files', async () => { const apiFile: File = { path: 'src/api.ts', name: 'api.ts', content: `export interface User { id: number name: string } export function getUser(id: number): User { return { id, name: 'Unknown' } }`, } const componentFile: File = { path: 'src/UserComponent.tsx', name: 'UserComponent.tsx', content: `import { getUser } from './api' export function UserComponent({ userId }: { userId: number }) { const user = getUser(userId) return
{user.name}
}`, } const result = await zai .patch( [apiFile, componentFile], 'add an email field to the User interface and update the component to display it' ) .result() expect(result.output).toHaveLength(2) const patchedApi = result.output.find((f) => f.name === 'api.ts') const patchedComponent = result.output.find((f) => f.name === 'UserComponent.tsx') // API should have email field expect(patchedApi?.content).toContain('email') // Component should display email expect(patchedComponent?.content).toContain('email') expect(patchedApi?.content).toMatchInlineSnapshot(` "export interface User { id: number name: string email: string } export function getUser(id: number): User { return { id, name: 'Unknown', email: 'unknown@example.com' } }" `) expect(patchedApi?.patch).toMatchInlineSnapshot(` "◼︎>003| email: string ◼︎=007| return { id, name: 'Unknown', email: 'unknown@example.com' }" `) expect(patchedComponent?.content).toMatchInlineSnapshot(` "import { getUser } from './api' export function UserComponent({ userId }: { userId: number }) { const user = getUser(userId) return
{user.name} ({user.email})
}" `) expect(patchedComponent?.patch).toMatchInlineSnapshot(`"◼︎=005| return
{user.name} ({user.email})
"`) }) it('refactors two files in a related way (rename function and update usage)', async () => { const utilsFile: File = { path: 'src/utils.ts', name: 'utils.ts', content: `export function calculateTotal(items: number[]): number { return items.reduce((sum, item) => sum + item, 0) }`, } const appFile: File = { path: 'src/app.ts', name: 'app.ts', content: `import { calculateTotal } from './utils' const prices = [10, 20, 30] const total = calculateTotal(prices) console.log(total)`, } const result = await zai .patch([utilsFile, appFile], 'rename the calculateTotal function to computeSum and update all usages') .result() expect(result.output).toHaveLength(2) const patchedUtils = result.output.find((f) => f.name === 'utils.ts') const patchedApp = result.output.find((f) => f.name === 'app.ts') // Utils should have new name expect(patchedUtils?.content).toContain('computeSum') expect(patchedUtils?.content).not.toContain('calculateTotal') // App should import and use new name expect(patchedApp?.content).toContain('computeSum') expect(patchedApp?.content).not.toContain('calculateTotal') expect(patchedUtils?.content).toMatchInlineSnapshot(` "export function computeSum(items: number[]): number { return items.reduce((sum, item) => sum + item, 0) }" `) expect(patchedUtils?.patch).toMatchInlineSnapshot(`"◼︎=001|export function computeSum(items: number[]): number {"`) expect(patchedApp?.content).toMatchInlineSnapshot(` "import { computeSum } from './utils' const prices = [10, 20, 30] const total = computeSum(prices) console.log(total)" `) expect(patchedApp?.patch).toMatchInlineSnapshot(` "◼︎=001|import { computeSum } from './utils' ◼︎=004|const total = computeSum(prices)" `) }) it('leaves files unchanged when they do not need changes (5 files, only 2 need changes)', async () => { const asyncFile1: File = { path: 'src/api1.ts', name: 'api1.ts', content: `export async function fetchUser(id: number) { const response = await fetch('/api/user/' + id) return response.json() }`, } const asyncFile2: File = { path: 'src/api2.ts', name: 'api2.ts', content: `export async function saveData(data: any) { const response = await fetch('/api/save', { method: 'POST', body: JSON.stringify(data) }) return response.json() }`, } const syncFile1: File = { path: 'src/utils.ts', name: 'utils.ts', content: `export function formatName(first: string, last: string) { return first + ' ' + last }`, } const syncFile2: File = { path: 'src/constants.ts', name: 'constants.ts', content: `export const API_URL = 'https://api.example.com' export const TIMEOUT = 5000`, } const configFile: File = { path: 'config.json', name: 'config.json', content: `{ "name": "app", "version": "1.0.0" }`, } const files = [asyncFile1, asyncFile2, syncFile1, syncFile2, configFile] const result = await zai .patch(files, 'add try-catch error handling to all async functions that call fetch') .result() expect(result.output).toHaveLength(5) // Find the patched files const patchedApi1 = result.output.find((f) => f.name === 'api1.ts') const patchedApi2 = result.output.find((f) => f.name === 'api2.ts') const patchedUtils = result.output.find((f) => f.name === 'utils.ts') const patchedConstants = result.output.find((f) => f.name === 'constants.ts') const patchedConfig = result.output.find((f) => f.name === 'config.json') // Async files should have try-catch expect(patchedApi1?.content).toContain('try') expect(patchedApi1?.content).toContain('catch') expect(patchedApi2?.content).toContain('try') expect(patchedApi2?.content).toContain('catch') // Sync files and config should be unchanged expect(patchedUtils?.patch).toBe('') expect(patchedConstants?.patch).toBe('') expect(patchedConfig?.patch).toBe('') // Check that async files have patches expect(patchedApi1?.patch).not.toBe('') expect(patchedApi2?.patch).not.toBe('') expect(patchedApi1?.content).toMatchInlineSnapshot(` "export async function fetchUser(id: number) { try { const response = await fetch('/api/user/' + id) return response.json() } catch (error) { console.error('Error fetching user:', error) throw error } }" `) expect(patchedApi1?.patch).toMatchInlineSnapshot(` "◼︎=2-3|try { const response = await fetch('/api/user/' + id) return response.json() } catch (error) { console.error('Error fetching user:', error) throw error }" `) expect(patchedApi2?.content).toMatchInlineSnapshot(` "export async function saveData(data: any) { try { const response = await fetch('/api/save', { method: 'POST', body: JSON.stringify(data) }) return response.json() } catch (error) { console.error('Error saving data:', error) throw error } }" `) expect(patchedApi2?.patch).toMatchInlineSnapshot(` "◼︎=2-3|try { const response = await fetch('/api/save', { method: 'POST', body: JSON.stringify(data) }) return response.json() } catch (error) { console.error('Error saving data:', error) throw error }" `) }) it('patches a file with many lines (500 lines) updating ~50 lines spread throughout', async () => { // Create a file with 500 lines where every 10th line has a TODO comment const lines: string[] = [] for (let i = 0; i < 500; i++) { if (i % 10 === 0) { lines.push(` // TODO: implement feature ${i / 10}`) } else { lines.push(` const var${i} = ${i}`) } } const largeFile: File = { path: 'src/large.ts', name: 'large.ts', content: `function largeFunction() {\n${lines.join('\n')}\n}`, } const result = await zai.patch([largeFile], 'replace all TODO comments with FIXME comments').result() expect(result.output).toHaveLength(1) const patched = result.output[0] // All TODO should be replaced with FIXME expect(patched.content).not.toContain('TODO') expect(patched.content).toContain('FIXME') // Should still have the function structure expect(patched.content).toContain('function largeFunction()') expect(patched.content).toContain('const var1 = 1') expect(patched.content).toContain('const var499 = 499') // Count FIXME occurrences (should be 50) const fixmeCount = (patched.content.match(/FIXME/g) || []).length expect(fixmeCount).toBe(50) // Patch should have replacements expect(patched.patch).toContain('FIXME') expect(patched.patch).not.toBe('') expect(patched.content).toMatchInlineSnapshot(` "function largeFunction() { // FIXME: implement feature 0 const var1 = 1 const var2 = 2 const var3 = 3 const var4 = 4 const var5 = 5 const var6 = 6 const var7 = 7 const var8 = 8 const var9 = 9 // FIXME: implement feature 1 const var11 = 11 const var12 = 12 const var13 = 13 const var14 = 14 const var15 = 15 const var16 = 16 const var17 = 17 const var18 = 18 const var19 = 19 // FIXME: implement feature 2 const var21 = 21 const var22 = 22 const var23 = 23 const var24 = 24 const var25 = 25 const var26 = 26 const var27 = 27 const var28 = 28 const var29 = 29 // FIXME: implement feature 3 const var31 = 31 const var32 = 32 const var33 = 33 const var34 = 34 const var35 = 35 const var36 = 36 const var37 = 37 const var38 = 38 const var39 = 39 // FIXME: implement feature 4 const var41 = 41 const var42 = 42 const var43 = 43 const var44 = 44 const var45 = 45 const var46 = 46 const var47 = 47 const var48 = 48 const var49 = 49 // FIXME: implement feature 5 const var51 = 51 const var52 = 52 const var53 = 53 const var54 = 54 const var55 = 55 const var56 = 56 const var57 = 57 const var58 = 58 const var59 = 59 // FIXME: implement feature 6 const var61 = 61 const var62 = 62 const var63 = 63 const var64 = 64 const var65 = 65 const var66 = 66 const var67 = 67 const var68 = 68 const var69 = 69 // FIXME: implement feature 7 const var71 = 71 const var72 = 72 const var73 = 73 const var74 = 74 const var75 = 75 const var76 = 76 const var77 = 77 const var78 = 78 const var79 = 79 // FIXME: implement feature 8 const var81 = 81 const var82 = 82 const var83 = 83 const var84 = 84 const var85 = 85 const var86 = 86 const var87 = 87 const var88 = 88 const var89 = 89 // FIXME: implement feature 9 const var91 = 91 const var92 = 92 const var93 = 93 const var94 = 94 const var95 = 95 const var96 = 96 const var97 = 97 const var98 = 98 const var99 = 99 // FIXME: implement feature 10 const var101 = 101 const var102 = 102 const var103 = 103 const var104 = 104 const var105 = 105 const var106 = 106 const var107 = 107 const var108 = 108 const var109 = 109 // FIXME: implement feature 11 const var111 = 111 const var112 = 112 const var113 = 113 const var114 = 114 const var115 = 115 const var116 = 116 const var117 = 117 const var118 = 118 const var119 = 119 // FIXME: implement feature 12 const var121 = 121 const var122 = 122 const var123 = 123 const var124 = 124 const var125 = 125 const var126 = 126 const var127 = 127 const var128 = 128 const var129 = 129 // FIXME: implement feature 13 const var131 = 131 const var132 = 132 const var133 = 133 const var134 = 134 const var135 = 135 const var136 = 136 const var137 = 137 const var138 = 138 const var139 = 139 // FIXME: implement feature 14 const var141 = 141 const var142 = 142 const var143 = 143 const var144 = 144 const var145 = 145 const var146 = 146 const var147 = 147 const var148 = 148 const var149 = 149 // FIXME: implement feature 15 const var151 = 151 const var152 = 152 const var153 = 153 const var154 = 154 const var155 = 155 const var156 = 156 const var157 = 157 const var158 = 158 const var159 = 159 // FIXME: implement feature 16 const var161 = 161 const var162 = 162 const var163 = 163 const var164 = 164 const var165 = 165 const var166 = 166 const var167 = 167 const var168 = 168 const var169 = 169 // FIXME: implement feature 17 const var171 = 171 const var172 = 172 const var173 = 173 const var174 = 174 const var175 = 175 const var176 = 176 const var177 = 177 const var178 = 178 const var179 = 179 // FIXME: implement feature 18 const var181 = 181 const var182 = 182 const var183 = 183 const var184 = 184 const var185 = 185 const var186 = 186 const var187 = 187 const var188 = 188 const var189 = 189 // FIXME: implement feature 19 const var191 = 191 const var192 = 192 const var193 = 193 const var194 = 194 const var195 = 195 const var196 = 196 const var197 = 197 const var198 = 198 const var199 = 199 // FIXME: implement feature 20 const var201 = 201 const var202 = 202 const var203 = 203 const var204 = 204 const var205 = 205 const var206 = 206 const var207 = 207 const var208 = 208 const var209 = 209 // FIXME: implement feature 21 const var211 = 211 const var212 = 212 const var213 = 213 const var214 = 214 const var215 = 215 const var216 = 216 const var217 = 217 const var218 = 218 const var219 = 219 // FIXME: implement feature 22 const var221 = 221 const var222 = 222 const var223 = 223 const var224 = 224 const var225 = 225 const var226 = 226 const var227 = 227 const var228 = 228 const var229 = 229 // FIXME: implement feature 23 const var231 = 231 const var232 = 232 const var233 = 233 const var234 = 234 const var235 = 235 const var236 = 236 const var237 = 237 const var238 = 238 const var239 = 239 // FIXME: implement feature 24 const var241 = 241 const var242 = 242 const var243 = 243 const var244 = 244 const var245 = 245 const var246 = 246 const var247 = 247 const var248 = 248 const var249 = 249 // FIXME: implement feature 25 const var251 = 251 const var252 = 252 const var253 = 253 const var254 = 254 const var255 = 255 const var256 = 256 const var257 = 257 const var258 = 258 const var259 = 259 // FIXME: implement feature 26 const var261 = 261 const var262 = 262 const var263 = 263 const var264 = 264 const var265 = 265 const var266 = 266 const var267 = 267 const var268 = 268 const var269 = 269 // FIXME: implement feature 27 const var271 = 271 const var272 = 272 const var273 = 273 const var274 = 274 const var275 = 275 const var276 = 276 const var277 = 277 const var278 = 278 const var279 = 279 // FIXME: implement feature 28 const var281 = 281 const var282 = 282 const var283 = 283 const var284 = 284 const var285 = 285 const var286 = 286 const var287 = 287 const var288 = 288 const var289 = 289 // FIXME: implement feature 29 const var291 = 291 const var292 = 292 const var293 = 293 const var294 = 294 const var295 = 295 const var296 = 296 const var297 = 297 const var298 = 298 const var299 = 299 // FIXME: implement feature 30 const var301 = 301 const var302 = 302 const var303 = 303 const var304 = 304 const var305 = 305 const var306 = 306 const var307 = 307 const var308 = 308 const var309 = 309 // FIXME: implement feature 31 const var311 = 311 const var312 = 312 const var313 = 313 const var314 = 314 const var315 = 315 const var316 = 316 const var317 = 317 const var318 = 318 const var319 = 319 // FIXME: implement feature 32 const var321 = 321 const var322 = 322 const var323 = 323 const var324 = 324 const var325 = 325 const var326 = 326 const var327 = 327 const var328 = 328 const var329 = 329 // FIXME: implement feature 33 const var331 = 331 const var332 = 332 const var333 = 333 const var334 = 334 const var335 = 335 const var336 = 336 const var337 = 337 const var338 = 338 const var339 = 339 // FIXME: implement feature 34 const var341 = 341 const var342 = 342 const var343 = 343 const var344 = 344 const var345 = 345 const var346 = 346 const var347 = 347 const var348 = 348 const var349 = 349 // FIXME: implement feature 35 const var351 = 351 const var352 = 352 const var353 = 353 const var354 = 354 const var355 = 355 const var356 = 356 const var357 = 357 const var358 = 358 const var359 = 359 // FIXME: implement feature 36 const var361 = 361 const var362 = 362 const var363 = 363 const var364 = 364 const var365 = 365 const var366 = 366 const var367 = 367 const var368 = 368 const var369 = 369 // FIXME: implement feature 37 const var371 = 371 const var372 = 372 const var373 = 373 const var374 = 374 const var375 = 375 const var376 = 376 const var377 = 377 const var378 = 378 const var379 = 379 // FIXME: implement feature 38 const var381 = 381 const var382 = 382 const var383 = 383 const var384 = 384 const var385 = 385 const var386 = 386 const var387 = 387 const var388 = 388 const var389 = 389 // FIXME: implement feature 39 const var391 = 391 const var392 = 392 const var393 = 393 const var394 = 394 const var395 = 395 const var396 = 396 const var397 = 397 const var398 = 398 const var399 = 399 // FIXME: implement feature 40 const var401 = 401 const var402 = 402 const var403 = 403 const var404 = 404 const var405 = 405 const var406 = 406 const var407 = 407 const var408 = 408 const var409 = 409 // FIXME: implement feature 41 const var411 = 411 const var412 = 412 const var413 = 413 const var414 = 414 const var415 = 415 const var416 = 416 const var417 = 417 const var418 = 418 const var419 = 419 // FIXME: implement feature 42 const var421 = 421 const var422 = 422 const var423 = 423 const var424 = 424 const var425 = 425 const var426 = 426 const var427 = 427 const var428 = 428 const var429 = 429 // FIXME: implement feature 43 const var431 = 431 const var432 = 432 const var433 = 433 const var434 = 434 const var435 = 435 const var436 = 436 const var437 = 437 const var438 = 438 const var439 = 439 // FIXME: implement feature 44 const var441 = 441 const var442 = 442 const var443 = 443 const var444 = 444 const var445 = 445 const var446 = 446 const var447 = 447 const var448 = 448 const var449 = 449 // FIXME: implement feature 45 const var451 = 451 const var452 = 452 const var453 = 453 const var454 = 454 const var455 = 455 const var456 = 456 const var457 = 457 const var458 = 458 const var459 = 459 // FIXME: implement feature 46 const var461 = 461 const var462 = 462 const var463 = 463 const var464 = 464 const var465 = 465 const var466 = 466 const var467 = 467 const var468 = 468 const var469 = 469 // FIXME: implement feature 47 const var471 = 471 const var472 = 472 const var473 = 473 const var474 = 474 const var475 = 475 const var476 = 476 const var477 = 477 const var478 = 478 const var479 = 479 // FIXME: implement feature 48 const var481 = 481 const var482 = 482 const var483 = 483 const var484 = 484 const var485 = 485 const var486 = 486 const var487 = 487 const var488 = 488 const var489 = 489 // FIXME: implement feature 49 const var491 = 491 const var492 = 492 const var493 = 493 const var494 = 494 const var495 = 495 const var496 = 496 const var497 = 497 const var498 = 498 const var499 = 499 }" `) expect(patched.patch).toMatchInlineSnapshot(` "◼︎=2| // FIXME: implement feature 0 ◼︎=12| // FIXME: implement feature 1 ◼︎=22| // FIXME: implement feature 2 ◼︎=32| // FIXME: implement feature 3 ◼︎=42| // FIXME: implement feature 4 ◼︎=52| // FIXME: implement feature 5 ◼︎=62| // FIXME: implement feature 6 ◼︎=72| // FIXME: implement feature 7 ◼︎=82| // FIXME: implement feature 8 ◼︎=92| // FIXME: implement feature 9 ◼︎=102| // FIXME: implement feature 10 ◼︎=112| // FIXME: implement feature 11 ◼︎=122| // FIXME: implement feature 12 ◼︎=132| // FIXME: implement feature 13 ◼︎=142| // FIXME: implement feature 14 ◼︎=152| // FIXME: implement feature 15 ◼︎=162| // FIXME: implement feature 16 ◼︎=172| // FIXME: implement feature 17 ◼︎=182| // FIXME: implement feature 18 ◼︎=192| // FIXME: implement feature 19 ◼︎=202| // FIXME: implement feature 20 ◼︎=212| // FIXME: implement feature 21 ◼︎=222| // FIXME: implement feature 22 ◼︎=232| // FIXME: implement feature 23 ◼︎=242| // FIXME: implement feature 24 ◼︎=252| // FIXME: implement feature 25 ◼︎=262| // FIXME: implement feature 26 ◼︎=272| // FIXME: implement feature 27 ◼︎=282| // FIXME: implement feature 28 ◼︎=292| // FIXME: implement feature 29 ◼︎=302| // FIXME: implement feature 30 ◼︎=312| // FIXME: implement feature 31 ◼︎=322| // FIXME: implement feature 32 ◼︎=332| // FIXME: implement feature 33 ◼︎=342| // FIXME: implement feature 34 ◼︎=352| // FIXME: implement feature 35 ◼︎=362| // FIXME: implement feature 36 ◼︎=372| // FIXME: implement feature 37 ◼︎=382| // FIXME: implement feature 38 ◼︎=392| // FIXME: implement feature 39 ◼︎=402| // FIXME: implement feature 40 ◼︎=412| // FIXME: implement feature 41 ◼︎=422| // FIXME: implement feature 42 ◼︎=432| // FIXME: implement feature 43 ◼︎=442| // FIXME: implement feature 44 ◼︎=452| // FIXME: implement feature 45 ◼︎=462| // FIXME: implement feature 46 ◼︎=472| // FIXME: implement feature 47 ◼︎=482| // FIXME: implement feature 48 ◼︎=492| // FIXME: implement feature 49" `) }) it('refactors a function in a large file (200 lines): renames it, changes parameters, updates all usages', async () => { // Create a 200-line file with a function defined at the top and used throughout const lines: string[] = [] lines.push('// Utility functions') lines.push('function processItem(item: string): string {') lines.push(' return item.toUpperCase()') lines.push('}') lines.push('') lines.push('// Main code') // Add 194 more lines with multiple usages of processItem for (let i = 0; i < 194; i++) { if (i % 20 === 0) { lines.push(`const result${i} = processItem('value${i}')`) } else if (i % 30 === 0) { lines.push(`console.log(processItem('test${i}'))`) } else { lines.push(`const data${i} = 'some data ${i}'`) } } const largeFile: File = { path: 'src/processor.ts', name: 'processor.ts', content: lines.join('\n'), } const result = await zai .patch( [largeFile], 'rename processItem to transformText and add a second parameter "options: { uppercase: boolean }" with default value { uppercase: false }. Update all function calls to use the new signature. For string where number value in the 1st param is above 100, make it uppercase.' ) .result() expect(result.output).toHaveLength(1) const patched = result.output[0] // Should not have old function name expect(patched.content).not.toContain('processItem') // Should have new function name expect(patched.content).toContain('transformText') // Should have the new parameter expect(patched.content).toContain('options') expect(patched.content).toContain('uppercase') // All usages should be updated const transformTextCount = (patched.content.match(/transformText/g) || []).length expect(transformTextCount).toBeGreaterThan(1) // Function definition + usages // Patch should not be empty expect(patched.patch).not.toBe('') expect(patched.content).toMatchInlineSnapshot(` "// Utility functions function transformText(item: string, options: { uppercase: boolean } = { uppercase: false }): string { return options.uppercase ? item.toUpperCase() : item } // Main code const result0 = transformText('value0', { uppercase: false }) const data1 = 'some data 1' const data2 = 'some data 2' const data3 = 'some data 3' const data4 = 'some data 4' const data5 = 'some data 5' const data6 = 'some data 6' const data7 = 'some data 7' const data8 = 'some data 8' const data9 = 'some data 9' const data10 = 'some data 10' const data11 = 'some data 11' const data12 = 'some data 12' const data13 = 'some data 13' const data14 = 'some data 14' const data15 = 'some data 15' const data16 = 'some data 16' const data17 = 'some data 17' const data18 = 'some data 18' const data19 = 'some data 19' const result20 = transformText('value20', { uppercase: false }) const data21 = 'some data 21' const data22 = 'some data 22' const data23 = 'some data 23' const data24 = 'some data 24' const data25 = 'some data 25' const data26 = 'some data 26' const data27 = 'some data 27' const data28 = 'some data 28' const data29 = 'some data 29' console.log(transformText('test30', { uppercase: false })) const data31 = 'some data 31' const data32 = 'some data 32' const data33 = 'some data 33' const data34 = 'some data 34' const data35 = 'some data 35' const data36 = 'some data 36' const data37 = 'some data 37' const data38 = 'some data 38' const data39 = 'some data 39' const result40 = transformText('value40', { uppercase: false }) const data41 = 'some data 41' const data42 = 'some data 42' const data43 = 'some data 43' const data44 = 'some data 44' const data45 = 'some data 45' const data46 = 'some data 46' const data47 = 'some data 47' const data48 = 'some data 48' const data49 = 'some data 49' const data50 = 'some data 50' const data51 = 'some data 51' const data52 = 'some data 52' const data53 = 'some data 53' const data54 = 'some data 54' const data55 = 'some data 55' const data56 = 'some data 56' const data57 = 'some data 57' const data58 = 'some data 58' const data59 = 'some data 59' const result60 = transformText('value60', { uppercase: false }) const data61 = 'some data 61' const data62 = 'some data 62' const data63 = 'some data 63' const data64 = 'some data 64' const data65 = 'some data 65' const data66 = 'some data 66' const data67 = 'some data 67' const data68 = 'some data 68' const data69 = 'some data 69' const data70 = 'some data 70' const data71 = 'some data 71' const data72 = 'some data 72' const data73 = 'some data 73' const data74 = 'some data 74' const data75 = 'some data 75' const data76 = 'some data 76' const data77 = 'some data 77' const data78 = 'some data 78' const data79 = 'some data 79' const result80 = transformText('value80', { uppercase: false }) const data81 = 'some data 81' const data82 = 'some data 82' const data83 = 'some data 83' const data84 = 'some data 84' const data85 = 'some data 85' const data86 = 'some data 86' const data87 = 'some data 87' const data88 = 'some data 88' const data89 = 'some data 89' console.log(transformText('test90', { uppercase: false })) const data91 = 'some data 91' const data92 = 'some data 92' const data93 = 'some data 93' const data94 = 'some data 94' const data95 = 'some data 95' const data96 = 'some data 96' const data97 = 'some data 97' const data98 = 'some data 98' const data99 = 'some data 99' const result100 = transformText('value100', { uppercase: false }) const data101 = 'some data 101' const data102 = 'some data 102' const data103 = 'some data 103' const data104 = 'some data 104' const data105 = 'some data 105' const data106 = 'some data 106' const data107 = 'some data 107' const data108 = 'some data 108' const data109 = 'some data 109' const data110 = 'some data 110' const data111 = 'some data 111' const data112 = 'some data 112' const data113 = 'some data 113' const data114 = 'some data 114' const data115 = 'some data 115' const data116 = 'some data 116' const data117 = 'some data 117' const data118 = 'some data 118' const data119 = 'some data 119' const result120 = transformText('value120', { uppercase: true }) const data121 = 'some data 121' const data122 = 'some data 122' const data123 = 'some data 123' const data124 = 'some data 124' const data125 = 'some data 125' const data126 = 'some data 126' const data127 = 'some data 127' const data128 = 'some data 128' const data129 = 'some data 129' const data130 = 'some data 130' const data131 = 'some data 131' const data132 = 'some data 132' const data133 = 'some data 133' const data134 = 'some data 134' const data135 = 'some data 135' const data136 = 'some data 136' const data137 = 'some data 137' const data138 = 'some data 138' const data139 = 'some data 139' const result140 = transformText('value140', { uppercase: true }) const data141 = 'some data 141' const data142 = 'some data 142' const data143 = 'some data 143' const data144 = 'some data 144' const data145 = 'some data 145' const data146 = 'some data 146' const data147 = 'some data 147' const data148 = 'some data 148' const data149 = 'some data 149' console.log(transformText('test150', { uppercase: true })) const data151 = 'some data 151' const data152 = 'some data 152' const data153 = 'some data 153' const data154 = 'some data 154' const data155 = 'some data 155' const data156 = 'some data 156' const data157 = 'some data 157' const data158 = 'some data 158' const data159 = 'some data 159' const result160 = transformText('value160', { uppercase: true }) const data161 = 'some data 161' const data162 = 'some data 162' const data163 = 'some data 163' const data164 = 'some data 164' const data165 = 'some data 165' const data166 = 'some data 166' const data167 = 'some data 167' const data168 = 'some data 168' const data169 = 'some data 169' const data170 = 'some data 170' const data171 = 'some data 171' const data172 = 'some data 172' const data173 = 'some data 173' const data174 = 'some data 174' const data175 = 'some data 175' const data176 = 'some data 176' const data177 = 'some data 177' const data178 = 'some data 178' const data179 = 'some data 179' const result180 = transformText('value180', { uppercase: true }) const data181 = 'some data 181' const data182 = 'some data 182' const data183 = 'some data 183' const data184 = 'some data 184' const data185 = 'some data 185' const data186 = 'some data 186' const data187 = 'some data 187' const data188 = 'some data 188' const data189 = 'some data 189' const data190 = 'some data 190' const data191 = 'some data 191' const data192 = 'some data 192' const data193 = 'some data 193'" `) expect(patched.patch).toMatchInlineSnapshot(` "◼︎=2-4|function transformText(item: string, options: { uppercase: boolean } = { uppercase: false }): string { return options.uppercase ? item.toUpperCase() : item } ◼︎=7|const result0 = transformText('value0', { uppercase: false }) ◼︎=27|const result20 = transformText('value20', { uppercase: false }) ◼︎=37|console.log(transformText('test30', { uppercase: false })) ◼︎=47|const result40 = transformText('value40', { uppercase: false }) ◼︎=67|const result60 = transformText('value60', { uppercase: false }) ◼︎=87|const result80 = transformText('value80', { uppercase: false }) ◼︎=97|console.log(transformText('test90', { uppercase: false })) ◼︎=107|const result100 = transformText('value100', { uppercase: false }) ◼︎=127|const result120 = transformText('value120', { uppercase: true }) ◼︎=147|const result140 = transformText('value140', { uppercase: true }) ◼︎=157|console.log(transformText('test150', { uppercase: true })) ◼︎=167|const result160 = transformText('value160', { uppercase: true }) ◼︎=187|const result180 = transformText('value180', { uppercase: true })" `) }) it('handles large file with chunking enabled (single file > maxTokensPerChunk)', async () => { // Create a very large file (2000 lines) const lines: string[] = [] for (let i = 0; i < 2000; i++) { if (i % 100 === 0) { lines.push(` // TODO: section ${i / 100}`) } else { lines.push(` const variable${i} = ${i}`) } } const veryLargeFile: File = { path: 'src/giant.ts', name: 'giant.ts', content: `function giant() {\n${lines.join('\n')}\n}`, } const result = await zai .patch([veryLargeFile], 'replace all TODO comments with DONE comments', { maxTokensPerChunk: 5000, // Force chunking }) .result() expect(result.output).toHaveLength(1) const patched = result.output[0] // Verify chunking occurred - multiple LLM calls were made expect(result.usage.requests.requests).toBeGreaterThan(1) console.log(`Chunking test: ${result.usage.requests.requests} LLM calls made`) // All TODO should be replaced with DONE expect(patched.content).not.toContain('TODO') expect(patched.content).toContain('DONE') // Should still have the function structure expect(patched.content).toContain('function giant()') // Count DONE occurrences const doneCount = (patched.content.match(/DONE/g) || []).length expect(doneCount).toBe(20) expect(patched.patch).toContain('DONE') expect(patched.patch).not.toBe('') }) it('handles multiple files with chunking (total files > maxTokensPerChunk)', async () => { // Create 5 moderately sized files const files: File[] = [] for (let fileIdx = 0; fileIdx < 5; fileIdx++) { const lines: string[] = [] for (let i = 0; i < 300; i++) { if (i % 50 === 0) { lines.push(` // TODO: feature ${fileIdx}-${i / 50}`) } else { lines.push(` const var${i} = ${i}`) } } files.push({ path: `src/file${fileIdx}.ts`, name: `file${fileIdx}.ts`, content: `function file${fileIdx}() {\n${lines.join('\n')}\n}`, }) } const result = await zai .patch(files, 'replace all TODO comments with COMPLETED comments', { maxTokensPerChunk: 8000, // Force batching }) .result() expect(result.output).toHaveLength(5) // Verify batching occurred - multiple LLM calls were made expect(result.usage.requests.requests).toBeGreaterThan(1) console.log(`Batching test: ${result.usage.requests.requests} LLM calls made`) // Check that all files were processed for (let i = 0; i < 5; i++) { const patched = result.output.find((f) => f.name === `file${i}.ts`) expect(patched).toBeDefined() expect(patched?.content).not.toContain('TODO') expect(patched?.content).toContain('COMPLETED') // Each file should have 6 COMPLETED comments const completedCount = (patched?.content.match(/COMPLETED/g) || []).length expect(completedCount).toBe(6) } }) }) describe.sequential('zai.learn.patch', { timeout: 60_000 }, () => { const client = getClient() let tableName = 'ZaiTestPatchInternalTable' let taskId = 'patch' let zai = getZai() beforeEach(async () => { zai = getZai().with({ activeLearning: { enable: true, taskId, tableName, }, }) }) afterEach(async () => { try { await client.deleteTableRows({ table: tableName, deleteAllRows: true }) } catch (err) {} }) afterAll(async () => { try { await client.deleteTable({ table: tableName }) } catch (err) {} }) it('learns patch patterns from examples', async () => { const adapter = new TableAdapter({ client, tableName, }) const testFile: File = { path: 'component.tsx', name: 'component.tsx', content: 'export const Component = () =>
Test
', } // First call - learns the pattern const first = await zai.learn(taskId).patch([testFile], 'add TypeScript FC type').result() let rows = await client.findTableRows({ table: tableName }) expect(rows.rows.length).toBeGreaterThanOrEqual(1) // Save an approved example with a specific pattern await adapter.saveExample({ key: 'example1', taskId: `zai/${taskId}`, taskType: 'zai.patch', instructions: 'add TypeScript FC type', input: [ { path: 'test.tsx', name: 'test.tsx', content: 'export const MyComponent = () =>
Hello
', }, ], output: [ { path: 'test.tsx', name: 'test.tsx', content: 'import { FC } from "react"\n\nexport const MyComponent: FC = () =>
Hello
', }, ], metadata, status: 'approved', }) // Second call - should use the learned pattern const second = await zai.learn(taskId).patch([testFile], 'add TypeScript FC type').result() rows = await client.findTableRows({ table: tableName }) expect(rows.rows.length).toBe(2) expect(second.output[0].content).toContain('FC') expect(second.output[0].content).toContain('react') expect(second.output[0].patch).toMatchInlineSnapshot(` "◼︎<1|import React from 'react'; ◼︎=1|export const Component: React.FC = () =>
Test
" `) }) }) describe('zai.patch — JSON file operations', { timeout: 60_000 }, () => { const zai = getZai() it('changes a value inside a simple JSON object', async () => { const file: File = { path: 'config.json', name: 'config.json', content: JSON.stringify({ name: 'my-app', version: '1.0.0', debug: false }, null, 2), } const { output } = await zai.patch([file], 'change the version to "2.0.0"').result() expect(output).toHaveLength(1) expect(output[0].name).toBe('config.json') expect(output[0].path).toBe('config.json') expect(output[0].content).toMatch(JSON.stringify({ name: 'my-app', version: '2.0.0', debug: false }, null, 2)) }) it('changes multiple properties inside a simple JSON object', async () => { const file: File = { path: 'config.json', name: 'config.json', content: JSON.stringify({ name: 'my-app', version: '1.0.0', debug: false, port: 3000 }, null, 2), } const result = await zai .patch([file], 'make the name "super-app", debug = true, change port should be 8080') .result() expect(result.output).toHaveLength(1) const parsed = JSON.parse(result.output[0].content) expect(parsed.name).toBe('super-app') expect(parsed.debug).toBe(true) expect(parsed.port).toBe(8080) expect(parsed.version).toBe('1.0.0') }) it('changes multiple properties inside a complex JSON object', async () => { const file: File = { path: 'config.json', name: 'config.json', content: JSON.stringify( { app: { name: 'my-app', version: '1.0.0' }, server: { host: 'localhost', port: 3000, ssl: { enabled: false, cert: '' } }, logging: { level: 'info', format: 'text' }, }, null, 2 ), } const result = await zai .patch( [file], 'change app.name to "production-app", server.port to 443, server.ssl.enabled to true, and logging.level to "warn"' ) .result() expect(result.output).toHaveLength(1) const parsed = JSON.parse(result.output[0].content) expect(parsed.app.name).toBe('production-app') expect(parsed.app.version).toBe('1.0.0') expect(parsed.server.port).toBe(443) expect(parsed.server.ssl.enabled).toBe(true) expect(parsed.server.host).toBe('localhost') expect(parsed.logging.level).toBe('warn') expect(parsed.logging.format).toBe('text') }) it('adds additional properties in a simple JSON', async () => { const file: File = { path: 'config.json', name: 'config.json', content: JSON.stringify({ name: 'my-app', version: '1.0.0' }, null, 2), } const result = await zai .patch([file], 'add "description": "A sample application" and "license": "MIT" to the JSON') .result() expect(result.output).toHaveLength(1) const parsed = JSON.parse(result.output[0].content) expect(parsed.name).toBe('my-app') expect(parsed.version).toBe('1.0.0') expect(parsed.description).toBe('A sample application') expect(parsed.license).toBe('MIT') }) it('adds additional properties in a complex JSON', async () => { const file: File = { path: 'config.json', name: 'config.json', content: JSON.stringify( { database: { host: 'localhost', port: 5432 }, cache: { enabled: true }, }, null, 2 ), } const result = await zai .patch( [file], 'add "name": "mydb" and "ssl": true inside the database object, and add "ttl": 3600 inside the cache object' ) .result() expect(result.output).toHaveLength(1) const parsed = JSON.parse(result.output[0].content) expect(parsed.database.host).toBe('localhost') expect(parsed.database.port).toBe(5432) expect(parsed.database.name).toBe('mydb') expect(parsed.database.ssl).toBe(true) expect(parsed.cache.enabled).toBe(true) expect(parsed.cache.ttl).toBe(3600) }) it('removes a property from a simple JSON', async () => { const file: File = { path: 'config.json', name: 'config.json', content: JSON.stringify({ name: 'my-app', version: '1.0.0', deprecated: true, debug: false }, null, 2), } const result = await zai.patch([file], 'remove the "deprecated" property').result() expect(result.output).toHaveLength(1) const parsed = JSON.parse(result.output[0].content) expect(parsed.name).toBe('my-app') expect(parsed.version).toBe('1.0.0') expect(parsed.debug).toBe(false) expect(parsed).not.toHaveProperty('deprecated') }) it('removes multiple properties from a simple JSON', async () => { const file: File = { path: 'config.json', name: 'config.json', content: JSON.stringify( { name: 'my-app', version: '1.0.0', deprecated: true, temporary: 'value', debug: false, legacy: true }, null, 2 ), } const result = await zai.patch([file], 'remove the "deprecated", "temporary", and "legacy" properties').result() expect(result.output).toHaveLength(1) const parsed = JSON.parse(result.output[0].content) expect(parsed.name).toBe('my-app') expect(parsed.version).toBe('1.0.0') expect(parsed.debug).toBe(false) expect(parsed).not.toHaveProperty('deprecated') expect(parsed).not.toHaveProperty('temporary') expect(parsed).not.toHaveProperty('legacy') }) it('removes items inside a JSON array inside a JSON object', async () => { const file: File = { path: 'config.json', name: 'config.json', content: JSON.stringify( { name: 'my-app', contributors: [ { name: 'Alice', role: 'lead' }, { name: 'Bob', role: 'dev' }, { name: 'Charlie', role: 'intern' }, { name: 'Diana', role: 'dev' }, ], }, null, 2 ), } const result = await zai.patch([file], 'remove Bob and Charlie from the contributors array').result() expect(result.output).toHaveLength(1) const parsed = JSON.parse(result.output[0].content) expect(parsed.name).toBe('my-app') expect(parsed.contributors).toHaveLength(2) expect(parsed.contributors.map((c: any) => c.name)).toEqual(['Alice', 'Diana']) }) it('adds, removes, and changes properties inside a simple JSON', async () => { const file: File = { path: 'package.json', name: 'package.json', content: JSON.stringify({ name: 'old-name', version: '0.1.0', private: true, deprecated: true }, null, 2), } const result = await zai .patch( [file], 'change "name" to "new-name", change "version" to "1.0.0", remove the "deprecated" property, and add "license": "MIT"' ) .result() expect(result.output).toHaveLength(1) const parsed = JSON.parse(result.output[0].content) expect(parsed.name).toBe('new-name') expect(parsed.version).toBe('1.0.0') expect(parsed.private).toBe(true) expect(parsed.license).toBe('MIT') expect(parsed).not.toHaveProperty('deprecated') }) it('adds, removes, and changes properties inside a complex JSON', async () => { const file: File = { path: 'config.json', name: 'config.json', content: JSON.stringify( { app: { name: 'old-app', version: '0.1.0', experimental: true }, database: { host: 'localhost', port: 5432, pool: { min: 2, max: 10, idleTimeout: 30000 } }, features: { darkMode: false, betaFeatures: true, analytics: { enabled: false, provider: 'none' } }, }, null, 2 ), } const result = await zai .patch( [file], 'change app.name to "new-app" and app.version to "1.0.0", remove app.experimental, change database.pool.max to 20, remove features.betaFeatures, change features.analytics.enabled to true and features.analytics.provider to "mixpanel", and add features.darkMode as true and database.pool.timeout as 5000' ) .result() expect(result.output).toHaveLength(1) const parsed = JSON.parse(result.output[0].content) // Changed expect(parsed.app.name).toBe('new-app') expect(parsed.app.version).toBe('1.0.0') expect(parsed.database.pool.max).toBe(20) expect(parsed.features.analytics.enabled).toBe(true) expect(parsed.features.analytics.provider).toBe('mixpanel') expect(parsed.features.darkMode).toBe(true) // Removed expect(parsed.app).not.toHaveProperty('experimental') expect(parsed.features).not.toHaveProperty('betaFeatures') // Added expect(parsed.database.pool.timeout).toBe(5000) // Unchanged expect(parsed.database.host).toBe('localhost') expect(parsed.database.port).toBe(5432) expect(parsed.database.pool.min).toBe(2) expect(parsed.database.pool.idleTimeout).toBe(30000) }) it('restructures a realistic package.json with many simultaneous changes', async () => { const file: File = { path: 'package.json', name: 'package.json', content: JSON.stringify( { name: '@acme/api-server', version: '2.4.1', description: 'Acme API Server', main: 'dist/index.js', scripts: { build: 'tsc', dev: 'ts-node src/index.ts', test: 'jest', lint: 'eslint src/', 'db:migrate': 'knex migrate:latest', 'db:seed': 'knex seed:run', }, dependencies: { express: '^4.18.2', knex: '^3.1.0', pg: '^8.11.3', zod: '^3.22.4', dotenv: '^16.3.1', cors: '^2.8.5', helmet: '^7.1.0', jsonwebtoken: '^9.0.2', bcrypt: '^5.1.1', }, devDependencies: { typescript: '^5.3.3', '@types/node': '^20.10.0', '@types/express': '^4.17.21', jest: '^29.7.0', 'ts-jest': '^29.1.1', eslint: '^8.55.0', '@types/cors': '^2.8.17', '@types/jsonwebtoken': '^9.0.5', '@types/bcrypt': '^5.0.2', }, engines: { node: '>=18.0.0' }, repository: { type: 'git', url: 'https://github.com/acme/api-server.git', }, author: 'Acme Corp', license: 'UNLICENSED', private: true, }, null, 2 ), } const result = await zai .patch( [file], [ 'Migrate from Express+Jest+ESLint to Hono+Vitest+Biome:', '- change scripts.build to "bun build src/index.ts --outdir dist"', '- change scripts.dev to "bun --hot src/index.ts"', '- change scripts.test to "vitest"', '- change scripts.lint to "biome check src/"', '- remove express, cors, helmet, dotenv from dependencies', '- add hono "^4.0.0" to dependencies', '- remove all @types/* packages, jest, ts-jest, eslint from devDependencies', '- add vitest "^1.2.0" and @biomejs/biome "^1.5.0" to devDependencies', '- bump the version to "3.0.0"', '- change engines.node to ">=20.0.0"', '- change license to "MIT"', '- remove the private field', '- add a "type": "module" field', ].join('\n') ) .result() expect(result.output).toHaveLength(1) const parsed = JSON.parse(result.output[0].content) // Version & metadata expect(parsed.version).toBe('3.0.0') expect(parsed.license).toBe('MIT') expect(parsed).not.toHaveProperty('private') expect(parsed.type).toBe('module') expect(parsed.engines.node).toBe('>=20.0.0') // Scripts expect(parsed.scripts.test).toBe('vitest') expect(parsed.scripts.lint).toBe('biome check src/') expect(parsed.scripts.dev).toContain('bun') expect(parsed.scripts.build).toContain('bun') // Dependencies — removed expect(parsed.dependencies).not.toHaveProperty('express') expect(parsed.dependencies).not.toHaveProperty('cors') expect(parsed.dependencies).not.toHaveProperty('helmet') expect(parsed.dependencies).not.toHaveProperty('dotenv') // Dependencies — added expect(parsed.dependencies.hono).toBeDefined() // Dependencies — kept expect(parsed.dependencies.knex).toBeDefined() expect(parsed.dependencies.pg).toBeDefined() expect(parsed.dependencies.zod).toBeDefined() expect(parsed.dependencies.jsonwebtoken).toBeDefined() expect(parsed.dependencies.bcrypt).toBeDefined() // DevDependencies — removed expect(parsed.devDependencies).not.toHaveProperty('jest') expect(parsed.devDependencies).not.toHaveProperty('ts-jest') expect(parsed.devDependencies).not.toHaveProperty('eslint') expect(parsed.devDependencies).not.toHaveProperty('@types/node') expect(parsed.devDependencies).not.toHaveProperty('@types/express') expect(parsed.devDependencies).not.toHaveProperty('@types/cors') expect(parsed.devDependencies).not.toHaveProperty('@types/jsonwebtoken') expect(parsed.devDependencies).not.toHaveProperty('@types/bcrypt') // DevDependencies — added expect(parsed.devDependencies.vitest).toBeDefined() expect(parsed.devDependencies['@biomejs/biome']).toBeDefined() // DevDependencies — kept expect(parsed.devDependencies.typescript).toBeDefined() // Unchanged expect(parsed.name).toBe('@acme/api-server') expect(parsed.description).toBe('Acme API Server') expect(parsed.scripts['db:migrate']).toBe('knex migrate:latest') expect(parsed.scripts['db:seed']).toBe('knex seed:run') expect(parsed.repository.url).toBe('https://github.com/acme/api-server.git') expect(parsed.author).toBe('Acme Corp') }) it('restructures deep nested objects and arrays in a CI/CD pipeline config', async () => { const file: File = { path: 'pipeline.json', name: 'pipeline.json', content: JSON.stringify( { pipeline: { name: 'deploy-prod', version: 1, triggers: [ { type: 'push', branches: ['main', 'release/*'] }, { type: 'schedule', cron: '0 2 * * 1' }, { type: 'manual', approvers: ['alice', 'bob', 'charlie'] }, ], stages: [ { name: 'build', timeout: 600, steps: [ { run: 'npm ci', cache: true }, { run: 'npm run build', artifacts: ['dist/**'] }, { run: 'npm test', retry: 2 }, ], }, { name: 'security', timeout: 300, steps: [ { run: 'npm audit', allowFailure: true }, { run: 'snyk test', env: { SNYK_TOKEN: '$SNYK_TOKEN' } }, ], }, { name: 'deploy', timeout: 900, dependsOn: ['build', 'security'], steps: [ { run: 'docker build -t app:latest .' }, { run: 'docker push app:latest' }, { run: 'kubectl apply -f k8s/', retry: 3 }, ], }, ], notifications: { onSuccess: [{ type: 'slack', channel: '#deploys' }], onFailure: [ { type: 'slack', channel: '#deploys' }, { type: 'email', recipients: ['oncall@acme.com'] }, { type: 'pagerduty', severity: 'high' }, ], }, }, }, null, 2 ), } const result = await zai .patch( [file], [ 'Make the following changes to the pipeline config:', '- bump pipeline.version to 2', '- remove the schedule trigger from the triggers array', '- remove "charlie" from the manual trigger approvers and add "diana"', '- remove the entire security stage from stages', '- in the deploy stage: remove "security" from dependsOn (keep "build"), and change retry on the kubectl step to 5', '- in notifications.onFailure: remove the pagerduty entry and add "platform@acme.com" to the email recipients', ].join('\n') ) .result() expect(result.output).toHaveLength(1) const parsed = JSON.parse(result.output[0].content) const p = parsed.pipeline // Version expect(p.version).toBe(2) // Triggers — schedule removed expect(p.triggers.find((t: any) => t.type === 'schedule')).toBeUndefined() // Triggers — push unchanged const pushTrigger = p.triggers.find((t: any) => t.type === 'push') expect(pushTrigger.branches).toContain('main') expect(pushTrigger.branches).toContain('release/*') // Triggers — manual approvers updated const manualTrigger = p.triggers.find((t: any) => t.type === 'manual') expect(manualTrigger.approvers).toContain('alice') expect(manualTrigger.approvers).toContain('bob') expect(manualTrigger.approvers).not.toContain('charlie') expect(manualTrigger.approvers).toContain('diana') // Stages — security removed expect(p.stages.find((s: any) => s.name === 'security')).toBeUndefined() // Stages — build unchanged const build = p.stages.find((s: any) => s.name === 'build') expect(build.timeout).toBe(600) expect(build.steps).toHaveLength(3) // Stages — deploy const deploy = p.stages.find((s: any) => s.name === 'deploy') expect(deploy.dependsOn).toContain('build') expect(deploy.dependsOn).not.toContain('security') const kubectlStep = deploy.steps.find((s: any) => s.run.includes('kubectl')) expect(kubectlStep.retry).toBe(5) // Notifications — pagerduty removed expect(p.notifications.onFailure.find((n: any) => n.type === 'pagerduty')).toBeUndefined() // Notifications — email recipients updated const emailNotif = p.notifications.onFailure.find((n: any) => n.type === 'email') expect(emailNotif.recipients).toContain('oncall@acme.com') expect(emailNotif.recipients).toContain('platform@acme.com') // Unchanged expect(p.name).toBe('deploy-prod') expect(p.notifications.onSuccess).toHaveLength(1) expect(p.notifications.onSuccess[0].channel).toBe('#deploys') }) it('validates patched JSON against a zui schema', async () => { const schema = z.object({ name: z.string(), version: z.string(), port: z.number().min(1).max(65535), debug: z.boolean(), }) const file: File = { path: 'config.json', name: 'config.json', content: JSON.stringify({ name: 'my-app', version: '1.0.0', port: 3000, debug: false }, null, 2), } const result = await zai.patch([file], 'change port to 8080 and set debug to true', { schema }).result() expect(result.output).toHaveLength(1) const parsed = JSON.parse(result.output[0].content) // Schema validation should pass const safe = schema.safeParse(parsed) expect(safe.success).toBe(true) expect(parsed.port).toBe(8080) expect(parsed.debug).toBe(true) expect(parsed.name).toBe('my-app') expect(parsed.version).toBe('1.0.0') }) it('rejects non-zui schemas', async () => { const schema = { _output: undefined, safeParse: () => ({ success: true, data: { version: '1.0.0' } }), } await expect( zai .patch([configFile], 'change port to 8080 and set debug to true', { schema: schema as any, }) .result() ).rejects.toThrow('@bpinternal/zui') }) it('validates patched JSON against a nested zui schema', async () => { const schema = z.object({ database: z.object({ host: z.string(), port: z.number().min(5434), credentials: z.object({ username: z.string(), password: z.string().min(1), }), }), features: z.array(z.string()), }) const file: File = { path: 'config.json', name: 'config.json', content: JSON.stringify( { database: { host: 'localhost', port: 5432, credentials: { username: 'admin', password: 'secret' }, }, features: ['auth', 'logging'], }, null, 2 ), } const result = await zai .patch( [file], 'change database host to "db.prod.internal", port to 5433 or higher, and add "caching" to features', { schema, } ) .result() expect(result.output).toHaveLength(1) const parsed = JSON.parse(result.output[0].content) const safe = schema.safeParse(parsed) expect(safe.success).toBe(true) expect(parsed.database.host).toBe('db.prod.internal') expect(parsed.database.port).toBe(5434) expect(parsed.database.credentials.username).toBe('admin') expect(parsed.database.credentials.password).toBe('secret') expect(parsed.features).toContain('auth') expect(parsed.features).toContain('logging') expect(parsed.features).toContain('caching') }) })