Testing Plugins
Use two layers:
- Go unit tests for pure transform logic.
- End-to-end tests that run
ttscagainst a fixture project.
Go Unit Tests
Keep the core logic callable without CLI flags:
func applyBanner(fileName, text string, config map[string]any) (string, error) {
// pure transform logic
}Test it directly:
func TestApplyBanner(t *testing.T) {
out, err := applyBanner("dist/main.js", "console.log(1);\n", map[string]any{
"text": "x",
})
if err != nil {
t.Fatal(err)
}
if !strings.HasPrefix(out, "/**\n * ----------------------------------------------------------------\n * x\n *\n * @packageDocumentation\n */\n") {
t.Fatalf("missing banner:\n%s", out)
}
}Unit tests should cover:
- happy path;
- invalid config;
- no-op file kinds;
- idempotence;
- AST matching helpers;
- text edit edge cases.
References:
End-to-End Tests
An end-to-end test should:
- copy or create a temporary fixture project;
- link/install the plugin under
node_modules; - run the real
ttsclauncher; - assert on emitted files, diagnostics, and exit code.
Minimal Node test skeleton:
const assert = require("node:assert/strict");
const child_process = require("node:child_process");
const fs = require("node:fs");
const os = require("node:os");
const path = require("node:path");
const test = require("node:test");
const ttscBin = require.resolve("ttsc/lib/launcher/ttsc.js");
test("plugin transforms source before emit", () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "my-plugin-"));
fs.mkdirSync(path.join(root, "src"), { recursive: true });
fs.writeFileSync(path.join(root, "src/main.ts"), `export const x = 1;\n`);
fs.writeFileSync(
path.join(root, "tsconfig.json"),
JSON.stringify({
compilerOptions: {
target: "ES2022",
module: "commonjs",
rootDir: "src",
outDir: "dist",
plugins: [{ transform: "my-plugin" }],
},
include: ["src"],
}),
);
const result = child_process.spawnSync(
process.execPath,
[ttscBin, "--cwd", root, "--emit"],
{ encoding: "utf8", windowsHide: true },
);
assert.equal(result.status, 0, result.stderr);
const js = fs.readFileSync(path.join(root, "dist/main.js"), "utf8");
assert.match(js, /expected transform result/);
});Cache in Tests
Use a shared cache directory per test run:
const cache = fs.mkdtempSync(path.join(os.tmpdir(), "ttsc-plugin-cache-"));
env: {
...process.env,
TTSC_CACHE_DIR: cache
}This cold-builds once and keeps the suite fast.
Use a fresh cache when the test asserts first-build behavior.
What to Assert
For successful builds:
- exit status is
0; - emitted output contains the expected transform result;
- runtime output still works when relevant;
- stderr contains only expected cache-build logs.
For failures:
- exit status is non-zero;
- stderr contains a specific, user-actionable message.
Avoid whole-file snapshots unless the exact output is the contract. Compiler boilerplate can legitimately change.
TypeScript-Go Drift Tests
If your plugin imports a shim symbol, add an end-to-end case that exercises that exact code path. When ttsc bumps TypeScript-Go, a moved symbol should fail in CI, not in a user’s install.
For this repository’s full test matrix and release smoke checks, see Workspace Release.
Go Coverage Audit
Go logic coverage can be audited with:
pnpm run coverage:goThis command is a manual Go coverage gate, separate from pnpm test and CI’s
Run Tests step. It enforces exact 100% block coverage for the runtime Go
logic it measures and fails on profile-generation errors, uncovered blocks, or
invalid coverage-profile merges.
The audit covers behavioral Go packages and fixture backends:
packages/ttsc/cmd/platform,packages/ttsc/cmd/ttsc,packages/ttsc/driver,packages/ttsc/internal/cwd, andpackages/ttsc/utilitypackages/banner/plugin,packages/paths/plugin, andpackages/strip/pluginpackages/lint/linthost(engine internals;packages/lint/plugin/main.gois the thin wrapper)tests/go-transformer
Generated shim re-export files and the shim generator under packages/ttsc/shim
and packages/ttsc/tools/gen_shims are drift-managed code, not part of this
runtime coverage gate.