Files
goclaw/tests/integration/v3_vault_store_ext_test.go
viettranx 7cfcbbf9db fix(vault): include shared docs in agent read paths (#917)
- Patch vault_search, ListDocuments, CountDocuments, ListTreeEntries to
  include shared docs (agent_id IS NULL) for agents in the tenant
- Keep DELETE/GetDocument/GetByBasename strict (auth-intent)
- Add CHECK invariant vault_documents_scope_consistency (PG migration
  000055 NOT VALID, SQLite triggers v24) to prevent future drift
- Update docs and changelog

Affects PostgreSQL and SQLite (desktop edition).
2026-04-16 14:17:48 +07:00

722 lines
21 KiB
Go

//go:build integration
package integration
import (
"context"
"database/sql"
"errors"
"sort"
"testing"
"github.com/google/uuid"
"github.com/nextlevelbuilder/goclaw/internal/store"
)
// TestStoreVault_CountDocuments verifies CountDocuments returns correct count per tenant+agent.
func TestStoreVault_CountDocuments(t *testing.T) {
db := testDB(t)
tenantID, agentID := seedTenantAgent(t, db)
ctx := tenantCtx(tenantID)
vs := newVaultStore(db)
tid := tenantID.String()
aid := agentID.String()
for _, p := range []string{"count/a.md", "count/b.md", "count/c.md"} {
if err := vs.UpsertDocument(ctx, makeVaultDoc(tid, aid, p, "Count Doc "+p)); err != nil {
t.Fatalf("UpsertDocument %s: %v", p, err)
}
}
n, err := vs.CountDocuments(ctx, tid, aid, store.VaultListOptions{})
if err != nil {
t.Fatalf("CountDocuments: %v", err)
}
if n < 3 {
t.Errorf("CountDocuments = %d, want >= 3", n)
}
}
// TestStoreVault_GetDocumentsByIDs verifies batch fetch with tenant isolation.
func TestStoreVault_GetDocumentsByIDs(t *testing.T) {
db := testDB(t)
tenantID, agentID := seedTenantAgent(t, db)
ctx := tenantCtx(tenantID)
vs := newVaultStore(db)
tid := tenantID.String()
aid := agentID.String()
docA := makeVaultDoc(tid, aid, "batch/a.md", "Batch A")
docB := makeVaultDoc(tid, aid, "batch/b.md", "Batch B")
for _, d := range []*store.VaultDocument{docA, docB} {
if err := vs.UpsertDocument(ctx, d); err != nil {
t.Fatalf("UpsertDocument: %v", err)
}
}
results, err := vs.GetDocumentsByIDs(ctx, tid, []string{docA.ID, docB.ID})
if err != nil {
t.Fatalf("GetDocumentsByIDs: %v", err)
}
if len(results) != 2 {
t.Errorf("GetDocumentsByIDs len = %d, want 2", len(results))
}
byID := map[string]store.VaultDocument{}
for _, d := range results {
byID[d.ID] = d
}
if byID[docA.ID].Title != "Batch A" {
t.Errorf("docA title = %q, want %q", byID[docA.ID].Title, "Batch A")
}
if byID[docB.ID].Title != "Batch B" {
t.Errorf("docB title = %q, want %q", byID[docB.ID].Title, "Batch B")
}
}
// TestStoreVault_GetDocumentsByIDs_CrossTenantIsolation verifies cross-tenant isolation in batch fetch.
func TestStoreVault_GetDocumentsByIDs_CrossTenantIsolation(t *testing.T) {
db := testDB(t)
tenantA, agentA := seedTenantAgent(t, db)
tenantB, _ := seedTenantAgent(t, db)
ctxA := tenantCtx(tenantA)
vs := newVaultStore(db)
tidA := tenantA.String()
tidB := tenantB.String()
aidA := agentA.String()
docA := makeVaultDoc(tidA, aidA, "iso/secret.md", "Tenant A Secret")
if err := vs.UpsertDocument(ctxA, docA); err != nil {
t.Fatalf("UpsertDocument tenantA: %v", err)
}
// Fetch with tenant B — should return empty slice (ID exists but wrong tenant).
results, err := vs.GetDocumentsByIDs(ctxA, tidB, []string{docA.ID})
if err != nil {
t.Fatalf("GetDocumentsByIDs cross-tenant: %v", err)
}
if len(results) != 0 {
t.Errorf("expected 0 results for cross-tenant fetch, got %d", len(results))
}
}
// TestStoreVault_UpdateHash verifies UpdateHash modifies the content_hash column.
func TestStoreVault_UpdateHash(t *testing.T) {
db := testDB(t)
tenantID, agentID := seedTenantAgent(t, db)
ctx := tenantCtx(tenantID)
vs := newVaultStore(db)
tid := tenantID.String()
aid := agentID.String()
doc := makeVaultDoc(tid, aid, "hash/doc.md", "Hash Update Test")
if err := vs.UpsertDocument(ctx, doc); err != nil {
t.Fatalf("UpsertDocument: %v", err)
}
newHash := "deadbeef" + uuid.New().String()[:8]
if err := vs.UpdateHash(ctx, tid, doc.ID, newHash); err != nil {
t.Fatalf("UpdateHash: %v", err)
}
got, err := vs.GetDocumentByID(ctx, tid, doc.ID)
if err != nil {
t.Fatalf("GetDocumentByID after UpdateHash: %v", err)
}
if got.ContentHash != newHash {
t.Errorf("ContentHash = %q, want %q", got.ContentHash, newHash)
}
}
// TestStoreVault_ListDocuments_DocTypeFilter verifies DocTypes filter on ListDocuments.
func TestStoreVault_ListDocuments_DocTypeFilter(t *testing.T) {
db := testDB(t)
tenantID, agentID := seedTenantAgent(t, db)
ctx := tenantCtx(tenantID)
vs := newVaultStore(db)
tid := tenantID.String()
aid := agentID.String()
// Insert docs of two different types.
for _, tc := range []struct {
path string
docType string
}{
{"types/note1.md", "note"},
{"types/note2.md", "note"},
{"types/skill1.md", "skill"},
} {
d := makeVaultDoc(tid, aid, tc.path, "Doc "+tc.path)
d.DocType = tc.docType
if err := vs.UpsertDocument(ctx, d); err != nil {
t.Fatalf("UpsertDocument %s: %v", tc.path, err)
}
}
notes, err := vs.ListDocuments(ctx, tid, aid, store.VaultListOptions{
DocTypes: []string{"note"},
Limit: 50,
})
if err != nil {
t.Fatalf("ListDocuments notes filter: %v", err)
}
for _, d := range notes {
if d.DocType != "note" {
t.Errorf("ListDocuments with DocType=note returned doc with type=%q", d.DocType)
}
}
skills, err := vs.ListDocuments(ctx, tid, aid, store.VaultListOptions{
DocTypes: []string{"skill"},
Limit: 50,
})
if err != nil {
t.Fatalf("ListDocuments skills filter: %v", err)
}
for _, d := range skills {
if d.DocType != "skill" {
t.Errorf("ListDocuments with DocType=skill returned doc with type=%q", d.DocType)
}
}
}
// TestStoreVault_FTSSearch verifies text search returns relevant results.
func TestStoreVault_FTSSearch(t *testing.T) {
db := testDB(t)
tenantID, agentID := seedTenantAgent(t, db)
ctx := tenantCtx(tenantID)
vs := newVaultStore(db)
tid := tenantID.String()
aid := agentID.String()
docs := []struct {
path string
title string
summary string
}{
{"fts/go.md", "Go Programming", "Go concurrency goroutines channels"},
{"fts/python.md", "Python Basics", "Python scripting data science"},
{"fts/rust.md", "Rust Systems", "Rust memory safety ownership model"},
}
for _, d := range docs {
vd := makeVaultDoc(tid, aid, d.path, d.title)
vd.Summary = d.summary
if err := vs.UpsertDocument(ctx, vd); err != nil {
t.Fatalf("UpsertDocument %s: %v", d.path, err)
}
}
// Search for "goroutines" — should match the Go doc.
results, err := vs.Search(ctx, store.VaultSearchOptions{
Query: "goroutines",
TenantID: tid,
AgentID: aid,
MaxResults: 5,
})
if err != nil {
t.Fatalf("Search: %v", err)
}
// The Go doc must appear in results (FTS hit on summary).
found := false
for _, r := range results {
if r.Document.Path == "fts/go.md" {
found = true
}
}
if !found && len(results) > 0 {
// If search returned something, verify it's relevant (not strict — FTS ranking varies).
t.Logf("Search returned %d results but fts/go.md not top-ranked — acceptable", len(results))
}
}
// TestStoreVault_DeleteDocument_ErrNoRows verifies correct error after delete.
func TestStoreVault_GetDocument_NotFound_ErrNoRows(t *testing.T) {
db := testDB(t)
tenantID, agentID := seedTenantAgent(t, db)
ctx := tenantCtx(tenantID)
vs := newVaultStore(db)
tid := tenantID.String()
aid := agentID.String()
_, err := vs.GetDocument(ctx, tid, aid, "nonexistent/path.md")
if !errors.Is(err, sql.ErrNoRows) {
t.Errorf("expected sql.ErrNoRows for missing doc, got %v", err)
}
}
// TestStoreVault_Link_TenantIsolation verifies vault links respect tenant boundary.
func TestStoreVault_Link_TenantIsolation(t *testing.T) {
db := testDB(t)
tenantA, agentA := seedTenantAgent(t, db)
tenantB, agentB := seedTenantAgent(t, db)
ctxA := tenantCtx(tenantA)
vs := newVaultStore(db)
tidA := tenantA.String()
tidB := tenantB.String()
aidA := agentA.String()
aidB := agentB.String()
// Create docs in tenant A.
docA1 := makeVaultDoc(tidA, aidA, "link-iso/a1.md", "A Doc 1")
docA2 := makeVaultDoc(tidA, aidA, "link-iso/a2.md", "A Doc 2")
for _, d := range []*store.VaultDocument{docA1, docA2} {
if err := vs.UpsertDocument(ctxA, d); err != nil {
t.Fatalf("UpsertDocument tenantA: %v", err)
}
}
// Create a link between docs in tenant A.
link := &store.VaultLink{
FromDocID: docA1.ID,
ToDocID: docA2.ID,
LinkType: "wikilink",
Context: "cross-link test",
}
if err := vs.CreateLink(ctxA, link); err != nil {
t.Fatalf("CreateLink tenantA: %v", err)
}
// Tenant B must not see tenant A's outlinks.
outB, err := vs.GetOutLinks(ctxA, tidB, docA1.ID)
if err != nil {
t.Fatalf("GetOutLinks cross-tenant: %v", err)
}
for _, l := range outB {
if l.FromDocID == docA1.ID {
t.Error("tenant B sees tenant A vault link — isolation broken")
}
}
// Tenant A must see its own link.
outA, err := vs.GetOutLinks(ctxA, tidA, docA1.ID)
if err != nil {
t.Fatalf("GetOutLinks tenantA: %v", err)
}
if len(outA) != 1 {
t.Errorf("tenant A outlinks = %d, want 1", len(outA))
}
// Create a doc in tenant B (separate cleanup via seedTenantAgent).
docB := makeVaultDoc(tidB, aidB, "link-iso/b.md", "B Doc")
_ = docB // suppress unused warning; seedTenantAgent cleanup handles vault_documents via tenantB
}
// TestStoreVault_ListDocuments_Pagination verifies limit/offset pagination.
func TestStoreVault_ListDocuments_Pagination(t *testing.T) {
db := testDB(t)
tenantID, agentID := seedTenantAgent(t, db)
ctx := tenantCtx(tenantID)
vs := newVaultStore(db)
tid := tenantID.String()
aid := agentID.String()
for i := 0; i < 5; i++ {
p := "page/doc" + string(rune('A'+i)) + ".md"
if err := vs.UpsertDocument(ctx, makeVaultDoc(tid, aid, p, "Page Doc "+p)); err != nil {
t.Fatalf("UpsertDocument %d: %v", i, err)
}
}
t.Run("page1_limit2", func(t *testing.T) {
docs, err := vs.ListDocuments(ctx, tid, aid, store.VaultListOptions{Limit: 2, Offset: 0})
if err != nil {
t.Fatalf("ListDocuments page1: %v", err)
}
if len(docs) != 2 {
t.Errorf("page1 len = %d, want 2", len(docs))
}
})
t.Run("page2_limit2", func(t *testing.T) {
docs, err := vs.ListDocuments(ctx, tid, aid, store.VaultListOptions{Limit: 2, Offset: 2})
if err != nil {
t.Fatalf("ListDocuments page2: %v", err)
}
if len(docs) == 0 {
t.Error("page2: expected at least 1 doc")
}
})
t.Run("last_page", func(t *testing.T) {
docs, err := vs.ListDocuments(ctx, tid, aid, store.VaultListOptions{Limit: 2, Offset: 4})
if err != nil {
t.Fatalf("ListDocuments last page: %v", err)
}
// At least 1 (the 5th doc).
if len(docs) < 1 {
t.Error("last page: expected at least 1 doc")
}
})
}
// ---- Characterization tests: shared doc visibility (RED on unpatched code) ----
// TestStoreVault_List_IncludesShared asserts ListDocuments returns shared (agent_id=NULL) docs
// alongside personal docs for the same tenant+agent.
// BUG: currently fails — strict "AND agent_id = $N" excludes NULL rows.
func TestStoreVault_List_IncludesShared(t *testing.T) {
db := testDB(t)
tenantID, agentID := seedTenantAgent(t, db)
ctx := tenantCtx(tenantID)
vs := newVaultStore(db)
tid := tenantID.String()
aid := agentID.String()
// Seed personal doc (agent_id = A1).
personal := makeVaultDoc(tid, aid, "agents/a1/alpha.md", "personal alpha doc")
personal.Summary = "personal alpha text"
if err := vs.UpsertDocument(ctx, personal); err != nil {
t.Fatalf("upsert personal: %v", err)
}
// Seed shared doc (agent_id = NULL).
shared := makeSharedVaultDoc(tid, "shared/alpha.md", "shared alpha doc")
shared.Summary = "shared alpha text"
if err := vs.UpsertDocument(ctx, shared); err != nil {
t.Fatalf("upsert shared: %v", err)
}
docs, err := vs.ListDocuments(ctx, tid, aid, store.VaultListOptions{Limit: 50})
if err != nil {
t.Fatalf("ListDocuments: %v", err)
}
paths := make([]string, 0, len(docs))
for _, d := range docs {
paths = append(paths, d.Path)
}
sort.Strings(paths)
t.Logf("ListDocuments returned %d docs: %v", len(docs), paths)
foundShared := false
foundPersonal := false
for _, d := range docs {
if d.Path == "shared/alpha.md" {
foundShared = true
}
if d.Path == "agents/a1/alpha.md" {
foundPersonal = true
}
}
if !foundPersonal {
t.Error("ListDocuments: personal doc agents/a1/alpha.md not found")
}
if !foundShared {
t.Errorf("ListDocuments: shared doc shared/alpha.md not found — BUG: shared docs excluded (agent_id=NULL filtered out)")
}
}
// TestStoreVault_Count_IncludesShared asserts CountDocuments counts shared docs.
// BUG: currently fails — shared docs excluded from count.
func TestStoreVault_Count_IncludesShared(t *testing.T) {
db := testDB(t)
tenantID, agentID := seedTenantAgent(t, db)
ctx := tenantCtx(tenantID)
vs := newVaultStore(db)
tid := tenantID.String()
aid := agentID.String()
personal := makeVaultDoc(tid, aid, "agents/a1/cnt-alpha.md", "personal alpha doc")
personal.Summary = "personal alpha text"
if err := vs.UpsertDocument(ctx, personal); err != nil {
t.Fatalf("upsert personal: %v", err)
}
shared := makeSharedVaultDoc(tid, "shared/cnt-alpha.md", "shared alpha doc")
shared.Summary = "shared alpha text"
if err := vs.UpsertDocument(ctx, shared); err != nil {
t.Fatalf("upsert shared: %v", err)
}
n, err := vs.CountDocuments(ctx, tid, aid, store.VaultListOptions{})
if err != nil {
t.Fatalf("CountDocuments: %v", err)
}
t.Logf("CountDocuments = %d", n)
if n < 2 {
t.Errorf("CountDocuments = %d, want >= 2 — BUG: shared docs (agent_id=NULL) excluded from count", n)
}
}
// TestStoreVault_Search_SharedDocsVisible asserts Search (FTS) returns shared docs.
// BUG: currently fails — "AND agent_id = $N" in ftsSearch excludes NULL rows.
func TestStoreVault_Search_SharedDocsVisible(t *testing.T) {
db := testDB(t)
tenantID, agentID := seedTenantAgent(t, db)
ctx := tenantCtx(tenantID)
vs := newVaultStore(db)
tid := tenantID.String()
aid := agentID.String()
personal := makeVaultDoc(tid, aid, "agents/a1/srch-alpha.md", "personal alpha doc")
personal.Summary = "personal alpha text"
if err := vs.UpsertDocument(ctx, personal); err != nil {
t.Fatalf("upsert personal: %v", err)
}
shared := makeSharedVaultDoc(tid, "shared/srch-alpha.md", "shared alpha doc")
shared.Summary = "shared alpha text"
if err := vs.UpsertDocument(ctx, shared); err != nil {
t.Fatalf("upsert shared: %v", err)
}
results, err := vs.Search(ctx, store.VaultSearchOptions{
Query: "alpha",
TenantID: tid,
AgentID: aid,
MaxResults: 20,
})
if err != nil {
t.Fatalf("Search: %v", err)
}
paths := make([]string, 0, len(results))
for _, r := range results {
paths = append(paths, r.Document.Path)
}
sort.Strings(paths)
t.Logf("Search returned %d results: %v", len(results), paths)
foundShared := false
foundPersonal := false
for _, r := range results {
if r.Document.Path == "shared/srch-alpha.md" {
foundShared = true
}
if r.Document.Path == "agents/a1/srch-alpha.md" {
foundPersonal = true
}
}
if !foundPersonal {
t.Error("Search: personal doc agents/a1/srch-alpha.md not found")
}
if !foundShared {
t.Errorf("Search: shared doc shared/srch-alpha.md not found — BUG: shared docs (agent_id=NULL) excluded from FTS results")
}
}
// TestStoreVault_Tree_SharedDocsVisible asserts ListTreeEntries includes shared docs.
// BUG: currently fails — appendTreeFilters adds "AND agent_id = $N" (PG line 774).
func TestStoreVault_Tree_SharedDocsVisible(t *testing.T) {
db := testDB(t)
tenantID, agentID := seedTenantAgent(t, db)
ctx := tenantCtx(tenantID)
vs := newVaultStore(db)
tid := tenantID.String()
aid := agentID.String()
personal := makeVaultDoc(tid, aid, "agents/a1/tree-alpha.md", "personal alpha doc")
personal.Summary = "personal alpha text"
if err := vs.UpsertDocument(ctx, personal); err != nil {
t.Fatalf("upsert personal: %v", err)
}
shared := makeSharedVaultDoc(tid, "shared/tree-alpha.md", "shared alpha doc")
shared.Summary = "shared alpha text"
if err := vs.UpsertDocument(ctx, shared); err != nil {
t.Fatalf("upsert shared: %v", err)
}
entries, err := vs.ListTreeEntries(ctx, tid, store.VaultTreeOptions{AgentID: aid, Path: ""})
if err != nil {
t.Fatalf("ListTreeEntries: %v", err)
}
names := make([]string, 0, len(entries))
for _, e := range entries {
names = append(names, e.Path)
}
sort.Strings(names)
t.Logf("ListTreeEntries returned %d entries: %v", len(entries), names)
foundSharedFolder := false
for _, e := range entries {
// The shared doc is at shared/tree-alpha.md; at root level we expect "shared" folder entry.
if e.Path == "shared" || e.Path == "shared/tree-alpha.md" {
foundSharedFolder = true
}
}
if !foundSharedFolder {
t.Errorf("ListTreeEntries: 'shared' folder/file not found — BUG: shared docs (agent_id=NULL) excluded from tree (appendTreeFilters strict agent filter)")
}
}
// TestStoreVault_Search_TeamAgentSeesShared asserts that a team-context Search
// returns shared docs in addition to personal + team docs.
// BUG: currently fails — searchTeamFilter.append uses strict "AND team_id = $N"
// when teamID is set, which excludes shared docs (team_id=NULL, agent_id=NULL).
// NOTE: Search uses opts.TeamID directly (not RunContext).
func TestStoreVault_Search_TeamAgentSeesShared(t *testing.T) {
db := testDB(t)
tenantID, agentID := seedTenantAgent(t, db)
ctx := tenantCtx(tenantID)
vs := newVaultStore(db)
tid := tenantID.String()
aid := agentID.String()
teamID, _ := seedTeam(t, db, tenantID, agentID)
teamIDStr := teamID.String()
// Personal doc (agent_id = A1, team_id = NULL).
personal := makeVaultDoc(tid, aid, "agents/a1/team-alpha.md", "personal alpha doc")
personal.Summary = "personal alpha text"
if err := vs.UpsertDocument(ctx, personal); err != nil {
t.Fatalf("upsert personal: %v", err)
}
// Team doc (agent_id = NULL, team_id = T1).
team := makeTeamVaultDoc(tid, teamIDStr, "teams/t1/team-alpha.md", "team alpha doc")
team.Summary = "team alpha text"
if err := vs.UpsertDocument(ctx, team); err != nil {
t.Fatalf("upsert team: %v", err)
}
// Shared doc (agent_id = NULL, team_id = NULL).
shared := makeSharedVaultDoc(tid, "shared/team-alpha.md", "shared alpha doc")
shared.Summary = "shared alpha text"
if err := vs.UpsertDocument(ctx, shared); err != nil {
t.Fatalf("upsert shared: %v", err)
}
results, err := vs.Search(ctx, store.VaultSearchOptions{
Query: "alpha",
TenantID: tid,
AgentID: aid,
TeamID: &teamIDStr,
MaxResults: 20,
})
if err != nil {
t.Fatalf("Search with TeamID: %v", err)
}
paths := make([]string, 0, len(results))
for _, r := range results {
paths = append(paths, r.Document.Path)
}
sort.Strings(paths)
t.Logf("Search (TeamID=%s) returned %d results: %v", teamIDStr, len(results), paths)
foundPersonal := false
foundTeam := false
foundShared := false
for _, r := range results {
switch r.Document.Path {
case "agents/a1/team-alpha.md":
foundPersonal = true
case "teams/t1/team-alpha.md":
foundTeam = true
case "shared/team-alpha.md":
foundShared = true
}
}
if !foundPersonal {
t.Error("Search: personal doc agents/a1/team-alpha.md not found")
}
if !foundTeam {
t.Error("Search: team doc teams/t1/team-alpha.md not found")
}
if !foundShared {
t.Errorf("Search: shared doc shared/team-alpha.md not found — BUG: strict team_id filter excludes shared docs (team_id=NULL, agent_id=NULL)")
}
}
// ---- Guard tests (must PASS now and stay green through all phases) ----
// TestStoreVault_Delete_RemainsStrict asserts that DeleteDocument with a specific agentID
// does NOT delete shared docs (agent_id=NULL) — the strict agent filter protects shared data.
func TestStoreVault_Delete_RemainsStrict(t *testing.T) {
db := testDB(t)
tenantID, agentID := seedTenantAgent(t, db)
ctx := tenantCtx(tenantID)
vs := newVaultStore(db)
tid := tenantID.String()
aid := agentID.String()
shared := makeSharedVaultDoc(tid, "shared/del-guard.md", "shared alpha doc")
if err := vs.UpsertDocument(ctx, shared); err != nil {
t.Fatalf("upsert shared: %v", err)
}
// Attempt to delete the shared doc using agentID — should NOT match (agent_id=NULL ≠ agentID).
if err := vs.DeleteDocument(ctx, tid, aid, "shared/del-guard.md"); err != nil {
t.Fatalf("DeleteDocument: %v", err)
}
// Verify shared doc still exists via raw SQL (bypasses store agent filter).
var count int
err := db.QueryRowContext(ctx,
`SELECT COUNT(*) FROM vault_documents WHERE tenant_id = $1 AND path = $2 AND agent_id IS NULL`,
tenantID, "shared/del-guard.md").Scan(&count)
if err != nil {
t.Fatalf("raw count query: %v", err)
}
if count != 1 {
t.Errorf("shared doc deleted by agent-scoped DeleteDocument — guard broken (count=%d, want 1)", count)
}
}
// TestStoreVault_Search_CrossTenantIsolation asserts that shared docs from tenant A
// are NOT visible to a search from tenant B.
func TestStoreVault_Search_CrossTenantIsolation(t *testing.T) {
db := testDB(t)
tenantA, agentA := seedTenantAgent(t, db)
tenantB, agentB := seedTenantAgent(t, db)
ctxA := tenantCtx(tenantA)
vs := newVaultStore(db)
tidA := tenantA.String()
tidB := tenantB.String()
aidA := agentA.String()
aidB := agentB.String()
// Seed shared doc in tenant A.
shared := makeSharedVaultDoc(tidA, "shared/iso-alpha.md", "shared alpha doc")
shared.Summary = "shared alpha text"
if err := vs.UpsertDocument(ctxA, shared); err != nil {
t.Fatalf("upsert shared tenantA: %v", err)
}
// Search from tenant B — must return 0 results for "alpha".
results, err := vs.Search(context.Background(), store.VaultSearchOptions{
Query: "alpha",
TenantID: tidB,
AgentID: aidB,
MaxResults: 20,
})
if err != nil {
t.Fatalf("Search tenantB: %v", err)
}
t.Logf("Cross-tenant search returned %d results (want 0)", len(results))
// Filter to only shared paths to catch leaks.
for _, r := range results {
if r.Document.TenantID == tidA {
t.Errorf("cross-tenant isolation broken: tenant B search returned tenant A doc %q", r.Document.Path)
}
}
// Confirm agent A doc is isolated as a sanity baseline.
_ = aidA
}