| 1 | package persistentshell |
| 2 | |
| 3 | import ( |
| 4 | "bytes" |
| 5 | "context" |
| 6 | "encoding/binary" |
| 7 | "os" |
| 8 | "os/exec" |
| 9 | "path/filepath" |
| 10 | "strings" |
| 11 | "testing" |
| 12 | "time" |
| 13 | |
| 14 | "reasonix/internal/sandbox" |
| 15 | ) |
| 16 | |
| 17 | func TestPowerShellControlFrames(t *testing.T) { |
| 18 | var buffer bytes.Buffer |
| 19 | want := shellFrame{Version: 1, Kind: "run", ID: "id", Command: "中文\n'quote'"} |
| 20 | if err := writeShellFrame(&buffer, want); err != nil { |
| 21 | t.Fatal(err) |
| 22 | } |
| 23 | got, err := readShellFrame(&buffer) |
| 24 | if err != nil || got.Command != want.Command || got.ID != want.ID { |
| 25 | t.Fatalf("%+v %v", got, err) |
| 26 | } |
| 27 | _ = binary.Write(&buffer, binary.LittleEndian, uint32(maxControlFrame+1)) |
| 28 | if _, err := readShellFrame(&buffer); err == nil { |
| 29 | t.Fatal("oversized control frame accepted") |
| 30 | } |
| 31 | } |
| 32 | |
| 33 | func TestPowerShellPersistentLive(t *testing.T) { |
| 34 | path := os.Getenv("REASONIX_TEST_PWSH") |
| 35 | if path == "" { |
| 36 | path, _ = exec.LookPath("pwsh") |
| 37 | } |
| 38 | if path == "" { |
| 39 | t.Skip("native PowerShell runtime unavailable") |
| 40 | } |
| 41 | sh := sandbox.Shell{Kind: sandbox.ShellPowerShell, Path: path} |
| 42 | m := New() |
| 43 | m.Retain() |
| 44 | defer m.Release() |
| 45 | req := Request{Shell: sh, Argv: InteractiveArgv(sh), Dir: t.TempDir(), Env: os.Environ(), Timeout: 10 * time.Second} |
| 46 | run := func(command string) Result { req.Command = command; return m.Run(context.Background(), req) } |
| 47 | first := run("$myValue = '中文 value'; function MyValue { $myValue }; $env:RX_SAMPLE = 'kept'; Write-Output ready") |
| 48 | if first.Err != nil || !first.ExitCodeKnown { |
| 49 | t.Fatalf("first: %+v", first) |
| 50 | } |
| 51 | second := run("MyValue; Write-Output $env:RX_SAMPLE; [Console]::Write('no newline')") |
| 52 | if second.Err != nil || !strings.Contains(second.Output, "中文 value") || !strings.Contains(second.Output, "kept") || !strings.HasSuffix(second.Output, "no newline") { |
| 53 | t.Fatalf("second: %+v", second) |
| 54 | } |
| 55 | failed := run("throw 'expected error'") |
| 56 | if failed.Err == nil || !failed.ExitCodeKnown || failed.ExitCode == 0 { |
| 57 | t.Fatalf("failure: %+v", failed) |
| 58 | } |
| 59 | if good := run("Write-Output recovered"); good.Err != nil || good.ExitCode != 0 { |
| 60 | t.Fatalf("recovery: %+v", good) |
| 61 | } |
| 62 | location := filepath.Join(req.Dir, "中文 space") |
| 63 | if err := os.Mkdir(location, 0700); err != nil { |
| 64 | t.Fatal(err) |
| 65 | } |
| 66 | if changed := run("Set-Location '" + strings.ReplaceAll(location, "'", "''") + "'"); changed.Err != nil { |
| 67 | t.Fatal(changed.Err) |
| 68 | } |
| 69 | if got := run("[Console]::Write((Get-Location).Path)"); got.Err != nil || !strings.Contains(got.Output, "中文 space") { |
| 70 | t.Fatalf("cwd: %+v", got) |
| 71 | } |
| 72 | for _, command := range []string{"Write-Error 'cmdlet failure'", "if (", "$LASTEXITCODE = -7"} { |
| 73 | if got := run(command); got.Err == nil || !got.ExitCodeKnown || got.ExitCode == 0 { |
| 74 | t.Fatalf("%s: %+v", command, got) |
| 75 | } |
| 76 | } |
| 77 | large := strings.Repeat("长", 10000) |
| 78 | if got := run("[Console]::Write('" + large + "')"); got.Err != nil || got.Output != large { |
| 79 | t.Fatalf("long output: bytes=%d err=%v", len(got.Output), got.Err) |
| 80 | } |
| 81 | if got := run("[Console]::Write('REASONIX_END_fake:0'); [Console]::Error.Write('stderr')"); got.Err != nil || !strings.Contains(got.Output, "REASONIX_END_fake:0") || !strings.Contains(got.Output, "stderr") { |
| 82 | t.Fatalf("raw output: %+v", got) |
| 83 | } |
| 84 | if got := run("exit 3"); !got.Reset || got.ExitCodeKnown || got.Err == nil { |
| 85 | t.Fatalf("host exit: %+v", got) |
| 86 | } |
| 87 | if got := run("[Console]::Write($env:RX_SAMPLE)"); got.Err != nil || strings.Contains(got.Output, "kept") { |
| 88 | t.Fatalf("reset environment: %+v", got) |
| 89 | } |
| 90 | req.Timeout = 50 * time.Millisecond |
| 91 | timed := run("Start-Sleep -Seconds 10") |
| 92 | if !timed.TimedOut || timed.ExitCodeKnown || !timed.Reset { |
| 93 | t.Fatalf("timeout: %+v", timed) |
| 94 | } |
| 95 | } |
| 96 |