From 6039252a7f3e7ddbcfaa61af0640547203810a11 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 22 Dec 2025 22:30:55 +0000 Subject: [PATCH 01/10] Initial plan From dbdc3a711039fa5a1d12cb956b3278b42a1e3945 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 22 Dec 2025 22:40:17 +0000 Subject: [PATCH 02/10] Improve asymmetric matcher diff display - Modified `replaceAsymmetricMatcher` to unwrap container matchers (ArrayContaining, ObjectContaining) - When matchers match, replace expected with actual value to show uniform structure - When matchers don't match, unwrap container matchers but keep simple matchers for clarity - Added test cases to demonstrate improved diff output Co-authored-by: sheremet-va <16173870+sheremet-va@users.noreply.github.com> --- packages/utils/src/diff/index.ts | 55 +++++++++++++- test/core/test/asymmetric-diff.test.ts | 101 +++++++++++++++++++++++++ 2 files changed, 152 insertions(+), 4 deletions(-) create mode 100644 test/core/test/asymmetric-diff.test.ts diff --git a/packages/utils/src/diff/index.ts b/packages/utils/src/diff/index.ts index 09f37d0e4db2..42bbafc6af5e 100644 --- a/packages/utils/src/diff/index.ts +++ b/packages/utils/src/diff/index.ts @@ -336,13 +336,60 @@ export function replaceAsymmetricMatcher( const expectedValue = expected[key] const actualValue = actual[key] if (isAsymmetricMatcher(expectedValue)) { - if (expectedValue.asymmetricMatch(actualValue)) { - actual[key] = expectedValue + const matches = expectedValue.asymmetricMatch(actualValue) + // For container matchers (ArrayContaining, ObjectContaining), unwrap and recursively process + if (expectedValue.sample !== undefined && isReplaceable(actualValue, expectedValue.sample)) { + if (matches) { + // Matcher matches: unwrap and recursively process to show actual structure + const replaced = replaceAsymmetricMatcher( + actualValue, + expectedValue.sample, + actualReplaced, + expectedReplaced, + ) + actual[key] = replaced.replacedActual + expected[key] = replaced.replacedExpected + } + else { + // Matcher doesn't match: unwrap but keep structure to show mismatch + const replaced = replaceAsymmetricMatcher( + actualValue, + expectedValue.sample, + actualReplaced, + expectedReplaced, + ) + actual[key] = replaced.replacedActual + expected[key] = replaced.replacedExpected + } + } + else { + // Simple matchers (StringContaining, Any, etc.) + if (matches) { + // When matcher matches, replace expected with actual value + // so they appear the same in the diff + expected[key] = actualValue + } + // When matcher doesn't match, keep both as-is to show the difference } } else if (isAsymmetricMatcher(actualValue)) { - if (actualValue.asymmetricMatch(expectedValue)) { - expected[key] = actualValue + const matches = actualValue.asymmetricMatch(expectedValue) + // For container matchers in actual (rare case) + if (actualValue.sample !== undefined && isReplaceable(actualValue.sample, expectedValue)) { + const replaced = replaceAsymmetricMatcher( + actualValue.sample, + expectedValue, + actualReplaced, + expectedReplaced, + ) + actual[key] = replaced.replacedActual + expected[key] = replaced.replacedExpected + } + else { + if (matches) { + // When matcher matches, replace actual with expected value + actual[key] = expectedValue + } } } else if (isReplaceable(actualValue, expectedValue)) { diff --git a/test/core/test/asymmetric-diff.test.ts b/test/core/test/asymmetric-diff.test.ts new file mode 100644 index 000000000000..6ae9e13f1584 --- /dev/null +++ b/test/core/test/asymmetric-diff.test.ts @@ -0,0 +1,101 @@ +import { describe, expect, it } from 'vitest' + +describe('asymmetric matcher diff display', () => { + it('shows clear diff when simple property mismatch', () => { + const actual = { + user: { + name: 'John', + age: 25, + email: 'john@example.com', + }, + } + + // Test should fail - name doesn't contain "Jane" + try { + expect(actual).toMatchObject({ + user: expect.objectContaining({ + name: expect.stringContaining('Jane'), + age: expect.any(Number), + email: expect.stringContaining('example.com'), + }), + }) + } + catch (error: any) { + console.log('Mismatch with objectContaining:') + console.log(error.message) + throw error + } + }) + + it('shows clear diff with nested objectContaining - complex case', () => { + // Actual data structure similar to the issue example + const actual = { + model: 'veo-3.1-generate-preview', + instances: [ + { + prompt: 'walk', // This doesn't match the expected regex + referenceImages: [ + { + image: { + gcsUri: 'gs://example/person1.jpg', + mimeType: 'image/png', // Mismatch: expected jpeg + }, + referenceType: 'asset', + }, + { + image: { + gcsUri: 'gs://example/person.jpg', // Mismatch: doesn't contain "person2.png" + mimeType: 'image/png', + }, + referenceType: 'asset', + }, + ], + }, + ], + parameters: { + durationSeconds: '8', // Mismatch: string instead of number + aspectRatio: '16:9', + generateAudio: true, + }, + } + + // This should fail with multiple mismatches + try { + expect(actual).toMatchObject({ + model: expect.stringMatching(/^veo-3\.1-(fast-)?generate-preview$/), + instances: expect.arrayContaining([ + expect.objectContaining({ + prompt: expect.stringMatching(/^(?=.*walking)(?=.*together)(?=.*park).*/i), + referenceImages: expect.arrayContaining([ + expect.objectContaining({ + image: expect.objectContaining({ + gcsUri: expect.stringContaining('person1.jpg'), + mimeType: 'image/jpeg', + }), + referenceType: expect.stringMatching(/^(asset|style)$/), + }), + expect.objectContaining({ + image: expect.objectContaining({ + gcsUri: expect.stringContaining('person2.png'), + mimeType: 'image/png', + }), + referenceType: expect.stringMatching(/^(asset|style)$/), + }), + ]), + }), + ]), + parameters: expect.objectContaining({ + durationSeconds: expect.any(Number), + aspectRatio: '16:9', + generateAudio: expect.any(Boolean), + }), + }) + } + catch (error: any) { + console.log('Complex nested structure diff:') + console.log(error.message) + throw error + } + }) +}) + From 27316297d4045e418d53fd2dd37ae38313c68744 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 22 Dec 2025 22:44:33 +0000 Subject: [PATCH 03/10] Update tests and fix linting issues - Updated snapshots in jest-expect.test.ts and expect.test.ts to reflect improved diff output - Removed console.log statements from asymmetric-diff.test.ts - Changed to use expect().toThrowError() pattern instead of try-catch - All tests passing, linting clean, typecheck passing Co-authored-by: sheremet-va <16173870+sheremet-va@users.noreply.github.com> --- test/core/test/asymmetric-diff.test.ts | 27 ++++++++------------------ test/core/test/expect.test.ts | 2 +- test/core/test/jest-expect.test.ts | 2 +- 3 files changed, 10 insertions(+), 21 deletions(-) diff --git a/test/core/test/asymmetric-diff.test.ts b/test/core/test/asymmetric-diff.test.ts index 6ae9e13f1584..07ea50159d2d 100644 --- a/test/core/test/asymmetric-diff.test.ts +++ b/test/core/test/asymmetric-diff.test.ts @@ -11,7 +11,7 @@ describe('asymmetric matcher diff display', () => { } // Test should fail - name doesn't contain "Jane" - try { + expect(() => { expect(actual).toMatchObject({ user: expect.objectContaining({ name: expect.stringContaining('Jane'), @@ -19,12 +19,7 @@ describe('asymmetric matcher diff display', () => { email: expect.stringContaining('example.com'), }), }) - } - catch (error: any) { - console.log('Mismatch with objectContaining:') - console.log(error.message) - throw error - } + }).toThrowError() }) it('shows clear diff with nested objectContaining - complex case', () => { @@ -33,18 +28,18 @@ describe('asymmetric matcher diff display', () => { model: 'veo-3.1-generate-preview', instances: [ { - prompt: 'walk', // This doesn't match the expected regex + prompt: 'walk', // This doesn't match the expected regex referenceImages: [ { image: { gcsUri: 'gs://example/person1.jpg', - mimeType: 'image/png', // Mismatch: expected jpeg + mimeType: 'image/png', // Mismatch: expected jpeg }, referenceType: 'asset', }, { image: { - gcsUri: 'gs://example/person.jpg', // Mismatch: doesn't contain "person2.png" + gcsUri: 'gs://example/person.jpg', // Mismatch: doesn't contain "person2.png" mimeType: 'image/png', }, referenceType: 'asset', @@ -53,14 +48,14 @@ describe('asymmetric matcher diff display', () => { }, ], parameters: { - durationSeconds: '8', // Mismatch: string instead of number + durationSeconds: '8', // Mismatch: string instead of number aspectRatio: '16:9', generateAudio: true, }, } // This should fail with multiple mismatches - try { + expect(() => { expect(actual).toMatchObject({ model: expect.stringMatching(/^veo-3\.1-(fast-)?generate-preview$/), instances: expect.arrayContaining([ @@ -90,12 +85,6 @@ describe('asymmetric matcher diff display', () => { generateAudio: expect.any(Boolean), }), }) - } - catch (error: any) { - console.log('Complex nested structure diff:') - console.log(error.message) - throw error - } + }).toThrowError() }) }) - diff --git a/test/core/test/expect.test.ts b/test/core/test/expect.test.ts index 7abb03cd00bb..4d4b1e92abe5 100644 --- a/test/core/test/expect.test.ts +++ b/test/core/test/expect.test.ts @@ -630,7 +630,7 @@ describe('Standard Schema', () => { - ], - }, + "age": "thirty", - "name": SchemaMatching, + "name": "John", }, }" `) diff --git a/test/core/test/jest-expect.test.ts b/test/core/test/jest-expect.test.ts index 8e3a2c9d9856..cee708112ac4 100644 --- a/test/core/test/jest-expect.test.ts +++ b/test/core/test/jest-expect.test.ts @@ -1318,7 +1318,7 @@ it('correctly prints diff with asymmetric matchers', () => { + Received { - "a": Any, + "a": 1, - "b": Any, + "b": "string", }" From e27de875259e4b1a48cfa8495785487975d811dc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 22 Dec 2025 22:47:09 +0000 Subject: [PATCH 04/10] Add safety check for sample property access - Added 'in' operator check before accessing sample property on asymmetric matchers - Prevents potential runtime errors if sample property doesn't exist - Addresses code review feedback Co-authored-by: sheremet-va <16173870+sheremet-va@users.noreply.github.com> --- packages/utils/src/diff/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/utils/src/diff/index.ts b/packages/utils/src/diff/index.ts index 42bbafc6af5e..b345871c4d1d 100644 --- a/packages/utils/src/diff/index.ts +++ b/packages/utils/src/diff/index.ts @@ -338,7 +338,7 @@ export function replaceAsymmetricMatcher( if (isAsymmetricMatcher(expectedValue)) { const matches = expectedValue.asymmetricMatch(actualValue) // For container matchers (ArrayContaining, ObjectContaining), unwrap and recursively process - if (expectedValue.sample !== undefined && isReplaceable(actualValue, expectedValue.sample)) { + if ('sample' in expectedValue && expectedValue.sample !== undefined && isReplaceable(actualValue, expectedValue.sample)) { if (matches) { // Matcher matches: unwrap and recursively process to show actual structure const replaced = replaceAsymmetricMatcher( @@ -375,7 +375,7 @@ export function replaceAsymmetricMatcher( else if (isAsymmetricMatcher(actualValue)) { const matches = actualValue.asymmetricMatch(expectedValue) // For container matchers in actual (rare case) - if (actualValue.sample !== undefined && isReplaceable(actualValue.sample, expectedValue)) { + if ('sample' in actualValue && actualValue.sample !== undefined && isReplaceable(actualValue.sample, expectedValue)) { const replaced = replaceAsymmetricMatcher( actualValue.sample, expectedValue, From a091b45e19ba3f9d4f3c468bb26d5a2116f49370 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 22 Dec 2025 22:56:55 +0000 Subject: [PATCH 05/10] Improve test quality by validating error.diff with inline snapshots - Changed tests to use try-catch with expect.unreachable() pattern - Added imports for stripVTControlCharacters and processError - Validate actual diff output using toMatchInlineSnapshot() - Snapshots show the improved diff format with clear structure Co-authored-by: sheremet-va <16173870+sheremet-va@users.noreply.github.com> --- test/core/test/asymmetric-diff.test.ts | 69 ++++++++++++++++++++++++-- 1 file changed, 65 insertions(+), 4 deletions(-) diff --git a/test/core/test/asymmetric-diff.test.ts b/test/core/test/asymmetric-diff.test.ts index 07ea50159d2d..d4900cecebe2 100644 --- a/test/core/test/asymmetric-diff.test.ts +++ b/test/core/test/asymmetric-diff.test.ts @@ -1,3 +1,5 @@ +import { stripVTControlCharacters } from 'node:util' +import { processError } from '@vitest/utils/error' import { describe, expect, it } from 'vitest' describe('asymmetric matcher diff display', () => { @@ -11,7 +13,7 @@ describe('asymmetric matcher diff display', () => { } // Test should fail - name doesn't contain "Jane" - expect(() => { + try { expect(actual).toMatchObject({ user: expect.objectContaining({ name: expect.stringContaining('Jane'), @@ -19,7 +21,24 @@ describe('asymmetric matcher diff display', () => { email: expect.stringContaining('example.com'), }), }) - }).toThrowError() + expect.unreachable() + } + catch (err) { + const error = processError(err) + expect(stripVTControlCharacters(error.diff)).toMatchInlineSnapshot(` + "- Expected + + Received + + { + "user": { + "age": 25, + "email": "john@example.com", + - "name": StringContaining "Jane", + + "name": "John", + }, + }" + `) + } }) it('shows clear diff with nested objectContaining - complex case', () => { @@ -55,7 +74,7 @@ describe('asymmetric matcher diff display', () => { } // This should fail with multiple mismatches - expect(() => { + try { expect(actual).toMatchObject({ model: expect.stringMatching(/^veo-3\.1-(fast-)?generate-preview$/), instances: expect.arrayContaining([ @@ -85,6 +104,48 @@ describe('asymmetric matcher diff display', () => { generateAudio: expect.any(Boolean), }), }) - }).toThrowError() + expect.unreachable() + } + catch (err) { + const error = processError(err) + expect(stripVTControlCharacters(error.diff)).toMatchInlineSnapshot(` + "- Expected + + Received + + { + "instances": [ + { + - "prompt": StringMatching /^(?=.*walking)(?=.*together)(?=.*park).*/i, + + "prompt": "walk", + "referenceImages": [ + { + "image": { + "gcsUri": "gs://example/person1.jpg", + - "mimeType": "image/jpeg", + + "mimeType": "image/png", + }, + "referenceType": "asset", + }, + { + "image": { + - "gcsUri": StringContaining "person2.png", + + "gcsUri": "gs://example/person.jpg", + "mimeType": "image/png", + }, + "referenceType": "asset", + }, + ], + }, + ], + "model": "veo-3.1-generate-preview", + "parameters": { + "aspectRatio": "16:9", + - "durationSeconds": Any, + + "durationSeconds": "8", + "generateAudio": true, + }, + }" + `) + } }) }) From 2da042692cf8c491d039a4bb640e1813a5f147e8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 22 Dec 2025 23:18:32 +0000 Subject: [PATCH 06/10] Move asymmetric matcher tests to diff.test.ts and use existing helper - Removed asymmetric-diff.test.ts file - Moved tests to diff.test.ts using the existing getErrorDiff helper - Removed redundant getMatchObjectErrorDiff helper (toEqual works identically) - Updated existing test snapshots to reflect improved diff output - All tests passing Co-authored-by: sheremet-va <16173870+sheremet-va@users.noreply.github.com> --- test/core/test/asymmetric-diff.test.ts | 151 ------------------------- test/core/test/diff.test.ts | 147 ++++++++++++++++++++++-- 2 files changed, 140 insertions(+), 158 deletions(-) delete mode 100644 test/core/test/asymmetric-diff.test.ts diff --git a/test/core/test/asymmetric-diff.test.ts b/test/core/test/asymmetric-diff.test.ts deleted file mode 100644 index d4900cecebe2..000000000000 --- a/test/core/test/asymmetric-diff.test.ts +++ /dev/null @@ -1,151 +0,0 @@ -import { stripVTControlCharacters } from 'node:util' -import { processError } from '@vitest/utils/error' -import { describe, expect, it } from 'vitest' - -describe('asymmetric matcher diff display', () => { - it('shows clear diff when simple property mismatch', () => { - const actual = { - user: { - name: 'John', - age: 25, - email: 'john@example.com', - }, - } - - // Test should fail - name doesn't contain "Jane" - try { - expect(actual).toMatchObject({ - user: expect.objectContaining({ - name: expect.stringContaining('Jane'), - age: expect.any(Number), - email: expect.stringContaining('example.com'), - }), - }) - expect.unreachable() - } - catch (err) { - const error = processError(err) - expect(stripVTControlCharacters(error.diff)).toMatchInlineSnapshot(` - "- Expected - + Received - - { - "user": { - "age": 25, - "email": "john@example.com", - - "name": StringContaining "Jane", - + "name": "John", - }, - }" - `) - } - }) - - it('shows clear diff with nested objectContaining - complex case', () => { - // Actual data structure similar to the issue example - const actual = { - model: 'veo-3.1-generate-preview', - instances: [ - { - prompt: 'walk', // This doesn't match the expected regex - referenceImages: [ - { - image: { - gcsUri: 'gs://example/person1.jpg', - mimeType: 'image/png', // Mismatch: expected jpeg - }, - referenceType: 'asset', - }, - { - image: { - gcsUri: 'gs://example/person.jpg', // Mismatch: doesn't contain "person2.png" - mimeType: 'image/png', - }, - referenceType: 'asset', - }, - ], - }, - ], - parameters: { - durationSeconds: '8', // Mismatch: string instead of number - aspectRatio: '16:9', - generateAudio: true, - }, - } - - // This should fail with multiple mismatches - try { - expect(actual).toMatchObject({ - model: expect.stringMatching(/^veo-3\.1-(fast-)?generate-preview$/), - instances: expect.arrayContaining([ - expect.objectContaining({ - prompt: expect.stringMatching(/^(?=.*walking)(?=.*together)(?=.*park).*/i), - referenceImages: expect.arrayContaining([ - expect.objectContaining({ - image: expect.objectContaining({ - gcsUri: expect.stringContaining('person1.jpg'), - mimeType: 'image/jpeg', - }), - referenceType: expect.stringMatching(/^(asset|style)$/), - }), - expect.objectContaining({ - image: expect.objectContaining({ - gcsUri: expect.stringContaining('person2.png'), - mimeType: 'image/png', - }), - referenceType: expect.stringMatching(/^(asset|style)$/), - }), - ]), - }), - ]), - parameters: expect.objectContaining({ - durationSeconds: expect.any(Number), - aspectRatio: '16:9', - generateAudio: expect.any(Boolean), - }), - }) - expect.unreachable() - } - catch (err) { - const error = processError(err) - expect(stripVTControlCharacters(error.diff)).toMatchInlineSnapshot(` - "- Expected - + Received - - { - "instances": [ - { - - "prompt": StringMatching /^(?=.*walking)(?=.*together)(?=.*park).*/i, - + "prompt": "walk", - "referenceImages": [ - { - "image": { - "gcsUri": "gs://example/person1.jpg", - - "mimeType": "image/jpeg", - + "mimeType": "image/png", - }, - "referenceType": "asset", - }, - { - "image": { - - "gcsUri": StringContaining "person2.png", - + "gcsUri": "gs://example/person.jpg", - "mimeType": "image/png", - }, - "referenceType": "asset", - }, - ], - }, - ], - "model": "veo-3.1-generate-preview", - "parameters": { - "aspectRatio": "16:9", - - "durationSeconds": Any, - + "durationSeconds": "8", - "generateAudio": true, - }, - }" - `) - } - }) -}) diff --git a/test/core/test/diff.test.ts b/test/core/test/diff.test.ts index e037aa3d0acf..df74efc3bef0 100644 --- a/test/core/test/diff.test.ts +++ b/test/core/test/diff.test.ts @@ -142,7 +142,7 @@ test('asymmetric matcher in object', () => { { - "x": 1, + "x": 0, - "y": Anything, + "y": "foo", }" `) }) @@ -159,7 +159,7 @@ test('asymmetric matcher in object with truncated diff', () => { + Received { - "w": Anything, + "w": "foo", - "x": 1, + "x": 0, ... Diff result is truncated" @@ -174,7 +174,7 @@ test('asymmetric matcher in array', () => { [ - 1, + 0, - Anything, + "foo", ]" `) }) @@ -211,12 +211,12 @@ test('asymmetric matcher in nested', () => { { - "x": 1, + "x": 0, - "y": Anything, + "y": "foo", }, [ - 1, + 0, - Anything, + "bar", ], ]" `) @@ -237,8 +237,8 @@ test('asymmetric matcher in nested with truncated diff', () => { { - "x": 1, + "x": 0, - "y": Anything, - "z": Anything, + "y": "foo", + "z": "bar", ... Diff result is truncated" `) }) @@ -353,3 +353,136 @@ function getErrorDiff(actual: unknown, expected: unknown, options?: DiffOptions) } return expect.unreachable() } + +test('asymmetric matcher with objectContaining - simple case', () => { + const actual = { + user: { + name: 'John', + age: 25, + email: 'john@example.com', + }, + } + + const expected = { + user: expect.objectContaining({ + name: expect.stringContaining('Jane'), + age: expect.any(Number), + email: expect.stringContaining('example.com'), + }), + } + + expect(stripVTControlCharacters(getErrorDiff(actual, expected))).toMatchInlineSnapshot(` + "- Expected + + Received + + { + "user": { + "age": 25, + "email": "john@example.com", + - "name": StringContaining "Jane", + + "name": "John", + }, + }" + `) +}) + +test('asymmetric matcher with nested objectContaining and arrayContaining', () => { + const actual = { + model: 'veo-3.1-generate-preview', + instances: [ + { + prompt: 'walk', + referenceImages: [ + { + image: { + gcsUri: 'gs://example/person1.jpg', + mimeType: 'image/png', + }, + referenceType: 'asset', + }, + { + image: { + gcsUri: 'gs://example/person.jpg', + mimeType: 'image/png', + }, + referenceType: 'asset', + }, + ], + }, + ], + parameters: { + durationSeconds: '8', + aspectRatio: '16:9', + generateAudio: true, + }, + } + + const expected = { + model: expect.stringMatching(/^veo-3\.1-(fast-)?generate-preview$/), + instances: expect.arrayContaining([ + expect.objectContaining({ + prompt: expect.stringMatching(/^(?=.*walking)(?=.*together)(?=.*park).*/i), + referenceImages: expect.arrayContaining([ + expect.objectContaining({ + image: expect.objectContaining({ + gcsUri: expect.stringContaining('person1.jpg'), + mimeType: 'image/jpeg', + }), + referenceType: expect.stringMatching(/^(asset|style)$/), + }), + expect.objectContaining({ + image: expect.objectContaining({ + gcsUri: expect.stringContaining('person2.png'), + mimeType: 'image/png', + }), + referenceType: expect.stringMatching(/^(asset|style)$/), + }), + ]), + }), + ]), + parameters: expect.objectContaining({ + durationSeconds: expect.any(Number), + aspectRatio: '16:9', + generateAudio: expect.any(Boolean), + }), + } + + expect(stripVTControlCharacters(getErrorDiff(actual, expected))).toMatchInlineSnapshot(` + "- Expected + + Received + + { + "instances": [ + { + - "prompt": StringMatching /^(?=.*walking)(?=.*together)(?=.*park).*/i, + + "prompt": "walk", + "referenceImages": [ + { + "image": { + "gcsUri": "gs://example/person1.jpg", + - "mimeType": "image/jpeg", + + "mimeType": "image/png", + }, + "referenceType": "asset", + }, + { + "image": { + - "gcsUri": StringContaining "person2.png", + + "gcsUri": "gs://example/person.jpg", + "mimeType": "image/png", + }, + "referenceType": "asset", + }, + ], + }, + ], + "model": "veo-3.1-generate-preview", + "parameters": { + "aspectRatio": "16:9", + - "durationSeconds": Any, + + "durationSeconds": "8", + "generateAudio": true, + }, + }" + `) +}) From 30f32e00d3715d6c83356560fe357b298e87d3a2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 24 Dec 2025 08:21:38 +0000 Subject: [PATCH 07/10] Add test case demonstrating need for recursion in container matcher processing - Added test showing nested matchers within objectContaining - Demonstrates that recursion ensures nested matchers (Any, StringContaining) are processed - Without recursion, nested matchers wouldn't be replaced with actual values - Test shows matching nested matchers display actual values, non-matching show clear diff Co-authored-by: hi-ogawa <4232207+hi-ogawa@users.noreply.github.com> --- test/core/test/diff.test.ts | 47 +++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/test/core/test/diff.test.ts b/test/core/test/diff.test.ts index df74efc3bef0..a49795bb53b7 100644 --- a/test/core/test/diff.test.ts +++ b/test/core/test/diff.test.ts @@ -486,3 +486,50 @@ test('asymmetric matcher with nested objectContaining and arrayContaining', () = }" `) }) + +test('asymmetric matcher recursion - nested matchers in container', () => { + // This test demonstrates why recursion is needed when container matchers match. + // If we simply did `expected[key] = actualValue` without recursion, + // nested asymmetric matchers wouldn't be processed, and the diff would show + // the entire nested structure even when some nested matchers match. + const actual = { + data: { + id: 1, + name: 'John', + metadata: { + created: '2024-01-01', + updated: '2024-01-02', + }, + }, + } + + const expected = { + data: expect.objectContaining({ + id: expect.any(Number), // This nested matcher matches - should show actual value + name: expect.stringContaining('Jane'), // This nested matcher doesn't match - should show mismatch + metadata: expect.objectContaining({ + created: expect.any(String), // This nested matcher matches - should show actual value + updated: expect.any(String), // This nested matcher matches - should show actual value + }), + }), + } + + // With recursion, nested matchers are properly processed: + // - Matching matchers (id, created, updated) are replaced with actual values + // - Non-matching matchers (name) show the mismatch clearly + expect(stripVTControlCharacters(getErrorDiff(actual, expected))).toMatchInlineSnapshot(` + "- Expected + + Received + + @@ -3,8 +3,8 @@ + "id": 1, + "metadata": { + "created": "2024-01-01", + "updated": "2024-01-02", + }, + - "name": StringContaining "Jane", + + "name": "John", + }, + }" + `) +}) From 344f5c3f02e43659540180dab589ed6fca936d11 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 24 Dec 2025 08:34:17 +0000 Subject: [PATCH 08/10] Remove test that doesn't demonstrate recursion necessity Removed the test case that was intended to show why recursion is needed, as it doesn't actually fail when recursion is removed (as pointed out in code review). The recursion in lines 342-363 processes the .sample property of container matchers, but a proper test demonstrating its necessity would require showing different behavior, which the current test doesn't achieve. Co-authored-by: hi-ogawa <4232207+hi-ogawa@users.noreply.github.com> --- test/core/test/diff.test.ts | 47 ------------------------------------- 1 file changed, 47 deletions(-) diff --git a/test/core/test/diff.test.ts b/test/core/test/diff.test.ts index a49795bb53b7..df74efc3bef0 100644 --- a/test/core/test/diff.test.ts +++ b/test/core/test/diff.test.ts @@ -486,50 +486,3 @@ test('asymmetric matcher with nested objectContaining and arrayContaining', () = }" `) }) - -test('asymmetric matcher recursion - nested matchers in container', () => { - // This test demonstrates why recursion is needed when container matchers match. - // If we simply did `expected[key] = actualValue` without recursion, - // nested asymmetric matchers wouldn't be processed, and the diff would show - // the entire nested structure even when some nested matchers match. - const actual = { - data: { - id: 1, - name: 'John', - metadata: { - created: '2024-01-01', - updated: '2024-01-02', - }, - }, - } - - const expected = { - data: expect.objectContaining({ - id: expect.any(Number), // This nested matcher matches - should show actual value - name: expect.stringContaining('Jane'), // This nested matcher doesn't match - should show mismatch - metadata: expect.objectContaining({ - created: expect.any(String), // This nested matcher matches - should show actual value - updated: expect.any(String), // This nested matcher matches - should show actual value - }), - }), - } - - // With recursion, nested matchers are properly processed: - // - Matching matchers (id, created, updated) are replaced with actual values - // - Non-matching matchers (name) show the mismatch clearly - expect(stripVTControlCharacters(getErrorDiff(actual, expected))).toMatchInlineSnapshot(` - "- Expected - + Received - - @@ -3,8 +3,8 @@ - "id": 1, - "metadata": { - "created": "2024-01-01", - "updated": "2024-01-02", - }, - - "name": StringContaining "Jane", - + "name": "John", - }, - }" - `) -}) From 705cb6d4ce3afbab1f636478d4963a86cbf70db6 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Wed, 24 Dec 2025 17:58:16 +0900 Subject: [PATCH 09/10] refactor: simplify happy path --- packages/utils/src/diff/index.ts | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/packages/utils/src/diff/index.ts b/packages/utils/src/diff/index.ts index b345871c4d1d..a157e81171e5 100644 --- a/packages/utils/src/diff/index.ts +++ b/packages/utils/src/diff/index.ts @@ -340,15 +340,7 @@ export function replaceAsymmetricMatcher( // For container matchers (ArrayContaining, ObjectContaining), unwrap and recursively process if ('sample' in expectedValue && expectedValue.sample !== undefined && isReplaceable(actualValue, expectedValue.sample)) { if (matches) { - // Matcher matches: unwrap and recursively process to show actual structure - const replaced = replaceAsymmetricMatcher( - actualValue, - expectedValue.sample, - actualReplaced, - expectedReplaced, - ) - actual[key] = replaced.replacedActual - expected[key] = replaced.replacedExpected + expected[key] = actualValue } else { // Matcher doesn't match: unwrap but keep structure to show mismatch From 80368bcbfa2fd7117951dc54606f9acfd4b90a5c Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Wed, 24 Dec 2025 18:09:32 +0900 Subject: [PATCH 10/10] refactor: moving around --- packages/utils/src/diff/index.ts | 53 ++++++++++++-------------------- 1 file changed, 19 insertions(+), 34 deletions(-) diff --git a/packages/utils/src/diff/index.ts b/packages/utils/src/diff/index.ts index a157e81171e5..6887e55537b4 100644 --- a/packages/utils/src/diff/index.ts +++ b/packages/utils/src/diff/index.ts @@ -336,38 +336,29 @@ export function replaceAsymmetricMatcher( const expectedValue = expected[key] const actualValue = actual[key] if (isAsymmetricMatcher(expectedValue)) { - const matches = expectedValue.asymmetricMatch(actualValue) - // For container matchers (ArrayContaining, ObjectContaining), unwrap and recursively process - if ('sample' in expectedValue && expectedValue.sample !== undefined && isReplaceable(actualValue, expectedValue.sample)) { - if (matches) { - expected[key] = actualValue - } - else { - // Matcher doesn't match: unwrap but keep structure to show mismatch - const replaced = replaceAsymmetricMatcher( - actualValue, - expectedValue.sample, - actualReplaced, - expectedReplaced, - ) - actual[key] = replaced.replacedActual - expected[key] = replaced.replacedExpected - } + if (expectedValue.asymmetricMatch(actualValue)) { + // When matcher matches, replace expected with actual value + // so they appear the same in the diff + expected[key] = actualValue } - else { - // Simple matchers (StringContaining, Any, etc.) - if (matches) { - // When matcher matches, replace expected with actual value - // so they appear the same in the diff - expected[key] = actualValue - } - // When matcher doesn't match, keep both as-is to show the difference + else if ('sample' in expectedValue && expectedValue.sample !== undefined && isReplaceable(actualValue, expectedValue.sample)) { + // For container matchers (ArrayContaining, ObjectContaining), unwrap and recursively process + // Matcher doesn't match: unwrap but keep structure to show mismatch + const replaced = replaceAsymmetricMatcher( + actualValue, + expectedValue.sample, + actualReplaced, + expectedReplaced, + ) + actual[key] = replaced.replacedActual + expected[key] = replaced.replacedExpected } } else if (isAsymmetricMatcher(actualValue)) { - const matches = actualValue.asymmetricMatch(expectedValue) - // For container matchers in actual (rare case) - if ('sample' in actualValue && actualValue.sample !== undefined && isReplaceable(actualValue.sample, expectedValue)) { + if (actualValue.asymmetricMatch(expectedValue)) { + actual[key] = expectedValue + } + else if ('sample' in actualValue && actualValue.sample !== undefined && isReplaceable(actualValue.sample, expectedValue)) { const replaced = replaceAsymmetricMatcher( actualValue.sample, expectedValue, @@ -377,12 +368,6 @@ export function replaceAsymmetricMatcher( actual[key] = replaced.replacedActual expected[key] = replaced.replacedExpected } - else { - if (matches) { - // When matcher matches, replace actual with expected value - actual[key] = expectedValue - } - } } else if (isReplaceable(actualValue, expectedValue)) { const replaced = replaceAsymmetricMatcher(