Fix: done taskovi bez reporta sada prikazuju "Izveštaj" dugme. Dodato 10 novih testova za timestamp tracking i report prikaz. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
426 lines
10 KiB
Go
426 lines
10 KiB
Go
package server
|
|
|
|
import (
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestMoveTask_AddsTimestamp(t *testing.T) {
|
|
srv := setupTestServer(t)
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/task/T08/move?to=ready", nil)
|
|
w := httptest.NewRecorder()
|
|
srv.Router.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
|
|
// Read the moved file and check for timestamp
|
|
content, err := os.ReadFile(filepath.Join(srv.Config.TasksDir, "ready", "T08.md"))
|
|
if err != nil {
|
|
t.Fatalf("failed to read moved file: %v", err)
|
|
}
|
|
text := string(content)
|
|
if !containsStr(text, "## Vremena") {
|
|
t.Error("expected '## Vremena' section in task file")
|
|
}
|
|
if !containsStr(text, "Odobren (→ready)") {
|
|
t.Error("expected 'Odobren (→ready)' timestamp")
|
|
}
|
|
}
|
|
|
|
func TestMoveTask_AppendsMultipleTimestamps(t *testing.T) {
|
|
srv := setupTestServer(t)
|
|
|
|
// Move backlog → ready
|
|
req := httptest.NewRequest(http.MethodPost, "/task/T08/move?to=ready", nil)
|
|
w := httptest.NewRecorder()
|
|
srv.Router.ServeHTTP(w, req)
|
|
|
|
// Move ready → backlog (return)
|
|
req2 := httptest.NewRequest(http.MethodPost, "/task/T08/move?to=backlog", nil)
|
|
w2 := httptest.NewRecorder()
|
|
// Need to re-fetch since task is now in ready/
|
|
srv.Router.ServeHTTP(w2, req2)
|
|
|
|
// Move backlog → ready again
|
|
req3 := httptest.NewRequest(http.MethodPost, "/task/T08/move?to=ready", nil)
|
|
w3 := httptest.NewRecorder()
|
|
srv.Router.ServeHTTP(w3, req3)
|
|
|
|
content, _ := os.ReadFile(filepath.Join(srv.Config.TasksDir, "ready", "T08.md"))
|
|
text := string(content)
|
|
|
|
// Should have two "Odobren (→ready)" entries
|
|
count := strings.Count(text, "Odobren (→ready)")
|
|
if count != 2 {
|
|
t.Errorf("expected 2 'Odobren (→ready)' timestamps, got %d", count)
|
|
}
|
|
}
|
|
|
|
func TestAppendTimestamp_CreatesTable(t *testing.T) {
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "test.md")
|
|
os.WriteFile(path, []byte("# T01: Test\n\nSome content.\n"), 0644)
|
|
|
|
err := appendTimestamp(path, "Odobren (→ready)")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
content, _ := os.ReadFile(path)
|
|
text := string(content)
|
|
if !containsStr(text, "## Vremena") {
|
|
t.Error("expected Vremena section")
|
|
}
|
|
if !containsStr(text, "| Događaj | Vreme |") {
|
|
t.Error("expected table header")
|
|
}
|
|
if !containsStr(text, "Odobren (→ready)") {
|
|
t.Error("expected timestamp row")
|
|
}
|
|
}
|
|
|
|
func TestAppendTimestamp_AppendsToExisting(t *testing.T) {
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "test.md")
|
|
os.WriteFile(path, []byte("# T01: Test\n\n## Vremena\n\n| Događaj | Vreme |\n|---------|-------|\n| Kreiran | 2026-02-20 14:00 |\n"), 0644)
|
|
|
|
err := appendTimestamp(path, "Odobren (→ready)")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
content, _ := os.ReadFile(path)
|
|
text := string(content)
|
|
if !containsStr(text, "Kreiran") {
|
|
t.Error("expected existing row preserved")
|
|
}
|
|
if !containsStr(text, "Odobren (→ready)") {
|
|
t.Error("expected new timestamp row")
|
|
}
|
|
}
|
|
|
|
func TestRunTask_AddsTimestamp(t *testing.T) {
|
|
srv := setupTestServer(t)
|
|
|
|
// Move T08 to ready
|
|
os.Rename(
|
|
filepath.Join(srv.Config.TasksDir, "backlog", "T08.md"),
|
|
filepath.Join(srv.Config.TasksDir, "ready", "T08.md"),
|
|
)
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/task/T08/run", nil)
|
|
w := httptest.NewRecorder()
|
|
srv.Router.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
|
|
// Task should now be in active/
|
|
content, _ := os.ReadFile(filepath.Join(srv.Config.TasksDir, "active", "T08.md"))
|
|
text := string(content)
|
|
if !containsStr(text, "Pokrenut") {
|
|
t.Error("expected 'Pokrenut' timestamp after run")
|
|
}
|
|
// Verify it's no longer in ready/
|
|
if _, err := os.Stat(filepath.Join(srv.Config.TasksDir, "ready", "T08.md")); err == nil {
|
|
t.Error("task should no longer be in ready/")
|
|
}
|
|
}
|
|
|
|
func TestAppendTimestamp_Format(t *testing.T) {
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "test.md")
|
|
os.WriteFile(path, []byte("# T01: Test\n"), 0644)
|
|
|
|
appendTimestamp(path, "Pokrenut (→active)")
|
|
|
|
content, _ := os.ReadFile(path)
|
|
text := string(content)
|
|
|
|
// Verify timestamp format YYYY-MM-DD HH:MM
|
|
re := regexp.MustCompile(`\| Pokrenut \(→active\) \| \d{4}-\d{2}-\d{2} \d{2}:\d{2} \|`)
|
|
if !re.MatchString(text) {
|
|
t.Errorf("expected timestamp format YYYY-MM-DD HH:MM, got:\n%s", text)
|
|
}
|
|
}
|
|
|
|
func TestAppendTimestamp_FileNotFound(t *testing.T) {
|
|
err := appendTimestamp("/nonexistent/path/task.md", "test")
|
|
if err == nil {
|
|
t.Error("expected error for nonexistent file")
|
|
}
|
|
}
|
|
|
|
func TestMoveEventLabel_AllFolders(t *testing.T) {
|
|
expected := map[string]string{
|
|
"ready": "Odobren (→ready)",
|
|
"active": "Pokrenut (→active)",
|
|
"review": "Završen (→review)",
|
|
"done": "Odobren (→done)",
|
|
}
|
|
|
|
for folder, label := range expected {
|
|
got, ok := moveEventLabel[folder]
|
|
if !ok {
|
|
t.Errorf("missing label for folder %s", folder)
|
|
continue
|
|
}
|
|
if got != label {
|
|
t.Errorf("folder %s: expected %q, got %q", folder, label, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestDoneTimestamp_ReviewToDone(t *testing.T) {
|
|
srv := setupTestServer(t)
|
|
|
|
// Put T08 in review
|
|
os.Rename(
|
|
filepath.Join(srv.Config.TasksDir, "backlog", "T08.md"),
|
|
filepath.Join(srv.Config.TasksDir, "review", "T08.md"),
|
|
)
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/api/task/T08/move?to=done", nil)
|
|
w := httptest.NewRecorder()
|
|
srv.Router.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
|
|
content, _ := os.ReadFile(filepath.Join(srv.Config.TasksDir, "done", "T08.md"))
|
|
text := string(content)
|
|
|
|
if !containsStr(text, "Odobren (→done)") {
|
|
t.Error("expected 'Odobren (→done)' timestamp")
|
|
}
|
|
}
|
|
|
|
func TestTaskDetail_DoneShowsReportButton(t *testing.T) {
|
|
srv := setupTestServer(t)
|
|
|
|
// T01 is in done and has a report
|
|
req := httptest.NewRequest(http.MethodGet, "/task/T01", nil)
|
|
w := httptest.NewRecorder()
|
|
srv.Router.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d", w.Code)
|
|
}
|
|
|
|
body := w.Body.String()
|
|
if !containsStr(body, "/report/T01") {
|
|
t.Error("expected report link for done task")
|
|
}
|
|
}
|
|
|
|
func TestTaskDetail_DoneWithoutReportShowsButton(t *testing.T) {
|
|
srv := setupTestServer(t)
|
|
|
|
// Create a done task without a report
|
|
os.WriteFile(
|
|
filepath.Join(srv.Config.TasksDir, "done", "T02.md"),
|
|
[]byte("# T02: Bez reporta\n\n**Agent:** coder\n**Model:** Sonnet\n**Zavisi od:** —\n\n---\n\n## Opis\n\nTest task bez reporta.\n"),
|
|
0644,
|
|
)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/task/T02", nil)
|
|
w := httptest.NewRecorder()
|
|
srv.Router.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d", w.Code)
|
|
}
|
|
|
|
body := w.Body.String()
|
|
if !containsStr(body, "/report/T02") {
|
|
t.Error("expected report button for done task even without report file")
|
|
}
|
|
}
|
|
|
|
func TestTimestampVisibleInTaskDetail(t *testing.T) {
|
|
srv := setupTestServer(t)
|
|
|
|
// Add timestamps to T01 (done task)
|
|
taskPath := filepath.Join(srv.Config.TasksDir, "done", "T01.md")
|
|
appendTimestamp(taskPath, "Kreiran")
|
|
appendTimestamp(taskPath, "Pokrenut (→active)")
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/task/T01", nil)
|
|
w := httptest.NewRecorder()
|
|
srv.Router.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d", w.Code)
|
|
}
|
|
|
|
body := w.Body.String()
|
|
if !containsStr(body, "Vremena") {
|
|
t.Error("expected Vremena section visible in task detail")
|
|
}
|
|
}
|
|
|
|
func TestStripAnsi(t *testing.T) {
|
|
tests := []struct {
|
|
input string
|
|
expected string
|
|
}{
|
|
{"hello", "hello"},
|
|
{"\x1b[32mgreen\x1b[0m", "green"},
|
|
{"\x1b[1;31mbold red\x1b[0m", "bold red"},
|
|
{"\x1b[?25l\x1b[?25h", ""},
|
|
{"no \x1b[4munderline\x1b[24m here", "no underline here"},
|
|
{"\x1b]0;title\x07text", "text"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
got := stripAnsi(tt.input)
|
|
if got != tt.expected {
|
|
t.Errorf("stripAnsi(%q) = %q, want %q", tt.input, got, tt.expected)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestReadPTY_SplitsLines(t *testing.T) {
|
|
r, w, _ := os.Pipe()
|
|
|
|
var lines []string
|
|
done := make(chan bool)
|
|
|
|
go func() {
|
|
readPTY(r, func(line string) {
|
|
lines = append(lines, line)
|
|
})
|
|
done <- true
|
|
}()
|
|
|
|
w.WriteString("line1\nline2\nline3\n")
|
|
w.Close()
|
|
<-done
|
|
|
|
if len(lines) != 3 {
|
|
t.Fatalf("expected 3 lines, got %d: %v", len(lines), lines)
|
|
}
|
|
if lines[0] != "line1" || lines[1] != "line2" || lines[2] != "line3" {
|
|
t.Errorf("unexpected lines: %v", lines)
|
|
}
|
|
}
|
|
|
|
func TestReadPTY_StripsAnsi(t *testing.T) {
|
|
r, w, _ := os.Pipe()
|
|
|
|
var lines []string
|
|
done := make(chan bool)
|
|
|
|
go func() {
|
|
readPTY(r, func(line string) {
|
|
lines = append(lines, line)
|
|
})
|
|
done <- true
|
|
}()
|
|
|
|
w.WriteString("\x1b[32mcolored\x1b[0m\n")
|
|
w.Close()
|
|
<-done
|
|
|
|
if len(lines) != 1 {
|
|
t.Fatalf("expected 1 line, got %d", len(lines))
|
|
}
|
|
if lines[0] != "colored" {
|
|
t.Errorf("expected 'colored', got %q", lines[0])
|
|
}
|
|
}
|
|
|
|
func TestReadPTY_HandlesPartialChunks(t *testing.T) {
|
|
r, w, _ := os.Pipe()
|
|
|
|
var lines []string
|
|
done := make(chan bool)
|
|
|
|
go func() {
|
|
readPTY(r, func(line string) {
|
|
lines = append(lines, line)
|
|
})
|
|
done <- true
|
|
}()
|
|
|
|
// Write partial, then complete
|
|
w.WriteString("partial")
|
|
w.Close()
|
|
<-done
|
|
|
|
if len(lines) != 1 {
|
|
t.Fatalf("expected 1 line for partial, got %d: %v", len(lines), lines)
|
|
}
|
|
if lines[0] != "partial" {
|
|
t.Errorf("expected 'partial', got %q", lines[0])
|
|
}
|
|
}
|
|
|
|
func TestReadPTY_HandlesCarriageReturn(t *testing.T) {
|
|
r, w, _ := os.Pipe()
|
|
|
|
var lines []string
|
|
done := make(chan bool)
|
|
|
|
go func() {
|
|
readPTY(r, func(line string) {
|
|
lines = append(lines, line)
|
|
})
|
|
done <- true
|
|
}()
|
|
|
|
w.WriteString("line1\r\nline2\r\n")
|
|
w.Close()
|
|
<-done
|
|
|
|
if len(lines) != 2 {
|
|
t.Fatalf("expected 2 lines, got %d: %v", len(lines), lines)
|
|
}
|
|
if lines[0] != "line1" || lines[1] != "line2" {
|
|
t.Errorf("unexpected lines: %v", lines)
|
|
}
|
|
}
|
|
|
|
func TestRingBuffer_WriteAndRead(t *testing.T) {
|
|
rb := NewRingBuffer(16)
|
|
rb.Write([]byte("hello"))
|
|
|
|
got := rb.Bytes()
|
|
if string(got) != "hello" {
|
|
t.Errorf("expected 'hello', got '%s'", got)
|
|
}
|
|
}
|
|
|
|
func TestRingBuffer_Overflow(t *testing.T) {
|
|
rb := NewRingBuffer(8)
|
|
rb.Write([]byte("abcdefgh")) // exactly fills
|
|
rb.Write([]byte("ij")) // wraps around
|
|
|
|
got := rb.Bytes()
|
|
// Should contain the last 8 bytes: "cdefghij"
|
|
if string(got) != "cdefghij" {
|
|
t.Errorf("expected 'cdefghij', got '%s'", got)
|
|
}
|
|
}
|
|
|
|
func TestRingBuffer_Reset(t *testing.T) {
|
|
rb := NewRingBuffer(16)
|
|
rb.Write([]byte("test"))
|
|
rb.Reset()
|
|
|
|
got := rb.Bytes()
|
|
if len(got) != 0 {
|
|
t.Errorf("expected empty after reset, got %d bytes", len(got))
|
|
}
|
|
}
|