Merge pull request #6511 from Kilo-Org/mark/contract-tests

test: add UI contract tests for ToolRegistry, getToolInfo, and DataProvider
This commit is contained in:
Mark IJbema
2026-03-02 18:03:08 +01:00
committed by GitHub
@@ -0,0 +1,113 @@
/**
* Runtime contract tests for kilo-vscode's dependencies on @kilocode/kilo-ui.
*
* These tests import the upstream UI modules directly and verify at runtime
* that the exports kilo-vscode depends on still exist with the expected shape.
*
* Because the upstream modules use SolidJS JSX (jsxImportSource: "solid-js"),
* they must be loaded from within packages/kilo-ui/ where bun picks up the
* correct tsconfig. We use Bun.spawnSync to run a small check script in that
* context.
*
* TypeScript types (OpenFileFn, ToolProps, ToolInfo) are erased at runtime,
* so those are verified via source analysis on the upstream file.
*/
import { describe, it, expect } from "bun:test"
import fs from "node:fs"
import path from "node:path"
const MONOREPO_ROOT = path.resolve(import.meta.dir, "../../../..")
const KILO_UI_DIR = path.join(MONOREPO_ROOT, "packages/kilo-ui")
const DATA_CONTEXT_FILE = path.join(MONOREPO_ROOT, "packages/ui/src/context/data.tsx")
const MESSAGE_PART_FILE = path.join(MONOREPO_ROOT, "packages/ui/src/components/message-part.tsx")
function check(code: string): { ok: boolean; output: string } {
const result = Bun.spawnSync(["bun", "--conditions=browser", "-e", code], {
cwd: KILO_UI_DIR,
stdout: "pipe",
stderr: "pipe",
})
const stdout = result.stdout.toString()
const stderr = result.stderr.toString()
return {
ok: result.exitCode === 0,
output: stdout + stderr,
}
}
/**
* Tool names that kilo-vscode overrides or uses directly.
* Sources:
* - VscodeToolOverrides.tsx: "bash"
* - TaskToolExpanded.tsx: "task"
* - TaskToolExpanded.tsx uses getToolInfo() which handles all of these
*/
const TOOL_NAMES_WE_DEPEND_ON = ["bash", "task", "read", "write", "glob", "edit", "todowrite"]
describe("ToolRegistry tool name contract (runtime)", () => {
it("all tools used by kilo-vscode are registered in ToolRegistry", () => {
const names = JSON.stringify(TOOL_NAMES_WE_DEPEND_ON)
const result = check(`
import { ToolRegistry } from "./src/components/message-part.tsx"
const names = ${names}
const missing = names.filter(n => typeof ToolRegistry.render(n) !== "function")
if (missing.length) {
console.error("Missing tools: " + missing.join(", "))
process.exit(1)
}
console.log("ok")
`)
expect(result.ok, `ToolRegistry check failed: ${result.output}`).toBe(true)
})
})
describe("getToolInfo() export contract (runtime)", () => {
it("getToolInfo is an exported function", () => {
// Note: getToolInfo() calls useI18n() internally, so we cannot invoke it
// outside a SolidJS rendering context. We verify it exists as a function.
const result = check(`
import { getToolInfo } from "./src/components/message-part.tsx"
if (typeof getToolInfo !== "function") {
console.error("getToolInfo is " + typeof getToolInfo)
process.exit(1)
}
console.log("ok")
`)
expect(result.ok, `getToolInfo check failed: ${result.output}`).toBe(true)
})
it("ToolInfo type still declares icon and title fields (source)", () => {
// ToolInfo is a TypeScript type erased at runtime, so we verify via source
const src = fs.readFileSync(MESSAGE_PART_FILE, "utf-8")
expect(src).toMatch(/export type ToolInfo\s*=\s*\{[^}]*icon\s*:/s)
expect(src).toMatch(/export type ToolInfo\s*=\s*\{[^}]*title\s*:/s)
})
})
describe("DataProvider contract (runtime)", () => {
it("DataProvider and useData are exported functions", () => {
const result = check(`
import { DataProvider, useData } from "./src/context/data.tsx"
if (typeof DataProvider !== "function") {
console.error("DataProvider is " + typeof DataProvider)
process.exit(1)
}
if (typeof useData !== "function") {
console.error("useData is " + typeof useData)
process.exit(1)
}
console.log("ok")
`)
expect(result.ok, `DataProvider check failed: ${result.output}`).toBe(true)
})
it("DataProvider accepts onOpenFile prop and exports OpenFileFn (source)", () => {
// onOpenFile and OpenFileFn are kilocode_change additions — TypeScript types
// erased at runtime, so we verify via source analysis
const src = fs.readFileSync(DATA_CONTEXT_FILE, "utf-8")
expect(src).toContain("onOpenFile")
expect(src).toContain("OpenFileFn")
expect(src).toMatch(/openFile:\s*props\.onOpenFile/)
})
})