fix(sync): keep session renames stable (#2043)

* fix(sync): keep session renames stable

* fix(sync): clarify rename mirror flow

* fix(sync): clarify archive comment

---------

Co-authored-by: bashrusakh <bashrusakh@users.noreply.github.com>
Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
This commit is contained in:
Leonid
2026-07-11 15:19:24 +03:00
committed by GitHub
co-authored by bashrusakh Bohdan Triapitsyn
parent 9bfc5bf0be
commit d5745aaac9
9 changed files with 234 additions and 43 deletions
@@ -1,4 +1,5 @@
import { describe, expect, test } from "bun:test"
import type { Session } from "@opencode-ai/sdk/v2"
import type { Event, Part, PermissionRequest, QuestionRequest, SessionStatus } from "@opencode-ai/sdk/v2/client"
import { applyDirectoryEvent } from "../event-reducer"
import { INITIAL_STATE, type State } from "../types"
@@ -55,6 +56,14 @@ function topLevelSessionOnlyPartUpdatedEvent(): Event {
} as Event
}
function buildSession(title: string, time: Session["time"]): Session {
return {
id: "ses_1",
title,
time,
} as Session
}
describe("applyDirectoryEvent", () => {
test("returns typed materialization when delta arrives before parts", () => {
const result = applyDirectoryEvent(state(), deltaEvent())
@@ -129,6 +138,20 @@ describe("applyDirectoryEvent", () => {
})
})
test("skips stale session.updated events so a newer title survives", () => {
const draft = state({ session: [buildSession("New Title", { created: 1, updated: 20 })] })
const result = applyDirectoryEvent(draft, {
type: "session.updated",
properties: {
info: buildSession("Old Title", { created: 1, updated: 10 }),
},
} as Event)
expect(result).toBe(false)
expect(draft.session[0]?.title).toBe("New Title")
})
test("applies part update without materialization when owning message exists", () => {
const draft = state({
message: { ses_1: [{ id: "msg_1", sessionID: "ses_1", role: "assistant", time: { created: 1 } } as never] },
@@ -0,0 +1,33 @@
import { describe, expect, test } from "bun:test"
import type { Session } from "@opencode-ai/sdk/v2"
import { shouldSkipStaleSessionEvent } from "../session-event-freshness"
const buildSession = (title: string, time: Partial<NonNullable<Session["time"]>>): Session => ({
id: "ses_1",
title,
time: time as Session["time"],
} as Session)
describe("shouldSkipStaleSessionEvent", () => {
test("skips a stale SSE session update after a newer local rename", () => {
const current = buildSession("New Title", { created: 1, updated: 20 })
const incoming = buildSession("Old Title", { created: 1, updated: 10 })
expect(shouldSkipStaleSessionEvent(current, incoming)).toBe(true)
})
test("allows a fresher SSE update to apply", () => {
const current = buildSession("Old Title", { created: 1, updated: 10 })
const incoming = buildSession("New Title", { created: 1, updated: 20 })
expect(shouldSkipStaleSessionEvent(current, incoming)).toBe(false)
})
test("falls back to created timestamp when updated is missing", () => {
const current = buildSession("Current", { created: 20 })
const incoming = buildSession("Incoming", { created: 10 })
expect(shouldSkipStaleSessionEvent(current, incoming)).toBe(true)
})
})
@@ -0,0 +1,46 @@
import { beforeEach, describe, expect, mock, test } from "bun:test"
import type { Event, Session } from "@opencode-ai/sdk/v2/client"
let currentSessions: Session[] = []
const upsertedSessions: Session[] = []
mock.module("@/stores/useGlobalSessionsStore", () => ({
useGlobalSessionsStore: {
getState: () => ({
activeSessions: currentSessions,
archivedSessions: [] as Session[],
upsertSession: (session: Session) => {
upsertedSessions.push(session)
},
}),
},
}))
import { applySessionEventToGlobalSessions } from "../session-event-router"
const buildSession = (title: string, time: Session["time"]): Session => ({
id: "ses_1",
title,
time,
} as Session)
const buildEvent = (session: Session): Event => ({
type: "session.updated",
properties: {
info: session,
},
} as Event)
describe("applySessionEventToGlobalSessions", () => {
beforeEach(() => {
currentSessions = []
upsertedSessions.length = 0
})
test("skips stale global session.updated echoes after a newer rename", () => {
currentSessions = [buildSession("New Title", { created: 1, updated: 20 })]
applySessionEventToGlobalSessions(buildEvent(buildSession("Old Title", { created: 1, updated: 10 })))
expect(upsertedSessions).toEqual([])
})
})