import { createHash } from 'node:crypto'; import { describe, expect, it, vi } from 'vitest'; import { Doc0SourceClient, Doc0SourceUnavailableError } from '../../src/document-workbench/doc0-source-client'; const hash = (value: string) => createHash('sha256').update(value).digest('hex'); const receipt = { documentId: 'doc-a', versionId: 'version-a', checksumSha256: 'a'.repeat(64), pipelineStage: 'parsed' }; const input = { tenantId: 'tenant-a', projectId: 'project-a', actorId: 'user-a', requestId: 'request-a', receipt }; const text = 'Acme 😀'; const metadata = { tenantId: input.tenantId, documentId: receipt.documentId, versionId: receipt.versionId, documentTextId: 'text-a', parserRunId: 'run-a', segmentsTruncated: false, segmentCount: 1, charCount: text.length, textSha256: hash(text) }; const segment = { tenantId: input.tenantId, documentId: receipt.documentId, versionId: receipt.versionId, documentTextId: 'text-a', parserRunId: 'run-a', segmentId: 'segment-a', sequence: 0, text, charStart: 0, charEnd: text.length, segmentSha256: hash(text), sourceVersionSha256: receipt.checksumSha256 }; const payload = { offset: 0, texts: [metadata], segments: [segment] }; describe('DOC0 extracted source client', () => { it('reads scoped, checksum-bound segment text without caching or redirects', async () => { const fetchImpl = vi.fn(async (url: string | URL | Request, init?: RequestInit) => { expect(String(url)).toContain('/v1/doc0/documents/doc-a/versions/version-a/segments?limit=100&offset=0'); expect(new Headers(init?.headers).get('x-tenant-id')).toBe('tenant-a'); expect(new Headers(init?.headers).get('x-actor-id')).toBe('user-a'); expect(init?.redirect).toBe('error'); expect(init?.cache).toBe('no-store'); return Response.json(payload); }); const client = new Doc0SourceClient({ baseUrl: 'https://doc0.test', serviceToken: 'test', fetchImpl }); expect(await client.read(input)).toEqual({ sources: [{ id: 'segment-a', text }], reportedTextSha256: hash(text) }); }); it('rejects incomplete, forged, cross-tenant or inconsistent text instead of passing it to a model', async () => { const bad = [ { ...payload, texts: [{ ...metadata, segmentsTruncated: true }] }, { ...payload, texts: [{ ...metadata, segmentCount: 2 }] }, { ...payload, texts: [{ ...metadata, tenantId: 'tenant-b' }] }, { ...payload, texts: [metadata, metadata] }, { ...payload, segments: [{ ...segment, sourceVersionSha256: 'b'.repeat(64) }] }, { ...payload, segments: [{ ...segment, text: 'fabricated' }] }, { ...payload, segments: [{ ...segment, tenantId: 'tenant-b' }] }, { ...payload, segments: [{ ...segment, sequence: 1 }] }, { ...payload, segments: [{ ...segment, charEnd: 5 }] }, { ...payload, offset: 1 }, ]; for (const candidate of bad) { const client = new Doc0SourceClient({ baseUrl: 'https://doc0.test', serviceToken: 'test', fetchImpl: async () => Response.json(candidate) }); await expect(client.read(input)).rejects.toBeInstanceOf(Doc0SourceUnavailableError); } }); it('rejects empty, oversized, unavailable and unparsed sources', async () => { const fetchImpl = vi.fn(async () => Response.json(payload)); const client = new Doc0SourceClient({ baseUrl: 'https://doc0.test', serviceToken: 'test', fetchImpl }); await expect(client.read({ ...input, receipt: { ...receipt, pipelineStage: 'parse_failed' } })).rejects.toBeInstanceOf(Doc0SourceUnavailableError); expect(fetchImpl).not.toHaveBeenCalled(); for (const response of [Response.json({ ...payload, texts: [] }), new Response('x'.repeat(1024 * 1024 + 1)), new Response('', { status: 503 })]) { const bad = new Doc0SourceClient({ baseUrl: 'https://doc0.test', serviceToken: 'test', fetchImpl: async () => response }); await expect(bad.read(input)).rejects.toBeInstanceOf(Doc0SourceUnavailableError); } }); it('pins the original scope and validated credential during an in-flight read', async () => { const mutable = structuredClone(input); const config = { baseUrl: 'https://doc0.test', serviceToken: 'original', fetchImpl: async (_url: string | URL | Request, init?: RequestInit) => { expect(new Headers(init?.headers).get('authorization')).toBe('Bearer original'); mutable.tenantId = 'tenant-b'; mutable.receipt.checksumSha256 = 'b'.repeat(64); return Response.json(payload); } }; const client = new Doc0SourceClient(config); config.serviceToken = 'changed'; expect(await client.read(mutable)).toMatchObject({ sources: [{ id: 'segment-a', text }] }); }); });