feat(wsl-mover): accept local WSL projects on the migration wire, only in their own distro (D1b-2) (#28466)

* wip(wsl-mover): local-wsl manifest source on the wire (D1b-2)

* wip(wsl-mover): narrow SSH-only steps, cover WSL source parsing

* feat(wsl-mover): WSL host orcad accepts its own distro's projects and advertises it (D1b-2)

* fix(wsl-mover): bind a WSL source to its distro registration

* fix(wsl-mover): keep the manifest parser under the line limit
This commit is contained in:
Neil
2026-10-13 18:44:38 -07:00
committed by GitHub
parent a0f3e78815
commit f7235f6e2e
17 changed files with 306 additions and 33 deletions
@@ -0,0 +1,64 @@
import { describe, expect, it } from 'vitest'
import type { OrcadMigrationManifestSource } from '../../shared/orcad-migration-manifest'
import {
assertOrcadMigrationSourceServedHere,
ORCAD_MIGRATION_SOURCE_NOT_THIS_HOST,
servesLocalWslMigrationSource
} from './orcad-migration-wsl-destination'
const WSL_SOURCE: OrcadMigrationManifestSource = {
kind: 'local-wsl',
distro: 'Ubuntu',
scopeRepoIds: ['repo-1'],
scopeFolderWorkspaceIds: [],
generation: 1
}
const SSH_SOURCE: OrcadMigrationManifestSource = {
sshTargetId: 'ssh-1',
sshTargetGeneration: 1,
targetLabel: 'Prod'
}
const WSL_HOST = { ORCA_WSL_ORCAD_HOST: '1', WSL_DISTRO_NAME: 'ubuntu' }
describe('WSL migration destination', () => {
it('serves the WSL source only on the orcad Orca launched inside a distro', () => {
expect(servesLocalWslMigrationSource(WSL_HOST, 'linux')).toBe(true)
// An SSH remote or Orca under WSLg is a WSL guest too, but not a WSL host's orcad.
expect(servesLocalWslMigrationSource({ WSL_DISTRO_NAME: 'Ubuntu' }, 'linux')).toBe(false)
expect(servesLocalWslMigrationSource(WSL_HOST, 'win32')).toBe(false)
})
it('accepts its own distro, case-insensitively', () => {
expect(() => assertOrcadMigrationSourceServedHere(WSL_SOURCE, WSL_HOST, 'linux')).not.toThrow()
})
it.each([
['another distro', { ...WSL_HOST, WSL_DISTRO_NAME: 'Debian' }, 'linux'],
['a plain Linux host', {}, 'linux'],
['a desktop', WSL_HOST, 'darwin']
] as const)('refuses a WSL source on %s', (_label, env, platform) => {
expect(() => assertOrcadMigrationSourceServedHere(WSL_SOURCE, env, platform)).toThrow(
ORCAD_MIGRATION_SOURCE_NOT_THIS_HOST
)
})
it('refuses a source from another registration of the same distro name', () => {
const source = { ...WSL_SOURCE, distroGuid: '{AAAA}' }
expect(() =>
assertOrcadMigrationSourceServedHere(
source,
{ ...WSL_HOST, ORCA_WSL_DISTRO_GUID: '{aaaa}' },
'linux'
)
).not.toThrow()
for (const env of [{ ...WSL_HOST, ORCA_WSL_DISTRO_GUID: '{BBBB}' }, WSL_HOST]) {
expect(() => assertOrcadMigrationSourceServedHere(source, env, 'linux')).toThrow(
ORCAD_MIGRATION_SOURCE_NOT_THIS_HOST
)
}
})
it('leaves the SSH conversion alone everywhere', () => {
expect(() => assertOrcadMigrationSourceServedHere(SSH_SOURCE, {}, 'linux')).not.toThrow()
})
})
@@ -0,0 +1,40 @@
/**
* Only the orcad an Orca desktop launched inside a distro may receive that distro's projects: the
* move keeps every file where it is, so any other host would own paths it cannot see.
*/
import type { OrcadMigrationManifestSource } from '../../shared/orcad-migration-manifest'
import { isOrcadMigrationLocalWslSource } from '../../shared/orcad-migration-source-kind'
import { isWslHostOrcadProcess } from '../ipc/wsl-guest-windows-drive-path'
export const ORCAD_MIGRATION_SOURCE_NOT_THIS_HOST = 'orcad_migration_source_not_this_host'
/** The distro's registry id, which the desktop passes at launch (D1b-4); the guest cannot read it. */
export const ORCAD_WSL_DISTRO_GUID_ENV = 'ORCA_WSL_DISTRO_GUID'
/** True on the orcad of a WSL host, which is the only host that advertises the WSL source kind. */
export function servesLocalWslMigrationSource(
env: NodeJS.ProcessEnv = process.env,
platform: NodeJS.Platform = process.platform
): boolean {
return isWslHostOrcadProcess(env, platform)
}
export function assertOrcadMigrationSourceServedHere(
source: OrcadMigrationManifestSource,
env: NodeJS.ProcessEnv = process.env,
platform: NodeJS.Platform = process.platform
): void {
if (!isOrcadMigrationLocalWslSource(source)) {
return
}
// WSL treats distro names case-insensitively, and so does every UNC path to one.
const distro = env.WSL_DISTRO_NAME?.trim().toLowerCase()
if (!servesLocalWslMigrationSource(env, platform) || distro !== source.distro.toLowerCase()) {
throw new Error(ORCAD_MIGRATION_SOURCE_NOT_THIS_HOST)
}
// A re-registered distro reuses the name but not the files. Unknown here fails closed.
const guid = env[ORCAD_WSL_DISTRO_GUID_ENV]?.trim().toLowerCase()
if (source.distroGuid !== undefined && guid !== source.distroGuid.toLowerCase()) {
throw new Error(ORCAD_MIGRATION_SOURCE_NOT_THIS_HOST)
}
}
@@ -5,6 +5,7 @@
*/
import type { StoreRuntimeState } from '../loading-store/store-runtime-state'
import type { OrcadMigrationManifest } from '../../../shared/orcad-migration-manifest'
import { requireOrcadMigrationSshSource } from '../../../shared/orcad-migration-source-kind'
import { orcadSourceFolderWorkspaceIds, repoBelongsToOrcadSource } from './orcad-source-ownership'
export function subtractOrcadSourceCatalogState(
@@ -14,7 +15,7 @@ export function subtractOrcadSourceCatalogState(
const repoIds = new Set(manifest.payload.repositories.map((repo) => repo.id))
const folderIds = new Set(manifest.payload.folderWorkspaces.map((workspace) => workspace.id))
const groupIds = new Set(manifest.payload.projectGroups.map((group) => group.id))
const targetId = manifest.source.sshTargetId
const targetId = requireOrcadMigrationSshSource(manifest.source).sshTargetId
// Before any repo goes: a folder owned through the repos inside it would read as local after.
const ownedFolderIds = orcadSourceFolderWorkspaceIds(state, targetId)
state.repos = state.repos.filter(
@@ -23,7 +24,7 @@ export function subtractOrcadSourceCatalogState(
state.folderWorkspaces = state.folderWorkspaces.filter(
(workspace) => !(folderIds.has(workspace.id) && ownedFolderIds.has(workspace.id))
)
removeUnreferencedSourceGroups(state, groupIds, manifest.source.sshTargetId)
removeUnreferencedSourceGroups(state, groupIds, targetId)
}
function removeUnreferencedSourceGroups(
@@ -1,5 +1,6 @@
import type { OrcadMigrationManifest } from '../../../shared/orcad-migration-manifest'
import { serializeOrcadMigrationValue } from '../../../shared/orcad-migration-manifest'
import { requireOrcadMigrationSshSource } from '../../../shared/orcad-migration-source-kind'
import { parsePersistedAutomationHostFilter } from '../../../shared/automation-host-filter'
import { hostStableKey } from '../../../shared/automation-owner-key'
import { toRuntimeExecutionHostId } from '../../../shared/execution-host'
@@ -47,7 +48,9 @@ export function subtractOrcadMigrationClientState(
}
}
}
const target = state.sshTargets.find((entry) => entry.id === source.sshTargetId)
const target = state.sshTargets.find(
(entry) => entry.id === requireOrcadMigrationSshSource(source).sshTargetId
)
if (target && clientState.savedPortForwards) {
target.portForwards = structuredClone(clientState.savedPortForwards)
}
@@ -1,3 +1,4 @@
import { requireOrcadMigrationSshSource } from '../../../shared/orcad-migration-source-kind'
import { LOCAL_EXECUTION_HOST_ID, toSshExecutionHostId } from '../../../shared/execution-host'
import type {
OrcadMigrationCatalogPayload,
@@ -43,10 +44,11 @@ export function createOrcadMigrationSourceScope(args: {
repos: readonly Repo[]
}): OrcadMigrationSourceScope {
const repoIds = new Set(args.catalog.repositories.map((repo) => repo.id))
const source = requireOrcadMigrationSshSource(args.source)
return {
targetId: args.source.sshTargetId,
targetGeneration: args.source.sshTargetGeneration,
hostId: toSshExecutionHostId(args.source.sshTargetId),
targetId: source.sshTargetId,
targetGeneration: source.sshTargetGeneration,
hostId: toSshExecutionHostId(source.sshTargetId),
repoIds,
folderWorkspaceKeys: new Set(
args.catalog.folderWorkspaces.map((workspace) => `folder:${workspace.id}`)
@@ -54,7 +56,7 @@ export function createOrcadMigrationSourceScope(args: {
sharedRepoIds: new Set(
args.repos
.filter(
(repo) => repoIds.has(repo.id) && !repoBelongsToOrcadSource(repo, args.source.sshTargetId)
(repo) => repoIds.has(repo.id) && !repoBelongsToOrcadSource(repo, source.sshTargetId)
)
.map((repo) => repo.id)
)
@@ -6,6 +6,8 @@ import {
runtimeBrowserUnavailableCause
} from './runtime-browser-commands-factory'
import { isBrowserIdentityModeStoreInitialized } from '../browser/browser-identity-mode-store'
import { servesLocalWslMigrationSource } from '../orcad/orcad-migration-wsl-destination'
import { ORCAD_MIGRATION_LOCAL_WSL_SOURCE_RUNTIME_CAPABILITY } from '../../shared/orcad-runtime-capabilities'
import type { RuntimeCapability } from '../../shared/protocol-version'
import {
BROWSER_CERTIFICATE_TRUST_RUNTIME_CAPABILITY,
@@ -117,6 +119,10 @@ export class OrcaRuntimeWithGetStatus extends OrcaRuntimeWithGetRuntimeId {
if (isBrowserIdentityModeStoreInitialized()) {
capabilities.push(BROWSER_IDENTITY_RUNTIME_CAPABILITY)
}
// Why not static: only the orcad inside a distro can receive that distro's projects.
if (servesLocalWslMigrationSource()) {
capabilities.push(ORCAD_MIGRATION_LOCAL_WSL_SOURCE_RUNTIME_CAPABILITY)
}
// Why: certificate proceed is owned by the browser-hosting process for both
// desktop webviews and offscreen pages. Advertise whenever either backend
// can host a page so remote clients can surface Proceed Anyway (Unsafe).
@@ -3,6 +3,7 @@ import { OrcadMigrationCatalogParams } from '../../../../shared/rpc-contract/orc
import { parseOrcadMigrationManifest } from '../../../../shared/orcad-migration-manifest'
import { OrcadMigrationSnapshotChunkRequestSchema } from '../../../../shared/orcad-migration-scrollback'
import { assertOrcadMigrationManifestDigest } from '../../../orcad/orcad-migration-manifest-digest'
import { assertOrcadMigrationSourceServedHere } from '../../../orcad/orcad-migration-wsl-destination'
import { defineMethod, type RpcContext } from '../core'
export const ORCAD_MIGRATION_METHODS = [
@@ -63,6 +64,7 @@ function migrationManifest(params: z.infer<typeof OrcadMigrationCatalogParams>)
const manifest = parseOrcadMigrationManifest(params.manifest)
// Raw, not parsed: parsing drops fields this host predates, which a newer client signed.
assertOrcadMigrationManifestDigest(params.manifest)
assertOrcadMigrationSourceServedHere(manifest.source)
return manifest
}
@@ -56,11 +56,17 @@ function sshManifest(): OrcadMigrationManifest {
return { ...unsigned, manifestSha256: computeOrcadMigrationManifestSha256(unsigned) }
}
/** A future kind as the wire carries it; this build's types know only the SSH source. */
/** A WSL source, or any other `kind` as a newer or confused peer could put on the wire. */
function manifestOfKind(kind: string): OrcadMigrationManifest {
const base = sshManifest()
const source = { ...base.source, kind }
return { ...base, source }
const source: OrcadMigrationManifest['source'] = {
kind: 'local-wsl',
distro: 'Ubuntu',
scopeRepoIds: [],
scopeFolderWorkspaceIds: [],
generation: 1
}
Object.assign(source, { kind })
return { ...sshManifest(), source }
}
const pairingCode = encodePairingOffer({
@@ -127,7 +127,7 @@ describe('orcad migration manifest export', () => {
{ migrationId: 'migration-legacy', now: () => new Date('2026-08-30T12:00:00.000Z') }
)
expect(manifest.source.sshTargetGeneration).toBeNull()
expect(manifest.source).toMatchObject({ sshTargetGeneration: null })
expect(target.generation).toBeUndefined()
})
@@ -1,5 +1,9 @@
// Field checks shared by the manifest and receipt parsers; each throws a labeled error.
/** Catalog bounds, shared by the manifest parser and the WSL source's scope lists. */
export const MAX_ORCAD_MIGRATION_REPOSITORIES = 1_024
export const MAX_ORCAD_MIGRATION_FOLDER_WORKSPACES = 16_384
/** The one bounded-array check; `errorPrefix` names the manifest or dormant error family. */
export function boundedList<T>(
value: unknown,
@@ -5,9 +5,15 @@ import type {
OrcadMigrationManifest,
OrcadMigrationManifestSource
} from './orcad-migration-manifest'
import {
assertLocalWslScopeMatchesPayload,
parseLocalWslSource
} from './orcad-migration-wsl-source-validation'
import type { ProjectGroup } from './project-group-types'
import type { Repo } from './repo-types'
import {
MAX_ORCAD_MIGRATION_FOLDER_WORKSPACES,
MAX_ORCAD_MIGRATION_REPOSITORIES,
assertUniqueIds,
boundedArray,
boundedStringArray,
@@ -21,12 +27,11 @@ import {
parseOrcadMigrationDormantState
} from './orcad-migration-dormant-state-validation'
export { MAX_ORCAD_MIGRATION_FOLDER_WORKSPACES, MAX_ORCAD_MIGRATION_REPOSITORIES }
export const ORCAD_MIGRATION_MANIFEST_VERSION = 1 as const
export type OrcadMigrationManifestVersion = typeof ORCAD_MIGRATION_MANIFEST_VERSION
export const MAX_ORCAD_MIGRATION_MANIFEST_BYTES = 768 * 1024
export const MAX_ORCAD_MIGRATION_REPOSITORIES = 1_024
export const MAX_ORCAD_MIGRATION_PROJECT_GROUPS = 4_096
export const MAX_ORCAD_MIGRATION_FOLDER_WORKSPACES = 16_384
export const MAX_ORCAD_MIGRATION_IMPORT_RECEIPTS = 64
export function parseOrcadMigrationManifest(value: unknown): OrcadMigrationManifest {
@@ -64,6 +69,7 @@ export function parseOrcadMigrationManifest(value: unknown): OrcadMigrationManif
if (bytes > MAX_ORCAD_MIGRATION_MANIFEST_BYTES) {
throw new Error('orcad_migration_manifest_too_large')
}
assertLocalWslScopeMatchesPayload(manifest)
return manifest
}
@@ -144,7 +150,10 @@ function parseSource(value: unknown): OrcadMigrationManifestSource {
if (!isRecord(value)) {
throw new Error('orcad_migration_manifest_source_invalid')
}
// Only the relay-era SSH source (no `kind`) is parsed here; other kinds ship with their capability.
if (value.kind === 'local-wsl') {
return parseLocalWslSource(value)
}
// An explicit 'ssh-relay' is refused too: SSH has exactly one encoding, so one digest.
if (value.kind !== undefined) {
throw new Error('orcad_migration_manifest_source_kind_unsupported')
}
+3 -3
View File
@@ -49,9 +49,9 @@ describe('orcad migration manifest', () => {
expect(normalized).toHaveLength(MAX_ORCAD_MIGRATION_IMPORT_RECEIPTS)
expect(normalized.some((entry) => entry.migrationId === 'migration-0')).toBe(false)
expect(
normalized.find((entry) => entry.migrationId === 'migration-10')?.source.targetLabel
).toBe('Newest')
expect(normalized.find((entry) => entry.migrationId === 'migration-10')?.source).toMatchObject({
targetLabel: 'Newest'
})
})
it('normalizes receipts without Node 20-only array methods', () => {
+17 -1
View File
@@ -42,7 +42,23 @@ export type OrcadMigrationSshManifestSource = {
targetLabel: string
}
export type OrcadMigrationManifestSource = OrcadMigrationSshManifestSource
/**
* Local WSL projects on this desktop, moving to the orcad that runs inside their distro. Scope is
* explicit so a destination never infers it from paths, and `generation` orders delta moves.
*/
export type OrcadMigrationLocalWslManifestSource = {
kind: 'local-wsl'
distro: string
/** The distro's registry id, when known: a re-registered distro of the same name is another one. */
distroGuid?: string
scopeRepoIds: string[]
scopeFolderWorkspaceIds: string[]
generation: number
}
export type OrcadMigrationManifestSource =
| OrcadMigrationSshManifestSource
| OrcadMigrationLocalWslManifestSource
export type OrcadMigrationCatalogPayload = {
repositories: Repo[]
+5 -2
View File
@@ -7,6 +7,7 @@ import {
parseOrcadMigrationManifest,
type OrcadMigrationManifest
} from './orcad-migration-manifest'
import { isOrcadMigrationLocalWslSource } from './orcad-migration-source-kind'
export const ORCAD_MIGRATION_SOURCE_CUTOVER_VERSION = 1
export const MAX_ORCAD_MIGRATION_SOURCE_CUTOVERS = 4
@@ -64,11 +65,13 @@ export function isRetainedOrcadMigrationSourceCutover(
export function parseOrcadMigrationSourceCutover(value: unknown): OrcadMigrationSourceCutover {
const record = CutoverRecordSchema.parse(value)
const manifest = parseOrcadMigrationManifest(record.manifest)
// An SSH journal binds an SSH manifest; a WSL manifest here is a mismatch, never a fallback.
const source = isOrcadMigrationLocalWslSource(manifest.source) ? null : manifest.source
if (
manifest.migrationId !== record.migrationId ||
manifest.manifestSha256 !== record.manifestSha256 ||
manifest.source.sshTargetId !== record.sshTargetId ||
manifest.source.sshTargetGeneration !== record.sshTargetGeneration ||
source?.sshTargetId !== record.sshTargetId ||
source.sshTargetGeneration !== record.sshTargetGeneration ||
manifest.destinationEnvironmentId !== record.destinationEnvironmentId
) {
throw new Error('orcad_migration_cutover_binding_mismatch')
+47 -12
View File
@@ -1,4 +1,5 @@
import { describe, expect, it } from 'vitest'
import { isRecord } from './orcad-migration-manifest-fields'
import {
ORCAD_MIGRATION_MANIFEST_VERSION,
normalizeOrcadMigrationImportReceipts,
@@ -15,6 +16,20 @@ import {
} from './orcad-runtime-capabilities'
const SSH_SOURCE = { sshTargetId: 'ssh-1', sshTargetGeneration: 2, targetLabel: 'Prod' }
const WSL_SOURCE = {
kind: 'local-wsl',
distro: 'Ubuntu',
scopeRepoIds: ['repo-1'],
scopeFolderWorkspaceIds: [],
generation: 1
}
const WSL_REPO = {
id: 'repo-1',
path: '/home/dev/app',
displayName: 'app',
badgeColor: '#000',
addedAt: 0
}
/** Exactly what a desktop that predates `source.kind` sends. */
function manifestWithSource(source: unknown) {
@@ -23,7 +38,11 @@ function manifestWithSource(source: unknown) {
migrationId: 'migration-kind',
createdAt: '2026-10-01T00:00:00.000Z',
source,
payload: { repositories: [], projectGroups: [], folderWorkspaces: [] },
payload: {
repositories: isRecord(source) && source.kind === 'local-wsl' ? [WSL_REPO] : [],
projectGroups: [],
folderWorkspaces: []
},
manifestSha256: 'a'.repeat(64)
}
}
@@ -35,7 +54,7 @@ describe('orcad migration source kind', () => {
expect(requiredOrcadMigrationSourceCapability(SSH_SOURCE)).toBeNull()
})
it('gates a WSL source on its own capability, which no destination advertises yet', () => {
it('gates a WSL source on its own capability, which only a WSL host advertises', () => {
expect(requiredOrcadMigrationSourceCapability({ kind: 'local-wsl' })).toBe(
ORCAD_MIGRATION_LOCAL_WSL_SOURCE_RUNTIME_CAPABILITY
)
@@ -67,16 +86,32 @@ describe('orcad migration manifest source on the destination', () => {
expect(normalizeOrcadMigrationImportReceipts([receipt])).toEqual([receipt])
})
it('refuses any source kind until it ships the parser for that kind', () => {
for (const source of [
{ ...SSH_SOURCE, kind: 'local-wsl' },
{ kind: 'local-wsl', distro: 'Ubuntu', targetLabel: 'Ubuntu' },
{ ...SSH_SOURCE, kind: 'ssh-relay' }
]) {
expect(() => parseOrcadMigrationManifest(manifestWithSource(source))).toThrow(
'orcad_migration_manifest_source_kind_unsupported'
)
}
it('refuses an explicit SSH kind, and a WSL source carrying SSH fields', () => {
expect(() =>
parseOrcadMigrationManifest(manifestWithSource({ ...SSH_SOURCE, kind: 'ssh-relay' }))
).toThrow('orcad_migration_manifest_source_kind_unsupported')
expect(() =>
parseOrcadMigrationManifest(manifestWithSource({ ...WSL_SOURCE, sshTargetId: 'ssh-1' }))
).toThrow('orcad_migration_manifest_source_invalid')
})
it('parses a WSL source whose scope is exactly the catalog it moves', () => {
const manifest = parseOrcadMigrationManifest(
manifestWithSource({ ...WSL_SOURCE, distroGuid: '{guid}' })
)
expect(manifest.source).toEqual({ ...WSL_SOURCE, distroGuid: '{guid}' })
})
it.each([
{ scopeRepoIds: ['repo-other'] },
{ scopeRepoIds: [] },
{ scopeFolderWorkspaceIds: ['folder-1'] },
{ generation: 0 },
{ distro: '' }
])('refuses a WSL source that does not describe its catalog: %j', (override) => {
expect(() =>
parseOrcadMigrationManifest(manifestWithSource({ ...WSL_SOURCE, ...override }))
).toThrow(/source/)
})
it('fails a WSL source closed on a destination that drops the kind', () => {
+21
View File
@@ -6,6 +6,11 @@
* sets `kind` and must never carry `sshTargetId`: an older destination strips unknown fields, so a
* non-SSH source without it fails that destination's parse instead of importing as an SSH host.
*/
import type {
OrcadMigrationLocalWslManifestSource,
OrcadMigrationManifestSource,
OrcadMigrationSshManifestSource
} from './orcad-migration-manifest'
import { ORCAD_MIGRATION_LOCAL_WSL_SOURCE_RUNTIME_CAPABILITY } from './orcad-runtime-capabilities'
export const ORCAD_MIGRATION_SSH_RELAY_SOURCE_KIND = 'ssh-relay' as const
@@ -52,3 +57,19 @@ export function requiredOrcadMigrationSourceCapability(
? null
: ORCAD_MIGRATION_SOURCE_KIND_CAPABILITIES[kind]
}
export function isOrcadMigrationLocalWslSource(
source: OrcadMigrationManifestSource
): source is OrcadMigrationLocalWslManifestSource {
return 'kind' in source && source.kind === 'local-wsl'
}
/** For the SSH conversion's own steps: a WSL manifest reaching them is a bug, never a fallback. */
export function requireOrcadMigrationSshSource(
source: OrcadMigrationManifestSource
): OrcadMigrationSshManifestSource {
if (isOrcadMigrationLocalWslSource(source)) {
throw new Error('orcad_migration_source_not_ssh')
}
return source
}
@@ -0,0 +1,61 @@
/** Parsing for the `local-wsl` manifest source: a desktop's own WSL projects, moving into their distro. */
import type {
OrcadMigrationLocalWslManifestSource,
OrcadMigrationManifest
} from './orcad-migration-manifest'
import {
MAX_ORCAD_MIGRATION_FOLDER_WORKSPACES,
MAX_ORCAD_MIGRATION_REPOSITORIES,
boundedStringArray,
requiredString
} from './orcad-migration-manifest-fields'
import { isOrcadMigrationLocalWslSource } from './orcad-migration-source-kind'
export function parseLocalWslSource(
value: Record<string, unknown>
): OrcadMigrationLocalWslManifestSource {
// A non-SSH source naming an SSH target could only be a confused or forged manifest.
if (value.sshTargetId !== undefined) {
throw new Error('orcad_migration_manifest_source_invalid')
}
const generation = value.generation
if (!Number.isSafeInteger(generation) || Number(generation) < 1) {
throw new Error('orcad_migration_manifest_source_generation_invalid')
}
return {
kind: 'local-wsl',
distro: requiredString(value.distro, 'source.distro'),
...(value.distroGuid === undefined
? {}
: { distroGuid: requiredString(value.distroGuid, 'source.distroGuid') }),
scopeRepoIds: boundedStringArray(
value.scopeRepoIds,
MAX_ORCAD_MIGRATION_REPOSITORIES,
'source.scopeRepoIds_invalid'
),
scopeFolderWorkspaceIds: boundedStringArray(
value.scopeFolderWorkspaceIds,
MAX_ORCAD_MIGRATION_FOLDER_WORKSPACES,
'source.scopeFolderWorkspaceIds_invalid'
),
generation: Number(generation)
}
}
/** The catalog a WSL source moves is exactly its declared scope; the host never infers scope. */
export function assertLocalWslScopeMatchesPayload(manifest: OrcadMigrationManifest): void {
const { source, payload } = manifest
if (!isOrcadMigrationLocalWslSource(source)) {
return
}
const sameIds = (declared: readonly string[], rows: readonly { id: string }[]): boolean => {
const ids = new Set(rows.map((row) => row.id))
return declared.length === ids.size && declared.every((id) => ids.has(id))
}
if (
!sameIds(source.scopeRepoIds, payload.repositories) ||
!sameIds(source.scopeFolderWorkspaceIds, payload.folderWorkspaces)
) {
throw new Error('orcad_migration_manifest_source_scope_mismatch')
}
}