| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113 |
- /**
- * Unit tests for the result-time contextual-diff computation (`src/diff.ts`):
- * the pure before/after → {@link FileDiff}[] hunk builder and the defensive
- * `meta` narrowing. These pin the exact hunk reconstruction (context lines,
- * multi-hunk replaceAll, pure insertion/deletion, no-op) that UIs render.
- */
- import { describe, expect, it } from 'vitest'
- import { computeHunkDiffs, diffsFromMeta, DIFF_CONTEXT } from '../src/diff.ts'
- import type { JsonValue } from '@deepseek-ai/dsh-util-values'
- const lines = (n: number): string => Array.from({ length: n }, (_, i) => `line${i + 1}`).join('\n') + '\n'
- describe('computeHunkDiffs', () => {
- it('a single-line change yields one hunk with ±context lines on both sides', () => {
- const before = lines(8)
- const after = before.replace('line4', 'CHANGED')
- const diffs = computeHunkDiffs('f.txt', before, after)
- expect(diffs).toEqual([{
- path: 'f.txt',
- oldText: 'line1\nline2\nline3\nline4\nline5\nline6\nline7',
- newText: 'line1\nline2\nline3\nCHANGED\nline5\nline6\nline7',
- }])
- })
- it('a scattered replace_all yields one FileDiff PER hunk (matching per-site editor blocks)', () => {
- const before = lines(20)
- const after = before.replace('line3', 'A').replace('line16', 'B')
- const diffs = computeHunkDiffs('f.txt', before, after)
- expect(diffs).toHaveLength(2)
- expect(diffs[0]?.path).toBe('f.txt')
- expect(diffs[0]?.oldText).toContain('line3')
- expect(diffs[0]?.newText).toContain('A')
- expect(diffs[1]?.oldText).toContain('line16')
- expect(diffs[1]?.newText).toContain('B')
- // The two hunks are distinct sites, not one merged block.
- expect(diffs[0]?.newText).not.toContain('B')
- expect(diffs[1]?.newText).not.toContain('A')
- })
- it('identical before/after (a no-op) yields no hunks', () => {
- expect(computeHunkDiffs('f.txt', 'same\n', 'same\n')).toEqual([])
- })
- it('a pure insertion into empty content reports oldText null (nothing to diff against)', () => {
- const diffs = computeHunkDiffs('f.txt', '', 'brand new\n')
- expect(diffs).toEqual([{ path: 'f.txt', oldText: null, newText: 'brand new' }])
- })
- it('a pure deletion of the whole file reports newText empty', () => {
- const diffs = computeHunkDiffs('f.txt', 'gone\n', '')
- expect(diffs).toEqual([{ path: 'f.txt', oldText: 'gone', newText: '' }])
- })
- it('drops the "\\ No newline at end of file" marker from a no-trailing-newline change', () => {
- const diffs = computeHunkDiffs('f.txt', 'x', 'y')
- // The marker line (starting with "\\") must never leak into a diff block.
- expect(diffs).toEqual([{ path: 'f.txt', oldText: 'x', newText: 'y' }])
- expect(diffs[0]?.oldText).not.toContain('\\')
- expect(diffs[0]?.newText).not.toContain('\\')
- })
- it('uses DIFF_CONTEXT (3) surrounding lines', () => {
- expect(DIFF_CONTEXT).toBe(3)
- const before = lines(20)
- const after = before.replace('line10', 'CHANGED')
- const [diff] = computeHunkDiffs('f.txt', before, after)
- // 3 context above (7,8,9) + the change + 3 below (11,12,13) = 7 lines a side.
- expect(diff?.oldText?.split('\n')).toHaveLength(7)
- expect(diff?.newText.split('\n')).toHaveLength(7)
- expect(diff?.oldText?.split('\n')[0]).toBe('line7')
- })
- })
- describe('diffsFromMeta (defensive narrowing)', () => {
- // The narrowing accepts an opaque JsonValue; a malformed payload is not a
- // statically-valid JsonValue, so route every case through one cast helper that
- // mirrors how a hand-edited/older session log delivers arbitrary shapes.
- const m = (value: unknown): JsonValue | undefined => value as JsonValue | undefined
- const good = { diffs: [{ path: 'f.txt', oldText: 'a', newText: 'b' }] }
- it('narrows a well-formed { diffs } payload', () => {
- expect(diffsFromMeta(m(good))).toEqual(good.diffs)
- })
- it('accepts a diff whose oldText is null (a create-style hunk)', () => {
- const meta = { diffs: [{ path: 'f.txt', oldText: null, newText: 'x' }] }
- expect(diffsFromMeta(m(meta))).toEqual(meta.diffs)
- })
- it('rejects undefined / non-object / array meta', () => {
- expect(diffsFromMeta(undefined)).toBeUndefined()
- expect(diffsFromMeta(null)).toBeUndefined()
- expect(diffsFromMeta(m('nope'))).toBeUndefined()
- expect(diffsFromMeta(m([]))).toBeUndefined()
- })
- it('rejects a missing / empty / non-array diffs field', () => {
- expect(diffsFromMeta(m({}))).toBeUndefined()
- expect(diffsFromMeta(m({ diffs: [] }))).toBeUndefined()
- expect(diffsFromMeta(m({ diffs: 'x' }))).toBeUndefined()
- })
- it('rejects a diffs array containing a malformed entry', () => {
- expect(diffsFromMeta(m({ diffs: [{ path: 'f.txt', oldText: 'a' }] }))).toBeUndefined()
- expect(diffsFromMeta(m({ diffs: [{ path: 1, oldText: 'a', newText: 'b' }] }))).toBeUndefined()
- expect(diffsFromMeta(m({ diffs: [{ path: 'f', oldText: 5, newText: 'b' }] }))).toBeUndefined()
- expect(diffsFromMeta(m({ diffs: [{ path: 'f', oldText: 'a', newText: 7 }] }))).toBeUndefined()
- expect(diffsFromMeta(m({ diffs: [null] }))).toBeUndefined()
- expect(diffsFromMeta(m({ diffs: ['x'] }))).toBeUndefined()
- expect(diffsFromMeta(m({ diffs: [[]] }))).toBeUndefined()
- })
- })
|