wip(ssh): hoist migration for SSH session partitions — NOT YET WORKING

Adds hoistSshPartitionsIntoLocalSession and wires it into load. The migration
is reached (verified: it sees the ssh:<target> partition on reopen) but the
hoisted tabs are not visible on the reloaded store, so the oracle is still red.

The oracle itself is now correct: it writes the partition, flushes, and REOPENS
the profile so the load-time migration is what is under test. Its anti-vacuity
checks pass — the partition survives the reopen and local starts empty.

Do not ship. Next step is in the commit body of the follow-up.
This commit is contained in:
Neil
2026-08-13 19:18:22 -07:00
parent d2aba6e369
commit 55d6ae877b
2 changed files with 96 additions and 9 deletions
+72
View File
@@ -2849,6 +2849,70 @@ export type PtyBindingSourceExpectation = {
incarnationId?: string
}
/**
* Fold SSH pane membership left in `workspaceSessionsByHostId["ssh:<target>"]` into the local
* session, which is the only plane the binding writer maintains.
*
* Why this has to exist: consolidating the two planes removed the writer but not the data. A pane
* the local session has never heard of looks new to `persistPtyBinding`, and reattach passes
* `mayCreate: false`, so the binding is refused and rolled back — for every pane the upgrading
* user had open. Local always wins; the partition only supplies panes local does not already know.
*/
export function hoistSshPartitionsIntoLocalSession(state: PersistedState): boolean {
const partitions = state.workspaceSessionsByHostId
if (!partitions) {
return false
}
const local = state.workspaceSession
if (!local) {
return false
}
let changed = false
for (const [hostId, partition] of Object.entries(partitions)) {
if (!hostId.startsWith('ssh:') || !partition) {
continue
}
for (const [worktreeId, tabs] of Object.entries(partition.tabsByWorktree ?? {})) {
const existing = local.tabsByWorktree?.[worktreeId] ?? []
const known = new Set(existing.map((tab) => tab.id))
const missing = (tabs ?? []).filter((tab) => !known.has(tab.id))
if (missing.length === 0) {
continue
}
local.tabsByWorktree = { ...local.tabsByWorktree, [worktreeId]: [...existing, ...missing] }
changed = true
}
for (const [tabId, layout] of Object.entries(partition.terminalLayoutsByTabId ?? {})) {
if (!layout || local.terminalLayoutsByTabId?.[tabId]) {
continue
}
local.terminalLayoutsByTabId = { ...local.terminalLayoutsByTabId, [tabId]: layout }
changed = true
}
for (const [paneKey, incarnationId] of Object.entries(
partition.terminalPtyIncarnationsByPaneKey ?? {}
)) {
if (local.terminalPtyIncarnationsByPaneKey?.[paneKey] !== undefined) {
continue
}
local.terminalPtyIncarnationsByPaneKey = {
...local.terminalPtyIncarnationsByPaneKey,
[paneKey]: incarnationId
}
changed = true
}
for (const [worktreeId, tabId] of Object.entries(partition.activeTabIdByWorktree ?? {})) {
if (local.activeTabIdByWorktree?.[worktreeId] !== undefined) {
continue
}
local.activeTabIdByWorktree = { ...local.activeTabIdByWorktree, [worktreeId]: tabId }
changed = true
}
}
return changed
}
export class Store {
private state: PersistedState
private readonly dataFile: string
@@ -3938,6 +4002,14 @@ export class Store {
migrated.githubCache = readGithubCacheSnapshot(this.dataFile) ?? migrated.githubCache
}
// Why: earlier builds wrote SSH pane membership into `ssh:<target>` partitions; this build's
// binding writer is local-only and its reattach refuses to create, so an unhoisted partition
// means every pane is refused and the user's tabs are discarded. Runs on the final state so a
// later migration cannot replace the session object out from under it.
if (hoistSshPartitionsIntoLocalSession(migrated)) {
this.loadNeedsSave = true
}
logPersistenceStartupMilestone('persistence-load-done', {
repos: migrated.repos.length,
workspaceSessionBytes: Buffer.byteLength(JSON.stringify(migrated.workspaceSession))
@@ -44,6 +44,15 @@ async function createStore(state: Record<string, unknown> = {}) {
return new Store()
}
/** Reopen the SAME profile — the upgrade launch. The hoist migration runs at load, so a test that
* writes the partition into an already-loaded store would never exercise it. */
async function reopenStore() {
vi.resetModules()
const { Store, initDataPath } = await import('./persistence')
initDataPath()
return new Store()
}
/** One SSH pane, exactly as the partitioned build persisted it. */
function paneSession() {
return {
@@ -71,14 +80,18 @@ describe('an SSH pane whose membership was persisted into the ssh partition', ()
})
it('is still bound on reattach after upgrading to a build that reads local', async () => {
// Only the ssh partition holds the pane — the shape an upgrading user actually carries.
// Written through the store, the way the partitioned build wrote it.
const store = await createStore()
store.setWorkspaceSession(paneSession() as never, SSH_PARTITION)
// The old build: pane membership written into the ssh partition, nothing in local.
const before = await createStore()
before.setWorkspaceSession(paneSession() as never, SSH_PARTITION)
expect(
before.getWorkspaceSession(SSH_PARTITION).tabsByWorktree?.[WORKTREE],
'the ssh partition never received the pane, so this proves nothing'
).toHaveLength(1)
expect(before.getWorkspaceSession().tabsByWorktree?.[WORKTREE] ?? []).toHaveLength(0)
before.flushOrThrow()
// Anti-vacuity: the pane really is in the ssh partition and really is absent from local.
expect(store.getWorkspaceSession(SSH_PARTITION).tabsByWorktree?.[WORKTREE]).toHaveLength(1)
expect(store.getWorkspaceSession().tabsByWorktree?.[WORKTREE] ?? []).toHaveLength(0)
// The upgrade launch.
const store = await reopenStore()
// The reattach bind: no hostId, refuses to create.
const bound = store.persistPtyBinding({
@@ -99,8 +112,10 @@ describe('an SSH pane whose membership was persisted into the ssh partition', ()
// `mayCreate` existed, the same state re-created the tab instead of discarding it. That is what
// "before, many of the tabs were retained" described — they were being recreated.
it('was recreated rather than discarded before the refusal was introduced', async () => {
const store = await createStore()
store.setWorkspaceSession(paneSession() as never, SSH_PARTITION)
const before = await createStore()
before.setWorkspaceSession(paneSession() as never, SSH_PARTITION)
before.flushOrThrow()
const store = await reopenStore()
const bound = store.persistPtyBinding({
worktreeId: WORKTREE,