Spracherkennung für: .ts vermutete Sprache: Unknown {[0] [0] [0]} [Methode: Schwerpunktbildung, einfache Gewichte, sechs Dimensionen]
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { captureEnv } from "../../test-utils/env.js";
import { resolveOAuthRefreshLockPath } from "./paths.js";
describe("resolveOAuthRefreshLockPath", () => {
const envSnapshot = captureEnv(["OPENCLAW_STATE_DIR"]);
let stateDir = "";
beforeEach(async () => {
stateDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-auth-lock-path-"));
process.env.OPENCLAW_STATE_DIR = stateDir;
});
afterEach(async () => {
envSnapshot.restore();
await fs.rm(stateDir, { recursive: true, force: true });
});
it("keeps lock paths inside the oauth-refresh directory for dot-segment ids", () => {
const refreshLockDir = path.join(stateDir, "locks", "oauth-refresh");
const dotSegmentPath = resolveOAuthRefreshLockPath("openai-codex", "..");
const currentDirPath = resolveOAuthRefreshLockPath("openai-codex", ".");
expect(path.dirname(dotSegmentPath)).toBe(refreshLockDir);
expect(path.dirname(currentDirPath)).toBe(refreshLockDir);
expect(path.basename(dotSegmentPath)).toMatch(/^sha256-[0-9a-f]{64}$/);
expect(path.basename(currentDirPath)).toMatch(/^sha256-[0-9a-f]{64}$/);
expect(path.basename(dotSegmentPath)).not.toBe(path.basename(currentDirPath));
});
it("hashes profile ids so distinct values stay distinct", () => {
expect(resolveOAuthRefreshLockPath("openai-codex", "openai-codex:work/test")).not.toBe(
resolveOAuthRefreshLockPath("openai-codex", "openai-codex_work:test"),
);
// Unicode normalization / collation corner cases must still hash distinctly.
expect(resolveOAuthRefreshLockPath("openai-codex", "«c")).not.toBe(
resolveOAuthRefreshLockPath("openai-codex", "઼"),
);
});
it("hashes distinct providers to distinct paths for the same profileId", () => {
// The new (provider, profileId) keying is the whole point of P2 from
// review: a shared profileId across providers must not collide.
expect(resolveOAuthRefreshLockPath("openai-codex", "shared:default")).not.toBe(
resolveOAuthRefreshLockPath("anthropic", "shared:default"),
);
});
it("is immune to simple concat collisions at the provider/profile boundary", () => {
// With a plain `${provider}:${profileId}` hash input, the pair
// ("a", "b:c") would collide with ("a:b", "c"). The NUL separator
// in the hash input rules that out.
expect(resolveOAuthRefreshLockPath("a", "b:c")).not.toBe(
resolveOAuthRefreshLockPath("a:b", "c"),
);
});
it("keeps lock filenames short for long profile ids", () => {
const longProfileId = `openai-codex:${"x".repeat(512)}`;
const basename = path.basename(resolveOAuthRefreshLockPath("openai-codex", longProfileId));
expect(basename).toMatch(/^sha256-[0-9a-f]{64}$/);
expect(Buffer.byteLength(basename, "utf8")).toBeLessThan(255);
});
it("is deterministic: same (provider, profileId) produces the same path", () => {
const first = resolveOAuthRefreshLockPath("openai-codex", "openai-codex:default");
const second = resolveOAuthRefreshLockPath("openai-codex", "openai-codex:default");
expect(first).toBe(second);
});
it("returns a valid path on a clean install where the locks/ directory does not yet exist", async () => {
// Defensive check: even on a fresh install with no lock hierarchy
// populated, the function must return a safe path. withFileLock
// internally creates missing parent dirs, but this test pins the
// expectation so a future change to remove that guarantee would
// fail loudly.
const locksDir = path.join(stateDir, "locks", "oauth-refresh");
// Sanity precondition: parent dir must not exist yet.
await expect(fs.stat(locksDir)).rejects.toThrow();
const resolved = resolveOAuthRefreshLockPath("openai-codex", "openai-codex:default");
expect(path.dirname(resolved)).toBe(locksDir);
expect(path.basename(resolved)).toMatch(/^sha256-[0-9a-f]{64}$/);
// Function itself must not create the directory (path resolver only).
await expect(fs.stat(locksDir)).rejects.toThrow();
});
it("never embeds path separators or .. in the basename", () => {
const hazards = [
["openai-codex", "../etc/passwd"],
["openai-codex", "../../../../secrets"],
["openai-codex", "openai\\codex"],
["openai-codex", "openai/codex/default"],
["openai-codex", "profile\x00with-null"],
["openai-codex", "profile\nwith-newline"],
["openai-codex", "profile with spaces"],
["../../etc", "passwd"],
["provider\x00with-null", "default"],
] as const;
for (const [provider, id] of hazards) {
const basename = path.basename(resolveOAuthRefreshLockPath(provider, id));
expect(basename).toMatch(/^sha256-[0-9a-f]{64}$/);
expect(basename).not.toContain("/");
expect(basename).not.toContain("\\");
expect(basename).not.toContain("..");
expect(basename).not.toContain("\x00");
expect(basename).not.toContain("\n");
}
});
});
describe("resolveOAuthRefreshLockPath fuzz", () => {
const envSnapshot = captureEnv(["OPENCLAW_STATE_DIR"]);
let stateDir = "";
beforeEach(async () => {
stateDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-auth-lock-path-fuzz-"));
process.env.OPENCLAW_STATE_DIR = stateDir;
});
afterEach(async () => {
envSnapshot.restore();
await fs.rm(stateDir, { recursive: true, force: true });
});
function makeSeededRandom(seed: number): () => number {
// Mulberry32 — small, stable, seedable PRNG so the fuzz run is reproducible
// even if the suite later becomes picky about test ordering.
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 randomProfileId(rng: () => number, maxLen: number): string {
const len = Math.floor(rng() * maxLen);
const chars: string[] = [];
for (let i = 0; i < len; i += 1) {
// Cover BMP + surrogate-pair range + control chars + ASCII + path hazards.
const category = Math.floor(rng() * 5);
const code =
category === 0
? Math.floor(rng() * 128) // ASCII
: category === 1
? Math.floor(rng() * 32) // control chars (including \0, \n, \r, etc.)
: category === 2
? 0x10000 + Math.floor(rng() * 0xeffff) // supplementary planes
: category === 3
? Math.floor(rng() * 0xd800) // BMP non-surrogate
: 0x0f00 + Math.floor(rng() * 0x0100); // misc unicode
chars.push(String.fromCodePoint(code));
}
return chars.join("");
}
it("always produces a basename that matches sha256-<hex64> regardless of input", () => {
const rng = makeSeededRandom(0x2026_0417);
for (let i = 0; i < 500; i += 1) {
const provider = randomProfileId(rng, 64) || "openai-codex";
const id = randomProfileId(rng, 4096);
const basename = path.basename(resolveOAuthRefreshLockPath(provider, id));
expect(basename).toMatch(/^sha256-[0-9a-f]{64}$/);
expect(Buffer.byteLength(basename, "utf8")).toBeLessThan(255);
// sha256-<64 hex> = 71 chars, no path hazards. Explicit substring
// checks (no control-char regex) to keep lint happy.
expect(basename).not.toContain("\\");
expect(basename).not.toContain("/");
expect(basename).not.toContain("\u0000");
expect(basename).not.toContain("\n");
expect(basename).not.toContain("\r");
expect(basename).not.toContain("..");
}
});
it("always resolves to a path inside <stateDir>/locks/oauth-refresh", () => {
const rng = makeSeededRandom(0xdecafbad);
const expectedDir = path.join(stateDir, "locks", "oauth-refresh");
for (let i = 0; i < 200; i += 1) {
const provider = randomProfileId(rng, 32) || "openai-codex";
const id = randomProfileId(rng, 1024);
const resolved = resolveOAuthRefreshLockPath(provider, id);
expect(path.dirname(resolved)).toBe(expectedDir);
// Normalized path must still live under the expected directory — defense
// against any future change that lets a profile id escape the scope.
expect(path.normalize(resolved).startsWith(expectedDir + path.sep)).toBe(true);
}
});
it("distinct (provider, profileId) inputs produce distinct outputs over a large random sample", () => {
const rng = makeSeededRandom(0x1234_5678);
const seen = new Map<string, string>();
let collisions = 0;
for (let i = 0; i < 2000; i += 1) {
const provider = randomProfileId(rng, 32) || "p";
const id = randomProfileId(rng, 256);
const composite = `${provider}\u0000${id}`;
const resolved = resolveOAuthRefreshLockPath(provider, id);
const existing = seen.get(resolved);
if (existing !== undefined && existing !== composite) {
collisions += 1;
}
seen.set(resolved, composite);
}
expect(collisions).toBe(0);
});
it("holding provider fixed, distinct profileIds never collide", () => {
const rng = makeSeededRandom(0xf00dbabe);
const seen = new Map<string, string>();
let collisions = 0;
for (let i = 0; i < 1000; i += 1) {
const id = randomProfileId(rng, 128) || `id-${i}`;
const resolved = resolveOAuthRefreshLockPath("openai-codex", id);
const existing = seen.get(resolved);
if (existing !== undefined && existing !== id) {
collisions += 1;
}
seen.set(resolved, id);
}
expect(collisions).toBe(0);
});
it("holding profileId fixed, distinct providers never collide", () => {
const rng = makeSeededRandom(0xbad1d00d);
const seen = new Map<string, string>();
let collisions = 0;
for (let i = 0; i < 500; i += 1) {
const provider = randomProfileId(rng, 64) || `provider-${i}`;
const resolved = resolveOAuthRefreshLockPath(provider, "shared-profile-id");
const existing = seen.get(resolved);
if (existing !== undefined && existing !== provider) {
collisions += 1;
}
seen.set(resolved, provider);
}
expect(collisions).toBe(0);
});
});
¤ Dauer der Verarbeitung: 0.17 Sekunden
(vorverarbeitet am 2026-04-27)
¤
*© Formatika GbR, Deutschland
|
|