ReviewOS

also looking at this

stacks/buddy

fix(rules): apply a rule's autoMigrate, closing the rule-effects family

#1423
Merged glennmichael123 wants to merge fix/rule-automigrate into main
2 files +90 -1
test/upgrade-wiring.test.tsmodified+76-0
Changes to test/upgrade-wiring.test.ts
@@ -220,3 +220,79 @@ describe('report placement', () => {
220220 expect(appendUpgradeReport('## Tables', 'report')).toContain('report')
221221 })
222222})
223
224/**
225 * `autoMigrate` on a package rule was resolved into `ResolvedEffects` and
226 * aggregated by `mergeGroupEffects`, and nothing read either — only the
227 * repository-wide `ai.majorUpgrades.autoMigrate` ever reached the migration.
228 */
229describe('rule-level autoMigrate', () => {
230 /** A plan with a change, so attempting the migration is distinguishable. */
231 const WITH_CHANGES = {
232 changes: [{
233 description: 'the legacy import was removed',
234 action: 'import from the package root instead',
235 affectedFiles: [],
236 automatable: true,
237 }],
238 confidence: 'high',
239 effort: 2,
240 risks: [],
241 }
242
243 function configWith(rules: unknown[], globalAutoMigrate?: boolean): BuddyConfig {
244 return {
245 ai: {
246 majorUpgrades: {
247 enabled: true,
248 ...(globalAutoMigrate === undefined ? {} : { autoMigrate: globalAutoMigrate }),
249 },
250 },
251 packages: { strategy: 'all', rules },
252 } as unknown as BuddyConfig
253 }
254
255 /** Analysis alone is one completion; attempting the migration adds another. */
256 async function completions(config: BuddyConfig, update = makeUpdate()): Promise<number> {
257 const ai = scriptedClient(WITH_CHANGES)
258 await analyzeGroupMajors({ ...baseOptions(config, ai), updates: [update] })
259 return ai.requests.length
260 }
261
262 it('success case - a rule turns the migration on where the global is off', async () => {
263 expect(await completions(configWith([{ matchPackages: ['react'], autoMigrate: true }]))).toBe(2)
264 })
265
266 it('failure case - a rule turns it off where the global is on', async () => {
267 expect(await completions(configWith([{ matchPackages: ['react'], autoMigrate: false }], true))).toBe(1)
268 })
269
270 it('success case - the global still applies where no rule matches', async () => {
271 expect(await completions(configWith([{ matchPackages: ['lodash'], autoMigrate: true }], true))).toBe(2)
272 expect(await completions(configWith([{ matchPackages: ['lodash'], autoMigrate: true }], false))).toBe(1)
273 })
274
275 it('success case - each package decides for itself', async () => {
276 // The documented reading is that migrating what can be migrated leaves
277 // everything else exactly as it would have been — which is an argument
278 // for resolving per package rather than letting one opt-in carry a group.
279 const ai = scriptedClient(WITH_CHANGES)
280 await analyzeGroupMajors({
281 ...baseOptions(configWith([{ matchPackages: ['react'], autoMigrate: true }]), ai),
282 updates: [makeUpdate({ name: 'react' }), makeUpdate({ name: 'lodash' })],
283 })
284
285 // Two analyses, and one migration attempt — react's.
286 expect(ai.requests.length).toBe(3)
287 })
288
289 it('edge case - a rule saying nothing about migration changes nothing', async () => {
290 expect(await completions(configWith([{ matchPackages: ['react'], groupName: 'React' }], false))).toBe(1)
291 expect(await completions(configWith([{ matchPackages: ['react'], groupName: 'React' }], true))).toBe(2)
292 })
293
294 it('edge case - no rules leaves the global in charge', async () => {
295 expect(await completions(configWith([], true))).toBe(2)
296 expect(await completions(configWith([]))).toBe(1)
297 })
298})