194 lines
5.8 KiB
Go
194 lines
5.8 KiB
Go
package clusterlog
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestReviewQueueUsesContentHashAndReviewerSnapshot(t *testing.T) {
|
|
root := testProjectRoot(t)
|
|
ana := testAdmin(t, "ana", 1, "owner", "admin")
|
|
bob := testAdmin(t, "bob", 2, "admin")
|
|
registry := AdminRegistry{
|
|
Version: 1,
|
|
ReviewPolicy: ReviewPolicy{Default: "all-active", IncludeAuthor: true},
|
|
Admins: []Admin{ana, bob},
|
|
}
|
|
if err := saveAdmins(root, registry); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
memoryPath := filepath.Join(root, "content", "memorias", "memory.md")
|
|
mustWriteTestFile(t, memoryPath, `+++
|
|
title = "Cambio de red"
|
|
description = "Ajuste controlado"
|
|
date = 2026-08-12T10:00:00Z
|
|
authors = ["ana"]
|
|
slug = "cambio-red"
|
|
|
|
[taxonomies]
|
|
tags = ["red"]
|
|
systems = ["spine-01"]
|
|
|
|
[extra]
|
|
id = "MEM-TEST-001"
|
|
author_id = "ana"
|
|
author_fingerprint = "`+ana.SSHKeys[0].Fingerprint+`"
|
|
risk = "high"
|
|
review_required = true
|
|
review_policy = "all-active"
|
|
reviewer_snapshot = true
|
|
reviewers = ["ana", "bob"]
|
|
+++
|
|
|
|
## Cambio
|
|
|
|
Versión uno.
|
|
`)
|
|
app := New(Options{Root: root, Now: func() time.Time { return time.Date(2026, 8, 12, 12, 0, 0, 0, time.UTC) }})
|
|
state, err := app.BuildState(root)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(state.ReviewQueue) != 1 || len(state.ReviewQueue[0].MissingReviewers) != 2 {
|
|
t.Fatalf("unexpected initial queue: %#v", state.ReviewQueue)
|
|
}
|
|
memory, _, err := findMemory(root, "MEM-TEST-001")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
event := ReviewEvent{
|
|
ID: "REV-TEST-001", EntryID: memory.EntryID, AdminID: "ana",
|
|
Fingerprint: ana.SSHKeys[0].Fingerprint, ReviewedAt: "2026-08-12T12:01:00Z",
|
|
ContentSHA256: memory.ContentSHA256,
|
|
}
|
|
if _, err := writeReviewEvent(root, event); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
state, err = app.BuildState(root)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := state.ReviewQueue[0].MissingReviewers; len(got) != 1 || got[0] != "bob" {
|
|
t.Fatalf("unexpected missing reviewers after review: %#v", got)
|
|
}
|
|
|
|
// Adding a new administrator must not reopen old entries because the
|
|
// required reviewer set is captured when the memory is created.
|
|
carol := testAdmin(t, "carol", 3, "admin")
|
|
registry.Admins = append(registry.Admins, carol)
|
|
if err := saveAdmins(root, registry); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
state, err = app.BuildState(root)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if state.ReviewQueue[0].RequiredCount != 2 || contains(state.ReviewQueue[0].MissingReviewers, "carol") {
|
|
t.Fatalf("snapshot unexpectedly changed: %#v", state.ReviewQueue[0])
|
|
}
|
|
|
|
// Any edit changes the hash, invalidating the old review automatically.
|
|
data, err := os.ReadFile(memoryPath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(memoryPath, append(data, []byte("\nCorrección posterior.\n")...), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
state, err = app.BuildState(root)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := state.ReviewQueue[0].MissingReviewers; len(got) != 2 || !contains(got, "ana") || !contains(got, "bob") {
|
|
t.Fatalf("old reviews were not invalidated: %#v", got)
|
|
}
|
|
}
|
|
|
|
func TestTaskProjectionFromIndependentEventFiles(t *testing.T) {
|
|
root := testProjectRoot(t)
|
|
ana := testAdmin(t, "ana", 1, "owner", "admin")
|
|
if err := saveAdmins(root, AdminRegistry{Version: 1, ReviewPolicy: ReviewPolicy{Default: "all-active", IncludeAuthor: true}, Admins: []Admin{ana}}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
mustWriteTestFile(t, filepath.Join(root, "content", "tareas", "task.md"), `+++
|
|
title = "Probar respaldo"
|
|
description = "Prueba de tarea"
|
|
date = 2026-08-12T10:00:00Z
|
|
authors = ["ana"]
|
|
slug = "probar-respaldo"
|
|
|
|
[taxonomies]
|
|
tags = ["backup"]
|
|
systems = ["etcd"]
|
|
|
|
[extra]
|
|
id = "TSK-TEST-001"
|
|
status = "todo"
|
|
priority = "high"
|
|
project_id = ""
|
|
assignees = ["ana"]
|
|
estimate_minutes = 60
|
|
due = "2026-08-20"
|
|
+++
|
|
|
|
## Objetivo
|
|
|
|
Validar el respaldo.
|
|
`)
|
|
events := []TaskEvent{
|
|
{ID: "EVT-3", TaskID: "TSK-TEST-001", Type: "status", AdminID: "ana", Fingerprint: ana.SSHKeys[0].Fingerprint, At: "2026-08-12T10:30:00Z", Status: "blocked"},
|
|
{ID: "EVT-1", TaskID: "TSK-TEST-001", Type: "start", AdminID: "ana", Fingerprint: ana.SSHKeys[0].Fingerprint, At: "2026-08-12T10:00:00Z"},
|
|
{ID: "EVT-2", TaskID: "TSK-TEST-001", Type: "stop", AdminID: "ana", Fingerprint: ana.SSHKeys[0].Fingerprint, At: "2026-08-12T10:25:00Z", Minutes: 25},
|
|
{ID: "EVT-4", TaskID: "TSK-TEST-001", Type: "adjust", AdminID: "ana", Fingerprint: ana.SSHKeys[0].Fingerprint, At: "2026-08-12T10:31:00Z", Minutes: 5},
|
|
}
|
|
// Intentionally write out of chronological order: loading must sort by At.
|
|
if _, err := writeTaskEvents(root, events); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
state, err := New(Options{}).BuildState(root)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(state.Tasks) != 1 {
|
|
t.Fatalf("unexpected tasks: %#v", state.Tasks)
|
|
}
|
|
got := state.Tasks[0]
|
|
if got.ActualMinutes != 30 || got.Status != "blocked" || len(got.RunningBy) != 0 || got.RunningBy == nil {
|
|
t.Fatalf("unexpected task projection: %#v", got)
|
|
}
|
|
}
|
|
|
|
func TestSyncRegeneratesWhenProjectionVersionChanges(t *testing.T) {
|
|
root := testProjectRoot(t)
|
|
app := New(Options{Root: root, Now: func() time.Time { return time.Date(2026, 8, 12, 15, 0, 0, 0, time.UTC) }})
|
|
state, changed, path, err := app.Sync(root, false)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !changed || state.Version != siteStateVersion {
|
|
t.Fatalf("unexpected initial sync: changed=%v state=%#v", changed, state)
|
|
}
|
|
|
|
var stale SiteState
|
|
if err := loadJSON(path, &stale); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
stale.Version = siteStateVersion - 1
|
|
if err := writeJSONAtomic(path, stale); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if _, _, _, err := app.Sync(root, true); err == nil {
|
|
t.Fatal("sync --check accepted a stale projection version")
|
|
}
|
|
state, changed, _, err = app.Sync(root, false)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !changed || state.Version != siteStateVersion {
|
|
t.Fatalf("projection was not regenerated: changed=%v state=%#v", changed, state)
|
|
}
|
|
}
|