microsoft/compiler-and-fourslash-tests
> How to write, run, and debug compiler tests and fourslash (LSP) tests in the typescript-go repository. Covers test file formats, directives, markers, baseline management, and the fourslash verification API.
npx skills add https://github.com/microsoft/typescript-go --skill compiler-and-fourslash-tests
This guide covers the complete testing workflow for the typescript-go repository, including compiler tests (type-checking, emit, diagnostics) and fourslash tests (language server features like completions, hover, go-to-definition).
Compiler tests validate the TypeScript compiler's behavior: diagnostics, JavaScript emit, source maps, type/symbol baselines, and more. Each test is a .ts or .tsx file that the test runner compiles, then compares output against stored baselines.
| Path | Purpose |
|------|---------|
| testdata/tests/cases/compiler/ | Regression tests (local to this repo) |
| testdata/tests/cases/conformance/ | Conformance tests (local to this repo) |
| _submodules/TypeScript/tests/cases/compiler/ | Submodule tests from upstream TypeScript |
| _submodules/TypeScript/tests/cases/conformance/ | Submodule conformance tests from upstream |
A compiler test is just a .ts or .tsx file — no Go code needed. Place it in testdata/tests/cases/compiler/ for regression tests or testdata/tests/cases/conformance/<subdir>/ for conformance tests.
// testdata/tests/cases/compiler/myNewTest.ts
const x: number = "hello"; // expect type error
Set compiler options with // @option: value comment directives at the top of the file:
// @target: es2020
// @strict: true
// @declaration: true
// @jsx: react
// @noEmit: true
const x: number = 42;
Use // @filename: directives to define multiple files in one test:
// @target: es2015
// @module: commonjs
// @filename: /src/utils.ts
export function greet(name: string): string {
return `Hello, ${name}`;
}
// @filename: /src/main.ts
import { greet } from "./utils";
const msg: number = greet("world"); // type error
Options can specify multiple comma-separated values to generate separate sub-test configurations:
// @target: es2015, esnext
// @module: commonjs, esnext
// @strict: true, false
export const x = 1;
This generates a sub-test for each combination, with names like myTest.ts (target=es2015,module=commonjs,strict=true).
Note: // @lib: is not variant — commas add additional lib files rather than creating separate test configurations:
// @lib: es2020,dom
Use // @symlink: to create symlinks in the virtual filesystem:
// @symlink: /src -> /node_modules/mylib
// @currentDirectory: /custom/path — Set the working directory// @noImplicitReferences — Don't auto-include referenced filesAlways use npx hereby test to run tests. It ensures a clean state by clearing stale baselines before running, so results are always trustworthy. Trust the results — if hereby test passes, the tests pass.
It's generally best to run all tests — the full suite is very quick and will find issues you didn't realize you were introducing:
npx hereby test # Run ALL tests — recommended, fast, and catches unexpected breakage
If a test fails, the output will include the full test name and package, which you can use to re-run it directly with go test for debugging (see below).
Use go test directly only when you need verbose output for a specific test to debug with print statements. The test output from hereby test will tell you the exact package and test name to use:
go test ./internal/testrunner/ -run 'TestLocal/myNewTest' -v
The test entry points are:
TestLocal — runs tests from testdata/tests/cases/ (both compiler/ and conformance/)TestSubmodule — runs tests from _submodules/TypeScript/tests/cases/ and generates diff baselinesFor each test file, the runner:
// @option:, // @filename:, etc.)error — Verifies diagnostics against .errors.txt baselineoutput — Verifies JavaScript emit against .js baselinesourcemap — Verifies source map outputsourcemap record — Verifies source map recordunion ordering — Validates AST union type orderingsource file parent pointers — Validates AST structure integrityBaselines are the expected output files that test results are compared against.
| Directory | Purpose |
|-----------|---------|
| testdata/baselines/reference/ | Golden/expected baselines (committed to repo) |
| testdata/baselines/local/ | Generated during test runs (not committed) |
| Extension | Content |
|-----------|---------|
| .errors.txt | Diagnostic error messages |
| .js | Emitted JavaScript |
| .d.ts | Declaration output |
| .symbols | Symbol information |
| .types | Type information |
| .sourcemap.txt | Source map output |
| .trace.json | Trace output |
git diff --diff-filter=AM --no-index ./testdata/baselines/reference ./testdata/baselines/local
Important: Only accept baselines immediately after a successful npx hereby test run. The hereby test command clears stale baselines before running, so accepting after it guarantees you're only accepting baselines from the current test run. If you accept without running hereby test first, you risk accepting old/stale baselines from previous runs.
npx hereby test # MUST run this first — clears stale state
npx hereby baseline-accept # Then accept the baselines
The baseline-accept task:
local/ to reference/ (excluding .delete files).delete markers in local/Fourslash tests validate language server (LSP) features: completions, hover/quick info, go-to-definition, find references, rename, code fixes, formatting, and more. They're Go test files that set up TypeScript source with position markers, then verify LSP responses.
| Path | Purpose |
|------|---------|
| internal/fourslash/tests/*.go | Hand-written fourslash tests |
| internal/fourslash/tests/gen/*.go | Auto-generated from upstream TypeScript fourslash tests |
| internal/fourslash/tests/manual/*.go | gen tests migrated to manual with npm run makemanual |
| internal/fourslash/ | Test harness and utilities |
| internal/fourslash/tests/util/ | Shared test constants (DefaultCommitCharacters, etc.) |
Key difference: Generated tests in gen/ use fourslash.SkipIfFailing(t) for tests that are known to not yet work. Hand-written tests should always pass. Tests in manual/ are generated tests that have been migrated and possibly modified — they should not be created from scratch.
Create a Go test file in internal/fourslash/tests/. The file uses the fourslash_test package.
package fourslash_test
import (
"testing"
"github.com/microsoft/typescript-go/internal/fourslash"
"github.com/microsoft/typescript-go/internal/testutil"
)
func TestMyFeature(t *testing.T) {
t.Parallel()
defer testutil.RecoverAndFail(t, "Panic on fourslash test")
const content = `
var x/*marker1*/ = 42;
`
f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content)
defer done()
f.VerifyQuickInfoAt(t, "marker1", "var x: number", "")
}
func TestBasicQuickInfo(t *testing.T) {
t.Parallel()
defer testutil.RecoverAndFail(t, "Panic on fourslash test")
const content = `
/**
* Some var
*/
var someVar/*1*/ = 123;
/**
* Other var
* See {@link someVar}
*/
var otherVar/*2*/ = someVar;
`
f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content)
defer done()
f.VerifyQuickInfoAt(t, "1", "var someVar: number", "Some var")
f.VerifyQuickInfoAt(t, "2", "var otherVar: number",
"Other var\nSee [someVar](file:///basicQuickInfo.ts#4,5-4,12)")
}
func TestBasicEdit(t *testing.T) {
t.Parallel()
defer testutil.RecoverAndFail(t, "Panic on fourslash test")
const content = `export {};
interface Point {
x: number;
y: number;
}
declare const p: Point;
p/*a*/`
f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content)
defer done()
f.GoToMarker(t, "a")
f.Insert(t, ".")
f.GoToEOF(t)
f.VerifyCompletions(t, nil, &fourslash.CompletionsExpectedList{
IsIncomplete: false,
ItemDefaults: &fourslash.CompletionsExpectedItemDefaults{
CommitCharacters: &DefaultCommitCharacters,
},
Items: &fourslash.CompletionsExpectedItems{
Exact: []fourslash.CompletionsExpectedItem{
&lsproto.CompletionItem{
Label: "x",
Kind: new(lsproto.CompletionItemKindField),
SortText: new(string(ls.SortTextLocationPriority)),
},
"y",
},
},
})
}
Markers define cursor positions and text ranges in the test content:
| Syntax | Description | Example |
|--------|-------------|---------|
| /*name*/ | Named position marker | var x/*pos*/ = 1; |
| /*1*/, /*2*/ | Numbered markers | foo(/*1*/, /*2*/) |
| [|text|] | Range marker (selects text) | [|let x: number|] |
Use // @Filename: (capital F) to define multiple files:
const content = `
// @Filename: /src/utils.ts
export function greet(name: string) { return name; }
// @Filename: /src/main.ts
import { greet } from "./utils";
greet(/*marker*/"world");
`
Embed a tsconfig.json file or use directive comments:
const content = `
// @Filename: /tsconfig.json
{ "compilerOptions": { "strict": true, "target": "es2020" } }
// @Filename: /src/test.ts
const x/*1*/ = 42;
`
The fourslash.FourslashTest type (variable f) provides these verification methods:
f.VerifyQuickInfoAt(t, "marker", "var x: number", "documentation text")
f.VerifyBaselineHover(t) // generates baseline file
f.VerifyCompletions(t, "marker", &fourslash.CompletionsExpectedList{
IsIncomplete: false,
ItemDefaults: &fourslash.CompletionsExpectedItemDefaults{
CommitCharacters: &DefaultCommitCharacters,
EditRange: Ignored,
},
Items: &fourslash.CompletionsExpectedItems{
Includes: []fourslash.CompletionsExpectedItem{
&lsproto.CompletionItem{Label: "myVar"},
},
// Or use Exact for exact match:
// Exact: []fourslash.CompletionsExpectedItem{"x", "y"},
},
})
Import the test utilities for shared constants:
import . "github.com/microsoft/typescript-go/internal/fourslash/tests/util"
// Provides: DefaultCommitCharacters, Ignored, CompletionGlobalThisItem, etc.
f.VerifyBaselineGoToDefinition(t) // baseline-based
f.VerifyBaselineGoToTypeDefinition(t)
f.VerifyBaselineGoToImplementation(t)
f.VerifyBaselineFindAllReferences(t)
f.VerifyBaselineRename(t)
f.VerifyNoErrors(t)
f.VerifyErrorExistsBetweenMarkers(t, "start", "end")
f.VerifyBaselineNonSuggestionDiagnostics(t)
f.VerifyBaselineSignatureHelp(t)
f.VerifyNoSignatureHelp(t)
f.GoToMarker(t, "marker") // move cursor to marker position
f.Insert(t, ".") // type text at cursor
f.Backspace(t, 3) // delete 3 characters before cursor
f.DeleteAtCaret(t, 5) // delete 5 characters after cursor
f.Paste(t, "new text") // paste text
f.Replace(t, start, len, "replacement")
f.GoToEOF(t) // move to end of file
f.GoToFile(t, "/src/main.ts") // switch to another file
f.VerifyBaselineDocumentHighlights(t)
f.VerifyBaselineDocumentSymbol(t)
f.VerifyBaselineCallHierarchy(t)
f.VerifyBaselineInlayHints(t)
f.VerifyBaselineSelectionRanges(t)
f.VerifyBaselineClosingTags(t)
f.FormatDocument(t, "/test.ts")
f.VerifyOrganizeImports(t, expectedContent, actionKind, prefs)
# Run ALL tests (recommended — fast, ensures clean state, catches unexpected breakage)
npx hereby test
# For print-debugging a specific test with verbose output
go test ./internal/fourslash/tests -run TestBasicQuickInfo -v
Fourslash tests that use VerifyBaseline* methods generate baselines under:
testdata/baselines/reference/fourslash/<command>/
Where <command> is one of: quickInfo, signatureHelp, goToDefinition, goToType, goToImplementation, findAllReferences, documentHighlights, findRenameLocations, callHierarchy, Code Lenses, Document Symbols, Inlay Hints, etc.
File extensions vary by command:
.baseline — quickInfo, signatureHelp, diagnostics, etc..baseline.jsonc — most other features.baseline.md — auto imports.callHierarchy.txt — call hierarchyAccept baselines the same way as compiler tests — but only after running npx hereby test:
npx hereby test # MUST run first to clear stale baselines
npx hereby baseline-accept
Generated tests (in gen/) are auto-converted from the upstream TypeScript fourslash test suite using the script at internal/fourslash/_scripts/convertFourslash.mts. They:
fourslash.SkipIfFailing(t) for tests that don't pass yetTests in manual/ are gen tests that have been migrated with npm run makemanual. They should not be created from scratch — only use makemanual to move a generated test that needs modification.
Hand-written tests (directly in internal/fourslash/tests/):
SkipIfFailing)| Command | Description |
|---------|-------------|
| npx hereby test | Run all tests (recommended — fast, clears stale state) |
| npx hereby baseline-accept | Accept local baselines as new reference |
| npx hereby format | Format code (uses dprint) |
| npx hereby lint | Run linters (uses golangci-lint) |
testdata/tests/cases/compiler/myTest.ts with test code and directivesnpx hereby testgit diff --diff-filter=AM --no-index ./testdata/baselines/reference ./testdata/baselines/localhereby test): npx hereby baseline-acceptinternal/fourslash/tests/myTest_test.go with the test functionnpx hereby testgit diff --diff-filter=AM --no-index ./testdata/baselines/reference ./testdata/baselines/localhereby test): npx hereby baseline-acceptnpx hereby testgo test ./internal/testrunner/ -run 'TestLocal/failingTest' -vgit diff --diff-filter=AM --no-index ./testdata/baselines/reference ./testdata/baselines/localnpx hereby test again, then accept: npx hereby baseline-acceptIf a test panics without a clear stack trace, run all tests in the package sequentially with verbose mode to identify which test caused the panic:
go test ./internal/testrunner/ -parallel=1 -v
The last test that shows as running before the panic output is the one that caused it.
Take microsoft/compiler-and-fourslash-tests from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.
The instructions reference npx.
Without those the skill loads but fails at the first command.