carlos / platform Public

appconfig: RenderHeldUnderBefore, the held render under the claim with carry (c) (moves Task 7, Q2.2)

The landing's configured step renders the held row under the claim's
held-config selection. RenderHeldUnderBefore is RenderHeldUnder with
RenderHeldBefore's before hook: the work is chosen from the merged
versions, before journals the sha256 of the bytes to be placed, and only
then does the selection write them. A refusal from before writes nothing,
never enters the selection and sends no ack. platform-1e reviews it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 0141cae0be31b4aeac0917da9ce3406234b52f71 parent 6f1f0d3
2 files changed, +139 −0
  • internal/appconfig/renderheld.go +19 −0
  • internal/appconfig/renderheld_claim_test.go +120 −0
diff --git a/internal/appconfig/renderheld.go b/internal/appconfig/renderheld.go
index 6c36b82ae..1d9b5ebd7 100644
--- a/internal/appconfig/renderheld.go
+++ b/internal/appconfig/renderheld.go
@@ -87,6 +87,25 @@ func RenderHeldUnder(ctx context.Context, s release.Store, priv *ecdh.PrivateKey
return renderHeld(ctx, s, priv, dataDir, rt, op, box, work, nil)
}
+// RenderHeldUnderBefore is RenderHeldUnder with RenderHeldBefore's before
+// (moves after claim Plan 12, Task 7; platform-1e's answer to Q2.2): the
+// held render the landing's configured step runs under the claim, which
+// keeps carry (c). The ownership checks, the merge, the renderer, the
+// writer and the move-bound ack are RenderHeld's. work is called once the
+// merged versions are known, as for RenderHeldUnder; before is then called
+// with the hex sha256 of exactly the bytes the render places (or finds in
+// place), and only after it returns is anything written, through the
+// selection work returned. An error from before ends the render: nothing is
+// written, the selection is never entered and no ack is sent. A nil work or
+// a nil before is refused before anything is read. Like RenderHeldUnder it
+// does not consult CARLOS_HOST_CLAIM: the selection is the authority.
+func RenderHeldUnderBefore(ctx context.Context, s release.Store, priv *ecdh.PrivateKey, dataDir string, rt registry.Route, op string, box string, work func(HeldRender) (hostwrite.Work, error), before func(sha256hex string) error) (HeldRender, error) {
+ if work == nil || before == nil {
+ return HeldRender{}, hostwrite.ErrUnavailable
+ }
+ return renderHeld(ctx, s, priv, dataDir, rt, op, box, work, before)
+}
+
func renderHeld(ctx context.Context, s release.Store, priv *ecdh.PrivateKey, dataDir string, rt registry.Route, op string, box string, work func(HeldRender) (hostwrite.Work, error), before func(string) error) (HeldRender, error) {
if err := heldRenderable(rt, op, box); err != nil {
return HeldRender{}, err
diff --git a/internal/appconfig/renderheld_claim_test.go b/internal/appconfig/renderheld_claim_test.go
index 002944b63..0d2347efc 100644
--- a/internal/appconfig/renderheld_claim_test.go
+++ b/internal/appconfig/renderheld_claim_test.go
@@ -2,8 +2,11 @@ package appconfig
import (
"context"
+ "crypto/sha256"
+ "encoding/hex"
"errors"
"os"
+ "strings"
"testing"
"github.com/carlosframework/platform/internal/hostwrite"
@@ -97,3 +100,120 @@ func TestRenderHeldUnderRefusals(t *testing.T) {
t.Fatalf("an ack was written: %v", err)
}
}
+
+// orderedWork is a claim selection stand-in that records, in log, each
+// write it lets through.
+type orderedWork struct{ log *[]string }
+
+func (w orderedWork) With(_ context.Context, id hostwrite.Identity, fn func() error) error {
+ *w.log = append(*w.log, "write")
+ return fn()
+}
+
+// TestRenderHeldUnderBeforeJournalsBeforeTheWrite (moves after claim Plan
+// 12, Task 7; platform-1e's answer to Q2.2): the held render under the claim
+// keeps carry (c). The work is chosen from the merged versions, then before
+// learns the sha256 of exactly the bytes the render places, and only then
+// does the selection write them. A refusal from before writes nothing, sends
+// no ack and never enters the selection; a missing before or work is refused
+// before anything is read or written; the ownership checks are RenderHeld's.
+func TestRenderHeldUnderBeforeJournalsBeforeTheWrite(t *testing.T) {
+ ctx, s, _, dd, privB64, _ := execKit(t)
+ priv, err := ParsePrivateKey(privB64, "test")
+ if err != nil {
+ t.Fatal(err)
+ }
+ def, env, overlay := heldFixture(t, ctx, s)
+ t.Setenv("CARLOS_HOST_CLAIM", "1")
+ rt := landingRow()
+ path := ConfigEnvPath(dd, rt)
+ noAck := func(t *testing.T) {
+ t.Helper()
+ if _, err := os.Stat(path); !os.IsNotExist(err) {
+ t.Fatalf("a file was written: %v", err)
+ }
+ if _, _, err := s.Get(ctx, ConfigAckKey(heldBox, heldHost)); !errors.Is(err, os.ErrNotExist) {
+ t.Fatalf("an ack was written: %v", err)
+ }
+ }
+
+ t.Run("refusals", func(t *testing.T) {
+ pick := func(HeldRender) (hostwrite.Work, error) { return &recordingWork{}, nil }
+ if _, err := RenderHeldUnderBefore(ctx, s, priv, dd, rt, heldOp, heldBox, pick, nil); !errors.Is(err, hostwrite.ErrUnavailable) {
+ t.Fatalf("no before: %v", err)
+ }
+ if _, err := RenderHeldUnderBefore(ctx, s, priv, dd, rt, heldOp, heldBox, nil, func(string) error { return nil }); !errors.Is(err, hostwrite.ErrUnavailable) {
+ t.Fatalf("no work: %v", err)
+ }
+ ready := rt
+ ready.Status = "ready"
+ if _, err := RenderHeldUnderBefore(ctx, s, priv, dd, ready, heldOp, heldBox, pick, func(string) error { return nil }); !errors.Is(err, ErrNotHeld) {
+ t.Fatalf("a row SyncExec owns: %v", err)
+ }
+ other := rt
+ other.HoldOp = "mv-other"
+ if _, err := RenderHeldUnderBefore(ctx, s, priv, dd, other, heldOp, heldBox, pick, func(string) error { return nil }); !errors.Is(err, ErrNotHeld) {
+ t.Fatalf("a row another op holds: %v", err)
+ }
+ noAck(t)
+ })
+
+ t.Run("a refusing journal writes nothing", func(t *testing.T) {
+ var log []string
+ refused := errors.New("the journal refused")
+ _, err := RenderHeldUnderBefore(ctx, s, priv, dd, rt, heldOp, heldBox, func(HeldRender) (hostwrite.Work, error) {
+ log = append(log, "select")
+ return orderedWork{&log}, nil
+ }, func(string) error {
+ log = append(log, "journal")
+ return refused
+ })
+ if !errors.Is(err, refused) {
+ t.Fatalf("a refusing journal: %v", err)
+ }
+ if strings.Join(log, ",") != "select,journal" {
+ t.Fatalf("order %v: the selection wrote after a refused journal", log)
+ }
+ noAck(t)
+ })
+
+ t.Run("the journal comes before the write", func(t *testing.T) {
+ var (
+ log []string
+ seen HeldRender
+ told string
+ )
+ hr, err := RenderHeldUnderBefore(ctx, s, priv, dd, rt, heldOp, heldBox, func(pre HeldRender) (hostwrite.Work, error) {
+ log = append(log, "select")
+ seen = pre
+ return orderedWork{&log}, nil
+ }, func(sha string) error {
+ log = append(log, "journal")
+ if _, err := os.Stat(path); !os.IsNotExist(err) {
+ t.Errorf("the file was there before the journal: %v", err)
+ }
+ told = sha
+ return nil
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(log) < 3 || log[0] != "select" || log[1] != "journal" || log[2] != "write" {
+ t.Fatalf("order %v, want the selection, then the journal, then the writes", log)
+ }
+ if seen.BundleETags["default"] != def || seen.BundleETags["environment"] != env || seen.OverlayETag != overlay {
+ t.Fatalf("the work was chosen from %+v", seen)
+ }
+ body, err := os.ReadFile(path)
+ if err != nil {
+ t.Fatal(err)
+ }
+ sum := sha256.Sum256(body)
+ if want := hex.EncodeToString(sum[:]); told != want || hr.SHA256 != want {
+ t.Fatalf("journal %q, render %q, file %q", told, hr.SHA256, want)
+ }
+ if a := readAck(t, ctx, s); a.MoveOp != heldOp || a.FileSHA256 != told || a.Box != heldBox {
+ t.Fatalf("ack: %+v", a)
+ }
+ })
+}