Files
PierrunoYTandClaude Opus 5.5 13e2596b2e chore: move repository references to Twigpine/zero (#1105)
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]>
2026-10-17 07:13:21 +08:00

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)
}
}