|
| 1 | +import { readdirSync, readFileSync, statSync } from "node:fs"; |
| 2 | +import path from "node:path"; |
| 3 | +import { describe, expect, it } from "vitest"; |
| 4 | + |
| 5 | +/** |
| 6 | + * `chats.messages` is gone: the transcript lives in `chat_messages`, one row per message. |
| 7 | + * |
| 8 | + * TypeScript already catches a reference through the Drizzle schema — the column isn't |
| 9 | + * there, so it doesn't compile. A raw-SQL reference compiles fine and fails at runtime, |
| 10 | + * which is the hole this scan covers. Zero hits today is the point; the test exists so a |
| 11 | + * reintroduction is caught rather than deployed. |
| 12 | + */ |
| 13 | + |
| 14 | +const ROOT = path.resolve(__dirname, "../../.."); |
| 15 | + |
| 16 | +const SCANNED = [ |
| 17 | + "apps/webapp/app", |
| 18 | + "internal-packages/dashboard-agent/src", |
| 19 | + "internal-packages/dashboard-agent-db/src", |
| 20 | +]; |
| 21 | + |
| 22 | +/** A qualified reference to the dropped column, in any of the spellings Postgres accepts. */ |
| 23 | +const QUALIFIED = /"?\bchats"?\s*\.\s*"?messages"?/i; |
| 24 | + |
| 25 | +/** An unqualified one, inside a literal that is plainly SQL against `chats`. */ |
| 26 | +const SQL_LITERAL = /`[^`]*`|"(?:[^"\\\n]|\\.)*"|'(?:[^'\\\n]|\\.)*'/g; |
| 27 | +const SQL_VERB = /\b(select|insert\s+into|update|delete\s+from)\b/i; |
| 28 | +const NAMES_CHATS = /(?<![\w."])chats\b/i; |
| 29 | +// Word-boundary on both sides and no leading `_`, so `chat_messages` is not a hit. |
| 30 | +const BARE_MESSAGES = /(?<![\w."])messages\b/i; |
| 31 | + |
| 32 | +function sourceFiles(dir: string): string[] { |
| 33 | + const found: string[] = []; |
| 34 | + for (const entry of readdirSync(dir)) { |
| 35 | + const full = path.join(dir, entry); |
| 36 | + if (statSync(full).isDirectory()) { |
| 37 | + if (entry === "node_modules" || entry === "drizzle") continue; |
| 38 | + found.push(...sourceFiles(full)); |
| 39 | + continue; |
| 40 | + } |
| 41 | + if (!/\.(ts|tsx)$/.test(entry)) continue; |
| 42 | + // Tests are excluded: they are allowed to name the old column to describe it. |
| 43 | + if (/\.(test|eval|spec)\.tsx?$/.test(entry)) continue; |
| 44 | + found.push(full); |
| 45 | + } |
| 46 | + return found; |
| 47 | +} |
| 48 | + |
| 49 | +function offences(file: string): string[] { |
| 50 | + const text = readFileSync(file, "utf8"); |
| 51 | + const found: string[] = []; |
| 52 | + |
| 53 | + for (const [index, line] of text.split("\n").entries()) { |
| 54 | + if (QUALIFIED.test(line)) |
| 55 | + found.push(`${path.relative(ROOT, file)}:${index + 1} ${line.trim()}`); |
| 56 | + } |
| 57 | + |
| 58 | + for (const literal of text.match(SQL_LITERAL) ?? []) { |
| 59 | + if (!SQL_VERB.test(literal) || !NAMES_CHATS.test(literal)) continue; |
| 60 | + if (!BARE_MESSAGES.test(literal)) continue; |
| 61 | + found.push(`${path.relative(ROOT, file)} (sql literal) ${literal.slice(0, 120)}`); |
| 62 | + } |
| 63 | + |
| 64 | + return found; |
| 65 | +} |
| 66 | + |
| 67 | +describe("the dropped chats.messages column", () => { |
| 68 | + it("is not referenced by any production source, including in raw SQL", () => { |
| 69 | + const files = SCANNED.flatMap((dir) => sourceFiles(path.join(ROOT, dir))); |
| 70 | + // A scan that found nothing to read would pass vacuously. |
| 71 | + expect(files.length).toBeGreaterThan(200); |
| 72 | + |
| 73 | + expect(files.flatMap(offences)).toEqual([]); |
| 74 | + }); |
| 75 | +}); |
0 commit comments