bootstrap: iniciar bitácora operativa
This commit is contained in:
@@ -0,0 +1,193 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user