feat(v0.5.3): summary fields for end_reason / ownership / adoption
This commit is contained in:
@@ -26,6 +26,25 @@ type SessionSummary struct {
|
||||
FilesModified []string `json:"files_modified"`
|
||||
FilesDeleted []string `json:"files_deleted"`
|
||||
NotesUpdated bool `json:"notes_updated"`
|
||||
|
||||
// EndReason is "child_exited" for direct mode, "tmux_session_ended" for
|
||||
// persistent mode (both owner-create and adopted reattach). Optional;
|
||||
// pre-v0.5.3 summaries omit it.
|
||||
EndReason string `json:"end_reason,omitempty"`
|
||||
|
||||
// DetectedVia distinguishes the mechanism that observed session end:
|
||||
// "child_exit" for direct mode, "polling" for persistent mode.
|
||||
DetectedVia string `json:"detected_via,omitempty"`
|
||||
|
||||
// SessionOwnership is "created" if this ctask process originated the
|
||||
// session (owner-create) or "adopted" if it took over an orphaned
|
||||
// persistent session. Omitted in direct mode.
|
||||
SessionOwnership string `json:"session_ownership,omitempty"`
|
||||
|
||||
// AdoptedFromOrphanAt records the moment adoption took place. Set only
|
||||
// for adopted reattach.
|
||||
AdoptedFromOrphanAt *time.Time `json:"adopted_from_orphan_at,omitempty"`
|
||||
|
||||
// EndManifest captures the workspace file list at end-of-session so the
|
||||
// next session can diff current state against it (Layer 3). This field
|
||||
// is ctask-internal; it is not part of the public summary format shown
|
||||
|
||||
@@ -169,3 +169,64 @@ func TestFormatLaunchContextRendersChanged(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSummaryNewFieldsRoundTrip(t *testing.T) {
|
||||
tmp := t.TempDir()
|
||||
path := filepath.Join(tmp, ".ctask", "last-session-summary.json")
|
||||
|
||||
adoptedAt := time.Now().UTC().Truncate(time.Second)
|
||||
s := &SessionSummary{
|
||||
SessionID: "host-1-20260508140000",
|
||||
Hostname: "host",
|
||||
Agent: "claude",
|
||||
Mode: "local",
|
||||
StartedAt: adoptedAt,
|
||||
EndedAt: adoptedAt.Add(10 * time.Minute),
|
||||
DurationSeconds: 600,
|
||||
EndReason: "tmux_session_ended",
|
||||
DetectedVia: "polling",
|
||||
SessionOwnership: "adopted",
|
||||
AdoptedFromOrphanAt: &adoptedAt,
|
||||
}
|
||||
if err := WriteSummary(path, s); err != nil {
|
||||
t.Fatalf("WriteSummary: %v", err)
|
||||
}
|
||||
got, err := ReadSummary(path)
|
||||
if err != nil {
|
||||
t.Fatalf("ReadSummary: %v", err)
|
||||
}
|
||||
if got.EndReason != "tmux_session_ended" {
|
||||
t.Errorf("EndReason: got %q", got.EndReason)
|
||||
}
|
||||
if got.DetectedVia != "polling" {
|
||||
t.Errorf("DetectedVia: got %q", got.DetectedVia)
|
||||
}
|
||||
if got.SessionOwnership != "adopted" {
|
||||
t.Errorf("SessionOwnership: got %q", got.SessionOwnership)
|
||||
}
|
||||
if got.AdoptedFromOrphanAt == nil || !got.AdoptedFromOrphanAt.Equal(adoptedAt) {
|
||||
t.Errorf("AdoptedFromOrphanAt: got %v, want %v", got.AdoptedFromOrphanAt, adoptedAt)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSummaryNewFieldsOmittedWhenEmpty(t *testing.T) {
|
||||
tmp := t.TempDir()
|
||||
path := filepath.Join(tmp, ".ctask", "last-session-summary.json")
|
||||
|
||||
s := &SessionSummary{
|
||||
SessionID: "x", Hostname: "h", Agent: "claude", Mode: "local",
|
||||
StartedAt: time.Now().UTC(), EndedAt: time.Now().UTC(),
|
||||
}
|
||||
if err := WriteSummary(path, s); err != nil {
|
||||
t.Fatalf("WriteSummary: %v", err)
|
||||
}
|
||||
body, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatalf("read: %v", err)
|
||||
}
|
||||
for _, key := range []string{"end_reason", "detected_via", "session_ownership", "adopted_from_orphan_at"} {
|
||||
if strings.Contains(string(body), key) {
|
||||
t.Errorf("expected %q to be omitted; body:\n%s", key, body)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user