|
| 1 | +import type { PackageUpdate } from '../src/types' |
| 2 | +import { describe, expect, it } from 'bun:test' |
| 3 | +import { updateDependencyFile } from '../src/utils/dependency-file-parser' |
| 4 | + |
| 5 | +describe('Regression Test - Dependency Constraint Preservation', () => { |
| 6 | + describe('zip/unzip cross-contamination bug', () => { |
| 7 | + it('should preserve correct constraints for zip and unzip packages', async () => { |
| 8 | + // This is the exact content from the stacks deps.yaml file |
| 9 | + const content = `# Please note, this file is auto-generated based on your ./config & local environment. |
| 10 | +# It's recommended to keep it in your project's root directory, and to commit it. |
| 11 | +# |
| 12 | +# The framework understands your project's dependencies, knows which ones are |
| 13 | +# already installed in your environment, and which aren't — prompting |
| 14 | +# to automatically install missing dependencies when needed. |
| 15 | +# |
| 16 | +# To learn more, please visit: |
| 17 | +# https://stacksjs.org/docs/dependency-management |
| 18 | +
|
| 19 | +dependencies: |
| 20 | + aws/cli: ^2.22.26 |
| 21 | + bun: ^1.2.13 |
| 22 | + gh: ^2.69.0 |
| 23 | + zip: ^3.0 |
| 24 | + unzip: ^6.0 |
| 25 | + sqlite3: ^3.47.2 |
| 26 | + node: ^22.12.0 # only temporarily needed until bun & vue-tsc issue is resolved |
| 27 | + # mailpit: ^1.21.8 |
| 28 | + # redis: ^7.4.1 |
| 29 | + # rust: ^1.74.1 |
| 30 | + # openjdk.org: ^21.0.3.6` |
| 31 | + |
| 32 | + // These are the actual updates that would be generated by dependency resolution |
| 33 | + const updates: PackageUpdate[] = [ |
| 34 | + { |
| 35 | + name: 'zip', |
| 36 | + currentVersion: '^3.0', |
| 37 | + newVersion: '3.0.0', |
| 38 | + updateType: 'patch', |
| 39 | + dependencyType: 'dependencies', |
| 40 | + file: 'deps.yaml', |
| 41 | + metadata: undefined, |
| 42 | + }, |
| 43 | + { |
| 44 | + name: 'unzip', |
| 45 | + currentVersion: '^6.0', |
| 46 | + newVersion: '6.0.0', |
| 47 | + updateType: 'patch', |
| 48 | + dependencyType: 'dependencies', |
| 49 | + file: 'deps.yaml', |
| 50 | + metadata: undefined, |
| 51 | + }, |
| 52 | + ] |
| 53 | + |
| 54 | + const result = await updateDependencyFile('deps.yaml', content, updates) |
| 55 | + |
| 56 | + // CRITICAL: These are the exact assertions that must pass to prevent regression |
| 57 | + expect(result).toContain('zip: ^3.0.0') // zip should be updated to ^3.0.0 |
| 58 | + expect(result).toContain('unzip: ^6.0.0') // unzip should be updated to ^6.0.0 (NOT ^3.0.0!) |
| 59 | + |
| 60 | + // Ensure no cross-contamination happened |
| 61 | + expect(result).not.toContain('unzip: ^3.0.0') // This was the bug - unzip getting zip's version |
| 62 | + expect(result).not.toMatch(/^\s*zip: \^6\.0\.0/m) // zip should not get unzip's version |
| 63 | + |
| 64 | + // Verify other packages remain unchanged |
| 65 | + expect(result).toContain('aws/cli: ^2.22.26') |
| 66 | + expect(result).toContain('bun: ^1.2.13') |
| 67 | + expect(result).toContain('gh: ^2.69.0') |
| 68 | + expect(result).toContain('sqlite3: ^3.47.2') |
| 69 | + expect(result).toContain('node: ^22.12.0') |
| 70 | + }) |
| 71 | + |
| 72 | + it('should handle mixed updates in real-world scenario', async () => { |
| 73 | + const content = `dependencies: |
| 74 | + aws/cli: ^2.22.26 |
| 75 | + bun: ^1.2.19 |
| 76 | + gh: ^2.76.1 |
| 77 | + zip: ^3.0 |
| 78 | + unzip: ^6.0 |
| 79 | + sqlite3: ^3.50.3 |
| 80 | + node: ^22.17.1` |
| 81 | + |
| 82 | + // Simulate what happens when multiple packages get updated at once |
| 83 | + const updates: PackageUpdate[] = [ |
| 84 | + { |
| 85 | + name: 'aws/cli', |
| 86 | + currentVersion: '^2.22.26', |
| 87 | + newVersion: '2.27.60', |
| 88 | + updateType: 'minor', |
| 89 | + dependencyType: 'dependencies', |
| 90 | + file: 'deps.yaml', |
| 91 | + metadata: undefined, |
| 92 | + }, |
| 93 | + { |
| 94 | + name: 'bun', |
| 95 | + currentVersion: '^1.2.19', |
| 96 | + newVersion: '1.2.19', |
| 97 | + updateType: 'patch', |
| 98 | + dependencyType: 'dependencies', |
| 99 | + file: 'deps.yaml', |
| 100 | + metadata: undefined, |
| 101 | + }, |
| 102 | + { |
| 103 | + name: 'zip', |
| 104 | + currentVersion: '^3.0', |
| 105 | + newVersion: '3.0.0', |
| 106 | + updateType: 'patch', |
| 107 | + dependencyType: 'dependencies', |
| 108 | + file: 'deps.yaml', |
| 109 | + metadata: undefined, |
| 110 | + }, |
| 111 | + { |
| 112 | + name: 'unzip', |
| 113 | + currentVersion: '^6.0', |
| 114 | + newVersion: '6.0.0', |
| 115 | + updateType: 'patch', |
| 116 | + dependencyType: 'dependencies', |
| 117 | + file: 'deps.yaml', |
| 118 | + metadata: undefined, |
| 119 | + }, |
| 120 | + ] |
| 121 | + |
| 122 | + const result = await updateDependencyFile('deps.yaml', content, updates) |
| 123 | + |
| 124 | + // This reproduces the exact scenario from the user's reported issue |
| 125 | + expect(result).toContain('aws/cli: ^2.27.60') |
| 126 | + expect(result).toContain('bun: ^1.2.19') |
| 127 | + expect(result).toContain('zip: ^3.0.0') |
| 128 | + expect(result).toContain('unzip: ^6.0.0') // MUST be ^6.0.0, not ^3.0.0! |
| 129 | + |
| 130 | + // Log the result for debugging if the test fails |
| 131 | + if (result.includes('unzip: ^3.0.0')) { |
| 132 | + console.log('REGRESSION: unzip got wrong version!') |
| 133 | + console.log('Full result:') |
| 134 | + console.log(result) |
| 135 | + } |
| 136 | + }) |
| 137 | + }) |
| 138 | + |
| 139 | + describe('similar package name edge cases', () => { |
| 140 | + it('should handle packages that are substrings of each other', async () => { |
| 141 | + const content = `dependencies: |
| 142 | + react: ^18.0.0 |
| 143 | + react-dom: ^18.0.0 |
| 144 | + react-router: ^6.0.0 |
| 145 | + react-router-dom: ^6.0.0` |
| 146 | + |
| 147 | + const updates: PackageUpdate[] = [ |
| 148 | + { |
| 149 | + name: 'react', |
| 150 | + currentVersion: '^18.0.0', |
| 151 | + newVersion: '18.2.0', |
| 152 | + updateType: 'minor', |
| 153 | + dependencyType: 'dependencies', |
| 154 | + file: 'deps.yaml', |
| 155 | + metadata: undefined, |
| 156 | + }, |
| 157 | + ] |
| 158 | + |
| 159 | + const result = await updateDependencyFile('deps.yaml', content, updates) |
| 160 | + |
| 161 | + expect(result).toContain('react: ^18.2.0') |
| 162 | + expect(result).toContain('react-dom: ^18.0.0') |
| 163 | + expect(result).toContain('react-router: ^6.0.0') |
| 164 | + expect(result).toContain('react-router-dom: ^6.0.0') |
| 165 | + }) |
| 166 | + |
| 167 | + it('should handle packages with special characters in YAML format', async () => { |
| 168 | + const content = `dependencies: |
| 169 | + "@types/node": ^20.0.0 |
| 170 | + "@types/node-fetch": ^2.0.0 |
| 171 | + node: ^20.0.0` |
| 172 | + |
| 173 | + const updates: PackageUpdate[] = [ |
| 174 | + { |
| 175 | + name: 'node', |
| 176 | + currentVersion: '^20.0.0', |
| 177 | + newVersion: '20.5.0', |
| 178 | + updateType: 'minor', |
| 179 | + dependencyType: 'dependencies', |
| 180 | + file: 'deps.yaml', |
| 181 | + metadata: undefined, |
| 182 | + }, |
| 183 | + ] |
| 184 | + |
| 185 | + const result = await updateDependencyFile('deps.yaml', content, updates) |
| 186 | + |
| 187 | + // Should only update unquoted 'node', not '@types/node' or '@types/node-fetch' |
| 188 | + expect(result).toContain('node: ^20.5.0') |
| 189 | + expect(result).toContain('"@types/node": ^20.0.0') |
| 190 | + expect(result).toContain('"@types/node-fetch": ^2.0.0') |
| 191 | + }) |
| 192 | + }) |
| 193 | +}) |
0 commit comments