Files
diegosouzapw--omniroute/tests/unit/qoder-jobtoken-exchange-4683.test.ts
2026-07-13 13:39:12 +08:00

205 lines
8.0 KiB
TypeScript

import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
// #4683: Qoder PAT (`pt-*`) chat requests failed with a Cosy 500 because OmniRoute
// injected the raw `pt-*` PAT into the Cosy `security_oauth_token`. The official
// qodercli uses a TWO-step flow: exchange the PAT for a short-lived `jt-*` job token
// at openapi.qoder.sh/api/v1/jobToken/exchange, then carry the `jt-*` in the Cosy
// envelope. These tests assert the exchange now happens and the `jt-*` is what flows
// downstream — the raw `pt-*` must never be the Cosy token anymore.
const {
parseQoderJobTokenResponse,
exchangeQoderJobToken,
resolveQoderJobToken,
isQoderPatToken,
validateQoderCliPat,
__clearQoderJobTokenCache,
} = await import("../../open-sse/services/qoderCli.ts");
function jsonResponse(body: unknown, init: { ok?: boolean; status?: number } = {}) {
return {
ok: init.ok ?? true,
status: init.status ?? 200,
json: async () => body,
text: async () => (typeof body === "string" ? body : JSON.stringify(body)),
} as unknown as Response;
}
test("#4683 isQoderPatToken only matches pt-* tokens", () => {
assert.equal(isQoderPatToken("pt-abc"), true);
assert.equal(isQoderPatToken("jt-abc"), false);
assert.equal(isQoderPatToken("sk-abc"), false);
assert.equal(isQoderPatToken(""), false);
});
test("#4683 parseQoderJobTokenResponse extracts jt-* across response shapes", () => {
assert.equal(parseQoderJobTokenResponse({ job_token: "jt-1" })?.jobToken, "jt-1");
assert.equal(parseQoderJobTokenResponse({ data: { jobToken: "jt-2" } })?.jobToken, "jt-2");
// expires_in is reported in seconds -> milliseconds.
assert.equal(
parseQoderJobTokenResponse({ job_token: "jt-3", expires_in: 86400 })?.expiresInMs,
86400 * 1000
);
// No jt-* anywhere -> null.
assert.equal(parseQoderJobTokenResponse({ token: "pt-nope" }), null);
assert.equal(parseQoderJobTokenResponse(null), null);
});
test("#4683 exchangeQoderJobToken POSTs the PAT to the exchange endpoint", async () => {
const calls: { url: string; body: unknown }[] = [];
const fetchImpl = async (url: string, init?: Record<string, unknown>) => {
calls.push({ url, body: JSON.parse(String(init?.body ?? "{}")) });
return jsonResponse({ job_token: "jt-from-exchange", expires_in: 86400 });
};
const result = await exchangeQoderJobToken("pt-secret", { fetchImpl });
assert.equal(result?.jobToken, "jt-from-exchange");
assert.equal(calls.length, 1);
assert.match(calls[0].url, /openapi\.qoder\.sh\/api\/v1\/jobToken\/exchange/);
assert.deepEqual(calls[0].body, { personal_token: "pt-secret" });
});
test("#4683 resolveQoderJobToken exchanges a pt-* once and caches the jt-*", async () => {
__clearQoderJobTokenCache();
let fetchCount = 0;
const fetchImpl = async () => {
fetchCount += 1;
return jsonResponse({ job_token: "jt-cached", expires_in: 86400 });
};
const first = await resolveQoderJobToken("pt-x", { fetchImpl, now: 1_000 });
const second = await resolveQoderJobToken("pt-x", { fetchImpl, now: 2_000 });
assert.equal(first, "jt-cached");
assert.equal(second, "jt-cached");
assert.equal(fetchCount, 1, "second resolve must hit the cache, not re-exchange");
__clearQoderJobTokenCache();
});
test("#4683 resolveQoderJobToken coalesces concurrent pt-* exchanges", async () => {
__clearQoderJobTokenCache();
let fetchCount = 0;
let releaseExchange: (() => void) | undefined;
let markExchangeStarted: (() => void) | undefined;
const exchangeStarted = new Promise<void>((resolveStarted) => {
markExchangeStarted = resolveStarted;
});
const fetchImpl = async () => {
fetchCount += 1;
markExchangeStarted?.();
await new Promise<void>((release) => {
releaseExchange = release;
});
return jsonResponse({ job_token: "jt-shared", expires_in: 86400 });
};
const resolves = Array.from({ length: 8 }, () =>
resolveQoderJobToken("pt-concurrent", { fetchImpl, now: 1_000 })
);
await exchangeStarted;
releaseExchange?.();
const tokens = await Promise.all(resolves);
assert.deepEqual(tokens, Array(8).fill("jt-shared"));
assert.equal(fetchCount, 1, "concurrent resolves must share one upstream exchange");
__clearQoderJobTokenCache();
});
test("#4683 resolveQoderJobToken does not let the first caller abort poison shared waiters", async () => {
__clearQoderJobTokenCache();
let fetchCount = 0;
let releaseExchange: (() => void) | undefined;
let markExchangeStarted: (() => void) | undefined;
const exchangeStarted = new Promise<void>((resolveStarted) => {
markExchangeStarted = resolveStarted;
});
const firstCaller = new AbortController();
const fetchImpl = async (_url: string, init?: Record<string, unknown>) => {
fetchCount += 1;
markExchangeStarted?.();
await new Promise<void>((resolve, reject) => {
releaseExchange = resolve;
const signal = init?.signal as AbortSignal | undefined;
signal?.addEventListener("abort", () => reject(signal.reason ?? new Error("aborted")), {
once: true,
});
});
return jsonResponse({ job_token: "jt-unpoisoned", expires_in: 86400 });
};
const first = resolveQoderJobToken("pt-abort-shared", {
fetchImpl,
now: 1_000,
signal: firstCaller.signal,
}).catch((error: unknown) => error);
await exchangeStarted;
const second = resolveQoderJobToken("pt-abort-shared", { fetchImpl, now: 1_000 });
firstCaller.abort(new Error("caller went away"));
releaseExchange?.();
assert.equal(await second, "jt-unpoisoned");
assert.equal(fetchCount, 1, "the unaffected waiter must share the original exchange");
await first;
__clearQoderJobTokenCache();
});
test("#4683 resolveQoderJobToken passes a jt-* through without exchanging", async () => {
__clearQoderJobTokenCache();
let fetchCount = 0;
const fetchImpl = async () => {
fetchCount += 1;
return jsonResponse({ job_token: "jt-unused" });
};
const resolved = await resolveQoderJobToken("jt-already", { fetchImpl });
assert.equal(resolved, "jt-already");
assert.equal(fetchCount, 0);
});
test("#4683 resolveQoderJobToken falls back to the PAT when the exchange fails", async () => {
__clearQoderJobTokenCache();
const fetchImpl = async () => jsonResponse({ error: "nope" }, { ok: false, status: 500 });
const resolved = await resolveQoderJobToken("pt-y", { fetchImpl });
assert.equal(resolved, "pt-y", "graceful fallback keeps prior behavior");
__clearQoderJobTokenCache();
});
// The jobToken exchange helpers above are retained (and independently unit-tested)
// but are no longer on the PAT validation path: the Cosy HTTP protocol moved to a
// WASM-signed envelope that OmniRoute cannot reproduce, so validation now delegates
// to the local qodercli binary. This test guards that new contract — validation
// must NOT hit the (dead) jobToken/Cosy HTTP endpoints.
test("validateQoderCliPat validates via qodercli and makes no Cosy/jobToken HTTP calls", async () => {
__clearQoderJobTokenCache();
const originalFetch = globalThis.fetch;
const prevBin = process.env.CLI_QODER_BIN;
const urls: string[] = [];
// @ts-ignore - test stub
globalThis.fetch = async (url: string) => {
urls.push(String(url));
return jsonResponse({ success: true });
};
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "qodercli-stub-"));
const stub = path.join(dir, "qodercli");
fs.writeFileSync(stub, '#!/bin/sh\nprintf "MODEL\\nAuto\\n"\nexit 0\n', { mode: 0o755 });
process.env.CLI_QODER_BIN = stub;
try {
const res = await validateQoderCliPat({ apiKey: "pt-live" });
assert.equal(res.valid, true);
assert.equal(
urls.some((u) => u.includes("/jobToken/exchange") || u.includes("agent_chat_generation")),
false,
"validation must not call the dead Cosy/jobToken HTTP endpoints"
);
} finally {
globalThis.fetch = originalFetch;
if (prevBin === undefined) delete process.env.CLI_QODER_BIN;
else process.env.CLI_QODER_BIN = prevBin;
fs.rmSync(dir, { recursive: true, force: true });
__clearQoderJobTokenCache();
}
});