test(wsl-mover): kill the real move driver at every point against a real host store; abort a reset move's leftover stage on reconnect

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Neil
2026-10-22 04:15:58 -07:00
co-authored by Claude
parent a63e7012a2
commit e8fa137a86
8 changed files with 554 additions and 10 deletions
@@ -0,0 +1,122 @@
/**
* A WSL host for the move driver backed by a real orcad profile store, so staging claims, blob
* staging, the commit's blob check and receipts are the production code. Every mutation passes a
* kill point before it lands and after it is durable (a lost reply). A kill stops the whole desktop
* process: from then on every call throws, so no catch handler can tidy up what a crash would leave.
*/
import type {
OrcadMigrationCatalogState,
OrcadMigrationManifest
} from '../../../../shared/orcad-migration/orcad-migration-manifest'
import { ORCAD_MIGRATION_BLOB_KIND_CONTRACTS } from '../../../../shared/orcad-migration/orcad-migration-blob-transfer'
import {
ORCAD_MIGRATION_BLOB_CHANNEL_RUNTIME_CAPABILITY,
ORCAD_MIGRATION_LOCAL_WSL_SOURCE_RUNTIME_CAPABILITY
} from '../../../../shared/orcad-runtime-capabilities'
import type { Store } from '../../loading-store/store'
import type { WslMoveHost } from '../../moving-wsl-projects/wsl-move-driver-ports'
export class WslMoveKilled extends Error {}
export type WslMoveKillSwitch = {
/** The point the next kill fires at; null runs clean. */
killAt: string | null
/** True from the kill until the test restarts the process. */
dead: boolean
points: string[]
}
export function createWslMoveKillSwitch(): WslMoveKillSwitch {
return { killAt: null, dead: false, points: [] }
}
export function passWslMoveKillPoint(kill: WslMoveKillSwitch, name: string): void {
if (kill.dead) {
throw new WslMoveKilled(`dead at ${name}`)
}
kill.points.push(name)
if (kill.killAt === name) {
kill.killAt = null
kill.dead = true
throw new WslMoveKilled(name)
}
}
/** Every capability a current WSL host advertises for a move. */
export const WSL_MOVE_STORE_HOST_CAPABILITIES: readonly string[] = [
ORCAD_MIGRATION_LOCAL_WSL_SOURCE_RUNTIME_CAPABILITY,
ORCAD_MIGRATION_BLOB_CHANNEL_RUNTIME_CAPABILITY,
...Object.values(ORCAD_MIGRATION_BLOB_KIND_CONTRACTS).map((contract) => contract.capability)
]
export type WslMoveStoreHostOptions = {
/** The host's current store; an orcad restart swaps it for one reopened from disk. */
store: () => Store
kill: WslMoveKillSwitch
/** Points named `<prefix>stage:before` etc., so two desktops on one host stay apart. */
prefix?: string
environmentId?: string
name?: string
}
export function createWslMoveStoreHost(options: WslMoveStoreHostOptions): WslMoveHost {
const { store, kill } = options
const prefix = options.prefix ?? ''
const at = (name: string): void => passWslMoveKillPoint(kill, `${prefix}${name}`)
// Like the RPC layer: a mutation answers only once it is on disk.
const mutate = async <T>(name: string, apply: () => T): Promise<T> => {
at(`${name}:before`)
const result = apply()
await store().flushPendingOrThrowAsync()
at(`${name}:lost-reply`)
return result
}
const read = (manifest: OrcadMigrationManifest): OrcadMigrationCatalogState => {
at('read-state')
return store().getOrcadMigrationCatalogState(manifest)
}
return {
environmentId: options.environmentId ?? 'wsl:Ubuntu',
name: options.name ?? 'Ubuntu',
capabilities: WSL_MOVE_STORE_HOST_CAPABILITIES,
catalog: {
readState: async (manifest) => read(manifest),
stage: (manifest) => mutate('stage', () => store().stageOrcadMigrationCatalog(manifest)),
commit: (manifest) =>
mutate('commit', () => store().commitStagedOrcadMigrationCatalog(manifest)),
abort: async (manifest) => ({
...(await mutate('abort', () => store().abortStagedOrcadMigrationCatalog(manifest))),
durableAbsent: true
}),
abortByReference: (manifest) =>
mutate('abort-by-reference', () =>
store().abortStagedOrcadMigrationCatalogByReference({
migrationId: manifest.migrationId,
manifestSha256: manifest.manifestSha256
})
),
stageChunk: (request) =>
mutate('snapshot-chunk', () => store().stageOrcadMigrationSnapshotChunk(request))
},
commitWithBlobs: (manifest, expectedBlobs) =>
mutate('commit', () =>
store().commitStagedOrcadMigrationCatalog(manifest, { expectedBlobs })
),
blobs: {
stageChunk: (request) =>
mutate(`blob:${request.blob.blobKind}`, () => store().stageOrcadMigrationBlobChunk(request))
},
readHolding: async () => {
at('read-holding')
return {
repoIds: store()
.getRepos()
.map((repo) => repo.id),
folderWorkspaceIds: store()
.getFolderWorkspaces()
.map((folder) => folder.id),
highestGeneration: 0
}
}
}
}
@@ -0,0 +1,309 @@
/**
* D1b-10 fault injection over the real move driver and a real orcad profile store (#29826's
* pattern): the desktop is killed at every point a clean move passes (each journal write, each
* host call before it lands and after its reply is lost, each pause step, each desktop rewrite
* store), optionally with orcad restarted too. After the restart and the reconnect, the move is
* back at zero or fully moved, never in between; a second restart changes nothing; and a move
* reset to zero can be run again from scratch.
*/
import { mkdtempSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { WSL_HOSTS_INTERNAL_GATE_ENV } from '../../../shared/wsl-host-internal-gate'
import { wslMoveJournalKey } from '../../../shared/wsl-move/wsl-move-cutover'
import { createStore, testState } from '../../persistence-test-harness'
import type { Store } from '../loading-store/store'
import {
createWslMoveWorld,
type WslMoveWorld
} from '../__fixtures__/wsl-mover/wsl-move-driver-world'
import {
createWslMoveKillSwitch,
createWslMoveStoreHost,
passWslMoveKillPoint,
type WslMoveKillSwitch
} from '../__fixtures__/wsl-mover/wsl-move-store-host'
import type * as Journal from './wsl-move-cutover-journal'
import { listWslMoveCutoverChain, listWslMoveCutovers } from './wsl-move-cutover-journal'
import { runWslMoveDesktopRewritePass } from './wsl-move-desktop-rewrite'
import {
registerWslMoveDesktopRewriteStore,
resetWslMoveDesktopRewriteStoresForTests,
setWslMoveDesktopRewriteRegistryCompleteForTests
} from './wsl-move-desktop-rewrite-stores'
import { moveProjectsToWslHost } from './wsl-move-driver'
import { resolveWslMoveFence } from './wsl-move-fence'
import { listWslMoveOwedHostAborts, settleWslMoveOwedHostAborts } from './wsl-move-owed-host-aborts'
import { recoverInterruptedWslMoves, resetWslMoveToZero } from './wsl-move-reset'
import { runWslMoveRetentionPass } from './wsl-move-retention'
vi.mock('electron', () => ({
app: { getPath: () => testState.dir },
safeStorage: {
isEncryptionAvailable: () => true,
encryptString: (plaintext: string) => Buffer.from(`encrypted:${plaintext}`, 'utf-8'),
decryptString: (ciphertext: Buffer) => ciphertext.toString('utf-8').slice('encrypted:'.length)
}
}))
vi.mock('../../telemetry/client', () => ({ track: vi.fn() }))
vi.mock('../../telemetry/cohort-classifier', () => ({
getCohortAtEmit: vi.fn(() => ({ nth_repo_added: 1 }))
}))
// The journal's writes and removals are kill points too; the hook is set per test.
const journalKill = vi.hoisted(() => ({ at: null as ((name: string) => void) | null }))
vi.mock('./wsl-move-cutover-journal', async (importOriginal) => {
const actual = await importOriginal<typeof Journal>()
const around = <A extends unknown[]>(name: (...args: A) => string, run: (...args: A) => void) => {
return (...args: A): void => {
journalKill.at?.(`${name(...args)}:before`)
run(...args)
journalKill.at?.(`${name(...args)}:after`)
}
}
return {
...actual,
writeWslMoveCutover: around(
(_path: string, cutover: Parameters<typeof actual.writeWslMoveCutover>[1]) =>
`journal:${cutover.phase}`,
actual.writeWslMoveCutover
),
removeWslMoveCutover: around(() => 'journal:remove', actual.removeWslMoveCutover),
removeUncommittedWslMoveCutover: around(
() => 'journal:remove-uncommitted',
actual.removeUncommittedWslMoveCutover
)
}
})
const STORE_IDS = ['main-profile', 'renderer-ui', 'browser'] as const
const request = { distro: 'Ubuntu' }
/** `zero-abort-owed`: the desktop is at zero; the host's leftover stage goes on its next contact. */
type Outcome = 'back-at-zero' | 'zero-abort-owed' | 'fully-moved' | 'waiting-for-host' | 'stuck'
type Rig = {
userDataPath: string
kill: WslMoveKillSwitch
world: WslMoveWorld
host: Store
rewritten: Map<string, number>
}
let rig: Rig
const dirs: string[] = []
function tempDir(prefix: string): string {
const dir = mkdtempSync(join(tmpdir(), prefix))
dirs.push(dir)
return dir
}
function newRig(): Rig {
resetWslMoveDesktopRewriteStoresForTests()
setWslMoveDesktopRewriteRegistryCompleteForTests(true)
testState.dir = tempDir('wsl-move-host-')
const userDataPath = tempDir('wsl-move-desktop-')
const kill = createWslMoveKillSwitch()
const world = createWslMoveWorld(userDataPath)
const next: Rig = { userDataPath, kill, world, host: createStore(), rewritten: new Map() }
const at = (name: string): void => passWslMoveKillPoint(kill, name)
journalKill.at = at
const host = createWslMoveStoreHost({ store: () => next.host, kill })
world.deps.connectHost = async () => {
at('connect')
return host
}
const { quiesce } = world.deps
world.deps.quiesce = {
sleepWorkspaces: async (ids) => (at('sleep'), quiesce.sleepWorkspaces(ids)),
livePtyIds: quiesce.livePtyIds,
captureRestoreHints: async (panes, distro) => (
at('restore-hints'),
quiesce.captureRestoreHints(panes, distro)
),
proveExited: async (scope) => (at('prove-exited'), quiesce.proveExited(scope))
}
for (const id of STORE_IDS) {
registerWslMoveDesktopRewriteStore({
id,
side: 'main',
run: () => {
at(`store:${id}:run`)
next.rewritten.set(id, 1)
},
flush: () => at(`store:${id}:flush`)
})
}
return next
}
const retentionStore = (r: Rig) => ({
getRepos: () => r.world.repos,
getFolderWorkspaces: () => [],
setWslMoveRetainedMarks: () => 1,
flushPendingOrThrowAsync: async () => {}
})
/** Startup, then the user's next move attempt reaching the host (which settles the old one). */
async function restart(r: Rig, options: { hostUp: boolean; orcadRestarted: boolean }) {
r.kill.dead = false
if (options.orcadRestarted) {
await r.host.flushPendingOrThrowAsync().catch(() => {})
r.host = createStore()
}
await recoverInterruptedWslMoves(r.userDataPath, () => {})
await runWslMoveDesktopRewritePass({ userDataPath: r.userDataPath, warn: () => {} })
if (options.hostUp) {
// What the next move's prepare does once it reaches the host, before it stages anything.
const key = wslMoveJournalKey('Ubuntu')
const host = await r.world.deps.connectHost('Ubuntu')
if (host) {
await settleWslMoveOwedHostAborts(r.userDataPath, key, host.catalog)
}
const head = listWslMoveCutoverChain(r.userDataPath, key).at(-1)
if (head) {
await resetWslMoveToZero(
{ userDataPath: r.userDataPath, destination: host?.catalog ?? null },
head.migrationId
)
await r.world.deps.finishOnDesktop()
}
}
await runWslMoveRetentionPass({ userDataPath: r.userDataPath, store: retentionStore(r) })
}
function outcomeOf(r: Rig): Outcome {
const records = listWslMoveCutovers(r.userDataPath)
const fence = resolveWslMoveFence(r.userDataPath)
const onHost = r.host.getRepos().some((repo) => repo.id === 'repo-1')
if (records.length === 0) {
const staged = r.host.hasStagedOrcadMigrationCatalog()
const owed = listWslMoveOwedHostAborts(r.userDataPath).length > 0
const clean =
!onHost &&
(!staged || owed) &&
fence.fenced.repoIds.size === 0 &&
fence.hidden.repoIds.size === 0 &&
fence.sessionFrozen.repoIds.size === 0 &&
r.rewritten.size === 0
if (!clean) {
return 'stuck'
}
return owed ? 'zero-abort-owed' : 'back-at-zero'
}
const [record] = records
if (records.length === 1 && record.phase === 'retained') {
const allOnce = STORE_IDS.every((id) => r.rewritten.get(id) === 1)
return allOnce &&
onHost &&
fence.hidden.repoIds.has('repo-1') &&
fence.fenced.repoIds.size === 0
? 'fully-moved'
: 'stuck'
}
if (records.length === 1 && record.phase === 'blobs-transferred') {
return 'waiting-for-host'
}
return 'stuck'
}
beforeEach(() => {
vi.stubEnv(WSL_HOSTS_INTERNAL_GATE_ENV, '1')
// A killed process's catch handlers warn about the calls that now throw.
vi.spyOn(console, 'warn').mockImplementation(() => {})
rig = newRig()
})
afterEach(() => {
journalKill.at = null
vi.restoreAllMocks()
vi.unstubAllEnvs()
resetWslMoveDesktopRewriteStoresForTests()
for (const dir of dirs.splice(0)) {
rmSync(dir, { recursive: true, force: true })
}
})
async function killedMove(r: Rig, killAt: string): Promise<void> {
r.kill.killAt = killAt
await moveProjectsToWslHost(r.world.deps, request).catch(() => {})
// Retention runs on the journal change after the move, in the same process.
await runWslMoveRetentionPass({ userDataPath: r.userDataPath, store: retentionStore(r) }).catch(
() => {}
)
expect(r.kill.dead, `kill point ${killAt} fired`).toBe(true)
}
/** Every kill point a clean move passes, from a throwaway run. */
async function killPoints(): Promise<string[]> {
const probe = newRig()
const result = await moveProjectsToWslHost(probe.world.deps, request)
expect(result).toMatchObject({ outcome: 'moved' })
await runWslMoveRetentionPass({ userDataPath: probe.userDataPath, store: retentionStore(probe) })
return [...new Set(probe.kill.points)]
}
describe('WSL move driver fault injection (real host store)', () => {
it('a clean move ends fully moved on a real host store', async () => {
expect(await moveProjectsToWslHost(rig.world.deps, request)).toMatchObject({
outcome: 'moved'
})
await runWslMoveRetentionPass({ userDataPath: rig.userDataPath, store: retentionStore(rig) })
expect(outcomeOf(rig)).toBe('fully-moved')
})
it.each([false, true])(
'every kill point ends at zero or fully moved, and survives a second restart (orcad restarted: %s)',
{ timeout: 120_000 },
async (orcadRestarted) => {
const points = await killPoints()
expect(points.length).toBeGreaterThan(30)
const matrix: Record<string, { hostDown: Outcome; hostUp: Outcome }> = {}
for (const killAt of points) {
const r = newRig()
await killedMove(r, killAt)
await restart(r, { hostUp: false, orcadRestarted })
const hostDown = outcomeOf(r)
await restart(r, { hostUp: true, orcadRestarted })
const hostUp = outcomeOf(r)
matrix[killAt] = { hostDown, hostUp }
await restart(r, { hostUp: true, orcadRestarted })
expect(outcomeOf(r), `${killAt}: a second restart changes nothing`).toBe(hostUp)
if (hostUp === 'back-at-zero') {
const again = await moveProjectsToWslHost(r.world.deps, request)
expect(again, `a fresh move after ${killAt}`).toMatchObject({ outcome: 'moved' })
}
}
for (const [killAt, result] of Object.entries(matrix)) {
expect(['back-at-zero', 'fully-moved'], `${killAt} with the host up`).toContain(
result.hostUp
)
// Without the host, only a possible commit waits; everything else resolves locally.
expect(
['back-at-zero', 'zero-abort-owed', 'fully-moved', 'waiting-for-host'],
killAt
).toContain(result.hostDown)
}
// The commit point: a kill before the commit lands resets; one after it moves forward.
expect(matrix['commit:before']).toEqual({
hostDown: 'waiting-for-host',
hostUp: 'back-at-zero'
})
expect(matrix['commit:lost-reply']).toEqual({
hostDown: 'waiting-for-host',
hostUp: 'fully-moved'
})
// A kill after the host staged: the desktop resets alone, and the stage goes on reconnect.
expect(matrix['journal:destination-staged:after']).toEqual({
hostDown: 'zero-abort-owed',
hostUp: 'back-at-zero'
})
expect(matrix['store:browser:run']).toEqual({
hostDown: 'fully-moved',
hostUp: 'fully-moved'
})
}
)
})
@@ -22,6 +22,7 @@ import type {
import { assertWslMoveManifestFits } from './wsl-move-manifest-budget'
import { WslMoveScopeChangedError } from './wsl-move-manifest-export'
import { wslMoveHostReadiness } from './wsl-move-preflight'
import { settleWslMoveOwedHostAborts } from './wsl-move-owed-host-aborts'
import { resetWslMoveToZero } from './wsl-move-reset'
import { assessWslMoveTerminals, type WslMoveGateBlocker } from './wsl-move-terminal-gate'
@@ -75,7 +76,10 @@ async function settleInterruptedWslMove(
host: WslMoveHost,
request: WslMoveRequest
): Promise<WslMoveRefusal | null> {
const head = listWslMoveCutoverChain(deps.userDataPath, wslMoveJournalKey(request.distro)).at(-1)
const journalKey = wslMoveJournalKey(request.distro)
// Before this move stages: a leftover stage of an earlier reset would refuse it.
await settleWslMoveOwedHostAborts(deps.userDataPath, journalKey, host.catalog)
const head = listWslMoveCutoverChain(deps.userDataPath, journalKey).at(-1)
if (
!head ||
WSL_MOVE_CUTOVER_PHASES.indexOf(head.phase) >=
@@ -0,0 +1,95 @@
/**
* A stage a reset move may have left on the WSL host. A reset without the host's answer (a restart
* while the host is down) lifts the desktop's record but cannot abort the host's stage, which then
* keeps its claims on those projects for the stage's 7-day lifetime and refuses every retry. The
* reset leaves this record; the next move into the distro aborts that stage by identity first.
*/
import type { OrcadMigrationCatalogReferenceAbortResult } from '../../../shared/orcad-migration/orcad-migration-manifest'
import { isRecord } from '../../../shared/orcad-migration/orcad-migration-manifest-fields'
import type { WslMoveCutover } from '../../../shared/wsl-move/wsl-move-cutover'
import { createMigrationJournalDirectory } from '../migrating-orcad-catalog/migration-journal-directory'
import type { OrcadMigrationDestinationCatalog } from '../../ssh/orcad-migration-cutover-coordinator'
export type WslMoveOwedHostAbort = {
migrationId: string
manifestSha256: string
journalKey: string
}
const owed = createMigrationJournalDirectory<WslMoveOwedHostAbort>({
// Its own directory: builds without it never read it, and their stage simply expires.
directoryName: 'orcad-migration-wsl-owed-aborts',
maxFileBytes: 4096,
parse: parseOwedAbort,
unreadable: (detail) => new Error(`wsl_move_owed_aborts_unreadable: ${detail}`)
})
function parseOwedAbort(value: unknown): WslMoveOwedHostAbort {
if (
!isRecord(value) ||
typeof value.migrationId !== 'string' ||
typeof value.manifestSha256 !== 'string' ||
!/^[a-f0-9]{64}$/.test(value.manifestSha256) ||
typeof value.journalKey !== 'string'
) {
throw new Error('wsl_move_owed_abort_invalid')
}
return {
migrationId: value.migrationId,
manifestSha256: value.manifestSha256,
journalKey: value.journalKey
}
}
/** Durable before it returns, so it lands before the record it outlives is removed. */
export function recordWslMoveOwedHostAbort(
userDataPath: string,
cutover: Pick<WslMoveCutover, 'migrationId' | 'manifestSha256' | 'journalKey'>
): void {
owed.write(userDataPath, {
migrationId: cutover.migrationId,
manifestSha256: cutover.manifestSha256,
journalKey: cutover.journalKey
})
}
export function listWslMoveOwedHostAborts(userDataPath: string): WslMoveOwedHostAbort[] {
return owed.list(userDataPath)
}
/**
* Aborts each owed stage of this distro on its now-connected host. Only an answer settles one:
* a failure keeps it for the next move, whose own stage the leftover would refuse anyway.
*/
export async function settleWslMoveOwedHostAborts(
userDataPath: string,
journalKey: string,
destination: Pick<OrcadMigrationDestinationCatalog, 'abortByReference'>,
warn: (message: string) => void = (message) => console.warn(message)
): Promise<void> {
let pending: WslMoveOwedHostAbort[]
try {
pending = owed.list(userDataPath).filter((entry) => entry.journalKey === journalKey)
} catch (error) {
warn(`[wsl-move] Could not read the owed host aborts: ${describe(error)}`)
return
}
for (const entry of pending) {
let answer: OrcadMigrationCatalogReferenceAbortResult
try {
answer = await destination.abortByReference(entry)
} catch (error) {
warn(`[wsl-move] Could not abort a leftover stage on the host: ${describe(error)}`)
continue
}
if (answer.state === 'committed') {
// No commit was ever requested for it; leave the host's state alone and stop asking.
warn(`[wsl-move] A reset move ${entry.migrationId} reads as committed on the host`)
}
owed.remove(userDataPath, entry.migrationId)
}
}
function describe(error: unknown): string {
return error instanceof Error ? error.message : String(error)
}
@@ -28,6 +28,7 @@ import {
removeUncommittedWslMoveCutover,
writeWslMoveCutover
} from './wsl-move-cutover-journal'
import { recordWslMoveOwedHostAbort } from './wsl-move-owed-host-aborts'
/** The only phase from which the desktop may ask the destination to commit. */
export const WSL_MOVE_COMMIT_REQUEST_PHASE: WslMoveCutoverPhase = 'blobs-transferred'
@@ -53,6 +54,8 @@ export type WslMoveResetContext = {
const phaseIndex = (phase: WslMoveCutoverPhase): number => WSL_MOVE_CUTOVER_PHASES.indexOf(phase)
const COMMITTED_FROM = phaseIndex('destination-committed')
// The stage request goes out from `quiesced`, so from there the host may hold one.
const STAGE_MAY_EXIST_FROM = phaseIndex('quiesced')
export async function resetWslMoveToZero(
context: WslMoveResetContext,
@@ -68,7 +71,13 @@ export async function resetWslMoveToZero(
return { outcome: 'committed', cutover }
}
const commitMayExist = cutover.phase === WSL_MOVE_COMMIT_REQUEST_PHASE
const release = (evidence: 'commit-never-requested' | 'catalog-absent'): WslMoveResetResult => {
const release = (
evidence: 'commit-never-requested' | 'catalog-absent',
hostConfirmedAbsent = false
): WslMoveResetResult => {
if (!hostConfirmedAbsent && phaseIndex(cutover.phase) >= STAGE_MAY_EXIST_FROM) {
recordWslMoveOwedHostAbort(context.userDataPath, cutover)
}
removeUncommittedWslMoveCutover(context.userDataPath, migrationId)
return { outcome: 'reset', evidence }
}
@@ -94,7 +103,7 @@ export async function resetWslMoveToZero(
return commitMayExist ? byReference() : release('commit-never-requested')
}
if (state.state === 'absent') {
return release(commitMayExist ? 'catalog-absent' : 'commit-never-requested')
return release(commitMayExist ? 'catalog-absent' : 'commit-never-requested', true)
}
if (state.state !== 'committed') {
return { outcome: 'unverifiable', reason: `catalog_${state.state}` }
@@ -114,7 +123,10 @@ async function resetByReference(
context: WslMoveResetContext & { destination: OrcadMigrationDestinationCatalog },
cutover: WslMoveCutover,
commitMayExist: boolean,
release: (evidence: 'commit-never-requested' | 'catalog-absent') => WslMoveResetResult
release: (
evidence: 'commit-never-requested' | 'catalog-absent',
hostConfirmedAbsent?: boolean
) => WslMoveResetResult
): Promise<WslMoveResetResult> {
let aborted: OrcadMigrationCatalogReferenceAbortResult
try {
@@ -125,7 +137,7 @@ async function resetByReference(
: release('commit-never-requested')
}
if (aborted.state === 'absent') {
return release(commitMayExist ? 'catalog-absent' : 'commit-never-requested')
return release(commitMayExist ? 'catalog-absent' : 'commit-never-requested', true)
}
if (!commitMayExist) {
return { outcome: 'unverifiable', reason: 'committed_without_commit_request' }
@@ -1,7 +1,7 @@
import { parseOrcadMigrationCatalogReferenceAbortResult } from '../../shared/orcad-migration/orcad-migration-catalog-state'
import type {
OrcadMigrationCatalogReferenceAbortResult,
OrcadMigrationManifest
OrcadMigrationCatalogReference,
OrcadMigrationCatalogReferenceAbortResult
} from '../../shared/orcad-migration/orcad-migration-manifest'
import { ORCAD_MIGRATION_CATALOG_ABORT_BY_REFERENCE_RUNTIME_CAPABILITY } from '../../shared/orcad-runtime-capabilities'
import { parsePairingCode } from '../../shared/pairing'
@@ -19,7 +19,7 @@ export const ORCAD_MIGRATION_DESTINATION_ABORT_BY_REFERENCE_UNSUPPORTED =
/** Sends only the manifest's identity, for a manifest whose whole request would overflow the frame. */
export async function abortRemoteOrcadMigrationCatalogByReference(
pairingCode: string,
manifest: OrcadMigrationManifest,
manifest: OrcadMigrationCatalogReference,
options: CatalogRequestOptions = {}
): Promise<OrcadMigrationCatalogReferenceAbortResult> {
const pairing = parsePairingCode(pairingCode)
@@ -9,6 +9,7 @@
*/
import type {
OrcadMigrationCatalogAbortResult,
OrcadMigrationCatalogReference,
OrcadMigrationCatalogReferenceAbortResult,
OrcadMigrationCatalogState,
OrcadMigrationManifest
@@ -45,7 +46,7 @@ export type OrcadMigrationDestinationCatalog = {
abort: (manifest: OrcadMigrationManifest) => Promise<OrcadMigrationCatalogAbortResult>
/** Sends only the manifest's identity; for a journal whose manifest no request can carry. */
abortByReference: (
manifest: OrcadMigrationManifest
manifest: OrcadMigrationCatalogReference
) => Promise<OrcadMigrationCatalogReferenceAbortResult>
stageChunk: (
request: OrcadMigrationSnapshotChunkRequest
@@ -17,7 +17,8 @@ export type FakeOrcadMigrationDestination = { commits: number } & {
*/
export function fakeOrcadMigrationDestination(): FakeOrcadMigrationDestination {
const states = new Map<string, 'absent' | 'staged' | 'committed'>()
const stateOf = (manifest: OrcadMigrationManifest) => states.get(manifest.migrationId) ?? 'absent'
const stateOf = (manifest: Pick<OrcadMigrationManifest, 'migrationId'>) =>
states.get(manifest.migrationId) ?? 'absent'
const view = (manifest: OrcadMigrationManifest): OrcadMigrationCatalogState => {
const state = stateOf(manifest)
const base = { migrationId: manifest.migrationId, manifestSha256: manifest.manifestSha256 }