Pipeline completo de publicação no LinkedIn: evaluator → redator → editor → art → director → publisher - Seed com 37 posts em _sugestoes.md - Sorteio de formato com N=3 bloqueados (format-history) - Reciclagem mensal de posts com rotação de formato - Revisão via Telegram com chat livre (Gemini 2.5 Flash) - Publicação via LinkedIn API (OAuth2) - Makefile com targets para Windows/Linux/ARM64 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
112 lines
3.0 KiB
Go
112 lines
3.0 KiB
Go
package state_test
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
"ldpost/shared/state"
|
|
)
|
|
|
|
func TestSaveAndLoadState(t *testing.T) {
|
|
dir := t.TempDir()
|
|
postPath := filepath.Join(dir, "Codigo", "test-slug")
|
|
|
|
s := state.NewPostState("test-slug", "Codigo", "como", "Test tema", "Test trend")
|
|
s.Status = state.StatusWaitingRedator
|
|
s.SetEtapa("evaluator", state.EtapaDone)
|
|
|
|
if err := state.SaveState(postPath, s); err != nil {
|
|
t.Fatalf("SaveState: %v", err)
|
|
}
|
|
|
|
loaded, err := state.LoadState(postPath)
|
|
if err != nil {
|
|
t.Fatalf("LoadState: %v", err)
|
|
}
|
|
if loaded.Slug != "test-slug" {
|
|
t.Errorf("slug: got %q, want %q", loaded.Slug, "test-slug")
|
|
}
|
|
if loaded.Status != state.StatusWaitingRedator {
|
|
t.Errorf("status: got %q, want %q", loaded.Status, state.StatusWaitingRedator)
|
|
}
|
|
if loaded.Etapas.Evaluator != state.EtapaDone {
|
|
t.Errorf("evaluator etapa: got %q, want %q", loaded.Etapas.Evaluator, state.EtapaDone)
|
|
}
|
|
}
|
|
|
|
func TestAtomicWrite(t *testing.T) {
|
|
dir := t.TempDir()
|
|
postPath := filepath.Join(dir, "Cat", "slug-atomic")
|
|
|
|
// Write initial state
|
|
s1 := state.NewPostState("slug-atomic", "Cat", "como", "Tema 1", "Trend 1")
|
|
if err := state.SaveState(postPath, s1); err != nil {
|
|
t.Fatalf("first SaveState: %v", err)
|
|
}
|
|
|
|
// Verify original file exists
|
|
stateFile := filepath.Join(postPath, "work", "state.json")
|
|
before, err := os.ReadFile(stateFile)
|
|
if err != nil {
|
|
t.Fatalf("read state file: %v", err)
|
|
}
|
|
|
|
// Write second state
|
|
s2 := state.NewPostState("slug-atomic", "Cat", "erro", "Tema 2", "Trend 2")
|
|
if err := state.SaveState(postPath, s2); err != nil {
|
|
t.Fatalf("second SaveState: %v", err)
|
|
}
|
|
|
|
after, err := os.ReadFile(stateFile)
|
|
if err != nil {
|
|
t.Fatalf("read state file after: %v", err)
|
|
}
|
|
|
|
// Files should differ (second write changed the content)
|
|
if string(before) == string(after) {
|
|
t.Error("estado não mudou após segundo SaveState")
|
|
}
|
|
|
|
// Verify no .tmp files left behind
|
|
tmpFile := stateFile + ".tmp"
|
|
if _, err := os.Stat(tmpFile); !os.IsNotExist(err) {
|
|
t.Error("arquivo .tmp deixado para trás após SaveState")
|
|
}
|
|
}
|
|
|
|
func TestSetEtapa(t *testing.T) {
|
|
s := state.NewPostState("slug", "Cat", "como", "tema", "trend")
|
|
|
|
agentes := []string{"evaluator", "redator", "editor", "art", "director", "publisher"}
|
|
for _, ag := range agentes {
|
|
before := s.UpdatedAt
|
|
time.Sleep(time.Millisecond) // ensure time advances
|
|
s.SetEtapa(ag, state.EtapaDone)
|
|
|
|
if s.UpdatedAt.Equal(before) || s.UpdatedAt.Before(before) {
|
|
t.Errorf("SetEtapa(%s): UpdatedAt não avançou", ag)
|
|
}
|
|
}
|
|
|
|
if s.Etapas.Evaluator != state.EtapaDone {
|
|
t.Errorf("evaluator etapa: got %q", s.Etapas.Evaluator)
|
|
}
|
|
if s.Etapas.Publisher != state.EtapaDone {
|
|
t.Errorf("publisher etapa: got %q", s.Etapas.Publisher)
|
|
}
|
|
}
|
|
|
|
func TestIsStatus(t *testing.T) {
|
|
s := state.NewPostState("slug", "Cat", "como", "tema", "trend")
|
|
s.Status = state.StatusWaitingEditor
|
|
|
|
if !s.IsStatus(state.StatusWaitingEditor) {
|
|
t.Error("IsStatus deveria retornar true")
|
|
}
|
|
if s.IsStatus(state.StatusPublished) {
|
|
t.Error("IsStatus deveria retornar false")
|
|
}
|
|
}
|