Add conversation pkg

This commit is contained in:
dwrz
2026-10-23 23:41:35 +00:00
parent ad493d5aec
commit 2426509531
9 changed files with 4751 additions and 42 deletions
+156 -18
View File
@@ -8,6 +8,7 @@ import (
"log/slog"
"reflect"
"testing"
"time"
"uuid"
"github.com/mattn/go-sqlite3"
@@ -33,6 +34,9 @@ func TestQueries(t *testing.T) {
t.Run("Messaging", func(t *testing.T) {
testMessaging(t, ctx, db)
})
t.Run("SequenceShift", func(t *testing.T) {
testSequenceShift(t, ctx, db)
})
t.Run("ConversationTimestamp", func(t *testing.T) {
testConversationTimestamp(t, ctx, db)
})
@@ -160,7 +164,7 @@ func testMessaging(t *testing.T, ctx context.Context, db *SQLite) {
ConversationId: c.Id,
Sequence: seq,
Role: MessageRoleSystem,
Content: `{"role":"system","content":"You are helpful"}`,
Content: jsontext.Value(`{"role":"system","content":"You are helpful"}`),
})
if err != nil {
t.Fatalf("InsertMessage: %v", err)
@@ -175,7 +179,7 @@ func testMessaging(t *testing.T, ctx context.Context, db *SQLite) {
ConversationId: c.Id,
Sequence: seq,
Role: MessageRoleUser,
Content: `{"role":"user","content":"Hello"}`,
Content: jsontext.Value(`{"role":"user","content":"Hello"}`),
})
if err != nil {
t.Fatalf("InsertMessage: %v", err)
@@ -192,7 +196,7 @@ func testMessaging(t *testing.T, ctx context.Context, db *SQLite) {
Sequence: seq,
Role: MessageRoleAssistant,
Metadata: jsontext.Value(metadata),
Content: `{"role":"assistant","content":null,"tool_calls":[{"id":"call_abc","type":"function","function":{"name":"get_weather","arguments":"{\"location\":\"NYC\"}"}}]}`,
Content: jsontext.Value(`{"role":"assistant","content":null,"tool_calls":[{"id":"call_abc","type":"function","function":{"name":"get_weather","arguments":"{\"location\":\"NYC\"}"}}]}`),
})
if err != nil {
t.Fatalf("InsertMessage: %v", err)
@@ -207,7 +211,7 @@ func testMessaging(t *testing.T, ctx context.Context, db *SQLite) {
ConversationId: c.Id,
Sequence: seq,
Role: MessageRoleTool,
Content: `{"role":"tool","content":"{\"temperature\":72}","tool_call_id":"call_abc"}`,
Content: jsontext.Value(`{"role":"tool","content":"{\"temperature\":72}","tool_call_id":"call_abc"}`),
})
if err != nil {
t.Fatalf("InsertMessage: %v", err)
@@ -238,17 +242,17 @@ func testMessaging(t *testing.T, ctx context.Context, db *SQLite) {
t.Errorf("message %d metadata = %v, want nil", i, message.Metadata)
}
}
if err := qw.UpdateMessageContent(ctx, UpdateMessageContentParams{
if _, err := qw.UpdateMessageContent(ctx, UpdateMessageContentParams{
Id: messages[2].Id,
Content: `{"role":"assistant","content":"edited"}`,
Content: jsontext.Value(`{"role":"assistant","content":"edited"}`),
}); err != nil {
t.Fatalf("UpdateMessageContent: %v", err)
}
// Test UpdateMessageContent.
if err := qw.UpdateMessageContent(ctx, UpdateMessageContentParams{
if _, err := qw.UpdateMessageContent(ctx, UpdateMessageContentParams{
Id: toolMsg.Id,
Content: `{"role":"tool","content":"{\"temperature\":75}","tool_call_id":"call_abc"}`,
Content: jsontext.Value(`{"role":"tool","content":"{\"temperature\":75}","tool_call_id":"call_abc"}`),
}); err != nil {
t.Fatalf("UpdateMessageContent: %v", err)
}
@@ -260,7 +264,7 @@ func testMessaging(t *testing.T, ctx context.Context, db *SQLite) {
if updated[2].Metadata == nil || string(updated[2].Metadata) != metadata {
t.Errorf("edited assistant metadata = %v", updated[2].Metadata)
}
if updated[3].Content != `{"role":"tool","content":"{\"temperature\":75}","tool_call_id":"call_abc"}` {
if string(updated[3].Content) != `{"role":"tool","content":"{\"temperature\":75}","tool_call_id":"call_abc"}` {
t.Errorf("updated content mismatch: %s", updated[3].Content)
}
for _, tt := range []struct {
@@ -283,7 +287,7 @@ func testMessaging(t *testing.T, ctx context.Context, db *SQLite) {
t.Fatal(err)
}
inserted, err := qw.InsertMessage(ctx, InsertMessageParams{
Content: `{"role":"assistant","content":"answer"}`,
Content: jsontext.Value(`{"role":"assistant","content":"answer"}`),
ConversationId: c.Id,
Metadata: tt.metadata,
Role: MessageRoleAssistant,
@@ -361,7 +365,7 @@ func testMessaging(t *testing.T, ctx context.Context, db *SQLite) {
t.Fatal(err)
}
inserted, err := qw.InsertMessage(ctx, InsertMessageParams{
Content: `{"role":"assistant","content":"answer"}`,
Content: jsontext.Value(`{"role":"assistant","content":"answer"}`),
ConversationId: c.Id,
Id: tt.id,
Role: MessageRoleAssistant,
@@ -397,7 +401,7 @@ func testMessaging(t *testing.T, ctx context.Context, db *SQLite) {
t.Fatal(err)
}
_, err = New(tx).InsertMessage(ctx, InsertMessageParams{
Content: `{"role":"assistant","content":"rolled back"}`,
Content: jsontext.Value(`{"role":"assistant","content":"rolled back"}`),
ConversationId: c.Id,
Metadata: jsontext.Value(metadata),
Role: MessageRoleAssistant,
@@ -416,6 +420,140 @@ func testMessaging(t *testing.T, ctx context.Context, db *SQLite) {
}
}
func testSequenceShift(t *testing.T, ctx context.Context, db *SQLite) {
var (
qr = New(db.Reader)
qw = New(db.Writer)
)
u, err := qw.InsertUser(ctx, InsertUserParams{
Name: "sequence-shift",
Password: "test",
Role: UserRoleOwner,
})
if err != nil {
t.Fatalf("InsertUser: %v", err)
}
c, err := qw.InsertConversation(ctx, InsertConversationParams{
UserId: u.Id,
})
if err != nil {
t.Fatalf("InsertConversation: %v", err)
}
insert := func(text string) {
t.Helper()
seq, err := qw.NextSequence(ctx, c.Id)
if err != nil {
t.Fatalf("NextSequence: %v", err)
}
if _, err := qw.InsertMessage(ctx, InsertMessageParams{
ConversationId: c.Id,
Sequence: seq,
Role: MessageRoleUser,
Content: jsontext.Value(`{"role":"user","content":"` + text + `"}`),
}); err != nil {
t.Fatalf("InsertMessage: %v", err)
}
}
for _, text := range []string{"one", "two", "three", "four"} {
insert(text)
}
// updated_at has millisecond precision, so sleep to put the
// shift in a later millisecond than the inserts.
time.Sleep(10 * time.Millisecond)
before, err := qr.SelectMessagesConversationId(ctx, c.Id)
if err != nil {
t.Fatalf("SelectMessagesConversationId: %v", err)
}
// Deleting "two" and shifting closes the gap: "three" moves to
// 2 and "four" to 3, while "one" stays put.
if err := qw.DeleteMessageId(ctx, before[1].Id); err != nil {
t.Fatalf("DeleteMessageId: %v", err)
}
if err := qw.UpdateMessagesSequence(ctx, c.Id, before[1].Sequence); err != nil {
t.Fatalf("UpdateMessagesSequence: %v", err)
}
moved, err := qr.SelectMessagesConversationId(ctx, c.Id)
if err != nil {
t.Fatalf("SelectMessagesConversationId: %v", err)
}
if len(moved) != 3 {
t.Fatalf("expected 3 messages, got %d", len(moved))
}
if moved[0].Sequence != 1 {
t.Errorf("unshifted row sequence = %d, want 1", moved[0].Sequence)
}
if !moved[0].UpdatedAt.Equal(before[0].UpdatedAt) {
t.Errorf("unshifted row updated_at = %s, want %s",
moved[0].UpdatedAt, before[0].UpdatedAt)
}
for i, wantSeq := range []int{2, 3} {
row, prev := moved[i+1], before[i+2]
if row.Id != prev.Id {
t.Fatalf("row %d id = %s, want %s", i+1, row.Id, prev.Id)
}
if row.Sequence != wantSeq {
t.Errorf("row %d sequence = %d, want %d",
i+1, row.Sequence, wantSeq)
}
if !row.UpdatedAt.After(prev.UpdatedAt) {
t.Errorf("row %d updated_at = %s, want later than %s",
i+1, row.UpdatedAt, prev.UpdatedAt)
}
}
// Free sequence 2 by incrementing rows 2 and 3, inside a
// transaction as the doc requires.
time.Sleep(10 * time.Millisecond)
tx, err := db.Writer.BeginTx(ctx, nil)
if err != nil {
t.Fatalf("BeginTx: %v", err)
}
if err := New(tx).UpdateMessagesSequenceIncrement(ctx, c.Id, 2); err != nil {
tx.Rollback()
t.Fatalf("UpdateMessagesSequenceIncrement: %v", err)
}
if err := tx.Commit(); err != nil {
t.Fatalf("Commit: %v", err)
}
after, err := qr.SelectMessagesConversationId(ctx, c.Id)
if err != nil {
t.Fatalf("SelectMessagesConversationId: %v", err)
}
if len(after) != 3 {
t.Fatalf("expected 3 messages, got %d", len(after))
}
if after[0].Sequence != 1 {
t.Errorf("unshifted row sequence = %d, want 1", after[0].Sequence)
}
if !after[0].UpdatedAt.Equal(before[0].UpdatedAt) {
t.Errorf("unshifted row updated_at = %s, want %s",
after[0].UpdatedAt, before[0].UpdatedAt)
}
for i, wantSeq := range []int{3, 4} {
row, prev := after[i+1], moved[i+1]
if row.Id != prev.Id {
t.Fatalf("row %d id = %s, want %s", i+1, row.Id, prev.Id)
}
if row.Sequence != wantSeq {
t.Errorf("row %d sequence = %d, want %d",
i+1, row.Sequence, wantSeq)
}
if !row.UpdatedAt.After(prev.UpdatedAt) {
t.Errorf("row %d updated_at = %s, want later than %s",
i+1, row.UpdatedAt, prev.UpdatedAt)
}
}
}
func testConversationTimestamp(t *testing.T, ctx context.Context, database *SQLite) {
var (
qr = New(database.Reader)
@@ -448,7 +586,7 @@ func testConversationTimestamp(t *testing.T, ctx context.Context, database *SQLi
ConversationId: c.Id,
Sequence: 0,
Role: MessageRoleUser,
Content: `{"role":"user","content":"Hello"}`,
Content: jsontext.Value(`{"role":"user","content":"Hello"}`),
})
if err != nil {
t.Fatalf("InsertMessage: %v", err)
@@ -469,9 +607,9 @@ func testConversationTimestamp(t *testing.T, ctx context.Context, database *SQLi
); err != nil {
t.Fatalf("set conversation timestamp before update: %v", err)
}
if err := qw.UpdateMessageContent(ctx, UpdateMessageContentParams{
if _, err := qw.UpdateMessageContent(ctx, UpdateMessageContentParams{
Id: msg.Id,
Content: `{"role":"user","content":"Updated"}`,
Content: jsontext.Value(`{"role":"user","content":"Updated"}`),
}); err != nil {
t.Fatalf("UpdateMessageContent: %v", err)
}
@@ -529,7 +667,7 @@ func testDelete(t *testing.T, ctx context.Context, db *SQLite) {
ConversationId: c.Id,
Sequence: 1,
Role: MessageRoleUser,
Content: `{"role":"user","content":"Hello"}`,
Content: jsontext.Value(`{"role":"user","content":"Hello"}`),
})
if err != nil {
t.Fatalf("InsertMessage: %v", err)
@@ -585,7 +723,7 @@ func testExecutions(t *testing.T, ctx context.Context, db *SQLite) {
ConversationId: c.Id,
Sequence: 1,
Role: MessageRoleAssistant,
Content: `{"role":"assistant","content":null,"tool_calls":[{"id":"call_abc","type":"function","function":{"name":"host_run","arguments":"{\"command\":\"rm -rf /\"}"}}]}`,
Content: jsontext.Value(`{"role":"assistant","content":null,"tool_calls":[{"id":"call_abc","type":"function","function":{"name":"host_run","arguments":"{\"command\":\"rm -rf /\"}"}}]}`),
})
if err != nil {
t.Fatalf("InsertMessage: %v", err)
@@ -721,7 +859,7 @@ func testExecutions(t *testing.T, ctx context.Context, db *SQLite) {
ConversationId: c.Id,
Sequence: 2,
Role: MessageRoleAssistant,
Content: `{"role":"assistant","content":"second"}`,
Content: jsontext.Value(`{"role":"assistant","content":"second"}`),
})
if err != nil {
t.Fatalf("InsertMessage second: %v", err)