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

927 lines
32 KiB
Go

package server_test
import (
"context"
"database/sql"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"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"
corerecall "go.kenn.io/agentsview/internal/recall"
"go.kenn.io/agentsview/internal/server"
"go.kenn.io/agentsview/internal/service"
)
type listRecallEntriesResponse struct {
RecallEntries []db.RecallResult `json:"entries"`
TrustedOnly bool `json:"trusted_only"`
}
type queryRecallEntriesResponse struct {
QueryID string `json:"query_id"`
MissReason string `json:"miss_reason"`
RecallEntries []db.RecallResult `json:"entries"`
TrustedOnly bool `json:"trusted_only"`
Summary *service.RecallQuerySummary `json:"summary,omitempty"`
Context string `json:"context,omitempty"`
ContextMeta *service.RecallContextMeta `json:"context_meta,omitempty"`
ContextEntries []db.RecallResult `json:"context_entries,omitempty"`
}
type readOnlyRecallQueryStore struct {
db.Store
queryCalls int
}
func (s *readOnlyRecallQueryStore) QueryRecallEntries(
context.Context, db.RecallQuery,
) (db.RecallPage, error) {
s.queryCalls++
return db.RecallPage{}, db.ErrReadOnly
}
func (*readOnlyRecallQueryStore) ReadOnly() bool { return true }
func TestListRecallEntriesFiltersByProject(t *testing.T) {
te := setup(t)
seedRecallEntrySession(t, te)
seedRecallEntry(t, te, db.RecallEntry{
ID: "m1",
Title: "Check cwd before file reads",
Body: "Verify cwd before retrying failed reads.",
Project: "agentsview",
Agent: "codex",
SourceSessionID: "recall-session",
})
seedRecallEntry(t, te, db.RecallEntry{
ID: "m2",
Title: "Other project note",
Body: "Unrelated note.",
Project: "other",
Agent: "codex",
SourceSessionID: "other-session",
})
w := te.get(t, "/api/v1/recall/entries?q=cwd&project=agentsview&agent=codex")
assertStatus(t, w, http.StatusOK)
r := decode[listRecallEntriesResponse](t, w)
require.Len(t, r.RecallEntries, 1)
assert.Equal(t, "m1", r.RecallEntries[0].ID)
}
func TestListRecallEntriesFiltersBySourceSessionID(t *testing.T) {
te := setup(t)
seedRecallEntrySession(t, te)
seedRecallEntry(t, te, db.RecallEntry{
ID: "m-session",
Title: "Session scoped cwd recall",
Body: "Verify cwd before retrying failed reads.",
Project: "agentsview",
Agent: "codex",
SourceSessionID: "recall-session",
})
seedRecallEntry(t, te, db.RecallEntry{
ID: "m-other-session",
Title: "Other session cwd recall",
Body: "Verify cwd before retrying failed reads.",
Project: "agentsview",
Agent: "codex",
SourceSessionID: "other-session",
})
w := te.get(t, "/api/v1/recall/entries?q=cwd&source_session_id=recall-session")
assertStatus(t, w, http.StatusOK)
r := decode[listRecallEntriesResponse](t, w)
require.Len(t, r.RecallEntries, 1)
assert.Equal(t, "m-session", r.RecallEntries[0].ID)
}
func TestListRecallEntriesFiltersBySourceEpisodeID(t *testing.T) {
te := setup(t)
seedRecallEntrySession(t, te)
seedRecallEntry(t, te, db.RecallEntry{
ID: "m-episode",
Title: "Episode scoped cwd recall",
Body: "Verify cwd before retrying failed reads.",
Project: "agentsview",
Agent: "codex",
SourceSessionID: "recall-session",
SourceEpisodeID: "recall-session:chunk:0001",
})
seedRecallEntry(t, te, db.RecallEntry{
ID: "m-other-episode",
Title: "Other episode cwd recall",
Body: "Verify cwd before retrying failed reads.",
Project: "agentsview",
Agent: "codex",
SourceSessionID: "recall-session",
SourceEpisodeID: "recall-session:chunk:0002",
})
w := te.get(t, "/api/v1/recall/entries?q=cwd&source_episode_id=recall-session:chunk:0001")
assertStatus(t, w, http.StatusOK)
r := decode[listRecallEntriesResponse](t, w)
require.Len(t, r.RecallEntries, 1)
assert.Equal(t, "m-episode", r.RecallEntries[0].ID)
}
func TestListRecallEntriesFiltersTrustedOnly(t *testing.T) {
te := setup(t)
seedRecallEntrySession(t, te)
seedRecallEntry(t, te, db.RecallEntry{
ID: "trusted",
ReviewState: corerecall.ReviewStateHumanReviewed,
Title: "Trusted cwd recall",
Body: "Recover from wrong cwd before reading files.",
Project: "agentsview",
Agent: "codex",
SourceSessionID: "recall-session",
Transferable: true,
ProvenanceOK: true,
})
seedRecallEntry(t, te, db.RecallEntry{
ID: "untrusted",
ReviewState: corerecall.ReviewStateHumanReviewed,
Title: "Untrusted cwd recall",
Body: "Recover from wrong cwd before reading files.",
Project: "agentsview",
Agent: "codex",
SourceSessionID: "recall-session",
Transferable: true,
ProvenanceOK: false,
})
w := te.get(t, "/api/v1/recall/entries?q=wrong%20cwd%20files&trusted_only=true")
assertStatus(t, w, http.StatusOK)
r := decode[listRecallEntriesResponse](t, w)
require.Len(t, r.RecallEntries, 1)
assert.Equal(t, "trusted", r.RecallEntries[0].ID)
assert.True(t, r.TrustedOnly)
}
func TestListRecallEntriesTrustedOnlyRejectsArchivedStatus(t *testing.T) {
te := setup(t)
seedRecallEntrySession(t, te)
seedRecallEntry(t, te, db.RecallEntry{
ID: "trusted-status-control",
ReviewState: corerecall.ReviewStateHumanReviewed,
Title: "Trusted cwd control",
Body: "Check the cwd before reading files.",
SourceSessionID: "recall-session",
Transferable: true,
ProvenanceOK: true,
})
w := te.get(t,
"/api/v1/recall/entries?trusted_only=true&status=archived")
assertStatus(t, w, http.StatusBadRequest)
assertErrorResponse(t, w,
`invalid recall query: trusted_only requires status "accepted"`)
w = te.get(t,
"/api/v1/recall/entries?trusted_only=true&status=accepted")
assertStatus(t, w, http.StatusOK)
r := decode[listRecallEntriesResponse](t, w)
require.Len(t, r.RecallEntries, 1)
assert.Equal(t, "trusted-status-control", r.RecallEntries[0].ID)
}
func TestListRecallEntriesTrustedOnlyRejectsArchivedStatusBeforeReadOnlyStore(
t *testing.T,
) {
dataDir := t.TempDir()
cfg := config.Config{
Host: "127.0.0.1",
Port: 0,
DataDir: dataDir,
DBPath: filepath.Join(dataDir, "unused.db"),
WriteTimeout: 30 * time.Second,
}
store := &readOnlyRecallQueryStore{}
srv := server.New(cfg, store, nil)
handler := wrapTestHandler(cfg, srv.Handler())
req := httptest.NewRequest(http.MethodGet,
"/api/v1/recall/entries?trusted_only=true&status=archived", nil)
w := httptest.NewRecorder()
handler.ServeHTTP(w, req)
assertStatus(t, w, http.StatusBadRequest)
assertErrorResponse(t, w,
`invalid recall query: trusted_only requires status "accepted"`)
assert.Zero(t, store.queryCalls,
"invalid filters must fail before a read-only store is queried")
}
func TestListRecallEntriesFiltersBySupersessionLinks(t *testing.T) {
te := setup(t)
seedRecallEntrySession(t, te)
seedRecallEntry(t, te, db.RecallEntry{
ID: "old-a",
Title: "Old retry policy A",
Body: "Retry flaky command once.",
Project: "agentsview",
Agent: "codex",
SourceSessionID: "recall-session",
})
seedRecallEntry(t, te, db.RecallEntry{
ID: "old-b",
Title: "Old retry policy B",
Body: "Retry flaky command twice.",
Project: "agentsview",
Agent: "codex",
SourceSessionID: "recall-session",
})
_, err := te.db.SupersedeRecallEntry(t.Context(), "old-a", db.RecallEntry{
ID: "new-a",
Type: "procedure",
Scope: "project",
Title: "Current retry policy A",
Body: "Retry flaky command three times.",
Project: "agentsview",
Agent: "codex",
SourceSessionID: "recall-session",
})
require.NoError(t, err)
_, err = te.db.SupersedeRecallEntry(t.Context(), "old-b", db.RecallEntry{
ID: "new-b",
Type: "procedure",
Scope: "project",
Title: "Current retry policy B",
Body: "Retry flaky command four times.",
Project: "agentsview",
Agent: "codex",
SourceSessionID: "recall-session",
})
require.NoError(t, err)
w := te.get(t, "/api/v1/recall/entries?supersedes_entry_id=old-a")
assertStatus(t, w, http.StatusOK)
replacements := decode[listRecallEntriesResponse](t, w)
require.Len(t, replacements.RecallEntries, 1)
assert.Equal(t, "new-a", replacements.RecallEntries[0].ID)
w = te.get(t, "/api/v1/recall/entries?status=archived&superseded_by_entry_id=new-a")
assertStatus(t, w, http.StatusOK)
archived := decode[listRecallEntriesResponse](t, w)
require.Len(t, archived.RecallEntries, 1)
assert.Equal(t, "old-a", archived.RecallEntries[0].ID)
}
func TestListRecallEntriesWithoutQueryUsesUpdatedOrder(t *testing.T) {
te := setup(t)
seedRecallEntrySession(t, te)
seedRecallEntry(t, te, db.RecallEntry{
ID: "older-source-first",
Title: "Older recall",
Body: "Generic accepted recall.",
Project: "agentsview",
Agent: "codex",
SourceSessionID: "recall-session",
SourceEpisodeID: "a-source",
})
seedRecallEntry(t, te, db.RecallEntry{
ID: "newer-source-second",
Title: "Newer recall",
Body: "Generic accepted recall.",
Project: "agentsview",
Agent: "codex",
SourceSessionID: "recall-session",
SourceEpisodeID: "z-source",
})
raw, err := sql.Open("sqlite3", filepath.Join(te.dataDir, "test.db"))
require.NoError(t, err)
t.Cleanup(func() { raw.Close() })
_, err = raw.Exec(`
UPDATE recall_entries SET updated_at = CASE id
WHEN 'older-source-first' THEN '2024-01-01T00:00:00Z'
WHEN 'newer-source-second' THEN '2024-02-01T00:00:00Z'
ELSE updated_at
END
WHERE id IN ('older-source-first', 'newer-source-second')`)
require.NoError(t, err)
w := te.get(t, "/api/v1/recall/entries?project=agentsview&agent=codex&limit=2")
assertStatus(t, w, http.StatusOK)
r := decode[listRecallEntriesResponse](t, w)
require.Len(t, r.RecallEntries, 2)
assert.Equal(t, "newer-source-second", r.RecallEntries[0].ID)
assert.Equal(t, "older-source-first", r.RecallEntries[1].ID)
}
func TestGetRecallEntryFoundAndMissing(t *testing.T) {
te := setup(t)
seedRecallEntrySession(t, te)
seedRecallEntry(t, te, db.RecallEntry{
ID: "m1",
Title: "Check cwd before file reads",
Body: "Verify cwd before retrying failed reads.",
Project: "agentsview",
Agent: "codex",
SourceSessionID: "recall-session",
Evidence: []db.RecallEvidence{
{
SessionID: "recall-session",
MessageStartOrdinal: 3,
MessageEndOrdinal: 7,
ToolUseID: "toolu_1",
},
},
})
w := te.get(t, "/api/v1/recall/entries/m1")
assertStatus(t, w, http.StatusOK)
got := decode[db.RecallEntry](t, w)
assert.Equal(t, "m1", got.ID)
require.Len(t, got.Evidence, 1)
assert.Equal(t, "toolu_1", got.Evidence[0].ToolUseID)
w = te.get(t, "/api/v1/recall/entries/missing")
assertStatus(t, w, http.StatusNotFound)
}
func TestQueryRecallEntriesReturnsContext(t *testing.T) {
te := setup(t)
seedRecallEntrySession(t, te)
seedRecallEntry(t, te, db.RecallEntry{
ID: "m1",
ReviewState: corerecall.ReviewStateHumanReviewed,
Title: "Check cwd before file reads",
Body: "Verify cwd before retrying failed reads.",
Project: "agentsview",
Agent: "codex",
SourceSessionID: "recall-session",
Evidence: []db.RecallEvidence{
{
SessionID: "recall-session",
MessageStartOrdinal: 3,
MessageEndOrdinal: 7,
ToolUseID: "toolu_1",
},
},
})
w := te.post(t, "/api/v1/recall/query", `{
"query": "cwd failed reads",
"project": "agentsview",
"agent": "codex",
"limit": 5,
"include_context": true,
"context_max_bytes": 300
}`)
assertStatus(t, w, http.StatusOK)
r := decode[queryRecallEntriesResponse](t, w)
assert.NotEmpty(t, r.QueryID)
assert.Empty(t, r.MissReason)
require.Len(t, r.RecallEntries, 1)
assert.Equal(t, "m1", r.RecallEntries[0].ID)
require.NotNil(t, r.Summary)
assert.Equal(t, 1, r.Summary.Count)
assert.Equal(t, 1, r.Summary.ByType["procedure"])
assert.Equal(t, 1, r.Summary.ByScope["project"])
assert.Equal(t, 1, r.Summary.ByMatchReason["keyword"])
assert.Equal(t, 1, r.Summary.BySourceSession["recall-session"])
assert.Contains(t, r.Context, "Check cwd before file reads")
assert.Contains(t, r.Context, "recall-session:3-7")
require.NotNil(t, r.ContextMeta)
assert.Equal(t, 1, r.ContextMeta.EntryCount)
assert.Equal(t, []string{"m1"}, r.ContextMeta.IncludedIDs)
event, err := te.db.GetRecallQueryEvent(context.Background(), r.QueryID)
require.NoError(t, err)
require.NotNil(t, event)
assert.Equal(t, r.QueryID, event.QueryID)
assert.Equal(t, "query", event.Surface)
assert.Equal(t, 1, event.ResultCount)
assert.Equal(t, 1, event.PackedCount)
require.Len(t, event.Exposures, 1)
assert.True(t, event.Exposures[0].Packed)
}
func TestQueryRecallEntriesFiltersBySourceSessionID(t *testing.T) {
te := setup(t)
seedRecallEntrySession(t, te)
seedRecallEntry(t, te, db.RecallEntry{
ID: "m-session",
Title: "Session scoped cwd recall",
Body: "Verify cwd before retrying failed reads.",
Project: "agentsview",
Agent: "codex",
SourceSessionID: "recall-session",
})
seedRecallEntry(t, te, db.RecallEntry{
ID: "m-other-session",
Title: "Other session cwd recall",
Body: "Verify cwd before retrying failed reads.",
Project: "agentsview",
Agent: "codex",
SourceSessionID: "other-session",
})
w := te.post(t, "/api/v1/recall/query", `{
"query": "cwd failed reads",
"source_session_id": "recall-session",
"limit": 5
}`)
assertStatus(t, w, http.StatusOK)
r := decode[queryRecallEntriesResponse](t, w)
require.Len(t, r.RecallEntries, 1)
assert.Equal(t, "m-session", r.RecallEntries[0].ID)
}
func TestQueryRecallEntriesFiltersBySourceEpisodeID(t *testing.T) {
te := setup(t)
seedRecallEntrySession(t, te)
seedRecallEntry(t, te, db.RecallEntry{
ID: "m-episode",
Title: "Episode scoped cwd recall",
Body: "Verify cwd before retrying failed reads.",
Project: "agentsview",
Agent: "codex",
SourceSessionID: "recall-session",
SourceEpisodeID: "recall-session:chunk:0001",
})
seedRecallEntry(t, te, db.RecallEntry{
ID: "m-other-episode",
Title: "Other episode cwd recall",
Body: "Verify cwd before retrying failed reads.",
Project: "agentsview",
Agent: "codex",
SourceSessionID: "recall-session",
SourceEpisodeID: "recall-session:chunk:0002",
})
w := te.post(t, "/api/v1/recall/query", `{
"query": "cwd failed reads",
"source_episode_id": "recall-session:chunk:0001",
"limit": 5
}`)
assertStatus(t, w, http.StatusOK)
r := decode[queryRecallEntriesResponse](t, w)
require.Len(t, r.RecallEntries, 1)
assert.Equal(t, "m-episode", r.RecallEntries[0].ID)
}
func TestQueryRecallEntriesFiltersTrustedOnly(t *testing.T) {
te := setup(t)
seedRecallEntrySession(t, te)
seedRecallEntry(t, te, db.RecallEntry{
ID: "trusted",
ReviewState: corerecall.ReviewStateHumanReviewed,
Title: "Trusted cwd recall",
Body: "Recover from wrong cwd before reading files.",
Project: "agentsview",
Agent: "codex",
SourceSessionID: "recall-session",
Transferable: true,
ProvenanceOK: true,
})
seedRecallEntry(t, te, db.RecallEntry{
ID: "untrusted",
ReviewState: corerecall.ReviewStateHumanReviewed,
Title: "Untrusted cwd recall",
Body: "Recover from wrong cwd before reading files.",
Project: "agentsview",
Agent: "codex",
SourceSessionID: "recall-session",
Transferable: false,
ProvenanceOK: true,
})
w := te.post(t, "/api/v1/recall/query", `{
"query":"wrong cwd files",
"trusted_only":true,
"limit":5
}`)
assertStatus(t, w, http.StatusOK)
r := decode[queryRecallEntriesResponse](t, w)
require.Len(t, r.RecallEntries, 1)
assert.Equal(t, "trusted", r.RecallEntries[0].ID)
assert.True(t, r.TrustedOnly)
}
func TestQueryRecallEntriesTrustedOnlyRejectsArchivedStatus(t *testing.T) {
te := setup(t)
seedRecallEntrySession(t, te)
seedRecallEntry(t, te, db.RecallEntry{
ID: "trusted-status-control",
ReviewState: corerecall.ReviewStateHumanReviewed,
Title: "Trusted cwd control",
Body: "Check the cwd before reading files.",
SourceSessionID: "recall-session",
Transferable: true,
ProvenanceOK: true,
})
w := te.post(t, "/api/v1/recall/query", `{
"query":"cwd",
"status":"archived",
"trusted_only":true
}`)
assertStatus(t, w, http.StatusBadRequest)
assertErrorResponse(t, w,
`invalid recall query: trusted_only requires status "accepted"`)
w = te.post(t, "/api/v1/recall/query", `{
"query":"cwd",
"status":" accepted ",
"trusted_only":true
}`)
assertStatus(t, w, http.StatusOK)
r := decode[queryRecallEntriesResponse](t, w)
require.Len(t, r.RecallEntries, 1)
assert.Equal(t, "trusted-status-control", r.RecallEntries[0].ID)
}
func TestQueryRecallEntriesPacksMultipleFocusedContextEntries(t *testing.T) {
te := setup(t)
seedRecallEntrySession(t, te)
seedRecallEntry(t, te, db.RecallEntry{
ID: "m1",
Title: "Incident filters labels overview",
Body: "Incident filters labels summary. " +
strings.Repeat("long unrelated filler ", 80),
Project: "agentsview",
Agent: "codex",
SourceSessionID: "recall-session",
})
seedRecallEntry(t, te, db.RecallEntry{
ID: "m2",
Title: "Incident label details",
Body: "Incident Mobile and Incident Portal are the useful labels.",
Project: "agentsview",
Agent: "codex",
SourceSessionID: "recall-session",
})
w := te.post(t, "/api/v1/recall/query", `{
"query": "Incident filters labels",
"project": "agentsview",
"agent": "codex",
"limit": 2,
"include_context": true,
"context_max_bytes": 900
}`)
assertStatus(t, w, http.StatusOK)
r := decode[queryRecallEntriesResponse](t, w)
require.NotNil(t, r.ContextMeta)
assert.Equal(t, 2, r.ContextMeta.EntryCount)
assert.Equal(t, []string{"m1", "m2"}, r.ContextMeta.IncludedIDs)
assert.Contains(t, r.Context, "Incident Mobile")
assert.LessOrEqual(t, len([]byte(r.Context)), 900)
}
func TestQueryRecallEntriesReturnsOnlyPackedContextEntries(t *testing.T) {
te := setup(t)
seedRecallEntrySession(t, te)
seedRecallEntry(t, te, db.RecallEntry{
ID: "packed",
Title: "Needle packed cwd recall",
Body: "Short needle cwd note.",
Project: "agentsview",
Agent: "codex",
SourceSessionID: "recall-session",
})
seedRecallEntry(t, te, db.RecallEntry{
ID: "omitted",
Title: "Omitted cwd recall",
Body: strings.Repeat("Long cwd detail ", 120),
Project: "agentsview",
Agent: "codex",
SourceSessionID: "recall-session",
})
w := te.post(t, "/api/v1/recall/query", `{
"query": "needle cwd recall",
"project": "agentsview",
"agent": "codex",
"limit": 2,
"include_context": true,
"context_max_bytes": 340
}`)
assertStatus(t, w, http.StatusOK)
r := decode[queryRecallEntriesResponse](t, w)
require.Len(t, r.RecallEntries, 2)
require.NotNil(t, r.ContextMeta)
assert.Equal(t, []string{"packed"}, r.ContextMeta.IncludedIDs)
require.Len(t, r.ContextEntries, 1)
assert.Equal(t, "packed", r.ContextEntries[0].ID)
}
func TestQueryRecallEntriesRejectsNegativeContextMaxBytes(t *testing.T) {
te := setup(t)
w := te.post(t, "/api/v1/recall/query", `{
"query": "cwd",
"include_context": true,
"context_max_bytes": -1
}`)
assertStatus(t, w, http.StatusBadRequest)
assertBodyContains(t, w, "context_max_bytes")
}
func TestQueryRecallEntriesRejectsNegativeLimit(t *testing.T) {
te := setup(t)
w := te.post(t, "/api/v1/recall/query", `{
"query": "cwd",
"limit": -1
}`)
assertStatus(t, w, http.StatusBadRequest)
assertBodyContains(t, w, "limit must be non-negative")
}
func TestListRecallEntriesInvalidLimit(t *testing.T) {
te := setup(t)
w := te.get(t, "/api/v1/recall/entries?limit=bad")
assertStatus(t, w, http.StatusBadRequest)
assertBodyContains(t, w, "invalid limit")
}
func TestListRecallEntriesRejectsNegativeLimit(t *testing.T) {
te := setup(t)
w := te.get(t, "/api/v1/recall/entries?limit=-1")
assertStatus(t, w, http.StatusBadRequest)
assertBodyContains(t, w, "limit must be non-negative")
}
func TestListRecallEntriesRejectsInvalidTrustedOnly(t *testing.T) {
te := setup(t)
w := te.get(t, "/api/v1/recall/entries?trusted_only=yes")
assertStatus(t, w, http.StatusBadRequest)
assertBodyContains(t, w, "invalid trusted_only parameter")
}
func TestImportRecallEntriesJSONL(t *testing.T) {
te := setup(t)
seedRecallEntrySession(t, te)
seedRecallImportEvidence(t, te)
w := te.post(t, "/api/v1/recall/import", `
{"candidate_id":"m1","type":"debugging_method","scope":"repository","title":"Check cwd before file reads","body":"Verify cwd before retrying failed reads.","project":"agentsview","agent":"codex","session_id":"recall-session","label":"correct","transferable":true,"provenance_ok":true,"evidence":{"ordinal_start":3,"ordinal_end":7,"tool_use_ids":["toolu_1"]}}
`)
assertStatus(t, w, http.StatusOK)
got := decode[db.RecallImportResult](t, w)
assert.Equal(t, 1, got.Imported)
w = te.get(t, "/api/v1/recall/entries/m1")
assertStatus(t, w, http.StatusOK)
recall := decode[db.RecallEntry](t, w)
assert.Equal(t, "Check cwd before file reads", recall.Title)
}
func TestImportRecallEntriesRefusesDefaultDataDirWithoutOverride(t *testing.T) {
home := t.TempDir()
defaultDataDir := filepath.Join(home, ".agentsview")
te := setup(t, func(c *config.Config) {
c.DataDir = defaultDataDir
c.DBPath = filepath.Join(defaultDataDir, "test.db")
})
seedRecallEntrySession(t, te)
seedRecallImportEvidence(t, te)
w := te.post(t, "/api/v1/recall/import", `
{"candidate_id":"m-default-import","type":"debugging_method","scope":"repository","title":"Check cwd before file reads","body":"Verify cwd before retrying failed reads.","project":"agentsview","agent":"codex","session_id":"recall-session","label":"correct","transferable":true,"provenance_ok":true,"evidence":{"ordinal_start":3,"ordinal_end":7,"tool_use_ids":["toolu_1"]}}
`)
assertStatus(t, w, http.StatusForbidden)
assertBodyContains(t, w, "default agentsview data directory")
assertBodyContains(t, w, "allow_production_import")
w = te.get(t, "/api/v1/recall/entries/m-default-import")
assertStatus(t, w, http.StatusNotFound)
}
func TestImportRecallEntriesDryRunRefusesDefaultDataDirWithoutOverride(t *testing.T) {
home := t.TempDir()
defaultDataDir := filepath.Join(home, ".agentsview")
te := setup(t, func(c *config.Config) {
c.DataDir = defaultDataDir
c.DBPath = filepath.Join(defaultDataDir, "test.db")
})
seedRecallEntrySession(t, te)
seedRecallImportEvidence(t, te)
w := te.post(t, "/api/v1/recall/import?dry_run=true", `
{"candidate_id":"m-default-import","type":"debugging_method","scope":"repository","title":"Check cwd before file reads","body":"Verify cwd before retrying failed reads.","project":"agentsview","agent":"codex","session_id":"recall-session","label":"correct","transferable":true,"provenance_ok":true,"evidence":{"ordinal_start":3,"ordinal_end":7,"tool_use_ids":["toolu_1"]}}
`)
assertStatus(t, w, http.StatusForbidden)
assertBodyContains(t, w, "default agentsview data directory")
assertBodyContains(t, w, "allow_production_import")
}
func TestImportRecallEntriesRefusesSymlinkedDefaultDataDirWithoutOverride(t *testing.T) {
home := t.TempDir()
defaultDataDir := filepath.Join(home, ".agentsview")
require.NoError(t, os.MkdirAll(defaultDataDir, 0o700))
link := filepath.Join(t.TempDir(), "recall-lab-data")
require.NoError(t, os.Symlink(defaultDataDir, link))
te := setup(t, func(c *config.Config) {
c.DataDir = link
c.DBPath = filepath.Join(link, "test.db")
})
seedRecallEntrySession(t, te)
seedRecallImportEvidence(t, te)
w := te.post(t, "/api/v1/recall/import", `
{"candidate_id":"m-symlink-import","type":"debugging_method","scope":"repository","title":"Check cwd before file reads","body":"Verify cwd before retrying failed reads.","project":"agentsview","agent":"codex","session_id":"recall-session","label":"correct","transferable":true,"provenance_ok":true,"evidence":{"ordinal_start":3,"ordinal_end":7,"tool_use_ids":["toolu_1"]}}
`)
assertStatus(t, w, http.StatusForbidden)
assertBodyContains(t, w, "default agentsview data directory")
assertBodyContains(t, w, "allow_production_import")
}
func TestImportRecallEntriesRefusesDefaultDBPathWithoutOverride(t *testing.T) {
home := t.TempDir()
defaultDB := filepath.Join(home, ".agentsview", "sessions.db")
// DataDir is an ordinary lab directory, but DBPath points into the
// production ~/.agentsview archive. The server must consult the DB path,
// not just DataDir, and still refuse.
labDataDir := filepath.Join(t.TempDir(), "recall-lab-data")
te := setup(t, func(c *config.Config) {
c.DataDir = labDataDir
c.DBPath = defaultDB
})
seedRecallEntrySession(t, te)
seedRecallImportEvidence(t, te)
w := te.post(t, "/api/v1/recall/import", `
{"candidate_id":"m-default-dbpath-import","type":"debugging_method","scope":"repository","title":"Check cwd before file reads","body":"Verify cwd before retrying failed reads.","project":"agentsview","agent":"codex","session_id":"recall-session","label":"correct","transferable":true,"provenance_ok":true,"evidence":{"ordinal_start":3,"ordinal_end":7,"tool_use_ids":["toolu_1"]}}
`)
assertStatus(t, w, http.StatusForbidden)
assertBodyContains(t, w, "default agentsview data directory")
assertBodyContains(t, w, "allow_production_import")
}
func TestImportRecallEntriesRejectsInvalidDryRunBeforeMutation(t *testing.T) {
te := setup(t)
seedRecallEntrySession(t, te)
seedRecallImportEvidence(t, te)
w := te.post(t, "/api/v1/recall/import?dry_run=yes", `
{"candidate_id":"m-invalid-dry-run","type":"debugging_method","scope":"repository","title":"Check cwd before file reads","body":"Verify cwd before retrying failed reads.","project":"agentsview","agent":"codex","session_id":"recall-session","label":"correct","transferable":true,"provenance_ok":true,"evidence":{"ordinal_start":3,"ordinal_end":7,"tool_use_ids":["toolu_1"]}}
`)
assertStatus(t, w, http.StatusBadRequest)
assertBodyContains(t, w, "invalid dry_run parameter")
got := te.get(t, "/api/v1/recall/entries/m-invalid-dry-run")
assertStatus(t, got, http.StatusNotFound)
}
func TestImportRecallEntriesAllowsDefaultDataDirWithOverride(t *testing.T) {
home := t.TempDir()
defaultDataDir := filepath.Join(home, ".agentsview")
te := setup(t, func(c *config.Config) {
c.DataDir = defaultDataDir
c.DBPath = filepath.Join(defaultDataDir, "test.db")
})
seedRecallEntrySession(t, te)
seedRecallImportEvidence(t, te)
w := te.post(t, "/api/v1/recall/import?allow_production_import=true", `
{"candidate_id":"m-default-import","type":"debugging_method","scope":"repository","title":"Check cwd before file reads","body":"Verify cwd before retrying failed reads.","project":"agentsview","agent":"codex","session_id":"recall-session","label":"correct","transferable":true,"provenance_ok":true,"evidence":{"ordinal_start":3,"ordinal_end":7,"tool_use_ids":["toolu_1"]}}
`)
assertStatus(t, w, http.StatusOK)
got := decode[db.RecallImportResult](t, w)
assert.Equal(t, 1, got.Imported)
}
func TestImportRecallEntriesRejectsNumericProductionOverride(t *testing.T) {
home := t.TempDir()
defaultDataDir := filepath.Join(home, ".agentsview")
te := setup(t, func(c *config.Config) {
c.DataDir = defaultDataDir
c.DBPath = filepath.Join(defaultDataDir, "test.db")
})
seedRecallEntrySession(t, te)
seedRecallImportEvidence(t, te)
w := te.post(t, "/api/v1/recall/import?allow_production_import=1", `
{"candidate_id":"m-numeric-override","type":"debugging_method","scope":"repository","title":"Check cwd before file reads","body":"Verify cwd before retrying failed reads.","project":"agentsview","agent":"codex","session_id":"recall-session","label":"correct","transferable":true,"provenance_ok":true,"evidence":{"ordinal_start":3,"ordinal_end":7,"tool_use_ids":["toolu_1"]}}
`)
assertStatus(t, w, http.StatusBadRequest)
assertBodyContains(t, w, "invalid allow_production_import parameter")
got := te.get(t, "/api/v1/recall/entries/m-numeric-override")
assertStatus(t, got, http.StatusNotFound)
}
func TestImportRecallEntriesRequiresExistingEvidenceByDefault(t *testing.T) {
te := setup(t)
w := te.post(t, "/api/v1/recall/import", `
{"candidate_id":"m-missing-session","type":"debugging_method","scope":"repository","title":"Check cwd before file reads","body":"Verify cwd before retrying failed reads.","project":"agentsview","agent":"codex","session_id":"s-not-imported","label":"correct","transferable":true,"provenance_ok":true,"evidence":{"ordinal_start":3,"ordinal_end":7}}
`)
assertStatus(t, w, http.StatusBadRequest)
assertBodyContains(t, w, "source session s-not-imported not found")
w = te.get(t, "/api/v1/recall/entries/m-missing-session")
assertStatus(t, w, http.StatusNotFound)
}
func TestImportRecallEntriesAllowsPlaceholderSessionsWhenExplicit(t *testing.T) {
te := setup(t)
w := te.post(t, "/api/v1/recall/import?allow_placeholder_sessions=true", `
{"candidate_id":"m-placeholder","type":"debugging_method","scope":"repository","title":"Check cwd before file reads","body":"Verify cwd before retrying failed reads.","project":"agentsview","agent":"codex","session_id":"s-placeholder","label":"correct","transferable":true,"provenance_ok":true,"evidence":{"ordinal_start":3,"ordinal_end":7}}
`)
assertStatus(t, w, http.StatusOK)
got := decode[db.RecallImportResult](t, w)
assert.Equal(t, 1, got.Imported)
w = te.get(t, "/api/v1/recall/entries/m-placeholder")
assertStatus(t, w, http.StatusOK)
recall := decode[db.RecallEntry](t, w)
assert.Equal(t, "s-placeholder", recall.SourceSessionID)
}
func seedRecallEntrySession(t *testing.T, te *testEnv) {
t.Helper()
te.seedSession(t, "recall-session", "agentsview", 3, func(s *db.Session) {
s.Agent = "codex"
s.Cwd = "/repo/agentsview"
s.GitBranch = "main"
})
te.seedSession(t, "other-session", "other", 3, func(s *db.Session) {
s.Agent = "codex"
})
}
func seedRecallImportEvidence(t *testing.T, te *testEnv) {
t.Helper()
messages := []db.Message{
dbtest.UserMsg("recall-session", 3, "File reads failed from the wrong cwd."),
dbtest.AsstMsg("recall-session", 4, "I will inspect cwd."),
dbtest.UserMsg("recall-session", 5, "Retry failed."),
dbtest.AsstMsg("recall-session", 6, "[Read: main.go]"),
dbtest.UserMsg("recall-session", 7, "That fixed it."),
}
messages[3].HasToolUse = true
messages[3].ToolCalls = []db.ToolCall{
{
SessionID: "recall-session",
ToolName: "Read",
Category: "Read",
ToolUseID: "toolu_1",
},
}
dbtest.SeedMessages(t, te.db, messages...)
}
func seedRecallEntry(t *testing.T, te *testEnv, m db.RecallEntry) {
t.Helper()
if m.Type == "" {
m.Type = "procedure"
}
if m.Scope == "" {
m.Scope = "project"
}
if m.Status == "" {
m.Status = "accepted"
}
_, err := te.db.InsertRecallEntry(m)
require.NoError(t, err, "InsertRecallEntry")
}