Files
weichenandCursor 7f8331f893 fix(docrevision): harden empty docID, per-doc auth, and failed-save honesty
Stage generation without a real document must not panic or enter the ledger.
Unknown orphan IDs are denied, write paths check author/lease/lifecycle, PATCH
no longer returns a fake 200, and overlay matching survives client/server block id drift.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-13 09:02:54 +00:00

87 lines
2.9 KiB
Go

package docrevision_test
import (
"context"
"testing"
"chat-agent-system/internal/db"
"chat-agent-system/internal/docrevision"
"github.com/jackc/pgx/v5/pgxpool"
)
func TestPGPersistSurvivesNewServiceInstance(t *testing.T) {
dsn := db.SkipIfNoTestDatabaseURL(t)
ctx := context.Background()
pool, err := pgxpool.New(ctx, dsn)
if err != nil {
t.Fatal(err)
}
defer pool.Close()
// Best-effort: skip if schema 00028 tables missing.
var n int
if err := pool.QueryRow(ctx, `SELECT COUNT(*) FROM information_schema.tables WHERE table_name='document_versions'`).Scan(&n); err != nil || n == 0 {
t.Skip("document_versions table missing; migrate 00028 required")
}
docID := "doc_pg_persist_test"
_, _ = pool.Exec(ctx, `DELETE FROM document_change_events WHERE doc_id=$1`, docID)
_, _ = pool.Exec(ctx, `DELETE FROM document_agent_candidates WHERE doc_id=$1`, docID)
_, _ = pool.Exec(ctx, `DELETE FROM document_change_reports WHERE doc_id=$1`, docID)
_, _ = pool.Exec(ctx, `DELETE FROM document_versions WHERE doc_id=$1`, docID)
_, _ = pool.Exec(ctx, `DELETE FROM document_snapshots WHERE doc_id=$1`, docID)
svc1 := docrevision.NewService(pool)
st, err := svc1.OpenTemplateVersion(ctx, docID, "# persist\n\nhello\n", docrevision.ActorSnapshot{DisplayName: "t"}, 1, "ws/persist")
if err != nil {
t.Fatal(err)
}
_, err = svc1.CommitHumanRevision(ctx, docrevision.HumanRevisionRequest{
DocID: docID, Markdown: "# persist\n\nhello edited\n",
ExpectedHeadSnapshot: &st.HeadSnapshotID, ExpectedRevisionNo: &st.HeadRevisionNo,
IdempotencyKey: "pg-e1", ActorUserID: 1, LifecycleState: "drafting", WorkDir: "ws/persist", ProjectID: 1,
})
if err != nil {
t.Fatal(err)
}
// Simulate process restart: brand-new service, same pool.
svc2 := docrevision.NewService(pool)
got, err := svc2.GetRevisionState(ctx, docID)
if err != nil {
t.Fatalf("hydrate after restart: %v", err)
}
if got.HeadRevisionNo != 1 {
t.Fatalf("want revision 1 after restart, got %+v", got)
}
ov, err := svc2.ManualOverlay(ctx, docID)
if err != nil || ov == nil {
t.Fatalf("overlay after restart: %v", err)
}
st2, err := svc2.GetRevisionState(ctx, docID)
if err != nil {
t.Fatal(err)
}
cand, _, err := svc2.CreateAgentCandidate(ctx, docrevision.AgentCandidateRequest{
DocID: docID, RunID: "pg-cand-1", InputSnapshotID: st2.HeadSnapshotID,
OutputMarkdown: "# persist\n\nhello edited by agent\n",
ActorKind: docrevision.ActorAgent,
})
if err != nil || cand == nil {
t.Fatalf("create candidate: %v", err)
}
svc3 := docrevision.NewService(pool)
gotCand, err := svc3.GetCandidate(ctx, docID, cand.ID)
if err != nil || gotCand == nil || gotCand.DocID != docID {
t.Fatalf("hydrate candidate after restart: cand=%v err=%v", gotCand, err)
}
if _, err := svc3.CommitAgentVersion(ctx, docID, cand.ID, docrevision.CommitAgentDecision{
Decision: "reject", ActorUserID: 1,
}); err != nil {
t.Fatalf("decision after restart: %v", err)
}
}