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

376 lines
14 KiB
Go

package sync_test
// Integration coverage for parse-diff over the DB-backed
// provider-authoritative agents (Forge, Piebald, Warp). Each provider
// discovers a shared SQLite store as one virtual source per session and
// re-parses it through the provider facade, so these tests prove the
// report-only run actually re-parses and compares those sessions rather than
// bucketing them as skipped/"database-backed agent". Assertions go through the
// exported DiffClass/ParseDiffTotals contract, never rendered strings.
import (
"database/sql"
"fmt"
"os"
"path/filepath"
"testing"
_ "github.com/mattn/go-sqlite3"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.kenn.io/agentsview/internal/parser"
"go.kenn.io/agentsview/internal/sync"
)
// warpTestDB is a minimal Warp SQLite store for the sync test package,
// mirroring the schema the parser reads (agent_conversations + ai_queries).
type warpTestDB struct {
path string
db *sql.DB
}
func createWarpDB(t *testing.T, dir string) *warpTestDB {
t.Helper()
path := filepath.Join(dir, "warp.sqlite")
d, err := sql.Open("sqlite3", path)
require.NoError(t, err, "opening warp test db")
t.Cleanup(func() { _ = d.Close() })
_, err = d.Exec(`
CREATE TABLE agent_conversations (
id INTEGER PRIMARY KEY NOT NULL,
conversation_id TEXT NOT NULL,
conversation_data TEXT NOT NULL,
last_modified_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE UNIQUE INDEX ux_agent_conversations_conversation_id
ON agent_conversations (conversation_id);
CREATE TABLE ai_queries (
id INTEGER PRIMARY KEY NOT NULL,
exchange_id TEXT NOT NULL,
conversation_id TEXT NOT NULL,
start_ts DATETIME NOT NULL,
input TEXT NOT NULL,
working_directory TEXT,
output_status TEXT NOT NULL,
model_id TEXT NOT NULL DEFAULT ''
);
CREATE UNIQUE INDEX ux_ai_queries_exchange_id
ON ai_queries(exchange_id);
`)
require.NoError(t, err, "creating warp schema")
return &warpTestDB{path: path, db: d}
}
// addConversation seeds one Warp conversation with one user-query exchange per
// prompt. conversation_data stays "{}" so no aggregate tool messages are
// synthesized and the message count equals the number of prompts.
func (w *warpTestDB) addConversation(
t *testing.T, convID, lastModified string, prompts ...string,
) {
t.Helper()
_, err := w.db.Exec(
`INSERT INTO agent_conversations
(conversation_id, conversation_data, last_modified_at)
VALUES (?, '{}', ?)`,
convID, lastModified,
)
require.NoError(t, err, "insert warp conversation")
for i, p := range prompts {
input := fmt.Sprintf(`[{"Query":{"text":%q,"context":[]}}]`, p)
_, err := w.db.Exec(
`INSERT INTO ai_queries
(exchange_id, conversation_id, start_ts, input,
working_directory, output_status, model_id)
VALUES (?, ?, ?, ?, ?, ?, ?)`,
fmt.Sprintf("%s-ex-%d", convID, i), convID,
fmt.Sprintf("2026-04-07 09:5%d:00.000000", i), input,
"/Users/alice/code/myproject", `"Completed"`, "auto-genius",
)
require.NoError(t, err, "insert warp exchange")
}
}
func createWindsurfWorkspaceDB(t *testing.T, root, payload string) string {
t.Helper()
workspaceDir := filepath.Join(root, "workspaceStorage", "workspace-hash")
require.NoError(t, os.MkdirAll(workspaceDir, 0o755))
require.NoError(t,
os.WriteFile(
filepath.Join(workspaceDir, "workspace.json"),
[]byte(`{"folder":"file:///work/demo"}`),
0o644,
),
)
dbPath := filepath.Join(workspaceDir, "state.vscdb")
conn, err := sql.Open("sqlite3", dbPath)
require.NoError(t, err)
t.Cleanup(func() { _ = conn.Close() })
_, 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",
payload,
)
require.NoError(t, err)
return dbPath
}
// TestParseDiffCoversForge proves Forge's shared .forge.db, discovered as one
// virtual source per conversation, is re-parsed and compared by parse-diff.
// Examined:1/Identical:1 means the stored conversation was matched and vetted,
// not bucketed as skipped/"database-backed agent".
func TestParseDiffCoversForge(t *testing.T) {
env := setupSingleAgentTestEnv(t, parser.AgentForge)
forge := createForgeDB(t, env.forgeDir)
forge.addConversation(
t, "forge-clean-1", "Forge Clean",
forgeTestContext("Add Forge coverage.", "Added Forge coverage."),
"2026-05-02 09:58:15.741021507",
"2026-05-02 10:00:16.848497543",
`{"input_tokens":360,"output_tokens":55,"cached_input_tokens":85}`,
)
runSyncAndAssert(t, env.engine, sync.SyncStats{TotalSessions: 1, Synced: 1})
report := runParseDiff(t, env, sync.ParseDiffOptions{
Agents: []parser.AgentType{parser.AgentForge},
})
assert.Equal(t, sync.ParseDiffTotals{Examined: 1, Identical: 1},
report.Totals, "forge conversation must be examined, not skipped")
assert.Equal(t, 1, report.FilesExamined, ".forge.db examined")
assert.False(t, report.HasFailures(), "clean forge run")
}
// TestParseDiffCoversPiebald proves Piebald's shared app.db is re-parsed and
// compared per chat.
func TestParseDiffCoversPiebald(t *testing.T) {
env := setupSingleAgentTestEnv(t, parser.AgentPiebald)
piebald := createPiebaldDB(t, env.piebaldDir)
piebald.addChat(t, 7, "Piebald Clean",
"Add Piebald coverage.", "Added Piebald coverage.",
"2026-05-01T10:05:00Z")
runSyncAndAssert(t, env.engine, sync.SyncStats{TotalSessions: 1, Synced: 1})
report := runParseDiff(t, env, sync.ParseDiffOptions{
Agents: []parser.AgentType{parser.AgentPiebald},
})
assert.Equal(t, sync.ParseDiffTotals{Examined: 1, Identical: 1},
report.Totals, "piebald chat must be examined, not skipped")
assert.Equal(t, 1, report.FilesExamined, "app.db examined")
assert.False(t, report.HasFailures(), "clean piebald run")
}
// TestParseDiffCoversWarp proves Warp's shared warp.sqlite is re-parsed and
// compared per conversation.
func TestParseDiffCoversWarp(t *testing.T) {
env := setupSingleAgentTestEnv(t, parser.AgentWarp)
warp := createWarpDB(t, env.warpDir)
warp.addConversation(t, "warp-clean-1", "2026-04-07 10:00:00",
"Fix the JSON parsing bug.")
runSyncAndAssert(t, env.engine, sync.SyncStats{TotalSessions: 1, Synced: 1})
report := runParseDiff(t, env, sync.ParseDiffOptions{
Agents: []parser.AgentType{parser.AgentWarp},
})
assert.Equal(t, sync.ParseDiffTotals{Examined: 1, Identical: 1},
report.Totals, "warp conversation must be examined, not skipped")
assert.Equal(t, 1, report.FilesExamined, "warp.sqlite examined")
assert.False(t, report.HasFailures(), "clean warp run")
}
// TestParseDiffForgeDetectsStoredDrift mutates a stored Forge row after a sync
// and verifies the re-parse from the unchanged .forge.db surfaces the drift as
// DiffChanged. This proves the DB-backed re-parse+compare path is real, not a
// no-op that always reports identical, and that the raced guard does not mask
// the change (the virtual DB source is held out of the guard, so it keeps its
// real changed verdict).
func TestParseDiffForgeDetectsStoredDrift(t *testing.T) {
env := setupSingleAgentTestEnv(t, parser.AgentForge)
forge := createForgeDB(t, env.forgeDir)
forge.addConversation(
t, "forge-drift-1", "Forge Drift",
forgeTestContext("Original prompt.", "Original answer."),
"2026-05-02 09:58:15.741021507",
"2026-05-02 10:00:16.848497543",
`{"input_tokens":360,"output_tokens":55,"cached_input_tokens":85}`,
)
runSyncAndAssert(t, env.engine, sync.SyncStats{TotalSessions: 1, Synced: 1})
// Stored-row drift against the unchanged source.
mutateDB(t, env,
"UPDATE sessions SET first_message = ? WHERE id = ?",
"drifted first message", "forge:forge-drift-1")
report := runParseDiff(t, env, sync.ParseDiffOptions{
Agents: []parser.AgentType{parser.AgentForge},
})
assert.Equal(t, sync.ParseDiffTotals{Examined: 1, Changed: 1},
report.Totals, "forge drift must be detected as changed")
sd := findSessionDiff(report, "forge:forge-drift-1")
require.NotNil(t, sd, "drifted forge session must be listed")
assert.Equal(t, sync.DiffChanged, sd.Class, "class")
assert.ElementsMatch(t, []string{sync.FieldFirstMessage},
sessionDiffFieldNames(sd, false), "non-informational fields")
assert.Equal(t, 1, report.FieldCounts[sync.FieldFirstMessage],
"FieldCounts[first_message]")
assert.True(t, report.HasFailures(), "drift must trip --fail-on-change")
}
// TestParseDiffDBBackedLimitScopesPerSession is the per-session-keying acid
// test. A shared .forge.db holds two conversations; --limit 1 samples one and
// cuts the other. Because these providers discover one virtual source per
// session, the cut conversation must be reported as an ordinary
// not-sampled skip -- NOT fanned into a presence "changed" false positive
// (which a shared-DB base key would produce) and NOT bucketed as
// "database-backed agent" (which the pre-relaxation sweep would produce for a
// FileBased=false agent). Totals.Changed must stay 0.
func TestParseDiffDBBackedLimitScopesPerSession(t *testing.T) {
env := setupSingleAgentTestEnv(t, parser.AgentForge)
forge := createForgeDB(t, env.forgeDir)
forge.addConversation(
t, "forge-a", "Conversation A",
forgeTestContext("Prompt A.", "Answer A."),
"2026-05-02 09:00:00", "2026-05-02 09:01:00",
`{"input_tokens":100,"output_tokens":20}`,
)
forge.addConversation(
t, "forge-b", "Conversation B",
forgeTestContext("Prompt B.", "Answer B."),
"2026-05-02 09:00:00", "2026-05-02 09:02:00",
`{"input_tokens":120,"output_tokens":25}`,
)
runSyncAndAssert(t, env.engine, sync.SyncStats{TotalSessions: 2, Synced: 2})
report := runParseDiff(t, env, sync.ParseDiffOptions{
Agents: []parser.AgentType{parser.AgentForge},
Limit: 1,
})
assert.True(t, report.FilesLimited, "files limited")
assert.Equal(t, sync.ParseDiffTotals{
Examined: 1, Identical: 1, Skipped: 1,
}, report.Totals, "one conversation sampled, one cut")
// The cut conversation must never be counted as parser drift.
assert.Zero(t, report.Totals.Changed,
"cut sibling conversation must not become a presence change")
assert.Empty(t, report.FieldCounts,
"no field drift from an unsampled sibling")
// Exactly one session is listed: the cut one, as a not-sampled skip.
var skipped []sync.SessionDiff
for _, s := range report.Sessions {
if s.Class == sync.DiffSkipped {
skipped = append(skipped, s)
}
}
require.Len(t, skipped, 1, "exactly one skipped session listed")
assert.Contains(t, skipped[0].Reason, "limit",
"cut DB-backed session must read as not-sampled, "+
"not 'database-backed agent'")
}
// TestParseDiffDBBackedLimitOrdersByPerSessionMtime is the ordering acid test.
// A shared .forge.db holds two conversations whose virtual-path order (by
// conversation ID) is the opposite of their updated_at order: forge-a sorts
// lexicographically first but is older, forge-b sorts later but is newer. Under
// --limit 1 the newest session must be the one sampled, so forge-a is cut.
// Before the ordering fix both virtual sources stat to mtime 0, --limit
// tie-breaks on the lexicographically-earlier path, forge-a is sampled instead,
// and the newer forge-b is dropped -- so this asserted skip would be
// "forge:forge-b" and the test would fail.
func TestParseDiffDBBackedLimitOrdersByPerSessionMtime(t *testing.T) {
env := setupSingleAgentTestEnv(t, parser.AgentForge)
forge := createForgeDB(t, env.forgeDir)
// forge-a: sorts first by path, older updated_at.
forge.addConversation(
t, "forge-a", "Older A",
forgeTestContext("Prompt A.", "Answer A."),
"2026-05-01 09:00:00", "2026-05-01 09:00:00",
`{"input_tokens":100,"output_tokens":20}`,
)
// forge-b: sorts later by path, newer updated_at.
forge.addConversation(
t, "forge-b", "Newer B",
forgeTestContext("Prompt B.", "Answer B."),
"2026-06-01 09:00:00", "2026-06-01 09:00:00",
`{"input_tokens":120,"output_tokens":25}`,
)
runSyncAndAssert(t, env.engine, sync.SyncStats{TotalSessions: 2, Synced: 2})
report := runParseDiff(t, env, sync.ParseDiffOptions{
Agents: []parser.AgentType{parser.AgentForge},
Limit: 1,
})
assert.True(t, report.FilesLimited, "files limited")
assert.Equal(t, sync.ParseDiffTotals{
Examined: 1, Identical: 1, Skipped: 1,
}, report.Totals, "the newer conversation is sampled, the older cut")
// A clean identical sample is not listed, so the sole listed session is the
// cut (older) one -- and it must be forge-a, proving the newer forge-b won
// the --limit slot by mtime rather than by lexicographic path.
var skipped []sync.SessionDiff
for _, s := range report.Sessions {
if s.Class == sync.DiffSkipped {
skipped = append(skipped, s)
}
}
require.Len(t, skipped, 1, "exactly one skipped session listed")
assert.Equal(t, "forge:forge-a", skipped[0].SessionID,
"the older conversation must be the one cut by --limit")
assert.Contains(t, skipped[0].Reason, "limit",
"cut session reads as not-sampled")
}
func TestParseDiffWindsurfLimitScopesPerSession(t *testing.T) {
env := setupSingleAgentTestEnv(t, parser.AgentWindsurf)
createWindsurfWorkspaceDB(t, env.windsurfDir, `{
"tabs": [
{
"tabId": "windsurf-a",
"chatTitle": "Conversation A",
"bubbles": [
{"type": "user", "text": "Prompt A."},
{"type": "assistant", "text": "Answer A."}
]
},
{
"tabId": "windsurf-b",
"chatTitle": "Conversation B",
"bubbles": [
{"type": "user", "text": "Prompt B."},
{"type": "assistant", "text": "Answer B."}
]
}
]
}`)
runSyncAndAssert(t, env.engine, sync.SyncStats{TotalSessions: 2, Synced: 2})
report := runParseDiff(t, env, sync.ParseDiffOptions{
Agents: []parser.AgentType{parser.AgentWindsurf},
Limit: 1,
})
assert.True(t, report.FilesLimited, "files limited")
assert.Equal(t, sync.ParseDiffTotals{
Examined: 1, Identical: 1, Skipped: 1,
}, report.Totals, "one Windsurf tab sampled, one cut")
assert.Zero(t, report.Totals.Changed,
"cut Windsurf sibling must not become a presence change")
assert.Empty(t, report.FieldCounts,
"no field drift from an unsampled Windsurf sibling")
var skipped []sync.SessionDiff
for _, s := range report.Sessions {
if s.Class == sync.DiffSkipped {
skipped = append(skipped, s)
}
}
require.Len(t, skipped, 1, "exactly one skipped Windsurf session listed")
assert.Contains(t, skipped[0].Reason, "limit",
"cut Windsurf session reads as not-sampled")
}