package server import ( "net/http" "net/http/httptest" "os" "path/filepath" "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 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)) } }