Skip to content

Commit e986247

Browse files
test(docs): add comprehensive tests and monitoring for llms.txt endpoints
- Add unit tests for middleware rewrites (llms.txt, llms-full.txt, .md) - Add unit tests for markdown route slug handling - Create synthetic monitoring script with incident.io integration - Add GitHub Actions cron workflow for 5-minute health checks - Configure biome.json to allow console statements in monitoring scripts The monitoring script checks all configured sites and: - Sends Slack alerts on failures - Creates incidents in incident.io when endpoints are down - Auto-resolves incidents when endpoints recover Co-Authored-By: [email protected] <[email protected]>
1 parent 2f26f0e commit e986247

File tree

5 files changed

+555
-1
lines changed

5 files changed

+555
-1
lines changed
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
name: Monitor LLM-friendly Docs Endpoints
2+
3+
on:
4+
schedule:
5+
# Run every 5 minutes
6+
- cron: '*/5 * * * *'
7+
workflow_dispatch: # Allow manual triggering
8+
9+
jobs:
10+
monitor:
11+
runs-on: ubuntu-latest
12+
timeout-minutes: 10
13+
14+
steps:
15+
- name: Checkout code
16+
uses: actions/checkout@v4
17+
18+
- name: Setup Node.js
19+
uses: actions/setup-node@v4
20+
with:
21+
node-version: '20'
22+
23+
- name: Install dependencies
24+
run: |
25+
npm install -g tsx
26+
npm install node-fetch
27+
28+
- name: Run monitoring script
29+
env:
30+
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL_DOCS_INCIDENTS }}
31+
INCIDENT_IO_API_KEY: ${{ secrets.INCIDENT_IO_API_KEY }}
32+
run: |
33+
tsx scripts/monitor/check-llms-md-endpoints.ts
34+
35+
- name: Report status
36+
if: always()
37+
run: |
38+
if [ $? -eq 0 ]; then
39+
echo "✅ All endpoints healthy"
40+
else
41+
echo "❌ Some endpoints failed - alerts sent"
42+
fi

