mirror of
https://github.com/HugeFrog24/go-telegram-bot.git
synced 2026-06-29 22:07:12 +00:00
204 lines
5.5 KiB
Go
204 lines
5.5 KiB
Go
package main
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
)
|
|
|
|
func TestFormatUploadFilename(t *testing.T) {
|
|
cases := []struct {
|
|
botID uint
|
|
chatID int64
|
|
tgMessageID int
|
|
ext string
|
|
want string
|
|
}{
|
|
{1, 12345, 42, "jpg", "tg-1-12345-42.jpg"},
|
|
// Negative chat IDs are how Telegram represents groups/channels —
|
|
// %d preserves the leading minus, no special handling needed.
|
|
{7, -1001234567890, 1, "png", "tg-7--1001234567890-1.png"},
|
|
{0, 0, 0, "webp", "tg-0-0-0.webp"},
|
|
}
|
|
for _, tc := range cases {
|
|
got := formatUploadFilename(tc.botID, tc.chatID, tc.tgMessageID, tc.ext)
|
|
assert.Equal(t, tc.want, got)
|
|
}
|
|
}
|
|
|
|
func TestParseMissingFileIDFromBody(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
body string
|
|
want string
|
|
}{
|
|
{
|
|
name: "canonical Anthropic file-not-found body",
|
|
body: `{"type":"error","error":{"type":"invalid_request_error","message":"File not found: file_011CNha8iCJcU1wXNR6q4V8w"}}`,
|
|
want: "file_011CNha8iCJcU1wXNR6q4V8w",
|
|
},
|
|
{
|
|
name: "trailing punctuation after the id is excluded",
|
|
body: `something File not found: file_abc123! more text`,
|
|
want: "file_abc123",
|
|
},
|
|
{
|
|
name: "body without the prefix yields empty",
|
|
body: `{"type":"error","error":{"message":"Model not found: claude-foo"}}`,
|
|
want: "",
|
|
},
|
|
{
|
|
name: "id at the very end of the buffer",
|
|
body: `File not found: file_xyz789`,
|
|
want: "file_xyz789",
|
|
},
|
|
{
|
|
name: "empty body",
|
|
body: "",
|
|
want: "",
|
|
},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
assert.Equal(t, tc.want, parseMissingFileIDFromBody(tc.body))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestStripDeadFileIDs(t *testing.T) {
|
|
dead := map[string]struct{}{
|
|
"file_a": {},
|
|
"file_b": {},
|
|
}
|
|
cases := []struct {
|
|
name string
|
|
input []string
|
|
wantSurvivors []string
|
|
wantDirty bool
|
|
}{
|
|
{
|
|
name: "no overlap returns input verbatim",
|
|
input: []string{"file_x", "file_y"},
|
|
wantSurvivors: []string{"file_x", "file_y"},
|
|
wantDirty: false,
|
|
},
|
|
{
|
|
name: "partial overlap returns survivors and reports dirty",
|
|
input: []string{"file_a", "file_x", "file_b", "file_y"},
|
|
wantSurvivors: []string{"file_x", "file_y"},
|
|
wantDirty: true,
|
|
},
|
|
{
|
|
name: "all dead returns empty survivors and dirty",
|
|
input: []string{"file_a", "file_b"},
|
|
wantSurvivors: []string{},
|
|
wantDirty: true,
|
|
},
|
|
{
|
|
name: "empty input is not dirty",
|
|
input: []string{},
|
|
wantSurvivors: []string{},
|
|
wantDirty: false,
|
|
},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
survivors, dirty := stripDeadFileIDs(tc.input, dead)
|
|
assert.Equal(t, tc.wantSurvivors, survivors)
|
|
assert.Equal(t, tc.wantDirty, dirty)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMarkFilesPendingCleanup(t *testing.T) {
|
|
b, _ := setupBotForTest(t, 123)
|
|
chatID := int64(555)
|
|
|
|
// Row 1: has dead file_a + alive file_x → should be updated with survivors.
|
|
row1 := Message{
|
|
BotID: b.botID,
|
|
ChatID: chatID,
|
|
UserID: 777,
|
|
Username: "u",
|
|
UserRole: "user",
|
|
Text: "look at these",
|
|
Timestamp: time.Now(),
|
|
IsUser: true,
|
|
ImageFileIDs: []string{"file_a", "file_x"},
|
|
}
|
|
assert.NoError(t, b.db.Create(&row1).Error)
|
|
|
|
// Row 2: only dead files → ImageFileIDs should become nil.
|
|
row2 := Message{
|
|
BotID: b.botID,
|
|
ChatID: chatID,
|
|
UserID: 777,
|
|
Username: "u",
|
|
UserRole: "user",
|
|
Text: "screenshot",
|
|
Timestamp: time.Now(),
|
|
IsUser: true,
|
|
ImageFileIDs: []string{"file_a", "file_b"},
|
|
}
|
|
assert.NoError(t, b.db.Create(&row2).Error)
|
|
|
|
// Row 3: no dead files → should be untouched.
|
|
row3 := Message{
|
|
BotID: b.botID,
|
|
ChatID: chatID,
|
|
UserID: 777,
|
|
Username: "u",
|
|
UserRole: "user",
|
|
Text: "another",
|
|
Timestamp: time.Now(),
|
|
IsUser: true,
|
|
ImageFileIDs: []string{"file_x", "file_y"},
|
|
}
|
|
assert.NoError(t, b.db.Create(&row3).Error)
|
|
|
|
// Row 4: different chat → must NOT be touched even if it references a dead file.
|
|
row4 := Message{
|
|
BotID: b.botID,
|
|
ChatID: 999,
|
|
UserID: 777,
|
|
Username: "u",
|
|
UserRole: "user",
|
|
Text: "other chat",
|
|
Timestamp: time.Now(),
|
|
IsUser: true,
|
|
ImageFileIDs: []string{"file_a"},
|
|
}
|
|
assert.NoError(t, b.db.Create(&row4).Error)
|
|
|
|
updated, err := b.markFilesPendingCleanup(t.Context(), chatID, []string{"file_a", "file_b"})
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, 2, updated, "rows 1 and 2 should have been updated")
|
|
|
|
// Row 1: only file_x should remain; FilesCleanedAt MUST stay nil because
|
|
// file_x is still alive on Anthropic and a future death of it must remain
|
|
// visible to the reconciliation job's `WHERE files_cleaned_at IS NULL` filter.
|
|
var r1 Message
|
|
assert.NoError(t, b.db.First(&r1, row1.ID).Error)
|
|
assert.Equal(t, []string{"file_x"}, r1.ImageFileIDs)
|
|
assert.Nil(t, r1.FilesCleanedAt)
|
|
|
|
// Row 2: all gone → ImageFileIDs nil/empty; FilesCleanedAt set.
|
|
var r2 Message
|
|
assert.NoError(t, b.db.First(&r2, row2.ID).Error)
|
|
assert.Empty(t, r2.ImageFileIDs)
|
|
assert.NotNil(t, r2.FilesCleanedAt)
|
|
|
|
// Row 3: untouched.
|
|
var r3 Message
|
|
assert.NoError(t, b.db.First(&r3, row3.ID).Error)
|
|
assert.Equal(t, []string{"file_x", "file_y"}, r3.ImageFileIDs)
|
|
assert.Nil(t, r3.FilesCleanedAt)
|
|
|
|
// Row 4: untouched despite referencing a dead file — scope is per-chat.
|
|
var r4 Message
|
|
assert.NoError(t, b.db.First(&r4, row4.ID).Error)
|
|
assert.Equal(t, []string{"file_a"}, r4.ImageFileIDs)
|
|
assert.Nil(t, r4.FilesCleanedAt)
|
|
}
|