Files
2026-07-13 13:39:12 +08:00

179 lines
6.5 KiB
TypeScript

import test from "node:test";
import assert from "node:assert/strict";
const BASE_URL = "http://localhost:20128";
test("contract: /api/v1 OPTIONS exposes CORS and allowed methods", async () => {
const { OPTIONS } = await import("../../src/app/api/v1/route.ts");
const response = await OPTIONS();
assert.equal(response.status, 200);
assert.ok(response.headers.has("Access-Control-Allow-Methods"));
});
test("contract: /api/v1/embeddings OPTIONS exposes POST/GET/OPTIONS", async () => {
const { OPTIONS } = await import("../../src/app/api/v1/embeddings/route.ts");
const response = await OPTIONS();
const allowMethods = response.headers.get("Access-Control-Allow-Methods") || "";
assert.equal(response.status, 200);
assert.ok(allowMethods.includes("GET"));
assert.ok(allowMethods.includes("POST"));
assert.ok(allowMethods.includes("OPTIONS"));
});
test("contract: /api/v1 and /api/v1/models return consistent model IDs", async () => {
const [{ GET: getV1 }, { GET: getV1Models }] = await Promise.all([
import("../../src/app/api/v1/route.ts"),
import("../../src/app/api/v1/models/route.ts"),
]);
const [v1Response, v1ModelsResponse] = await Promise.all([
getV1(new Request(`${BASE_URL}/api/v1`, { method: "GET" })),
getV1Models(new Request(`${BASE_URL}/api/v1/models`, { method: "GET" })),
]);
assert.equal(v1Response.status, 200);
assert.equal(v1ModelsResponse.status, 200);
const v1Body = (await v1Response.json()) as any;
const v1ModelsBody = (await v1ModelsResponse.json()) as any;
assert.equal(v1Body.object, "list");
assert.equal(v1ModelsBody.object, "list");
assert.ok(Array.isArray(v1Body.data));
assert.ok(Array.isArray(v1ModelsBody.data));
const v1Ids = [...new Set(v1Body.data.map((item: any) => item.id))].sort();
const v1ModelsIds = [...new Set(v1ModelsBody.data.map((item: any) => item.id))].sort();
assert.deepEqual(v1Ids, v1ModelsIds);
});
test("contract: /api/v1/models returns OpenAI-compatible model shape", async () => {
const { GET: getV1Models } = await import("../../src/app/api/v1/models/route.ts");
const response = await getV1Models(new Request(`${BASE_URL}/api/v1/models`, { method: "GET" }));
assert.equal(response.status, 200);
const body = (await response.json()) as any;
assert.equal(body.object, "list");
assert.ok(Array.isArray(body.data));
// In CI environments without provider connections, models list may be empty — skip shape check
if (body.data.length > 0) {
const first = body.data[0];
assert.equal(typeof first.id, "string");
assert.equal(first.object, "model");
assert.equal(typeof first.created, "number");
assert.equal(typeof first.owned_by, "string");
}
});
test("contract: /api/v1/embeddings GET returns embedding model listing shape", async () => {
const { GET: getEmbeddings } = await import("../../src/app/api/v1/embeddings/route.ts");
const response = await getEmbeddings();
assert.equal(response.status, 200);
const body = (await response.json()) as any;
assert.equal(body.object, "list");
assert.ok(Array.isArray(body.data));
// In CI environments without provider connections, the filtered specialty catalog may be empty.
if (body.data.length > 0) {
const first = body.data[0];
assert.equal(first.object, "model");
assert.equal(first.type, "embedding");
assert.equal(typeof first.id, "string");
assert.equal(typeof first.owned_by, "string");
}
});
test("contract: /api/v1/images/generations GET returns image model listing shape", async () => {
const { GET: getImageModels } = await import("../../src/app/api/v1/images/generations/route.ts");
const response = await getImageModels();
assert.equal(response.status, 200);
const body = (await response.json()) as any;
assert.equal(body.object, "list");
assert.ok(Array.isArray(body.data));
// In CI environments without provider connections, the filtered specialty catalog may be empty.
if (body.data.length > 0) {
const first = body.data[0];
assert.equal(first.object, "model");
assert.equal(first.type, "image");
assert.equal(typeof first.id, "string");
assert.equal(typeof first.owned_by, "string");
}
});
test("contract: /api/v1/messages/count_tokens returns 400 on invalid JSON", async () => {
const { POST: countTokens } = await import("../../src/app/api/v1/messages/count_tokens/route.ts");
const response = await countTokens(
new Request(`${BASE_URL}/api/v1/messages/count_tokens`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: "not-json",
})
);
assert.equal(response.status, 400);
const body = (await response.json()) as any;
assert.ok(body.error, "error payload should exist");
assert.ok(
typeof body.error === "string" || typeof body.error === "object",
"error payload should be string or object"
);
});
test("contract: /api/v1/messages/count_tokens rejects empty messages payload", async () => {
const { POST: countTokens } = await import("../../src/app/api/v1/messages/count_tokens/route.ts");
const response = await countTokens(
new Request(`${BASE_URL}/api/v1/messages/count_tokens`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ messages: [] }),
})
);
assert.equal(response.status, 400);
const body = (await response.json()) as any;
assert.ok(body.error, "error payload should exist");
assert.ok(
typeof body.error === "string" || typeof body.error === "object",
"error payload should be string or object"
);
});
test("contract: /api/v1/messages/count_tokens computes token estimate from text content", async () => {
const { POST: countTokens } = await import("../../src/app/api/v1/messages/count_tokens/route.ts");
const payload = {
messages: [
{ role: "user", content: "abcd" }, // 4 chars
{
role: "assistant",
content: [{ type: "text", text: "12345678" }], // 8 chars
},
],
};
const response = await countTokens(
new Request(`${BASE_URL}/api/v1/messages/count_tokens`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
})
);
assert.equal(response.status, 200);
const body = (await response.json()) as any;
// Real tiktoken count (countTextTokens): "abcd" => 1, "12345678" => 3 (digits split).
// The previous expectation (3) was the old ceil(chars/4) heuristic, replaced by the
// tiktoken-based estimator; the accurate total for this payload is 4.
assert.equal(body.input_tokens, 4);
});