mirror of
https://github.com/Twigpine/zero.git
synced 2026-11-09 18:08:31 -10:00
Update the Go module path, imports, install scripts, docs, workflows, packaging and action defaults from Gitlawb/zero to Twigpine/zero. The npm scope (already @twigpine/zero on main, with its @gitlawb/zero migration notes), LICENSE, action author, the OpenGateway provider and historical CHANGELOG links are unchanged. The X-AIMLAPI-Integration-Repo header keeps Gitlawb/zero: it is what AIMLAPI sees for attribution, so changing it needs a maintainer's yes. docs/INSTALL.md keeps `go install github.com/Gitlawb/zero/...`, because every tagged release so far declares that module path. The Homebrew formula comment names the Twigpine/homebrew-tap tap. Co-authored-by: Claude Opus 5.5 <[email protected]>
212 lines
6.0 KiB
Go
212 lines
6.0 KiB
Go
package cli
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/Twigpine/zero/internal/background"
|
|
)
|
|
|
|
// isolateDaemonPaths points DefaultPaths at a temp dir so the test never touches
|
|
// a real daemon on the dev machine.
|
|
func isolateDaemonPaths(t *testing.T) {
|
|
t.Helper()
|
|
t.Setenv("XDG_RUNTIME_DIR", t.TempDir())
|
|
}
|
|
|
|
func runDaemonCLI(t *testing.T, args ...string) (int, string, string) {
|
|
t.Helper()
|
|
var out, errb bytes.Buffer
|
|
code := runDaemon(args, &out, &errb, appDeps{})
|
|
return code, out.String(), errb.String()
|
|
}
|
|
|
|
func TestDaemonUsage(t *testing.T) {
|
|
code, _, _ := runDaemonCLI(t)
|
|
if code != exitUsage {
|
|
t.Fatalf("no-args exit = %d, want exitUsage", code)
|
|
}
|
|
code, out, _ := runDaemonCLI(t, "--help")
|
|
if code != exitSuccess || !strings.Contains(out, "Usage: zero daemon") {
|
|
t.Fatalf("--help exit=%d out=%q", code, out)
|
|
}
|
|
}
|
|
|
|
func TestDaemonUnknownSubcommand(t *testing.T) {
|
|
code, _, errb := runDaemonCLI(t, "frobnicate")
|
|
if code != exitUsage {
|
|
t.Fatalf("unknown subcommand exit = %d, want exitUsage", code)
|
|
}
|
|
if !strings.Contains(errb, "unknown daemon subcommand") {
|
|
t.Fatalf("stderr = %q, want unknown-subcommand message", errb)
|
|
}
|
|
}
|
|
|
|
func TestDaemonRunRequiresSession(t *testing.T) {
|
|
isolateDaemonPaths(t)
|
|
code, _, errb := runDaemonCLI(t, "run", "--prompt", "hi")
|
|
if code != exitUsage {
|
|
t.Fatalf("run without --session exit = %d, want exitUsage", code)
|
|
}
|
|
if !strings.Contains(errb, "--session") {
|
|
t.Fatalf("stderr = %q, want a --session hint", errb)
|
|
}
|
|
}
|
|
|
|
func TestDaemonRunRequiresPromptOrArgs(t *testing.T) {
|
|
isolateDaemonPaths(t)
|
|
code, _, errb := runDaemonCLI(t, "run", "--session", "s1")
|
|
if code != exitUsage {
|
|
t.Fatalf("run without prompt/args exit = %d, want exitUsage", code)
|
|
}
|
|
if !strings.Contains(errb, "--prompt") {
|
|
t.Fatalf("stderr = %q, want a --prompt hint", errb)
|
|
}
|
|
}
|
|
|
|
func TestDaemonStopWhenNotRunning(t *testing.T) {
|
|
isolateDaemonPaths(t)
|
|
code, out, _ := runDaemonCLI(t, "stop")
|
|
if code != exitSuccess {
|
|
t.Fatalf("stop (not running) exit = %d, want exitSuccess", code)
|
|
}
|
|
if !strings.Contains(out, "not running") {
|
|
t.Fatalf("stop output = %q, want 'not running'", out)
|
|
}
|
|
}
|
|
|
|
func TestDaemonStatusWhenNotRunning(t *testing.T) {
|
|
isolateDaemonPaths(t)
|
|
code, out, _ := runDaemonCLI(t, "status")
|
|
if code != exitSuccess {
|
|
t.Fatalf("status (not running) exit = %d, want exitSuccess", code)
|
|
}
|
|
if !strings.Contains(out, "not running") {
|
|
t.Fatalf("status output = %q, want 'not running'", out)
|
|
}
|
|
}
|
|
|
|
func TestDaemonAttachRequiresSession(t *testing.T) {
|
|
isolateDaemonPaths(t)
|
|
code, _, errb := runDaemonCLI(t, "attach")
|
|
if code != exitUsage {
|
|
t.Fatalf("attach without session exit = %d, want exitUsage", code)
|
|
}
|
|
if !strings.Contains(errb, "session") {
|
|
t.Fatalf("stderr = %q, want a session hint", errb)
|
|
}
|
|
}
|
|
|
|
func TestDaemonRunWhenNotRunning(t *testing.T) {
|
|
isolateDaemonPaths(t)
|
|
code, _, errb := runDaemonCLI(t, "run", "--session", "s1", "--prompt", "hello")
|
|
if code != exitCrash {
|
|
t.Fatalf("run (no daemon) exit = %d, want exitCrash", code)
|
|
}
|
|
if !strings.Contains(errb, "not running") {
|
|
t.Fatalf("stderr = %q, want 'not running'", errb)
|
|
}
|
|
}
|
|
|
|
func TestDaemonSubcommandsRejectExtraArgs(t *testing.T) {
|
|
isolateDaemonPaths(t)
|
|
cases := [][]string{
|
|
{"stop", "oops"},
|
|
{"status", "oops"},
|
|
{"attach", "s1", "extra"},
|
|
}
|
|
for _, args := range cases {
|
|
code, _, errb := runDaemonCLI(t, args...)
|
|
if code != exitUsage {
|
|
t.Fatalf("%v exit = %d, want exitUsage (reject extra args); stderr=%q", args, code, errb)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestWaitForDaemonReadinessChecksTimeoutBoundary(t *testing.T) {
|
|
checks := 0
|
|
ready := waitForDaemonReadiness(func() bool {
|
|
checks++
|
|
return checks == 2
|
|
}, 0, time.Millisecond)
|
|
if !ready {
|
|
t.Fatal("daemon that became reachable at the timeout boundary was not detected")
|
|
}
|
|
if checks != 2 {
|
|
t.Fatalf("reachability checks = %d, want initial and final checks", checks)
|
|
}
|
|
}
|
|
|
|
func TestTerminateAndReapDaemonProcess(t *testing.T) {
|
|
cmd := daemonDetachedChildCommand("hang")
|
|
if err := cmd.Start(); err != nil {
|
|
t.Fatalf("start helper process: %v", err)
|
|
}
|
|
|
|
if err := terminateAndReapDaemonProcess(cmd); err != nil {
|
|
t.Fatalf("terminate and reap helper process: %v", err)
|
|
}
|
|
if cmd.ProcessState == nil {
|
|
t.Fatalf("helper process was not reaped: state=%v", cmd.ProcessState)
|
|
}
|
|
if cmd.ProcessState.Success() {
|
|
t.Fatalf("helper process unexpectedly exited successfully: %v", cmd.ProcessState)
|
|
}
|
|
}
|
|
|
|
func TestStartAndAwaitDaemonProcessTimeoutTerminatesAndReaps(t *testing.T) {
|
|
cmd := daemonDetachedChildCommand("hang")
|
|
if err := startAndAwaitDaemonProcess(cmd, func() bool { return false }, 0, time.Millisecond); !errors.Is(err, errDaemonStartTimeout) {
|
|
t.Fatalf("start and await error = %v, want timeout", err)
|
|
}
|
|
if cmd.ProcessState == nil {
|
|
t.Fatal("timed-out helper process was not reaped")
|
|
}
|
|
}
|
|
|
|
func TestStartAndAwaitDaemonProcessReleasesReadyChild(t *testing.T) {
|
|
marker := filepath.Join(t.TempDir(), "child-finished")
|
|
cmd := daemonDetachedChildCommand("mark", "ZERO_TEST_DAEMON_CHILD_MARKER="+marker)
|
|
if err := startAndAwaitDaemonProcess(cmd, func() bool { return true }, time.Second, time.Millisecond); err != nil {
|
|
t.Fatalf("start and await ready helper: %v", err)
|
|
}
|
|
|
|
deadline := time.Now().Add(5 * time.Second)
|
|
for time.Now().Before(deadline) {
|
|
if _, err := os.Stat(marker); err == nil {
|
|
return
|
|
}
|
|
time.Sleep(10 * time.Millisecond)
|
|
}
|
|
t.Fatal("released helper process did not remain alive to write its marker")
|
|
}
|
|
|
|
func daemonDetachedChildCommand(mode string, extraEnv ...string) *exec.Cmd {
|
|
cmd := exec.Command(os.Args[0], "-test.run=TestDaemonDetachedChildProcess")
|
|
cmd.Env = append(os.Environ(), "ZERO_TEST_DAEMON_DETACHED_CHILD="+mode)
|
|
cmd.Env = append(cmd.Env, extraEnv...)
|
|
background.ConfigureChildProcessGroup(cmd)
|
|
return cmd
|
|
}
|
|
|
|
func TestDaemonDetachedChildProcess(t *testing.T) {
|
|
switch os.Getenv("ZERO_TEST_DAEMON_DETACHED_CHILD") {
|
|
case "":
|
|
return
|
|
case "mark":
|
|
if err := os.WriteFile(os.Getenv("ZERO_TEST_DAEMON_CHILD_MARKER"), []byte("ready"), 0o600); err != nil {
|
|
os.Exit(2)
|
|
}
|
|
os.Exit(0)
|
|
}
|
|
for {
|
|
time.Sleep(time.Hour)
|
|
}
|
|
}
|