Skip to content

Commit abf5ff4

Browse files
committed
test(knowledge): add smoke script and OpenAPI contract test
Expose npm run smoke:knowledge for health + openapi + POST /v1/search. Add offline vitest asserting knowledge-v1.yaml includes v1 paths. Made-with: Cursor
1 parent e182815 commit abf5ff4

File tree

3 files changed

+93
-0
lines changed

3 files changed

+93
-0
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
"start": "node dist/index.js",
99
"dev": "tsx --env-file=.env src/index.ts",
1010
"test": "vitest run",
11+
"smoke:knowledge": "node scripts/smoke-knowledge-search.mjs",
1112
"bot": "node --env-file=.env dist/bot/polling.js",
1213
"bot:dev": "tsx --env-file=.env src/bot/polling.ts",
1314
"apply-branding": "tsx --env-file=.env src/bot/apply-branding.ts",

scripts/smoke-knowledge-search.mjs

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
#!/usr/bin/env node
2+
/**
3+
* Primitive smoke test for public knowledge API (behind Caddy).
4+
*
5+
* Env:
6+
* KNOWLEDGE_SMOKE_BASE — default https://spawn-dock.w3voice.net/knowledge
7+
* KNOWLEDGE_SMOKE_TOKEN — optional Bearer (same as API_TOKEN / Telegram /gettoken)
8+
* API_TOKEN — used if KNOWLEDGE_SMOKE_TOKEN unset
9+
*
10+
* After rotating Qwen OAuth on the host/GitHub, redeploy so `search` receives fresh
11+
* `PROD_QWEN_OAUTH_CREDS` / `QWEN_OAUTH_CREDS_B64` in `.env`.
12+
*/
13+
const base = (process.env.KNOWLEDGE_SMOKE_BASE || "https://spawn-dock.w3voice.net/knowledge").replace(
14+
/\/$/,
15+
"",
16+
);
17+
const token = (process.env.KNOWLEDGE_SMOKE_TOKEN || process.env.API_TOKEN || "").trim();
18+
19+
async function mustOk(label, res, previewLen = 500) {
20+
const text = await res.text();
21+
if (!res.ok) {
22+
throw new Error(`${label} HTTP ${res.status}: ${text.slice(0, previewLen)}`);
23+
}
24+
return text;
25+
}
26+
27+
async function main() {
28+
const healthRes = await fetch(`${base}/api/v1/health`);
29+
await mustOk("GET /api/v1/health", healthRes);
30+
31+
const openapiRes = await fetch(`${base}/openapi.yaml`);
32+
const yaml = await mustOk("GET /openapi.yaml", openapiRes, 200);
33+
if (!yaml.includes("/v1/search")) {
34+
throw new Error("openapi.yaml missing /v1/search");
35+
}
36+
37+
const headers = { "content-type": "application/json" };
38+
if (token) {
39+
headers.authorization = `Bearer ${token}`;
40+
}
41+
42+
const searchRes = await fetch(`${base}/api/v1/search`, {
43+
method: "POST",
44+
headers,
45+
body: JSON.stringify({ query: "SpawnDock TMA", locale: "en" }),
46+
});
47+
const body = await searchRes.text();
48+
if (!searchRes.ok) {
49+
throw new Error(`POST /api/v1/search HTTP ${searchRes.status}: ${body.slice(0, 2000)}`);
50+
}
51+
let json;
52+
try {
53+
json = JSON.parse(body);
54+
} catch {
55+
throw new Error("search response is not JSON");
56+
}
57+
if (typeof json.answer !== "string" || json.answer.length === 0) {
58+
throw new Error('search response missing non-empty "answer"');
59+
}
60+
61+
console.log(
62+
JSON.stringify({
63+
ok: true,
64+
base,
65+
bearer: token ? "yes" : "no",
66+
answerPreview: json.answer.slice(0, 120),
67+
}),
68+
);
69+
}
70+
71+
main().catch((err) => {
72+
console.error(err instanceof Error ? err.message : err);
73+
process.exit(1);
74+
});
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { describe, expect, it } from "vitest";
2+
import { readFileSync } from "node:fs";
3+
import { dirname, resolve } from "node:path";
4+
import { fileURLToPath } from "node:url";
5+
6+
const __dirname = dirname(fileURLToPath(import.meta.url));
7+
8+
/** Offline check: OpenAPI file matches PRD paths (no network). */
9+
describe("knowledge OpenAPI contract", () => {
10+
it("contains v1 health and search paths", () => {
11+
const p = resolve(__dirname, "../../openapi/knowledge-v1.yaml");
12+
const yaml = readFileSync(p, "utf8");
13+
expect(yaml).toContain("/v1/health");
14+
expect(yaml).toContain("/v1/search");
15+
expect(yaml).toContain("servers:");
16+
expect(yaml).toContain("/knowledge/api");
17+
});
18+
});

0 commit comments

Comments
 (0)