Files
orca/mobile/src/tasks/worktree-create-retry.test.ts
T
Jinwoo Hong 8f15f217a2 Preserve user-set workspace names across branch changes (#17448)
* fix(worktrees): preserve user workspace names across branch changes

* test(worktrees): cover pinned rename metadata

* fix(workspaces): address display-name review edge cases

* fix(workspaces): keep automatic names fresh across refreshes

* fix(workspaces): preserve legacy CLI labels

* fix(workspaces): preserve display-name provenance across hosts

* fix(workspaces): honor legacy display-name provenance

* fix(workspaces): fence display-name refresh races

* fix(workspaces): accept peer renames from provenance-less hosts

The old-host preserve fence kept a pinned local label on every refresh,
which also suppressed a legitimate rename another client persisted
through the same host until app restart. Narrow it to labels the host
re-derived itself (branch short name, or path basename when detached);
any other changed label in a mode-less response is explicit meta a peer
wrote there. Stale prior-label responses stay covered by the downstream
staleness fence, in-flight writes by the pending fence.

* refactor(workspaces): unify display-name pin derivation

Three call sites (renderer optimistic update, local IPC updateMeta
handler, remote worktree.set handler) each restated the same formula;
a future edit to one would silently skew provenance between paths.
2026-08-31 19:08:05 -04:00

767 lines
29 KiB
TypeScript

import { describe, expect, it, vi } from 'vitest'
import type { RpcClient } from '../transport/rpc-client'
import { markRpcDeliveryUnknown } from '../transport/rpc-delivery-ambiguity'
import { LogicalClientCutoverError } from '../transport/stable-logical-rpc-client'
import type { ConnectionState } from '../transport/types'
import {
getWorktreeCreateReplayWindowMs,
WORKTREE_CREATE_DEDUPE_TTL_LEGACY_HOST_MS,
type WorktreeCreateIdempotencySupport
} from './worktree-create-idempotency-policy'
import {
createWorktreeWithNameRetry,
WORKTREE_CREATE_AMBIGUOUS_RECONNECT_WAIT_MS
} from './worktree-create-retry'
import { WORKTREE_CREATE_TIMEOUT_MS } from './workspace-create-timeout'
type Attempt = { method: string; params: Record<string, unknown> }
const IDEMPOTENT_CREATE_SUPPORT = {
dedupeTtlMs: WORKTREE_CREATE_DEDUPE_TTL_LEGACY_HOST_MS
}
const LEGACY_HOST_REPLAY_WINDOW_MS = getWorktreeCreateReplayWindowMs(IDEMPOTENT_CREATE_SUPPORT)
// Drives the transport state a replay has to wait on. Production couples the two:
// a socket close rejects the pending frame AND drops the client off 'connected'.
function connectionController(): {
getState: () => ConnectionState
onStateChange: (listener: (state: ConnectionState) => void) => () => void
set: (next: ConnectionState) => void
} {
let state: ConnectionState = 'connected'
const listeners = new Set<(next: ConnectionState) => void>()
return {
getState: () => state,
onStateChange: (listener) => {
listeners.add(listener)
return () => listeners.delete(listener)
},
set: (next) => {
state = next
for (const listener of listeners) {
listener(next)
}
}
}
}
// Lets a parked replay reach its state wait before the test resumes the transport.
async function flush(): Promise<void> {
await new Promise((resolve) => setTimeout(resolve, 0))
}
// A client whose per-call outcome is scripted: return an id, a server error
// message, or throw (transport-level rejection, e.g. a connection-migration
// cutover). Records every call so tests can assert on the clientMutationId.
function scriptedClient(
outcomes: Array<
| { id: string; displayName?: string }
| { errorMessage: string }
// takesMs models how long the ambiguity took to SURFACE — a clean close is
// instant, a half-open socket waits out the liveness watchdog or the timeout.
// reconnectsAfterMs brings the transport back so the next replay can run.
| { throws: unknown; dropsConnection?: boolean; takesMs?: number; reconnectsAfterMs?: number }
>,
attempts: Attempt[],
connection?: ReturnType<typeof connectionController>,
// Wall-clock stamp of the last frame that really arrived. Absent = a transport that
// can't vouch for one, which must fall back to the send; a function models a live
// socket whose stamp keeps advancing.
lastInboundAt?: number | (() => number)
): RpcClient {
let call = 0
return {
getState: () => connection?.getState() ?? 'connected',
getLastInboundAt: () =>
(typeof lastInboundAt === 'function' ? lastInboundAt() : lastInboundAt) ?? null,
onStateChange: (listener: (state: ConnectionState) => void) =>
connection?.onStateChange(listener) ?? (() => {}),
sendRequest: async (method: string, params?: unknown) => {
attempts.push({ method, params: (params ?? {}) as Record<string, unknown> })
const outcome = outcomes[Math.min(call, outcomes.length - 1)]!
call += 1
if ('throws' in outcome) {
if (outcome.takesMs !== undefined) {
await new Promise((resolve) => setTimeout(resolve, outcome.takesMs))
}
if (outcome.dropsConnection) {
connection?.set('reconnecting')
if (outcome.reconnectsAfterMs !== undefined) {
setTimeout(() => connection?.set('connected'), outcome.reconnectsAfterMs)
}
}
throw outcome.throws
}
if ('errorMessage' in outcome) {
return {
id: '1',
ok: false,
error: { code: 'x', message: outcome.errorMessage },
_meta: { runtimeId: 'r' }
}
}
return {
id: '1',
ok: true,
result: {
worktree: {
id: outcome.id,
...(outcome.displayName !== undefined ? { displayName: outcome.displayName } : {})
}
},
_meta: { runtimeId: 'r' }
}
}
} as unknown as RpcClient
}
describe('createWorktreeWithNameRetry', () => {
it('waits for capability detection before sending a create', async () => {
const attempts: Attempt[] = []
const client = scriptedClient([{ id: 'wt-ready' }], attempts)
let resolveSupport!: (supported: WorktreeCreateIdempotencySupport) => void
const support = new Promise<WorktreeCreateIdempotencySupport>((resolve) => {
resolveSupport = resolve
})
const pending = createWorktreeWithNameRetry({
client,
baseName: 'puffin',
buildParams: (name) => ({ repo: 'id:r', name }),
worktreeCreateIdempotency: support,
mintMutationId: () => 'key-ready'
})
await Promise.resolve()
expect(attempts).toHaveLength(0)
resolveSupport(IDEMPOTENT_CREATE_SUPPORT)
await expect(pending).resolves.toEqual({ worktreeId: 'wt-ready', name: 'puffin' })
expect(attempts).toHaveLength(1)
expect(attempts[0]!.params.clientMutationId).toBe('key-ready')
})
it('stamps a clientMutationId on the create request', async () => {
const attempts: Attempt[] = []
const client = scriptedClient([{ id: 'wt-1' }], attempts)
const result = await createWorktreeWithNameRetry({
client,
baseName: 'otter',
buildParams: (name) => ({ repo: 'id:r', name }),
worktreeCreateIdempotency: IDEMPOTENT_CREATE_SUPPORT,
mintMutationId: () => 'key-1'
})
expect(result).toEqual({ worktreeId: 'wt-1', name: 'otter' })
expect(attempts).toHaveLength(1)
expect(attempts[0]!.params).toMatchObject({ name: 'otter', clientMutationId: 'key-1' })
})
it('retries a connection-migration cutover with the SAME key, then succeeds', async () => {
const attempts: Attempt[] = []
const client = scriptedClient(
[{ throws: new LogicalClientCutoverError() }, { id: 'wt-2' }],
attempts
)
const result = await createWorktreeWithNameRetry({
client,
baseName: 'seal',
buildParams: (name) => ({ repo: 'id:r', name }),
worktreeCreateIdempotency: IDEMPOTENT_CREATE_SUPPORT,
mintMutationId: () => 'key-mig'
})
expect(result).toEqual({ worktreeId: 'wt-2', name: 'seal' })
expect(attempts).toHaveLength(2)
// Idempotency: both the interrupted send and the retry carry one key so the
// host dedupes instead of creating a duplicate worktree.
expect(attempts[0]!.params.clientMutationId).toBe('key-mig')
expect(attempts[1]!.params.clientMutationId).toBe('key-mig')
expect(attempts[1]!.params.name).toBe('seal')
})
it('gives up after the cutover retry budget and rethrows', async () => {
const attempts: Attempt[] = []
const client = scriptedClient([{ throws: new LogicalClientCutoverError() }], attempts)
await expect(
createWorktreeWithNameRetry({
client,
baseName: 'crab',
buildParams: (name) => ({ repo: 'id:r', name }),
worktreeCreateIdempotency: IDEMPOTENT_CREATE_SUPPORT,
mintMutationId: () => 'key-x'
})
).rejects.toBeInstanceOf(LogicalClientCutoverError)
// Initial attempt + 5 retries.
expect(attempts).toHaveLength(6)
})
// Also the delivery-ambiguity guard: an unmarked error is a DEFINITE failure, so
// neither the cutover nor the replay path may re-issue it.
it('does not treat an ordinary transport error as a cutover', async () => {
const attempts: Attempt[] = []
const client = scriptedClient([{ throws: new Error('Request timed out') }], attempts)
await expect(
createWorktreeWithNameRetry({
client,
baseName: 'eel',
buildParams: (name) => ({ repo: 'id:r', name }),
worktreeCreateIdempotency: IDEMPOTENT_CREATE_SUPPORT,
mintMutationId: () => 'key-t'
})
).rejects.toThrow('Request timed out')
expect(attempts).toHaveLength(1)
})
it('mints a fresh key per candidate when a name collision bumps the suffix', async () => {
const attempts: Attempt[] = []
const client = scriptedClient(
[{ errorMessage: 'already exists locally' }, { id: 'wt-3' }],
attempts
)
let n = 0
const result = await createWorktreeWithNameRetry({
client,
baseName: 'topic',
buildParams: (name) => ({ repo: 'id:r', name }),
worktreeCreateIdempotency: IDEMPOTENT_CREATE_SUPPORT,
mintMutationId: () => `key-${(n += 1)}`
})
expect(result).toEqual({ worktreeId: 'wt-3', name: 'topic-2' })
expect(attempts).toHaveLength(2)
// A collision is a genuinely different create, so it gets a distinct key.
expect(attempts[0]!.params.clientMutationId).toBe('key-1')
expect(attempts[1]!.params.clientMutationId).toBe('key-2')
expect(attempts[1]!.params.name).toBe('topic-2')
})
it('uses the host-selected display name after a collision retry', async () => {
const attempts: Attempt[] = []
const client = scriptedClient(
[{ errorMessage: 'already exists locally' }, { id: 'wt-host-name', displayName: 'topic-3' }],
attempts
)
await expect(
createWorktreeWithNameRetry({
client,
baseName: 'topic',
buildParams: (name) => ({ repo: 'id:r', name }),
worktreeCreateIdempotency: false
})
).resolves.toEqual({ worktreeId: 'wt-host-name', name: 'topic-3' })
})
it('falls back to the client candidate when an older host omits displayName', async () => {
const attempts: Attempt[] = []
const client = scriptedClient(
[{ errorMessage: 'already exists locally' }, { id: 'wt-legacy' }],
attempts
)
await expect(
createWorktreeWithNameRetry({
client,
baseName: 'topic',
buildParams: (name) => ({ repo: 'id:r', name }),
worktreeCreateIdempotency: false
})
).resolves.toEqual({ worktreeId: 'wt-legacy', name: 'topic-2' })
})
it('advances generated retries without nesting suffixes', async () => {
const attempts: Attempt[] = []
const client = scriptedClient(
[{ errorMessage: 'already exists locally' }, { id: 'wt-generated' }],
attempts
)
const result = await createWorktreeWithNameRetry({
client,
baseName: 'nautilus-2',
nameWasGenerated: true,
buildParams: (name) => ({ repo: 'id:r', name, nameWasGenerated: true }),
worktreeCreateIdempotency: false
})
expect(result).toEqual({ worktreeId: 'wt-generated', name: 'nautilus-3' })
expect(attempts.map((attempt) => attempt.params.name)).toEqual(['nautilus-2', 'nautilus-3'])
})
it('does not replay an ambiguous cutover when the host lacks idempotency support', async () => {
const attempts: Attempt[] = []
const client = scriptedClient([{ throws: new LogicalClientCutoverError() }], attempts)
await expect(
createWorktreeWithNameRetry({
client,
baseName: 'ray',
buildParams: (name) => ({ repo: 'id:r', name }),
worktreeCreateIdempotency: false,
mintMutationId: () => 'must-not-be-used'
})
).rejects.toBeInstanceOf(LogicalClientCutoverError)
expect(attempts).toHaveLength(1)
expect(attempts[0]!.params.clientMutationId).toBeUndefined()
})
it('replays a delivery-ambiguous socket drop with the SAME key once the transport returns', async () => {
const attempts: Attempt[] = []
const connection = connectionController()
const client = scriptedClient(
[
{
throws: markRpcDeliveryUnknown(new Error('Connection interrupted')),
dropsConnection: true
},
{ id: 'wt-drop' }
],
attempts,
connection
)
const pending = createWorktreeWithNameRetry({
client,
baseName: 'urchin',
buildParams: (name) => ({ repo: 'id:r', name }),
worktreeCreateIdempotency: IDEMPOTENT_CREATE_SUPPORT,
mintMutationId: () => 'key-drop'
})
await flush()
// Parked on the reconnect, not resent onto a dead socket.
expect(attempts).toHaveLength(1)
connection.set('connected')
await expect(pending).resolves.toEqual({ worktreeId: 'wt-drop', name: 'urchin' })
expect(attempts).toHaveLength(2)
// Idempotency: the host dedupes the replay against the create it may already
// have finished, instead of building a second worktree.
expect(attempts[1]!.params.clientMutationId).toBe('key-drop')
expect(attempts[1]!.params.name).toBe('urchin')
})
it('does not replay a delivery-ambiguous drop when the host lacks idempotency support', async () => {
const attempts: Attempt[] = []
const client = scriptedClient(
[{ throws: markRpcDeliveryUnknown(new Error('Connection interrupted')) }],
attempts
)
await expect(
createWorktreeWithNameRetry({
client,
baseName: 'limpet',
buildParams: (name) => ({ repo: 'id:r', name }),
worktreeCreateIdempotency: false,
mintMutationId: () => 'must-not-be-used'
})
).rejects.toThrow('Connection interrupted')
expect(attempts).toHaveLength(1)
expect(attempts[0]!.params.clientMutationId).toBeUndefined()
})
it('disables ambiguous replay for a malformed advertisement', async () => {
const attempts: Attempt[] = []
const connection = connectionController()
const client = scriptedClient(
[
{
throws: markRpcDeliveryUnknown(new Error('Connection interrupted')),
dropsConnection: true
}
],
attempts,
connection
)
await expect(
createWorktreeWithNameRetry({
client,
baseName: 'limpet',
buildParams: (name) => ({ repo: 'id:r', name }),
worktreeCreateIdempotency: { dedupeTtlMs: 0 },
mintMutationId: () => 'key-malformed'
})
).rejects.toThrow('Connection interrupted')
expect(attempts).toHaveLength(1)
expect(attempts[0]!.params.clientMutationId).toBe('key-malformed')
})
it('gives up after the delivery-ambiguity replay budget, still inside the dedupe TTL', async () => {
vi.useFakeTimers()
try {
const attempts: Attempt[] = []
const connection = connectionController()
const client = scriptedClient(
[
{
throws: markRpcDeliveryUnknown(new Error('Connection interrupted')),
dropsConnection: true,
reconnectsAfterMs: 1_000
}
],
attempts,
connection
)
const startedAt = Date.now()
let settledAt = -1
const pending = createWorktreeWithNameRetry({
client,
baseName: 'barnacle',
buildParams: (name) => ({ repo: 'id:r', name }),
worktreeCreateIdempotency: IDEMPOTENT_CREATE_SUPPORT,
mintMutationId: () => 'key-budget'
}).catch((error: unknown) => {
settledAt = Date.now()
throw error
})
const settled = expect(pending).rejects.toThrow('Connection interrupted')
await vi.advanceTimersByTimeAsync(WORKTREE_CREATE_DEDUPE_TTL_LEGACY_HOST_MS)
await settled
// Initial attempt + 2 replays.
expect(attempts).toHaveLength(3)
// The budget is a count, but what keeps a replay reconciling instead of building
// a second worktree is wall clock: every attempt has to land while the host still
// holds the record, with the watchdog's detection latency already deducted.
expect(settledAt - startedAt).toBeLessThanOrEqual(LEGACY_HOST_REPLAY_WINDOW_MS)
} finally {
vi.useRealTimers()
}
// Generous: advanceTimersByTimeAsync yields through REAL macrotasks between ticks,
// so vitest's default 5s real-time budget is reachable on a loaded runner.
}, 30_000)
it('surfaces the original ambiguity when the transport never comes back', async () => {
vi.useFakeTimers()
try {
const attempts: Attempt[] = []
const connection = connectionController()
const client = scriptedClient(
[
{
throws: markRpcDeliveryUnknown(new Error('Connection interrupted')),
dropsConnection: true
}
],
attempts,
connection
)
const pending = createWorktreeWithNameRetry({
client,
baseName: 'anemone',
buildParams: (name) => ({ repo: 'id:r', name }),
worktreeCreateIdempotency: IDEMPOTENT_CREATE_SUPPORT,
mintMutationId: () => 'key-stuck'
})
const settled = expect(pending).rejects.toThrow('Connection interrupted')
// A phone that never reconnects must not leave the Create spinner parked.
await vi.advanceTimersByTimeAsync(60_000)
await settled
expect(attempts).toHaveLength(1)
} finally {
vi.useRealTimers()
}
// Generous: advanceTimersByTimeAsync yields through REAL macrotasks between ticks,
// so vitest's default 5s real-time budget is reachable on a loaded runner.
}, 30_000)
it('stops replaying when the host-advertised dedupe record has expired', async () => {
vi.useFakeTimers()
try {
const attempts: Attempt[] = []
const connection = connectionController()
const client = scriptedClient(
[
{
throws: markRpcDeliveryUnknown(new Error('Connection interrupted')),
dropsConnection: true,
takesMs: 12_000,
reconnectsAfterMs: 1_000
},
{ id: 'wt-duplicate' }
],
attempts,
connection
)
const pending = createWorktreeWithNameRetry({
client,
baseName: 'mussel',
buildParams: (name) => ({ repo: 'id:r', name }),
worktreeCreateIdempotency: { dedupeTtlMs: 20_000 },
mintMutationId: () => 'key-short-ttl'
})
const settled = expect(pending).rejects.toThrow('Connection interrupted')
await vi.advanceTimersByTimeAsync(30_000)
await settled
expect(attempts).toHaveLength(1)
} finally {
vi.useRealTimers()
}
})
it('does not replay an ambiguity that surfaced while the transport stayed connected', async () => {
vi.useFakeTimers()
try {
const attempts: Attempt[] = []
const connection = connectionController()
const client = scriptedClient(
[
{
// A silently dropped response frame leaves the socket alive, so nothing
// rejects until the request timeout — by then the host resolved at an
// unknowable point and its dedupe record is very likely gone.
throws: markRpcDeliveryUnknown(new Error('Request timed out: worktree.create')),
takesMs: WORKTREE_CREATE_TIMEOUT_MS
},
{ id: 'wt-duplicate' }
],
attempts,
connection,
// Watchdog probes keep answering throughout, so the replay window looks wide
// open on arrival: only the still-connected guard can stop this replay.
() => Date.now()
)
const pending = createWorktreeWithNameRetry({
client,
baseName: 'cuttlefish',
buildParams: (name) => ({ repo: 'id:r', name }),
worktreeCreateIdempotency: IDEMPOTENT_CREATE_SUPPORT,
mintMutationId: () => 'key-expired'
})
const settled = expect(pending).rejects.toThrow('Request timed out')
await vi.advanceTimersByTimeAsync(WORKTREE_CREATE_TIMEOUT_MS)
await settled
// Never left 'connected', so no drop was ever detected and the staleness of the
// ambiguity is unbounded. Re-sending would miss the record and build a SECOND
// worktree — for a folder workspace, one with the very same name.
expect(connection.getState()).toBe('connected')
expect(attempts).toHaveLength(1)
} finally {
vi.useRealTimers()
}
// Generous: advanceTimersByTimeAsync yields through REAL macrotasks between ticks,
// so vitest's default 5s real-time budget is reachable on a loaded runner.
}, 30_000)
it('clamps a later reconnect wait to what is left of the replay window', async () => {
vi.useFakeTimers()
try {
const attempts: Attempt[] = []
const connection = connectionController()
const client = scriptedClient(
[
{
// Most of the window is already gone by the time the drop is reported, so the
// remainder — not the full wait — is what the second wait gets.
throws: markRpcDeliveryUnknown(new Error('Connection interrupted')),
dropsConnection: true,
takesMs: 30_000,
reconnectsAfterMs: 1_000
},
{
// Second drop, and this time the phone never comes back.
throws: markRpcDeliveryUnknown(new Error('Connection interrupted')),
dropsConnection: true
}
],
attempts,
connection
)
const startedAt = Date.now()
let settledAt = -1
const pending = createWorktreeWithNameRetry({
client,
baseName: 'nautilus',
buildParams: (name) => ({ repo: 'id:r', name }),
worktreeCreateIdempotency: IDEMPOTENT_CREATE_SUPPORT,
mintMutationId: () => 'key-clamped'
}).catch((error: unknown) => {
settledAt = Date.now()
throw error
})
const settled = expect(pending).rejects.toThrow('Connection interrupted')
await vi.advanceTimersByTimeAsync(LEGACY_HOST_REPLAY_WINDOW_MS)
await settled
expect(attempts).toHaveLength(2)
// The window is anchored at the FIRST ambiguity, so the second wait gets only the
// remainder — it must not restart a full wait and carry the replay past the record.
expect(settledAt - startedAt).toBe(LEGACY_HOST_REPLAY_WINDOW_MS)
} finally {
vi.useRealTimers()
}
// Generous: advanceTimersByTimeAsync yields through REAL macrotasks between ticks,
// so vitest's default 5s real-time budget is reachable on a loaded runner.
}, 30_000)
it('refuses a replay when the ambiguity surfaced long after the last inbound frame', async () => {
vi.useFakeTimers()
try {
const attempts: Attempt[] = []
const connection = connectionController()
const startedAt = Date.now()
const client = scriptedClient(
[
{
throws: markRpcDeliveryUnknown(new Error('Connection interrupted')),
dropsConnection: true,
reconnectsAfterMs: 1_000,
// The phone was backgrounded: the OS suspended JS and killed the socket, and
// nothing observed the drop until the app came back ten minutes later.
takesMs: 600_000
}
],
attempts,
connection,
startedAt + 2_000
)
const pending = createWorktreeWithNameRetry({
client,
baseName: 'limpet',
buildParams: (name) => ({ repo: 'id:r', name }),
worktreeCreateIdempotency: IDEMPOTENT_CREATE_SUPPORT,
mintMutationId: () => 'key-stale'
})
const settled = expect(pending).rejects.toThrow('Connection interrupted')
await vi.advanceTimersByTimeAsync(700_000)
await settled
// The host forgot this create ~9 minutes ago. A replay would not reconcile — it
// would build a second worktree — so the create fails instead.
expect(attempts).toHaveLength(1)
} finally {
vi.useRealTimers()
}
// Generous: advanceTimersByTimeAsync yields through REAL macrotasks between ticks,
// so vitest's default 5s real-time budget is reachable on a loaded runner.
}, 30_000)
it('still replays a long-running create whose socket stayed live until it dropped', async () => {
vi.useFakeTimers()
try {
const attempts: Attempt[] = []
const connection = connectionController()
const startedAt = Date.now()
const client = scriptedClient(
[
{
// Three minutes of clone/fetch with the socket answering probes throughout,
// then it drops and the watchdog reports it four seconds later.
throws: markRpcDeliveryUnknown(new Error('Connection interrupted')),
dropsConnection: true,
reconnectsAfterMs: 1_000,
takesMs: 184_000
},
{ id: 'wt-long' }
],
attempts,
connection,
startedAt + 180_000
)
const pending = createWorktreeWithNameRetry({
client,
baseName: 'kelp',
buildParams: (name) => ({ repo: 'id:r', name }),
worktreeCreateIdempotency: IDEMPOTENT_CREATE_SUPPORT,
mintMutationId: () => 'key-long'
})
await vi.advanceTimersByTimeAsync(250_000)
// Anchoring at the send would refuse this — the create outran the TTL long before it
// went ambiguous — but the record only starts ticking when the host RESOLVES, and the
// live socket proves our knowledge was current.
expect(await pending).toEqual({ worktreeId: 'wt-long', name: 'kelp' })
expect(attempts).toHaveLength(2)
expect(attempts[0]!.params.clientMutationId).toBe('key-long')
expect(attempts[1]!.params.clientMutationId).toBe('key-long')
} finally {
vi.useRealTimers()
}
// Generous: advanceTimersByTimeAsync yields through REAL macrotasks between ticks,
// so vitest's default 5s real-time budget is reachable on a loaded runner.
}, 30_000)
it('does not extend the replay window when the replacement session reports fresher inbound activity', async () => {
vi.useFakeTimers()
try {
const attempts: Attempt[] = []
const connection = connectionController()
const startedAt = Date.now()
// The replacement session starts answering probes, so its inbound stamp is far
// newer than anything that bears on when the ORIGINAL create resolved host-side.
let lastInboundAt = startedAt + 1_000
setTimeout(() => {
lastInboundAt = startedAt + 45_000
}, 45_000)
const client = scriptedClient(
[
{
throws: markRpcDeliveryUnknown(new Error('Connection lost')),
dropsConnection: true,
takesMs: 40_000,
reconnectsAfterMs: 1_000
},
{
throws: markRpcDeliveryUnknown(new Error('Connection lost')),
dropsConnection: true,
takesMs: 20_000,
reconnectsAfterMs: 1_000
},
{ id: 'wt-third' }
],
attempts,
connection,
() => lastInboundAt
)
const pending = createWorktreeWithNameRetry({
client,
baseName: 'urchin',
buildParams: (name) => ({ repo: 'id:r', name }),
worktreeCreateIdempotency: IDEMPOTENT_CREATE_SUPPORT,
mintMutationId: () => 'key-anchored'
})
const settled = expect(pending).rejects.toThrow('Connection lost')
await vi.advanceTimersByTimeAsync(120_000)
await settled
// The deadline is fixed at the FIRST ambiguity. Re-reading it here would buy another
// 50s of window off a stamp that says nothing about the original request.
expect(attempts).toHaveLength(2)
} finally {
vi.useRealTimers()
}
// Generous: advanceTimersByTimeAsync yields through REAL macrotasks between ticks,
// so vitest's default 5s real-time budget is reachable on a loaded runner.
}, 30_000)
it('does not replay a dropped transport error that was never marked delivery-unknown', async () => {
const attempts: Attempt[] = []
const connection = connectionController()
const client = scriptedClient(
// Unmarked: the frame is known NOT to have reached the wire, so the host never saw
// it and a replay would be a second create, not a reconciliation. The transport
// still leaves 'connected', so only the delivery-unknown check stops this.
[{ throws: new Error('Socket closed before send'), dropsConnection: true }],
attempts,
connection
)
await expect(
createWorktreeWithNameRetry({
client,
baseName: 'urchin',
buildParams: (name) => ({ repo: 'id:r', name }),
worktreeCreateIdempotency: IDEMPOTENT_CREATE_SUPPORT,
mintMutationId: () => 'key-unmarked'
})
).rejects.toThrow('Socket closed before send')
expect(attempts).toHaveLength(1)
})
it('keeps the replay window strictly inside the host dedupe TTL', () => {
// The window is measured from a lower bound on when the host could have resolved, so
// it has to leave the record room for the replay to still be in flight. Widening it
// to the whole TTL would let the last replay land on a record that just expired.
expect(LEGACY_HOST_REPLAY_WINDOW_MS).toBeGreaterThan(0)
expect(LEGACY_HOST_REPLAY_WINDOW_MS).toBeLessThan(WORKTREE_CREATE_DEDUPE_TTL_LEGACY_HOST_MS)
// A reconnect wait is clamped when a short advertisement leaves less time.
const shortReplayWindowMs = getWorktreeCreateReplayWindowMs({ dedupeTtlMs: 20_000 })
expect(Math.min(WORKTREE_CREATE_AMBIGUOUS_RECONNECT_WAIT_MS, shortReplayWindowMs)).toBe(10_000)
})
})