Skip to content

Add replay test#2826

Merged
gabritto merged 11 commits intomainfrom
gabritto/replay_test
Feb 19, 2026
Merged

Add replay test#2826
gabritto merged 11 commits intomainfrom
gabritto/replay_test

Conversation

@gabritto
Copy link
Member

@gabritto gabritto commented Feb 18, 2026

To debug a replay file obtained from the fuzzer:

  • Edit the "Launch replay test" task in your launch.json with a path to the replay file and the test directory (e.g. /code/replay.txt, code/Project)
  • Launch the task

This PR adds a test + launch task to run the commands present in a replay script obtained from the fuzzer. The test is skipped unless the replay flag is provided.
This is needed for us to be able to conveniently debug problems found by the fuzzer. With Strada, we used to be able to launch tsserver with --inspect-brk=... and then attach to that process for debugging, but IIUC there isn't an equivalent thing in Go.

I refactored the LSP client implementation out of fourslash for reuse in the replay test.

Copilot AI review requested due to automatic review settings February 18, 2026 23:11
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a reusable in-process LSP test client (factored out of the fourslash harness) and adds an opt-in “replay” test to execute message scripts captured from fuzzing, plus a VS Code launch template entry to run that replay test.

Changes:

  • Add internal/testutil/lsptestutil.LSPClient to share LSP request/response plumbing across tests.
  • Refactor fourslash to use the shared LSP client helper.
  • Add cmd/tsgo replay test (skipped unless -replay flag is provided) and a corresponding VS Code launch template.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
internal/testutil/lsptestutil/lspclient.go New shared LSP test client with async routing + typed request/notification helpers.
internal/fourslash/fourslash.go Replace embedded fourslash LSP client plumbing with lsptestutil.LSPClient.
internal/fourslash/statebaseline.go Update state baselining to use the refactored client/server access paths.
cmd/tsgo/replay_test.go New opt-in replay test that reads a replay log and replays LSP messages against an in-process server.
.vscode/launch.template.json Add a launch config for running the replay test.

Comment on lines 72 to 110
scanner := bufio.NewScanner(f)

if !scanner.Scan() {
t.Fatalf("replay file is empty")
}

rootDirPlaceholder := "@PROJECT_ROOT@"
rootDirUriPlaceholder := "@PROJECT_ROOT_URI@"
firstLine := scanner.Bytes()
var initObj initialArguments
err = json.Unmarshal(firstLine, &initObj)
if err != nil {
t.Fatalf("failed to parse initial arguments: %v", err)
}

if initObj.RootDirPlaceholder != "" {
rootDirPlaceholder = initObj.RootDirPlaceholder
}
if initObj.RootDirUriPlaceholder != "" {
rootDirUriPlaceholder = initObj.RootDirUriPlaceholder
}

rootDirReplacer := strings.NewReplacer(
rootDirPlaceholder, *testDir,
rootDirUriPlaceholder, string(testDirUri),
)

var messages []*rawMessage
var id int32 = 1
for scanner.Scan() {
line := scanner.Text()
line = rootDirReplacer.Replace(line)
var rawMsg rawMessage
err := json.Unmarshal([]byte(line), &rawMsg)
if err != nil {
t.Fatalf("failed to parse message: %v", err)
}
messages = append(messages, &rawMsg)
}
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The replay file is read using bufio.Scanner without increasing the buffer size or checking scanner.Err() after the scan loop. Scanner has a default ~64K token limit, so large JSON lines (common for LSP didChange payloads) will fail with ErrTooLong and silently truncate the replay. Set a larger buffer via scanner.Buffer(...) and check scanner.Err() after the loop to fail with a clear error.

Copilot uses AI. Check for mistakes.
Gabriela Araujo Britto added 2 commits February 18, 2026 23:23
@DanielRosenwasser
Copy link
Member

With Strada, we used to be able to launch tsserver with --inspect-brk=... and then attach to that process for debugging, but IIUC there isn't an equivalent thing in Go.

Isn't it possible to run with dlv exec in place of the usual binary path?

I'm guessing the big issue is that tsgo actually starts up Node.js, and it's painful to get the platform-native executable path.

@gabritto
Copy link
Member Author

With Strada, we used to be able to launch tsserver with --inspect-brk=... and then attach to that process for debugging, but IIUC there isn't an equivalent thing in Go.

Isn't it possible to run with dlv exec in place of the usual binary path?

I'm guessing the big issue is that tsgo actually starts up Node.js, and it's painful to get the platform-native executable path.

Yes, but you also want the executable that has been built

With Strada, we used to be able to launch tsserver with --inspect-brk=... and then attach to that process for debugging, but IIUC there isn't an equivalent thing in Go.

Isn't it possible to run with dlv exec in place of the usual binary path?

I'm guessing the big issue is that tsgo actually starts up Node.js, and it's painful to get the platform-native executable path.

You're right, I had forgotten about that, but another problem that was on my mind is that you want a binary that was compiled without optimizations, otherwise it's gonna be a bad time debugging it. In general it seemed simpler to me for the workflow to use a test in our repo, unless we think the fuzzer client is going to evolve into something complicated.

@DanielRosenwasser
Copy link
Member

another problem that was on my mind is that you want a binary that was compiled without optimizations, otherwise it's gonna be a bad time debugging it

Luckily, there is no optimization level - just differing build flags (e.g. nodebug).

@DanielRosenwasser
Copy link
Member

Thinking about this some more, I think it's okay to have two replay scripts - one in the repo for ease of iteration, and one published on npm that's just JS/TS.

@jakebailey
Copy link
Member

jakebailey commented Feb 19, 2026

Luckily, there is no optimization level - just differing build flags (e.g. nodebug).

There is, if you do hereby build:debug or run the test through a launch task.

@DanielRosenwasser
Copy link
Member

I think we are saying the same thing - there's no -O3 vs -O0, but there are times where the build differs based on race-detection, assertion checks, etc., right?

@jakebailey
Copy link
Member

The default build Go does is effectively -O3. Launching with delve (or, building with build:debug) is effectively -O0 -g.

@jakebailey
Copy link
Member

This doesn't really have to be a test, though, since you can use a go run task (like we have for tsgo) that takes args.

@gabritto
Copy link
Member Author

This doesn't really have to be a test, though, since you can use a go run task (like we have for tsgo) that takes args.

Making it not a test would slightly complicate reuse of the LSP client (which takes in a *testing.T).

@jakebailey
Copy link
Member

Making it not a test would slightly complicate reuse of the LSP client (which takes in a *testing.T).

Yep, I wouldn't try and deal with that either. We'd have to introduce a new main package to do this, which is kind of icky, but not impossible within internal or something.

@gabritto gabritto enabled auto-merge February 19, 2026 01:08
@gabritto gabritto added this pull request to the merge queue Feb 19, 2026
Merged via the queue into main with commit dc45b04 Feb 19, 2026
20 checks passed
@gabritto gabritto deleted the gabritto/replay_test branch February 19, 2026 01:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

Comments