carlos / platform Public

zdd-6 final fix wave 1: W5 draft markers on the remaining new copy

zdd.settings.save on the Deploys card's Save button; zdd.apps-set.flag-app, .error and .unreadable for apps_swap.go's new flag help and error texts; the swap door's 429 gets its own em-dash-free draft, zdd.api.swap-limited. The table placeholder dash stays (ruling). Astra plan 6 R1 #9.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev b78dab8fc0df83bfbf7d486bcd9e7abb77aea80d parent 2410a5b
5 files changed, +141 −6
  • apps_swap.go +10 −4
  • apps_swap_markers_test.go +88 −0
  • internal/console/apiswap.go +5 −1
  • internal/console/apiswap_test.go +37 −0
  • internal/console/templates/app_settings.html +1 −1
diff --git a/apps_swap.go b/apps_swap.go
index 9582af12b..1b6bed8d0 100644
--- a/apps_swap.go
+++ b/apps_swap.go
@@ -32,6 +32,8 @@ Any member of the app may change these. They reach the app's boxes with its
config, within seconds.`
const (
+ // TODO-COPY zdd.apps-set.flag-app
+ appsSetFlagApp = "app whose swap policy to set"
// TODO-COPY zdd.apps-set.flag-swap
appsSetFlagSwap = "overlap or hold: how a running instance takes a new build"
// TODO-COPY zdd.apps-set.flag-concurrency
@@ -54,6 +56,10 @@ const (
appsSetHoldNote = " each instance stops before its new build starts, and requests wait until it is up"
// TODO-COPY zdd.apps-set.mismatch
appsSetMismatch = "apps set: the change did not stick (the console stored %s, %d at a time). Run it again"
+ // TODO-COPY zdd.apps-set.error
+ appsSetError = "apps set: %v"
+ // TODO-COPY zdd.apps-set.unreadable
+ appsSetUnreadable = "apps set: unreadable response: %w"
// TODO-COPY zdd.apps-set.old-console
appsSetOldConsole = "not found: either %s is not an app in this account, or this console predates carlos apps set (deploy a console built from current main)"
)
@@ -134,7 +140,7 @@ func appsSetStuck(req swapPolicyReq, got apiSwapPolicy) error {
func runAppsSet(args []string) {
lead, args := leadingArg(args)
fs := flag.NewFlagSet("apps set", flag.ExitOnError)
- appF := appFlag(fs, "app whose swap policy to set")
+ appF := appFlag(fs, appsSetFlagApp)
swap := fs.String("swap", "", appsSetFlagSwap)
concurrency := fs.Int("swap-concurrency", 0, appsSetFlagConcurrency)
acctRaw := fs.String("account", "", "account (sqid or name) the app belongs to (default: your only account, or the configured/chosen one)")
@@ -147,7 +153,7 @@ func runAppsSet(args []string) {
log.Fatal(err)
}
if err := validAppName(app); err != nil {
- log.Fatalf("apps set: %v", err)
+ log.Fatalf(appsSetError, err)
}
req, err := appsSetRequest(*swap, *concurrency, flagWasSet(fs, "swap"), flagWasSet(fs, "swap-concurrency"))
if err != nil {
@@ -163,7 +169,7 @@ func runAppsSet(args []string) {
acct := resolvedAccount(sess, acctRaw, acctIDRaw)
got, err := sess.setSwapPolicy(context.Background(), acct, app, req)
if err != nil {
- log.Fatalf("apps set: %v", err)
+ log.Fatalf(appsSetError, err)
}
if err := appsSetStuck(req, got); err != nil {
log.Fatal(err)
@@ -192,7 +198,7 @@ func (s *apiSession) setSwapPolicy(ctx context.Context, a apiAcct, app string, r
}
var out apiSwapPolicy
if err := json.Unmarshal(body, &out); err != nil {
- return apiSwapPolicy{}, fmt.Errorf("apps set: unreadable response: %w", err)
+ return apiSwapPolicy{}, fmt.Errorf(appsSetUnreadable, err)
}
return out, nil
}
diff --git a/apps_swap_markers_test.go b/apps_swap_markers_test.go
new file mode 100644
index 000000000..428109905
--- /dev/null
+++ b/apps_swap_markers_test.go
@@ -0,0 +1,88 @@
+package main
+
+import (
+ "go/ast"
+ "go/parser"
+ "go/token"
+ "strconv"
+ "strings"
+ "testing"
+)
+
+// Astra plan 6 R1 #9: every member-facing string `carlos apps set` adds is a
+// marked draft. In apps_swap.go, the help text of a flag and the text of an
+// error it prints must be a constant whose declaration carries a
+// "TODO-COPY zdd." marker. The only literals allowed there are the help
+// strings shared, word for word, with the other verbs' --account,
+// --account-id and --console flags: existing copy, not new.
+func TestAppsSetCopyIsMarkedDrafts(t *testing.T) {
+ shared := map[string]bool{
+ "account (sqid or name) the app belongs to (default: your only account, or the configured/chosen one)": true,
+ "account sqid, matched as a sqid ONLY — supersedes --account": true,
+ "console to act through (default: the only logged-in one)": true,
+ }
+ fset := token.NewFileSet()
+ f, err := parser.ParseFile(fset, "apps_swap.go", nil, parser.ParseComments)
+ if err != nil {
+ t.Fatal(err)
+ }
+ marked := map[string]bool{}
+ for _, d := range f.Decls {
+ g, ok := d.(*ast.GenDecl)
+ if !ok || g.Tok != token.CONST {
+ continue
+ }
+ for _, sp := range g.Specs {
+ vs := sp.(*ast.ValueSpec)
+ doc := ""
+ if vs.Doc != nil {
+ doc = vs.Doc.Text()
+ }
+ if len(g.Specs) == 1 && g.Doc != nil {
+ doc += g.Doc.Text()
+ }
+ for _, n := range vs.Names {
+ marked[n.Name] = strings.Contains(doc, "TODO-COPY zdd.")
+ }
+ }
+ }
+ // copyArg is which argument of each call is member-facing text.
+ copyArg := map[string]int{"fs.String": 2, "fs.Int": 2, "appFlag": 1, "consoleOption": 1,
+ "fmt.Errorf": 0, "errors.New": 0, "log.Fatal": 0, "log.Fatalf": 0}
+ checked := 0
+ ast.Inspect(f, func(n ast.Node) bool {
+ call, ok := n.(*ast.CallExpr)
+ if !ok {
+ return true
+ }
+ name := ""
+ switch fn := call.Fun.(type) {
+ case *ast.Ident:
+ name = fn.Name
+ case *ast.SelectorExpr:
+ if x, ok := fn.X.(*ast.Ident); ok {
+ name = x.Name + "." + fn.Sel.Name
+ }
+ }
+ i, ok := copyArg[name]
+ if !ok || i >= len(call.Args) {
+ return true
+ }
+ checked++
+ pos := fset.Position(call.Pos())
+ switch a := call.Args[i].(type) {
+ case *ast.BasicLit:
+ if s, _ := strconv.Unquote(a.Value); !shared[s] {
+ t.Errorf("%s: %s(%s): new copy as a bare literal; make it a marked draft constant", pos, name, a.Value)
+ }
+ case *ast.Ident:
+ if m, known := marked[a.Name]; known && !m {
+ t.Errorf("%s: %s(%s): its constant has no TODO-COPY marker", pos, name, a.Name)
+ }
+ }
+ return true
+ })
+ if checked < 10 {
+ t.Fatalf("only %d copy sites checked in apps_swap.go: the scan is broken", checked)
+ }
+}
diff --git a/internal/console/apiswap.go b/internal/console/apiswap.go
index 644ff4853..5e73d8a76 100644
--- a/internal/console/apiswap.go
+++ b/internal/console/apiswap.go
@@ -25,6 +25,10 @@ const (
apiSwapBad = `swap must be "overlap" or "hold"`
// TODO-COPY zdd.api.concurrency-bad
apiSwapConcurrencyBad = "swap_concurrency must be a whole number, 1 or more"
+ // apiSwapLimited is this door's own draft: the sibling doors' sentence
+ // carries an em dash (Astra plan 6 R1 #9).
+ // TODO-COPY zdd.api.swap-limited
+ apiSwapLimited = "too many requests. wait a minute and try again"
// apiSwapAudit is app, mode, number, then "cli" or "console".
// TODO-COPY zdd.api.swap-audit
apiSwapAudit = "set the swap policy for %s to %s, %d at a time (%s)"
@@ -37,7 +41,7 @@ func (s *Server) handleAPISwapPut(w http.ResponseWriter, r *http.Request) {
}
p, _ := principalFrom(r.Context())
if !s.allowWrite(p, time.Now()) {
- jsonError(w, http.StatusTooManyRequests, "too many requests — wait a minute and try again")
+ jsonError(w, http.StatusTooManyRequests, apiSwapLimited)
return
}
// Pointers, so an absent half ("leave it") differs from a present one.
diff --git a/internal/console/apiswap_test.go b/internal/console/apiswap_test.go
index 80ff58e26..eb9ec97dd 100644
--- a/internal/console/apiswap_test.go
+++ b/internal/console/apiswap_test.go
@@ -69,3 +69,40 @@ func TestAPISwapNamesAMisspeltKey(t *testing.T) {
t.Fatalf("typo = %d %s, want a 400 naming swap_conurrency", code, body)
}
}
+
+// Astra plan 6 R1 #9: the swap door's rate-limit answer is its own marked
+// draft (zdd.api.swap-limited), not the sibling doors' em-dash sentence.
+func TestAPISwapRateLimitIsAMarkedDraftWithNoEmDash(t *testing.T) {
+ fx := newConsoleFixture(t)
+ token := cliToken(t, fx, "paul@keymail.dev", "code-swap-limited")
+ u := fx.ts.URL + "/api/cli/apps/" + fx.sqid + "/kass/swap"
+ for i := 0; i < cliWriteBurst+3; i++ {
+ code, _, body := rawReq(t, http.MethodPut, u, token, `{"swap":"overlap"}`)
+ if code != http.StatusTooManyRequests {
+ continue
+ }
+ var out struct{ Error string }
+ if err := json.Unmarshal([]byte(body), &out); err != nil || out.Error != "too many requests. wait a minute and try again" {
+ t.Fatalf("429 = %s (%v)", body, err)
+ }
+ return
+ }
+ t.Fatalf("the swap door was never rate-limited in %d attempts", cliWriteBurst+3)
+}
+
+// Astra plan 6 R1 #9: the Deploys card's Save button is new copy, so it
+// carries its draft marker like the card's other strings.
+func TestDeploysCardSaveButtonIsMarked(t *testing.T) {
+ raw, err := os.ReadFile("templates/app_settings.html")
+ if err != nil {
+ t.Fatal(err)
+ }
+ _, card, ok := strings.Cut(string(raw), "{{/* TODO-COPY zdd.settings.heading */}}")
+ if !ok {
+ t.Fatal("no Deploys card in app_settings.html")
+ }
+ card, _, _ = strings.Cut(card, "</form>")
+ if !strings.Contains(card, `<button type="submit" class="btn-sm">{{/* TODO-COPY zdd.settings.save */}}Save</button>`) {
+ t.Fatalf("the Deploys card's Save button has no zdd.settings.save marker:\n%s", card)
+ }
+}
diff --git a/internal/console/templates/app_settings.html b/internal/console/templates/app_settings.html
index 958e3ca07..58f2a42cb 100644
--- a/internal/console/templates/app_settings.html
+++ b/internal/console/templates/app_settings.html
@@ -377,7 +377,7 @@ is off until you turn it on.</p>
<label>{{/* TODO-COPY zdd.settings.concurrency-label */}}{{.SwapCopy.ConcurrencyLabel}}
<input type="number" name="swap_concurrency" min="1" step="1" value="{{.SwapPolicy.Concurrency}}" required>
</label>
- <button type="submit" class="btn-sm">Save</button>
+ <button type="submit" class="btn-sm">{{/* TODO-COPY zdd.settings.save */}}Save</button>
</form>
<p class="small">{{/* TODO-COPY zdd.settings.migrations-note */}}{{.SwapCopy.MigrationsNote}}</p>
{{end}}