f99010fae1
Desktop Artifacts / Desktop Build (Linux) (push) Waiting to run
Desktop Artifacts / Desktop Build (Windows) (push) Waiting to run
Desktop Artifacts / Desktop Build (Linux (arm64)) (push) Waiting to run
Desktop Artifacts (macOS) / Desktop Build (macOS (aarch64)) (push) Waiting to run
Desktop Artifacts (macOS) / Desktop Build (macOS (x86_64)) (push) Waiting to run
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
244 lines
6.7 KiB
Go
244 lines
6.7 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"path/filepath"
|
|
"strings"
|
|
stdsync "sync"
|
|
"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/agentsview/internal/dbtest"
|
|
"go.kenn.io/agentsview/internal/postgres"
|
|
syncpkg "go.kenn.io/agentsview/internal/sync"
|
|
)
|
|
|
|
// TestDaemonPushHeartbeat pins the daemon-delegated push's client-side
|
|
// output: an immediate delegation announcement, elapsed-time heartbeats
|
|
// while the blocking POST is in flight, and silence after stop.
|
|
func TestDaemonPushHeartbeat(t *testing.T) {
|
|
orig := daemonPushHeartbeatInterval
|
|
daemonPushHeartbeatInterval = 5 * time.Millisecond
|
|
t.Cleanup(func() { daemonPushHeartbeatInterval = orig })
|
|
|
|
var out syncBuffer
|
|
stop := startDaemonPushHeartbeatTo(&out, "PostgreSQL")
|
|
assert.Contains(t, out.String(),
|
|
"Pushing to PostgreSQL via the local daemon")
|
|
|
|
require.Eventually(t, func() bool {
|
|
return strings.Contains(out.String(),
|
|
"still pushing to PostgreSQL via the daemon")
|
|
}, time.Second, time.Millisecond, "heartbeat line must appear")
|
|
|
|
stop()
|
|
settled := out.String()
|
|
time.Sleep(20 * time.Millisecond)
|
|
assert.Equal(t, settled, out.String(), "no output after stop")
|
|
}
|
|
|
|
// TestDaemonPushProgressSilencesHeartbeat pins that the first streamed
|
|
// progress event stops the elapsed-time heartbeat for good: once the daemon
|
|
// starts reporting real progress, heartbeat lines must not interleave with
|
|
// the in-place progress line.
|
|
func TestDaemonPushProgressSilencesHeartbeat(t *testing.T) {
|
|
orig := daemonPushHeartbeatInterval
|
|
daemonPushHeartbeatInterval = 50 * time.Millisecond
|
|
t.Cleanup(func() { daemonPushHeartbeatInterval = orig })
|
|
|
|
out := captureStdout(t, func() {
|
|
onProgress, finish := daemonPushProgress(
|
|
"PostgreSQL", newPGPushProgressPrinter(),
|
|
)
|
|
onProgress(postgres.PushProgress{SessionsDone: 1, SessionsTotal: 2})
|
|
time.Sleep(150 * time.Millisecond)
|
|
finish()
|
|
})
|
|
|
|
assert.Contains(t, out, "Pushing to PostgreSQL via the local daemon")
|
|
assert.Contains(t, out, "Pushing... 1/2 sessions")
|
|
assert.NotContains(t, out, "still pushing",
|
|
"heartbeat must stay silent after the first progress event")
|
|
}
|
|
|
|
// syncBuffer is a mutex-guarded bytes.Buffer: the heartbeat goroutine writes
|
|
// while the test reads.
|
|
type syncBuffer struct {
|
|
mu stdsync.Mutex
|
|
buf bytes.Buffer
|
|
}
|
|
|
|
func (b *syncBuffer) Write(p []byte) (int, error) {
|
|
b.mu.Lock()
|
|
defer b.mu.Unlock()
|
|
return b.buf.Write(p)
|
|
}
|
|
|
|
func (b *syncBuffer) String() string {
|
|
b.mu.Lock()
|
|
defer b.mu.Unlock()
|
|
return b.buf.String()
|
|
}
|
|
|
|
func TestLocalArchiveWriteBackendPGPushStopsAfterCanceledLocalSync(t *testing.T) {
|
|
testLocalArchivePushStopsAfterCanceledSync(t,
|
|
func(backend *localArchiveWriteBackend, ctx context.Context) error {
|
|
_, err := backend.PGPush(
|
|
ctx, pgTargetSelection{}, PGPushConfig{}, nil, nil,
|
|
)
|
|
return err
|
|
})
|
|
}
|
|
|
|
func TestLocalArchiveWriteBackendDuckDBPushStopsAfterCanceledLocalSync(t *testing.T) {
|
|
testLocalArchivePushStopsAfterCanceledSync(t,
|
|
func(backend *localArchiveWriteBackend, ctx context.Context) error {
|
|
_, err := backend.DuckDBPush(
|
|
ctx, config.DuckDBConfig{}, DuckDBPushConfig{}, nil, nil,
|
|
)
|
|
return err
|
|
})
|
|
}
|
|
|
|
func TestLocalArchiveWriteBackendDuckDBPushUsesConfiguredRemoteURL(t *testing.T) {
|
|
backend := testLocalArchiveWriteBackend(t)
|
|
|
|
captureStdout(t, func() {
|
|
_, err := backend.DuckDBPush(
|
|
context.Background(),
|
|
config.DuckDBConfig{
|
|
URL: "quack:https://duck.example.test",
|
|
MachineName: "workstation",
|
|
},
|
|
DuckDBPushConfig{},
|
|
nil,
|
|
nil,
|
|
)
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "duckdb quack token is required")
|
|
})
|
|
}
|
|
|
|
func TestLocalArchiveWriteBackendDuckDBPushValidatesRemoteBeforeLocalSync(t *testing.T) {
|
|
backend := testLocalArchiveWriteBackend(t)
|
|
|
|
var err error
|
|
out := captureStdout(t, func() {
|
|
_, err = backend.DuckDBPush(
|
|
context.Background(),
|
|
config.DuckDBConfig{
|
|
URL: "quack:https://duck.example.test",
|
|
MachineName: "workstation",
|
|
},
|
|
DuckDBPushConfig{Full: true},
|
|
nil,
|
|
nil,
|
|
)
|
|
})
|
|
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "duckdb quack token is required")
|
|
assert.NotContains(t, out, "Database:")
|
|
assert.NotContains(t, out, "Opening DuckDB mirror")
|
|
}
|
|
|
|
func TestRunPGWatchStartupSyncFallsBackAfterAbortedResync(t *testing.T) {
|
|
database := dbtest.OpenTestDB(t)
|
|
missingPath := filepath.Join(t.TempDir(), "missing.jsonl")
|
|
dbtest.SeedSession(t, database, "existing", "proj",
|
|
func(s *db.Session) {
|
|
s.FilePath = &missingPath
|
|
})
|
|
engine := syncpkg.NewEngine(database, syncpkg.EngineConfig{})
|
|
|
|
didResync, err := runPGWatchStartupSync(
|
|
context.Background(), engine, true,
|
|
)
|
|
|
|
require.NoError(t, err)
|
|
assert.True(t, didResync)
|
|
assert.False(t, engine.LastSyncStats().Aborted)
|
|
assert.False(t, engine.LastSync().IsZero())
|
|
}
|
|
|
|
func TestLocalArchiveWriteBackendPGPushWatchCanceledStartupIsClean(t *testing.T) {
|
|
backend := testLocalArchiveWriteBackend(t)
|
|
|
|
err := backend.PGPushWatch(
|
|
canceledContext(),
|
|
pgTargetSelection{},
|
|
PGPushConfig{},
|
|
nil,
|
|
nil,
|
|
time.Millisecond,
|
|
time.Millisecond,
|
|
)
|
|
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestResolveArchiveWriteBackendCopiesNoSyncRuntime(t *testing.T) {
|
|
dataDir := t.TempDir()
|
|
host, port := testPingServer(t)
|
|
_, err := WriteDaemonRuntimeWithAuthAndNoSync(
|
|
dataDir, host, port, "test", false, false, true,
|
|
)
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() { RemoveDaemonRuntime(dataDir) })
|
|
|
|
backend, cleanup, err := resolveArchiveWriteBackend(
|
|
context.Background(), config.Config{DataDir: dataDir},
|
|
)
|
|
require.NoError(t, err)
|
|
t.Cleanup(cleanup)
|
|
|
|
daemonBackend, ok := backend.(daemonArchiveWriteBackend)
|
|
require.True(t, ok)
|
|
assert.True(t, daemonBackend.appCfg.NoSync)
|
|
}
|
|
|
|
// canceledContext returns a context that has already been canceled.
|
|
func canceledContext() context.Context {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cancel()
|
|
return ctx
|
|
}
|
|
|
|
// testLocalArchivePushStopsAfterCanceledSync asserts that a local push aborts
|
|
// with context.Canceled when its context is already canceled. push runs the
|
|
// backend-specific push call and returns its error.
|
|
func testLocalArchivePushStopsAfterCanceledSync(
|
|
t *testing.T,
|
|
push func(*localArchiveWriteBackend, context.Context) error,
|
|
) {
|
|
t.Helper()
|
|
backend := testLocalArchiveWriteBackend(t)
|
|
|
|
var err error
|
|
captureStdout(t, func() {
|
|
err = push(backend, canceledContext())
|
|
})
|
|
|
|
require.ErrorIs(t, err, context.Canceled)
|
|
}
|
|
|
|
func testLocalArchiveWriteBackend(t *testing.T) *localArchiveWriteBackend {
|
|
t.Helper()
|
|
dataDir := t.TempDir()
|
|
dbPath := filepath.Join(dataDir, "sessions.db")
|
|
database := dbtest.OpenTestDBAt(t, dbPath)
|
|
|
|
return &localArchiveWriteBackend{
|
|
appCfg: config.Config{
|
|
DataDir: dataDir,
|
|
DBPath: dbPath,
|
|
},
|
|
database: database,
|
|
}
|
|
}
|