Files
kenn-io--agentsview/internal/server/huma_routes_remote_sync_internal_test.go
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

479 lines
16 KiB
Go

package server
import (
"archive/tar"
"bytes"
"compress/gzip"
"database/sql"
"encoding/json"
"errors"
"io"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
"time"
_ "github.com/mattn/go-sqlite3"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.kenn.io/agentsview/internal/config"
"go.kenn.io/agentsview/internal/dbtest"
"go.kenn.io/agentsview/internal/parser"
"go.kenn.io/agentsview/internal/remotesync"
)
func newRemoteSyncServer(t *testing.T) (*Server, http.Handler, string) {
t.Helper()
dir := t.TempDir()
dbPath := filepath.Join(dir, "test.db")
database := dbtest.OpenTestDBAt(t, dbPath)
claudeDir := filepath.Join(dir, "claude")
require.NoError(t, os.MkdirAll(claudeDir, 0o755))
sessionPath := filepath.Join(claudeDir, "session.jsonl")
require.NoError(t, os.WriteFile(sessionPath, []byte("{}\n"), 0o644))
mtime := time.Date(2026, 6, 28, 12, 0, 0, 0, time.UTC)
require.NoError(t, os.Chtimes(sessionPath, mtime, mtime))
srv := New(config.Config{
Host: "127.0.0.1",
Port: 8080,
DataDir: dir,
DBPath: dbPath,
AuthToken: "remote-token",
RequireAuth: false,
WriteTimeout: 30 * time.Second,
AgentDirs: map[parser.AgentType][]string{
parser.AgentClaude: {claudeDir},
},
}, database, nil)
return srv, srv.Handler(), sessionPath
}
func TestRemoteSyncTargetsRequiresBearerAndBypassesHostCheck(t *testing.T) {
_, handler, _ := newRemoteSyncServer(t)
req := httptest.NewRequest(http.MethodGet, "/api/v1/remote-sync/targets", nil)
req.Host = "devbox.tailnet.ts.net:8080"
req.Header.Set("Authorization", "Bearer remote-token")
w := httptest.NewRecorder()
handler.ServeHTTP(w, req)
require.Equal(t, http.StatusOK, w.Code, "body: %s", w.Body.String())
assert.Contains(t, w.Body.String(), "claude")
}
func TestRemoteSyncArchiveRejectsUnresolvedPath(t *testing.T) {
_, handler, _ := newRemoteSyncServer(t)
body := bytes.NewBufferString(`{"dirs":{"claude":["/etc"]}}`)
req := httptest.NewRequest(http.MethodPost, "/api/v1/remote-sync/archive", body)
req.Header.Set("Authorization", "Bearer remote-token")
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
handler.ServeHTTP(w, req)
assert.Equal(t, http.StatusForbidden, w.Code)
}
func TestRemoteSyncArchiveStreamsTar(t *testing.T) {
_, handler, sessionPath := newRemoteSyncServer(t)
targets := map[string]any{
"dirs": map[string][]string{
"claude": {filepath.Dir(sessionPath)},
},
}
payload, err := json.Marshal(targets)
require.NoError(t, err)
req := httptest.NewRequest(http.MethodPost, "/api/v1/remote-sync/archive", bytes.NewReader(payload))
req.Header.Set("Authorization", "Bearer remote-token")
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
handler.ServeHTTP(w, req)
require.Equal(t, http.StatusOK, w.Code, "body: %s", w.Body.String())
assert.Equal(t, "application/x-tar", w.Header().Get("Content-Type"))
tr := tar.NewReader(bytes.NewReader(w.Body.Bytes()))
for {
hdr, err := tr.Next()
if err == io.EOF {
require.FailNow(t, "session file not found in tar")
}
require.NoError(t, err)
if pathBaseSlash(hdr.Name) == filepath.Base(sessionPath) {
assert.Equal(t, byte(tar.TypeReg), hdr.Typeflag)
return
}
}
}
// newWindsurfRemoteSyncServer builds a remote-sync server whose only
// agent is Windsurf (a file-scoped, sanitized agent). It returns the
// handler, the resolved targets the client would see, and the raw
// state.vscdb path under the Windsurf root.
func newWindsurfRemoteSyncServer(t *testing.T) (http.Handler, remotesync.TargetSet, string) {
t.Helper()
dir := t.TempDir()
dbPath := filepath.Join(dir, "test.db")
database := dbtest.OpenTestDBAt(t, dbPath)
windsurfRoot := filepath.Join(dir, "Windsurf", "User")
workspaceDir := filepath.Join(windsurfRoot, "workspaceStorage", "workspace-a")
stateDB := filepath.Join(workspaceDir, parser.WindsurfStateDBName)
workspaceJSON := filepath.Join(workspaceDir, "workspace.json")
secretPath := filepath.Join(workspaceDir, "extension-secret.json")
require.NoError(t, os.MkdirAll(workspaceDir, 0o755))
closeStateDB := writeWindsurfArchiveStateDB(t, stateDB)
t.Cleanup(closeStateDB)
require.NoError(t, os.WriteFile(workspaceJSON, []byte(`{"folder":"file:///work/demo"}`), 0o644))
require.NoError(t, os.WriteFile(secretPath, []byte("do not archive"), 0o644))
srv := New(config.Config{
Host: "127.0.0.1",
Port: 8080,
DataDir: dir,
DBPath: dbPath,
AuthToken: "remote-token",
RequireAuth: false,
WriteTimeout: 30 * time.Second,
AgentDirs: map[parser.AgentType][]string{
parser.AgentWindsurf: {windsurfRoot},
},
}, database, nil)
handler := srv.Handler()
targetReq := httptest.NewRequest(http.MethodGet, "/api/v1/remote-sync/targets", nil)
targetReq.Header.Set("Authorization", "Bearer remote-token")
targetW := httptest.NewRecorder()
handler.ServeHTTP(targetW, targetReq)
require.Equal(t, http.StatusOK, targetW.Code, "body: %s", targetW.Body.String())
var targets remotesync.TargetSet
require.NoError(t, json.Unmarshal(targetW.Body.Bytes(), &targets))
return handler, targets, stateDB
}
func TestRemoteSyncManifestRefusesFileScopedAgents(t *testing.T) {
handler, targets, _ := newWindsurfRemoteSyncServer(t)
payload, err := json.Marshal(targets)
require.NoError(t, err)
req := httptest.NewRequest(http.MethodPost, "/api/v1/remote-sync/manifest", bytes.NewReader(payload))
req.Header.Set("Authorization", "Bearer remote-token")
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
handler.ServeHTTP(w, req)
// 501 is in the client's manifest-unsupported set, so the client
// falls back to the full-archive flow (which sanitizes Windsurf).
assert.Equal(t, http.StatusNotImplemented, w.Code, "body: %s", w.Body.String())
}
func TestRemoteSyncArchiveRejectsDeltaForFileScopedAgent(t *testing.T) {
handler, targets, stateDB := newWindsurfRemoteSyncServer(t)
// A malicious client that skips the manifest and requests the raw,
// unsanitized state.vscdb as a delta must be refused.
req := remotesync.ArchiveRequest{
TargetSet: targets,
DeltaFiles: []string{stateDB},
}
payload, err := json.Marshal(req)
require.NoError(t, err)
archiveReq := httptest.NewRequest(http.MethodPost, "/api/v1/remote-sync/archive", bytes.NewReader(payload))
archiveReq.Header.Set("Authorization", "Bearer remote-token")
archiveReq.Header.Set("Content-Type", "application/json")
archiveW := httptest.NewRecorder()
handler.ServeHTTP(archiveW, archiveReq)
assert.Equal(t, http.StatusForbidden, archiveW.Code, "body: %s", archiveW.Body.String())
assert.NotContains(t, archiveW.Body.String(), "extension secret value")
}
func TestRemoteSyncArchiveWindsurfStreamsSanitizedStateDB(t *testing.T) {
handler, targets, _ := newWindsurfRemoteSyncServer(t)
payload, err := json.Marshal(targets)
require.NoError(t, err)
archiveReq := httptest.NewRequest(http.MethodPost, "/api/v1/remote-sync/archive", bytes.NewReader(payload))
archiveReq.Header.Set("Authorization", "Bearer remote-token")
archiveReq.Header.Set("Content-Type", "application/json")
archiveW := httptest.NewRecorder()
handler.ServeHTTP(archiveW, archiveReq)
require.Equal(t, http.StatusOK, archiveW.Code, "body: %s", archiveW.Body.String())
archiveBytes := archiveW.Body.Bytes()
assert.NotContains(t, string(archiveBytes), "extension secret value")
entries := tarEntries(t, archiveBytes)
names := tarEntryNames(entries)
stateEntry, ok := tarEntryWithSuffix(entries, "workspace-a/"+parser.WindsurfStateDBName)
require.True(t, ok, "entries: %v", names)
assert.True(t, hasTarEntrySuffix(entries, "workspace-a/workspace.json"), "entries: %v", entries)
assert.False(t, hasTarEntrySuffix(entries, "workspace-a/extension-secret.json"), "entries: %v", entries)
assert.False(t, hasTarEntrySuffix(entries, "workspace-a/"+parser.WindsurfStateDBName+"-wal"), "entries: %v", entries)
assert.False(t, hasTarEntrySuffix(entries, "workspace-a/"+parser.WindsurfStateDBName+"-shm"), "entries: %v", entries)
assertSanitizedWindsurfArchiveDB(t, stateEntry.Body)
}
func pathBaseSlash(p string) string {
i := strings.LastIndex(p, "/")
if i < 0 {
return p
}
return p[i+1:]
}
type tarTestEntry struct {
Name string
Body []byte
}
func tarEntries(t *testing.T, archive []byte) []tarTestEntry {
t.Helper()
tr := tar.NewReader(bytes.NewReader(archive))
var entries []tarTestEntry
for {
hdr, err := tr.Next()
if err == io.EOF {
return entries
}
require.NoError(t, err)
body, err := io.ReadAll(tr)
require.NoError(t, err)
entries = append(entries, tarTestEntry{Name: hdr.Name, Body: body})
}
}
func tarEntryNames(entries []tarTestEntry) []string {
names := make([]string, 0, len(entries))
for _, entry := range entries {
names = append(names, entry.Name)
}
return names
}
func hasTarEntrySuffix(entries []tarTestEntry, suffix string) bool {
_, ok := tarEntryWithSuffix(entries, suffix)
return ok
}
func tarEntryWithSuffix(entries []tarTestEntry, suffix string) (tarTestEntry, bool) {
for _, entry := range entries {
if strings.HasSuffix(entry.Name, suffix) {
return entry, true
}
}
return tarTestEntry{}, false
}
func writeWindsurfArchiveStateDB(t *testing.T, dbPath string) func() {
t.Helper()
conn, err := sql.Open("sqlite3", dbPath)
require.NoError(t, err)
conn.SetMaxOpenConns(1)
_, err = conn.Exec(`PRAGMA journal_mode=WAL`)
require.NoError(t, err)
_, err = conn.Exec(`CREATE TABLE ItemTable (key TEXT PRIMARY KEY, value TEXT)`)
require.NoError(t, err)
_, err = conn.Exec(
`INSERT INTO ItemTable (key, value) VALUES (?, ?)`,
"workbench.panel.aichat.view.aichat.chatdata",
`{"version":1,"sessionId":"remote-windsurf","requests":[{"requestId":"request-1","message":{"text":"Remote chat"},"response":[{"value":"Remote answer"}],"timestamp":1710000000000}]}`,
)
require.NoError(t, err)
_, err = conn.Exec(
`INSERT INTO ItemTable (key, value) VALUES (?, ?)`,
"extension.secret",
"extension secret value",
)
require.NoError(t, err)
require.FileExists(t, dbPath+"-wal")
return func() {
require.NoError(t, conn.Close())
}
}
func assertSanitizedWindsurfArchiveDB(t *testing.T, body []byte) {
t.Helper()
dbPath := filepath.Join(t.TempDir(), "state.vscdb")
require.NoError(t, os.WriteFile(dbPath, body, 0o644))
conn, err := sql.Open("sqlite3", dbPath)
require.NoError(t, err)
defer conn.Close()
rows, err := conn.Query(`SELECT key, value FROM ItemTable ORDER BY key`)
require.NoError(t, err)
defer rows.Close()
got := make(map[string]string)
for rows.Next() {
var key, value string
require.NoError(t, rows.Scan(&key, &value))
got[key] = value
}
require.NoError(t, rows.Err())
require.Len(t, got, 1)
for key, value := range got {
assert.Equal(t, "workbench.panel.aichat.view.aichat.chatdata", key)
assert.Contains(t, value, "Remote chat")
assert.NotContains(t, value, "extension secret value")
}
}
func TestRemoteSyncArchiveDoesNotAppendErrorAfterStreamingStarts(t *testing.T) {
srv, _, sessionPath := newRemoteSyncServer(t)
targets := map[string]any{
"dirs": map[string][]string{
"claude": {filepath.Dir(sessionPath)},
},
}
payload, err := json.Marshal(targets)
require.NoError(t, err)
req := httptest.NewRequest(http.MethodPost, "/api/v1/remote-sync/archive", bytes.NewReader(payload))
req.Header.Set("Authorization", "Bearer remote-token")
req.Header.Set("Content-Type", "application/json")
w := &errorOnFirstWriteRecorder{
ResponseRecorder: httptest.NewRecorder(),
}
srv.remoteSyncArchiveHTTP(w, req)
assert.NotContains(t, w.Body.String(), "forced tar write error")
}
type errorOnFirstWriteRecorder struct {
*httptest.ResponseRecorder
failed bool
}
func (w *errorOnFirstWriteRecorder) Write(p []byte) (int, error) {
n, err := w.ResponseRecorder.Write(p)
if !w.failed {
w.failed = true
return n, errors.New("forced tar write error")
}
return n, err
}
func TestRemoteSyncManifestListsFiles(t *testing.T) {
_, handler, sessionPath := newRemoteSyncServer(t)
payload, err := json.Marshal(map[string]any{
"dirs": map[string][]string{"claude": {filepath.Dir(sessionPath)}},
})
require.NoError(t, err)
req := httptest.NewRequest(http.MethodPost, "/api/v1/remote-sync/manifest",
bytes.NewReader(payload))
req.Header.Set("Authorization", "Bearer remote-token")
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
handler.ServeHTTP(w, req)
require.Equal(t, http.StatusOK, w.Code, "body: %s", w.Body.String())
require.Equal(t, "gzip", w.Header().Get("Content-Encoding"))
gz, err := gzip.NewReader(bytes.NewReader(w.Body.Bytes()))
require.NoError(t, err)
var manifest remotesync.Manifest
require.NoError(t, json.NewDecoder(gz).Decode(&manifest))
require.Len(t, manifest.Files, 1)
assert.Equal(t, sessionPath, manifest.Files[0].Path)
assert.Equal(t, int64(3), manifest.Files[0].Size)
info, err := os.Stat(sessionPath)
require.NoError(t, err)
assert.Equal(t, info.ModTime().UnixNano(), manifest.Files[0].MtimeNS)
}
func TestRemoteSyncManifestRejectsUnresolvedPath(t *testing.T) {
_, handler, _ := newRemoteSyncServer(t)
body := bytes.NewBufferString(`{"dirs":{"claude":["/etc"]}}`)
req := httptest.NewRequest(http.MethodPost, "/api/v1/remote-sync/manifest", body)
req.Header.Set("Authorization", "Bearer remote-token")
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
handler.ServeHTTP(w, req)
assert.Equal(t, http.StatusForbidden, w.Code)
}
func TestRemoteSyncArchiveDeltaStreamsOnlyRequestedFiles(t *testing.T) {
_, handler, sessionPath := newRemoteSyncServer(t)
other := filepath.Join(filepath.Dir(sessionPath), "other.jsonl")
require.NoError(t, os.WriteFile(other, []byte("{}\n"), 0o644))
payload, err := json.Marshal(map[string]any{
"dirs": map[string][]string{"claude": {filepath.Dir(sessionPath)}},
"files": []string{other},
})
require.NoError(t, err)
req := httptest.NewRequest(http.MethodPost, "/api/v1/remote-sync/archive",
bytes.NewReader(payload))
req.Header.Set("Authorization", "Bearer remote-token")
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
handler.ServeHTTP(w, req)
require.Equal(t, http.StatusOK, w.Code, "body: %s", w.Body.String())
names := []string{}
tr := tar.NewReader(bytes.NewReader(w.Body.Bytes()))
for {
hdr, err := tr.Next()
if err == io.EOF {
break
}
require.NoError(t, err)
names = append(names, pathBaseSlash(hdr.Name))
}
assert.Equal(t, []string{"other.jsonl"}, names)
}
func TestRemoteSyncArchiveDeltaRejectsFileOutsideAllowedDirs(t *testing.T) {
_, handler, sessionPath := newRemoteSyncServer(t)
payload, err := json.Marshal(map[string]any{
"dirs": map[string][]string{"claude": {filepath.Dir(sessionPath)}},
"files": []string{"/etc/passwd"},
})
require.NoError(t, err)
req := httptest.NewRequest(http.MethodPost, "/api/v1/remote-sync/archive",
bytes.NewReader(payload))
req.Header.Set("Authorization", "Bearer remote-token")
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
handler.ServeHTTP(w, req)
assert.Equal(t, http.StatusForbidden, w.Code)
}
func TestRemoteSyncArchiveGzipsWhenAdvertised(t *testing.T) {
_, handler, sessionPath := newRemoteSyncServer(t)
payload, err := json.Marshal(map[string]any{
"dirs": map[string][]string{"claude": {filepath.Dir(sessionPath)}},
})
require.NoError(t, err)
req := httptest.NewRequest(http.MethodPost, "/api/v1/remote-sync/archive",
bytes.NewReader(payload))
req.Header.Set("Authorization", "Bearer remote-token")
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept-Encoding", "gzip")
w := httptest.NewRecorder()
handler.ServeHTTP(w, req)
require.Equal(t, http.StatusOK, w.Code)
require.Equal(t, "gzip", w.Header().Get("Content-Encoding"))
gz, err := gzip.NewReader(bytes.NewReader(w.Body.Bytes()))
require.NoError(t, err)
tr := tar.NewReader(gz)
hdr, err := tr.Next()
require.NoError(t, err)
assert.NotEmpty(t, hdr.Name)
}
func TestRemoteSyncArchiveExplicitEmptyDeltaReturnsEmptyTar(t *testing.T) {
_, handler, sessionPath := newRemoteSyncServer(t)
payload, err := json.Marshal(map[string]any{
"dirs": map[string][]string{"claude": {filepath.Dir(sessionPath)}},
"files": []string{},
})
require.NoError(t, err)
req := httptest.NewRequest(http.MethodPost, "/api/v1/remote-sync/archive", bytes.NewReader(payload))
req.Header.Set("Authorization", "Bearer remote-token")
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
handler.ServeHTTP(w, req)
require.Equal(t, http.StatusOK, w.Code, "body: %s", w.Body.String())
tr := tar.NewReader(bytes.NewReader(w.Body.Bytes()))
_, err = tr.Next()
assert.Equal(t, io.EOF, err, "explicit empty delta must stream an empty tar")
}