Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 90 additions & 0 deletions internal/store/prompt_local_origin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,96 @@ package store

import "testing"

func TestPreviewPromptSource(t *testing.T) {
s := newTestStore(t)
if err := s.CreateSession("session", "alpha", "/work"); err != nil {
t.Fatal(err)
}
id, _, err := s.AddPromptWithResult(AddPromptParams{SessionID: "session", Project: "beta", SourceInboxID: "local", Content: "local"})
if err != nil {
t.Fatal(err)
}
var key string
if err := s.DB().QueryRow(`SELECT sync_id FROM user_prompts WHERE id=?`, id).Scan(&key); err != nil {
t.Fatal(err)
}
type previewCase struct {
name, key string
want PromptSourcePreview
found bool
}
cases := []previewCase{
{"verified local", key, PromptSourcePreview{"session", "local", "beta", key, "live"}, true},
{"missing", "missing", PromptSourcePreview{}, false},
{"near-match sync ID", key + "-other", PromptSourcePreview{}, false},
{"empty key", "", PromptSourcePreview{}, false},
}
check := func(tc previewCase) {
t.Helper()
t.Run(tc.name, func(t *testing.T) {
got, found, err := s.PreviewPromptSource(tc.key)
if err != nil || found != tc.found || got != tc.want {
t.Fatalf("preview %q: got %+v, found %v, err %v; want %+v, found %v", tc.key, got, found, err, tc.want, tc.found)
}
})
}
for _, tc := range cases {
check(tc)
}
if _, err := s.DB().Exec(`UPDATE user_prompts SET local_creation_session_id=NULL,local_creation_inbox_id=NULL,local_creation_project=NULL WHERE id=?`, id); err != nil {
t.Fatal(err)
}
check(previewCase{"unmarked local row", key, cases[0].want, true})

fixtures := []struct {
name, statement string
}{
{"old imported-style row", `INSERT INTO user_prompts(sync_id,session_id,source_inbox_id,project,content) VALUES ('old','session','old-key','beta','old')`},
{"tombstone", `INSERT INTO prompt_tombstones(sync_id,session_id,source_inbox_id,project) VALUES ('deleted','session','gone','beta')`},
{"duplicate live", `INSERT INTO user_prompts(sync_id,session_id,source_inbox_id,project,content) VALUES ('old','session','second','beta','duplicate')`},
{"live plus tombstone", `INSERT INTO user_prompts(sync_id,session_id,source_inbox_id,project,content) VALUES ('deleted','session','live','beta','conflict')`},
{"idless", `INSERT INTO user_prompts(sync_id,session_id,project,content) VALUES ('idless','session','beta','idless')`},
{"blank project", `INSERT INTO user_prompts(sync_id,session_id,source_inbox_id,project,content) VALUES ('blank-project','session','inbox','','blank')`},
{"blank session", `INSERT INTO prompt_tombstones(sync_id,session_id,source_inbox_id,project) VALUES ('blank-session','','inbox','beta')`},
{"whitespace-only identity", `INSERT INTO prompt_tombstones(sync_id,session_id,source_inbox_id,project) VALUES ('whitespace-identity',' ',' ',' ')`},
}
for i, fixture := range fixtures {
if _, err := s.DB().Exec(fixture.statement); err != nil {
t.Fatalf("%s: %v", fixture.name, err)
}
if i == 0 {
check(previewCase{fixture.name, "old", PromptSourcePreview{"session", "old-key", "beta", "old", "live"}, true})
}
if i == 1 {
check(previewCase{fixture.name, "deleted", PromptSourcePreview{"session", "gone", "beta", "deleted", "deleted"}, true})
}
}
for _, tc := range []previewCase{
{"duplicate live", "old", PromptSourcePreview{}, false},
{"live plus tombstone", "deleted", PromptSourcePreview{}, false},
{"idless", "idless", PromptSourcePreview{}, false},
{"blank project", "blank-project", PromptSourcePreview{}, false},
{"blank session", "blank-session", PromptSourcePreview{}, false},
{"whitespace-only identity", "whitespace-identity", PromptSourcePreview{}, false},
} {
check(tc)
}
var originCount int
if err := s.DB().QueryRow(`SELECT count(*) FROM user_prompts WHERE id=? AND local_creation_session_id IS NULL AND local_creation_inbox_id IS NULL AND local_creation_project IS NULL`, id).Scan(&originCount); err != nil || originCount != 1 {
t.Fatalf("origin changed: %d %v", originCount, err)
}
}

func TestPreviewPromptSourceDBError(t *testing.T) {
s := newTestStore(t) // isolated temp database; schema damage cannot affect another test.
if _, err := s.DB().Exec(`DROP TABLE prompt_tombstones`); err != nil {
t.Fatal(err)
}
if got, found, err := s.PreviewPromptSource("missing"); err == nil || found || got != (PromptSourcePreview{}) {
t.Fatalf("DB error: %+v %v %v", got, found, err)
}
}

func TestLocalPromptCreationIdentity(t *testing.T) {
s := newTestStore(t)
if err := s.CreateSession("session", "alpha", "/work"); err != nil {
Expand Down
44 changes: 44 additions & 0 deletions internal/store/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -3920,6 +3920,50 @@ func (s *Store) AddPromptWithResult(p AddPromptParams) (int64, bool, error) {
return promptID, inserted, nil
}

// PromptSourcePreview describes observed prompt data, not ownership or authority.
type PromptSourcePreview struct {
SessionID string
SourceInboxID string
Project string
SyncID string
Kind string // live or deleted
}

// PreviewPromptSource reads one exact sync ID from live prompts or tombstones.
// Ambiguous or incomplete observations return no preview; project is prompt data only.
func (s *Store) PreviewPromptSource(syncID string) (PromptSourcePreview, bool, error) {
if strings.TrimSpace(syncID) == "" {
return PromptSourcePreview{}, false, nil
}
rows, err := s.db.Query(`SELECT ifnull(session_id,''), ifnull(source_inbox_id,''), ifnull(project,''), ifnull(sync_id,''), 'live'
FROM user_prompts WHERE sync_id = ?
UNION ALL
SELECT ifnull(session_id,''), ifnull(source_inbox_id,''), ifnull(project,''), ifnull(sync_id,''), 'deleted'
FROM prompt_tombstones WHERE sync_id = ? LIMIT 2`, syncID, syncID)
if err != nil {
return PromptSourcePreview{}, false, err
}
defer func() { _ = rows.Close() }()
var preview PromptSourcePreview
if !rows.Next() {
return preview, false, rows.Err()
}
if err := rows.Scan(&preview.SessionID, &preview.SourceInboxID, &preview.Project, &preview.SyncID, &preview.Kind); err != nil {
return PromptSourcePreview{}, false, err
}
if rows.Next() {
return PromptSourcePreview{}, false, nil
}
if err := rows.Err(); err != nil {
return PromptSourcePreview{}, false, err
}
if strings.TrimSpace(preview.SessionID) == "" || strings.TrimSpace(preview.SourceInboxID) == "" ||
strings.TrimSpace(preview.Project) == "" || strings.TrimSpace(preview.SyncID) == "" {
return PromptSourcePreview{}, false, nil
}
return preview, true, nil
}

// LocalPromptCreationIdentity returns verified local creation identity by exact sync ID.
// A tombstone is eligible only when its separately recorded local origin matches.
func (s *Store) LocalPromptCreationIdentity(syncID string) (session, inbox, project string, eligible bool, err error) {
Expand Down
Loading