From 31095168d212df0bb2d0a3ebc28819c63a4921eb Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Tue, 23 Jun 2026 12:53:03 +0200 Subject: [PATCH] test(ci): focus macOS PR coverage --- .github/workflows/test.yml | 1 + .../opencode/script/kilocode/test-profile.ts | 99 +++++++++++++++++++ packages/opencode/script/test-runner.ts | 43 +++++++- .../test/kilocode/test-profile.test.ts | 27 +++++ turbo.json | 1 + 5 files changed, 168 insertions(+), 3 deletions(-) create mode 100644 packages/opencode/script/kilocode/test-profile.ts create mode 100644 packages/opencode/test/kilocode/test-profile.test.ts diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 63a404e9a4b..4b59bbb1208 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -81,6 +81,7 @@ jobs: run: bun turbo test:ci --filter='!@kilocode/kilo-jetbrains' env: KILO_EXPERIMENTAL_DISABLE_FILEWATCHER: ${{ runner.os == 'Windows' && 'true' || 'false' }} + KILO_TEST_PROFILE: ${{ runner.os == 'macOS' && github.event_name == 'pull_request' && 'darwin' || '' }} # kilocode_change - name: Run HttpApi exerciser gates if: runner.os == 'Linux' # kilocode_change diff --git a/packages/opencode/script/kilocode/test-profile.ts b/packages/opencode/script/kilocode/test-profile.ts new file mode 100644 index 00000000000..6e69d05ca36 --- /dev/null +++ b/packages/opencode/script/kilocode/test-profile.ts @@ -0,0 +1,99 @@ +export namespace TestProfile { + // Broad globs keep platform coverage maintainable as tests are added or renamed. + // Full macOS runs on main remain the backstop for tests outside these areas. + const profiles = { + darwin: { + description: "Darwin-native process, terminal, filesystem, worktree, and runtime coverage", + groups: { + cli: [ + "cli/acp/*.test.ts", + "cli/install-artifact.test.ts", + "cli/run/{footer.view,run-process,scrollback.surface}.test.{ts,tsx}", + "cli/serve/*.test.ts", + "cli/smokes/*.test.ts", + "cli/tui/{app-lifecycle,dialog-prompt,diff-viewer-file-tree,diff-viewer,inline-tool-wrap-snapshot,keymap,plugin-loader-entrypoint,slot-replace,thread,use-event}.test.{ts,tsx}", + ], + filesystem: [ + "file/{index,path-traversal,ripgrep}.test.ts", + "git/*.test.ts", + "image/*.test.ts", + "plugin/{install-concurrency,loader-shared}.test.ts", + "reference/*.test.ts", + "snapshot/*.test.ts", + "tool/{external-directory,glob,grep,read,repo_clone,repo_overview,shell}.test.ts", + "util/{filesystem,module,process,which}.test.ts", + ], + kilo: [ + "kilocode/{background-process,bin-tree-sitter-env,daemon,external-directory-boundary,indexing-worker,indexing-worktree,mcp-oauth-callback,primary-worktree,snapshot-freeze-repro,snapshot-revert-move,snapshot-seed}.test.ts", + "kilocode/server/{listener-runtime,worktree-list}.test.ts", + "kilocode/session-export/{e2e,sequence,worker,workspace-provider}.test.ts", + "kilocode/session-export/worker/{storage,zstd}.test.ts", + "kilocode/sessions/*.test.ts", + "kilocode/worktree*.test.ts", + ], + process: ["provider/header-timeout.test.ts", "session/{prompt,retry}.test.ts", "shell/*.test.ts"], + project: ["project/*.test.ts"], + pty: ["pty/pty-*.test.ts", "server/httpapi-pty*.test.ts"], + server: [ + "server/{httpapi-compression,httpapi-experimental,httpapi-file,httpapi-listen,httpapi-workspace-routing,project-init-git,workspace-proxy,worktree-endpoint-repro}.test.ts", + ], + }, + }, + } as const + + export const names = Object.keys(profiles) + + export function resolve(name: string, all: readonly string[]) { + const profile = profiles[name as keyof typeof profiles] + if (!profile) { + return { + ok: false as const, + error: `Unknown test profile "${name}". Available profiles: ${names.join(", ")}`, + } + } + + const groups = Object.entries(profile.groups) + const patterns = groups.flatMap(([, patterns]) => patterns) + const malformed = patterns.filter( + (pattern) => + pattern.startsWith("/") || + pattern.startsWith("test/") || + pattern.includes("\\") || + pattern.split("/").includes("..") || + !/\.test\.(ts|tsx|\{ts,tsx\})$/.test(pattern), + ) + const seen = new Set() + const duplicates = patterns.filter((pattern) => { + if (seen.has(pattern)) return true + seen.add(pattern) + return false + }) + const unsorted = groups + .filter(([, patterns]) => + patterns.some((pattern, index) => index > 0 && patterns[index - 1].localeCompare(pattern) > 0), + ) + .map(([group]) => group) + const globs = patterns.map((pattern) => ({ pattern, glob: new Bun.Glob(pattern) })) + const unmatched = globs.filter((item) => !all.some((file) => item.glob.match(file))).map((item) => item.pattern) + const errors = [ + malformed.length > 0 ? `Malformed patterns: ${malformed.join(", ")}` : "", + duplicates.length > 0 ? `Duplicate patterns: ${duplicates.join(", ")}` : "", + unmatched.length > 0 ? `Unmatched patterns: ${unmatched.join(", ")}` : "", + unsorted.length > 0 ? `Unsorted groups: ${unsorted.join(", ")}` : "", + patterns.length === 0 ? "Profile contains no patterns" : "", + ].filter(Boolean) + + if (errors.length > 0) { + return { + ok: false as const, + error: `Invalid test profile "${name}":\n${errors.map((error) => `- ${error}`).join("\n")}`, + } + } + + return { + ok: true as const, + description: profile.description, + files: all.filter((file) => globs.some((item) => item.glob.match(file))), + } + } +} diff --git a/packages/opencode/script/test-runner.ts b/packages/opencode/script/test-runner.ts index 7ee61e1ed9b..04bb0033517 100644 --- a/packages/opencode/script/test-runner.ts +++ b/packages/opencode/script/test-runner.ts @@ -7,6 +7,7 @@ import os from "os" import path from "path" import fs from "fs/promises" +import { TestProfile } from "./kilocode/test-profile" const root = path.resolve(import.meta.dir, "..") const argv = process.argv.slice(2) @@ -29,6 +30,7 @@ if (argv.includes("--help") || argv.includes("-h")) { " --timeout Per-test timeout passed to bun test (default: 60000)", " --file-timeout Per-file process timeout (default: 300000)", " --retries Extra attempts for failing files (default: 1)", + " --profile Run a curated test profile (env: KILO_TEST_PROFILE)", " --bail Stop on first failure", " --verbose Show full output for every file", " -h, --help Show this help", @@ -50,6 +52,15 @@ function opt(name: string, fallback: number) { return i >= 0 && i + 1 < argv.length ? Number(argv[i + 1]) || fallback : fallback } +function text(name: string) { + const i = argv.indexOf(`--${name}`) + if (i < 0) return + const value = argv[i + 1] + if (value && !value.startsWith("-")) return value + console.error(`Missing value for --${name}`) + process.exit(2) +} + const ci = argv.includes("--ci") const bail = argv.includes("--bail") const verbose = argv.includes("--verbose") @@ -60,8 +71,15 @@ const concurrency = opt("concurrency", Math.min(4, os.cpus().length)) const timeout = opt("timeout", 60000) const deadline = opt("file-timeout", 300000) const retries = opt("retries", 1) +const flag = text("profile") +const env = process.env.KILO_TEST_PROFILE?.trim() || undefined +if (flag && env && flag !== env) { + console.error(`Conflicting test profiles: --profile=${flag}, KILO_TEST_PROFILE=${env}`) + process.exit(2) +} +const profile = flag ?? env -const valued = new Set(["--concurrency", "--timeout", "--file-timeout", "--retries"]) +const valued = new Set(["--concurrency", "--timeout", "--file-timeout", "--retries", "--profile"]) const patterns = argv.filter((arg, i) => { if (arg.startsWith("-")) return false if (i > 0 && valued.has(argv[i - 1])) return false @@ -92,9 +110,28 @@ export const skipped = new Set([ "mcp/oauth-browser.test.ts", ]) +const selected = (() => { + if (!profile) return all + const result = TestProfile.resolve(profile, all) + if (!result.ok) { + console.error(result.error) + process.exit(2) + } + const blocked = result.files.filter((file) => skipped.has(file)) + if (blocked.length > 0) { + console.error(`Test profile "${profile}" contains skipped files:\n${blocked.map((file) => `- ${file}`).join("\n")}`) + process.exit(2) + } + console.log(`Using test profile "${profile}": ${result.description} (${result.files.length} files)`) + return result.files +})() const matched = - patterns.length > 0 ? all.filter((f) => patterns.some((p) => f.includes(p) || path.join("test", f).includes(p))) : all -const files = patterns.length > 0 ? matched : matched.filter((f) => !skipped.has(f)) // kilocode_change + patterns.length > 0 + ? selected.filter((file) => + patterns.some((pattern) => file.includes(pattern) || path.join("test", file).includes(pattern)), + ) + : selected +const files = patterns.length > 0 && !profile ? matched : matched.filter((file) => !skipped.has(file)) // kilocode_change if (files.length === 0) { console.log("No test files found") diff --git a/packages/opencode/test/kilocode/test-profile.test.ts b/packages/opencode/test/kilocode/test-profile.test.ts new file mode 100644 index 00000000000..a7b5653f519 --- /dev/null +++ b/packages/opencode/test/kilocode/test-profile.test.ts @@ -0,0 +1,27 @@ +import { describe, expect, test } from "bun:test" +import path from "path" +import { TestProfile } from "../../script/kilocode/test-profile" + +const root = path.resolve(import.meta.dir, "..") +const glob = new Bun.Glob("**/*.test.{ts,tsx}") +const all = (await Array.fromAsync(glob.scan({ cwd: root }))).sort() + +describe("test profiles", () => { + test("darwin profile contains valid test files", () => { + const result = TestProfile.resolve("darwin", all) + expect(result.ok).toBe(true) + if (!result.ok) return + expect(result.files.length).toBeGreaterThan(50) + expect(result.files).toContain("pty/pty-session.test.ts") + expect(result.files).toContain("kilocode/sessions/remote-ws.test.ts") + expect(result.files).toContain("kilocode/sessions/remote-sender.test.ts") + }) + + test("unknown profiles fail with available names", () => { + const result = TestProfile.resolve("unknown", all) + expect(result).toEqual({ + ok: false, + error: 'Unknown test profile "unknown". Available profiles: darwin', + }) + }) +}) diff --git a/turbo.json b/turbo.json index 303b53aa1f5..c0f379a64d3 100644 --- a/turbo.json +++ b/turbo.json @@ -25,6 +25,7 @@ }, "@kilocode/cli#test:ci": { "dependsOn": ["^build"], + "env": ["KILO_TEST_PROFILE"], "outputs": [".artifacts/unit/junit.xml"], "passThroughEnv": ["*"] },