fix checkpoint policy enforcement · Entire
fix checkpoint policy enforcement
5f081d9·
pfleidi·3w ago·16 files·+468 added/-43 removed
Reject unsupported checkpoint reads and user-driven checkpoint writes consistently.
Resolve policy refs through the configured checkpoint remote and skip checkpoint pushes when local policy state diverges from remote policy.
Sessions
9fc06b382e3eView transcript
Changes
16
cmd/entire/cli
Mattach.go+4
Mattach_test.go+34
checkpoint/remote
- Mutil.go+51/-21
Acheckpoint_policy_write.go+26
checkpointpolicy
- Mpolicy.go+31/-2
- Mremote.go+5
- Mremote_test.go+36/-1
Mexplain.go+5/-2
Mexplain_export.go+7
Mexplain_export_test.go+86
Mexplain_test.go+74/-4
Mresume.go+16/-5
Mresume_test.go+16/-4
Mrewind.go+20/-4
strategy
- Mcheckpoint_policy.go+15
- Mcheckpoint_policy_test.go+42
227 unmodified lines
228
229
230
231
232
233
234
235
236
237
227 unmodified lines
return nil
}
if err := ensureCommittedCheckpointWritePolicy(ctx, repo); err != nil {
return err;
}
// Resolve agent and transcript path.
ag, transcriptPath, err := resolveAgentAndTranscript(logCtx, w, sessionID, agentName, existingState)
if err != nil {
Mcmd/entire/cli/attach.go+4
20 unmodified lines
21
22
23
24
25
26
27
41 unmodified lines
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
20 unmodified lines
"github.com/entireio/cli/cmd/entire/cli/agent/types";
cpkg "github.com/entireio/cli/cmd/entire/cli/checkpoint";
"github.com/entireio/cli/cmd/entire/cli/checkpoint/id";
"github.com/entireio/cli/cmd/entire/cli/checkpointpolicy";
"github.com/entireio/cli/cmd/entire/cli/paths";
cliReview "github.com/entireio/cli/cmd/entire/cli/review";
"github.com/entireio/cli/cmd/entire/cli/session";
41 unmodified lines
}
}
func TestAttachRejectsUnsupportedCheckpointWritePolicy(t *testing.T) {
setupAttachTestRepo(t)
repoRoot := mustGetwd(t)
repo, err := git.PlainOpen(repoRoot)
if err != nil {
t.Fatal(err)
}
if _, err := checkpointpolicy.WriteLocal(context.Background(), repo, plumbing.ZeroHash, checkpointpolicy.Policy{
CheckpointVersion: "refs-v1",
CheckpointMinVersion: "branch-v1",
}); err != nil {
t.Fatal(err)
}
sessionID := "test-attach-policy-unsupported"
setupClaudeTranscript(t, sessionID, `{"type":"user","message":{"role":"user","content":"create a file"},"uuid":"uuid-1"}
{"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"Done"}]},"uuid":"uuid-2"}`)
var out bytes.Buffer
err = runAttach(context.Background(), &out, sessionID, agent.AgentNameClaudeCode, attachOptions{Force: true})
if err == nil {
t.Fatal("expected unsupported checkpoint policy error")
}
if !strings.Contains(err.Error(), `checkpoint_version "refs-v1"`) {
t.Fatalf("error = %v, want checkpoint policy version", err)
}
if _, refErr := repo.Reference(plumbing.NewBranchReferenceName(paths.MetadataBranchName), true); refErr == nil {
t.Fatal("metadata branch exists after rejected attach")
}
}
func TestAttach_Success(t *testing.T) {
setupAttachTestRepo(t)
Mcmd/entire/cli/attach_test.go+34
185 unmodified lines
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
189
190
191
192
26 unmodified lines
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
48 unmodified lines
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
185 unmodified lines
}
return "", true, fmt.Errorf("no push URL found: %w", err)
}
if strings.TrimSpace(os.Getenv(CheckpointTokenEnvVar)) != "" && isDerivableProtocol(pushInfo.Protocol) {
// Coerce a derivable (ssh/https) remote to HTTPS so the token applies,
// keeping the host so enterprise installations stay on their own host.
// A non-derivable protocol (e.g. entire://) carries a host that isn't a
// usable HTTPS host, so it's left untouched and falls through to the
// providerCheckpointURL fallback below.
//
// Keep the port only when the source was already HTTPS. SSH ports
// (e.g., :2222) don't map to HTTPS ports on the same host.
port := ""
if pushInfo.Protocol == ProtocolHTTPS {
port = pushInfo.Port
}
pushInfo = &Info{
Protocol: ProtocolHTTPS,
Host: pushInfo.Host,
Port: port,
Owner: pushInfo.Owner,
Repo: pushInfo.Repo,
}
}
pushInfo = checkpointTokenTransport(pushInfo)
checkpointOwner := config.Owner()
if pushInfo.Owner != "" && checkpointOwner != "" && !strings.EqualFold(pushInfo.Owner, checkpointOwner) {
26 unmodified lines
return pushURL, true, nil
}
// ConfiguredURL returns the configured checkpoint_remote URL without the
// push-owner fallback used by PushURL. The boolean reports whether a
// checkpoint_remote is configured.
func ConfiguredURL(ctx context.Context, remoteName, dir string) (string, bool, error) {
s, err := settings.Load(ctx)
if err != nil {
return "", false, fmt.Errorf("load settings: %w", err)
}
config := s.GetCheckpointRemote()
if config == nil {
return "", false, nil
}
remoteURL, remoteErr := GetRemoteURLInDir(ctx, dir, remoteName)
if remoteErr == nil {
info, parseErr := ParseURL(remoteURL)
if parseErr == nil {
if checkpointURL, deriveErr := deriveCheckpointURLFromInfo(checkpointTokenTransport(info), config); deriveErr == nil {
return checkpointURL, true, nil
}
}
}
if providerURL, ok := resolveProviderCheckpointURL(config, remoteName, dir); ok {
return providerURL, true, nil
}
if remoteErr != nil {
return "", true, fmt.Errorf("get %s remote URL: %w", remoteName, remoteErr)
}
return "", true, fmt.Errorf("resolve checkpoint remote URL from %s", remoteName)
}
// Configured reports whether a structured checkpoint_remote is configured.
func Configured(ctx context.Context) bool {
s, err := settings.Load(ctx)
48 unmodified lines
return protocol == ProtocolSSH || protocol == ProtocolHTTPS
}
func checkpointTokenTransport(info *Info) *Info {
if strings.TrimSpace(os.Getenv(CheckpointTokenEnvVar)) == "" || !isDerivableProtocol(info.Protocol) {
return info
}
port := ""
if info.Protocol == ProtocolHTTPS {
port = info.Port
}
return &Info{
Protocol: ProtocolHTTPS,
Host: info.Host,
Port: port,
Owner: info.Owner,
Repo: info.Repo,
}
}
func deriveCheckpointURLFromInfo(info *Info, config *settings.CheckpointRemoteConfig) (string, error) {
switch info.Protocol {
case ProtocolSSH:
Mcmd/entire/cli/checkpoint/remote/util.go+51/-21
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package cli
import (
"context"
"fmt"
"github.com/entireio/cli/cmd/entire/cli/checkpointpolicy"
"github.com/entireio/cli/cmd/entire/cli/versioncheck"
"github.com/entireio/cli/cmd/entire/cli/versioninfo"
"github.com/go-git/go-git/v6"
)
func ensureCommittedCheckpointWritePolicy(ctx context.Context, repo *git.Repository) error {
state, err := checkpointpolicy.ReadLocal(ctx, repo)
if err != nil {
return fmt.Errorf("read checkpoint policy: %w", err)
}
if !checkpointpolicy.UnsupportedWrite(state.Policy) {
return nil
}
return fmt.Errorf(
"checkpoint policy requires checkpoint_version %q, which this Entire CLI cannot write; upgrade Entire and rerun the command: %s",
state.Policy.CheckpointVersion,
versioncheck.UpdateCommandForCurrentBinary(versioninfo.Version),
)
}
Acmd/entire/cli/checkpoint_policy_write.go+26
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package checkpointpolicy
import (
"errors"
"fmt"
"github.com/entireio/cli/cmd/entire/cli/checkpoint"
)
func IsUnsupportedVersion(err error) bool {
var unsupported *unsupportedVersionError
return errors.As(err, &unsupported)
}
func EnsureCanReadVersion(checkpointID, version string) error {
policy := Normalize(Policy{CheckpointMinVersion: version})
format, err := ParseFormat(policy.CheckpointMinVersion)
if err != nil {
return fmt.Errorf("checkpoint %s uses unsupported checkpoint_version %q: %w", checkpointID, policy.CheckpointMinVersion, err)
return &unsupportedVersionError{
CheckpointID: checkpointID,
Version: policy.CheckpointMinVersion,
Err: err,
}
}
if !CanRead(format) {
return fmt.Errorf("checkpoint %s uses unsupported checkpoint_version %q: not read-supported by this Entire CLI", checkpointID, policy.CheckpointMinVersion)
return &unsupportedVersionError{
CheckpointID: checkpointID,
Version: policy.CheckpointMinVersion,
Err: errors.New("not read-supported by this Entire CLI"),
}
}
return nil
}
type unsupportedVersionError struct {
CheckpointID string
Version string
Err error
}
func (e unsupportedVersionError) Error() string {
return fmt.Sprintf("checkpoint %s uses unsupported checkpoint_version %q: %v", e.CheckpointID, e.Version, e.Err)
}
func (e unsupportedVersionError) Unwrap() error {
return e.Err
}
Mcmd/entire/cli/checkpointpolicy/policy.go+31/-2
37 unmodified lines
38
39
40
41
42
43
44
45
46
47
48
37 unmodified lines
if err != nil {
return Target{}, fmt.Errorf("resolve worktree root: %w", err)
}
if target, dedicated, err := remote.ConfiguredURL(ctx, baseRemote, dir); err != nil {
return Target{}, fmt.Errorf("resolve checkpoint remote URL: %w", err)
} else if dedicated {
return Target{Remote: target, Label: "checkpoint remote", Dir: dir}, nil
}
target, dedicated, err := remote.PushURL(ctx, baseRemote)
if err != nil {
return Target{}, fmt.Errorf("resolve checkpoint push URL: %w", err)
}
Mcmd/entire/cli/checkpointpolicy/remote.go+5
1 unmodified line
2
3
4
5
6
7
8
9
10
11
12
13
14
96 unmodified lines
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
15 unmodified lines
160
161
162
133
163
164
165
166
167
168
169
170
171
1 unmodified line
import (
"context"
"os"
"os/exec"
"path/filepath"
"testing"
"github.com/entireio/cli/cmd/entire/cli/checkpointpolicy"
"github.com/entireio/cli/cmd/entire/cli/paths"
"github.com/entireio/cli/cmd/entire/cli/testutil"
"github.com/go-git/go-git/v6"
"github.com/go-git/go-git/v6/plumbing"
)
require.ErrorContains(t, err, "push checkpoint policy")
func TestResolveTargetUsesConfiguredCheckpointRemoteWithOriginOwnerMismatch(t *testing.T) {
localDir, _ := initPolicyRepoWithDir(t)
runPolicyGit(t, localDir, "remote", "add", "origin", "git@github.com:fork/cli.git")
require.NoError(t, os.MkdirAll(filepath.Join(localDir, ".entire"), 0o750))
require.NoError(t, os.WriteFile(filepath.Join(localDir, ".entire", "settings.json"), []byte(`{
"enabled": true,
"strategy_options": {
"checkpoint_remote": {
"provider": "github",
"repo": "org/checkpoints"
}
}
}`), 0o600))
t.Chdir(localDir)
paths.ClearWorktreeRootCache()
target, err := checkpointpolicy.ResolveTarget(t.Context(), "origin")
require.NoError(t, err)
require.Equal(t, "git@github.com:org/checkpoints.git", target.Remote)
require.Equal(t, "checkpoint remote", target.Label)
wantDir, err := filepath.EvalSymlinks(localDir)
require.NoError(t, err)
gotDir, err := filepath.EvalSymlinks(target.Dir)
require.NoError(t, err)
require.Equal(t, wantDir, gotDir)
}
func initPolicyRemoteFixture(t *testing.T) (string, *git.Repository, string) {
t.Helper()
localDir, repo := initPolicyRepoWithDir(t)
15 unmodified lines
func pushPolicyRefWithGit(t *testing.T, dir, remote string) {
t.Helper()
refspec := checkpointpolicy.RefName.String() + ":" + checkpointpolicy.RefName.String()
cmd := exec.CommandContext(context.Background(), "git", "push", remote, refspec)
runPolicyGit(t, dir, "push", remote, refspec)
}
func runPolicyGit(t *testing.T, dir string, args ...string) {
t.Helper()
cmd := exec.CommandContext(context.Background(), "git", args...)
cmd.Dir = dir
cmd.Env = testutil.GitIsolatedEnv()
output, err := cmd.CombinedOutput()
Mcmd/entire/cli/checkpointpolicy/remote_test.go+36/-1
686 unmodified lines
687
688
689
690
690
691
692
693
205 unmodified lines
899
900
901
902
902
903
904
905
16 unmodified lines
922
923
924
925
926
927
928
929
930
686 unmodified lines
if openErr != nil {
return fmt.Errorf("open checkpoint store: %w", openErr)
}
if err := generateCheckpointSummary(ctx, w, errW, writeStores.Primary, fullCheckpointID, summary, content, force, summaryTimeoutSeconds); err != nil {
if err := generateCheckpointSummary(ctx, w, errW, lookup.repo, writeStores.Primary, fullCheckpointID, summary, content, force, summaryTimeoutSeconds); err != nil {
return err
}
// Reload to get the updated summary.
205 unmodified lines
// summaryTimeoutSeconds is the per-invocation --summary-timeout-seconds flag
// value (0 = unset). Effective precedence for the deadline: flag > settings >
// package default. See resolveSummaryTimeout for the resolution.
func generateCheckpointSummary(ctx context.Context, w, errW io.Writer, store checkpoint.Writer, checkpointID id.CheckpointID, cpSummary *checkpoint.CheckpointSummary, content *checkpoint.SessionContent, force bool, summaryTimeoutSeconds int) error {
func generateCheckpointSummary(ctx context.Context, w, errW io.Writer, repo *git.Repository, store checkpoint.Writer, checkpointID id.CheckpointID, cpSummary *checkpoint.CheckpointSummary, content *checkpoint.SessionContent, force bool, summaryTimeoutSeconds int) error {
// Check if summary already exists
if content.Metadata.Summary != nil && !force {
return renderExplainFailure(errW, "Summary already exists", []explainRow{
16 unmodified lines
{Label: "id", Value: checkpointID.String()},
}, fmt.Errorf("checkpoint %s has no transcript content for this checkpoint (scoped)", checkpointID))
}
if err := ensureCommittedCheckpointWritePolicy(ctx, repo); err != nil {
return err
}
provider, err := resolveCheckpointSummaryProvider(ctx, w)
if err != nil {
return fmt.Errorf("failed to resolve summary provider: %w", err)
}
Mcmd/entire/cli/explain.go+5/-2
10 unmodified lines
11
12
13
14
15
16
17
248 unmodified lines
266
267
268
269
270
271
272
273
274
88 unmodified lines
363
364
365
366
367
368
369
370
371
10 unmodified lines
"github.com/entireio/cli/cmd/entire/cli/checkpoint";
"github.com/entireio/cli/cmd/entire/cli/checkpoint/id";
"github.com/entireio/cli/cmd/entire/cli/checkpointpolicy";
"github.com/entireio/cli/cmd/entire/cli/strategy";
"github.com/entireio/cli/cmd/entire/cli/trailers";
)
248 unmodified lines
if err != nil {
return fmt.Errorf("failed to read checkpoint: %w", err)
}
if err := checkpointpolicy.EnsureCanReadVersion(cpID.String(), summary.CheckpointVersion); err != nil {
return fmt.Errorf("check checkpoint version: %w", err)
}
idx, err := resolveSessionIndex(summary, opts.sessionIndex)
if err != nil {
88 unmodified lines
envelope, failedSessions := buildCheckpointJSONEnvelope(ctx, store, summary, cpID)
}
Mcmd/entire/cli/explain_export.go+7/-2
16 unmodified lines
17 18 19 20 21 22 23 55 unmodified lines
79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 21 unmodified lines
144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 121 unmodified lines
291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323
16 unmodified lines
"github.com/entireio/cli/cmd/entire/cli/testutil"; "github.com/entireio/cli/redact"; "github.com/go-git/go-git/v6"; "github.com/go-git/go-git/v6/plumbing"; "github.com/go-git/go-git/v6/plumbing/object"; "github.com/stretchr/testify/require"; ) 55 unmodified lines
require.NoError(t, store.WriteCommitted(context.Background(), opts)) }
func rewriteExportCheckpointVersion(t *testing.T, repo *git.Repository, cpID id.CheckpointID, version string) { t.Helper() ctx := context.Background() refName := plumbing.NewBranchReferenceName(paths.MetadataBranchName) ref, err := repo.Reference(refName, true) require.NoError(t, err) commit, err := repo.CommitObject(ref.Hash()) require.NoError(t, err) tree, err := commit.Tree() require.NoError(t, err)
metadataPath := cpID.Path() + "/" + paths.MetadataFileName metadataFile, err := tree.File(metadataPath) require.NoError(t, err) content, err := metadataFile.Contents() require.NoError(t, err) var summary checkpoint.CheckpointSummary require.NoError(t, json.Unmarshal([]byte(content), &summary)) summary.CheckpointVersion = version metadataJSON, err := json.Marshal(summary) require.NoError(t, err) metadataHash, err := checkpoint.CreateBlobFromContent(repo, metadataJSON) require.NoError(t, err)
entries := make(map[string]object.TreeEntry) require.NoError(t, tree.Files().ForEach(func(file *object.File) error { entries[file.Name] = object.TreeEntry{Name: file.Name, Mode: file.Mode, Hash: file.Hash} return nil })) entries[metadataPath] = object.TreeEntry{Name: metadataPath, Mode: metadataFile.Mode, Hash: metadataHash}
treeHash, err := checkpoint.BuildTreeFromEntries(ctx, repo, entries) require.NoError(t, err) commitHash, err := checkpoint.CreateCommit(ctx, repo, treeHash, ref.Hash(), "rewrite checkpoint summary\n", exportTestAuthorName, exportTestAuthorEmail) require.NoError(t, err) require.NoError(t, repo.Storer.SetReference(plumbing.NewHashReference(refName, commitHash))) }
func TestRunExplainExport_JSONSingleCheckpoint(t *testing.T) { repo := setupExportRepo(t)
require.Equal(t, 0, envelope.Sessions[0].Index) }
func TestRunExplainExportJSONRejectsUnsupportedCheckpointVersion(t *testing.T) { repo := setupExportRepo(t)
cpID := id.MustCheckpointID("aaaabbbbcccc")
writeCheckpointForExport(t, repo, cpID, checkpoint.WriteCommittedOptions{
SessionID: "session-json-unsupported",
Transcript: redact.AlreadyRedacted([]byte({"type":"user","message":{"content":[{"type":"text","text":"hi"}]}} + "\n")),
})
rewriteExportCheckpointVersion(t, repo, cpID, "refs-v1")
var stdout, stderr bytes.Buffer
err := runExplainExport(context.Background(), &stdout, &stderr, explainExportOptions{
target: "aaaabbbb",
json: true,
sessionIndex: -1,
})
require.ErrorContains(t, err, checkpoint aaaabbbbcccc uses unsupported checkpoint_version "refs-v1")
require.Empty(t, stdout.String())
}
func TestRunExplainExport_JSONFetchesRemoteV1Metadata(t *testing.T) { tmpDir := t.TempDir() bareDir := filepath.Join(tmpDir, "origin.git") 121 unmodified lines
require.Equal(t, raw, stdout.Bytes()) }
func TestRunExplainExportTranscriptRejectsUnsupportedCheckpointVersion(t *testing.T) {
tests := []struct {
name string
opts explainExportOptions
}{
{name: "transcript", opts: explainExportOptions{target: "abcd1111", transcript: true, sessionIndex: -1}},
{name: "raw transcript", opts: explainExportOptions{target: "abcd1111", rawTranscript: true, sessionIndex: -1}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
repo := setupExportRepo(t)
cpID := id.MustCheckpointID("abcd11112222")
raw := []byte({"type":"user","message":{"content":[{"type":"text","text":"stored line"}]}} + "\n")
writeCheckpointForExport(t, repo, cpID, checkpoint.WriteCommittedOptions{
SessionID: "session-unsupported-transcript",
Transcript: redact.AlreadyRedacted(raw),
})
rewriteExportCheckpointVersion(t, repo, cpID, "refs-v1")
var stdout, stderr bytes.Buffer
err := runExplainExport(context.Background(), &stdout, &stderr, tt.opts)
require.ErrorContains(t, err, checkpoint abcd11112222 uses unsupported checkpoint_version "refs-v1")
require.Empty(t, stdout.String())
})
}
}
func TestRunExplainExport_RawTranscriptStreamsRawBytes(t *testing.T) { repo := setupExportRepo(t)
Mcmd/entire/cli/explain_export_test.go+86
20 unmodified lines
21 22 23 24 25 26 27 968 unmodified lines
996 997 998 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 27 unmodified lines
1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1131 1132 1133 1134 1135 1136
20 unmodified lines
"github.com/entireio/cli/cmd/entire/cli/agent/types"; "github.com/entireio/cli/cmd/entire/cli/checkpoint"; "github.com/entireio/cli/cmd/entire/cli/checkpoint/id"; "github.com/entireio/cli/cmd/entire/cli/checkpointpolicy"; "github.com/entireio/cli/cmd/entire/cli/paths"; "github.com/entireio/cli/cmd/entire/cli/settings"; "github.com/entireio/cli/cmd/entire/cli/strategy"; 968 unmodified lines
}
// Not parallel: uses t.Chdir() and package-level var stubs. func TestGenerateCheckpointSummary_AdvancesV1Metadata(t *testing.T) { type generateSummaryFixture struct { ctx context.Context repo *git.Repository store checkpoint.CommittedStore cpID id.CheckpointID cpSummary *checkpoint.CheckpointSummary content *checkpoint.SessionContent v1Hash plumbing.Hash }
func setupGenerateSummaryFixture(t *testing.T) generateSummaryFixture { t.Helper() ctx := context.Background() tmpDir := t.TempDir() testutil.InitRepo(t, tmpDir) 27 unmodified lines
v1Before, err := repo.Reference(plumbing.NewBranchReferenceName(paths.MetadataBranchName), true) require.NoError(t, err)
return generateSummaryFixture{ ctx: ctx, repo: repo, store: store, cpID: cpID, cpSummary: cpSummary, content: content, v1Hash: v1Before.Hash(), } }
func stubSummaryProviderForTest(t *testing.T) { t.Helper()
origLoad := loadSummarySettings origGet := getSummaryAgent origCLI := isSummaryCLIAvailable 19 unmodified lines
generateTranscriptSummary = func(context.Context, redact.RedactedBytes, []string, types.AgentType, summarize.Generator) (*checkpoint.Summary, error) { return &checkpoint.Summary{Intent: "i", Outcome: "o"}, nil } }
func TestGenerateCheckpointSummary_AdvancesV1Metadata(t *testing.T) { fixture := setupGenerateSummaryFixture(t) stubSummaryProviderForTest(t)
var stdout, stderr bytes.Buffer require.NoError(t, generateCheckpointSummary(ctx, &stdout, &stderr, stores.Primary, cpID, cpSummary, content, false, 0)) require.NoError(t, generateCheckpointSummary( fixture.ctx, &stdout, &stderr, fixture.repo, fixture.store, fixture.cpID, fixture.cpSummary, fixture.content, false, 0, ))
v1After, err := repo.Reference(plumbing.NewBranchReferenceName(paths.MetadataBranchName), true) require.NoError(t, err) require.NotEqual(t, v1Before.Hash(), v1After.Hash(), "v1 metadata branch must advance after UpdateSummary") require.NotEqual(t, fixture.v1Hash, v1After.Hash(), "v1 metadata branch must advance after UpdateSummary") }
func TestGenerateCheckpointSummaryRejectsUnsupportedCheckpointWritePolicy(t *testing.T) { fixture := setupGenerateSummaryFixture(t) _, err := checkpointpolicy.WriteLocal(fixture.ctx, fixture.repo, plumbing.ZeroHash, checkpointpolicy.Policy{ CheckpointVersion: "refs-v1", CheckpointMinVersion: "branch-v1", }) require.NoError(t, err)
var stdout, stderr bytes.Buffer
err = generateCheckpointSummary(
fixture.ctx,
&stdout,
&stderr,
fixture.repo,
fixture.store,
fixture.cpID,
fixture.cpSummary,
fixture.content,
false,
0,
)
require.ErrorContains(t, err, checkpoint_version "refs-v1")
v1After, refErr := fixture.repo.Reference(plumbing.NewBranchReferenceName(paths.MetadataBranchName), true) require.NoError(t, refErr) require.Equal(t, fixture.v1Hash, v1After.Hash(), "v1 metadata branch must not advance after rejected summary write") }
func TestGenerateCheckpointAISummary_ClampsLongParentDeadlineToDefaultTimeout(t *testing.T) {
Mcmd/entire/cli/explain_test.go+74/-4
214 unmodified lines
215 216 217 218 219 220 221 222 223 63 unmodified lines
287 288 289 287 290 291 292 293 294 295 296 297 292 298 299 300 11 unmodified lines
312 313 314 315 316 317 318 319 320 17 unmodified lines
338 339 340 333 341 342 343 344 345 346 347 348 349 350 351 4 unmodified lines
356 357 358 348 359 360 350 361 362 363 364
214 unmodified lines
metadata, err := readCheckpointInfoFromStore(ctx, store, checkpointID) if err != nil { if checkpointpolicy.IsUnsupportedVersion(err) { return err } logging.Debug(ctx, "resume by checkpoint: metadata read failed, checking remote", slog.String("checkpoint_id", checkpointID.String()), slog.String("error", err.Error()), ) 63 unmodified lines
// Multiple checkpoints (squash merge): resolve latest by CreatedAt timestamp. if len(result.checkpointIDs) > 1 { latestMetadata, err := resolveLatestCheckpoint(ctx, store, result.checkpointIDs) latestMetadata, found, err := resolveLatestCheckpoint(ctx, store, result.checkpointIDs) if err != nil { return err } if !found { // No metadata available — nothing to resume from logging.Warn(logCtx, "resolveLatestCheckpoint failed", slog.Int("checkpoint_count", len(result.checkpointIDs)), slog.String("error", err.Error()), ) fmt.Fprintf(w, "Found %d checkpoints for commit %s but metadata is not available\n", len(result.checkpointIDs), result.commitHash[:7]) 11 unmodified lines
if storeErr == nil { metadata = storeInfo } else { if checkpointpolicy.IsUnsupportedVersion(storeErr) { return storeErr } logging.Debug(ctx, "checkpoint store metadata read failed", slog.String("checkpoint_id", checkpointID.String()), slog.String("error", storeErr.Error()), ) 17 unmodified lines
// resolveLatestCheckpoint reads metadata for each checkpoint ID and returns // the checkpoint with the latest CreatedAt. func resolveLatestCheckpoint(ctx context.Context, store checkpointInfoReader, checkpointIDs []id.CheckpointID) (*strategy.CheckpointInfo, error) { func resolveLatestCheckpoint(ctx context.Context, store checkpointInfoReader, checkpointIDs []id.CheckpointID) (*strategy.CheckpointInfo, bool, error) { infoMap := make(map[id.CheckpointID]strategy.CheckpointInfo, len(checkpointIDs)) for _, cpID := range checkpointIDs { metadata, readErr := readCheckpointInfoFromStore(ctx, store, cpID) if readErr != nil { if checkpointpolicy.IsUnsupportedVersion(readErr) { return nil, false, readErr } logging.Debug(ctx, "resolveLatestCheckpoint: checkpoint metadata read failed", slog.String("checkpoint_id", cpID.String()), slog.String("error", readErr.Error()), ) 4 unmodified lines
} latest, found := strategy.ResolveLatestCheckpointFromMap(checkpointIDs, infoMap) if !found { return nil, errors.New("no checkpoint metadata found") return nil, false, nil } return &latest, nil return &latest, true, nil }
type checkpointInfoReader interface {
Mcmd/entire/cli/resume.go+16/-5
570 unmodified lines
571 572 573 574 574 575 576 577 578 579 580 581 582 583 2 unmodified lines
586 587 588 586 589 590 591 592 593 594 595 596 597 598 21 unmodified lines
620 621 622 617 623 624 625 626 627 628 629 630 631 632 92 unmodified lines
725 726 727 719 728 729 730 731 732 733 734 735 736 737
570 unmodified lines
// simulating git CLI squash merge trailer order. reverseOrderIDs := []id.CheckpointID{cpID3, cpID2, cpID1} reader := checkpoint.NewGitStore(repo, checkpoint.DefaultV1Refs()) latest, err := resolveLatestCheckpoint(context.Background(), reader, reverseOrderIDs) latest, found, err := resolveLatestCheckpoint(context.Background(), reader, reverseOrderIDs) if err != nil { t.Fatalf("resolveLatestCheckpoint() error = %v", err) } if !found { t.Fatal("resolveLatestCheckpoint() found = false") }
// Should return the newest checkpoint regardless of input order if latest.CheckpointID.String() != cpID3.String() { 2 unmodified lines
// Also verify with chronological order chronologicalIDs := []id.CheckpointID{cpID1, cpID2, cpID3} latest2, err := resolveLatestCheckpoint(context.Background(), reader, chronologicalIDs) latest2, found, err := resolveLatestCheckpoint(context.Background(), reader, chronologicalIDs) if err != nil { t.Fatalf("resolveLatestCheckpoint() error = %v", err) } if !found { t.Fatal("resolveLatestCheckpoint() found = false") } if latest2.CheckpointID.String() != cpID3.String() { t.Errorf("resolveLatestCheckpoint() = %s, want newest %s", latest2.CheckpointID, cpID3) } 21 unmodified lines
latest, err := resolveLatestCheckpoint(context.Background(), store, []id.CheckpointID{targetID}) latest, found, err := resolveLatestCheckpoint(context.Background(), store, []id.CheckpointID{targetID}) if err != nil { t.Fatalf("resolveLatestCheckpoint() error = %v", err) } if !found { t.Fatal("resolveLatestCheckpoint() found = false") } if latest.CheckpointID != targetID { t.Errorf("resolveLatestCheckpoint() = %s, want %s", latest.CheckpointID, targetID) }
Mcmd/entire/cli/resume_test.go+16/-4
17 unmodified lines
18 19 20 21 22 23 24 306 unmodified lines
331 332 333 333 334 335 336 337 338 339 340 341 342 194 unmodified lines
537 538 539 536 540 541 542 543 544 545 546 547 548 180 unmodified lines
729 730 731 725 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 9 unmodified lines
760 761 762 747 763 764 765 766
17 unmodified lines
"github.com/entireio/cli/cmd/entire/cli/agent/types"; "github.com/entireio/cli/cmd/entire/cli/checkpoint"; "github.com/entireio/cli/cmd/entire/cli/checkpoint/id"; "github.com/entireio/cli/cmd/entire/cli/checkpointpolicy"; "github.com/entireio/cli/cmd/entire/cli/gitrepo"; "github.com/entireio/cli/cmd/entire/cli/jsonutil"; "github.com/entireio/cli/cmd/entire/cli/logging"; 306 unmodified lines
var restored bool if !selectedPoint.CheckpointID.IsEmpty() { // Try checkpoint storage first for committed checkpoints if returnedSessionID, err := restoreSessionTranscriptFromStrategy(ctx, selectedPoint.CheckpointID, sessionID, agent); err == nil { returnedSessionID, err := restoreSessionTranscriptFromStrategy(ctx, selectedPoint.CheckpointID, sessionID, agent) if err == nil { sessionID = returnedSessionID restored = true } else if checkpointpolicy.IsUnsupportedVersion(err) { return err } }
194 unmodified lines
180 unmodified lines
if err != nil { return "", fmt.Errorf("open checkpoint store: %w", err) } content, returnedSessionID, err := checkpoint.ReadRawSessionLogForCheckpoint(ctx, stores.Primary, cpID) summary, err := checkpoint.ReadCommittedCheckpoint(ctx, stores.Primary, cpID) if err != nil { return "", fmt.Errorf("failed to read checkpoint: %w", err) } if err := checkpointpolicy.EnsureCanReadVersion(cpID.String(), summary.CheckpointVersion); err != nil { return "", fmt.Errorf("check checkpoint version: %w", err) }
content, err := checkpoint.ReadLatestSessionContent(ctx, stores.Primary, cpID, summary) if err != nil { return "", fmt.Errorf("failed to get session log: %w", err) }
// Use session ID returned from checkpoint if available // Otherwise fall back to the passed-in sessionID returnedSessionID := content.Metadata.SessionID if returnedSessionID != "" { sessionID = returnedSessionID }
}
Mcmd/entire/cli/rewind.go+20/-4
49 unmodified lines
50
51
52
53
54
55
56
57
58
59
11 unmodified lines
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
49 unmodified lines
warnOrLogCheckpointPolicySyncFailure(ctx, err)
return true
}
if state.Source == checkpointpolicy.SourceLocalDiverged {
warnOrLogCheckpointPolicyDiverged(ctx, state)
return false
}
if !checkpointpolicy.UnsupportedWrite(state.Policy) {
return true
}
11 unmodified lines
)
}
func warnOrLogCheckpointPolicyDiverged(ctx context.Context, state checkpointpolicy.State) {
if interactive.CanPromptInteractively() {
fmt.Fprintf(stderrWriter, "[entire] Could not reconcile checkpoint policy: %s\n", state.Warning)
return
}
logging.Warn(ctx, "checkpoint policy diverged; skipping checkpoint push",
slog.String("local_hash", state.Hash.String()),
slog.String("remote_hash", state.RemoteHash.String()),
)
}
func warnOrLogUnsupportedCheckpointWrite(ctx context.Context, policy checkpointpolicy.Policy) {
warning := checkpointpolicy.UpgradeWarning(versioncheck.UpdateCommandForCurrentBinary(versioninfo.Version))
if interactive.CanPromptInteractively() {
Mcmd/entire/cli/strategy/checkpoint_policy.go+15
50 unmodified lines
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
50 unmodified lines
require.Empty(t, strings.TrimSpace(out))
}
func TestPrePushSkipsCheckpointPushWhenPolicyDiverged(t *testing.T) {
workDir := setupRepoWithCheckpointBranch(t)
bareDir := filepath.Join(t.TempDir(), "remote.git")
_, err := git.PlainInit(bareDir, true)
require.NoError(t, err)
runCheckpointPolicyGit(t, workDir, "remote", "add", "origin", bareDir)
repo, err := git.PlainOpen(workDir)
require.NoError(t, err)
t.Cleanup(func() {
_ = repo.Close()
})
baseHash, err := checkpointpolicy.WriteLocal(t.Context(), repo, plumbing.ZeroHash, checkpointpolicy.DefaultPolicy())
require.NoError(t, err)
runCheckpointPolicyGit(t, workDir, "push", bareDir, checkpointpolicy.RefName.String()+":"+checkpointpolicy.RefName.String())
localHash, err := checkpointpolicy.WriteLocal(t.Context(), repo, baseHash, checkpointpolicy.DefaultPolicy())
require.NoError(t, err)
_, err = checkpointpolicy.WriteLocal(t.Context(), repo, baseHash, checkpointpolicy.Policy{
CheckpointVersion: "refs-v1",
CheckpointMinVersion: "branch-v1",
})
require.NoError(t, err)
runCheckpointPolicyGit(t, workDir, "push", bareDir, checkpointpolicy.RefName.String()+":"+checkpointpolicy.RefName.String())
require.NoError(t, checkpointpolicy.SetRef(repo, checkpointpolicy.RefName, localHash))
t.Chdir(workDir)
paths.ClearWorktreeRootCache()
t.Setenv(interactive.EnvTestTTY, "1")
oldWriter := stderrWriter
var stderr bytes.Buffer
stderrWriter = &stderr
t.Cleanup(func() { stderrWriter = oldWriter })
err = NewManualCommitStrategy().PrePush(context.Background(), "origin")
require.NoError(t, err)
require.Contains(t, stderr.String(), "Could not reconcile checkpoint policy")
out := runCheckpointPolicyGit(t, workDir, "ls-remote", bareDir, "refs/heads/"+paths.MetadataBranchName)
require.Empty(t, strings.TrimSpace(out))
}
func runCheckpointPolicyGit(t *testing.T, dir string, args ...string) string {
t.Helper()
cmd := exec.CommandContext(context.Background(), "git", args...)