import { describe, expect, test } from "bun:test" import path from "node:path" import { Glob } from "bun" import ts from "typescript" /* * The pay-as-you-go models product is "OpenCode Models". Its old name, "Zen" or "OpenCode Zen", must * not come back into the site's copy. This reads the text a reader can see (string literals, template * text and JSX text in src, the site's JSON data and the public text files) and fails on the old name. * Code identifiers (ZenData, zenApiKey), i18n keys ("zen.faq.q1"), the /zen redirect and the * zen/v1 API paths are lowercase or part of a longer word, so they don't match. */ const root = path.join(import.meta.dir, "..") // "Zen" as a word with a capital, or "OpenCode Zen" in any case. Lowercase "zen" alone is an identifier or a path. const matches = (text: string) => /(? allowed.some((entry) => entry.file === file && (entry.text === undefined || text.includes(entry.text))) function visibleText(file: string, source: string) { const sourceFile = ts.createSourceFile(file, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX) const texts: string[] = [] const visit = (node: ts.Node) => { if ( ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node) || ts.isTemplateHead(node) || ts.isTemplateMiddle(node) || ts.isTemplateTail(node) || ts.isJsxText(node) ) texts.push(node.text) ts.forEachChild(node, visit) } visit(sourceFile) return texts } async function scan(pattern: string, read: (file: string, source: string) => string[]) { // Glob returns backslash paths on Windows; the allowlist uses forward slashes. const files = (await Array.fromAsync(new Glob(pattern).scan({ cwd: root }))).map((file) => file.replaceAll("\\", "/")) const found = await Promise.all( files.map(async (file) => { const source = await Bun.file(path.join(root, file)).text() return read(file, source) .filter((text) => matches(text) && !isAllowed(file, text)) .map((text) => `${file}: ${text.slice(0, 160)}`) }), ) return found.flat() } describe("product name", () => { test("the matcher finds the old name and skips identifiers", () => { expect( ["Zen", "OpenCode Zen", "opencode zen", "Use Zen with", "Zenを使用", "Zen'i", "将 Zen 与"].every(matches), ).toBe(true) expect( ["zen.faq.q1", "/zen", "/zen/v1/chat/completions", "ZenData", "zenApiKey", "Zenburn", "citizen", "frozen"].some( matches, ), ).toBe(false) }) test("site copy in src doesn't use the old name", async () => { expect(await scan("src/**/*.{ts,tsx}", visibleText)).toEqual([]) }) test("the site's JSON data doesn't use the old name", async () => { expect(await scan("src/**/*.json", (_, source) => [source])).toEqual([]) }) test("public text files don't use the old name", async () => { expect(await scan("public/**/*.{txt,md,json,webmanifest,xml}", (_, source) => [source])).toEqual([]) }) })