carlos / platform Public

settings-1 task 12: digests listing reports failed accounts

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 461b82bfaf6a97d6253e24a73c95b18f3b005e1d parent ed3be37
2 files changed, +163 −24
  • channels_digests.go +50 −20
  • channels_digests_test.go +113 −4
diff --git a/channels_digests.go b/channels_digests.go
index 5a65d28ef..03ee07c92 100644
--- a/channels_digests.go
+++ b/channels_digests.go
@@ -55,6 +55,8 @@ const (
textTurnedOff = "pointer digests off for %s. Boxes that have already seen digests keep refusing pointers without them"
// TODO-COPY digests.all-accounts
textAllAccounts = "every account"
+ // TODO-COPY digests.status-error
+ textStatusError = "pointer digests: unknown for %s, the report could not be read: %s"
// TODO-COPY digests.unknown-account (NEW: not in strings.json)
textUnknownAccount = "no account %s on this console, so nothing was changed for it"
)
@@ -201,17 +203,8 @@ func runChannelsDigests(args []string) {
log.Fatal(err)
}
if sess != nil && verb != "" {
- // The console's system door skips an account id it does not know and
- // still answers 200; an operator who named one must not read that as
- // success.
- for _, id := range ids {
- known := false
- for _, a := range rep.Accounts {
- known = known || a.Account == id
- }
- if !known {
- log.Fatalf(textUnknownAccount, id)
- }
+ if err := checkNamedAccounts(rep, ids); err != nil {
+ log.Fatal(err)
}
}
names := strings.Join(ids, ", ")
@@ -224,14 +217,24 @@ func runChannelsDigests(args []string) {
case "off":
fmt.Fprintf(stdout, textTurnedOff+"\n", names)
}
+ // An account whose report could not be read is "unknown", never "off",
+ // and an incomplete report never says the switch can be turned on.
+ failed := false
for _, a := range rep.Accounts {
- if a.On {
+ switch {
+ case a.Error != "":
+ failed = true
+ fmt.Fprintf(stdout, textStatusError+"\n", a.Account, a.Error)
+ case a.On:
fmt.Fprintf(stdout, textStatusOn+"\n", a.Account, a.Since)
- } else {
+ default:
fmt.Fprintf(stdout, textStatusOff+"\n", a.Account)
}
}
if len(rep.Channels) == 0 {
+ if failed {
+ return
+ }
fmt.Fprintln(stdout, textStatusNoneLeft)
return
}
@@ -243,6 +246,22 @@ func runChannelsDigests(args []string) {
w.Flush()
}
+// checkNamedAccounts is the guard for an operator who named accounts: the
+// console's system door skips an account id it does not know and still
+// answers 200, which must not read as success.
+func checkNamedAccounts(rep apiPointerDigests, ids []string) error {
+ for _, id := range ids {
+ known := false
+ for _, a := range rep.Accounts {
+ known = known || a.Account == id
+ }
+ if !known {
+ return fmt.Errorf(textUnknownAccount, id)
+ }
+ }
+ return nil
+}
+
// localPointerDigests is the direct-store form: the root key writes or
// removes each named account's marker, and the bucket is listed for the
// report.
@@ -260,22 +279,33 @@ func localPointerDigests(ctx context.Context, s release.Store, verb string, ids
if err != nil {
return rep, err
}
+ row := apiDigestAccount{Account: acct}
+ // A marker or listing that cannot be read marks this account's row,
+ // as the console's report does; it does not hide the other accounts.
+ marker, err := release.ReadDigestMarker(ctx, s, pub, acct)
+ if err != nil {
+ row.Error = err.Error()
+ rep.Accounts = append(rep.Accounts, row)
+ continue
+ }
+ if marker != nil {
+ row.On, row.Since = true, marker.At.UTC().Format(time.RFC3339)
+ }
dirs, err := s.ListDirs(ctx, "apps/"+acct+"/")
if err != nil {
- return rep, err
+ row.Error = err.Error()
+ rep.Accounts = append(rep.Accounts, row)
+ continue
}
- row := apiDigestAccount{Account: acct}
for _, d := range dirs {
app := strings.TrimSuffix(strings.TrimPrefix(d, "apps/"+acct+"/"), "/")
if !release.ValidApp(app) {
continue
}
- states, marker, err := release.DigestReport(ctx, s, pub, acct, app)
+ states, _, err := release.DigestReport(ctx, s, pub, acct, app)
if err != nil {
- return rep, err
- }
- if marker != nil {
- row.On, row.Since = true, marker.At.UTC().Format(time.RFC3339)
+ row.Error = err.Error()
+ break
}
for _, st := range states {
if !st.HeadDigest {
diff --git a/channels_digests_test.go b/channels_digests_test.go
index f8e19f634..d29bdef57 100644
--- a/channels_digests_test.go
+++ b/channels_digests_test.go
@@ -5,10 +5,12 @@ import (
"context"
"crypto/ed25519"
"encoding/json"
+ "fmt"
"net/http"
"net/http/httptest"
"strings"
"testing"
+ "time"
"github.com/carlosframework/platform/internal/release"
)
@@ -158,8 +160,8 @@ func TestChannelsDigestsAPIMode(t *testing.T) {
runChannelsDigests(nil)
out := buf.String()
for _, want := range []string{
- "pointer digests: on for bab since 2026-10-07T00:00:00Z",
- "pointer digests: off for bah",
+ fmt.Sprintf(textStatusOn, "bab", "2026-10-07T00:00:00Z"),
+ fmt.Sprintf(textStatusOff, "bah"),
strings.Split(textStatusHeader, "\t")[0], textStateStraggler, "beta",
} {
if !strings.Contains(out, want) {
@@ -175,7 +177,7 @@ func TestChannelsDigestsAPIMode(t *testing.T) {
if posted["on"] != true || len(posted["accounts"].([]any)) != 1 {
t.Fatalf("posted = %v", posted)
}
- if want := "pointer digests on for bab."; !strings.Contains(buf.String(), want) {
+ if want := fmt.Sprintf(textTurnedOn, "bab"); !strings.Contains(buf.String(), want) {
t.Fatalf("on output lacks %q:\n%s", want, buf.String())
}
}
@@ -228,9 +230,116 @@ func TestResignAPIMode(t *testing.T) {
if bodies[1]["digest"] != strings.Repeat("a", 64) || bodies[1]["channel"] != "stable" {
t.Fatalf("signing body = %v", bodies[1])
}
- for _, want := range []string{textResignComputed, textResignMissing, "re-signed titogo stable with digests"} {
+ for _, want := range []string{textResignComputed, textResignMissing, fmt.Sprintf(textResignDone, "titogo", "stable")} {
if !strings.Contains(out.String(), want) {
t.Fatalf("output lacks %q:\n%s", want, out.String())
}
}
}
+
+// An account whose report the console could not read is "unknown", never
+// "off", and never earns the none-left line.
+func TestChannelsDigestsAPIModeFailedAccount(t *testing.T) {
+ useTempCredentials(t)
+ clearDeploymentEnv(t)
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ json.NewEncoder(w).Encode(map[string]any{
+ "accounts": []map[string]any{
+ {"account": "bab", "on": true, "since": "2026-10-07T00:00:00Z"},
+ {"account": "bah", "on": false, "error": "marker did not verify"},
+ },
+ "channels": []map[string]any{},
+ })
+ }))
+ defer ts.Close()
+ loginTo(t, ts.URL, "vicky@keymail.dev")
+
+ buf := captureStdout(t)
+ runChannelsDigests(nil)
+ out := buf.String()
+ if want := fmt.Sprintf(textStatusError, "bah", "marker did not verify"); !strings.Contains(out, want) {
+ t.Fatalf("output lacks %q:\n%s", want, out)
+ }
+ if strings.Contains(out, fmt.Sprintf(textStatusOff, "bah")) || strings.Contains(out, textStatusNoneLeft) {
+ t.Fatalf("a failed account read as off, or the none-left line printed:\n%s", out)
+ }
+ if !strings.Contains(out, fmt.Sprintf(textStatusOn, "bab", "2026-10-07T00:00:00Z")) {
+ t.Fatalf("the healthy account is missing:\n%s", out)
+ }
+}
+
+func TestCheckNamedAccounts(t *testing.T) {
+ rep := apiPointerDigests{Accounts: []apiDigestAccount{{Account: "bab"}, {Account: "bah"}}}
+ if err := checkNamedAccounts(rep, []string{"bab", "bah"}); err != nil {
+ t.Fatalf("known accounts: %v", err)
+ }
+ if err := checkNamedAccounts(rep, nil); err != nil {
+ t.Fatalf("no names: %v", err)
+ }
+ err := checkNamedAccounts(rep, []string{"bab", "zzz"})
+ if err == nil || err.Error() != fmt.Sprintf(textUnknownAccount, "zzz") {
+ t.Fatalf("unknown account: %v", err)
+ }
+}
+
+func runDigests(t *testing.T, args ...string) string {
+ t.Helper()
+ buf := captureStdout(t)
+ runChannelsDigests(args)
+ return buf.String()
+}
+
+// Direct-store `channels digests`: bare lists, off removes the marker, on
+// writes it again (an account with no apps still reads on), and an account
+// whose marker cannot be verified is reported unknown.
+func TestChannelsDigestsDirectStore(t *testing.T) {
+ s, pub := directDigestStore(t) // account bab: marker on, km/stable at v1 with no digest
+ ctx := context.Background()
+ m, err := release.ReadDigestMarker(ctx, s, pub, "bab")
+ if err != nil || m == nil {
+ t.Fatalf("marker: %v %v", m, err)
+ }
+ since := m.At.UTC().Format(time.RFC3339)
+
+ out := runDigests(t, "--account-id", "bab")
+ for _, want := range []string{fmt.Sprintf(textStatusOn, "bab", since), "km", "stable", "v1", textStateNo} {
+ if !strings.Contains(out, want) {
+ t.Fatalf("listing lacks %q:\n%s", want, out)
+ }
+ }
+ if strings.Contains(out, textStatusNoneLeft) {
+ t.Fatalf("none-left printed with a channel still without digests:\n%s", out)
+ }
+
+ out = runDigests(t, "off", "--account-id", "bab")
+ if !strings.Contains(out, fmt.Sprintf(textTurnedOff, "bab")) || !strings.Contains(out, fmt.Sprintf(textStatusOff, "bab")) {
+ t.Fatalf("off:\n%s", out)
+ }
+ if m, _ := release.ReadDigestMarker(ctx, s, pub, "bab"); m != nil {
+ t.Fatal("off left the marker in place")
+ }
+
+ out = runDigests(t, "on", "--account-id", "bab", "--account-id", "bah")
+ if !strings.Contains(out, fmt.Sprintf(textTurnedOn, "bab, bah")) {
+ t.Fatalf("on:\n%s", out)
+ }
+ if m, _ := release.ReadDigestMarker(ctx, s, pub, "bab"); m == nil {
+ t.Fatal("on did not write the marker")
+ }
+ // bah has no apps at all: on, straight after turning it on.
+ if m, _ := release.ReadDigestMarker(ctx, s, pub, "bah"); m == nil || !strings.Contains(out, "pointer digests: on for bah since ") {
+ t.Fatalf("an account with no apps does not read on:\n%s", out)
+ }
+
+ // An unverifiable marker is unknown, and the none-left line stays away.
+ if _, err := s.Put(ctx, release.PointerDigestsKey("bai"), []byte("junk"), ""); err != nil {
+ t.Fatal(err)
+ }
+ out = runDigests(t, "--account-id", "bai")
+ if !strings.HasPrefix(strings.Split(out, "\n")[0], fmt.Sprintf(textStatusError, "bai", "")[:len("pointer digests: unknown for bai")]) {
+ t.Fatalf("unverifiable marker:\n%s", out)
+ }
+ if strings.Contains(out, textStatusNoneLeft) || strings.Contains(out, fmt.Sprintf(textStatusOff, "bai")) {
+ t.Fatalf("unverifiable marker read as off or done:\n%s", out)
+ }
+}