[Go] [Stage #QP2] Builtin Command Autocompletion works Locally but fails in tests

Info

Challenge: Build your own shell
Language: Go
Stage: #QP2 - Builtin completions

Context/Issue

I’m currently working on the “Build Your Own Shell” challenge in Go. Locally, my autocompletion works flawlessly, but CodeCrafters’ tests keep failing.

Interestingly, after my latest changes, running codecrafters test --previous started failing. However, everything still works fine in my local environment.

Logs

Output from codecrafters test --previous

[compile] Moved ./.codecrafters/run.sh → ./your_program.sh
[compile] Compilation successful.
Debug = true
[tester::#OO8] Running tests for Stage #OO8 (Print a prompt)
[tester::#OO8] Running ./your_program.sh
[your-program] $ 
[tester::#OO8] ✓ Received prompt
[tester::#OO8] Test passed.
[tester::#CZ2] Running tests for Stage #CZ2 (Handle invalid commands)
[tester::#CZ2] Running ./your_program.sh
[your-program] $ i
[tester::#CZ2] Output does not match expected value.
[tester::#CZ2] Expected: "$ invalid_banana_command"
[tester::#CZ2] Received: "$ i"
[tester::#CZ2] Assertion failed.
[tester::#CZ2] Test failed

Output from codecrafters test

[compile] Moved ./.codecrafters/run.sh → ./your_program.sh
[compile] Compilation successful.
Debug = true
[tester::#QP2] Running tests for Stage #QP2 (Autocompletion - Builtin completion)
[tester::#QP2] Running ./your_program.sh
[tester::#QP2] ✓ Received prompt ($ )
[tester::#QP2] Typed "ech"
[your-program] $ e
[tester::#QP2] Output does not match expected value.
[tester::#QP2] Expected: "$ ech"
[tester::#QP2] Received: "$ e"
[tester::#QP2] Assertion failed.
[tester::#QP2] Test failed

Local Env

Here’s a GIF of it working fine on my local machine:
2025-03-11 10.42.39

Hypothesis

I suspect that setting the terminal to raw mode with:

term.MakeRaw

is causing issues in the test environment. I need raw mode to capture tab keystrokes for autocompletion, but it seems to interfere with CodeCrafters’ testing setup.

If I switch to:

bufio.NewReader

all tests pass, but then I lose the ability to capture \t (tab) and other special characters.

Previously, I attempted using the go-tty library (mattn/go-tty), and ran into the same issue—likely because it also puts the terminal in raw mode. This makes me think the test environment doesn’t handle raw mode properly.

Relevant code

My code is spread across multiple files, so it’s tricky to share everything, but here’s the most relevant snippet:

// readlineWithCompletion: Grabs keystrokes in raw mode to handle special keys.
func (shell *Shell) readlineWithCompletion() (string, error) {
    fd, ok := isTerminal(shell.readIn)
    tty := int(fd.Fd())
    if !ok {
        return bufio.NewReader(shell.readIn).ReadString('\n')
    }

    oldState, err := term.MakeRaw(tty)
    if err != nil {
        return bufio.NewReader(shell.readIn).ReadString('\n')
    }
    defer term.Restore(tty, oldState)

    var input []rune
    for {
        keyPress, _, err := bufio.NewReader(shell.readIn).ReadRune()
        if err != nil {
            return "", err
        }

        switch key(keyPress) {
        case keyEnter:
            fmt.Fprint(shell.writerOut, "\r\n")
            return string(input), nil
        case keyTab:
            currInput := string(input)
            completed := shell.completionHandler.complete(currInput)
            if completed != currInput {
                input = []rune(completed)
                fmt.Fprintf(shell.writerOut, "\r$ %s", completed)
            }
        case keyBackspace:
            if len(input) > 0 {
                input = input[:len(input)-1]
                fmt.Fprint(shell.writerOut, "\b \b")
            }
        case keyCtrlC:
            fmt.Fprintf(shell.writerOut, "^C\r\n")
            return "", nil
        default:
            input = append(input, keyPress)
            fmt.Fprintf(shell.writerOut, "%c", keyPress)
        }
    }
}
// complete: Simplistic prefix-based completion for built-in commands
func (c *completionHandler) complete(input string) string {
    if len(input) == 0 {
        return input
    }
    fields := strings.Fields(input)
    prefix := fields[len(fields)-1]
    var matches []string

    for _, completion := range c.completions {
        if strings.HasPrefix(completion, prefix) {
            matches = append(matches, completion)
        }
    }
    if len(matches) == 0 {
        return input
    }
    return matches[0]
}

Please let me know if you need anything else from me.

Any help on this would be really appreciated!! Thanks in advance!

Note: This seems to be a related topic:
https://forum.codecrafters.io/t/stuck-on-stage-qp2/4294/13

However, I’m already doing the suggested solution.

Hey @dgiridharan, thanks for the detailed explanation!

The issue seems to be caused by creating a new bufio.NewReader for each character read inside the loop:

This resets the reader on every iteration, causing buffered input to be lost and preventing it from maintaining its state properly.

Try moving the creation of bufio.NewReader outside the loop to preserve the buffer. Let me know if that helps!

Wow, thanks for the quick response @andy1li!

Move this line outside of the main loop in shell.Start() fixed the issue and now I’m passing the tests for this stage!

func (shell *Shell) Start() error {
	shell.running = true
	reader := bufio.NewReader(shell.readIn)
	for shell.running {
		// Display prompt
		if _, err := fmt.Fprint(shell.writerOut, "$ "); err != nil {
			return err
		}

		// Wait for user input
		input, err := shell.readlineWithCompletion(reader)
		if err != nil {
			return err
		}

		// Skip empty commands
		input = strings.Trim(input, "\n ")
		if input == "" {
			continue
		}

		// Parse the user input and execute the cmd
		p := parser.NewParser(input)
		cmd, err := p.Parse(shell.exeToPaths)
		if err != nil {
			fmt.Fprintf(shell.writerOut, "Error parsing input: %s - error: %s\n", input, err)
			continue
		}

		// Skip empty cmds
		if cmd == nil {
			continue
		}
		shell.execCmd(cmd)
	}

	return nil
}

However the previous stage tests still fail:

❯ codecrafters test --previous
Initiating test run...
⚡ This is a turbo test run. https://codecrafters.io/turbo
Running tests. Logs should appear shortly...
[compile] Moved ./.codecrafters/run.sh → ./your_program.sh
[compile] Compilation successful.
Debug = true
[tester::#OO8] Running tests for Stage #OO8 (Print a prompt)
[tester::#OO8] Running ./your_program.sh
[your-program] $ 
[tester::#OO8] ✓ Received prompt
[tester::#OO8] Test passed.
[tester::#CZ2] Running tests for Stage #CZ2 (Handle invalid commands)
[tester::#CZ2] Running ./your_program.sh
[your-program] $ invalid_mango_command
[your-program]                        
[tester::#CZ2] Output does not match expected value.
[tester::#CZ2] Expected: "invalid_mango_command: command not found"
[tester::#CZ2] Received: "                       "
[tester::#CZ2] Assertion failed.
[tester::#CZ2] Test failed
View our article on debugging test failures: https://codecrafters.io/debug

As mentioned earlier, this is working locally.

Any ideas?

Thank you again :slight_smile: