Require committed remote current state
This commit is contained in:
@@ -11,37 +11,23 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
ErrCurrentRunPointerMissing = errors.New("current run pointer missing")
|
||||
ErrCurrentManifestMissing = errors.New("current manifest missing")
|
||||
ErrCurrentCommitPointerMissing = errors.New("current commit pointer missing")
|
||||
)
|
||||
|
||||
type CurrentRunPointerMissingError struct {
|
||||
type CurrentCommitPointerMissingError struct {
|
||||
Key string
|
||||
}
|
||||
|
||||
func (e *CurrentRunPointerMissingError) Error() string {
|
||||
return fmt.Sprintf("%s: %q", ErrCurrentRunPointerMissing, e.Key)
|
||||
func (e *CurrentCommitPointerMissingError) Error() string {
|
||||
return fmt.Sprintf("%s: %q; republish this session to establish committed remote state", ErrCurrentCommitPointerMissing, e.Key)
|
||||
}
|
||||
|
||||
func (e *CurrentRunPointerMissingError) Unwrap() error {
|
||||
return ErrCurrentRunPointerMissing
|
||||
}
|
||||
|
||||
type CurrentManifestMissingError struct {
|
||||
Key string
|
||||
}
|
||||
|
||||
func (e *CurrentManifestMissingError) Error() string {
|
||||
return fmt.Sprintf("%s: %q", ErrCurrentManifestMissing, e.Key)
|
||||
}
|
||||
|
||||
func (e *CurrentManifestMissingError) Unwrap() error {
|
||||
return ErrCurrentManifestMissing
|
||||
func (e *CurrentCommitPointerMissingError) Unwrap() error {
|
||||
return ErrCurrentCommitPointerMissing
|
||||
}
|
||||
|
||||
type CurrentState struct {
|
||||
SessionPrefix string
|
||||
CurrentRunIDKey string
|
||||
CurrentManifestKey string
|
||||
CurrentPointerKey string
|
||||
RunID string
|
||||
@@ -77,10 +63,10 @@ func LoadCurrentState(
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("check current commit pointer %q: %w", pointerKey, err)
|
||||
}
|
||||
if pointerExists {
|
||||
return loadCommittedCurrentState(ctx, store, prefix, pointerKey, validation)
|
||||
if !pointerExists {
|
||||
return nil, &CurrentCommitPointerMissingError{Key: pointerKey}
|
||||
}
|
||||
return loadLegacyCurrentState(ctx, store, prefix, validation)
|
||||
return loadCommittedCurrentState(ctx, store, prefix, pointerKey, validation)
|
||||
}
|
||||
|
||||
func ValidateCurrentStateIdentity(state *CurrentState, validation CurrentStateValidation) error {
|
||||
@@ -95,12 +81,12 @@ func ValidateCurrentStateIdentity(state *CurrentState, validation CurrentStateVa
|
||||
manifestRunID := strings.TrimSpace(state.Manifest.RunID)
|
||||
if manifestCampaign != "" {
|
||||
if err := ValidateSessionIdentity(manifestCampaign, manifestSessionID); err != nil {
|
||||
return fmt.Errorf("current manifest contains unsafe legacy identities; migrate remote state before use: %w", err)
|
||||
return fmt.Errorf("current manifest contains unsafe identities: %w", err)
|
||||
}
|
||||
}
|
||||
if manifestRunID != "" {
|
||||
if err := ValidateRunIdentity(manifestRunID); err != nil {
|
||||
return fmt.Errorf("current manifest contains an unsafe legacy run id; migrate remote state before use: %w", err)
|
||||
return fmt.Errorf("current manifest contains an unsafe run id: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,7 +119,7 @@ func ValidateCurrentStateIdentity(state *CurrentState, validation CurrentStateVa
|
||||
if manifestRunID != expectedRunID {
|
||||
return fmt.Errorf(
|
||||
"current run pointer %q references run %q but current manifest run_id is %q",
|
||||
state.CurrentRunIDKey,
|
||||
state.CurrentPointerKey,
|
||||
expectedRunID,
|
||||
manifestRunID,
|
||||
)
|
||||
|
||||
@@ -71,7 +71,6 @@ func loadCommittedCurrentState(
|
||||
|
||||
state := &CurrentState{
|
||||
SessionPrefix: sessionPrefix,
|
||||
CurrentRunIDKey: pointerKey,
|
||||
CurrentPointerKey: pointerKey,
|
||||
CurrentManifestKey: sessionManifest.DestinationKey,
|
||||
RunID: commit.RunID,
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
package artifacts
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"gitea.maximumdirect.net/eric/narratio/internal/adapters/storage"
|
||||
"gitea.maximumdirect.net/eric/narratio/internal/manifest"
|
||||
)
|
||||
|
||||
const (
|
||||
// MaxLegacyCurrentRunPointerBytes bounds the compatibility run selector.
|
||||
MaxLegacyCurrentRunPointerBytes int64 = 4 << 10
|
||||
// MaxLegacyCurrentManifestBytes bounds the compatibility session manifest.
|
||||
MaxLegacyCurrentManifestBytes int64 = MaxRemoteSessionManifestBytes
|
||||
)
|
||||
|
||||
// This file is temporary compatibility support for sessions published before
|
||||
// immutable current commits. It can be removed after legacy remote state is migrated.
|
||||
|
||||
func loadLegacyCurrentState(
|
||||
ctx context.Context,
|
||||
store storage.ObjectStore,
|
||||
sessionPrefix string,
|
||||
validation CurrentStateValidation,
|
||||
) (*CurrentState, error) {
|
||||
currentManifestKey, currentRunIDKey := ResolveCurrentStateKeys(sessionPrefix)
|
||||
runID, err := loadLegacyCurrentRunPointer(ctx, store, currentRunIDKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m, err := loadLegacyCurrentManifest(ctx, store, currentManifestKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
state := &CurrentState{
|
||||
SessionPrefix: sessionPrefix,
|
||||
CurrentRunIDKey: currentRunIDKey,
|
||||
CurrentManifestKey: currentManifestKey,
|
||||
RunID: runID,
|
||||
Manifest: m,
|
||||
}
|
||||
if err := ValidateCurrentStateIdentity(state, validation); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return state, nil
|
||||
}
|
||||
|
||||
func loadLegacyCurrentRunPointer(ctx context.Context, store storage.ObjectStore, currentRunIDKey string) (string, error) {
|
||||
key := strings.TrimSpace(currentRunIDKey)
|
||||
if key == "" {
|
||||
return "", fmt.Errorf("current run pointer key is required")
|
||||
}
|
||||
|
||||
_, data, err := readCurrentStateControlObject(ctx, store, key, "legacy current run pointer", MaxLegacyCurrentRunPointerBytes)
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return "", &CurrentRunPointerMissingError{Key: key}
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
runID := strings.TrimSpace(string(data))
|
||||
if runID == "" {
|
||||
return "", fmt.Errorf("current run pointer %q is empty", key)
|
||||
}
|
||||
if err := ValidateRunIdentity(runID); err != nil {
|
||||
return "", fmt.Errorf("current run pointer %q contains an unsafe legacy run id; migrate remote state before use: %w", key, err)
|
||||
}
|
||||
return runID, nil
|
||||
}
|
||||
|
||||
func loadLegacyCurrentManifest(ctx context.Context, store storage.ObjectStore, currentManifestKey string) (*manifest.Manifest, error) {
|
||||
key := strings.TrimSpace(currentManifestKey)
|
||||
if key == "" {
|
||||
return nil, fmt.Errorf("current manifest key is required")
|
||||
}
|
||||
|
||||
_, data, err := readCurrentStateControlObject(ctx, store, key, "legacy current manifest", MaxLegacyCurrentManifestBytes)
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return nil, &CurrentManifestMissingError{Key: key}
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
m, err := (&manifest.LocalStore{}).LoadReader(ctx, bytes.NewReader(data))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("current manifest decode failed: %w", err)
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
@@ -4,175 +4,22 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gitea.maximumdirect.net/eric/narratio/internal/adapters/storage"
|
||||
)
|
||||
|
||||
func TestLoadCurrentStateMissingRunPointer(t *testing.T) {
|
||||
func TestLoadCurrentStateRequiresCommitPointer(t *testing.T) {
|
||||
store := &storage.FakeBackend{}
|
||||
_, err := LoadCurrentState(context.Background(), store, testCurrentSessionPrefix(), CurrentStateValidation{})
|
||||
if err == nil {
|
||||
t.Fatal("LoadCurrentState() error = nil, want missing run pointer error")
|
||||
prefix := S3SessionPrefix("dnd", "sample-campaign", "2026-05-03")
|
||||
_, err := LoadCurrentState(context.Background(), store, prefix, CurrentStateValidation{})
|
||||
var missing *CurrentCommitPointerMissingError
|
||||
if !errors.As(err, &missing) || !errors.Is(err, ErrCurrentCommitPointerMissing) {
|
||||
t.Fatalf("LoadCurrentState() error = %v, want missing commit pointer", err)
|
||||
}
|
||||
var missing *CurrentRunPointerMissingError
|
||||
if !errors.As(err, &missing) {
|
||||
t.Fatalf("errors.As(err, *CurrentRunPointerMissingError) = false; err=%v", err)
|
||||
}
|
||||
if !errors.Is(err, ErrCurrentRunPointerMissing) {
|
||||
t.Fatalf("errors.Is(err, ErrCurrentRunPointerMissing) = false; err=%v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadCurrentStateMissingManifest(t *testing.T) {
|
||||
store := &storage.FakeBackend{}
|
||||
_, _, runIDKey := testCurrentStateKeys()
|
||||
store.SeedObject(storage.FakeObject{Key: runIDKey, Data: []byte("20260519T010203Z-a1b2c3d4\n")})
|
||||
|
||||
_, err := LoadCurrentState(context.Background(), store, testCurrentSessionPrefix(), CurrentStateValidation{})
|
||||
if err == nil {
|
||||
t.Fatal("LoadCurrentState() error = nil, want missing manifest error")
|
||||
}
|
||||
var missing *CurrentManifestMissingError
|
||||
if !errors.As(err, &missing) {
|
||||
t.Fatalf("errors.As(err, *CurrentManifestMissingError) = false; err=%v", err)
|
||||
}
|
||||
if !errors.Is(err, ErrCurrentManifestMissing) {
|
||||
t.Fatalf("errors.Is(err, ErrCurrentManifestMissing) = false; err=%v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadCurrentStateEmptyRunPointerFails(t *testing.T) {
|
||||
store := &storage.FakeBackend{}
|
||||
_, manifestKey, runIDKey := testCurrentStateKeys()
|
||||
store.SeedObject(storage.FakeObject{Key: runIDKey, Data: []byte(" \n\t")})
|
||||
store.SeedObject(storage.FakeObject{Key: manifestKey, Data: testManifestJSON(t, "2026-05-03", "sample-campaign", "20260519T010203Z-a1b2c3d4")})
|
||||
|
||||
_, err := LoadCurrentState(context.Background(), store, testCurrentSessionPrefix(), CurrentStateValidation{})
|
||||
if err == nil || !strings.Contains(err.Error(), "is empty") {
|
||||
t.Fatalf("error = %v, want empty run pointer failure", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadCurrentStateMalformedManifestFails(t *testing.T) {
|
||||
store := &storage.FakeBackend{}
|
||||
_, manifestKey, runIDKey := testCurrentStateKeys()
|
||||
store.SeedObject(storage.FakeObject{Key: runIDKey, Data: []byte("20260519T010203Z-a1b2c3d4\n")})
|
||||
store.SeedObject(storage.FakeObject{Key: manifestKey, Data: []byte("{invalid json")})
|
||||
|
||||
_, err := LoadCurrentState(context.Background(), store, testCurrentSessionPrefix(), CurrentStateValidation{})
|
||||
if err == nil || !strings.Contains(err.Error(), "current manifest decode failed") {
|
||||
t.Fatalf("error = %v, want manifest decode failure", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadCurrentStateCampaignMismatchFails(t *testing.T) {
|
||||
store := &storage.FakeBackend{}
|
||||
seedCurrentState(t, store, "2026-05-03", "wrong-campaign", "20260519T010203Z-a1b2c3d4")
|
||||
|
||||
_, err := LoadCurrentState(context.Background(), store, testCurrentSessionPrefix(), CurrentStateValidation{
|
||||
ExpectedCampaign: "sample-campaign",
|
||||
})
|
||||
if err == nil || !strings.Contains(err.Error(), "does not match expected campaign") {
|
||||
t.Fatalf("error = %v, want campaign mismatch failure", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadCurrentStateSessionMismatchFails(t *testing.T) {
|
||||
store := &storage.FakeBackend{}
|
||||
seedCurrentState(t, store, "wrong-session", "sample-campaign", "20260519T010203Z-a1b2c3d4")
|
||||
|
||||
_, err := LoadCurrentState(context.Background(), store, testCurrentSessionPrefix(), CurrentStateValidation{
|
||||
ExpectedSessionID: "2026-05-03",
|
||||
})
|
||||
if err == nil || !strings.Contains(err.Error(), "does not match expected session_id") {
|
||||
t.Fatalf("error = %v, want session mismatch failure", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadCurrentStateRunIDMismatchFails(t *testing.T) {
|
||||
store := &storage.FakeBackend{}
|
||||
seedCurrentState(t, store, "2026-05-03", "sample-campaign", "different-run-id")
|
||||
|
||||
_, err := LoadCurrentState(context.Background(), store, testCurrentSessionPrefix(), CurrentStateValidation{
|
||||
ValidateRunID: true,
|
||||
})
|
||||
if err == nil || !strings.Contains(err.Error(), "current manifest run_id") {
|
||||
t.Fatalf("error = %v, want run mismatch failure", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadCurrentStateReadsCoherentLegacyPair(t *testing.T) {
|
||||
store := &storage.FakeBackend{}
|
||||
seedCurrentState(t, store, "2026-05-03", "sample-campaign", "20260519T010203Z-a1b2c3d4")
|
||||
|
||||
state, err := LoadCurrentState(context.Background(), store, testCurrentSessionPrefix(), CurrentStateValidation{
|
||||
ExpectedCampaign: "sample-campaign",
|
||||
ExpectedSessionID: "2026-05-03",
|
||||
ValidateRunID: true,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("LoadCurrentState() error = %v", err)
|
||||
}
|
||||
if state.Commit != nil || state.Pointer != nil {
|
||||
t.Fatalf("legacy current state unexpectedly includes immutable commit data: %#v", state)
|
||||
}
|
||||
if len(store.Downloads) != 0 {
|
||||
t.Fatalf("legacy current-state downloads = %#v, want direct bounded reads", store.Downloads)
|
||||
}
|
||||
wantReads := []string{state.CurrentRunIDKey, state.CurrentManifestKey}
|
||||
if len(store.Reads) != len(wantReads) {
|
||||
t.Fatalf("legacy current-state reads = %#v, want %q", store.Reads, wantReads)
|
||||
}
|
||||
for index, want := range wantReads {
|
||||
if store.Reads[index].Key != want {
|
||||
t.Fatalf("legacy current-state read %d = %q, want %q", index, store.Reads[index].Key, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCurrentStateControlObjectLimitsAcceptExactAndRejectLimitPlusOne(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
category string
|
||||
key string
|
||||
limit int64
|
||||
}{
|
||||
{name: "commit pointer", category: "current commit pointer", key: "current/commit-pointer.json", limit: MaxCurrentCommitPointerBytes},
|
||||
{name: "commit manifest", category: "remote commit manifest", key: "runs/run/commit.json", limit: MaxRemoteCommitManifestBytes},
|
||||
{name: "session manifest", category: "committed session manifest", key: "runs/run/session-manifest.json", limit: MaxRemoteSessionManifestBytes},
|
||||
{name: "legacy run pointer", category: "legacy current run pointer", key: "current/run_id.txt", limit: MaxLegacyCurrentRunPointerBytes},
|
||||
{name: "legacy manifest", category: "legacy current manifest", key: "current/manifest.json", limit: MaxLegacyCurrentManifestBytes},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
store := &storage.FakeBackend{}
|
||||
exact := strings.Repeat("x", int(test.limit))
|
||||
store.SeedObject(storage.FakeObject{Key: test.key, Data: []byte(exact)})
|
||||
_, data, err := readCurrentStateControlObject(context.Background(), store, test.key, test.category, test.limit)
|
||||
if err != nil || string(data) != exact {
|
||||
t.Fatalf("exact-limit read data length=%d error=%v", len(data), err)
|
||||
}
|
||||
|
||||
store.SeedObject(storage.FakeObject{Key: test.key, Data: []byte(exact + "x")})
|
||||
_, _, err = readCurrentStateControlObject(context.Background(), store, test.key, test.category, test.limit)
|
||||
if err == nil || !strings.Contains(err.Error(), test.category) || !strings.Contains(err.Error(), test.key) || !strings.Contains(err.Error(), fmt.Sprint(test.limit)) {
|
||||
t.Fatalf("limit-plus-one error = %v, want category, key, and limit", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadCurrentStateRejectsTornLegacyPair(t *testing.T) {
|
||||
store := &storage.FakeBackend{}
|
||||
seedCurrentState(t, store, "2026-05-03", "sample-campaign", "different-run-id")
|
||||
|
||||
_, err := LoadCurrentState(context.Background(), store, testCurrentSessionPrefix(), CurrentStateValidation{ValidateRunID: true})
|
||||
if err == nil || !strings.Contains(err.Error(), "current manifest run_id") {
|
||||
t.Fatalf("error = %v, want torn legacy pair rejection", err)
|
||||
if missing.Key != S3CurrentCommitPointerKey(prefix) {
|
||||
t.Fatalf("missing pointer key = %q", missing.Key)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -180,32 +27,14 @@ func testCurrentSessionPrefix() string {
|
||||
return S3SessionPrefix("dnd", "sample-campaign", "2026-05-03")
|
||||
}
|
||||
|
||||
func testCurrentStateKeys() (sessionPrefix, manifestKey, runIDKey string) {
|
||||
sessionPrefix = testCurrentSessionPrefix()
|
||||
manifestKey, runIDKey = ResolveCurrentStateKeys(sessionPrefix)
|
||||
return sessionPrefix, manifestKey, runIDKey
|
||||
}
|
||||
|
||||
func seedCurrentState(t *testing.T, store *storage.FakeBackend, sessionID, campaign, manifestRunID string) {
|
||||
t.Helper()
|
||||
_, manifestKey, runIDKey := testCurrentStateKeys()
|
||||
store.SeedObject(storage.FakeObject{Key: runIDKey, Data: []byte("20260519T010203Z-a1b2c3d4\n")})
|
||||
store.SeedObject(storage.FakeObject{Key: manifestKey, Data: testManifestJSON(t, sessionID, campaign, manifestRunID)})
|
||||
}
|
||||
|
||||
func testManifestJSON(t *testing.T, sessionID, campaign, runID string) []byte {
|
||||
t.Helper()
|
||||
now := time.Date(2026, 5, 19, 23, 0, 0, 0, time.UTC).Format(time.RFC3339Nano)
|
||||
payload := map[string]any{
|
||||
"schema_version": "narratio.session-manifest.v1",
|
||||
"session_id": sessionID,
|
||||
"campaign": campaign,
|
||||
"run_id": runID,
|
||||
"created_at": now,
|
||||
"updated_at": now,
|
||||
"stages": map[string]any{},
|
||||
}
|
||||
data, err := json.Marshal(payload)
|
||||
data, err := json.Marshal(map[string]any{
|
||||
"schema_version": "narratio.session-manifest.v1", "session_id": sessionID,
|
||||
"campaign": campaign, "run_id": runID, "created_at": now, "updated_at": now,
|
||||
"stages": map[string]any{},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("marshal manifest payload: %v", err)
|
||||
}
|
||||
|
||||
@@ -76,8 +76,3 @@ func ResolvePublishRunPrefix(cfg *config.Config, m *manifest.Manifest) (string,
|
||||
}
|
||||
return S3RunPrefix(sessionPrefix, runID), nil
|
||||
}
|
||||
|
||||
// ResolveCurrentStateKeys returns current pointer keys for a session prefix.
|
||||
func ResolveCurrentStateKeys(sessionPrefix string) (manifestKey, runIDKey string) {
|
||||
return S3CurrentManifestKey(sessionPrefix), S3CurrentRunPointerKey(sessionPrefix)
|
||||
}
|
||||
|
||||
@@ -118,13 +118,3 @@ func TestResolvePublishIdentityErrorsAreDeterministic(t *testing.T) {
|
||||
t.Fatalf("error = %v, want missing run id", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveCurrentStateKeys(t *testing.T) {
|
||||
manifestKey, runIDKey := ResolveCurrentStateKeys("dnd/campaigns/forsaken/sessions/2026-04-19/")
|
||||
if manifestKey != "dnd/campaigns/forsaken/sessions/2026-04-19/current/manifest.json" {
|
||||
t.Fatalf("manifest key = %q", manifestKey)
|
||||
}
|
||||
if runIDKey != "dnd/campaigns/forsaken/sessions/2026-04-19/current/run_id.txt" {
|
||||
t.Fatalf("run id key = %q", runIDKey)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,18 +46,6 @@ func S3SessionLocksKey(sessionPrefix string) string {
|
||||
return path.Join(strings.TrimSuffix(cleanS3Key(sessionPrefix), "/"), "locks.yml")
|
||||
}
|
||||
|
||||
// S3CurrentManifestKey returns the current manifest pointer key.
|
||||
// Format: {session_prefix}/current/manifest.json
|
||||
func S3CurrentManifestKey(sessionPrefix string) string {
|
||||
return path.Join(strings.TrimSuffix(cleanS3Key(sessionPrefix), "/"), config.S3CurrentSegment, config.S3ManifestFile)
|
||||
}
|
||||
|
||||
// S3CurrentRunPointerKey returns the current run pointer key.
|
||||
// Format: {session_prefix}/current/run_id.txt
|
||||
func S3CurrentRunPointerKey(sessionPrefix string) string {
|
||||
return path.Join(strings.TrimSuffix(cleanS3Key(sessionPrefix), "/"), config.S3CurrentSegment, config.S3RunIDFile)
|
||||
}
|
||||
|
||||
// S3RunCommitKey returns the immutable commit manifest key for one run.
|
||||
// Format: {session_prefix}/runs/{run_id}/commit.json
|
||||
func S3RunCommitKey(sessionPrefix, runID string) string {
|
||||
|
||||
@@ -39,16 +39,6 @@ func TestS3KeyConstruction(t *testing.T) {
|
||||
t.Fatalf("session manifest key = %q, want %q", got, want)
|
||||
}
|
||||
|
||||
runPointer := S3CurrentRunPointerKey(sessionPrefix)
|
||||
if runPointer != "dnd/campaigns/forsaken/sessions/2026-04-19/current/run_id.txt" {
|
||||
t.Fatalf("run pointer key = %q", runPointer)
|
||||
}
|
||||
|
||||
manifestKey := S3CurrentManifestKey(sessionPrefix)
|
||||
if manifestKey != "dnd/campaigns/forsaken/sessions/2026-04-19/current/manifest.json" {
|
||||
t.Fatalf("manifest key = %q", manifestKey)
|
||||
}
|
||||
|
||||
commitPointer := S3CurrentCommitPointerKey(sessionPrefix)
|
||||
if commitPointer != "dnd/campaigns/forsaken/sessions/2026-04-19/current/commit-pointer.json" {
|
||||
t.Fatalf("commit pointer key = %q", commitPointer)
|
||||
|
||||
Reference in New Issue
Block a user