mirror of
https://github.com/vercel/workflow.git
synced 2026-09-14 19:59:43 +08:00
9ea125427f
* 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>
111 lines
4.1 KiB
TypeScript
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);
|
|
});
|
|
});
|