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
1214 lines
36 KiB
Go
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)
|
|
})
|
|
}
|
|
}
|