Files
wehub-resource-sync 0d3cb498a3
CI / Shell Format Check (push) Has been cancelled
CI / Check Ruby (3.4) (push) Has been cancelled
CI / CI Config (push) Has been cancelled
CI / Test on Node ${{ matrix.node }} and ${{ matrix.os }}${{ matrix.shard && format(' (shard {0}/3)', matrix.shard) || '' }} (push) Has been cancelled
CI / Build on Node ${{ matrix.node }} (push) Has been cancelled
CI / Style Check (push) Has been cancelled
CI / Generate Assets (push) Has been cancelled
CI / Check Python (3.14) (push) Has been cancelled
CI / Check Python (3.9) (push) Has been cancelled
CI / Build Docs (push) Has been cancelled
CI / Code Scan Action (push) Has been cancelled
CI / Site tests (push) Has been cancelled
CI / webui tests (push) Has been cancelled
CI / Run Integration Tests (push) Has been cancelled
CI / Run Smoke Tests (push) Has been cancelled
CI / Go Tests (push) Has been cancelled
CI / Share Test (push) Has been cancelled
CI / Redteam (Production API) (push) Has been cancelled
CI / Redteam (Staging API) (push) Has been cancelled
CI / GitHub Actions Lint (push) Has been cancelled
CI / Check Ruby (3.0) (push) Has been cancelled
release-please / release-please (push) Has been cancelled
release-please / build (push) Has been cancelled
release-please / publish-npm (push) Has been cancelled
release-please / publish-npm-backfill (push) Has been cancelled
release-please / docker (push) Has been cancelled
release-please / publish-code-scan-action (push) Has been cancelled
release-please / attest-code-scan-action (push) Has been cancelled
Deploy local.promptfoo.app / Deploy to Cloudflare Pages (push) Has been cancelled
Test and Publish Multi-arch Docker Image / test (push) Has been cancelled
Test and Publish Multi-arch Docker Image / build-docker-and-push-digests (map[digest-suffix:linux-amd64 platform:linux/amd64 runner:ubuntu-latest]) (push) Has been cancelled
Test and Publish Multi-arch Docker Image / build-docker-and-push-digests (map[digest-suffix:linux-arm64 platform:linux/arm64 runner:ubuntu-24.04-arm]) (push) Has been cancelled
Test and Publish Multi-arch Docker Image / merge-docker-digests (push) Has been cancelled
Test and Publish Multi-arch Docker Image / Attest Multi-arch Image (push) Has been cancelled
Validate Renovate Config / Validate Renovate Configuration (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:24:08 +08:00

98 lines
3.7 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import {
collectBlobHashes,
extractBlobHashesFromString,
extractBlobHashesFromValue,
normalizeBlobHash,
} from '../../src/blobs/blobRefs';
const HASH_A = 'a'.repeat(64);
const HASH_B = 'b'.repeat(64);
const uri = (hash: string) => `promptfoo://blob/${hash}`;
describe('normalizeBlobHash', () => {
it('lowercases the hash', () => {
expect(normalizeBlobHash('ABCDEF')).toBe('abcdef');
});
});
describe('extractBlobHashesFromString', () => {
it('returns an empty array for strings without a blob URI', () => {
expect(extractBlobHashesFromString('just some text')).toEqual([]);
});
it('extracts and normalizes every blob hash in the string', () => {
const value = `before ${uri(HASH_A.toUpperCase())} middle ${uri(HASH_B)} after`;
expect(extractBlobHashesFromString(value)).toEqual([HASH_A, HASH_B]);
});
it('skips long strings that do not start with the blob scheme when maxStringLength is set', () => {
const value = `${'x'.repeat(200)} ${uri(HASH_A)}`;
expect(extractBlobHashesFromString(value, 100)).toEqual([]);
});
it('still scans long strings that start with the blob scheme', () => {
const value = `${uri(HASH_A)}${'y'.repeat(200)}`;
expect(extractBlobHashesFromString(value, 100)).toEqual([HASH_A]);
});
});
describe('extractBlobHashesFromValue', () => {
it('extracts from a string', () => {
expect(extractBlobHashesFromValue(uri(HASH_A))).toEqual([HASH_A]);
});
it('extracts from an object with a valid hash field', () => {
expect(extractBlobHashesFromValue({ hash: HASH_A.toUpperCase() })).toEqual([HASH_A]);
});
it('extracts from an object with a uri field', () => {
expect(extractBlobHashesFromValue({ uri: uri(HASH_B) })).toEqual([HASH_B]);
});
it('returns an empty array for non-blob primitives and objects', () => {
expect(extractBlobHashesFromValue(42)).toEqual([]);
expect(extractBlobHashesFromValue(null)).toEqual([]);
expect(extractBlobHashesFromValue({ hash: 'not-a-hash' })).toEqual([]);
});
});
describe('collectBlobHashes', () => {
it('collects and deduplicates hashes across a nested structure', () => {
const value = {
results: [{ response: { output: uri(HASH_A) } }, { response: { output: uri(HASH_A) } }],
traces: [{ metadata: { attachment: uri(HASH_B) } }],
};
expect([...collectBlobHashes(value)].sort()).toEqual([HASH_A, HASH_B]);
});
it('terminates on cyclic objects without infinite recursion', () => {
const node: Record<string, unknown> = { output: uri(HASH_A) };
node.self = node;
expect([...collectBlobHashes(node)]).toEqual([HASH_A]);
});
it('stops recursing past maxDepth', () => {
// Root object is depth 0, the `b` object depth 1, its blob URI string depth 2.
const value = { a: { b: uri(HASH_A) } };
expect([...collectBlobHashes(value, { maxDepth: 1 })]).toEqual([]);
expect([...collectBlobHashes(value, { maxDepth: 2 })]).toEqual([HASH_A]);
});
it('does not overflow the stack on deeply nested input', () => {
let node: unknown = uri(HASH_A);
for (let depth = 0; depth < 50_000; depth += 1) {
node = { child: node };
}
// maxDepth bounds recursion long before the call stack is exhausted.
expect(() => collectBlobHashes(node, { maxDepth: 64 })).not.toThrow();
});
it('honors maxStringLength while still scanning blob-scheme-prefixed strings', () => {
const buried = { output: `${'x'.repeat(200)} ${uri(HASH_A)}` };
const prefixed = { output: `${uri(HASH_B)}${'y'.repeat(200)}` };
expect([...collectBlobHashes(buried, { maxStringLength: 100 })]).toEqual([]);
expect([...collectBlobHashes(prefixed, { maxStringLength: 100 })]).toEqual([HASH_B]);
});
});