diff --git a/src/agents/auth-profiles/oauth-identity.test.ts b/src/agents/auth-profiles/oauth-identity.test.ts index cbfaff0c7cb..4fef5d5385d 100644 --- a/src/agents/auth-profiles/oauth-identity.test.ts +++ b/src/agents/auth-profiles/oauth-identity.test.ts @@ -6,6 +6,7 @@ import { normalizeAuthIdentityToken, shouldMirrorRefreshedOAuthCredential, } from "./oauth-identity.js"; +import { makeSeededRandom, maybe, randomAsciiString as randomString } from "./oauth-test-utils.js"; import type { AuthProfileCredential } from "./types.js"; // Direct unit + fuzz tests for the cross-agent credential-mirroring identity @@ -177,30 +178,6 @@ describe("isSameOAuthIdentity", () => { // Fuzz tests. Seeded Mulberry32 so the run is reproducible. // --------------------------------------------------------------------------- -function makeSeededRandom(seed: number): () => number { - let t = seed >>> 0; - return () => { - t = (t + 0x6d2b79f5) >>> 0; - let r = t; - r = Math.imul(r ^ (r >>> 15), r | 1); - r ^= r + Math.imul(r ^ (r >>> 7), r | 61); - return ((r ^ (r >>> 14)) >>> 0) / 4294967296; - }; -} - -function randomString(rng: () => number, maxLen: number): string { - const len = Math.floor(rng() * maxLen); - const chars: string[] = []; - for (let i = 0; i < len; i += 1) { - chars.push(String.fromCodePoint(32 + Math.floor(rng() * 95))); // printable ASCII - } - return chars.join(""); -} - -function maybe(rng: () => number, value: T): T | undefined { - return rng() < 0.5 ? value : undefined; -} - describe("isSafeToCopyOAuthIdentity (unified copy gate, used for mirror and adopt)", () => { describe("positive matches", () => { it("accepts matching accountIds", () => { @@ -479,30 +456,6 @@ describe("shouldMirrorRefreshedOAuthCredential", () => { }); describe("isSafeToCopyOAuthIdentity fuzz", () => { - function makeSeededRandom(seed: number): () => number { - let t = seed >>> 0; - return () => { - t = (t + 0x6d2b79f5) >>> 0; - let r = t; - r = Math.imul(r ^ (r >>> 15), r | 1); - r ^= r + Math.imul(r ^ (r >>> 7), r | 61); - return ((r ^ (r >>> 14)) >>> 0) / 4294967296; - }; - } - - function randomString(rng: () => number, maxLen: number): string { - const len = Math.floor(rng() * maxLen); - const chars: string[] = []; - for (let i = 0; i < len; i += 1) { - chars.push(String.fromCodePoint(32 + Math.floor(rng() * 95))); - } - return chars.join(""); - } - - function maybe(rng: () => number, value: T): T | undefined { - return rng() < 0.5 ? value : undefined; - } - it("is reflexive: share(a, a) is always true", () => { const rng = makeSeededRandom(0x0172_0417); for (let i = 0; i < 1000; i += 1) { diff --git a/src/agents/auth-profiles/oauth-refresh-error.test.ts b/src/agents/auth-profiles/oauth-refresh-error.test.ts index ed5e939c1ba..fbb968ab494 100644 --- a/src/agents/auth-profiles/oauth-refresh-error.test.ts +++ b/src/agents/auth-profiles/oauth-refresh-error.test.ts @@ -1,4 +1,9 @@ import { describe, expect, it } from "vitest"; +import { + makeSeededRandom, + randomAsciiString as randomJunk, + randomlyCased, +} from "./oauth-test-utils.js"; import { isRefreshTokenReusedError } from "./oauth.js"; // Direct tests for the refresh_token_reused classifier. This is the gate that @@ -94,33 +99,6 @@ describe("isRefreshTokenReusedError", () => { }); describe("fuzz: random noisy messages", () => { - function makeSeededRandom(seed: number): () => number { - let t = seed >>> 0; - return () => { - t = (t + 0x6d2b79f5) >>> 0; - let r = t; - r = Math.imul(r ^ (r >>> 15), r | 1); - r ^= r + Math.imul(r ^ (r >>> 7), r | 61); - return ((r ^ (r >>> 14)) >>> 0) / 4294967296; - }; - } - - function randomJunk(rng: () => number, maxLen: number): string { - const len = Math.floor(rng() * maxLen); - const chars: string[] = []; - for (let i = 0; i < len; i += 1) { - chars.push(String.fromCodePoint(32 + Math.floor(rng() * 95))); - } - return chars.join(""); - } - - function randomlyCased(s: string, rng: () => number): string { - return s - .split("") - .map((c) => (rng() < 0.5 ? c.toUpperCase() : c.toLowerCase())) - .join(""); - } - it("always detects the marker when embedded at random positions with noise", () => { const rng = makeSeededRandom(0xabad1dea); const markers = [ diff --git a/src/agents/auth-profiles/oauth-test-utils.ts b/src/agents/auth-profiles/oauth-test-utils.ts index 55204bb6d99..74b0f8ac44d 100644 --- a/src/agents/auth-profiles/oauth-test-utils.ts +++ b/src/agents/auth-profiles/oauth-test-utils.ts @@ -52,3 +52,34 @@ export function createExpiredOauthStore(params: { }, }; } + +export function makeSeededRandom(seed: number): () => number { + let state = seed >>> 0; + return () => { + state = (state + 0x6d2b79f5) >>> 0; + let value = state; + value = Math.imul(value ^ (value >>> 15), value | 1); + value ^= value + Math.imul(value ^ (value >>> 7), value | 61); + return ((value ^ (value >>> 14)) >>> 0) / 4294967296; + }; +} + +export function randomAsciiString(rng: () => number, maxLen: number): string { + const len = Math.floor(rng() * maxLen); + const chars: string[] = []; + for (let index = 0; index < len; index += 1) { + chars.push(String.fromCodePoint(32 + Math.floor(rng() * 95))); + } + return chars.join(""); +} + +export function maybe(rng: () => number, value: T): T | undefined { + return rng() < 0.5 ? value : undefined; +} + +export function randomlyCased(value: string, rng: () => number): string { + return value + .split("") + .map((char) => (rng() < 0.5 ? char.toUpperCase() : char.toLowerCase())) + .join(""); +}