Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -67,16 +67,33 @@
E2E_CHECKPOINT_STORE: ${{ matrix.checkpoint_store }}
run: mise run test:e2e:canary

test-windows:
runs-on: windows-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version-file: go.mod
# Real-Windows tests must include Windows or MSYS in their top-level
# test name so this job selects them without running POSIX-only harnesses.
- name: Windows unit tests
Comment thread
gtrrz-victor marked this conversation as resolved.
run: go test ./cmd/entire/cli/... -run '(Windows|MSYS)' -count=1 -v

test:

Check warning

Code scanning / CodeQL

Workflow does not contain permissions Medium

Actions job or workflow does not limit the permissions of the GITHUB_TOKEN. Consider setting an explicit permissions block, using the following as a minimal starting point: {contents: read}
runs-on: ubuntu-latest
needs:
- test-core
- test-integration
- test-canary
- test-windows
if: ${{ always() }}
steps:
- name: Check dependencies
run: |
[ "${{ needs.test-core.result }}" = "success" ]
[ "${{ needs.test-integration.result }}" = "success" ]
[ "${{ needs.test-canary.result }}" = "success" ]
[ "${{ needs.test-windows.result }}" = "success" ]
29 changes: 15 additions & 14 deletions cmd/entire/cli/agent/hook_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,9 @@ func WrapWindowsProductionSilentHookCommand(command string) string {

// WrapWindowsProductionJSONWarningHookCommand emits a JSON hook response with a
// systemMessage field on stdout when the Entire CLI is missing from PATH. It
// avoids sh so Codex hooks still work from native Windows shells.
// avoids sh so Codex hooks still work from native Windows shells. Codex already
// runs hook commands through cmd.exe /C, so this JSON-bearing command uses that
// shell directly instead of adding a second quote-parsing layer.
func WrapWindowsProductionJSONWarningHookCommand(command string, format WarningFormat) string {
payload, err := jsonutil.MarshalWithNoHTMLEscape(struct {
SystemMessage string `json:"systemMessage,omitempty"`
Expand All @@ -103,17 +105,17 @@ func WrapWindowsProductionJSONWarningHookCommand(command string, format WarningF
}

return fmt.Sprintf(
`cmd.exe /d /s /c "where.exe entire >nul 2>nul & if errorlevel 1 (echo %s) else (%s)"`,
`where.exe entire >nul 2>nul & if errorlevel 1 (echo %s) else (%s)`,
escapeWindowsCMD(string(payload)),
command,
)
}

// WrapWindowsProductionPlainTextWarningHookCommand emits the warning as plain
// text to stdout when the Entire CLI is missing from PATH.
// WrapWindowsProductionPlainTextWarningHookCommand is the direct-shell fallback
// for WrapWindowsProductionJSONWarningHookCommand when JSON marshaling fails.
func WrapWindowsProductionPlainTextWarningHookCommand(command string, format WarningFormat) string {
return fmt.Sprintf(
`cmd.exe /d /s /c "where.exe entire >nul 2>nul & if errorlevel 1 (echo %s) else (%s)"`,
`where.exe entire >nul 2>nul & if errorlevel 1 (echo %s) else (%s)`,
escapeWindowsCMD(windowsPlainTextWarning(format)),
command,
)
Expand All @@ -130,7 +132,8 @@ func WrapProductionPlainTextWarningHookCommand(command string, format WarningFor
}

const productionHookWrapperPrefix = `sh -c 'if ! command -v entire >/dev/null 2>&1; then `
const windowsProductionHookWrapperPrefix = `cmd.exe /d /s /c "where.exe entire >nul 2>nul & if errorlevel 1 `
const windowsProductionHookWrapperPrefix = `where.exe entire >nul 2>nul & if errorlevel 1 `
const nestedWindowsProductionHookWrapperPrefix = `cmd.exe /d /s /c "where.exe entire >nul 2>nul & if errorlevel 1 `

// IsManagedHookCommand reports whether command is either a direct Entire hook
// command or one of Entire's production wrapper forms that exec that command.
Expand All @@ -146,7 +149,8 @@ func IsManagedHookCommand(command string, prefixes []string) bool {

return hasManagedHookPrefix(wrappedCommand, prefixes)
}
if strings.HasPrefix(command, windowsProductionHookWrapperPrefix) {
if strings.HasPrefix(command, windowsProductionHookWrapperPrefix) ||
strings.HasPrefix(command, nestedWindowsProductionHookWrapperPrefix) {
// The wrapped command lives in the `else (<command>)` branch. Take the
// last ` else (` so a warning string containing the marker can't fool us.
const elseMarker = " else ("
Expand Down Expand Up @@ -174,13 +178,10 @@ func hasManagedHookPrefix(command string, prefixes []string) bool {
// escapeWindowsCMD caret-escapes the cmd.exe block metacharacters that would
// otherwise terminate the `(echo …)` warning block or redirect its output.
//
// `%` is deliberately NOT escaped. These wrappers are a `cmd.exe /d /s /c`
// command line, not a batch script, so batch's `%%` doubling does not apply
// (it would print a literal `%%`), and caret-escaping `%` is not a thing cmd
// recognizes — `^%` would leak the caret. On the command line a lone `%` is
// literal and `%NAME%` only expands for a defined environment variable, so the
// fixed, %-free warning constant is emitted verbatim. If the warning text ever
// gains a `%NAME%` that collides with a real env var, revisit this.
// `%` is deliberately NOT escaped. Hook runners pass these strings to a cmd /c
// command line, not a batch script, so batch's `%%` doubling does not apply,
// and caret-escaping `%` would leak the caret. The fixed warning constants are
// %-free; if that changes, percent expansion needs separate handling.
func escapeWindowsCMD(s string) string {
replacer := strings.NewReplacer(
`^`, `^^`,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,21 @@
package agent

import (
"bytes"
"encoding/json"
"errors"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"syscall"
"testing"
)

// runWindowsWrapper executes a Windows hook wrapper string through cmd.exe and
// returns its stdout and exit code. The wrapper is written verbatim to a .bat
// file (rather than re-quoted into argv) so the test exercises exactly the
// string Entire writes into hooks.json. PATH is scoped so `where.exe entire`
// resolves only when entirePresent is true.
func runWindowsWrapper(t *testing.T, wrapper string, entirePresent bool) (string, int) {
// runWindowsWrapper mirrors Codex's Windows command runner: cmd.exe /C followed
// by the raw, quoted hook command. SysProcAttr.CmdLine is required because
// cmd.exe does not use the standard Windows argv unquoting rules.
func runWindowsWrapper(t *testing.T, wrapper string, entirePresent bool) (string, string, int) {
t.Helper()

sysRoot := os.Getenv("SystemRoot")
Expand All @@ -36,22 +35,28 @@ func runWindowsWrapper(t *testing.T, wrapper string, entirePresent bool) (string
t.Setenv("PATH", strings.Join(pathEntries, ";"))

runDir := t.TempDir()
batPath := filepath.Join(runDir, "run.bat")
if err := os.WriteFile(batPath, []byte(wrapper+"\r\n"), 0o700); err != nil {
t.Fatalf("write run.bat: %v", err)
cmdPath, err := exec.LookPath("cmd.exe")
if err != nil {
t.Fatalf("find cmd.exe: %v", err)
}

cmd := exec.CommandContext(t.Context(), "cmd.exe", "/d", "/s", "/c", batPath)
cmd := exec.CommandContext(t.Context(), cmdPath)
cmd.SysProcAttr = &syscall.SysProcAttr{
CmdLine: `"` + cmdPath + `" /C "` + wrapper + `"`,
}
cmd.Dir = runDir // clean CWD so `where` can't find a stray entire next to us
out, err := cmd.Output()
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err = cmd.Run()
if err != nil {
var exitErr *exec.ExitError
if errors.As(err, &exitErr) {
return string(out), exitErr.ExitCode()
return stdout.String(), stderr.String(), exitErr.ExitCode()
}
t.Fatalf("run wrapper: %v", err)
}
return string(out), 0
return stdout.String(), stderr.String(), 0
}

// TestWindowsWrappers_Execution verifies the cmd.exe wrappers behave correctly
Expand All @@ -60,69 +65,80 @@ func runWindowsWrapper(t *testing.T, wrapper string, entirePresent bool) (string
// (and propagates its exit code) when entire is present, and is skipped with a
// 0 exit when entire is absent, for both the silent and JSON-warning forms.
func TestWindowsWrappers_Execution(t *testing.T) {
if runtime.GOOS != windowsOS {
t.Skip("cmd.exe wrapper execution test runs only on Windows")
}
// No t.Parallel(): t.Setenv("PATH") forbids it.

const marker = "ENTIRE_HOOK_RAN"

t.Run("silent/present runs the command", func(t *testing.T) {
out, code := runWindowsWrapper(t, WrapWindowsProductionSilentHookCommand("echo "+marker), true)
out, stderr, code := runWindowsWrapper(t, WrapWindowsProductionSilentHookCommand("echo "+marker), true)
if !strings.Contains(out, marker) {
t.Fatalf("expected wrapped command to run; stdout=%q", out)
t.Fatalf("expected wrapped command to run; stdout=%q stderr=%q", out, stderr)
}
if code != 0 {
t.Fatalf("expected exit 0, got %d", code)
t.Fatalf("expected exit 0, got %d; stderr=%q", code, stderr)
}
})

t.Run("silent/present propagates the command exit code", func(t *testing.T) {
_, code := runWindowsWrapper(t, WrapWindowsProductionSilentHookCommand("cmd /c exit 7"), true)
_, stderr, code := runWindowsWrapper(t, WrapWindowsProductionSilentHookCommand("cmd /c exit 7"), true)
if code != 7 {
t.Fatalf("expected wrapped command exit code 7 to propagate, got %d", code)
t.Fatalf("expected wrapped command exit code 7 to propagate, got %d; stderr=%q", code, stderr)
}
})

t.Run("silent/absent skips the command and exits 0", func(t *testing.T) {
out, code := runWindowsWrapper(t, WrapWindowsProductionSilentHookCommand("echo "+marker), false)
out, stderr, code := runWindowsWrapper(t, WrapWindowsProductionSilentHookCommand("echo "+marker), false)
if strings.Contains(out, marker) {
t.Fatalf("wrapped command must NOT run when entire absent; stdout=%q", out)
t.Fatalf("wrapped command must NOT run when entire absent; stdout=%q stderr=%q", out, stderr)
}
if code != 0 {
t.Fatalf("expected exit 0 when entire absent, got %d", code)
t.Fatalf("expected exit 0 when entire absent, got %d; stderr=%q", code, stderr)
}
})

t.Run("json/absent emits valid JSON and skips the command", func(t *testing.T) {
out, code := runWindowsWrapper(t, WrapWindowsProductionJSONWarningHookCommand("echo "+marker, WarningFormatSingleLine), false)
out, stderr, code := runWindowsWrapper(t, WrapWindowsProductionJSONWarningHookCommand("echo "+marker, WarningFormatSingleLine), false)
if strings.Contains(out, marker) {
t.Fatalf("wrapped command must NOT run when entire absent; stdout=%q", out)
t.Fatalf("wrapped command must NOT run when entire absent; stdout=%q stderr=%q", out, stderr)
}
if code != 0 {
t.Fatalf("expected exit 0, got %d", code)
t.Fatalf("expected exit 0, got %d; stderr=%q", code, stderr)
}
var payload struct {
SystemMessage string `json:"systemMessage"`
}
if err := json.Unmarshal([]byte(strings.TrimSpace(out)), &payload); err != nil {
t.Fatalf("expected valid JSON on stdout, got %q (err %v)", out, err)
t.Fatalf("expected valid JSON on stdout, got %q stderr=%q (err %v)", out, stderr, err)
}
if !strings.Contains(payload.SystemMessage, "Entire CLI") {
t.Fatalf("unexpected systemMessage: %q", payload.SystemMessage)
}
})

t.Run("json/present runs the command without a warning", func(t *testing.T) {
out, code := runWindowsWrapper(t, WrapWindowsProductionJSONWarningHookCommand("echo "+marker, WarningFormatSingleLine), true)
out, stderr, code := runWindowsWrapper(t, WrapWindowsProductionJSONWarningHookCommand("echo "+marker, WarningFormatSingleLine), true)
if !strings.Contains(out, marker) {
t.Fatalf("expected wrapped command to run; stdout=%q", out)
t.Fatalf("expected wrapped command to run; stdout=%q stderr=%q", out, stderr)
}
if strings.Contains(out, "systemMessage") {
t.Fatalf("warning must NOT be emitted when entire present; stdout=%q", out)
t.Fatalf("warning must NOT be emitted when entire present; stdout=%q stderr=%q", out, stderr)
}
if code != 0 {
t.Fatalf("expected exit 0, got %d", code)
t.Fatalf("expected exit 0, got %d; stderr=%q", code, stderr)
}
})

t.Run("json/present propagates the command exit code", func(t *testing.T) {
out, stderr, code := runWindowsWrapper(
t,
WrapWindowsProductionJSONWarningHookCommand("cmd /c exit 7", WarningFormatSingleLine),
true,
)
if strings.Contains(out, "systemMessage") {
t.Fatalf("warning must NOT be emitted when entire present; stdout=%q stderr=%q", out, stderr)
}
if code != 7 {
t.Fatalf("expected wrapped command exit code 7 to propagate, got %d; stderr=%q", code, stderr)
}
})
}
13 changes: 13 additions & 0 deletions cmd/entire/cli/agent/hook_command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,9 @@ func TestWrapWindowsProductionJSONWarningHookCommand(t *testing.T) {
if strings.Contains(command, "sh -c") {
t.Fatalf("windows wrapper should not use sh, got %q", command)
}
if strings.HasPrefix(command, "cmd.exe ") {
t.Fatalf("windows JSON wrapper should use Codex's existing cmd.exe shell, got %q", command)
}
if !strings.Contains(command, "where.exe entire") {
t.Fatalf("windows wrapper missing PATH guard, got %q", command)
}
Expand All @@ -109,6 +112,9 @@ func TestWrapWindowsProductionSilentHookCommand(t *testing.T) {
if strings.Contains(command, "sh -c") {
t.Fatalf("windows wrapper should not use sh, got %q", command)
}
if !strings.HasPrefix(command, "cmd.exe ") {
t.Fatalf("silent windows wrapper should retain its explicit cmd.exe shell, got %q", command)
}
if !strings.Contains(command, "where.exe entire") {
t.Fatalf("windows wrapper missing PATH guard, got %q", command)
}
Expand All @@ -125,6 +131,9 @@ func TestWrapWindowsProductionPlainTextWarningHookCommandUsesSingleLineWarning(t

command := WrapWindowsProductionPlainTextWarningHookCommand("entire hooks codex session-start", WarningFormatMultiLine)

if strings.HasPrefix(command, "cmd.exe ") {
t.Fatalf("windows wrapper should use the hook runner's existing cmd.exe shell, got %q", command)
}
if strings.Contains(command, "\n") {
t.Fatalf("windows wrapper should keep warning command single-line, got %q", command)
}
Expand Down Expand Up @@ -201,6 +210,10 @@ func TestIsManagedHookCommand_WrappedPrefix(t *testing.T) {
) {
t.Fatal("expected windows wrapped json warning command to match")
}
nestedWindowsWrapper := `cmd.exe /d /s /c "where.exe entire >nul 2>nul & if errorlevel 1 (ver>nul) else (entire hooks codex stop)"`
if !IsManagedHookCommand(nestedWindowsWrapper, prefixes) {
t.Fatal("expected nested windows wrapper to remain managed")
}
}

func TestIsManagedHookCommand_DoesNotMatchSubstring(t *testing.T) {
Expand Down
Loading