Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ KEY_ENCRYPTION_SECRET=generate_with_openssl_rand_-base64_32
# Optional Gemini API key path (Google AI Studio)
GEMINI_API_KEY=your_gemini_api_key

# Optional Vertex ADC JSON. Next dotenv and Amplify Hosting env vars truncate
# unquoted multiline JSON to "{", which used to look like Gemini flaking.
# Paste a SINGLE LINE JSON string (or wrap the JSON in single quotes).
# If GEMINI_API_KEY is set, Vertex JSON is unused for chat.
# GOOGLE_APPLICATION_CREDENTIALS_JSON='{"type":"service_account","project_id":"..."}'

# Public deployments: set to true so visitors must supply their own key (pasted BYOK or saved).
# Leave unset or false for local dev with server-side .env keys.
REQUIRE_USER_API_KEYS=false
Expand Down
119 changes: 119 additions & 0 deletions src/__tests__/byok-route-guards.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,125 @@ describe("BYOK-required route guards", () => {
)
})

it("does not count an empty-fail Gemini row as a 4/4 yes-vote", async () => {
delete process.env.REQUIRE_USER_API_KEYS
generateGeminiVerdictWithApiKeyMock.mockResolvedValue(
JSON.stringify({ ...validVerdict, voteSplit: "4/4 unanimous" })
)
const debate: Message[] = [
messages[0],
{
id: "p1",
sender: "perplexity",
displayName: "Perplexity",
content: "Ship a monolith first.",
timestamp: new Date(),
},
{
id: "c1",
sender: "claude",
displayName: "Claude",
content: "Start with a monolith.",
timestamp: new Date(),
},
{
id: "g2",
sender: "gpt",
displayName: "GPT",
content: "A well-structured monolith is enough.",
timestamp: new Date(),
},
{
id: "gm-fail",
sender: "gemini",
displayName: "Gemini",
content: "Gemini couldn't reply this round.",
timestamp: new Date(),
failed: true,
},
]

const response = await consensusPOST(
jsonRequest("/api/consensus", {
messages: debate,
locale: "en",
responseLength: "medium",
}) as never
)

expect(response.status).toBe(200)
await expect(response.json()).resolves.toMatchObject({ voteSplit: "3/3 unanimous" })
const prompt = generateGeminiVerdictWithApiKeyMock.mock.calls[0]?.[0]?.userPrompt as string
expect(prompt).toContain("3 model(s) produced a real reply")
expect(prompt).not.toContain("couldn't reply this round")
})

it("does not count a prior-turn Gemini reply when Gemini empty-fails this turn", async () => {
delete process.env.REQUIRE_USER_API_KEYS
generateGeminiVerdictWithApiKeyMock.mockResolvedValue(
JSON.stringify({ ...validVerdict, voteSplit: "4/4 unanimous" })
)
const debate: Message[] = [
messages[0],
{
id: "gm-old",
sender: "gemini",
displayName: "Gemini",
content: "I answered the first question.",
timestamp: new Date(),
},
{
id: "u2",
sender: "user",
displayName: "You",
content: "What about an MVP?",
timestamp: new Date(),
},
{
id: "p1",
sender: "perplexity",
displayName: "Perplexity",
content: "Ship a monolith first.",
timestamp: new Date(),
},
{
id: "c1",
sender: "claude",
displayName: "Claude",
content: "Start with a monolith.",
timestamp: new Date(),
},
{
id: "g2",
sender: "gpt",
displayName: "GPT",
content: "A well-structured monolith is enough.",
timestamp: new Date(),
},
{
id: "gm-fail",
sender: "gemini",
displayName: "Gemini",
content: "Gemini couldn't reply this round.",
timestamp: new Date(),
failed: true,
},
]

const response = await consensusPOST(
jsonRequest("/api/consensus", {
messages: debate,
locale: "en",
responseLength: "medium",
}) as never
)

expect(response.status).toBe(200)
await expect(response.json()).resolves.toMatchObject({ voteSplit: "3/3 unanimous" })
const prompt = generateGeminiVerdictWithApiKeyMock.mock.calls[0]?.[0]?.userPrompt as string
expect(prompt).toContain("3 model(s) produced a real reply")
})

it("ocr proceeds with the saved Gemini key when BYOK is required", async () => {
authMock.mockResolvedValue({ user: { id: "user-1" } })
getUserProviderApiKeyMock.mockResolvedValue("user-gemini-key")
Expand Down
17 changes: 17 additions & 0 deletions src/__tests__/consensus-resolve.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,23 @@ describe("humanVerdictError", () => {
expect(humanVerdictError(new Error("timed out"), "ko")).toMatch(/오래/)
})

it("does not treat Vertex credentials JSON errors as a bad API key", () => {
expect(
humanVerdictError(
new Error(
"Vertex credentials JSON is invalid or truncated. Put GOOGLE_APPLICATION_CREDENTIALS_JSON on one line or wrap the JSON in single quotes."
)
)
).toMatch(/credentials JSON/i)
expect(
humanVerdictError(
new Error(
"Vertex credentials JSON is invalid or truncated. Put GOOGLE_APPLICATION_CREDENTIALS_JSON on one line or wrap the JSON in single quotes."
)
)
).not.toMatch(/current key/i)
})

it("exports a clear missing-key message", () => {
expect(NO_CONSENSUS_KEY_MESSAGE).toMatch(/API key/i)
})
Expand Down
Loading
Loading