biome.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -381,7 +381,8 @@
381381
"packages/workers/proxy/src/websocket.ts",
382382
"packages/generator-cli/src/**/*.ts",
383383
"packages/commons/visual-editor-server/src/mongodb-client.ts",
384-
"servers/fdr-lambda/src/index.ts"
384+
"servers/fdr-lambda/src/index.ts",
385+
"scripts/monitor/**/*.ts"
385386
],
386387
"linter": {
387388
"rules": {
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { NextRequest } from "next/server";
2+
import { describe, expect, it, vi } from "vitest";
3+
4+
describe("markdown route slug handling", () => {
5+
const createMockRequest = (pathname: string, searchParams: Record<string, string> = {}) => {
6+
const url = new URL(`https://example.com${pathname}`);
7+
Object.entries(searchParams).forEach(([key, value]) => {
8+
url.searchParams.set(key, value);
9+
});
10+
return new NextRequest(url);
11+
};
12+
13+
it("should prefer slug from search params over pathname", () => {
14+
const request = createMockRequest("/api/fern-docs/markdown", { slug: "docs/quickstart" });
15+
16+
const slugParam = request.nextUrl.searchParams.get("slug");
17+
const slug = slugParam ?? request.nextUrl.pathname.replace(/\.(md|mdx)$/, "");
18+
19+
expect(slug).toBe("docs/quickstart");
20+
});
21+
22+
it("should fallback to pathname parsing when slug param is not present", () => {
23+
const request = createMockRequest("/docs/quickstart.md");
24+
25+
const slugParam = request.nextUrl.searchParams.get("slug");
26+
const slug = slugParam ?? request.nextUrl.pathname.replace(/\.(md|mdx)$/, "");
27+
28+
expect(slug).toBe("/docs/quickstart");
29+
});
30+
31+
it("should handle .mdx extension in pathname fallback", () => {
32+
const request = createMockRequest("/docs/quickstart.mdx");
33+
34+
const slugParam = request.nextUrl.searchParams.get("slug");
35+
const slug = slugParam ?? request.nextUrl.pathname.replace(/\.(md|mdx)$/, "");
36+
37+
expect(slug).toBe("/docs/quickstart");
38+
});
39+
40+
it("should handle nested paths in slug param", () => {
41+
const request = createMockRequest("/api/fern-docs/markdown", {
42+
slug: "learn/sdks/overview/quickstart"
43+
});
44+
45+
const slugParam = request.nextUrl.searchParams.get("slug");
46+
const slug = slugParam ?? request.nextUrl.pathname.replace(/\.(md|mdx)$/, "");
47+
48+
expect(slug).toBe("learn/sdks/overview/quickstart");
49+
});
50+
51+
it("should handle empty slug param", () => {
52+
const request = createMockRequest("/api/fern-docs/markdown", { slug: "" });
53+
54+
const slugParam = request.nextUrl.searchParams.get("slug");
55+
const slug = slugParam ?? request.nextUrl.pathname.replace(/\.(md|mdx)$/, "");
56+
57+
expect(slug).toBe("");
58+
});
59+
});
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import { NextRequest } from "next/server";
2+
import { describe, expect, it } from "vitest";
3+
4+
describe("middleware rewrites", () => {
5+
const createMockRequest = (pathname: string, headers: Record<string, string> = {}) => {
6+
const url = `https://example.com${pathname}`;
7+
return new NextRequest(url, {
8+
headers: new Headers(headers)
9+
});
10+
};
11+
12+
describe("llms.txt routing", () => {
13+
it("should rewrite /llms.txt to /fern-docs/llms.txt with empty slug", async () => {
14+
const { middleware } = await import("./middleware");
15+
const request = createMockRequest("/llms.txt");
16+
const response = await middleware(request);
17+
18+
expect(response.headers.get("x-middleware-rewrite")).toContain("/fern-docs/llms.txt");
19+
});
20+
21+
it("should rewrite /docs/llms.txt to /fern-docs/llms.txt with slug=docs", async () => {
22+
const { middleware } = await import("./middleware");
23+
const request = createMockRequest("/docs/llms.txt");
24+
const response = await middleware(request);
25+
26+
expect(response.headers.get("x-middleware-rewrite")).toContain("/fern-docs/llms.txt");
27+
expect(response.headers.get("x-middleware-rewrite")).toContain("slug=docs");
28+
});
29+
30+
it("should rewrite /foo/bar/llms.txt to /fern-docs/llms.txt with slug=foo/bar", async () => {
31+
const { middleware } = await import("./middleware");
32+
const request = createMockRequest("/foo/bar/llms.txt");
33+
const response = await middleware(request);
34+
35+
expect(response.headers.get("x-middleware-rewrite")).toContain("/fern-docs/llms.txt");
36+
expect(response.headers.get("x-middleware-rewrite")).toContain("slug=foo%2Fbar");
37+
});
38+
});
39+
40+
describe("llms-full.txt routing", () => {
41+
it("should rewrite /llms-full.txt to /fern-docs/llms-full.txt with empty slug", async () => {
42+
const { middleware } = await import("./middleware");
43+
const request = createMockRequest("/llms-full.txt");
44+
const response = await middleware(request);
45+
46+
expect(response.headers.get("x-middleware-rewrite")).toContain("/fern-docs/llms-full.txt");
47+
});
48+
49+
it("should rewrite /docs/llms-full.txt to /fern-docs/llms-full.txt with slug=docs", async () => {
50+
const { middleware } = await import("./middleware");
51+
const request = createMockRequest("/docs/llms-full.txt");
52+
const response = await middleware(request);
53+
54+
expect(response.headers.get("x-middleware-rewrite")).toContain("/fern-docs/llms-full.txt");
55+
expect(response.headers.get("x-middleware-rewrite")).toContain("slug=docs");
56+
});
57+
});
58+
59+
describe("markdown routing", () => {
60+
it("should rewrite /page.md to /fern-docs/markdown with slug=page", async () => {
61+
const { middleware } = await import("./middleware");
62+
const request = createMockRequest("/page.md");
63+
const response = await middleware(request);
64+
65+
expect(response.headers.get("x-middleware-rewrite")).toContain("/fern-docs/markdown");
66+
expect(response.headers.get("x-middleware-rewrite")).toContain("slug=page");
67+
});
68+
69+
it("should rewrite /page.mdx to /fern-docs/markdown with slug=page", async () => {
70+
const { middleware } = await import("./middleware");
71+
const request = createMockRequest("/page.mdx");
72+
const response = await middleware(request);
73+
74+
expect(response.headers.get("x-middleware-rewrite")).toContain("/fern-docs/markdown");
75+
expect(response.headers.get("x-middleware-rewrite")).toContain("slug=page");
76+
});
77+
78+
it("should rewrite /learn/sdks/overview/quickstart.md to /fern-docs/markdown with correct slug", async () => {
79+
const { middleware } = await import("./middleware");
80+
const request = createMockRequest("/learn/sdks/overview/quickstart.md");
81+
const response = await middleware(request);
82+
83+
expect(response.headers.get("x-middleware-rewrite")).toContain("/fern-docs/markdown");
84+
expect(response.headers.get("x-middleware-rewrite")).toContain("slug=learn%2Fsdks%2Foverview%2Fquickstart");
85+
});
86+
});
87+
88+
describe("content negotiation", () => {
89+
it("should rewrite to /fern-docs/llms.txt when Accept header contains text/plain", async () => {
90+
const { middleware } = await import("./middleware");
91+
const request = createMockRequest("/some/page", { accept: "text/plain" });
92+
const response = await middleware(request);
93+
94+
expect(response.headers.get("x-middleware-rewrite")).toContain("/fern-docs/llms.txt");
95+
expect(response.headers.get("x-middleware-rewrite")).toContain("slug=some%2Fpage");
96+
});
97+
98+
it("should rewrite to /fern-docs/llms.txt when Accept header contains text/markdown", async () => {
99+
const { middleware } = await import("./middleware");
100+
const request = createMockRequest("/some/page", { accept: "text/markdown" });
101+
const response = await middleware(request);
102+
103+
expect(response.headers.get("x-middleware-rewrite")).toContain("/fern-docs/llms.txt");
104+
expect(response.headers.get("x-middleware-rewrite")).toContain("slug=some%2Fpage");
105+
});
106+
});
107+
108+
describe("legacy /api/fern-docs compatibility", () => {
109+
it("should rewrite /api/fern-docs/llms.txt to /fern-docs/llms.txt", async () => {
110+
const { middleware } = await import("./middleware");
111+
const request = createMockRequest("/api/fern-docs/llms.txt");
112+
const response = await middleware(request);
113+
114+
expect(response.headers.get("x-middleware-rewrite")).toContain("/fern-docs/llms.txt");
115+
});
116+
117+
it("should rewrite /api/fern-docs/markdown to /fern-docs/markdown", async () => {
118+
const { middleware } = await import("./middleware");
119+
const request = createMockRequest("/api/fern-docs/markdown");
120+
const response = await middleware(request);
121+
122+
expect(response.headers.get("x-middleware-rewrite")).toContain("/fern-docs/markdown");
123+
});
124+
});
125+
});

0 commit comments

Comments
 (0)