diff --git a/src/shared/mock-module-lifecycle-audit.test.ts b/src/shared/mock-module-lifecycle-audit.test.ts index 3f9e00c76..cdc7a453a 100644 --- a/src/shared/mock-module-lifecycle-audit.test.ts +++ b/src/shared/mock-module-lifecycle-audit.test.ts @@ -1,9 +1,10 @@ import { describe, expect, test } from "bun:test" -import { readdir, readFile } from "node:fs/promises" +import { readdir, readFile, stat } from "node:fs/promises" import path from "node:path" import ts from "typescript" const SOURCE_ROOT = path.resolve(import.meta.dir, "..") +const WORKSPACE_ROOT = path.resolve(SOURCE_ROOT, "..") const MOCK_MODULE_TOKEN = "mock.module" const MOCK_MODULE_LIFECYCLE_ALLOWLIST = new Map([ // TODO(MOCK-MODULE-AUDIT): add cleanup for team mailbox inbox module mocks. @@ -79,6 +80,31 @@ async function listTestFiles(directory: string): Promise { return nestedFiles.flat() } +async function listPackageTestFiles(): Promise { + const packagesDir = path.join(WORKSPACE_ROOT, "packages") + let packageNames: string[] = [] + try { + packageNames = await readdir(packagesDir) + } catch { + return [] + } + + const nestedFiles = await Promise.all(packageNames.map(async (name) => { + const packageSrc = path.join(packagesDir, name, "src") + try { + const s = await stat(packageSrc) + if (!s.isDirectory()) { + return [] + } + } catch { + return [] + } + return listTestFiles(packageSrc) + })) + + return nestedFiles.flat() +} + function relativeSourcePath(filePath: string): string { return path.relative(SOURCE_ROOT, filePath) } @@ -177,7 +203,7 @@ function hasCleanupPattern(sourceFile: ts.SourceFile): boolean { describe("mock.module lifecycle hygiene", () => { test("#given test files using mock.module #when audited #then each must pair with cleanup", async () => { // given - const files = await listTestFiles(SOURCE_ROOT) + const files = [...await listTestFiles(SOURCE_ROOT), ...await listPackageTestFiles()] const offenders: string[] = [] // when diff --git a/src/shared/opencode-coupling-audit.test.ts b/src/shared/opencode-coupling-audit.test.ts new file mode 100644 index 000000000..e05a47257 --- /dev/null +++ b/src/shared/opencode-coupling-audit.test.ts @@ -0,0 +1,108 @@ +import { describe, expect, test } from "bun:test" +import { readdir, readFile } from "node:fs/promises" +import path from "node:path" + +const WORKSPACE_ROOT = path.resolve(import.meta.dir, "../..") +const PACKAGES_DIR = path.join(WORKSPACE_ROOT, "packages") +const SKIP_PACKAGES = new Set(["ast-grep-mcp", "lsp-tools-mcp"]) + +const OPENCODE_IMPORT_RE = /from\s+['"](@opencode-ai\/[^'"]+|opencode\/[^'"]+)['"]/ +const BUN_API_RE = /\bBun\.(spawn|file|write|which|hash)\b/ + +async function listPackageSourceFiles(): Promise { + let packageNames: string[] = [] + try { + packageNames = await readdir(PACKAGES_DIR) + } catch { + return [] + } + + const nestedFiles = await Promise.all(packageNames.map(async (name) => { + if (SKIP_PACKAGES.has(name)) { + return [] + } + + const packageSrc = path.join(PACKAGES_DIR, name, "src") + const entries = await listSourceFilesRecursive(packageSrc) + return entries + })) + + return nestedFiles.flat() +} + +async function listSourceFilesRecursive(directory: string): Promise { + let entries: { name: string; isDirectory(): boolean; isFile(): boolean }[] = [] + try { + entries = await readdir(directory, { withFileTypes: true }) + } catch { + return [] + } + + const nestedFiles = await Promise.all(entries.map(async (entry) => { + const entryPath = path.join(directory, entry.name) + if (entry.isDirectory()) { + return listSourceFilesRecursive(entryPath) + } + if ( + entry.isFile() + && entry.name.endsWith(".ts") + && !entry.name.endsWith(".test.ts") + && !entry.name.endsWith(".d.ts") + ) { + return [entryPath] + } + return [] + })) + + return nestedFiles.flat() +} + +function relativeWorkspacePath(filePath: string): string { + return path.relative(WORKSPACE_ROOT, filePath) +} + +describe("package opencode coupling grep gate", () => { + test("#given package source files #when audited #then no file imports from @opencode-ai/* or opencode/", async () => { + // given + const files = await listPackageSourceFiles() + const offenders: string[] = [] + + // when + for (const filePath of files) { + const contents = await readFile(filePath, "utf8") + const lines = contents.split("\n") + for (let i = 0; i < lines.length; i++) { + const line = lines[i] + const match = OPENCODE_IMPORT_RE.exec(line) + if (match) { + offenders.push(`${relativeWorkspacePath(filePath)}:${i + 1} imports ${match[1]}`) + } + } + } + + // then + expect(offenders.sort()).toEqual([]) + }, 20_000) + + test("#given package source files #when audited #then no file uses Bun.spawn, Bun.file, Bun.write, Bun.which, or Bun.hash", async () => { + // given + const files = await listPackageSourceFiles() + const offenders: string[] = [] + + // when + for (const filePath of files) { + const contents = await readFile(filePath, "utf8") + const lines = contents.split("\n") + for (let i = 0; i < lines.length; i++) { + const line = lines[i] + const match = BUN_API_RE.exec(line) + if (match) { + offenders.push(`${relativeWorkspacePath(filePath)}:${i + 1} uses ${match[0]}`) + } + } + } + + // then + expect(offenders.sort()).toEqual([]) + }, 20_000) +}) diff --git a/src/shared/prompt-async-route-audit.test.ts b/src/shared/prompt-async-route-audit.test.ts index fc6f371fc..e1fa6df26 100644 --- a/src/shared/prompt-async-route-audit.test.ts +++ b/src/shared/prompt-async-route-audit.test.ts @@ -1,9 +1,10 @@ import { describe, expect, test } from "bun:test" -import { readdir, readFile } from "node:fs/promises" +import { readdir, readFile, stat } from "node:fs/promises" import path from "node:path" import ts from "typescript" const SOURCE_ROOT = path.resolve(import.meta.dir, "..") +const WORKSPACE_ROOT = path.resolve(SOURCE_ROOT, "..") const PROMPT_GATE_FILE = path.join(SOURCE_ROOT, "shared", "prompt-async-gate.ts") const RAW_PROMPT_ALLOWLIST = new Map([ [ @@ -45,6 +46,31 @@ async function listSourceFiles(directory: string): Promise { return nestedFiles.flat() } +async function listPackageSourceFiles(): Promise { + const packagesDir = path.join(WORKSPACE_ROOT, "packages") + let packageNames: string[] = [] + try { + packageNames = await readdir(packagesDir) + } catch { + return [] + } + + const nestedFiles = await Promise.all(packageNames.map(async (name) => { + const packageSrc = path.join(packagesDir, name, "src") + try { + const s = await stat(packageSrc) + if (!s.isDirectory()) { + return [] + } + } catch { + return [] + } + return listSourceFiles(packageSrc) + })) + + return nestedFiles.flat() +} + function relativeSourcePath(filePath: string): string { return path.relative(SOURCE_ROOT, filePath) } @@ -334,7 +360,7 @@ await dispatchInternalPrompt(options) test("#given production TypeScript sources #when prompt routes are audited #then only the shared gate may call raw OpenCode prompt APIs", async () => { // given - const files = await listSourceFiles(SOURCE_ROOT) + const files = [...await listSourceFiles(SOURCE_ROOT), ...await listPackageSourceFiles()] const offenders: string[] = [] // when @@ -358,7 +384,7 @@ await dispatchInternalPrompt(options) test("#given production TypeScript sources #when prompt gate callers are audited #then callers cannot disable the post-dispatch reservation hold", async () => { // given - const files = await listSourceFiles(SOURCE_ROOT) + const files = [...await listSourceFiles(SOURCE_ROOT), ...await listPackageSourceFiles()] const offenders: string[] = [] // when @@ -375,7 +401,7 @@ await dispatchInternalPrompt(options) test("#given production TypeScript sources #when prompt gate callers are audited #then callers cannot bypass the central prompt queue", async () => { // given - const files = await listSourceFiles(SOURCE_ROOT) + const files = [...await listSourceFiles(SOURCE_ROOT), ...await listPackageSourceFiles()] const offenders: string[] = [] // when @@ -392,7 +418,7 @@ await dispatchInternalPrompt(options) test("#given production TypeScript sources #when prompt gate callers are audited #then every route declares queue behavior explicitly", async () => { // given - const files = await listSourceFiles(SOURCE_ROOT) + const files = [...await listSourceFiles(SOURCE_ROOT), ...await listPackageSourceFiles()] const offenders: string[] = [] // when @@ -413,7 +439,7 @@ await dispatchInternalPrompt(options) test("#given production TypeScript sources #when model-suggestion prompt wrappers are audited #then every retry caller declares queue behavior explicitly", async () => { // given - const files = await listSourceFiles(SOURCE_ROOT) + const files = [...await listSourceFiles(SOURCE_ROOT), ...await listPackageSourceFiles()] const offenders: string[] = [] // when