Files
orchestra/internal/operations/debt_test.go
T
kami a757cffc78 Project a debt ledger from canonical history, read-only
Slice one of DEBT-DESIGN.md, with the four amendments applied. It writes
nothing: no new event types, no scheduling, no clustering, no
maintenance tasks. The point is to find out whether the model can
represent debt this project already knows about, before committing to a
durable schema.

The projected type lives in domain and the fold lives in store, so the
first implementation does not bake a read model into the command layer.
operations owns the one action that exists, CheckDebtEligibility, which
is a pure function returning explicit reasons like CheckSubmission.

Signatures carry their version in the string. Normalization rules will
change, and without a version that silently regroups history and moves
the recurrence counts eligibility was already decided on.

Observations require exactly one of event_id and legacy_ref. Imported
Fxx history predates the events that would justify it, and a fabricated
event id would break the provenance rule the ledger exists to enforce.

Incompleteness is reported, not hidden. Manual interventions and worker
observations are carried by no event type, so the ledger names both as
non-durable gaps rather than reading as "no operational cost". The
operational refusal reason says the intervention count is structurally
zero on every current log.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CVbaKucEYBjMqVeUgJUsc1
2026-08-29 02:00:58 +04:00

69 lines
2.6 KiB
Go

package operations
import (
"strings"
"testing"
"orchestra/internal/domain"
)
func debtItem(class domain.DebtClass, kinds ...struct {
kind domain.ObservationKind
task string
}) domain.DebtItem {
item := domain.DebtItem{Class: class}
for _, k := range kinds {
item.Observations = append(item.Observations, domain.DebtObservation{Kind: k.kind, TaskID: k.task})
}
return item
}
type obs = struct {
kind domain.ObservationKind
task string
}
// The thresholds are the whole policy, so each class gets its own case. Polish
// is the one that must never pass, or the ledger becomes a cleanup generator.
func TestDebtEligibilityPerClass(t *testing.T) {
fail := obs{domain.ObservationFailureClass, "t1"}
if !CheckDebtEligibility(debtItem(domain.DebtCorrectness, fail)).Eligible {
t.Fatal("correctness debt is eligible on first observation")
}
twice := debtItem(domain.DebtOperational, fail, obs{domain.ObservationFailureClass, "t1"})
if CheckDebtEligibility(twice).Eligible {
t.Fatal("two occurrences on one task must not qualify as operational debt")
}
spread := debtItem(domain.DebtOperational, fail,
obs{domain.ObservationFailureClass, "t2"}, obs{domain.ObservationFailureClass, "t3"})
if !CheckDebtEligibility(spread).Eligible {
t.Fatal("three occurrences across three tasks must qualify")
}
manual := debtItem(domain.DebtOperational, obs{domain.ObservationManualIntervention, "t1"})
if !CheckDebtEligibility(manual).Eligible {
t.Fatal("one manual intervention must qualify on its own")
}
blocked := debtItem(domain.DebtStructural,
obs{domain.ObservationBlockReason, "t1"}, obs{domain.ObservationBlockReason, "t2"})
if !CheckDebtEligibility(blocked).Eligible {
t.Fatal("two blocked tasks in one component must qualify as structural")
}
polish := debtItem(domain.DebtPolish, fail, fail, fail, fail, fail)
if CheckDebtEligibility(polish).Eligible {
t.Fatal("polish must never promote itself, at any recurrence")
}
}
// A refusal has to say what is missing. "Not eligible" alone sends an operator
// reading code, which is the mistake SubmissionCheck already documents.
func TestDebtRefusalNamesTheMissingEvidence(t *testing.T) {
check := CheckDebtEligibility(debtItem(domain.DebtOperational, obs{domain.ObservationFailureClass, "t1"}))
if check.Eligible || len(check.Reasons) == 0 {
t.Fatalf("want a refusal with reasons, got %+v", check)
}
joined := strings.Join(check.Reasons, " ")
if !strings.Contains(joined, "manual interventions are not recorded") {
t.Fatalf("the refusal must say the intervention count is structurally zero: %v", check.Reasons)
}
}