Files
opencode-antigravity-auth/src/plugin/cache.test.ts
Noe 314ac9d427 feat(claude): add multi-turn thinking signature caching and real-time SSE streaming
Implement comprehensive support for Claude thinking models with interleaved
thinking in multi-turn conversations:

- Add signature caching system to preserve and restore thinking block
  signatures across conversation turns, preventing "invalid signature" errors
- Enable real-time SSE streaming with immediate forwarding of thinking tokens
- Add interleaved-thinking-2025-05-14 beta header for Claude thinking models
- Implement smart system hints to encourage thinking during tool use
- Add VALIDATED mode for tool calling on Claude models
- Ensure output token limits accommodate thinking budgets
- Filter and sanitize thinking blocks, removing SDK-injected cache_control
- Add comprehensive test suites for auth, cache, and request-helpers modules
- Update build config to exclude test files from production builds
- Document streaming and thinking features in README
2025-12-17 15:52:40 +00:00

296 lines
9.4 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import {
resolveCachedAuth,
storeCachedAuth,
clearCachedAuth,
cacheSignature,
getCachedSignature,
clearSignatureCache,
} from "./cache";
import type { OAuthAuthDetails } from "./types";
function createAuth(overrides: Partial<OAuthAuthDetails> = {}): OAuthAuthDetails {
return {
type: "oauth",
refresh: "refresh-token|project-id",
access: "access-token",
expires: Date.now() + 3600000,
...overrides,
};
}
describe("Auth Cache", () => {
beforeEach(() => {
vi.useRealTimers();
clearCachedAuth();
});
afterEach(() => {
clearCachedAuth();
});
describe("resolveCachedAuth", () => {
it("returns input auth when no cache exists and caches it", () => {
const auth = createAuth();
const result = resolveCachedAuth(auth);
expect(result).toEqual(auth);
});
it("returns input auth when refresh key is empty", () => {
const auth = createAuth({ refresh: "" });
const result = resolveCachedAuth(auth);
expect(result).toEqual(auth);
});
it("returns input auth when it has valid (unexpired) access token", () => {
const oldAuth = createAuth({ access: "old-access", expires: Date.now() + 3600000 });
resolveCachedAuth(oldAuth); // cache it
const newAuth = createAuth({ access: "new-access", expires: Date.now() + 7200000 });
const result = resolveCachedAuth(newAuth);
expect(result.access).toBe("new-access");
});
it("returns cached auth when input auth is expired but cached is valid", () => {
vi.useFakeTimers();
vi.setSystemTime(new Date(0));
const validAuth = createAuth({
access: "valid-access",
expires: 3600000, // expires at t=3600000
});
resolveCachedAuth(validAuth); // cache it
// Now create an expired auth with the same refresh token
const expiredAuth = createAuth({
access: "expired-access",
expires: 30000, // expires within buffer (60s)
});
const result = resolveCachedAuth(expiredAuth);
expect(result.access).toBe("valid-access");
});
it("returns input auth when both are expired (updates cache)", () => {
vi.useFakeTimers();
vi.setSystemTime(new Date(0));
const expiredCached = createAuth({
access: "cached-expired",
expires: 30000, // expired within buffer
});
resolveCachedAuth(expiredCached);
const expiredNew = createAuth({
access: "new-expired",
expires: 20000, // also expired within buffer
});
const result = resolveCachedAuth(expiredNew);
expect(result.access).toBe("new-expired");
});
});
describe("storeCachedAuth", () => {
it("stores auth in cache", () => {
const auth = createAuth({ access: "stored-access" });
storeCachedAuth(auth);
const expiredAuth = createAuth({ access: "expired", expires: Date.now() - 1000 });
const result = resolveCachedAuth(expiredAuth);
expect(result.access).toBe("stored-access");
});
it("does nothing when refresh key is empty", () => {
const auth = createAuth({ refresh: "", access: "no-key-access" });
storeCachedAuth(auth);
// Should not be retrievable since key was empty
const testAuth = createAuth({ refresh: "", access: "test" });
const result = resolveCachedAuth(testAuth);
expect(result.access).toBe("test"); // returns the input, not cached
});
it("does nothing when refresh key is whitespace only", () => {
const auth = createAuth({ refresh: " ", access: "whitespace-access" });
storeCachedAuth(auth);
const testAuth = createAuth({ refresh: " ", access: "test" });
const result = resolveCachedAuth(testAuth);
expect(result.access).toBe("test");
});
});
describe("clearCachedAuth", () => {
it("clears all cache when no argument provided", () => {
storeCachedAuth(createAuth({ refresh: "token1|p", access: "access1" }));
storeCachedAuth(createAuth({ refresh: "token2|p", access: "access2" }));
clearCachedAuth();
const auth1 = createAuth({ refresh: "token1|p", access: "new1" });
const auth2 = createAuth({ refresh: "token2|p", access: "new2" });
expect(resolveCachedAuth(auth1).access).toBe("new1");
expect(resolveCachedAuth(auth2).access).toBe("new2");
});
it("clears specific refresh token from cache", () => {
storeCachedAuth(createAuth({ refresh: "token1|p", access: "access1" }));
storeCachedAuth(createAuth({ refresh: "token2|p", access: "access2" }));
clearCachedAuth("token1|p");
// token1 should be cleared
const expiredAuth1 = createAuth({ refresh: "token1|p", access: "new1", expires: Date.now() - 1000 });
expect(resolveCachedAuth(expiredAuth1).access).toBe("new1");
// token2 should still be cached
const expiredAuth2 = createAuth({ refresh: "token2|p", access: "new2", expires: Date.now() - 1000 });
expect(resolveCachedAuth(expiredAuth2).access).toBe("access2");
});
});
});
describe("Signature Cache", () => {
beforeEach(() => {
vi.useRealTimers();
clearSignatureCache();
});
afterEach(() => {
clearSignatureCache();
});
describe("cacheSignature", () => {
it("caches a signature for session and text", () => {
cacheSignature("session1", "thinking text", "sig123");
const result = getCachedSignature("session1", "thinking text");
expect(result).toBe("sig123");
});
it("does nothing when sessionId is empty", () => {
cacheSignature("", "text", "sig");
expect(getCachedSignature("", "text")).toBeUndefined();
});
it("does nothing when text is empty", () => {
cacheSignature("session", "", "sig");
expect(getCachedSignature("session", "")).toBeUndefined();
});
it("does nothing when signature is empty", () => {
cacheSignature("session", "text", "");
expect(getCachedSignature("session", "text")).toBeUndefined();
});
it("stores multiple signatures per session", () => {
cacheSignature("session1", "text1", "sig1");
cacheSignature("session1", "text2", "sig2");
expect(getCachedSignature("session1", "text1")).toBe("sig1");
expect(getCachedSignature("session1", "text2")).toBe("sig2");
});
it("stores signatures for different sessions independently", () => {
cacheSignature("session1", "text", "sig1");
cacheSignature("session2", "text", "sig2");
expect(getCachedSignature("session1", "text")).toBe("sig1");
expect(getCachedSignature("session2", "text")).toBe("sig2");
});
});
describe("getCachedSignature", () => {
it("returns undefined when session not found", () => {
expect(getCachedSignature("unknown", "text")).toBeUndefined();
});
it("returns undefined when text not found in session", () => {
cacheSignature("session", "known-text", "sig");
expect(getCachedSignature("session", "unknown-text")).toBeUndefined();
});
it("returns undefined when sessionId is empty", () => {
expect(getCachedSignature("", "text")).toBeUndefined();
});
it("returns undefined when text is empty", () => {
expect(getCachedSignature("session", "")).toBeUndefined();
});
it("returns undefined when signature is expired", () => {
vi.useFakeTimers();
vi.setSystemTime(new Date(0));
cacheSignature("session", "text", "sig");
// Advance time past TTL (1 hour = 3600000ms)
vi.setSystemTime(new Date(3600001));
expect(getCachedSignature("session", "text")).toBeUndefined();
});
it("returns signature when not expired", () => {
vi.useFakeTimers();
vi.setSystemTime(new Date(0));
cacheSignature("session", "text", "sig");
// Advance time but stay within TTL
vi.setSystemTime(new Date(3599999));
expect(getCachedSignature("session", "text")).toBe("sig");
});
});
describe("clearSignatureCache", () => {
it("clears all signature cache when no argument provided", () => {
cacheSignature("session1", "text", "sig1");
cacheSignature("session2", "text", "sig2");
clearSignatureCache();
expect(getCachedSignature("session1", "text")).toBeUndefined();
expect(getCachedSignature("session2", "text")).toBeUndefined();
});
it("clears specific session from cache", () => {
cacheSignature("session1", "text", "sig1");
cacheSignature("session2", "text", "sig2");
clearSignatureCache("session1");
expect(getCachedSignature("session1", "text")).toBeUndefined();
expect(getCachedSignature("session2", "text")).toBe("sig2");
});
});
describe("cache eviction", () => {
it("evicts entries when at capacity", () => {
vi.useFakeTimers();
vi.setSystemTime(new Date(0));
// Fill cache with 100 entries (MAX_ENTRIES_PER_SESSION)
for (let i = 0; i < 100; i++) {
vi.setSystemTime(new Date(i * 1000)); // stagger timestamps
cacheSignature("session", `text-${i}`, `sig-${i}`);
}
// Reset time to check entries
vi.setSystemTime(new Date(100 * 1000));
// Adding one more should trigger eviction
cacheSignature("session", "new-text", "new-sig");
// New entry should exist
expect(getCachedSignature("session", "new-text")).toBe("new-sig");
// Some old entries should have been evicted (oldest 25%)
// Entry at index 0 (timestamp 0) should be evicted
expect(getCachedSignature("session", "text-0")).toBeUndefined();
});
});
});