Add conversation pkg
This commit is contained in:
+156
-18
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user