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

1214 lines
36 KiB
Go

package main
import (
"context"
"encoding/json"
"errors"
"fmt"
"net"
"os"
"path/filepath"
"strconv"
"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"
)
func daemonRuntimeDir(t *testing.T) string {
t.Helper()
return filepath.Join(t.TempDir(), "runtime")
}
// freeTCPListener binds to a free loopback port and returns the
// listener (caller closes) and the port number. Tests that need
// an unreachable daemon close the listener after reserving the port.
func freeTCPListener(t *testing.T) (net.Listener, int) {
t.Helper()
l, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)
t.Cleanup(func() { l.Close() })
port := l.Addr().(*net.TCPAddr).Port
return l, port
}
// writeDaemonRuntimeForTest writes a runtime record for dir and
// registers its removal, centralizing the WriteDaemonRuntime plus
// RemoveDaemonRuntime cleanup pairing the transport tests repeat.
func writeDaemonRuntimeForTest(
t *testing.T, dir, host string, port int, daemonVersion string, readOnly bool,
) {
t.Helper()
_, err := WriteDaemonRuntime(dir, host, port, daemonVersion, readOnly)
require.NoError(t, err)
t.Cleanup(func() { RemoveDaemonRuntime(dir) })
}
// writeUnreachableDaemonRuntime reserves a free loopback port, releases
// it so the TCP probe fails, then writes a live-PID runtime record for
// it and registers cleanup. The record looks owned (live PID) but the
// daemon cannot be reached. Returns the unreachable port.
func writeUnreachableDaemonRuntime(t *testing.T, dir string, readOnly bool) int {
t.Helper()
ln, port := freeTCPListener(t)
ln.Close()
writeDaemonRuntimeForTest(t, dir, "127.0.0.1", port, "test", readOnly)
return port
}
func TestDetectTransport_UsesStartupStateFallbackWithoutRuntimeRecord(t *testing.T) {
dir := runtimeTestDir(t)
host, port := testPingServer(t)
createTime, ok := processCreateTimeMillis(os.Getpid())
require.True(t, ok)
writeStartupFallbackFixture(t, dir, host, port, os.Getpid(), strconv.FormatInt(createTime, 10))
tr, err := detectTransportContext(context.Background(), dir, "", time.Second)
require.NoError(t, err)
assert.Equal(t, transportHTTP, tr.Mode)
assert.Equal(t, fmt.Sprintf("http://%s:%d", host, port), tr.URL)
}
func TestDetectTransport_UsesStartupStateFallbackWhileExternalLockRemainsHeld(t *testing.T) {
dir := runtimeTestDir(t)
holdExternalStartupLockForTest(t, dir)
host, port := testPingServer(t)
createTime, ok := processCreateTimeMillis(os.Getpid())
require.True(t, ok)
state := startupState{
PID: os.Getpid(),
StartedAt: time.Now().Add(-time.Minute),
Phase: "starting HTTP server",
Host: host,
Port: port,
RuntimeError: "permission denied writing runtime record",
CreateTime: strconv.FormatInt(createTime, 10),
APIVersion: daemonAPIVersion,
DataVersion: db.CurrentDataVersion(),
UpdatedAt: time.Now(),
}
data, err := json.Marshal(state)
require.NoError(t, err)
require.NoError(t, os.WriteFile(startupStatePath(dir), data, 0o600))
tr, err := detectTransportContext(context.Background(), dir, "", time.Second)
require.NoError(t, err)
assert.Equal(t, transportHTTP, tr.Mode)
assert.Equal(t, fmt.Sprintf("http://%s:%d", host, port), tr.URL)
}
func TestEnsureTransport_SameVersionFallbackDoesNotAutostart(t *testing.T) {
dir := runtimeTestDir(t)
host, port := testPingServer(t)
createTime, ok := processCreateTimeMillis(os.Getpid())
require.True(t, ok)
writeStartupFallbackFixture(t, dir, host, port, os.Getpid(), strconv.FormatInt(createTime, 10))
setTestVersion(t, "test")
forbidStartBackgroundServeForTransport(t,
"same-version fallback daemon must not trigger autostart")
cfg := config.Config{DataDir: dir}
tr, err := ensureTransport(&cfg, transportIntentRead, time.Second)
require.NoError(t, err)
assert.Equal(t, transportHTTP, tr.Mode)
assert.Equal(t, fmt.Sprintf("http://%s:%d", host, port), tr.URL)
require.NotNil(t, tr.Runtime)
assert.Equal(t, "test", tr.Runtime.Record.Version)
}
// incompatibleRuntimeRecord builds a writable runtime record whose API
// version metadata is "0", which the compatibility check rejects. It
// models a daemon from an older release that still owns the archive.
func incompatibleRuntimeRecord(
host string, port int, daemonVersion string, noSync bool,
) daemon.RuntimeRecord {
meta := map[string]string{
runtimeHost: host,
runtimePort: strconv.Itoa(port),
runtimeReadOnly: "false",
runtimeAPIVersion: "0",
runtimeDataVersion: strconv.Itoa(db.CurrentDataVersion()),
}
if noSync {
meta[runtimeNoSync] = "true"
}
return daemon.RuntimeRecord{
PID: os.Getpid(),
Network: daemon.NetworkTCP,
Address: net.JoinHostPort(host, strconv.Itoa(port)),
Service: daemonService,
Version: daemonVersion,
StartedAt: time.Now(),
Metadata: meta,
}
}
// writeIncompatibleDaemonRuntime writes an incompatibleRuntimeRecord for
// dir and registers its removal.
func writeIncompatibleDaemonRuntime(
t *testing.T, dir, host string, port int, daemonVersion string, noSync bool,
) {
t.Helper()
_, err := writeRuntimeRecordForTest(
dir, incompatibleRuntimeRecord(host, port, daemonVersion, noSync),
)
require.NoError(t, err)
t.Cleanup(func() { RemoveDaemonRuntime(dir) })
}
// setTestVersion overrides the package build version for the duration of
// the test and restores it on cleanup.
func setTestVersion(t *testing.T, value string) {
t.Helper()
old := version
version = value
t.Cleanup(func() { version = old })
}
// stubStartBackgroundServeForTransport swaps the background-serve hook
// for the test and restores the original on cleanup.
func stubStartBackgroundServeForTransport(
t *testing.T,
fn func(context.Context, *config.Config, time.Duration) (*DaemonRuntime, error),
) {
t.Helper()
old := startBackgroundServeForTransport
startBackgroundServeForTransport = fn
t.Cleanup(func() { startBackgroundServeForTransport = old })
}
// forbidStartBackgroundServeForTransport fails the test if the
// background-serve hook is invoked, with msg describing the violation.
func forbidStartBackgroundServeForTransport(t *testing.T, msg string) {
t.Helper()
stubStartBackgroundServeForTransport(t, func(
context.Context, *config.Config, time.Duration,
) (*DaemonRuntime, error) {
t.Helper()
t.Fatal(msg)
return nil, nil
})
}
// stubStopDaemonRuntimeForUpgrade swaps the upgrade-stop hook for the
// test and restores the original on cleanup.
func stubStopDaemonRuntimeForUpgrade(
t *testing.T,
fn func(config.Config, *DaemonRuntime) error,
) {
t.Helper()
old := stopDaemonRuntimeForUpgrade
stopDaemonRuntimeForUpgrade = fn
t.Cleanup(func() { stopDaemonRuntimeForUpgrade = old })
}
// forbidStopDaemonRuntimeForUpgrade fails the test if the upgrade-stop
// hook is invoked, with msg describing the violation.
func forbidStopDaemonRuntimeForUpgrade(t *testing.T, msg string) {
t.Helper()
stubStopDaemonRuntimeForUpgrade(t, func(
config.Config, *DaemonRuntime,
) error {
t.Helper()
t.Fatal(msg)
return nil
})
}
// stubWaitForDaemonStartupForTransport swaps the startup-wait hook for
// the test and restores the original on cleanup.
func stubWaitForDaemonStartupForTransport(
t *testing.T,
fn func(context.Context, string, time.Duration, ...string) bool,
) {
t.Helper()
old := waitForDaemonStartupForTransport
waitForDaemonStartupForTransport = fn
t.Cleanup(func() { waitForDaemonStartupForTransport = old })
}
func TestDetectTransport_NoDaemon_ReturnsDirect(t *testing.T) {
t.Parallel()
dir := daemonRuntimeDir(t)
tr, err := detectTransport(dir, "", 100*time.Millisecond)
require.NoError(t, err)
assert.Equal(t, transportDirect, tr.Mode)
assert.False(t, tr.ReadOnly)
assert.Empty(t, tr.URL)
}
func TestDetectTransport_LocalServe_ReturnsHTTPWriteCapable(t *testing.T) {
t.Parallel()
dir := daemonRuntimeDir(t)
host, port := testPingServer(t)
writeDaemonRuntimeForTest(t, dir, host, port, "test", false)
tr, err := detectTransport(dir, "", 100*time.Millisecond)
require.NoError(t, err)
assert.Equal(t, transportHTTP, tr.Mode)
assert.False(t, tr.ReadOnly)
assert.Contains(t, tr.URL, "http://127.0.0.1:"+strconv.Itoa(port))
}
func TestDetectTransport_PGServe_ReturnsReadOnlyHTTP(t *testing.T) {
t.Parallel()
dir := daemonRuntimeDir(t)
host, port := testPingServer(t)
writeDaemonRuntimeForTest(t, dir, host, port, "test", true)
tr, err := detectTransport(dir, "", 100*time.Millisecond)
require.NoError(t, err)
assert.Equal(t, transportHTTP, tr.Mode)
assert.True(t, tr.ReadOnly)
assert.Contains(t, tr.URL, "http://127.0.0.1:"+strconv.Itoa(port))
}
func TestDetectTransport_AuthenticatedDaemonUsesBearerToken(t *testing.T) {
t.Parallel()
dir := daemonRuntimeDir(t)
host, port := testAuthenticatedPingServer(t, "secret")
writeDaemonRuntimeForTest(t, dir, host, port, "test", false)
tr, err := detectTransport(dir, "secret", 100*time.Millisecond)
require.NoError(t, err)
assert.Equal(t, transportHTTP, tr.Mode)
assert.False(t, tr.ReadOnly)
assert.Contains(t, tr.URL, "http://127.0.0.1:"+strconv.Itoa(port))
}
// TestDetectTransport_LocalServeWritableRecordWins verifies that a
// writable kit runtime record is exposed as a write-capable HTTP transport.
func TestDetectTransport_LocalServeWritableRecordWins(t *testing.T) {
t.Parallel()
dir := daemonRuntimeDir(t)
host, writablePort := testPingServer(t)
writeDaemonRuntimeForTest(t, dir, host, writablePort, "test", false)
tr, err := detectTransport(dir, "", 100*time.Millisecond)
require.NoError(t, err)
assert.Equal(t, transportHTTP, tr.Mode)
assert.False(t, tr.ReadOnly)
assert.Contains(t, tr.URL,
"http://127.0.0.1:"+strconv.Itoa(writablePort),
"expected URL to point at the writable daemon")
}
// TestDetectTransport_PGServeUnreachable_AllowsDirectWrite verifies
// that an unprobeable pg serve runtime record does not prove daemon
// ownership, so direct access remains available.
func TestDetectTransport_PGServeUnreachable_AllowsDirectWrite(t *testing.T) {
t.Parallel()
dir := daemonRuntimeDir(t)
writeUnreachableDaemonRuntime(t, dir, true) // readOnly = pg serve
tr, err := detectTransport(dir, "", 100*time.Millisecond)
require.NoError(t, err)
assert.Equal(t, transportDirect, tr.Mode)
}
// TestDetectTransport_LocalDaemonUnreachable_SetsDirectReadOnly verifies
// that a writable runtime record suppresses direct writes even when
// the daemon ping is temporarily unavailable.
func TestDetectTransport_LocalDaemonUnreachable_SetsDirectReadOnly(t *testing.T) {
t.Parallel()
dir := daemonRuntimeDir(t)
writeUnreachableDaemonRuntime(t, dir, false) // writable local
tr, err := detectTransport(dir, "", 100*time.Millisecond)
require.NoError(t, err)
assert.Equal(t, transportDirect, tr.Mode)
assert.True(t, tr.DirectReadOnly)
}
func TestDetectTransport_IncompatibleDaemonSetsDirectReason(t *testing.T) {
dir := daemonRuntimeDir(t)
host, port := testPingServer(t)
writeIncompatibleDaemonRuntime(t, dir, host, port, "old", false)
tr, err := detectTransport(dir, "", 100*time.Millisecond)
require.NoError(t, err)
assert.Equal(t, transportDirect, tr.Mode)
assert.True(t, tr.DirectReadOnly)
assert.True(t, tr.DirectIncompatible)
assert.Contains(t, tr.DirectReason, "API version")
}
func TestDetectTransport_LocalDaemonUnreachableDoesNotSetDirectIncompatible(t *testing.T) {
t.Parallel()
dir := daemonRuntimeDir(t)
writeUnreachableDaemonRuntime(t, dir, false)
tr, err := detectTransport(dir, "", 100*time.Millisecond)
require.NoError(t, err)
assert.Equal(t, transportDirect, tr.Mode)
assert.True(t, tr.DirectReadOnly)
assert.False(t, tr.DirectIncompatible)
assert.Equal(t, errLocalDaemonUnreachable.Error(), tr.DirectReason)
}
// TestDetectTransport_DaemonStarting simulates a server that's
// starting up (start lock held, no runtime record, no listener).
// The held kit lock makes IsDaemonStarting return true.
// The helper waits out the timeout then falls back to direct.
func TestDetectTransport_DaemonStarting_FallsBackToDirect(t *testing.T) {
t.Parallel()
dir := daemonRuntimeDir(t)
MarkDaemonStarting(dir)
t.Cleanup(func() { UnmarkDaemonStarting(dir) })
tr, err := detectTransport(dir, "", 100*time.Millisecond)
require.NoError(t, err)
// Still no runtime record after wait, so IsDaemonActive sees
// only the start lock and returns direct (writable) since
// no runtime record means no daemon claim.
assert.Equal(t, transportDirect, tr.Mode)
}
func TestEnsureTransport_ReadIntentStartsDaemon(t *testing.T) {
dir := daemonRuntimeDir(t)
cfg := config.Config{DataDir: dir}
var started bool
stubStartBackgroundServeForTransport(t, func(
_ context.Context, gotCfg *config.Config, wait time.Duration,
) (*DaemonRuntime, error) {
started = true
assert.Equal(t, dir, gotCfg.DataDir)
assert.Equal(t, backgroundAutoStartReadyTimeout, wait)
return &DaemonRuntime{
Host: "127.0.0.1",
Port: 12345,
}, nil
})
tr, err := ensureTransport(&cfg, transportIntentRead, 0)
require.NoError(t, err)
assert.True(t, started)
assert.Equal(t, transportHTTP, tr.Mode)
assert.Equal(t, "http://127.0.0.1:12345", tr.URL)
}
func TestEnsureTransport_ReadIntentNoDaemonEnvRefusesDirectRead(t *testing.T) {
dir := daemonRuntimeDir(t)
t.Setenv("AGENTSVIEW_NO_DAEMON", "1")
cfg := config.Config{DataDir: dir}
forbidStartBackgroundServeForTransport(t,
"AGENTSVIEW_NO_DAEMON must suppress daemon start")
_, err := ensureTransport(&cfg, transportIntentRead, 100*time.Millisecond)
require.Error(t, err)
assert.Contains(t, err.Error(), "direct SQLite reads are not supported")
assert.Contains(t, err.Error(), "agentsview daemon start")
}
func TestEnsureTransport_ReadIntentUnreachableDaemonRefusesDirectRead(t *testing.T) {
dir := daemonRuntimeDir(t)
writeUnreachableDaemonRuntime(t, dir, false)
cfg := config.Config{DataDir: dir}
forbidStartBackgroundServeForTransport(t,
"unreachable live daemon must not trigger a second start")
_, err := ensureTransport(&cfg, transportIntentRead, 100*time.Millisecond)
require.ErrorIs(t, err, errLocalDaemonUnreachable)
}
func TestEnsureTransport_ArchiveWriteStartsDaemon(t *testing.T) {
dir := daemonRuntimeDir(t)
cfg := config.Config{DataDir: dir, AuthToken: "secret"}
var started bool
stubStartBackgroundServeForTransport(t, func(
_ context.Context, gotCfg *config.Config, wait time.Duration,
) (*DaemonRuntime, error) {
started = true
assert.Equal(t, dir, gotCfg.DataDir)
assert.Equal(t, 100*time.Millisecond, wait)
return &DaemonRuntime{
Host: "127.0.0.1",
Port: 12345,
}, nil
})
tr, err := ensureTransport(
&cfg, transportIntentArchiveWrite, 100*time.Millisecond,
)
require.NoError(t, err)
assert.True(t, started)
assert.Equal(t, transportHTTP, tr.Mode)
assert.Equal(t, "http://127.0.0.1:12345", tr.URL)
}
func TestEnsureTransport_ArchiveWriteRestartsOlderDaemon(t *testing.T) {
dir := daemonRuntimeDir(t)
host, port := testPingServer(t)
_, err := WriteDaemonRuntimeWithAuthAndNoSync(
dir, host, port, "1.0.0", false, false, true,
)
require.NoError(t, err)
t.Cleanup(func() { RemoveDaemonRuntime(dir) })
setTestVersion(t, "1.1.0")
var started bool
stubStartBackgroundServeForTransport(t, func(
_ context.Context, gotCfg *config.Config, _ time.Duration,
) (*DaemonRuntime, error) {
started = true
assert.True(t, gotCfg.NoSync)
return &DaemonRuntime{
Host: "127.0.0.1",
Port: 23456,
}, nil
})
cfg := config.Config{DataDir: dir}
tr, err := ensureTransport(
&cfg, transportIntentArchiveWrite, 100*time.Millisecond,
)
require.NoError(t, err)
assert.True(t, started)
assert.Equal(t, transportHTTP, tr.Mode)
assert.Equal(t, "http://127.0.0.1:23456", tr.URL)
}
func TestEnsureTransport_ReadIntentRestartsOlderDaemon(t *testing.T) {
dir := daemonRuntimeDir(t)
host, port := testPingServer(t)
_, err := WriteDaemonRuntimeWithAuthAndNoSync(
dir, host, port, "1.0.0", false, false, true,
)
require.NoError(t, err)
t.Cleanup(func() { RemoveDaemonRuntime(dir) })
setTestVersion(t, "1.1.0")
var started bool
stubStartBackgroundServeForTransport(t, func(
_ context.Context, gotCfg *config.Config, _ time.Duration,
) (*DaemonRuntime, error) {
started = true
assert.True(t, gotCfg.NoSync)
return &DaemonRuntime{
Host: "127.0.0.1",
Port: 23456,
}, nil
})
cfg := config.Config{DataDir: dir}
tr, err := ensureTransport(
&cfg, transportIntentRead, 100*time.Millisecond,
)
require.NoError(t, err)
assert.True(t, started)
assert.Equal(t, transportHTTP, tr.Mode)
assert.Equal(t, "http://127.0.0.1:23456", tr.URL)
}
func TestEnsureTransport_ReadIntentNoDaemonEnvRefusesOlderDaemon(
t *testing.T,
) {
dir := daemonRuntimeDir(t)
host, port := testPingServer(t)
writeDaemonRuntimeForTest(t, dir, host, port, "1.0.0", false)
t.Setenv("AGENTSVIEW_NO_DAEMON", "1")
setTestVersion(t, "1.1.0")
forbidStartBackgroundServeForTransport(t,
"AGENTSVIEW_NO_DAEMON must suppress daemon replacement")
cfg := config.Config{DataDir: dir}
tr, err := ensureTransport(
&cfg, transportIntentRead, 100*time.Millisecond,
)
require.Error(t, err)
assert.Equal(t, transport{}, tr)
assert.Contains(t, err.Error(), "daemon restart required")
assert.Contains(t, err.Error(), "agentsview daemon restart")
}
func TestEnsureTransport_ReadIntentPreservesExplicitNoSyncWhenRestartingOlderDaemon(
t *testing.T,
) {
dir := daemonRuntimeDir(t)
host, port := testPingServer(t)
_, err := WriteDaemonRuntimeWithAuthAndNoSync(
dir, host, port, "1.0.0", false, false, false,
)
require.NoError(t, err)
t.Cleanup(func() { RemoveDaemonRuntime(dir) })
setTestVersion(t, "1.1.0")
var started bool
stubStartBackgroundServeForTransport(t, func(
_ context.Context, gotCfg *config.Config, _ time.Duration,
) (*DaemonRuntime, error) {
started = true
assert.True(t, gotCfg.NoSync)
return &DaemonRuntime{
Host: "127.0.0.1",
Port: 23456,
}, nil
})
cfg := config.Config{DataDir: dir, NoSync: true}
tr, err := ensureTransport(
&cfg, transportIntentRead, 100*time.Millisecond,
)
require.NoError(t, err)
assert.True(t, started)
assert.Equal(t, transportHTTP, tr.Mode)
}
func TestEnsureTransport_ArchiveWriteNoDaemonEnvKeepsOlderDaemon(t *testing.T) {
dir := daemonRuntimeDir(t)
host, port := testPingServer(t)
writeDaemonRuntimeForTest(t, dir, host, port, "1.0.0", false)
t.Setenv("AGENTSVIEW_NO_DAEMON", "1")
setTestVersion(t, "1.1.0")
forbidStopDaemonRuntimeForUpgrade(t,
"AGENTSVIEW_NO_DAEMON must not replace an older daemon")
forbidStartBackgroundServeForTransport(t,
"AGENTSVIEW_NO_DAEMON must not start a replacement daemon")
cfg := config.Config{DataDir: dir}
tr, err := ensureTransport(
&cfg, transportIntentArchiveWrite, 100*time.Millisecond,
)
require.NoError(t, err)
assert.Equal(t, transportHTTP, tr.Mode)
assert.Equal(t, "http://"+net.JoinHostPort(host, strconv.Itoa(port)), tr.URL)
}
func TestEnsureTransport_ArchiveWriteStopsOlderDaemonUnderLaunchLock(
t *testing.T,
) {
dir := daemonRuntimeDir(t)
host, port := testPingServer(t)
writeDaemonRuntimeForTest(t, dir, host, port, "1.0.0", false)
// This test exercises the real auto-start machinery; bypass the
// test-binary guard in autoStartBackgroundServe.
stubStartBackgroundServeForTransport(t, ensureBackgroundServe)
setTestVersion(t, "1.1.0")
stopErr := errors.New("stop after launch lock")
stubStopDaemonRuntimeForUpgrade(t, func(
config.Config, *DaemonRuntime,
) error {
launchLock, ok := acquireBackgroundLaunchLock(dir)
if ok {
require.NoError(t, launchLock.Unlock())
assert.Fail(t, "older daemon stopped without launch lock held")
}
return stopErr
})
cfg := config.Config{DataDir: dir}
_, err := ensureTransport(
&cfg, transportIntentArchiveWrite, 100*time.Millisecond,
)
assert.ErrorIs(t, err, stopErr)
}
func TestEnsureTransport_ArchiveWriteRestartsIncompatibleOlderDaemon(t *testing.T) {
dir := daemonRuntimeDir(t)
host, port := testPingServer(t)
writeIncompatibleDaemonRuntime(t, dir, host, port, "1.0.0", true)
setTestVersion(t, "1.1.0")
var started bool
stubStartBackgroundServeForTransport(t, func(
_ context.Context, gotCfg *config.Config, _ time.Duration,
) (*DaemonRuntime, error) {
started = true
assert.True(t, gotCfg.NoSync)
return &DaemonRuntime{
Host: "127.0.0.1",
Port: 23456,
}, nil
})
cfg := config.Config{DataDir: dir}
tr, err := ensureTransport(
&cfg, transportIntentArchiveWrite, 100*time.Millisecond,
)
require.NoError(t, err)
assert.True(t, started)
assert.Equal(t, transportHTTP, tr.Mode)
assert.Equal(t, "http://127.0.0.1:23456", tr.URL)
}
func TestEnsureTransport_ReadIntentPreservesExplicitNoSyncWhenRestartingIncompatibleDaemon(
t *testing.T,
) {
dir := daemonRuntimeDir(t)
host, port := testPingServer(t)
writeIncompatibleDaemonRuntime(t, dir, host, port, "1.0.0", false)
setTestVersion(t, "1.1.0")
var started bool
stubStartBackgroundServeForTransport(t, func(
_ context.Context, gotCfg *config.Config, _ time.Duration,
) (*DaemonRuntime, error) {
started = true
assert.True(t, gotCfg.NoSync)
return &DaemonRuntime{
Host: "127.0.0.1",
Port: 23456,
}, nil
})
cfg := config.Config{DataDir: dir, NoSync: true}
tr, err := ensureTransport(
&cfg, transportIntentRead, 100*time.Millisecond,
)
require.NoError(t, err)
assert.True(t, started)
assert.Equal(t, transportHTTP, tr.Mode)
}
func TestEnsureTransport_ReadIntentRestartsIncompatibleOlderDaemon(t *testing.T) {
dir := daemonRuntimeDir(t)
host, port := testPingServer(t)
writeIncompatibleDaemonRuntime(t, dir, host, port, "1.0.0", true)
setTestVersion(t, "1.1.0")
var started bool
stubStartBackgroundServeForTransport(t, func(
_ context.Context, gotCfg *config.Config, _ time.Duration,
) (*DaemonRuntime, error) {
started = true
assert.True(t, gotCfg.NoSync)
return &DaemonRuntime{
Host: "127.0.0.1",
Port: 23456,
}, nil
})
cfg := config.Config{DataDir: dir}
tr, err := ensureTransport(
&cfg, transportIntentRead, 100*time.Millisecond,
)
require.NoError(t, err)
assert.True(t, started)
assert.Equal(t, transportHTTP, tr.Mode)
assert.Equal(t, "http://127.0.0.1:23456", tr.URL)
}
func TestEnsureTransport_ArchiveWriteRestartsIncompatibleDaemonAfterExternalStartupAbort(
t *testing.T,
) {
setStartProbeTickForTest(t, 25*time.Millisecond)
dir := daemonRuntimeDir(t)
host, port := testPingServer(t)
writeIncompatibleDaemonRuntime(t, dir, host, port, "1.0.0", true)
unlockStart := holdExternalDaemonStartLock(t, dir)
setTestVersion(t, "1.1.0")
var started bool
stubStartBackgroundServeForTransport(t, func(
_ context.Context, gotCfg *config.Config, _ time.Duration,
) (*DaemonRuntime, error) {
started = true
assert.Equal(t, dir, gotCfg.DataDir)
assert.True(t, gotCfg.NoSync)
return &DaemonRuntime{
Host: "127.0.0.1",
Port: 23456,
}, nil
})
released := make(chan struct{})
go func() {
time.Sleep(2 * startProbeTick())
unlockStart()
close(released)
}()
cfg := config.Config{DataDir: dir}
tr, err := ensureTransport(
&cfg, transportIntentArchiveWrite, time.Second,
)
<-released
require.NoError(t, err)
assert.True(t, started)
assert.Equal(t, transportHTTP, tr.Mode)
assert.Equal(t, "http://127.0.0.1:23456", tr.URL)
}
func TestEnsureTransport_ArchiveWriteRejectsUnsafeOlderDaemonRestart(t *testing.T) {
dir := daemonRuntimeDir(t)
host, port := testPingServer(t)
writeDaemonRuntimeForTest(t, dir, host, port, "1.0.0", false)
setTestVersion(t, "1.1.0")
forbidStopDaemonRuntimeForUpgrade(t,
"unsafe restart must be rejected before stopping daemon")
forbidStartBackgroundServeForTransport(t,
"unsafe restart must be rejected before daemon start")
cfg := config.Config{DataDir: dir, Host: "0.0.0.0"}
_, err := ensureTransport(
&cfg, transportIntentArchiveWrite, 100*time.Millisecond,
)
require.Error(t, err)
assert.Contains(t, err.Error(), "refusing to auto-start")
assert.Contains(t, err.Error(), "0.0.0.0")
}
func TestEnsureTransport_ArchiveWriteDoesNotDowngradeNewerDaemon(t *testing.T) {
dir := daemonRuntimeDir(t)
host, port := testPingServer(t)
writeDaemonRuntimeForTest(t, dir, host, port, "1.1.0", false)
setTestVersion(t, "1.0.0")
forbidStopDaemonRuntimeForUpgrade(t, "older CLI must not stop a newer daemon")
forbidStartBackgroundServeForTransport(t,
"older CLI must not start over a newer daemon")
cfg := config.Config{DataDir: dir}
tr, err := ensureTransport(
&cfg, transportIntentArchiveWrite, 100*time.Millisecond,
)
require.NoError(t, err)
assert.Equal(t, transportHTTP, tr.Mode)
assert.Equal(t, "http://"+net.JoinHostPort(host, strconv.Itoa(port)), tr.URL)
}
func TestShouldUpgradeDaemonRuntimeTreatsMissingDaemonVersionAsOlderRelease(t *testing.T) {
rt := &DaemonRuntime{}
assert.True(t, shouldUpgradeDaemonRuntime(rt, "1.1.0"))
assert.False(t, shouldUpgradeDaemonRuntime(rt, "dev"))
}
func TestEnsureTransport_ArchiveWriteRefusesUnauthenticatedNonLoopbackAutoStart(
t *testing.T,
) {
dir := daemonRuntimeDir(t)
cfg := config.Config{DataDir: dir, Host: "0.0.0.0"}
forbidStartBackgroundServeForTransport(t,
"unsafe auto-start must be rejected before launch")
_, err := ensureTransport(
&cfg, transportIntentArchiveWrite, 100*time.Millisecond,
)
require.Error(t, err)
assert.Contains(t, err.Error(), "refusing to auto-start")
assert.Contains(t, err.Error(), "0.0.0.0")
}
func TestEnsureTransport_ArchiveWriteAllowsAuthenticatedNonLoopbackAutoStart(
t *testing.T,
) {
dir := daemonRuntimeDir(t)
cfg := config.Config{
DataDir: dir,
Host: "0.0.0.0",
RequireAuth: true,
}
var started bool
stubStartBackgroundServeForTransport(t, func(
context.Context, *config.Config, time.Duration,
) (*DaemonRuntime, error) {
started = true
return &DaemonRuntime{Host: "0.0.0.0", Port: 12345}, nil
})
tr, err := ensureTransport(
&cfg, transportIntentArchiveWrite, 100*time.Millisecond,
)
require.NoError(t, err)
assert.True(t, started)
assert.Equal(t, "http://127.0.0.1:12345", tr.URL)
}
func TestEnsureTransport_ArchiveWriteUsesAutoStartWaitForStartingDaemon(
t *testing.T,
) {
dir := daemonRuntimeDir(t)
MarkDaemonStarting(dir)
t.Cleanup(func() { UnmarkDaemonStarting(dir) })
var gotWait time.Duration
stubWaitForDaemonStartupForTransport(t, func(
_ context.Context, dataDir string, wait time.Duration, _ ...string,
) bool {
gotWait = wait
UnmarkDaemonStarting(dataDir)
return false
})
stubStartBackgroundServeForTransport(t, func(
context.Context, *config.Config, time.Duration,
) (*DaemonRuntime, error) {
return &DaemonRuntime{Host: "127.0.0.1", Port: 12345}, nil
})
cfg := config.Config{DataDir: dir}
_, err := ensureTransport(&cfg, transportIntentArchiveWrite, 0)
require.NoError(t, err)
assert.Equal(t, backgroundAutoStartReadyTimeout, gotWait)
}
func TestDetectTransportWaitsForExternalStartLockBeforeReturningRuntime(
t *testing.T,
) {
setStartProbeTickForTest(t, 25*time.Millisecond)
dir := daemonRuntimeDir(t)
oldHost, oldPort := testPingServer(t)
writeDaemonRuntimeForTest(t, dir, oldHost, oldPort, "1.0.0", false)
unlockStart := holdExternalDaemonStartLock(t, dir)
newHost, newPort := testPingServer(t)
published := make(chan error, 1)
go func() {
time.Sleep(2 * startProbeTick())
RemoveDaemonRuntime(dir)
_, err := WriteDaemonRuntime(dir, newHost, newPort, "1.1.0", false)
unlockStart()
published <- err
}()
tr, err := detectTransportContext(
context.Background(), dir, "", time.Second,
)
require.NoError(t, <-published)
require.NoError(t, err)
assert.Equal(t, transportHTTP, tr.Mode)
assert.Equal(t, "http://"+net.JoinHostPort(
newHost, strconv.Itoa(newPort),
), tr.URL)
}
func TestEnsureTransportArchiveWriteWaitsForBackgroundReplacementLock(
t *testing.T,
) {
setStartProbeTickForTest(t, 25*time.Millisecond)
dir := daemonRuntimeDir(t)
oldHost, oldPort := testPingServer(t)
writeDaemonRuntimeForTest(t, dir, oldHost, oldPort, version, false)
launchLock, ok := acquireBackgroundLaunchLock(dir)
require.True(t, ok)
t.Cleanup(func() { _ = launchLock.Unlock() })
newHost, newPort := testPingServer(t)
published := make(chan error, 1)
go func() {
time.Sleep(2 * startProbeTick())
RemoveDaemonRuntime(dir)
_, err := WriteDaemonRuntime(dir, newHost, newPort, version, false)
if err == nil {
err = launchLock.Unlock()
}
published <- err
}()
cfg := config.Config{DataDir: dir}
tr, err := ensureTransport(
&cfg, transportIntentArchiveWrite, time.Second,
)
require.NoError(t, <-published)
require.NoError(t, err)
assert.Equal(t, transportHTTP, tr.Mode)
assert.Equal(t, "http://"+net.JoinHostPort(
newHost, strconv.Itoa(newPort),
), tr.URL)
}
func TestEnsureTransportArchiveWriteAdoptsAuthAfterBackgroundLaunchWait(
t *testing.T,
) {
setStartProbeTickForTest(t, 25*time.Millisecond)
dir := daemonRuntimeDir(t)
t.Setenv("AGENTSVIEW_DATA_DIR", dir)
oldHost, oldPort := testPingServer(t)
writeDaemonRuntimeForTest(t, dir, oldHost, oldPort, version, false)
launchLock, ok := acquireBackgroundLaunchLock(dir)
require.True(t, ok)
t.Cleanup(func() { _ = launchLock.Unlock() })
const token = "generated-token"
newHost, newPort := testAuthenticatedPingServer(t, token)
published := make(chan error, 1)
go func() {
time.Sleep(2 * startProbeTick())
writeTestConfig(t, dir, `require_auth = true
auth_token = "generated-token"
`)
RemoveDaemonRuntime(dir)
_, err := WriteDaemonRuntimeWithAuth(
dir, newHost, newPort, version, false, true,
)
if err == nil {
err = launchLock.Unlock()
}
published <- err
}()
cfg := config.Config{DataDir: dir}
tr, err := ensureTransport(
&cfg, transportIntentArchiveWrite, time.Second,
)
require.NoError(t, <-published)
require.NoError(t, err)
assert.Equal(t, token, cfg.AuthToken)
assert.True(t, cfg.RequireAuth)
assert.Equal(t, transportHTTP, tr.Mode)
assert.Equal(t, "http://"+net.JoinHostPort(
newHost, strconv.Itoa(newPort),
), tr.URL)
}
func TestEnsureTransportReadAdoptsAuthAfterDaemonStartupWait(t *testing.T) {
setStartProbeTickForTest(t, 25*time.Millisecond)
dir := daemonRuntimeDir(t)
t.Setenv("AGENTSVIEW_DATA_DIR", dir)
unlockStart := holdExternalDaemonStartLock(t, dir)
const token = "generated-token"
newHost, newPort := testAuthenticatedPingServer(t, token)
published := make(chan error, 1)
go func() {
time.Sleep(2 * startProbeTick())
writeTestConfig(t, dir, `require_auth = true
auth_token = "generated-token"
`)
_, err := WriteDaemonRuntimeWithAuth(
dir, newHost, newPort, version, false, true,
)
unlockStart()
published <- err
}()
cfg := config.Config{DataDir: dir}
tr, err := ensureTransport(
&cfg, transportIntentRead, time.Second,
)
require.NoError(t, <-published)
require.NoError(t, err)
assert.Equal(t, token, cfg.AuthToken)
assert.True(t, cfg.RequireAuth)
assert.Equal(t, transportHTTP, tr.Mode)
assert.Equal(t, "http://"+net.JoinHostPort(
newHost, strconv.Itoa(newPort),
), tr.URL)
}
func TestWaitForBackgroundLaunchBeforeArchiveWriteRejectsFileDataDir(
t *testing.T,
) {
dataDir := filepath.Join(t.TempDir(), "not-a-directory")
require.NoError(t, os.WriteFile(dataDir, []byte("not a dir"), 0o600))
waited, err := waitForBackgroundLaunchBeforeArchiveWrite(
context.Background(), dataDir, 10*time.Millisecond,
)
require.Error(t, err)
assert.False(t, waited)
assert.Contains(t, err.Error(), "not a directory")
}
func TestEnsureTransportContextCancelDuringStartupWait(t *testing.T) {
dir := daemonRuntimeDir(t)
MarkDaemonStarting(dir)
t.Cleanup(func() { UnmarkDaemonStarting(dir) })
ctx, cancel := context.WithCancel(context.Background())
stubWaitForDaemonStartupForTransport(t, func(
gotCtx context.Context,
dataDir string,
_ time.Duration,
_ ...string,
) bool {
assert.Equal(t, dir, dataDir)
cancel()
<-gotCtx.Done()
UnmarkDaemonStarting(dataDir)
return false
})
forbidStartBackgroundServeForTransport(t,
"canceled startup wait must not start a daemon")
cfg := config.Config{DataDir: dir}
_, err := ensureTransportContext(
ctx, &cfg, transportIntentArchiveWrite, 100*time.Millisecond,
)
require.ErrorIs(t, err, context.Canceled)
}
func TestEnsureTransport_ArchiveWriteNoDaemonEnvUsesDirect(t *testing.T) {
dir := daemonRuntimeDir(t)
t.Setenv("AGENTSVIEW_NO_DAEMON", "1")
cfg := config.Config{DataDir: dir}
forbidStartBackgroundServeForTransport(t,
"AGENTSVIEW_NO_DAEMON must suppress daemon start")
tr, err := ensureTransport(&cfg, transportIntentArchiveWrite, 100*time.Millisecond)
require.NoError(t, err)
assert.Equal(t, transportDirect, tr.Mode)
}
func TestEnsureTransport_ArchiveWriteUnreachableDaemonRefuses(t *testing.T) {
dir := daemonRuntimeDir(t)
writeUnreachableDaemonRuntime(t, dir, false)
forbidStartBackgroundServeForTransport(t,
"unreachable live daemon must not trigger a second start")
cfg := config.Config{DataDir: dir}
_, err := ensureTransport(&cfg, transportIntentArchiveWrite, 100*time.Millisecond)
require.Error(t, err)
assert.ErrorIs(t, err, errLocalDaemonUnreachable)
}
func TestEnsureTransport_ArchiveWritePropagatesGeneratedAuthToken(t *testing.T) {
dir := daemonRuntimeDir(t)
cfg := config.Config{DataDir: dir}
stubStartBackgroundServeForTransport(t, func(
_ context.Context, gotCfg *config.Config, _ time.Duration,
) (*DaemonRuntime, error) {
gotCfg.AuthToken = "generated"
return &DaemonRuntime{
Host: "127.0.0.1",
Port: 12345,
}, nil
})
_, err := ensureTransport(&cfg, transportIntentArchiveWrite, 100*time.Millisecond)
require.NoError(t, err)
assert.Equal(t, "generated", cfg.AuthToken)
}
// TestNewService_HTTPMode verifies that newService returns a
// working HTTP-backed service and a cleanup function when the
// transport is HTTP mode. No DB is opened in this path.
func TestNewService_HTTPMode(t *testing.T) {
t.Parallel()
tr := transport{
Mode: transportHTTP,
URL: "http://127.0.0.1:8080",
}
svc, cleanup, err := newService(config.Config{}, tr)
require.NoError(t, err)
require.NotNil(t, svc)
require.NotNil(t, cleanup)
cleanup()
}
// TestNewService_DirectMode verifies that newService opens the
// local SQLite DB and returns a direct-backed service when the
// transport is direct mode. The cleanup function must close the
// DB.
func TestNewService_DirectMode(t *testing.T) {
t.Parallel()
dir := t.TempDir()
dbPath := filepath.Join(dir, "sessions.db")
seed, err := db.Open(dbPath)
require.NoError(t, err)
seed.Close()
cfg := config.Config{DBPath: dbPath}
svc, cleanup, err := newService(cfg, transport{Mode: transportDirect})
require.NoError(t, err)
require.NotNil(t, svc)
require.NotNil(t, cleanup)
cleanup()
}
// TestNewService_DirectReadOnly verifies that the DirectReadOnly branch
// opens the DB and returns a read-only service.
func TestNewService_DirectReadOnly(t *testing.T) {
t.Parallel()
dir := t.TempDir()
dbPath := filepath.Join(dir, "sessions.db")
seed, err := db.Open(dbPath)
require.NoError(t, err)
seed.Close()
cfg := config.Config{DBPath: dbPath}
svc, cleanup, err := newService(cfg, transport{
Mode: transportDirect,
DirectReadOnly: true,
})
require.NoError(t, err)
require.NotNil(t, svc)
require.NotNil(t, cleanup)
cleanup()
}
func TestNewService_DirectIncompatibleRefusesWithoutOpeningDB(t *testing.T) {
t.Parallel()
dbPath := filepath.Join(t.TempDir(), "missing.db")
cfg := config.Config{DBPath: dbPath}
svc, cleanup, err := newService(cfg, transport{
Mode: transportDirect,
DirectReadOnly: true,
DirectIncompatible: true,
DirectReason: "daemon data version 52 is incompatible with client data version 57",
})
require.Error(t, err)
assert.Nil(t, svc)
assert.Nil(t, cleanup)
assert.Contains(t, err.Error(), "daemon data version 52 is incompatible")
assert.Contains(t, err.Error(), "agentsview daemon restart")
assert.NoFileExists(t, dbPath)
}
func TestNewService_DirectModeMissingDBDoesNotCreate(t *testing.T) {
t.Parallel()
dir := t.TempDir()
dbPath := filepath.Join(dir, "sessions.db")
cfg := config.Config{DBPath: dbPath}
svc, cleanup, err := newService(cfg, transport{Mode: transportDirect})
require.Error(t, err)
assert.Nil(t, svc)
assert.Nil(t, cleanup)
assert.NoFileExists(t, dbPath)
}
func TestUrlFromDaemonRuntime_BindAllMapsToLoopback(t *testing.T) {
t.Parallel()
for _, tc := range []struct {
host string
want string
}{
{"", "http://127.0.0.1:8080"},
{"0.0.0.0", "http://127.0.0.1:8080"},
{"::", "http://[::1]:8080"},
{"192.168.1.10", "http://192.168.1.10:8080"},
} {
t.Run(tc.host, func(t *testing.T) {
got := urlFromDaemonRuntime(&DaemonRuntime{
Host: tc.host,
Port: 8080,
})
assert.Equal(t, tc.want, got)
})
}
}