Files
vercel__workflow/packages/web-shared/test/collapse-refs.test.ts
Karthik Kalyan 9ea125427f Decode UTF-8 stream chunks (#1852)
* Decode typed array stream chunks

* Render decoded stream bytes with raw view

* Render decoded bytes in data inspector

* Use generic byte inspector for streams

* review feedback: narrow stream-display exports, fix tab a11y, add collapseRefs tests

- Remove unused formatStreamChunkForDisplay/sanitizeStreamChunkForDisplay
  exports; keep only the formatArrayBufferViewForDisplay path actually used
  by DataInspector.
- Replace broken role=tablist/role=tab on the Decoded/Bytes switcher
  with aria-pressed toggle-button semantics.
- Export collapseRefs/isBytesDisplay and add regression tests covering
  typed-array detection (top-level, nested in object/array/Map/Set,
  DataView exclusion).

* Replace eval with JSON.parse in serialization revive helper (#1848)

* Replace eval with JSON.parse in serialization revive helper

devalue.stringify() always produces valid JSON — special values
(undefined, NaN, Infinity, -0) are encoded as negative integer
sentinels. JSON.parse yields the same flattened array form that
unflatten() expects, without the eval anti-pattern (VULN-918).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Drop redundant workflow package from changeset

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>

* Add e2e test for UTF-8 parseable stream chunks

Emits Uint8Array chunks containing multi-byte UTF-8 (Latin Extended,
CJK, emoji, RTL Arabic) plus a UTF-8 encoded JSON document, and
asserts each chunk round-trips through TextDecoder({ fatal: true }).
Exercises the same decode path the web inspector relies on for
typed-array stream values.

Made-with: Cursor

---------

Co-authored-by: Pranay Prakash <pranay.gp@gmail.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-29 09:22:05 -07:00

111 lines
4.1 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import {
collapseRefs,
isBytesDisplay,
} from '../src/components/ui/data-inspector.js';
/**
* `collapseRefs` is the single typed-array detection path used by
* `DataInspector`. AI agent stream chunks are commonly hydrated as
* `Uint8Array` values either directly or nested in plain objects / Map / Set
* (e.g. `{ delta: new Uint8Array(...) }`). These tests guard against the
* detection silently regressing.
*/
function readBytesDisplayText(value: unknown): string {
if (!isBytesDisplay(value)) {
throw new Error('expected BytesDisplay marker');
}
const desc = Object.getOwnPropertyDescriptor(value, 'text');
return desc?.value as string;
}
function readBytesDisplaySourceType(value: unknown): string | undefined {
if (!isBytesDisplay(value)) return undefined;
const desc = Object.getOwnPropertyDescriptor(value, 'decodedFrom');
return (desc?.value as { type: string } | undefined)?.type;
}
describe('collapseRefs typed-array handling', () => {
it('decodes a top-level Uint8Array as UTF-8 text', () => {
const chunk = new TextEncoder().encode('hello world');
const result = collapseRefs(chunk);
expect(isBytesDisplay(result)).toBe(true);
expect(readBytesDisplayText(result)).toBe('hello world');
expect(readBytesDisplaySourceType(result)).toBe('Uint8Array');
});
it('falls back to a compact summary for binary typed arrays', () => {
const chunk = new Uint8Array([0xff, 0xfe, 0xfd]);
const result = collapseRefs(chunk);
expect(isBytesDisplay(result)).toBe(true);
expect(readBytesDisplayText(result)).toBe('Uint8Array(3) [255, 254, 253]');
expect(readBytesDisplaySourceType(result)).toBeUndefined();
});
it('decodes typed arrays nested in plain objects', () => {
const chunk = new TextEncoder().encode('AI says hi');
const result = collapseRefs({ delta: chunk }) as Record<string, unknown>;
expect(isBytesDisplay(result.delta)).toBe(true);
expect(readBytesDisplayText(result.delta)).toBe('AI says hi');
});
it('decodes typed arrays nested in arrays', () => {
const chunk = new TextEncoder().encode('streamed');
const result = collapseRefs([chunk]) as unknown[];
expect(isBytesDisplay(result[0])).toBe(true);
expect(readBytesDisplayText(result[0])).toBe('streamed');
});
it('decodes typed arrays nested in Map values', () => {
const chunk = new TextEncoder().encode('map value');
const result = collapseRefs(new Map([['delta', chunk]]));
expect(result).toBeInstanceOf(Map);
const value = (result as Map<string, unknown>).get('delta');
expect(isBytesDisplay(value)).toBe(true);
expect(readBytesDisplayText(value)).toBe('map value');
});
it('decodes typed arrays nested in Set entries', () => {
const chunk = new TextEncoder().encode('set value');
const result = collapseRefs(new Set([chunk]));
expect(result).toBeInstanceOf(Set);
const [value] = Array.from(result as Set<unknown>);
expect(isBytesDisplay(value)).toBe(true);
expect(readBytesDisplayText(value)).toBe('set value');
});
it('handles non-Uint8Array typed array views', () => {
// Use values that are guaranteed to decode as invalid UTF-8 so we exercise
// the summary fallback path. 0xff80 in little-endian is an unpaired
// surrogate-leading byte sequence rejected by `TextDecoder({ fatal: true })`.
const chunk = new Int16Array([-128, -1, -128]);
const result = collapseRefs(chunk);
expect(isBytesDisplay(result)).toBe(true);
expect(readBytesDisplayText(result)).toMatch(/^Int16Array\(3\) \[/);
});
it('does not treat DataView as a bytes display target', () => {
const buf = new Uint8Array([0x68, 0x69]).buffer;
const view = new DataView(buf);
const result = collapseRefs(view);
expect(isBytesDisplay(result)).toBe(false);
expect(result).toBe(view);
});
it('passes primitives through unchanged', () => {
expect(collapseRefs('plain text')).toBe('plain text');
expect(collapseRefs(42)).toBe(42);
expect(collapseRefs(null)).toBe(null);
expect(collapseRefs(undefined)).toBe(undefined);
});
});