diff --git a/tools/hygiene/check-tick-history-shard-schema.test.ts b/tools/hygiene/check-tick-history-shard-schema.test.ts new file mode 100644 index 000000000..3e8142cbd --- /dev/null +++ b/tools/hygiene/check-tick-history-shard-schema.test.ts @@ -0,0 +1,112 @@ +import { afterAll, beforeAll, describe, expect, test } from "bun:test"; +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { dirname, join } from "node:path"; + +import type { ScanResult } from "./check-tick-history-shard-schema"; + +type ScanOne = (shardPath: string) => ScanResult; + +let TMPDIR: string; +let scanOne: ScanOne; +let priorRepoRoot: string | undefined; + +beforeAll(async () => { + TMPDIR = mkdtempSync(join(tmpdir(), "shard-schema-test-")); + priorRepoRoot = process.env["REPO_ROOT"]; + process.env["REPO_ROOT"] = TMPDIR; + const mod = await import("./check-tick-history-shard-schema"); + scanOne = mod.scanOne; +}); + +afterAll(() => { + if (priorRepoRoot === undefined) delete process.env["REPO_ROOT"]; + else process.env["REPO_ROOT"] = priorRepoRoot; + if (TMPDIR) rmSync(TMPDIR, { recursive: true, force: true }); +}); + +function writeShard(relPath: string, content: string): string { + const full = join(TMPDIR, "docs/hygiene-history/ticks", relPath); + mkdirSync(dirname(full), { recursive: true }); + writeFileSync(full, content); + return full; +} + +const SIX_COLS = "| 2026-05-17T12:34Z | a | b | c | d | e |\n"; + +describe("scanOne", () => { + test("accepts a valid HHMMZ.md shard", () => { + const path = writeShard("2026/05/17/1234Z.md", SIX_COLS); + const result = scanOne(path); + + expect(result.ok).toBe(true); + expect(result.violation).toBeUndefined(); + }); + + test("accepts HHMMZ-.md filename variant", () => { + const path = writeShard("2026/05/17/1234Z-abc123.md", SIX_COLS); + const result = scanOne(path); + + expect(result.ok).toBe(true); + }); + + test("accepts col1 with seconds (HH:MM:SSZ) when minutes match filename", () => { + const content = "| 2026-05-17T12:34:56Z | a | b | c | d | e |\n"; + const path = writeShard("2026/05/17/1234Z.md", content); + const result = scanOne(path); + + expect(result.ok).toBe(true); + }); + + test("flags filename that does not match any schema regex", () => { + const path = writeShard("2026/05/17/foo.md", SIX_COLS); + const result = scanOne(path); + + expect(result.ok).toBe(false); + expect(result.violation).toContain("filename does not match"); + }); + + test("flags col1 date mismatch with path date", () => { + const content = "| 2026-05-18T12:34Z | a | b | c | d | e |\n"; + const path = writeShard("2026/05/17/1234Z.md", content); + const result = scanOne(path); + + expect(result.ok).toBe(false); + expect(result.violation).toContain("does not match path date"); + }); + + test("flags col1 time mismatch with filename time", () => { + const content = "| 2026-05-17T12:35Z | a | b | c | d | e |\n"; + const path = writeShard("2026/05/17/1234Z.md", content); + const result = scanOne(path); + + expect(result.ok).toBe(false); + expect(result.violation).toContain("does not match filename"); + }); + + test("flags first row with fewer than 6 columns (7+ pipes)", () => { + const content = "| 2026-05-17T12:34Z | only one col |\n"; + const path = writeShard("2026/05/17/1234Z.md", content); + const result = scanOne(path); + + expect(result.ok).toBe(false); + expect(result.violation).toContain("pipes"); + }); + + test("flags empty file", () => { + const path = writeShard("2026/05/17/1234Z.md", ""); + const result = scanOne(path); + + expect(result.ok).toBe(false); + expect(result.violation).toBe("file is empty"); + }); + + test("flags col1 missing leading pipe-space-timestamp format", () => { + const content = "|2026-05-17T12:34Z | a | b | c | d | e |\n"; + const path = writeShard("2026/05/17/1234Z.md", content); + const result = scanOne(path); + + expect(result.ok).toBe(false); + expect(result.violation).toContain("col1 must be exactly"); + }); +}); diff --git a/tools/hygiene/check-tick-history-shard-schema.ts b/tools/hygiene/check-tick-history-shard-schema.ts index c08905e86..e539d5d10 100644 --- a/tools/hygiene/check-tick-history-shard-schema.ts +++ b/tools/hygiene/check-tick-history-shard-schema.ts @@ -35,7 +35,7 @@ const HASH_RE = /^(\d{4})(\d{2})Z-[0-9a-f]+$/; const COL1_RE = /^\|\s(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2})?Z)\s\|\s/; const SHARD_PREFIX = "docs/hygiene-history/ticks/"; -interface ScanResult { +export interface ScanResult { path: string; ok: boolean; violation?: string; @@ -49,7 +49,7 @@ function repoRelative(p: string): string { return normalizeToPosix(relative(ROOT, p)); } -function scanOne(shardPath: string): ScanResult { +export function scanOne(shardPath: string): ScanResult { const pathRel = repoRelative(resolve(ROOT, shardPath)); const base = basename(shardPath, ".md");