mirror of
https://github.com/tiennm99/miti99bot.git
synced 2026-04-28 12:20:42 +00:00
4acc471f6f
Doantu now mirrors semantle's pre-Workers-AI shape: a thin fetch wrapper around /random + /similarity on https://phow2sim.sg.miti99.com (overridable via PHOW2SIM_API_URL). Drops the local Viet22K wordlist + build script — the service owns vocabulary now. Promotes commands from protected to public so they show up in Telegram's native / menu.
179 lines
6.1 KiB
JavaScript
179 lines
6.1 KiB
JavaScript
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
import { UpstreamError, createClient } from "../../../src/modules/doantu/api-client.js";
|
|
|
|
describe("doantu/api-client", () => {
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
describe("UpstreamError", () => {
|
|
it("stores status and body metadata", () => {
|
|
const err = new UpstreamError("test", { status: 404, body: "not found" });
|
|
expect(err.message).toBe("test");
|
|
expect(err.status).toBe(404);
|
|
expect(err.body).toBe("not found");
|
|
expect(err.name).toBe("UpstreamError");
|
|
});
|
|
|
|
it("stores cause when provided", () => {
|
|
const cause = new Error("underlying");
|
|
const err = new UpstreamError("wrapper", { cause });
|
|
expect(err.cause).toBe(cause);
|
|
});
|
|
});
|
|
|
|
describe("createClient", () => {
|
|
it("randomWord builds correct URL with filters", async () => {
|
|
const client = createClient("https://api.test", { timeoutMs: 100 });
|
|
global.fetch = vi.fn((url) => {
|
|
expect(url).toContain("/random");
|
|
expect(url).toContain("min_len=2");
|
|
return Promise.resolve({
|
|
ok: true,
|
|
text: () => Promise.resolve('{"word":"chó"}'),
|
|
});
|
|
});
|
|
const res = await client.randomWord({ min_len: 2 });
|
|
expect(res.word).toBe("chó");
|
|
});
|
|
|
|
it("similarity builds URL with both words", async () => {
|
|
const client = createClient("https://api.test", { timeoutMs: 100 });
|
|
global.fetch = vi.fn((url) => {
|
|
expect(url).toContain("/similarity");
|
|
expect(url).toMatch(/a=ch%C3%B3/);
|
|
expect(url).toMatch(/b=m%C3%A8o/);
|
|
return Promise.resolve({
|
|
ok: true,
|
|
text: () =>
|
|
Promise.resolve(
|
|
'{"a":"chó","b":"mèo","in_vocab_a":true,"in_vocab_b":true,"canonical_a":"chó","canonical_b":"mèo","similarity":0.42}',
|
|
),
|
|
});
|
|
});
|
|
const res = await client.similarity("chó", "mèo");
|
|
expect(res.similarity).toBe(0.42);
|
|
expect(res.canonical_b).toBe("mèo");
|
|
});
|
|
|
|
it("URL-encodes multi-syllable Vietnamese guesses", async () => {
|
|
const client = createClient("https://api.test", { timeoutMs: 100 });
|
|
global.fetch = vi.fn((url) => {
|
|
expect(url).toMatch(/b=con\+ch%C3%B3|b=con%20ch%C3%B3/);
|
|
return Promise.resolve({
|
|
ok: true,
|
|
text: () =>
|
|
Promise.resolve(
|
|
'{"a":"mèo","b":"con chó","in_vocab_a":true,"in_vocab_b":true,"similarity":0.3}',
|
|
),
|
|
});
|
|
});
|
|
await client.similarity("mèo", "con chó");
|
|
expect(global.fetch).toHaveBeenCalled();
|
|
});
|
|
|
|
it("throws UpstreamError on non-2xx response", async () => {
|
|
const client = createClient("https://api.test", { timeoutMs: 100 });
|
|
global.fetch = vi.fn(() =>
|
|
Promise.resolve({
|
|
ok: false,
|
|
status: 500,
|
|
text: () => Promise.resolve("Internal Server Error"),
|
|
}),
|
|
);
|
|
await expect(client.randomWord()).rejects.toMatchObject({
|
|
name: "UpstreamError",
|
|
status: 500,
|
|
body: "Internal Server Error",
|
|
});
|
|
});
|
|
|
|
it("throws UpstreamError when response is not valid JSON", async () => {
|
|
const client = createClient("https://api.test", { timeoutMs: 100 });
|
|
global.fetch = vi.fn(() =>
|
|
Promise.resolve({
|
|
ok: true,
|
|
text: () => Promise.resolve("not json at all"),
|
|
}),
|
|
);
|
|
await expect(client.randomWord()).rejects.toMatchObject({
|
|
name: "UpstreamError",
|
|
});
|
|
});
|
|
|
|
it("throws UpstreamError on fetch failure", async () => {
|
|
const client = createClient("https://api.test", { timeoutMs: 100 });
|
|
global.fetch = vi.fn(() => Promise.reject(new Error("network error")));
|
|
await expect(client.randomWord()).rejects.toThrow("phow2sim fetch failed");
|
|
});
|
|
|
|
it("truncates response body to 500 chars on non-OK", async () => {
|
|
const client = createClient("https://api.test", { timeoutMs: 50 });
|
|
const longBody = "x".repeat(600);
|
|
global.fetch = vi.fn(() =>
|
|
Promise.resolve({
|
|
ok: false,
|
|
status: 400,
|
|
text: () => Promise.resolve(longBody),
|
|
}),
|
|
);
|
|
try {
|
|
await client.randomWord();
|
|
} catch (err) {
|
|
expect(err.body.length).toBe(500);
|
|
}
|
|
});
|
|
|
|
it("includes User-Agent header identifying doantu", async () => {
|
|
const client = createClient("https://api.test", { timeoutMs: 100 });
|
|
global.fetch = vi.fn((_, opts) => {
|
|
expect(opts.headers["User-Agent"]).toContain("miti99bot");
|
|
expect(opts.headers["User-Agent"]).toContain("doantu");
|
|
return Promise.resolve({
|
|
ok: true,
|
|
text: () => Promise.resolve('{"word":"chó"}'),
|
|
});
|
|
});
|
|
await client.randomWord();
|
|
});
|
|
|
|
it("includes Accept header", async () => {
|
|
const client = createClient("https://api.test", { timeoutMs: 100 });
|
|
global.fetch = vi.fn((_, opts) => {
|
|
expect(opts.headers.Accept).toBe("application/json");
|
|
return Promise.resolve({
|
|
ok: true,
|
|
text: () => Promise.resolve('{"word":"chó"}'),
|
|
});
|
|
});
|
|
await client.randomWord();
|
|
});
|
|
|
|
it("handles trailing slashes in API base URL", async () => {
|
|
const client = createClient("https://api.test///", { timeoutMs: 100 });
|
|
global.fetch = vi.fn((url) => {
|
|
expect(url.startsWith("https://api.test/")).toBe(true);
|
|
expect(url.startsWith("https://api.test////")).toBe(false);
|
|
return Promise.resolve({
|
|
ok: true,
|
|
text: () => Promise.resolve('{"word":"chó"}'),
|
|
});
|
|
});
|
|
await client.randomWord();
|
|
});
|
|
|
|
it("filters out undefined/null params", async () => {
|
|
const client = createClient("https://api.test", { timeoutMs: 100 });
|
|
global.fetch = vi.fn((url) => {
|
|
expect(url).not.toContain("min_len=");
|
|
expect(url).not.toContain("max_len=");
|
|
return Promise.resolve({
|
|
ok: true,
|
|
text: () => Promise.resolve('{"word":"chó"}'),
|
|
});
|
|
});
|
|
await client.randomWord({ min_len: undefined, max_len: null });
|
|
});
|
|
});
|
|
});
|