1b4448afb4
teabot watches Gitea repos and dispatches one-shot Claude Code sessions in Docker containers to work issues and review PRs, acting as configurable bot personalities. Claude-Session: https://claude.ai/code/session_015ur3i7D2azsMAWTSVABApv
123 lines
2.8 KiB
Go
123 lines
2.8 KiB
Go
package state
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestMarkAndQuery(t *testing.T) {
|
|
s, err := New(t.TempDir())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
const repo = "unkin/teabot"
|
|
|
|
if s.IssueProcessed(repo, 1) {
|
|
t.Error("fresh store should not report issue 1 processed")
|
|
}
|
|
s.MarkIssue(repo, 1)
|
|
if !s.IssueProcessed(repo, 1) {
|
|
t.Error("issue 1 should be processed after MarkIssue")
|
|
}
|
|
if !s.ActedOnIssue(repo, 1) {
|
|
t.Error("MarkIssue should also record acted-on")
|
|
}
|
|
if s.ActedOnPull(repo, 1) {
|
|
t.Error("issue mark must not set acted-on-pull")
|
|
}
|
|
|
|
s.MarkPull(repo, 2)
|
|
if !s.PullProcessed(repo, 2) || !s.ActedOnPull(repo, 2) {
|
|
t.Error("pull 2 should be processed and acted-on")
|
|
}
|
|
|
|
s.MarkComment(repo, 99)
|
|
if !s.CommentProcessed(repo, 99) {
|
|
t.Error("comment 99 should be processed")
|
|
}
|
|
if s.CommentProcessed(repo, 100) {
|
|
t.Error("comment 100 was never marked")
|
|
}
|
|
}
|
|
|
|
func TestPersistenceRoundTrip(t *testing.T) {
|
|
dir := t.TempDir()
|
|
s1, err := New(dir)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
const repo = "a/b"
|
|
s1.MarkIssue(repo, 10)
|
|
s1.MarkPull(repo, 11)
|
|
s1.MarkComment(repo, 12)
|
|
s1.MarkSeeded(repo)
|
|
now := time.Now().Truncate(time.Second)
|
|
s1.SetLastPoll(repo, now)
|
|
if err := s1.Save(); err != nil {
|
|
t.Fatalf("Save: %v", err)
|
|
}
|
|
|
|
// A fresh Store loaded from the same dir must see the persisted state.
|
|
s2, err := New(dir)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !s2.IssueProcessed(repo, 10) || !s2.PullProcessed(repo, 11) || !s2.CommentProcessed(repo, 12) {
|
|
t.Error("processed sets did not survive reload")
|
|
}
|
|
if !s2.ActedOnIssue(repo, 10) || !s2.ActedOnPull(repo, 11) {
|
|
t.Error("acted-on sets did not survive reload")
|
|
}
|
|
if !s2.Seeded(repo) {
|
|
t.Error("seeded flag did not survive reload")
|
|
}
|
|
if !s2.LastPoll(repo).Equal(now) {
|
|
t.Errorf("LastPoll = %v, want %v", s2.LastPoll(repo), now)
|
|
}
|
|
}
|
|
|
|
func TestSaveIsAtomicFile(t *testing.T) {
|
|
dir := t.TempDir()
|
|
s, err := New(dir)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
s.MarkIssue("a/b", 1)
|
|
if err := s.Save(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := os.Stat(filepath.Join(dir, StateFileName)); err != nil {
|
|
t.Errorf("state file missing after Save: %v", err)
|
|
}
|
|
// No leftover temp file.
|
|
if _, err := os.Stat(filepath.Join(dir, StateFileName+".tmp")); !os.IsNotExist(err) {
|
|
t.Error("temp file should not remain after atomic rename")
|
|
}
|
|
}
|
|
|
|
func TestSeededIndependentPerRepo(t *testing.T) {
|
|
s, err := New(t.TempDir())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
s.MarkSeeded("a/b")
|
|
if s.Seeded("c/d") {
|
|
t.Error("seeding a/b must not seed c/d")
|
|
}
|
|
if !s.Seeded("a/b") {
|
|
t.Error("a/b should be seeded")
|
|
}
|
|
}
|
|
|
|
func TestLoadCorruptStateFails(t *testing.T) {
|
|
dir := t.TempDir()
|
|
if err := os.WriteFile(filepath.Join(dir, StateFileName), []byte("{not json"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := New(dir); err == nil {
|
|
t.Error("expected error loading corrupt state file")
|
|
}
|
|
}
|