Files
wehub-resource-sync f99010fae1
CI / lint (push) Failing after 1s
CI / frontend (push) Failing after 1s
CI / scripts (push) Failing after 1s
CI / Go Test (ubuntu-latest) (push) Failing after 0s
CI / frontend-node-25 (push) Failing after 1s
CI / docs (push) Failing after 0s
CI / coverage (push) Failing after 0s
CI / e2e (push) Failing after 0s
Docker / build-and-push (push) Failing after 1s
CI / integration (push) Failing after 4m43s
CI / Go Test (windows-latest) (push) Has been cancelled
CI / Desktop Unit Tests (Windows) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Linux (arm64)) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Linux) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Windows) (push) Has been cancelled
Desktop Artifacts (macOS) / Desktop Build (macOS (aarch64)) (push) Has been cancelled
Desktop Artifacts (macOS) / Desktop Build (macOS (x86_64)) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:30:36 +08:00

1153 lines
42 KiB
Go

package main
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"strconv"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.kenn.io/agentsview/internal/config"
"go.kenn.io/agentsview/internal/db"
"go.kenn.io/kit/daemon"
)
type testDaemonLaunchLock struct {
unlocked bool
}
func (l *testDaemonLaunchLock) Unlock() error {
l.unlocked = true
return nil
}
func daemonCommandTestDeps(t *testing.T) (*daemonCommandDeps, *bytes.Buffer) {
t.Helper()
dir := t.TempDir()
out := new(bytes.Buffer)
deps := defaultDaemonCommandDeps()
deps.resolveDataDir = func() (string, error) { return dir, nil }
deps.mkdirAll = func(path string, mode os.FileMode) error {
assert.Equal(t, dir, path)
return nil
}
deps.loadConfig = func() (config.Config, error) {
return config.Config{DataDir: dir, DBPath: filepath.Join(dir, "sessions.db")}, nil
}
deps.loadReadOnlyConfig = deps.loadConfig
deps.acquireLaunchLockWithError = func(string) (daemonLaunchLock, bool, error) {
return &testDaemonLaunchLock{}, true, nil
}
deps.writableRecords = func(string, string) ([]daemon.RuntimeRecord, error) { return nil, nil }
deps.statusRecords = func(string, string) ([]daemon.RuntimeRecord, error) { return nil, nil }
deps.isStarting = func(string) bool { return false }
deps.readStartupState = func(string) *startupState { return nil }
deps.startBackground = func(
cfg config.Config, args []string, opts serveReplacementOptions,
policy backgroundLaunchPolicy,
) (backgroundLaunchResult, error) {
return backgroundLaunchResult{
Runtime: &DaemonRuntime{
Record: daemon.RuntimeRecord{PID: 314},
Host: "127.0.0.1", Port: 8080,
},
Started: true, LogPath: filepath.Join(dir, "serve.log"), childPID: 314,
}, nil
}
deps.waitContendedLaunch = func(string) daemonLaunchObservation {
return daemonLaunchObservation{LockHeld: true}
}
deps.stopTargetConfirmed = func(daemon.RuntimeRecord, string) bool { return true }
deps.stopProcess = func(daemon.RuntimeRecord, time.Duration) error { return nil }
deps.stopCaddy = func(io.Writer, daemon.RuntimeRecord) error { return nil }
deps.validateConfig = func(config.Config) error { return nil }
deps.checkDataVersion = func(string) error { return nil }
deps.probeRecord = func(rec daemon.RuntimeRecord, _ string) (daemon.PingInfo, bool) {
return daemon.PingInfo{PID: rec.PID}, true
}
deps.now = func() time.Time { return time.Unix(200, 0) }
return &deps, out
}
func executeDaemonCommand(
t *testing.T, deps daemonCommandDeps, out io.Writer, args ...string,
) error {
t.Helper()
cmd := newDaemonCommandWithDeps(deps)
cmd.SetOut(out)
cmd.SetErr(out)
cmd.SetArgs(args)
return cmd.Execute()
}
func executeServeCommand(
t *testing.T, deps daemonCommandDeps, out io.Writer, args ...string,
) error {
t.Helper()
cmd := newServeCommandWithDaemonDeps(deps)
cmd.SetOut(out)
cmd.SetErr(out)
cmd.SetArgs(args)
return cmd.Execute()
}
func testWritableRecord(pid int, path string) daemon.RuntimeRecord {
return daemon.RuntimeRecord{
PID: pid, Service: daemonService, Version: "1.2.3",
Network: daemon.NetworkTCP, Address: fmt.Sprintf("127.0.0.1:%d", 8000+pid),
SourcePath: path,
StartedAt: time.Unix(100, 0),
Metadata: map[string]string{
runtimeHost: "127.0.0.1", runtimePort: fmt.Sprint(8000 + pid),
runtimeAPIVersion: fmt.Sprint(daemonAPIVersion),
runtimeDataVersion: fmt.Sprint(db.CurrentDataVersion()),
},
}
}
func TestDaemonCommandRegistrationAndSurface(t *testing.T) {
root := newRootCommand()
daemonCmd, _, err := root.Find([]string{"daemon"})
require.NoError(t, err)
require.Equal(t, "daemon", daemonCmd.Name())
assert.Equal(t, groupCore, daemonCmd.GroupID)
names := make(map[string]bool)
for _, cmd := range daemonCmd.Commands() {
names[cmd.Name()] = true
assert.Nil(t, cmd.Flags().Lookup("host"))
assert.Nil(t, cmd.Flags().Lookup("no-sync"))
}
assert.Equal(t, map[string]bool{
"restart": true, "start": true, "status": true, "stop": true,
}, names)
help, err := executeCommand(root, "--help")
require.NoError(t, err)
assert.Contains(t, help, "daemon")
}
func TestDaemonCommandRejectsArgumentsAndServeFlags(t *testing.T) {
deps, out := daemonCommandTestDeps(t)
for _, subcommand := range []string{"start", "status", "stop", "restart"} {
t.Run(subcommand+" arg", func(t *testing.T) {
out.Reset()
err := executeDaemonCommand(t, *deps, out, subcommand, "extra")
require.Error(t, err)
assert.ErrorContains(t, err, "unknown command")
})
for _, flag := range []string{"--host", "--no-sync"} {
t.Run(subcommand+" "+flag, func(t *testing.T) {
out.Reset()
err := executeDaemonCommand(t, *deps, out, subcommand, flag)
require.Error(t, err)
assert.ErrorContains(t, err, "unknown flag")
})
}
}
}
func TestDaemonStartUsesConfigOnlyPolicyAndIsIdempotent(t *testing.T) {
deps, out := daemonCommandTestDeps(t)
var gotArgs []string
var gotPolicy backgroundLaunchPolicy
deps.startBackground = func(
cfg config.Config, args []string, _ serveReplacementOptions,
policy backgroundLaunchPolicy,
) (backgroundLaunchResult, error) {
gotArgs = append([]string(nil), args...)
gotPolicy = policy
return backgroundLaunchResult{
Runtime: &DaemonRuntime{Record: daemon.RuntimeRecord{PID: 41}, Host: "127.0.0.1", Port: 9090},
}, nil
}
require.NoError(t, executeDaemonCommand(t, *deps, out, "start"))
assert.Equal(t, []string{"serve"}, gotArgs)
assert.True(t, gotPolicy.ConfigOnly)
assert.Equal(t, "daemon start", gotPolicy.Operation)
assert.False(t, gotPolicy.Attached)
assert.Nil(t, gotPolicy.Context)
assert.Nil(t, gotPolicy.OnLaunch)
assert.Nil(t, gotPolicy.OnProgress)
assert.Contains(t, out.String(), "already running")
assert.Contains(t, out.String(), "pid 41")
}
func TestDaemonStopUsesStartupStateFallbackWhileStarting(t *testing.T) {
deps, out := daemonCommandTestDeps(t)
deps.isStarting = func(string) bool { return true }
deps.readStartupState = func(string) *startupState {
return &startupState{PID: 77, Phase: "starting HTTP server"}
}
var stopped daemon.RuntimeRecord
deps.writableRuntime = func(string, string) *DaemonRuntime {
return &DaemonRuntime{Record: testWritableRecord(77, ""), RuntimeFallback: true}
}
deps.stopProcess = func(rec daemon.RuntimeRecord, _ time.Duration) error {
stopped = rec
return nil
}
err := executeDaemonCommand(t, *deps, out, "stop")
require.NoError(t, err)
assert.Equal(t, 77, stopped.PID)
assert.Contains(t, out.String(), "Stopped agentsview (pid 77).")
assert.NotContains(t, out.String(), "starting up")
}
func TestDaemonStopUsesStartupStateFallbackWhenRuntimeStoreInspectionFails(t *testing.T) {
deps, out := daemonCommandTestDeps(t)
deps.writableRecords = func(string, string) ([]daemon.RuntimeRecord, error) {
return nil, errors.New("store unavailable")
}
deps.writableRuntime = func(string, string) *DaemonRuntime {
return &DaemonRuntime{Record: testWritableRecord(76, ""), RuntimeFallback: true}
}
var stopped daemon.RuntimeRecord
deps.stopProcess = func(rec daemon.RuntimeRecord, _ time.Duration) error {
stopped = rec
return nil
}
require.NoError(t, executeDaemonCommand(t, *deps, out, "stop"))
assert.Equal(t, 76, stopped.PID)
assert.Contains(t, out.String(), "Stopped agentsview (pid 76).")
}
func TestDaemonStopRegisteredRuntimePreservesStartupGuard(t *testing.T) {
deps, out := daemonCommandTestDeps(t)
deps.isStarting = func(string) bool { return true }
deps.writableRecords = func(string, string) ([]daemon.RuntimeRecord, error) {
return []daemon.RuntimeRecord{testWritableRecord(78, "runtime.json")}, nil
}
err := executeDaemonCommand(t, *deps, out, "stop")
require.Error(t, err)
assert.Contains(t, err.Error(), "startup is still in progress")
}
func TestDaemonStartUsesStartupStateFallbackWhileStarting(t *testing.T) {
deps, out := daemonCommandTestDeps(t)
deps.isStarting = func(string) bool { return true }
deps.writableRuntime = func(string, string) *DaemonRuntime {
return &DaemonRuntime{
Record: testWritableRecord(79, ""),
Host: "127.0.0.1",
Port: 8079,
RuntimeFallback: true,
}
}
starts := 0
deps.startBackground = func(
config.Config, []string, serveReplacementOptions, backgroundLaunchPolicy,
) (backgroundLaunchResult, error) {
starts++
return backgroundLaunchResult{}, nil
}
require.NoError(t, executeDaemonCommand(t, *deps, out, "start"))
assert.Zero(t, starts)
assert.Contains(t, out.String(), "already running")
assert.Contains(t, out.String(), "8079")
}
func TestDaemonStartPersistentStartupNeverLaunches(t *testing.T) {
deps, out := daemonCommandTestDeps(t)
deps.isStarting = func(string) bool { return true }
deps.readStartupState = func(string) *startupState {
return &startupState{PID: 77, StartedAt: time.Unix(100, 0), LogPath: "/tmp/serve.log", Phase: "sync"}
}
started := 0
deps.startBackground = func(config.Config, []string, serveReplacementOptions, backgroundLaunchPolicy) (backgroundLaunchResult, error) {
started++
return backgroundLaunchResult{}, nil
}
err := executeDaemonCommand(t, *deps, out, "start")
require.Error(t, err)
assert.ErrorContains(t, err, "startup is still in progress")
assert.ErrorContains(t, err, "pid 77")
assert.ErrorContains(t, err, "/tmp/serve.log")
assert.ErrorContains(t, err, "verify")
assert.Equal(t, 0, started)
}
func TestDaemonStartLateStartupLockDoesNotReportSuccess(t *testing.T) {
deps, out := daemonCommandTestDeps(t)
starting := false
deps.isStarting = func(string) bool { return starting }
deps.readStartupState = func(string) *startupState {
return &startupState{PID: 78, LogPath: "/tmp/late-serve.log"}
}
deps.startBackground = func(config.Config, []string, serveReplacementOptions, backgroundLaunchPolicy) (backgroundLaunchResult, error) {
starting = true
return backgroundLaunchResult{}, nil
}
err := executeDaemonCommand(t, *deps, out, "start")
require.Error(t, err)
assert.ErrorContains(t, err, "startup is still in progress")
assert.ErrorContains(t, err, "pid 78")
assert.NotContains(t, out.String(), "pid 0")
}
func TestDaemonStartLaunchContentionTerminalStates(t *testing.T) {
tests := []struct {
name string
observation daemonLaunchObservation
wantErr string
wantOut string
}{
{
name: "published writable",
observation: daemonLaunchObservation{Records: []daemon.RuntimeRecord{
testWritableRecord(21, "/runtime/21.json"),
}},
wantOut: "already running",
},
{
name: "multiple published writables",
observation: daemonLaunchObservation{Records: []daemon.RuntimeRecord{
testWritableRecord(31, "/runtime/31.json"),
testWritableRecord(32, "/runtime/32.json"),
}},
wantErr: "multiple writable agentsview daemons",
},
{
name: "persistent startup snapshot",
observation: daemonLaunchObservation{LockHeld: true, Starting: true, Snapshot: &startupState{
PID: 22, StartedAt: time.Unix(100, 0), LogPath: "/tmp/serve.log", Phase: "opening database",
}},
wantErr: "startup is still in progress",
},
{
name: "persistent launch lock without snapshot",
observation: daemonLaunchObservation{LockHeld: true},
wantErr: "launch is still in progress",
},
{
name: "owner failed without publishing",
observation: daemonLaunchObservation{},
wantErr: "startup failed",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
deps, out := daemonCommandTestDeps(t)
loadCalls, startCalls := 0, 0
deps.acquireLaunchLockWithError = func(string) (daemonLaunchLock, bool, error) { return nil, false, nil }
deps.waitContendedLaunch = func(string) daemonLaunchObservation { return tt.observation }
deps.loadConfig = func() (config.Config, error) { loadCalls++; return config.Config{}, nil }
deps.startBackground = func(config.Config, []string, serveReplacementOptions, backgroundLaunchPolicy) (backgroundLaunchResult, error) {
startCalls++
return backgroundLaunchResult{}, nil
}
err := executeDaemonCommand(t, *deps, out, "start")
if tt.wantErr == "" {
require.NoError(t, err)
assert.Contains(t, out.String(), tt.wantOut)
} else {
require.Error(t, err)
assert.ErrorContains(t, err, tt.wantErr)
}
assert.Equal(t, 0, loadCalls, "contender must not read or write config")
assert.Equal(t, 0, startCalls, "contender must never start a second writer")
})
}
}
func TestDaemonStatusRendersStoppedStartingReadOnlyAndIncompatible(t *testing.T) {
tests := []struct {
name string
setup func(*daemonCommandDeps)
wanted []string
unwanted []string
}{
{name: "stopped", setup: func(*daemonCommandDeps) {}, wanted: []string{"No agentsview daemon is running"}},
{name: "starting", setup: func(d *daemonCommandDeps) {
d.isStarting = func(string) bool { return true }
d.readStartupState = func(string) *startupState {
return &startupState{PID: 9, StartedAt: time.Unix(100, 0), Phase: "sync", LogPath: "/tmp/log"}
}
}, wanted: []string{"starting", "pid: 9", "phase: sync", "/tmp/log"}},
{name: "read only", setup: func(d *daemonCommandDeps) {
d.statusRecords = func(string, string) ([]daemon.RuntimeRecord, error) {
rec := testWritableRecord(10, "/runtime/10.json")
rec.Metadata[runtimeReadOnly] = "true"
return []daemon.RuntimeRecord{rec}, nil
}
}, wanted: []string{"No agentsview daemon is running."}, unwanted: []string{"running at", "mode: read-only"}},
{name: "incompatible", setup: func(d *daemonCommandDeps) {
d.statusRecords = func(string, string) ([]daemon.RuntimeRecord, error) {
rec := testWritableRecord(11, "/runtime/11.json")
rec.Metadata[runtimeAPIVersion] = "0"
return []daemon.RuntimeRecord{rec}, nil
}
d.probeRecord = func(rec daemon.RuntimeRecord, _ string) (daemon.PingInfo, bool) {
return daemon.PingInfo{PID: rec.PID}, true
}
}, wanted: []string{"incompatible", "pid: 11", "API version"}},
{name: "running", setup: func(d *daemonCommandDeps) {
d.statusRecords = func(string, string) ([]daemon.RuntimeRecord, error) {
return []daemon.RuntimeRecord{testWritableRecord(12, "/runtime/12.json")}, nil
}
d.probeRecord = func(rec daemon.RuntimeRecord, _ string) (daemon.PingInfo, bool) {
return daemon.PingInfo{PID: rec.PID}, true
}
}, wanted: []string{"running at http://127.0.0.1:8012", "pid: 12", "version: 1.2.3", "uptime:"}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
deps, out := daemonCommandTestDeps(t)
tt.setup(deps)
require.NoError(t, executeDaemonCommand(t, *deps, out, "status"))
for _, wanted := range tt.wanted {
assert.Contains(t, out.String(), wanted)
}
for _, unwanted := range tt.unwanted {
assert.NotContains(t, out.String(), unwanted)
}
})
}
}
func TestDaemonStartContentionStartLockWithoutSnapshotUsesRecoveryError(t *testing.T) {
deps, out := daemonCommandTestDeps(t)
deps.acquireLaunchLockWithError = func(string) (daemonLaunchLock, bool, error) { return nil, false, nil }
deps.waitContendedLaunch = func(string) daemonLaunchObservation {
return daemonLaunchObservation{LockHeld: true, Starting: true}
}
err := executeDaemonCommand(t, *deps, out, "start")
require.Error(t, err)
assert.ErrorContains(t, err, "startup is still in progress")
assert.ErrorContains(t, err, "runtime publication may have failed")
assert.ErrorContains(t, err, startupStateFileName)
assert.NotContains(t, err.Error(), "launch is still in progress")
}
func TestDaemonStartContentionHeldStartLockWinsOverPreexistingRecord(t *testing.T) {
deps, out := daemonCommandTestDeps(t)
deps.acquireLaunchLockWithError = func(string) (daemonLaunchLock, bool, error) { return nil, false, nil }
deps.waitContendedLaunch = func(string) daemonLaunchObservation {
return daemonLaunchObservation{
LockHeld: true,
Starting: true,
Snapshot: &startupState{PID: 140, LogPath: "/tmp/serve.log"},
Records: []daemon.RuntimeRecord{
testWritableRecord(141, "/runtime/141.json"),
},
}
}
configLoads, starts := 0, 0
deps.loadConfig = func() (config.Config, error) {
configLoads++
return config.Config{}, nil
}
deps.startBackground = func(config.Config, []string, serveReplacementOptions, backgroundLaunchPolicy) (backgroundLaunchResult, error) {
starts++
return backgroundLaunchResult{}, nil
}
err := executeDaemonCommand(t, *deps, out, "start")
require.Error(t, err)
assert.ErrorContains(t, err, "startup is still in progress")
assert.ErrorContains(t, err, "pid 140")
assert.NotContains(t, out.String(), "already running")
assert.Zero(t, configLoads)
assert.Zero(t, starts)
}
func TestDaemonStatusListsEveryWriterAndSurfacesInspectionErrors(t *testing.T) {
deps, out := daemonCommandTestDeps(t)
deps.statusRecords = func(string, string) ([]daemon.RuntimeRecord, error) {
return []daemon.RuntimeRecord{
testWritableRecord(31, "/runtime/31.json"),
testWritableRecord(32, "/runtime/32.json"),
}, nil
}
require.NoError(t, executeDaemonCommand(t, *deps, out, "status"))
assert.Contains(t, out.String(), "single-writer invariant")
assert.Contains(t, out.String(), "pid: 31")
assert.Contains(t, out.String(), "pid: 32")
out.Reset()
deps.statusRecords = func(string, string) ([]daemon.RuntimeRecord, error) { return nil, errors.New("store unavailable") }
err := executeDaemonCommand(t, *deps, out, "status")
require.Error(t, err)
assert.ErrorContains(t, err, "store unavailable")
deps.loadReadOnlyConfig = func() (config.Config, error) { return config.Config{}, errors.New("bad config") }
err = executeDaemonCommand(t, *deps, out, "status")
require.Error(t, err)
assert.ErrorContains(t, err, "bad config")
}
func TestDaemonStatusUsesStartupStateFallbackWhenRuntimeStoreInspectionFails(t *testing.T) {
deps, out := daemonCommandTestDeps(t)
deps.statusRecords = func(string, string) ([]daemon.RuntimeRecord, error) {
return nil, errors.New("store unavailable")
}
deps.writableRuntime = func(string, string) *DaemonRuntime {
return &DaemonRuntime{
Record: testWritableRecord(54, ""),
Host: "127.0.0.1",
Port: 8054,
RuntimeFallback: true,
}
}
require.NoError(t, executeDaemonCommand(t, *deps, out, "status"))
assert.Contains(t, out.String(), "pid: 54")
assert.Contains(t, out.String(), "runtime record unwritten")
}
func TestDaemonStatusNotRespondingIsUseful(t *testing.T) {
deps, out := daemonCommandTestDeps(t)
deps.statusRecords = func(string, string) ([]daemon.RuntimeRecord, error) {
return []daemon.RuntimeRecord{testWritableRecord(55, "/runtime/55.json")}, nil
}
deps.probeRecord = func(daemon.RuntimeRecord, string) (daemon.PingInfo, bool) {
return daemon.PingInfo{}, false
}
require.NoError(t, executeDaemonCommand(t, *deps, out, "status"))
assert.Contains(t, out.String(), "not responding")
assert.Contains(t, out.String(), "pid: 55")
assert.Contains(t, out.String(), "/runtime/55.json")
}
func TestDaemonStatusUsesPingVersionAsAuthoritative(t *testing.T) {
endpoint := newPingDaemon(t)
for _, tt := range []struct {
name string
recordVersion string
}{
{name: "missing runtime version"},
{name: "stale runtime version", recordVersion: "stale-record-version"},
} {
t.Run(tt.name, func(t *testing.T) {
rec := testWritableRecord(os.Getpid(), "")
rec.Version = tt.recordVersion
rec.Address = endpoint.Addr
rec.Metadata[runtimeHost] = endpoint.Host
rec.Metadata[runtimePort] = strconv.Itoa(endpoint.Port)
deps, out := daemonCommandTestDeps(t)
deps.statusRecords = func(string, string) ([]daemon.RuntimeRecord, error) {
return []daemon.RuntimeRecord{rec}, nil
}
deps.probeRecord = probeDaemonRecord
require.NoError(t, executeDaemonCommand(t, *deps, out, "status"))
assert.Contains(t, out.String(), "version: test")
assert.NotContains(t, out.String(), "stale-record-version")
})
}
}
func TestDaemonStatusDiscoversAuthenticatedLegacyDaemon(t *testing.T) {
dir := runtimeTestDir(t)
const token = "status-secret"
endpoint, legacyPath := writeAuthenticatedProbeableLegacyRuntime(
t, dir, token, legacyStateFile{Version: version},
)
deps, out := daemonCommandTestDeps(t)
deps.loadReadOnlyConfig = func() (config.Config, error) {
return config.Config{DataDir: dir, AuthToken: token}, nil
}
deps.statusRecords = daemonStatusRecords
require.NoError(t, executeDaemonCommand(t, *deps, out, "status"))
assert.Contains(t, out.String(), "running at")
assert.Contains(t, out.String(), fmt.Sprintf("pid: %d", os.Getpid()))
assert.Contains(t, out.String(), fmt.Sprintf(":%d", endpoint.Port))
assertPathRemoved(t, legacyPath, "authenticated legacy state should migrate")
}
func TestDaemonStopDiscoversAuthenticatedLegacyDaemon(t *testing.T) {
dir := runtimeTestDir(t)
const token = "stop-secret"
_, legacyPath := writeAuthenticatedProbeableLegacyRuntime(
t, dir, token, legacyStateFile{Version: version},
)
deps, out := daemonCommandTestDeps(t)
deps.resolveDataDir = func() (string, error) { return dir, nil }
deps.mkdirAll = func(path string, _ os.FileMode) error {
assert.Equal(t, dir, path)
return nil
}
deps.loadConfig = func() (config.Config, error) {
return config.Config{DataDir: dir, AuthToken: token}, nil
}
deps.writableRecords = writableDaemonRecords
var stopped []int
deps.stopProcess = func(rec daemon.RuntimeRecord, _ time.Duration) error {
stopped = append(stopped, rec.PID)
return nil
}
require.NoError(t, executeDaemonCommand(t, *deps, out, "stop"))
assert.Equal(t, []int{os.Getpid()}, stopped)
assert.Contains(t, out.String(), fmt.Sprintf("pid %d", os.Getpid()))
assertPathRemoved(t, legacyPath, "authenticated legacy state should migrate")
}
func TestDaemonStopPrevalidatesEveryWriterBeforeSignalling(t *testing.T) {
deps, out := daemonCommandTestDeps(t)
records := []daemon.RuntimeRecord{
testWritableRecord(61, "/runtime/61.json"),
testWritableRecord(62, "/runtime/62.json"),
}
deps.writableRecords = func(string, string) ([]daemon.RuntimeRecord, error) { return records, nil }
var prevalidated []int
deps.stopTargetConfirmed = func(rec daemon.RuntimeRecord, _ string) bool {
prevalidated = append(prevalidated, rec.PID)
return rec.PID != 61
}
var signalled []int
deps.stopProcess = func(rec daemon.RuntimeRecord, _ time.Duration) error {
signalled = append(signalled, rec.PID)
return nil
}
err := executeDaemonCommand(t, *deps, out, "stop")
require.Error(t, err)
assert.ErrorContains(t, err, "pid 61")
assert.ErrorContains(t, err, "/runtime/61.json")
assert.ErrorContains(t, err, "verify")
assert.Equal(t, []int{61, 62}, prevalidated, "every target must be checked before stop can return")
assert.Empty(t, signalled)
}
func TestDaemonStopStartingConfigFailureAndContentionSignalNobody(t *testing.T) {
tests := []struct {
name string
setup func(*daemonCommandDeps)
want string
}{
{name: "launch contention", setup: func(d *daemonCommandDeps) {
d.acquireLaunchLockWithError = func(string) (daemonLaunchLock, bool, error) { return nil, false, nil }
}, want: "launch lock"},
{name: "config failure", setup: func(d *daemonCommandDeps) {
d.loadConfig = func() (config.Config, error) { return config.Config{}, errors.New("bad config") }
}, want: "bad config"},
{name: "start lock with confirmed record", setup: func(d *daemonCommandDeps) {
d.isStarting = func(string) bool { return true }
d.readStartupState = func(string) *startupState { return &startupState{PID: 71, LogPath: "/tmp/serve.log"} }
d.writableRecords = func(string, string) ([]daemon.RuntimeRecord, error) {
return []daemon.RuntimeRecord{testWritableRecord(72, "/runtime/72.json")}, nil
}
}, want: "startup is still in progress"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
deps, out := daemonCommandTestDeps(t)
tt.setup(deps)
signals := 0
deps.stopProcess = func(daemon.RuntimeRecord, time.Duration) error { signals++; return nil }
err := executeDaemonCommand(t, *deps, out, "stop")
require.Error(t, err)
assert.ErrorContains(t, err, tt.want)
assert.Zero(t, signals)
})
}
}
func TestDaemonStopMultipleWritersAndRepeatIsIdempotent(t *testing.T) {
deps, out := daemonCommandTestDeps(t)
records := []daemon.RuntimeRecord{
testWritableRecord(81, "/runtime/81.json"),
testWritableRecord(82, "/runtime/82.json"),
}
deps.writableRecords = func(string, string) ([]daemon.RuntimeRecord, error) { return records, nil }
var stopped, caddy []int
deps.stopProcess = func(rec daemon.RuntimeRecord, _ time.Duration) error { stopped = append(stopped, rec.PID); return nil }
deps.stopCaddy = func(_ io.Writer, rec daemon.RuntimeRecord) error {
caddy = append(caddy, rec.PID)
return nil
}
require.NoError(t, executeDaemonCommand(t, *deps, out, "stop"))
assert.Equal(t, []int{81, 82}, stopped)
assert.Equal(t, []int{81, 82}, caddy)
assert.Contains(t, out.String(), "pid 81")
assert.Contains(t, out.String(), "pid 82")
records = nil
out.Reset()
require.NoError(t, executeDaemonCommand(t, *deps, out, "stop"))
assert.Contains(t, out.String(), "not running")
}
func TestDaemonRestartValidatesBeforeStoppingAndUsesFreshConfig(t *testing.T) {
deps, out := daemonCommandTestDeps(t)
dataDir := t.TempDir()
deps.resolveDataDir = func() (string, error) { return dataDir, nil }
deps.mkdirAll = func(path string, _ os.FileMode) error {
assert.Equal(t, dataDir, path)
return nil
}
rec := testWritableRecord(91, "/runtime/91.json")
rec.Metadata[runtimeNoSync] = "true"
deps.writableRecords = func(string, string) ([]daemon.RuntimeRecord, error) { return []daemon.RuntimeRecord{rec}, nil }
var order []string
deps.loadConfig = func() (config.Config, error) {
order = append(order, "config")
return config.Config{DataDir: dataDir, DBPath: "/fresh/sessions.db", NoSync: true}, nil
}
deps.checkDataVersion = func(path string) error {
order = append(order, "data")
assert.Equal(t, "/fresh/sessions.db", path)
return nil
}
deps.stopProcess = func(daemon.RuntimeRecord, time.Duration) error { order = append(order, "stop"); return nil }
deps.startBackground = func(cfg config.Config, args []string, _ serveReplacementOptions, policy backgroundLaunchPolicy) (backgroundLaunchResult, error) {
order = append(order, "start")
assert.True(t, cfg.NoSync, "loader result reaches config-only lower layer, which clears runtime NoSync")
assert.Equal(t, []string{"serve"}, args)
assert.True(t, policy.ConfigOnly)
assert.Equal(t, "daemon restart", policy.Operation)
return backgroundLaunchResult{Runtime: &DaemonRuntime{Record: daemon.RuntimeRecord{PID: 92}, Host: "127.0.0.1", Port: 8080}, Started: true, LogPath: "/fresh/serve.log"}, nil
}
require.NoError(t, executeDaemonCommand(t, *deps, out, "restart"))
assert.Equal(t, []string{"config", "data", "stop", "start"}, order)
assert.Contains(t, out.String(), "restarted")
}
func TestDaemonRestartStreamsProgressUntilReady(t *testing.T) {
deps, out := daemonCommandTestDeps(t)
deps.writableRecords = func(string, string) ([]daemon.RuntimeRecord, error) {
return []daemon.RuntimeRecord{testWritableRecord(91, "/runtime/91.json")}, nil
}
deps.startBackground = func(
_ config.Config, _ []string, _ serveReplacementOptions,
policy backgroundLaunchPolicy,
) (backgroundLaunchResult, error) {
assert.True(t, policy.Attached)
require.NotNil(t, policy.Context)
require.NotNil(t, policy.OnLaunch)
require.NotNil(t, policy.OnProgress)
policy.OnLaunch(92, "/data/serve.log")
policy.OnProgress(&startupState{Phase: "opening database"}, 2*time.Second)
policy.OnProgress(&startupState{Phase: "opening database"}, 4*time.Second)
policy.OnProgress(&startupState{
Phase: "initial sync", Detail: "claude: 120/450 sessions (27%)",
}, 6*time.Second)
policy.OnProgress(&startupState{
Phase: "initial sync", Detail: "claude: 120/450 sessions (27%)",
}, 8*time.Second)
policy.OnProgress(&startupState{
Phase: "initial sync", Detail: "claude: 120/450 sessions (27%)",
}, 11*time.Second)
policy.OnProgress(nil, 12*time.Second)
policy.OnProgress(&startupState{Detail: "incomplete snapshot"}, 13*time.Second)
policy.OnProgress(&startupState{Phase: "starting HTTP server"}, 18*time.Second)
return backgroundLaunchResult{
Runtime: &DaemonRuntime{
Record: daemon.RuntimeRecord{PID: 92},
Host: "127.0.0.1", Port: 8080,
},
Started: true, childPID: 92, LogPath: "/data/serve.log",
}, nil
}
require.NoError(t, executeDaemonCommand(t, *deps, out, "restart"))
assert.Equal(t, "Stopped agentsview (pid 91).\n"+
"Starting agentsview (pid 92)...\n"+
" log: /data/serve.log\n"+
" opening database (2s)\n"+
" initial sync: claude: 120/450 sessions (27%) (6s)\n"+
" initial sync: claude: 120/450 sessions (27%) (11s)\n"+
" starting HTTP server (18s)\n"+
"agentsview restarted at http://127.0.0.1:8080 (pid 92)\n", out.String())
}
func TestDaemonRestartProgressHeartbeatUsesUnroundedElapsed(t *testing.T) {
var out bytes.Buffer
progress := daemonRestartProgressWriter{w: &out}
startedAt := time.Unix(100, 0)
state := &startupState{StartedAt: startedAt, Phase: "initial sync"}
progress.progress(state, startupSnapshotElapsed(
state, startedAt, startedAt.Add(1400*time.Millisecond),
))
progress.progress(state, startupSnapshotElapsed(
state, startedAt, startedAt.Add(5600*time.Millisecond),
))
assert.Equal(t, " initial sync (1s)\n", out.String(),
"4.2 seconds of unchanged progress must not emit a heartbeat")
progress.progress(state, startupSnapshotElapsed(
state, startedAt, startedAt.Add(6400*time.Millisecond),
))
assert.Equal(t,
" initial sync (1s)\n"+
" initial sync (6s)\n",
out.String(),
)
}
func TestDaemonRestartCancellationLeavesReplacementRunning(t *testing.T) {
deps, out := daemonCommandTestDeps(t)
deps.writableRecords = func(string, string) ([]daemon.RuntimeRecord, error) {
return []daemon.RuntimeRecord{testWritableRecord(91, "/runtime/91.json")}, nil
}
ctx, cancel := context.WithCancel(context.Background())
var stopped []int
deps.stopProcess = func(rec daemon.RuntimeRecord, _ time.Duration) error {
stopped = append(stopped, rec.PID)
return nil
}
deps.startBackground = func(
_ config.Config, _ []string, _ serveReplacementOptions,
policy backgroundLaunchPolicy,
) (backgroundLaunchResult, error) {
policy.OnLaunch(92, "/data/serve.log")
cancel()
return backgroundLaunchResult{
Started: true, childPID: 92, LogPath: "/data/serve.log",
}, context.Canceled
}
cmd := newDaemonCommandWithDeps(*deps)
cmd.SetContext(ctx)
cmd.SetOut(out)
cmd.SetErr(out)
cmd.SetArgs([]string{"restart"})
err := cmd.Execute()
require.Error(t, err)
assert.ErrorContains(t, err, "pid 92")
assert.ErrorContains(t, err, "/data/serve.log")
assert.ErrorContains(t, err, "child continues running")
assert.ErrorContains(t, err, "agentsview daemon status")
assert.Equal(t, []int{91}, stopped,
"cancellation must not stop the replacement child")
assert.Contains(t, out.String(), "Starting agentsview (pid 92)...")
}
func TestDaemonRestartStoppedStartsAndReadOnlySurvives(t *testing.T) {
deps, out := daemonCommandTestDeps(t)
dir := runtimeTestDir(t)
rec := testWritableRecord(os.Getpid(), "")
rec.Metadata[runtimeReadOnly] = "true"
path, err := writeRuntimeRecordForTest(dir, rec)
require.NoError(t, err)
deps.resolveDataDir = func() (string, error) { return dir, nil }
deps.mkdirAll = func(path string, mode os.FileMode) error {
assert.Equal(t, dir, path)
return nil
}
deps.loadConfig = func() (config.Config, error) {
return config.Config{DataDir: dir, DBPath: filepath.Join(dir, "sessions.db")}, nil
}
deps.writableRecords = writableDaemonRecords
stopCalls := 0
deps.stopProcess = func(daemon.RuntimeRecord, time.Duration) error { stopCalls++; return nil }
require.NoError(t, executeDaemonCommand(t, *deps, out, "restart"))
assert.Zero(t, stopCalls)
assert.FileExists(t, path, "restart must preserve the read-only runtime")
assert.Contains(t, out.String(), "started (was not running)")
}
func TestServeRestartDelegatesToCanonicalWriterOnlyRestart(t *testing.T) {
requirePOSIXSignals(t, "requires POSIX sleep/process signals")
deps, out := daemonCommandTestDeps(t)
dir := runtimeTestDir(t)
pid, _ := startReapedSleepProcess(t)
createTime, ok := processCreateTimeMillis(pid)
require.True(t, ok, "read-only child create time must be available")
rec := testWritableRecord(pid, "")
rec.Metadata[runtimeReadOnly] = "true"
rec.Metadata[runtimeCreateTime] = strconv.FormatInt(createTime, 10)
path, err := writeRuntimeRecordForTest(dir, rec)
require.NoError(t, err)
deps.resolveDataDir = func() (string, error) { return dir, nil }
deps.mkdirAll = func(path string, _ os.FileMode) error {
assert.Equal(t, dir, path)
return nil
}
deps.loadConfig = func() (config.Config, error) {
return config.Config{DataDir: dir, DBPath: filepath.Join(dir, "sessions.db")}, nil
}
deps.writableRecords = writableDaemonRecords
stopCalls := 0
deps.stopProcess = func(daemon.RuntimeRecord, time.Duration) error {
stopCalls++
return nil
}
var gotPolicy backgroundLaunchPolicy
deps.startBackground = func(
_ config.Config, args []string, _ serveReplacementOptions,
policy backgroundLaunchPolicy,
) (backgroundLaunchResult, error) {
assert.Equal(t, []string{"serve"}, args)
gotPolicy = policy
return backgroundLaunchResult{
Runtime: &DaemonRuntime{
Record: daemon.RuntimeRecord{PID: 315},
Host: "127.0.0.1", Port: 8080,
},
Started: true,
}, nil
}
require.NoError(t, executeServeCommand(t, *deps, out, "restart"))
assert.Zero(t, stopCalls, "read-only servers must not be stopped")
assert.True(t, daemon.ProcessAlive(pid), "read-only server must remain alive")
assert.FileExists(t, path, "read-only runtime record must survive restart")
assert.True(t, gotPolicy.ConfigOnly)
assert.Equal(t, "daemon restart", gotPolicy.Operation)
assert.False(t, gotPolicy.Attached)
assert.Nil(t, gotPolicy.Context)
assert.Nil(t, gotPolicy.OnLaunch)
assert.Nil(t, gotPolicy.OnProgress)
assert.Contains(t, out.String(), "started (was not running)")
}
func TestServeRestartRejectsServeFlagsAndArguments(t *testing.T) {
tests := []struct {
name string
args []string
}{
{name: "host", args: []string{"restart", "--host", "0.0.0.0"}},
{name: "no sync", args: []string{"restart", "--no-sync"}},
{name: "host before subcommand", args: []string{"--host", "0.0.0.0", "restart"}},
{name: "no sync before subcommand", args: []string{"--no-sync", "restart"}},
{name: "argument", args: []string{"restart", "extra"}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
deps, out := daemonCommandTestDeps(t)
starts := 0
deps.startBackground = func(
config.Config, []string, serveReplacementOptions,
backgroundLaunchPolicy,
) (backgroundLaunchResult, error) {
starts++
return backgroundLaunchResult{}, nil
}
err := executeServeCommand(t, *deps, out, tt.args...)
require.Error(t, err)
assert.Zero(t, starts)
})
}
}
func TestDaemonRestartFailurePathsSignalNobody(t *testing.T) {
tests := []struct {
name string
setup func(*daemonCommandDeps)
want string
}{
{name: "data version", setup: func(d *daemonCommandDeps) {
d.checkDataVersion = func(string) error { return errors.New("data too new") }
}, want: "data too new"},
{name: "persistent start", setup: func(d *daemonCommandDeps) {
d.isStarting = func(string) bool { return true }
d.readStartupState = func(string) *startupState { return &startupState{PID: 111, LogPath: "/tmp/log"} }
}, want: "startup is still in progress"},
{name: "unconfirmed", setup: func(d *daemonCommandDeps) {
d.writableRecords = func(string, string) ([]daemon.RuntimeRecord, error) {
return []daemon.RuntimeRecord{testWritableRecord(112, "/runtime/112.json")}, nil
}
d.stopTargetConfirmed = func(daemon.RuntimeRecord, string) bool { return false }
}, want: "pid 112"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
deps, out := daemonCommandTestDeps(t)
tt.setup(deps)
signals := 0
deps.stopProcess = func(daemon.RuntimeRecord, time.Duration) error { signals++; return nil }
err := executeDaemonCommand(t, *deps, out, "restart")
require.Error(t, err)
assert.ErrorContains(t, err, tt.want)
assert.Zero(t, signals)
})
}
}
func TestDaemonRestartUsesStartupStateFallbackWhileStarting(t *testing.T) {
deps, out := daemonCommandTestDeps(t)
deps.isStarting = func(string) bool { return true }
deps.writableRuntime = func(string, string) *DaemonRuntime {
return &DaemonRuntime{
Record: testWritableRecord(113, ""),
Host: "127.0.0.1",
Port: 8113,
RuntimeFallback: true,
}
}
var stopped daemon.RuntimeRecord
deps.stopProcess = func(rec daemon.RuntimeRecord, _ time.Duration) error {
stopped = rec
return nil
}
deps.startBackground = func(
_ config.Config, args []string, _ serveReplacementOptions,
policy backgroundLaunchPolicy,
) (backgroundLaunchResult, error) {
assert.Equal(t, []string{"serve"}, args)
assert.Equal(t, "daemon restart", policy.Operation)
return backgroundLaunchResult{
Runtime: &DaemonRuntime{
Record: daemon.RuntimeRecord{PID: 315},
Host: "127.0.0.1", Port: 8080,
},
Started: true,
}, nil
}
require.NoError(t, executeDaemonCommand(t, *deps, out, "restart"))
assert.Equal(t, 113, stopped.PID)
assert.Contains(t, out.String(), "restarted")
assert.NotContains(t, out.String(), "startup is still in progress")
}
func TestDaemonRestartUsesStartupStateFallbackWhenRuntimeStoreInspectionFails(t *testing.T) {
deps, out := daemonCommandTestDeps(t)
deps.writableRecords = func(string, string) ([]daemon.RuntimeRecord, error) {
return nil, errors.New("store unavailable")
}
deps.writableRuntime = func(string, string) *DaemonRuntime {
return &DaemonRuntime{Record: testWritableRecord(114, ""), RuntimeFallback: true}
}
var stopped daemon.RuntimeRecord
deps.stopProcess = func(rec daemon.RuntimeRecord, _ time.Duration) error {
stopped = rec
return nil
}
require.NoError(t, executeDaemonCommand(t, *deps, out, "restart"))
assert.Equal(t, 114, stopped.PID)
assert.Contains(t, out.String(), "restarted")
}
func TestDaemonMutationsPreserveRuntimeStoreInspectionErrorWithoutFallback(t *testing.T) {
for _, command := range []string{"stop", "restart"} {
t.Run(command, func(t *testing.T) {
deps, out := daemonCommandTestDeps(t)
deps.writableRecords = func(string, string) ([]daemon.RuntimeRecord, error) {
return nil, errors.New("store unavailable")
}
err := executeDaemonCommand(t, *deps, out, command)
require.Error(t, err)
assert.ErrorContains(t, err, "store unavailable")
})
}
}
func TestDaemonRestartInvalidServeConfigDoesNotStopOrStart(t *testing.T) {
tests := []struct {
name string
cfg config.Config
wantErr string
}{
{
name: "non-loopback host without auth",
cfg: config.Config{Host: "0.0.0.0", Port: 8080},
wantErr: "require_auth",
},
{
name: "unsupported proxy",
cfg: config.Config{Host: "127.0.0.1", Port: 8080, Proxy: config.ProxyConfig{Mode: "nginx"}},
wantErr: "unsupported proxy mode",
},
{
name: "https proxy without TLS files",
cfg: config.Config{
Host: "127.0.0.1", Port: 8080,
PublicURL: "https://viewer.example.test",
Proxy: config.ProxyConfig{Mode: "caddy", Bin: os.Args[0]},
},
wantErr: "requires both tls_cert and tls_key",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
deps, out := daemonCommandTestDeps(t)
tt.cfg.DataDir = t.TempDir()
tt.cfg.DBPath = filepath.Join(tt.cfg.DataDir, "sessions.db")
deps.resolveDataDir = func() (string, error) { return tt.cfg.DataDir, nil }
deps.mkdirAll = func(path string, _ os.FileMode) error {
assert.Equal(t, tt.cfg.DataDir, path)
return nil
}
deps.loadConfig = func() (config.Config, error) { return tt.cfg, nil }
deps.validateConfig = validateServeConfig
deps.writableRecords = func(string, string) ([]daemon.RuntimeRecord, error) {
return []daemon.RuntimeRecord{testWritableRecord(121, "/runtime/121.json")}, nil
}
dataChecks, signals, starts := 0, 0, 0
deps.checkDataVersion = func(string) error { dataChecks++; return nil }
deps.stopProcess = func(daemon.RuntimeRecord, time.Duration) error { signals++; return nil }
deps.startBackground = func(config.Config, []string, serveReplacementOptions, backgroundLaunchPolicy) (backgroundLaunchResult, error) {
starts++
return backgroundLaunchResult{}, nil
}
err := executeDaemonCommand(t, *deps, out, "restart")
require.Error(t, err)
assert.ErrorContains(t, err, "daemon restart: invalid config")
assert.ErrorContains(t, err, tt.wantErr)
assert.Zero(t, dataChecks)
assert.Zero(t, signals)
assert.Zero(t, starts)
})
}
}
func TestDaemonRestartChildFailureIncludesLogAndHeldLock(t *testing.T) {
deps, out := daemonCommandTestDeps(t)
lock := &testDaemonLaunchLock{}
lockAcquisitions := 0
deps.acquireLaunchLockWithError = func(string) (daemonLaunchLock, bool, error) {
lockAcquisitions++
return lock, true, nil
}
deps.writableRecords = func(string, string) ([]daemon.RuntimeRecord, error) {
return []daemon.RuntimeRecord{testWritableRecord(131, "/runtime/131.json")}, nil
}
var order []string
deps.stopProcess = func(rec daemon.RuntimeRecord, _ time.Duration) error {
assert.Equal(t, 131, rec.PID)
assert.False(t, lock.unlocked, "restart must hold the launch lock while stopping")
order = append(order, "stop")
return nil
}
deps.startBackground = func(config.Config, []string, serveReplacementOptions, backgroundLaunchPolicy) (backgroundLaunchResult, error) {
assert.False(t, lock.unlocked, "restart must retain launch lock across stop/start gap")
order = append(order, "start")
return backgroundLaunchResult{LogPath: "/tmp/serve.log"}, errors.New("child exited")
}
err := executeDaemonCommand(t, *deps, out, "restart")
require.Error(t, err)
assert.ErrorContains(t, err, "child exited")
assert.ErrorContains(t, err, "/tmp/serve.log")
assert.Equal(t, []string{"stop", "start"}, order)
assert.Equal(t, 1, lockAcquisitions)
assert.True(t, lock.unlocked)
}
func TestDaemonStartConfigFailureReturnsError(t *testing.T) {
deps, out := daemonCommandTestDeps(t)
deps.loadConfig = func() (config.Config, error) { return config.Config{}, errors.New("config invalid") }
err := executeDaemonCommand(t, *deps, out, "start")
require.Error(t, err)
assert.True(t, strings.Contains(err.Error(), "config invalid"))
}