Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ jobs:

- run: npx tsc --noEmit

- run: pnpm test

- run: pnpm lint

- run: pnpm build
Expand Down
18 changes: 18 additions & 0 deletions config/paths.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { describe, expect, it } from "vitest";
import { isSafeSlug } from "./paths";

describe("isSafeSlug", () => {
it("accepts normal slugs", () => {
expect(isSafeSlug("yosemite-hike")).toBe(true);
expect(isSafeSlug("010125")).toBe(true);
expect(isSafeSlug("yc-23")).toBe(true);
});

it("rejects traversal and separators", () => {
expect(isSafeSlug("../secrets")).toBe(false);
expect(isSafeSlug("..")).toBe(false);
expect(isSafeSlug("a/b")).toBe(false);
expect(isSafeSlug("a\\b")).toBe(false);
expect(isSafeSlug("a\0b")).toBe(false);
});
});
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
"build": "next build",
"start": "next start",
"lint": "eslint .",
"test": "vitest run",
"test:watch": "vitest",
"format": "prettier --write .",
"format:check": "prettier --check .",
"generate:knowledge-map": "tsx scripts/generateKnowledgeMap.ts",
Expand Down Expand Up @@ -76,7 +78,8 @@
"sharp": "^0.34.5",
"tailwindcss": "^4.3.2",
"tsx": "^4.22.4",
"typescript": "^5.7.3"
"typescript": "^5.7.3",
"vitest": "^4.1.10"
},
"lint-staged": {
"*.{ts,tsx}": [
Expand Down
545 changes: 545 additions & 0 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

19 changes: 19 additions & 0 deletions utils/chunking/embeddingUtils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { describe, expect, it } from "vitest";
import { formatEmbeddingForPostgres, parseEmbedding } from "./embeddingUtils";

describe("embedding round-trip", () => {
it("formats for postgres and parses back", () => {
const vec = [0.1, -0.2, 3];
expect(parseEmbedding(formatEmbeddingForPostgres(vec))).toEqual(vec);
});

it("passes arrays through and parses JSON strings", () => {
expect(parseEmbedding([1, 2])).toEqual([1, 2]);
expect(parseEmbedding("[1,2]")).toEqual([1, 2]);
});

it("returns [] for garbage", () => {
expect(parseEmbedding(null)).toEqual([]);
expect(parseEmbedding(42)).toEqual([]);
});
});
64 changes: 64 additions & 0 deletions utils/content/heatmap.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { describe, expect, it } from "vitest";
import {
buildWeeks,
dateKey,
getMonthLabels,
groupPostsByDate,
wordCountColor,
WORD_COUNT_SCALE,
} from "./heatmap";
import type { PostMetadata } from "@/types/post";

describe("dateKey", () => {
it("zero-pads month and day", () => {
expect(dateKey(new Date(2025, 0, 2))).toBe("2025-01-02");
});
});

describe("groupPostsByDate", () => {
it("groups posts on the same day", () => {
const posts = [
{ date: "2025-01-02T08:00:00" },
{ date: "2025-01-02T20:00:00" },
{ date: "2025-01-03T00:00:00" },
] as PostMetadata[];
const grouped = groupPostsByDate(posts);
expect(grouped["2025-01-02"]).toHaveLength(2);
expect(grouped["2025-01-03"]).toHaveLength(1);
});
});

describe("buildWeeks", () => {
it("always yields full sunday-to-saturday weeks", () => {
const weeks = buildWeeks(2025, {});
expect(weeks.every((w) => w.length === 7)).toBe(true);
expect(weeks[0][0].date.getDay()).toBe(0);
expect(weeks.at(-1)![6].date.getDay()).toBe(6);
});

it("only attaches posts to days inside the year", () => {
// 2024-12-29 is a Sunday in the padding before 2025 starts
const posts = [{ date: "2024-12-29" } as PostMetadata];
const weeks = buildWeeks(2025, { "2024-12-29": posts });
const padded = weeks[0].find((d) => d.dateKey === "2024-12-29");
expect(padded?.posts).toEqual([]);
});
});

describe("getMonthLabels", () => {
it("labels the week containing each month's 1st", () => {
const labels = getMonthLabels(buildWeeks(2025, {}), 2025);
expect([...labels.values()]).toHaveLength(12);
expect(labels.get(0)).toBe("jan");
});
});

describe("wordCountColor", () => {
it("maps word counts to buckets", () => {
const post = (wordcount: number) => ({ wordcount }) as PostMetadata;
expect(wordCountColor([])).toBe(WORD_COUNT_SCALE[0].className);
expect(wordCountColor([post(100)])).toBe(WORD_COUNT_SCALE[1].className);
expect(wordCountColor([post(500)])).toBe(WORD_COUNT_SCALE[2].className);
expect(wordCountColor([post(5000)])).toBe(WORD_COUNT_SCALE[3].className);
});
});
38 changes: 38 additions & 0 deletions utils/content/markdown.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import fs from "fs";
import os from "os";
import path from "path";
import { afterAll, describe, expect, it } from "vitest";
import { readMarkdownFile, scanMarkdownDir } from "./markdown";

const dir = fs.mkdtempSync(path.join(os.tmpdir(), "markdown-test-"));
fs.writeFileSync(
path.join(dir, "hello.md"),
`---\ntitle: hello\ntags: [a, b]\n---\n\nbody text\n`
);
fs.writeFileSync(path.join(dir, "notes.txt"), "not markdown");

afterAll(() => fs.rmSync(dir, { recursive: true, force: true }));

describe("readMarkdownFile", () => {
it("parses frontmatter and content, and is JSON-serializable", () => {
const result = readMarkdownFile(path.join(dir, "hello.md"));
expect(result.data.title).toBe("hello");
expect(result.content.trim()).toBe("body text");
// `orig` (a Uint8Array) must be stripped or client components choke
expect(() => JSON.stringify(result)).not.toThrow();
expect("orig" in result).toBe(false);
});
});

describe("scanMarkdownDir", () => {
it("returns slugs for .md files only", () => {
const files = scanMarkdownDir(dir);
expect(files).toHaveLength(1);
expect(files[0].slug).toBe("hello");
expect(files[0].data.tags).toEqual(["a", "b"]);
});

it("returns [] for a missing directory", () => {
expect(scanMarkdownDir(path.join(dir, "nope"))).toEqual([]);
});
});
38 changes: 38 additions & 0 deletions utils/content/tags.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { describe, expect, it } from "vitest";
import { countTagFrequency, parseTags } from "./tags";
import type { PostMetadata } from "@/types/post";

describe("parseTags", () => {
it("passes arrays through", () => {
expect(parseTags(["a", "b"])).toEqual(["a", "b"]);
});

it("splits comma-separated strings and trims", () => {
expect(parseTags("a, b ,c")).toEqual(["a", "b", "c"]);
});

it("returns [] for missing or unknown shapes", () => {
expect(parseTags(undefined)).toEqual([]);
expect(parseTags(null)).toEqual([]);
expect(parseTags(42)).toEqual([]);
});
});

describe("countTagFrequency", () => {
const post = (tags: string[]) => ({ tags }) as PostMetadata;

it("counts and sorts descending", () => {
const result = countTagFrequency([
post(["ml", "life"]),
post(["ml"]),
post(["ml", "books"]),
]);
expect(result[0]).toEqual(["ml", 3]);
expect(result).toHaveLength(3);
});

it("skips excluded and empty tags", () => {
const result = countTagFrequency([post(["ml", "", "life"])], ["life"]);
expect(result).toEqual([["ml", 1]]);
});
});
30 changes: 30 additions & 0 deletions utils/dateUtils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { describe, expect, it } from "vitest";
import { extractPostDate, toISODateString } from "./dateUtils";

describe("extractPostDate", () => {
it("parses ISO frontmatter dates", () => {
const d = extractPostDate("posts/x.md", { date: "2024-03-15" });
expect(toISODateString(d)).toBe("2024-03-15");
});

it("parses written month names, with and without comma", () => {
expect(
extractPostDate("posts/x.md", { date: "March 15, 2024" }).getMonth()
).toBe(2);
expect(
extractPostDate("posts/x.md", { date: "mar 5 2024" }).getDate()
).toBe(5);
});

it("falls back to MMDDYY filename when frontmatter is missing", () => {
const d = extractPostDate("posts/010225.md", null);
expect(d.getFullYear()).toBe(2025);
expect(d.getMonth()).toBe(0);
expect(d.getDate()).toBe(2);
});

it("falls back to the default date when nothing parses", () => {
const d = extractPostDate("posts/yosemite.md", { date: "not a date" });
expect(d.getFullYear()).toBe(2020);
});
});
13 changes: 13 additions & 0 deletions vitest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { defineConfig } from "vitest/config";
import path from "path";

export default defineConfig({
resolve: {
alias: {
"@": path.resolve(__dirname),
},
},
test: {
include: ["utils/**/*.test.ts", "config/**/*.test.ts"],
},
});
Loading