chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Controllable fake upstream ReadableStream for integration tests.
|
||||
* Allows pushing chunks, closing, erroring, and observing cancel.
|
||||
*/
|
||||
export function fakeUpstreamStream() {
|
||||
let controllerRef: ReadableStreamDefaultController<Uint8Array> | null = null;
|
||||
let cancelCb: ((reason?: unknown) => void) | null = null;
|
||||
const enc = new TextEncoder();
|
||||
|
||||
const stream = new ReadableStream<Uint8Array>({
|
||||
start(controller) {
|
||||
controllerRef = controller;
|
||||
},
|
||||
cancel(reason) {
|
||||
cancelCb?.(reason);
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
stream,
|
||||
push: (s: string) => controllerRef?.enqueue(enc.encode(s)),
|
||||
close: () => controllerRef?.close(),
|
||||
error: (e: unknown) => controllerRef?.error(e),
|
||||
onCancel: (cb: (reason?: unknown) => void) => {
|
||||
cancelCb = cb;
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import http from "node:http";
|
||||
import type { AddressInfo } from "node:net";
|
||||
|
||||
export type FaultMode =
|
||||
| { kind: "ok"; body?: string }
|
||||
| { kind: "status"; code: number; body?: string }
|
||||
| { kind: "latency"; ms: number; body?: string }
|
||||
| { kind: "reset" }
|
||||
| { kind: "timeout" }
|
||||
| { kind: "slowDrip"; chunkMs: number; body?: string };
|
||||
|
||||
export interface FaultyUpstream {
|
||||
readonly url: string;
|
||||
setMode(mode: FaultMode): void;
|
||||
stop(): Promise<void>;
|
||||
}
|
||||
|
||||
export async function startFaultyUpstream(initial: FaultMode = { kind: "ok" }): Promise<FaultyUpstream> {
|
||||
let mode: FaultMode = initial;
|
||||
|
||||
const server = http.createServer((req, res) => {
|
||||
const m = mode;
|
||||
if (m.kind === "reset") {
|
||||
req.socket.destroy();
|
||||
return;
|
||||
}
|
||||
if (m.kind === "timeout") {
|
||||
return; // never respond; caller relies on AbortSignal/timeout
|
||||
}
|
||||
if (m.kind === "latency") {
|
||||
setTimeout(() => {
|
||||
res.writeHead(200, { "content-type": "text/plain" });
|
||||
res.end(m.body ?? "ok");
|
||||
}, m.ms);
|
||||
return;
|
||||
}
|
||||
if (m.kind === "slowDrip") {
|
||||
const body = m.body ?? "drip-drip-drip";
|
||||
res.writeHead(200, { "content-type": "text/plain" });
|
||||
let i = 0;
|
||||
const timer = setInterval(() => {
|
||||
if (i >= body.length) {
|
||||
clearInterval(timer);
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
res.write(body[i++]);
|
||||
}, m.chunkMs);
|
||||
req.on("close", () => clearInterval(timer));
|
||||
return;
|
||||
}
|
||||
const code = m.kind === "status" ? m.code : 200;
|
||||
res.writeHead(code, { "content-type": "text/plain" });
|
||||
res.end(m.body ?? (m.kind === "status" ? "error" : "ok"));
|
||||
});
|
||||
|
||||
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
|
||||
const { port } = server.address() as AddressInfo;
|
||||
|
||||
return {
|
||||
url: `http://127.0.0.1:${port}/`,
|
||||
setMode(next: FaultMode) {
|
||||
mode = next;
|
||||
},
|
||||
stop() {
|
||||
return new Promise<void>((resolve) => {
|
||||
server.closeAllConnections?.();
|
||||
server.close(() => resolve());
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const DEFAULT_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../snapshots");
|
||||
|
||||
/** Stable JSON serialization: object keys sorted recursively. */
|
||||
function stable(v: unknown): string {
|
||||
return JSON.stringify(
|
||||
v,
|
||||
(_k, val) =>
|
||||
val && typeof val === "object" && !Array.isArray(val)
|
||||
? Object.fromEntries(
|
||||
Object.keys(val as Record<string, unknown>)
|
||||
.sort()
|
||||
.map((k) => [k, (val as Record<string, unknown>)[k]])
|
||||
)
|
||||
: val,
|
||||
2
|
||||
);
|
||||
}
|
||||
|
||||
class GoldenMismatchError extends Error {
|
||||
constructor(name: string, expected: string, actual: string) {
|
||||
super(
|
||||
`golden mismatch for "${name}". Run with UPDATE_GOLDEN=1 to regenerate.\n--- expected\n${expected}\n--- actual\n${actual}`
|
||||
);
|
||||
this.name = "GoldenMismatchError";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare `value` with a stable JSON snapshot at `tests/snapshots/<name>.json`.
|
||||
* - First run (file missing): writes the file and passes.
|
||||
* - UPDATE_GOLDEN=1: rewrites the file and passes.
|
||||
* - Mismatch: throws GoldenMismatchError with a diff-friendly message.
|
||||
*
|
||||
* @param name - Slash-separated path under tests/snapshots/ (e.g. "translation/openai-to-claude/basic-chat")
|
||||
* @param value - The value to serialize and compare.
|
||||
* @param dir - Optional override for snapshot root directory (used in tests to isolate to tmpdir).
|
||||
*/
|
||||
export function goldenSnapshot(name: string, value: unknown, dir = DEFAULT_DIR): void {
|
||||
const file = path.join(dir, `${name}.json`);
|
||||
const serialized = stable(value);
|
||||
|
||||
if (process.env.UPDATE_GOLDEN === "1" || !fs.existsSync(file)) {
|
||||
fs.mkdirSync(path.dirname(file), { recursive: true });
|
||||
fs.writeFileSync(file, serialized + "\n");
|
||||
return;
|
||||
}
|
||||
|
||||
const expected = fs.readFileSync(file, "utf8").trimEnd();
|
||||
if (serialized !== expected) {
|
||||
throw new GoldenMismatchError(name, expected, serialized);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import { SignJWT } from "jose";
|
||||
|
||||
export const TEST_MANAGEMENT_JWT_SECRET = "test-management-jwt-secret";
|
||||
|
||||
function appendCookie(existingCookieHeader: string | null, cookie: string): string {
|
||||
if (!existingCookieHeader) return cookie;
|
||||
return `${existingCookieHeader}; ${cookie}`;
|
||||
}
|
||||
|
||||
export async function createManagementSessionToken(
|
||||
secret = process.env.JWT_SECRET || TEST_MANAGEMENT_JWT_SECRET
|
||||
): Promise<string> {
|
||||
process.env.JWT_SECRET = secret;
|
||||
|
||||
return new SignJWT({ authenticated: true })
|
||||
.setProtectedHeader({ alg: "HS256" })
|
||||
.setIssuedAt()
|
||||
.setExpirationTime("1h")
|
||||
.sign(new TextEncoder().encode(secret));
|
||||
}
|
||||
|
||||
export async function createManagementSessionHeaders(headers?: HeadersInit): Promise<Headers> {
|
||||
const requestHeaders = new Headers(headers);
|
||||
const token = await createManagementSessionToken();
|
||||
requestHeaders.set("cookie", appendCookie(requestHeaders.get("cookie"), `auth_token=${token}`));
|
||||
return requestHeaders;
|
||||
}
|
||||
|
||||
export async function makeManagementSessionRequest(
|
||||
url: string,
|
||||
options: {
|
||||
method?: string;
|
||||
token?: string;
|
||||
headers?: HeadersInit;
|
||||
body?: BodyInit | Record<string, unknown> | unknown[] | number | boolean | null;
|
||||
} = {}
|
||||
): Promise<Request> {
|
||||
const { method = "GET", token, headers, body } = options;
|
||||
const requestHeaders = await createManagementSessionHeaders(headers);
|
||||
|
||||
if (token) {
|
||||
requestHeaders.set("authorization", `Bearer ${token}`);
|
||||
}
|
||||
|
||||
const isFormData = typeof FormData !== "undefined" && body instanceof FormData;
|
||||
const isUrlSearchParams =
|
||||
typeof URLSearchParams !== "undefined" && body instanceof URLSearchParams;
|
||||
const isStringBody = typeof body === "string";
|
||||
const shouldSerializeJson =
|
||||
body !== undefined && !isFormData && !isUrlSearchParams && !isStringBody;
|
||||
|
||||
if (body !== undefined && !requestHeaders.has("content-type") && !isFormData) {
|
||||
requestHeaders.set(
|
||||
"content-type",
|
||||
isUrlSearchParams ? "application/x-www-form-urlencoded;charset=UTF-8" : "application/json"
|
||||
);
|
||||
}
|
||||
|
||||
return new Request(url, {
|
||||
method,
|
||||
headers: requestHeaders,
|
||||
body: shouldSerializeJson ? JSON.stringify(body) : body,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import fc from "fast-check";
|
||||
import { PROPERTY_SEED, PROPERTY_NUM_RUNS } from "../../scripts/quality/property-seed.mjs";
|
||||
|
||||
const envSeed = process.env.FC_SEED;
|
||||
export const seed = envSeed === "random" ? undefined : envSeed ? Number(envSeed) : PROPERTY_SEED;
|
||||
export const numRuns = process.env.FC_NUM_RUNS
|
||||
? Number(process.env.FC_NUM_RUNS)
|
||||
: PROPERTY_NUM_RUNS;
|
||||
|
||||
/** Apply the shared deterministic config; call once at top of each property test file. */
|
||||
export function configureProperties(): void {
|
||||
fc.configureGlobal({ seed, numRuns });
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../fixtures/translation");
|
||||
|
||||
export type TranslationCase = {
|
||||
name: string;
|
||||
sourceFormat: string;
|
||||
targetFormat: string;
|
||||
input: Record<string, unknown>;
|
||||
expected?: unknown;
|
||||
};
|
||||
|
||||
export type SseSequence = {
|
||||
name: string;
|
||||
chunks: string[];
|
||||
expectedText: string;
|
||||
};
|
||||
|
||||
export function loadTranslationFixtures(): TranslationCase[] {
|
||||
return ["openai-to-claude", "claude-to-openai", "openai-to-gemini", "gemini-to-openai"].flatMap(
|
||||
(f) => JSON.parse(fs.readFileSync(path.join(DIR, `${f}.json`), "utf8")) as TranslationCase[]
|
||||
);
|
||||
}
|
||||
|
||||
export function loadSseSequences(): SseSequence[] {
|
||||
return JSON.parse(
|
||||
fs.readFileSync(path.join(DIR, "sse-chunk-sequences.json"), "utf8")
|
||||
) as SseSequence[];
|
||||
}
|
||||
Reference in New Issue
Block a user