a Praxis lifecycle failure names Praxis (V-588)

praxisItemAction.handle returned a hardcoded per-verb constant on any error, so
a Praxis outage, a refused token, a contract mismatch and a decode failure all
said the same thing and none of them said "Praxis". The information already
existed: praxisClient embeds ecosystemHTTP, so the error is an *ecosystemError
with working classifiers, and handle() logged it, traced it and threw it away.

servicePraxis joins the two service constants and the failure goes through
ecosystemGap, which is what Nexus and Hexis already use. The per-verb string is
kept in front of it rather than replaced: it carries which operation did not
happen, and the trace is the only other place that exists. No new Russian is
added — both halves are lines that already ship.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-06 02:49:09 +04:00
parent 5d2fd91c06
commit 502327678f
2 changed files with 65 additions and 4 deletions
+11 -4
View File
@@ -22,8 +22,9 @@ import (
// already knows which one it was talking to — it records the same name in the
// trace (Vikunja #521).
const (
serviceNexus = "Nexus"
serviceHexis = "Hexis"
serviceNexus = "Nexus"
servicePraxis = "Praxis"
serviceHexis = "Hexis"
)
// serviceVars — the one-key map the eco_down and eco_denied lines take.
@@ -141,7 +142,13 @@ type praxisItemAction struct {
verbs []string
ask string // reply when no item id was given
op string // trace + log name of the operation
failure string // reply when the Praxis call errors
// failure is the first half of the reply when the Praxis call errors: which
// operation did not happen. ecosystemGap supplies the second half, which
// names Praxis and splits a refused token from an outage — those two used to
// produce the identical sentence and neither said "Praxis" (Vikunja #588).
// The verb is kept alongside the service name because the trace is the only
// other place it exists, and he is not reading the trace.
failure string
success string
call func(ctx context.Context, px *praxisClient, id string) error
}
@@ -158,7 +165,7 @@ func (a praxisItemAction) handle(ctx context.Context, h *reactiveHandler, px *pr
log.Printf("ecosystem: praxis %s %s: %v", a.op, id, err)
h.recordEcosystemTrace(ctx, "praxis", a.op, traceStatusForError(err), started,
mergeFields(traceErrorFields(err), map[string]any{"item_id": id}))
return a.failure
return a.failure + " " + ecosystemGap(servicePraxis, err)
}
h.recordPraxisTrace(ctx, a.op, started, map[string]any{"item_id": id})
return a.success
+54
View File
@@ -0,0 +1,54 @@
package main
import (
"context"
"strings"
"testing"
"github.com/kami/maven/internal/phraser"
)
// A Praxis lifecycle failure used to return a hardcoded constant that named the
// verb and never the service, so an outage, a refused token and a contract
// mismatch all produced the identical sentence (Vikunja #588). The helpers and
// the Hexis half of the same defect are in ecosystem_gap_test.go.
func TestPraxisLifecycle401NamesPraxis(t *testing.T) {
ctx := context.Background()
praxis := newFakePraxis(t, fixturePraxisAttentionItems())
h := newPraxisTestHandler(t, praxis)
praxis.SetFault(401)
reply := h.handlePraxisAct(ctx, praxisItemDec("resolve_item", "item_1"))
if !strings.Contains(reply, servicePraxis) {
t.Fatalf("praxis failure does not name Praxis: %q", reply)
}
if !strings.Contains(reply, phraser.A(phraser.EcoDenied, serviceVars(servicePraxis))) {
t.Fatalf("401 from praxis: got %q, want the denied line", reply)
}
// The verb that did not happen is still said: the trace is the only other
// place it exists and he is not reading the trace.
if !strings.Contains(reply, "не получилось отметить сделанным.") {
t.Errorf("reply dropped the operation that failed: %q", reply)
}
}
// TestPraxisLifecycleOutageDiffersFrom401 — the identity that was the bug.
func TestPraxisLifecycleOutageDiffersFrom401(t *testing.T) {
ctx := context.Background()
praxis := newFakePraxis(t, fixturePraxisAttentionItems())
h := newPraxisTestHandler(t, praxis)
praxis.SetFault(401)
refused := h.handlePraxisAct(ctx, praxisItemDec("acknowledge_item", "item_1"))
h.ecosystem = &ecosystemWiring{praxis: newPraxisClient(unreachableURL)}
outage := h.handlePraxisAct(ctx, praxisItemDec("acknowledge_item", "item_1"))
if refused == outage {
t.Fatalf("a refused token and an outage still say the same thing: %q", refused)
}
if !strings.Contains(outage, phraser.A(phraser.EcoDown, serviceVars(servicePraxis))) {
t.Fatalf("praxis outage: got %q, want the outage line naming Praxis", outage)
}
}