mirror of
https://github.com/anomalyco/opencode.git
synced 2026-11-14 20:21:39 +03:00
92 lines
3.7 KiB
TypeScript
92 lines
3.7 KiB
TypeScript
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) =>
|
|
/(?<![A-Za-z\u00C0-\u024F])Zen(?![A-Za-z\u00C0-\u024F])/u.test(text) || /opencode[\s-]+zen/i.test(text)
|
|
|
|
// Each entry is a file, and the text in it that may keep the old name, with the reason.
|
|
const allowed: { file: string; text?: string; reason: string }[] = [
|
|
{ file: "src/lib/github-snapshot.json", reason: "release notes from GitHub, which are history" },
|
|
{
|
|
file: "src/routes/zen/index.tsx",
|
|
reason: "the old /zen page, which redirects to /console/models (src/middleware.ts); its testimonials are quotes",
|
|
},
|
|
{
|
|
file: "src/lib/voice-answers.ts",
|
|
text: "opencode zen",
|
|
reason: "a question the voice assistant matches, so people who ask for the old name get the answer",
|
|
},
|
|
]
|
|
|
|
const isAllowed = (file: string, 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([])
|
|
})
|
|
})
|