File size: 9,548 Bytes
88c4c60 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 | /**
* Integration test: Antigravity (AG) prompt caching behavior.
*
* Verifies:
* 1. Same sessionId + repeated long prompt β cache hit (cachedContentTokenCount > 0)
* 2. Different sessionId (same account) β cache miss
* 3. Cross-account cache share? (call A warmup β B same prompt/session, check hit)
*
* Reads real OAuth refreshToken from ~/.9router/db.json.
* Enable with: AG_CACHE_TEST=1 npm test
*/
import { describe, it, expect } from "vitest";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import crypto from "node:crypto";
import { PROVIDERS } from "../../open-sse/config/providers.js";
import { ANTIGRAVITY_HEADERS, INTERNAL_REQUEST_HEADER } from "../../open-sse/config/appConstants.js";
const ENABLE = process.env.AG_CACHE_TEST === "1";
const DB_PATH = path.join(os.homedir(), ".9router", "db.json");
const OAUTH_TOKEN_URL = "https://oauth2.googleapis.com/token";
const MIN_CACHE_TOKENS = 100; // AG implicit cache threshold observed ~1024-2048
const LONG_TEXT = ("You are a careful assistant. Always follow these rules. ".repeat(300)).trim();
function loadAgConnections() {
if (!fs.existsSync(DB_PATH)) return [];
const db = JSON.parse(fs.readFileSync(DB_PATH, "utf8"));
return (db.providerConnections || []).filter(
c => c.provider === "antigravity" && c.isActive && c.refreshToken && c.projectId
);
}
async function refreshAccessToken(refreshToken) {
const { clientId, clientSecret } = PROVIDERS.antigravity;
const res = await fetch(OAUTH_TOKEN_URL, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
grant_type: "refresh_token",
refresh_token: refreshToken,
client_id: clientId,
client_secret: clientSecret
})
});
if (!res.ok) throw new Error(`refresh failed ${res.status}`);
const json = await res.json();
return json.access_token;
}
async function callAg({ accessToken, projectId, sessionId, longText, userText }) {
const baseUrl = PROVIDERS.antigravity.baseUrls[0];
const body = {
project: projectId,
model: "gemini-3-flash",
userAgent: "antigravity",
requestType: "agent",
requestId: `agent-${crypto.randomUUID()}`,
request: {
systemInstruction: { role: "system", parts: [{ text: longText }] },
contents: [{ role: "user", parts: [{ text: userText }] }],
sessionId
}
};
const res = await fetch(`${baseUrl}/v1internal:generateContent`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${accessToken}`,
"User-Agent": ANTIGRAVITY_HEADERS["User-Agent"],
[INTERNAL_REQUEST_HEADER.name]: INTERNAL_REQUEST_HEADER.value,
"X-Machine-Session-Id": sessionId
},
body: JSON.stringify(body)
});
const json = await res.json();
const usage = json?.response?.usageMetadata || json?.usageMetadata || {};
return {
status: res.status,
promptTokens: usage.promptTokenCount || 0,
cachedTokens: usage.cachedContentTokenCount || 0,
totalTokens: usage.totalTokenCount || 0,
raw: json
};
}
describe.skipIf(!ENABLE)("Antigravity cache behavior (real API)", () => {
const conns = loadAgConnections();
it("has at least one active AG connection with refreshToken", () => {
expect(conns.length).toBeGreaterThan(0);
});
it("same sessionId β cache hit on repeated call", async () => {
const [acc] = conns;
const token = await refreshAccessToken(acc.refreshToken);
const sessionId = `test-same-${crypto.randomUUID()}`;
const r1 = await callAg({ accessToken: token, projectId: acc.projectId, sessionId, longText: LONG_TEXT, userText: "Reply with OK only." });
const r2 = await callAg({ accessToken: token, projectId: acc.projectId, sessionId, longText: LONG_TEXT, userText: "Reply with OK only." });
console.log(`[same-session ${acc.email}] r1: prompt=${r1.promptTokens} cached=${r1.cachedTokens} | r2: prompt=${r2.promptTokens} cached=${r2.cachedTokens}`);
expect(r1.status).toBe(200);
expect(r2.status).toBe(200);
expect(r2.cachedTokens).toBeGreaterThanOrEqual(MIN_CACHE_TOKENS);
}, 60000);
it("different sessionId (same account) β cache still hits (session-independent)", async () => {
const [acc] = conns;
const token = await refreshAccessToken(acc.refreshToken);
const r1 = await callAg({ accessToken: token, projectId: acc.projectId, sessionId: `test-diff-a-${crypto.randomUUID()}`, longText: LONG_TEXT, userText: "Reply with OK only." });
const r2 = await callAg({ accessToken: token, projectId: acc.projectId, sessionId: `test-diff-b-${crypto.randomUUID()}`, longText: LONG_TEXT, userText: "Reply with OK only." });
console.log(`[diff-session ${acc.email}] r1: cached=${r1.cachedTokens} | r2: cached=${r2.cachedTokens}`);
expect(r1.status).toBe(200);
expect(r2.status).toBe(200);
// AG cache is content-based, not session-based β both calls hit
expect(r2.cachedTokens).toBeGreaterThanOrEqual(MIN_CACHE_TOKENS);
}, 60000);
it.skipIf(conns.length < 2)("cross-account β cache SHARED (content-based global cache)", async () => {
const [accA, accB] = conns;
const [tokenA, tokenB] = await Promise.all([
refreshAccessToken(accA.refreshToken),
refreshAccessToken(accB.refreshToken)
]);
// Account A warmup with its own sessionId
const a1 = await callAg({ accessToken: tokenA, projectId: accA.projectId, sessionId: `cross-a-${crypto.randomUUID()}`, longText: LONG_TEXT, userText: "Reply with OK only." });
// Account B with DIFFERENT sessionId β if cache shares across accounts, it still hits
const b1 = await callAg({ accessToken: tokenB, projectId: accB.projectId, sessionId: `cross-b-${crypto.randomUUID()}`, longText: LONG_TEXT, userText: "Reply with OK only." });
console.log(`[cross-account] A cached=${a1.cachedTokens} | B cached=${b1.cachedTokens} (${accA.email} β ${accB.email})`);
expect(a1.status).toBe(200);
expect(b1.status).toBe(200);
// Cache is shared globally across accounts (content-based)
expect(b1.cachedTokens).toBeGreaterThanOrEqual(MIN_CACHE_TOKENS);
}, 90000);
// βββ Codex-style sessionId comparison ββββββββββββββββββββββββββββββββ
// Codex derives sessionId from hash(conversation history), keeping it
// stable per-conversation. Test whether this strategy improves cache
// hit rate vs random sessionId on AG with a fresh unique prompt.
it("codex-style sessionId vs random sessionId on unique prompt", async () => {
const [acc] = conns;
const token = await refreshAccessToken(acc.refreshToken);
// Build a unique conversation so no pre-existing cache can interfere
const uniqueMarker = crypto.randomUUID();
const uniqueLong = `MARKER-${uniqueMarker}. ${LONG_TEXT}`;
const userText = "Reply with OK only.";
// Codex-style: sess_${sha256(systemInstruction + userContent).slice(0,32)}
const hash = crypto.createHash("sha256").update(uniqueLong + "\n" + userText).digest("hex").slice(0, 32);
const codexStyleSessionId = `sess_${hash}`;
const N = 4;
const randomResults = [];
const codexResults = [];
// Strategy A: random sessionId each call
for (let i = 0; i < N; i++) {
const r = await callAg({
accessToken: token, projectId: acc.projectId,
sessionId: `rand-${crypto.randomUUID()}`,
longText: uniqueLong, userText
});
randomResults.push(r);
console.log(`[random call ${i + 1}] cached=${r.cachedTokens}`);
}
// Strategy B: codex-style stable sessionId (same hash for every call)
for (let i = 0; i < N; i++) {
const r = await callAg({
accessToken: token, projectId: acc.projectId,
sessionId: codexStyleSessionId,
longText: uniqueLong, userText
});
codexResults.push(r);
console.log(`[codex call ${i + 1}] cached=${r.cachedTokens}`);
}
const randomHitRate = randomResults.filter(r => r.cachedTokens >= MIN_CACHE_TOKENS).length / N;
const codexHitRate = codexResults.filter(r => r.cachedTokens >= MIN_CACHE_TOKENS).length / N;
console.log(`[summary] randomHitRate=${randomHitRate} codexHitRate=${codexHitRate}`);
randomResults.forEach(r => expect(r.status).toBe(200));
codexResults.forEach(r => expect(r.status).toBe(200));
// No strict comparison β just report. AG cache is session-independent per prior tests.
}, 180000);
it("unique prompt (never seen) β explore when cache starts hitting", async () => {
const [acc] = conns;
const token = await refreshAccessToken(acc.refreshToken);
// Unique marker to guarantee no one has cached this exact prompt before
const uniqueLong = `UNIQUE-${crypto.randomUUID()}. ${LONG_TEXT}`;
const sessionId = `unique-${crypto.randomUUID()}`;
const results = [];
for (let i = 0; i < 5; i++) {
const r = await callAg({ accessToken: token, projectId: acc.projectId, sessionId, longText: uniqueLong, userText: "Reply with OK only." });
results.push(r);
console.log(`[unique-prompt call ${i + 1}] prompt=${r.promptTokens} cached=${r.cachedTokens}`);
}
results.forEach(r => expect(r.status).toBe(200));
// Log whether any call ever hits cache β no strict assertion (exploratory)
const anyHit = results.some(r => r.cachedTokens >= MIN_CACHE_TOKENS);
console.log(`[unique-prompt] any-hit=${anyHit}`);
}, 90000);
});
|