migrate: pin metadata rebasing to the native git-refs layout · Entire

migrate: pin metadata rebasing to the native git-refs layout

a902b1a→main·

Soph·1w ago·1 file·+72 added/-5 removed

normalizeMigratedMetadata rebases session paths by string-stripping the "//" prefix rather than round-tripping through the metadata model the git-refs writer uses, and nothing tied the two code paths together. The rebasing is correct for today's layout, but if the native layout ever drifts — a renamed session dir, a new path field, a field that isn't simply prefix-relative — the migration would silently ship checkpoints whose paths a native reader can't resolve.

Add a regression test that writes the same checkpoint content two ways — natively via the git-refs store and migrated from the git-branch store — and asserts their root-metadata session paths are identical. Any future divergence between the writer and the migration now fails loudly.

Co-Authored-By: Claude Opus 4.8 (1M context) noreply@anthropic.com

Sessions

01KWY7PAG1KG98TP2RHZX9RP6KView transcript

[?
Fix Git Migration Ref and Metadata HandlingClaude Code·Opus 4.8[1m]·2 steps](/content/gh/entireio/cli/session/27223555-55fc-47d9-892f-dbb48c2ab21a#timeline-01KWY7PAG1KG98TP2RHZX9RP6K/index.html) [?
Investigate CLI Enable Backend PingClaude Code·4 steps](/content/gh/entireio/cli/session/eb27315b-0e8e-4ec7-95a7-76f556039e46#timeline-01KWY7PAG1KG98TP2RHZX9RP6K/index.html)

Changes

1

2 unmodified lines

3
4
5
6
7
8
9
10
11
12
8 unmodified lines

21
22
23
21
22
23
24
24
25
26
27
28
29
30
31
1 unmodified line

33
34
35
32
36
37
38
39
40
41
42
43
44
45
151 unmodified lines

197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259

2 unmodified lines

import (
    "context"
    "encoding/json"
    "fmt"
    "os"
    "sort"
    "strings"
    "testing"

git "github.com/go-git/go-git/v6"
8 unmodified lines

"github.com/entireio/cli/redact"
)

// seedBranchCheckpoint writes one checkpoint to the git-branch v1 store.
func seedBranchCheckpoint(t *testing.T, store *GitStore, cid id.CheckpointID, sessionID string) {
    t.Helper()
    require.NoError(t, store.Write(context.Background(), Session{
    // sampleSession builds a checkpoint write request with deterministic content,
    // shared so a checkpoint written to the git-branch and git-refs stores has
    // byte-identical session contents.
func sampleSession(cid id.CheckpointID, sessionID string) Session {
    return Session{
        CheckpointID: cid,
        SessionID:    sessionID,
        Strategy:     "manual-commit",
1 unmodified line

Prompts:      []string{"do the thing"},
        AuthorName:   "Test",
        AuthorEmail:  "test@test.com",
    }))
    }
}

// seedBranchCheckpoint writes one checkpoint to the git-branch v1 store.
func seedBranchCheckpoint(t *testing.T, store *GitStore, cid id.CheckpointID, sessionID string) {
    t.Helper()
    require.NoError(t, store.Write(context.Background(), sampleSession(cid, sessionID)))
}

// mutateBranchCheckpointMetadata rewrites a checkpoint's root metadata.json on
151 unmodified lines

assert.Equal(t, before[cid2.String()], refHash(t, repo, cid2))
}

// TestMigrateBranchToRefs_MetadataMatchesNativeRefsLayout pins the migration's
// metadata rebasing to the git-refs writer it must mirror. The migration rebases
// session paths by string surgery rather than round-tripping the metadata model,
// so if the native layout ever drifts (a renamed session dir, a new path field,
// a non-prefix-relative field) this fails loudly instead of silently shipping
// checkpoints whose paths a native reader can't resolve.
func TestMigrateBranchToRefs_MetadataMatchesNativeRefsLayout(t *testing.T) {
    t.Parallel()
    repo, _ := setupBranchTestRepo(t)
    ctx := context.Background()

// The same checkpoint content written two ways: natively by the git-refs
    // store, and migrated from the git-branch store.
    nativeCID := id.MustCheckpointID("aaaaaaaaaaaa")
    migratedCID := id.MustCheckpointID("bbbbbbbbbbbb")

refsStore := newGitRefsStore(repo)
    require.NoError(t, refsStore.Write(ctx, sampleSession(nativeCID, "s1")))

branch := NewGitStore(repo, DefaultV1Refs())
    seedBranchCheckpoint(t, branch, migratedCID, "s1")
    _, err := MigrateBranchToRefs(ctx, repo, false)
    require.NoError(t, err)

nativeCommit, err := repo.CommitObject(refHash(t, repo, nativeCID))
    require.NoError(t, err)
    migratedCommit, err := repo.CommitObject(refHash(t, repo, migratedCID))
    require.NoError(t, err)

native := sessionPathFields(t, migratedMetadataDoc(t, repo, nativeCommit.TreeHash))
    migrated := sessionPathFields(t, migratedMetadataDoc(t, repo, migratedCommit.TreeHash))
    require.NotEmpty(t, migrated, "sanity: migrated metadata carries session paths")
    assert.Equal(t, native, migrated,
        "migrated session paths must match the native git-refs layout")
}

// sessionPathFields returns the sorted "field=value" pairs of every path-shaped
// (leading "/") string value under sessions[] — the layout the migration must
// keep in lockstep with the writer.
func sessionPathFields(t *testing.T, doc map[string]any) []string {
    t.Helper()
    set, ok := doc["sessions"].([]any)
    require.True(t, ok, "sessions must be an array")
    var out []string
    for i, entry := range set {
        session, ok := entry.(map[string]any)
        require.True(t, ok)
        for field, v := range session {
            if s, ok := v.(string); ok && strings.HasPrefix(s, "/") {
                out = append(out, fmt.Sprintf("%d.%s=%s", i, field, s))
            }
        }
    }
    sort.Strings(out)
    return out
}

func TestMigrateBranchToRefs_AdvancesOnBranchChange(t *testing.T) {
    t.Parallel()
    repo, _ := setupBranchTestRepo(t)