| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| |
| import assert from 'node:assert/strict'; |
| import { mkdir, mkdtemp, rm, stat, writeFile } from 'node:fs/promises'; |
| import { tmpdir } from 'node:os'; |
| import { join } from 'node:path'; |
| import test from 'node:test'; |
| import { |
| applyLocalHostDeploymentTransition, |
| claimLocalHostProcessDeployment, |
| handoffLocalHostProcessDeployment, |
| readLocalHostDeploymentRecord, |
| type RuntimeHostInstallationOwner, |
| type resolveRuntimeHostManagedDeploymentAuthority, |
| } from '@maka/runtime-host/operator'; |
| import { |
| INTERACTIVE_RUNTIME_HOST_COMPOSITION_ID, |
| RUNTIME_HOST_COMPATIBILITY_EPOCH, |
| RUNTIME_HOST_PROTOCOL_VERSION, |
| RUNTIME_HOST_REGISTRATION_SCHEMA_VERSION, |
| type HostRegistration, |
| } from '@maka/runtime-host/protocol'; |
| import { |
| reconcileRuntimeHostNpmGlobalDeployment, |
| reconcilePreparedRuntimeHostNpmGlobalDeployment, |
| resolveRuntimeHostLocalCliDeploymentRoot, |
| restartRuntimeHostNpmGlobalDeployment, |
| RuntimeHostLocalHandoffError, |
| stageRuntimeHostNpmGlobalDeploymentTarget, |
| } from '../runtime-host-local-handoff.js'; |
| import { prepareRuntimeHostPackageDeployment } from '../runtime-host-package-deployment.js'; |
| |
| const ROOT_ID = 'a'.repeat(64); |
| const INTEGRITY = `sha512-${Buffer.alloc(64, 7).toString('base64')}`; |
| const TARGET = { |
| kind: 'npm_registry' as const, |
| version: '2.0.0', |
| integrity: INTEGRITY, |
| }; |
| const CLI_OWNER = { |
| kind: 'cli' as const, |
| installationId: 'npm-global:stable-slot', |
| }; |
| const DESKTOP_OWNER: RuntimeHostInstallationOwner = { |
| kind: 'desktop', |
| installationId: 'desktop:stable', |
| }; |
| const PREVIOUS = { |
| kind: 'npm_registry' as const, |
| version: '1.0.0', |
| integrity: `sha512-${Buffer.alloc(64, 3).toString('base64')}`, |
| }; |
| |
| test('captured update owner is fenced again inside authority after its preliminary read', async (t) => { |
| const base = await mkdtemp(join(tmpdir(), 'maka-local-update-source-fence-')); |
| t.after(() => rm(base, { recursive: true, force: true })); |
| const options = { authorityRoot: join(base, 'authority') }; |
| const claimed = await applyLocalHostDeploymentTransition( |
| ROOT_ID, |
| { |
| kind: 'claim', |
| owner: CLI_OWNER, |
| selected: PREVIOUS, |
| }, |
| options, |
| ); |
| assert.ok(claimed.record); |
| const captured = claimed.record; |
| const result = await reconcilePreparedRuntimeHostNpmGlobalDeployment( |
| { |
| rootId: ROOT_ID, |
| transactionId: 'fenced-update', |
| target: TARGET, |
| activeWorkPolicy: 'refuse_active_work', |
| expectedOwner: { revision: captured.revision, owner: CLI_OWNER }, |
| installation: { |
| owner: CLI_OWNER, |
| observedRelease: { |
| version: PREVIOUS.version, |
| packageRoot: '/installed', |
| cliPath: '/installed/dist/cli.js', |
| }, |
| }, |
| staged: { |
| version: TARGET.version, |
| root: '/staged', |
| packageRoot: '/staged', |
| cliPath: '/staged/dist/cli.js', |
| candidateEntrypoint: '/staged/candidate.js', |
| launchGeneration: 'fenced-update', |
| cleanup: async () => {}, |
| rollback: async () => {}, |
| }, |
| }, |
| { |
| prepareUnownedHostCutover: async () => assert.fail('must not claim a changed owner'), |
| prepareHostCutover: async () => assert.fail('must not retire a changed owner'), |
| observeWriterRelease: async () => assert.fail('must not observe writer release'), |
| activateTarget: async () => assert.fail('must not activate'), |
| verifyTargetReady: async () => assert.fail('must not verify target'), |
| }, |
| options, |
| { |
| claim: claimLocalHostProcessDeployment, |
| handoff: handoffLocalHostProcessDeployment, |
| readRecord: async () => { |
| // A real competing owner transition lands after the observation but |
| // before the update takes its authority lease. The old CAS must survive. |
| await applyLocalHostDeploymentTransition( |
| ROOT_ID, |
| { |
| kind: 'release', |
| expectedRevision: captured.revision, |
| owner: CLI_OWNER, |
| }, |
| options, |
| ); |
| await applyLocalHostDeploymentTransition( |
| ROOT_ID, |
| { |
| kind: 'claim', |
| owner: DESKTOP_OWNER, |
| selected: PREVIOUS, |
| }, |
| options, |
| ); |
| return captured; |
| }, |
| }, |
| ); |
| assert.equal(result.kind, 'rejected'); |
| if (result.kind !== 'rejected') assert.fail('source owner fence must reject'); |
| assert.equal(result.reason, 'owner_changed'); |
| const current = await readLocalHostDeploymentRecord(ROOT_ID, options); |
| assert.equal(current?.state.kind, 'owned'); |
| if (current?.state.kind === 'owned') assert.deepEqual(current.state.owner, DESKTOP_OWNER); |
| }); |
| |
| test('local CLI deployment roots are stable for one OS account and isolated by owner and root', () => { |
| const first = resolveRuntimeHostLocalCliDeploymentRoot(ROOT_ID, CLI_OWNER, { |
| platform: 'linux', |
| homeDir: '/home/maka', |
| }); |
| assert.equal( |
| resolveRuntimeHostLocalCliDeploymentRoot(ROOT_ID, CLI_OWNER, { |
| platform: 'linux', |
| homeDir: '/home/maka', |
| }), |
| first, |
| ); |
| assert.match(first, /^\/home\/maka\/\.local\/share\/Maka\/runtime-host-deployments\/cli\//u); |
| assert.notEqual( |
| resolveRuntimeHostLocalCliDeploymentRoot( |
| ROOT_ID, |
| { ...CLI_OWNER, installationId: 'npm-global:other-slot' }, |
| { platform: 'linux', homeDir: '/home/maka' }, |
| ), |
| first, |
| ); |
| assert.notEqual( |
| resolveRuntimeHostLocalCliDeploymentRoot('b'.repeat(64), CLI_OWNER, { |
| platform: 'linux', |
| homeDir: '/home/maka', |
| }), |
| first, |
| ); |
| }); |
| |
| test('exact registry evidence becomes a persistent transaction-fenced Host candidate', async (t) => { |
| const base = await mkdtemp(join(tmpdir(), 'maka-local-handoff-stage-')); |
| t.after(() => rm(base, { recursive: true, force: true })); |
| const sourcePackageRoot = await selfContainedPackage(base, TARGET.version); |
| const pathOptions = { platform: 'linux' as const, homeDir: join(base, 'home') }; |
| const staged = await stageRuntimeHostNpmGlobalDeploymentTarget( |
| { |
| rootId: ROOT_ID, |
| owner: CLI_OWNER, |
| target: TARGET, |
| transactionId: 'transaction-one', |
| }, |
| pathOptions, |
| { |
| withPackage: async (candidate, use) => { |
| assert.deepEqual(candidate, TARGET); |
| return use(sourcePackageRoot); |
| }, |
| prepareDeployment: prepareRuntimeHostPackageDeployment, |
| }, |
| ); |
| |
| await rm(sourcePackageRoot, { recursive: true, force: true }); |
| assert.equal((await stat(staged.candidateEntrypoint)).isFile(), true); |
| assert.match(staged.root, /runtime-host-deployments\/cli/u); |
| assert.match(staged.root, new RegExp(`${ROOT_ID}$`, 'u')); |
| assert.match(staged.packageRoot, /registry-[a-f0-9]{64}$/u); |
| assert.match(staged.launchGeneration, /^npm-global-handoff:[a-f0-9]{64}$/u); |
| |
| const retried = await stageRuntimeHostNpmGlobalDeploymentTarget( |
| { |
| rootId: ROOT_ID, |
| owner: CLI_OWNER, |
| target: TARGET, |
| transactionId: 'transaction-one', |
| }, |
| pathOptions, |
| { |
| withPackage: async (_candidate, use) => use(staged.packageRoot), |
| prepareDeployment: prepareRuntimeHostPackageDeployment, |
| }, |
| ); |
| assert.equal(retried.packageRoot, staged.packageRoot); |
| assert.equal(retried.launchGeneration, staged.launchGeneration); |
| }); |
| |
| test('npm-global handoff stages before the one durable owner transaction', async (t) => { |
| const base = await mkdtemp(join(tmpdir(), 'maka-local-handoff-compose-')); |
| t.after(() => rm(base, { recursive: true, force: true })); |
| const sourcePackageRoot = await selfContainedPackage(base, TARGET.version); |
| const authorityRoot = join(base, 'authority'); |
| const claimed = await applyLocalHostDeploymentTransition( |
| ROOT_ID, |
| { kind: 'claim', owner: DESKTOP_OWNER, selected: PREVIOUS }, |
| { authorityRoot }, |
| ); |
| assert.equal(claimed.kind, 'applied'); |
| const events: string[] = []; |
| |
| const result = await reconcileRuntimeHostNpmGlobalDeployment( |
| { |
| rootId: ROOT_ID, |
| transactionId: 'desktop-to-cli', |
| target: TARGET, |
| activeWorkPolicy: 'refuse_active_work', |
| deploymentPathOptions: { platform: 'linux', homeDir: join(base, 'home') }, |
| }, |
| { |
| prepareUnownedHostCutover: async () => assert.fail('owner record already exists'), |
| async prepareHostCutover(rootId, _selected, target, staged, policy) { |
| const intent = await readLocalHostDeploymentRecord(rootId, { authorityRoot }); |
| assert.equal(intent?.state.kind, 'handoff'); |
| assert.equal((await stat(staged.candidateEntrypoint)).isFile(), true); |
| assert.deepEqual(target, TARGET); |
| events.push(`retire:${policy}`); |
| return { kind: 'target_absent' }; |
| }, |
| async observeWriterRelease() { |
| events.push('writer-released'); |
| }, |
| async activateTarget(_rootId, staged) { |
| events.push(`activate:${staged.launchGeneration}`); |
| }, |
| async verifyTargetReady(_rootId, target, staged) { |
| assert.deepEqual(target, TARGET); |
| assert.equal((await stat(staged.candidateEntrypoint)).isFile(), true); |
| events.push('ready'); |
| }, |
| }, |
| { authorityRoot }, |
| { |
| resolveInstallation: async () => ({ |
| owner: CLI_OWNER, |
| observedRelease: { |
| version: TARGET.version, |
| packageRoot: sourcePackageRoot, |
| cliPath: join(sourcePackageRoot, 'dist', 'cli.js'), |
| }, |
| }), |
| withPackage: async (_candidate, use) => use(sourcePackageRoot), |
| prepareDeployment: prepareRuntimeHostPackageDeployment, |
| }, |
| ); |
| |
| assert.equal(result.kind, 'completed'); |
| assert.equal(events.length, 4); |
| assert.equal(events[0], 'retire:refuse_active_work'); |
| assert.equal(events[1], 'writer-released'); |
| assert.match(events[2] ?? '', /^activate:npm-global-handoff:/u); |
| assert.equal(events[3], 'ready'); |
| assert.equal(result.record.state.kind, 'owned'); |
| assert.deepEqual(result.record.state.owner, CLI_OWNER); |
| assert.deepEqual(result.record.state.selected, TARGET); |
| }); |
| |
| test('package verification failure leaves deployment authority unchanged', async (t) => { |
| const base = await mkdtemp(join(tmpdir(), 'maka-local-handoff-stage-failure-')); |
| t.after(() => rm(base, { recursive: true, force: true })); |
| const authorityRoot = join(base, 'authority'); |
| const claimed = await applyLocalHostDeploymentTransition( |
| ROOT_ID, |
| { kind: 'claim', owner: DESKTOP_OWNER, selected: PREVIOUS }, |
| { authorityRoot }, |
| ); |
| assert.equal(claimed.kind, 'applied'); |
| |
| await assert.rejects( |
| reconcileRuntimeHostNpmGlobalDeployment( |
| { |
| rootId: ROOT_ID, |
| transactionId: 'failed-staging', |
| target: TARGET, |
| activeWorkPolicy: 'refuse_active_work', |
| deploymentPathOptions: { platform: 'linux', homeDir: join(base, 'home') }, |
| }, |
| { |
| prepareUnownedHostCutover: async () => assert.fail('cutover must not begin'), |
| prepareHostCutover: async () => assert.fail('retirement must not begin'), |
| observeWriterRelease: async () => assert.fail('writer observation must not begin'), |
| activateTarget: async () => assert.fail('activation must not begin'), |
| verifyTargetReady: async () => assert.fail('Ready verification must not begin'), |
| }, |
| { authorityRoot }, |
| { |
| resolveInstallation: async () => ({ |
| owner: CLI_OWNER, |
| observedRelease: { |
| version: TARGET.version, |
| packageRoot: base, |
| cliPath: join(base, 'dist', 'cli.js'), |
| }, |
| }), |
| withPackage: async () => { |
| throw new Error('registry verification failed'); |
| }, |
| }, |
| ), |
| /registry verification failed/u, |
| ); |
| assert.deepEqual(await readLocalHostDeploymentRecord(ROOT_ID, { authorityRoot }), claimed.record); |
| }); |
| |
| test('installed release skew is rejected before staging or authority mutation', async (t) => { |
| const base = await mkdtemp(join(tmpdir(), 'maka-local-handoff-installation-skew-')); |
| t.after(() => rm(base, { recursive: true, force: true })); |
| const authorityRoot = join(base, 'authority'); |
| const claimed = await applyLocalHostDeploymentTransition( |
| ROOT_ID, |
| { kind: 'claim', owner: DESKTOP_OWNER, selected: PREVIOUS }, |
| { authorityRoot }, |
| ); |
| assert.equal(claimed.kind, 'applied'); |
| let staged = false; |
| |
| await assert.rejects( |
| reconcileRuntimeHostNpmGlobalDeployment( |
| { |
| rootId: ROOT_ID, |
| transactionId: 'stale-installed-release', |
| target: TARGET, |
| activeWorkPolicy: 'refuse_active_work', |
| }, |
| { |
| prepareUnownedHostCutover: async () => assert.fail('cutover must not begin'), |
| prepareHostCutover: async () => assert.fail('retirement must not begin'), |
| observeWriterRelease: async () => assert.fail('writer observation must not begin'), |
| activateTarget: async () => assert.fail('activation must not begin'), |
| verifyTargetReady: async () => assert.fail('Ready verification must not begin'), |
| }, |
| { authorityRoot }, |
| { |
| resolveInstallation: async () => ({ |
| owner: CLI_OWNER, |
| observedRelease: { |
| version: '2.1.0', |
| packageRoot: base, |
| cliPath: join(base, 'dist', 'cli.js'), |
| }, |
| }), |
| withPackage: async () => { |
| staged = true; |
| throw new Error('must not stage'); |
| }, |
| }, |
| ), |
| (error: unknown) => |
| error instanceof RuntimeHostLocalHandoffError && error.code === 'installed_release_mismatch', |
| ); |
| assert.equal(staged, false); |
| assert.deepEqual(await readLocalHostDeploymentRecord(ROOT_ID, { authorityRoot }), claimed.record); |
| }); |
| |
| test('a changed npm installation invalidates handoff consent before staging or authority mutation', async () => { |
| const installation = { |
| owner: CLI_OWNER, |
| observedRelease: { |
| version: TARGET.version, |
| packageRoot: '/installation', |
| cliPath: '/installation/dist/cli.js', |
| }, |
| }; |
| const result = await restartRuntimeHostNpmGlobalDeployment( |
| { |
| rootPath: '/root-under-test', |
| registration: hostRegistration(), |
| expectedInstallation: installation, |
| }, |
| {}, |
| { |
| resolveInstallation: async () => ({ |
| ...installation, |
| observedRelease: { ...installation.observedRelease, version: '9.0.0' }, |
| }), |
| readRecord: async () => assert.fail('changed installation cannot enter deployment authority'), |
| resolveCandidate: async () => assert.fail('changed installation cannot stage a target'), |
| }, |
| ); |
| assert.deepEqual(result, { kind: 'changed' }); |
| }); |
| |
| test('explicit npm-global restart claims an exact staged legacy takeover', async (t) => { |
| const base = await mkdtemp(join(tmpdir(), 'maka-local-restart-')); |
| t.after(() => rm(base, { recursive: true, force: true })); |
| const sourcePackageRoot = await selfContainedPackage(base, TARGET.version); |
| const authorityRoot = join(base, 'authority'); |
| let closed = 0; |
| let launchedEntrypoint = ''; |
| |
| const result = await restartRuntimeHostNpmGlobalDeployment( |
| { |
| rootPath: join(base, 'root'), |
| registration: hostRegistration(), |
| deploymentPathOptions: { platform: 'linux', homeDir: join(base, 'home') }, |
| }, |
| { authorityRoot }, |
| { |
| resolveInstallation: async () => ({ |
| owner: CLI_OWNER, |
| observedRelease: { |
| version: TARGET.version, |
| packageRoot: sourcePackageRoot, |
| cliPath: join(sourcePackageRoot, 'dist', 'cli.js'), |
| }, |
| }), |
| resolveCandidate: async () => TARGET, |
| withPackage: async (_candidate, use) => use(sourcePackageRoot), |
| prepareDeployment: prepareRuntimeHostPackageDeployment, |
| connectExisting: async () => incompatibleHost(hostRegistration()), |
| activateTarget: async (input) => { |
| launchedEntrypoint = input.staged.candidateEntrypoint; |
| return { |
| kind: 'ready', |
| settle: async () => { |
| closed += 1; |
| }, |
| }; |
| }, |
| }, |
| ); |
| |
| assert.equal(result.kind, 'completed'); |
| assert.match(launchedEntrypoint, /execution-candidate-main\.js$/u); |
| assert.equal(closed >= 1, true); |
| const record = await readLocalHostDeploymentRecord(ROOT_ID, { authorityRoot }); |
| assert.deepEqual(record?.state, { kind: 'owned', owner: CLI_OWNER, selected: TARGET }); |
| }); |
| |
| test('legacy restart reports active work without claiming deployment authority', async (t) => { |
| const base = await mkdtemp(join(tmpdir(), 'maka-local-restart-active-')); |
| t.after(() => rm(base, { recursive: true, force: true })); |
| const sourcePackageRoot = await selfContainedPackage(base, TARGET.version); |
| const authorityRoot = join(base, 'authority'); |
| const observed = hostRegistration(); |
| |
| const result = await restartRuntimeHostNpmGlobalDeployment( |
| { |
| rootPath: join(base, 'root'), |
| registration: observed, |
| deploymentPathOptions: { platform: 'linux', homeDir: join(base, 'home') }, |
| }, |
| { authorityRoot }, |
| { |
| resolveInstallation: async () => ({ |
| owner: CLI_OWNER, |
| observedRelease: { |
| version: TARGET.version, |
| packageRoot: sourcePackageRoot, |
| cliPath: join(sourcePackageRoot, 'dist', 'cli.js'), |
| }, |
| }), |
| resolveCandidate: async () => TARGET, |
| withPackage: async (_candidate, use) => use(sourcePackageRoot), |
| prepareDeployment: prepareRuntimeHostPackageDeployment, |
| connectExisting: async () => incompatibleHost(observed), |
| activateTarget: async () => ({ |
| kind: 'active_work', |
| settle: async () => undefined, |
| }), |
| }, |
| ); |
| |
| assert.equal(result.kind, 'active_work'); |
| assert.equal(await readLocalHostDeploymentRecord(ROOT_ID, { authorityRoot }), undefined); |
| }); |
| |
| test('external npm replacement retires through the exact source package and commits the target', async (t) => { |
| const base = await mkdtemp(join(tmpdir(), 'maka-local-external-reconcile-')); |
| t.after(() => rm(base, { recursive: true, force: true })); |
| const sourcePackageRoot = await selfContainedPackage(base, PREVIOUS.version, { |
| sourceRetirementHelper: true, |
| }); |
| const targetPackageRoot = await selfContainedPackage(base, TARGET.version); |
| const authorityRoot = join(base, 'authority'); |
| await stageSelectedPackage(base, sourcePackageRoot); |
| const claimed = await applyLocalHostDeploymentTransition( |
| ROOT_ID, |
| { kind: 'claim', owner: CLI_OWNER, selected: PREVIOUS }, |
| { authorityRoot }, |
| ); |
| assert.equal(claimed.kind, 'applied'); |
| const events: string[] = []; |
| |
| const result = await restartRuntimeHostNpmGlobalDeployment( |
| { |
| rootPath: join(base, 'root'), |
| registration: hostRegistration(), |
| deploymentPathOptions: { platform: 'linux', homeDir: join(base, 'home') }, |
| activeWorkPolicy: 'interrupt_active_work', |
| }, |
| { authorityRoot }, |
| { |
| resolveInstallation: async () => ({ |
| owner: CLI_OWNER, |
| observedRelease: { |
| version: TARGET.version, |
| packageRoot: targetPackageRoot, |
| cliPath: join(targetPackageRoot, 'dist', 'cli.js'), |
| }, |
| }), |
| resolveCandidate: async () => TARGET, |
| withPackage: async (candidate, use) => { |
| assert.equal(candidate.version, TARGET.version); |
| return use(targetPackageRoot); |
| }, |
| prepareDeployment: prepareRuntimeHostPackageDeployment, |
| connectExisting: async () => incompatibleHost(hostRegistration()), |
| retireSource: async (input) => { |
| assert.match(input.sourceCliPath, /registry-[a-f0-9]{64}\/dist\/cli\.js$/u); |
| assert.equal(input.expectedHostEpoch, 'old-host'); |
| assert.equal(input.activeWorkPolicy, 'interrupt_active_work'); |
| events.push('source-retired'); |
| return 'prepared'; |
| }, |
| activateTarget: async (input) => { |
| assert.equal(input.target.version, TARGET.version); |
| events.push('target-ready'); |
| return { |
| kind: 'ready', |
| settle: async () => { |
| events.push('settled'); |
| }, |
| }; |
| }, |
| }, |
| ); |
| |
| assert.equal(result.kind, 'completed'); |
| assert.deepEqual(events, ['source-retired', 'target-ready', 'settled']); |
| const record = await readLocalHostDeploymentRecord(ROOT_ID, { authorityRoot }); |
| assert.equal(record?.state.kind, 'owned'); |
| assert.deepEqual(record?.state.selected, TARGET); |
| }); |
| |
| test('external source active work rolls back without launching the target', async (t) => { |
| const base = await mkdtemp(join(tmpdir(), 'maka-local-external-active-')); |
| t.after(() => rm(base, { recursive: true, force: true })); |
| const sourcePackageRoot = await selfContainedPackage(base, PREVIOUS.version, { |
| sourceRetirementHelper: true, |
| }); |
| const targetPackageRoot = await selfContainedPackage(base, TARGET.version); |
| const authorityRoot = join(base, 'authority'); |
| await stageSelectedPackage(base, sourcePackageRoot); |
| await applyLocalHostDeploymentTransition( |
| ROOT_ID, |
| { kind: 'claim', owner: CLI_OWNER, selected: PREVIOUS }, |
| { authorityRoot }, |
| ); |
| |
| const result = await restartRuntimeHostNpmGlobalDeployment( |
| { |
| rootPath: join(base, 'root'), |
| registration: hostRegistration(), |
| deploymentPathOptions: { platform: 'linux', homeDir: join(base, 'home') }, |
| }, |
| { authorityRoot }, |
| { |
| resolveInstallation: async () => ({ |
| owner: CLI_OWNER, |
| observedRelease: { |
| version: TARGET.version, |
| packageRoot: targetPackageRoot, |
| cliPath: join(targetPackageRoot, 'dist', 'cli.js'), |
| }, |
| }), |
| resolveCandidate: async () => TARGET, |
| withPackage: async (candidate, use) => { |
| assert.equal(candidate.version, TARGET.version); |
| return use(targetPackageRoot); |
| }, |
| prepareDeployment: prepareRuntimeHostPackageDeployment, |
| connectExisting: async () => incompatibleHost(hostRegistration()), |
| retireSource: async () => 'active_work', |
| activateTarget: async () => assert.fail('active source work must prevent target activation'), |
| }, |
| ); |
| |
| assert.equal(result.kind, 'active_work'); |
| const record = await readLocalHostDeploymentRecord(ROOT_ID, { authorityRoot }); |
| assert.deepEqual(record?.state, { kind: 'owned', owner: CLI_OWNER, selected: PREVIOUS }); |
| }); |
| |
| test('pre-helper source release keeps the bounded idle-only takeover path', async (t) => { |
| const base = await mkdtemp(join(tmpdir(), 'maka-local-external-pre-helper-')); |
| t.after(() => rm(base, { recursive: true, force: true })); |
| const sourcePackageRoot = await selfContainedPackage(base, PREVIOUS.version); |
| const targetPackageRoot = await selfContainedPackage(base, TARGET.version); |
| const authorityRoot = join(base, 'authority'); |
| await stageSelectedPackage(base, sourcePackageRoot); |
| await applyLocalHostDeploymentTransition( |
| ROOT_ID, |
| { kind: 'claim', owner: CLI_OWNER, selected: PREVIOUS }, |
| { authorityRoot }, |
| ); |
| let takeoverHostEpoch: string | undefined; |
| |
| const result = await restartRuntimeHostNpmGlobalDeployment( |
| { |
| rootPath: join(base, 'root'), |
| registration: hostRegistration(), |
| deploymentPathOptions: { platform: 'linux', homeDir: join(base, 'home') }, |
| }, |
| { authorityRoot }, |
| { |
| resolveInstallation: async () => ({ |
| owner: CLI_OWNER, |
| observedRelease: { |
| version: TARGET.version, |
| packageRoot: targetPackageRoot, |
| cliPath: join(targetPackageRoot, 'dist', 'cli.js'), |
| }, |
| }), |
| resolveCandidate: async () => TARGET, |
| withPackage: async (candidate, use) => { |
| assert.equal(candidate.version, TARGET.version); |
| return use(targetPackageRoot); |
| }, |
| prepareDeployment: prepareRuntimeHostPackageDeployment, |
| connectExisting: async () => incompatibleHost(hostRegistration()), |
| retireSource: async () => assert.fail('a pre-helper source must not run the new helper'), |
| activateTarget: async (input) => { |
| takeoverHostEpoch = input.takeoverHostEpoch; |
| return { kind: 'ready', settle: async () => undefined }; |
| }, |
| }, |
| ); |
| |
| assert.equal(result.kind, 'completed'); |
| assert.equal(takeoverHostEpoch, 'old-host'); |
| }); |
| |
| test('source helper failure preserves the durable handoff for recovery', async (t) => { |
| const base = await mkdtemp(join(tmpdir(), 'maka-local-external-helper-failure-')); |
| t.after(() => rm(base, { recursive: true, force: true })); |
| const sourcePackageRoot = await selfContainedPackage(base, PREVIOUS.version, { |
| sourceRetirementHelper: true, |
| }); |
| const targetPackageRoot = await selfContainedPackage(base, TARGET.version); |
| const authorityRoot = join(base, 'authority'); |
| await stageSelectedPackage(base, sourcePackageRoot); |
| await applyLocalHostDeploymentTransition( |
| ROOT_ID, |
| { kind: 'claim', owner: CLI_OWNER, selected: PREVIOUS }, |
| { authorityRoot }, |
| ); |
| |
| const result = await restartRuntimeHostNpmGlobalDeployment( |
| { |
| rootPath: join(base, 'root'), |
| registration: hostRegistration(), |
| deploymentPathOptions: { platform: 'linux', homeDir: join(base, 'home') }, |
| }, |
| { authorityRoot }, |
| { |
| resolveInstallation: async () => ({ |
| owner: CLI_OWNER, |
| observedRelease: { |
| version: TARGET.version, |
| packageRoot: targetPackageRoot, |
| cliPath: join(targetPackageRoot, 'dist', 'cli.js'), |
| }, |
| }), |
| resolveCandidate: async () => TARGET, |
| withPackage: async (candidate, use) => { |
| assert.equal(candidate.version, TARGET.version); |
| return use(targetPackageRoot); |
| }, |
| prepareDeployment: prepareRuntimeHostPackageDeployment, |
| connectExisting: async () => incompatibleHost(hostRegistration()), |
| retireSource: async () => { |
| throw new Error('source helper exited'); |
| }, |
| activateTarget: async () => assert.fail('a failed source helper must not launch the target'), |
| }, |
| ); |
| |
| assert.equal(result.kind, 'recovery_required'); |
| assert.equal( |
| result.kind === 'recovery_required' ? result.phase : undefined, |
| 'prepare_host_cutover', |
| ); |
| assert.equal( |
| (await readLocalHostDeploymentRecord(ROOT_ID, { authorityRoot }))?.state.kind, |
| 'handoff', |
| ); |
| }); |
| |
| function managedAuthority(): NonNullable< |
| Awaited<ReturnType<typeof resolveRuntimeHostManagedDeploymentAuthority>> |
| > { |
| return { |
| capability: { kind: 'interactive', canonicalPath: '/state', rootId: ROOT_ID } as never, |
| record: { |
| schemaVersion: 1, |
| state: 'active', |
| deploymentId: '00000000-0000-4000-8000-000000000001', |
| configRevision: 1, |
| deploymentRoot: '/deployment', |
| root: { path: '/state', id: ROOT_ID }, |
| projectDirectoryRoots: [], |
| launch: { kind: 'exact_package', nodePath: process.execPath, package: PREVIOUS }, |
| listeners: { localIpc: true }, |
| lifecycle: { mode: 'on_demand', availability: 'activation' }, |
| reconciliation: { trigger: 'manual' }, |
| }, |
| }; |
| } |
| |
| for (const changed of ['host_epoch', 'managed_before_restart', 'managed_during_cutover'] as const) { |
| test(`local restart fences ${changed} despite a matching old CLI owner`, async (t) => { |
| const base = await mkdtemp(join(tmpdir(), 'maka-local-external-host-race-')); |
| t.after(() => rm(base, { recursive: true, force: true })); |
| const sourcePackageRoot = await selfContainedPackage(base, PREVIOUS.version, { |
| sourceRetirementHelper: true, |
| }); |
| const targetPackageRoot = await selfContainedPackage(base, TARGET.version); |
| const authorityRoot = join(base, 'authority'); |
| await stageSelectedPackage(base, sourcePackageRoot); |
| const claimed = await applyLocalHostDeploymentTransition( |
| ROOT_ID, |
| { kind: 'claim', owner: CLI_OWNER, selected: PREVIOUS }, |
| { authorityRoot }, |
| ); |
| let staged = false; |
| const mutationEffects: string[] = []; |
| |
| const result = await restartRuntimeHostNpmGlobalDeployment( |
| { |
| rootPath: join(base, 'root'), |
| registration: hostRegistration(), |
| deploymentPathOptions: { platform: 'linux', homeDir: join(base, 'home') }, |
| }, |
| { authorityRoot }, |
| { |
| resolveManagedAuthority: async () => { |
| if (changed === 'managed_before_restart') return managedAuthority(); |
| if (changed !== 'managed_during_cutover' || !staged) return undefined; |
| assert.equal( |
| (await readLocalHostDeploymentRecord(ROOT_ID, { authorityRoot }))?.state.kind, |
| 'handoff', |
| ); |
| return managedAuthority(); |
| }, |
| resolveInstallation: async () => ({ |
| owner: CLI_OWNER, |
| observedRelease: { |
| version: TARGET.version, |
| packageRoot: targetPackageRoot, |
| cliPath: join(targetPackageRoot, 'dist', 'cli.js'), |
| }, |
| }), |
| resolveCandidate: async () => TARGET, |
| withPackage: async (_candidate, use) => use(targetPackageRoot), |
| prepareDeployment: async (input) => { |
| const target = await prepareRuntimeHostPackageDeployment(input); |
| staged = true; |
| return target; |
| }, |
| connectExisting: async () => |
| incompatibleHost( |
| hostRegistration({ |
| hostEpoch: changed === 'host_epoch' ? 'replacement-host' : 'old-host', |
| }), |
| ), |
| retireSource: async () => { |
| mutationEffects.push('retire'); |
| assert.fail('an unconfirmed replacement Host must not retire'); |
| }, |
| activateTarget: async () => { |
| mutationEffects.push('activate'); |
| assert.fail('an unconfirmed replacement Host must remain'); |
| }, |
| }, |
| ); |
| |
| assert.deepEqual(mutationEffects, []); |
| if (changed === 'managed_before_restart') { |
| assert.deepEqual(result, { kind: 'operator_required', reason: 'unowned_host' }); |
| assert.deepEqual( |
| await readLocalHostDeploymentRecord(ROOT_ID, { authorityRoot }), |
| claimed.record, |
| ); |
| return; |
| } |
| assert.equal(result.kind, 'recovery_required'); |
| if (result.kind !== 'recovery_required') |
| assert.fail('the cutover must retain truthful recovery evidence'); |
| assert.match( |
| String(result.cause), |
| changed === 'host_epoch' ? /Host.*changed/ : /managed.*operator/, |
| ); |
| assert.equal( |
| result.kind === 'recovery_required' ? result.phase : undefined, |
| 'prepare_host_cutover', |
| ); |
| assert.equal( |
| (await readLocalHostDeploymentRecord(ROOT_ID, { authorityRoot }))?.state.kind, |
| 'handoff', |
| ); |
| const pending = await readLocalHostDeploymentRecord(ROOT_ID, { authorityRoot }); |
| assert.deepEqual(pending?.state.selected, PREVIOUS); |
| }); |
| } |
| |
| test('external reconciliation asks the activator to adjudicate when npm changes again', async (t) => { |
| const base = await mkdtemp(join(tmpdir(), 'maka-local-external-installation-race-')); |
| t.after(() => rm(base, { recursive: true, force: true })); |
| const sourcePackageRoot = await selfContainedPackage(base, PREVIOUS.version, { |
| sourceRetirementHelper: true, |
| }); |
| const targetPackageRoot = await selfContainedPackage(base, TARGET.version); |
| const authorityRoot = join(base, 'authority'); |
| await stageSelectedPackage(base, sourcePackageRoot); |
| await applyLocalHostDeploymentTransition( |
| ROOT_ID, |
| { kind: 'claim', owner: CLI_OWNER, selected: PREVIOUS }, |
| { authorityRoot }, |
| ); |
| let installationReads = 0; |
| let settlementRequested = false; |
| |
| const result = await restartRuntimeHostNpmGlobalDeployment( |
| { |
| rootPath: join(base, 'root'), |
| registration: hostRegistration(), |
| deploymentPathOptions: { platform: 'linux', homeDir: join(base, 'home') }, |
| }, |
| { authorityRoot }, |
| { |
| resolveInstallation: async () => { |
| installationReads += 1; |
| return { |
| owner: CLI_OWNER, |
| observedRelease: { |
| version: installationReads === 1 ? TARGET.version : '3.0.0', |
| packageRoot: targetPackageRoot, |
| cliPath: join(targetPackageRoot, 'dist', 'cli.js'), |
| }, |
| }; |
| }, |
| resolveCandidate: async () => TARGET, |
| withPackage: async (candidate, use) => { |
| assert.equal(candidate.version, TARGET.version); |
| return use(targetPackageRoot); |
| }, |
| prepareDeployment: prepareRuntimeHostPackageDeployment, |
| connectExisting: async () => incompatibleHost(hostRegistration()), |
| retireSource: async () => 'prepared', |
| activateTarget: async () => ({ |
| kind: 'ready', |
| settle: async () => { |
| settlementRequested = true; |
| }, |
| }), |
| }, |
| ); |
| |
| assert.equal(result.kind, 'recovery_required'); |
| assert.equal(result.kind === 'recovery_required' ? result.phase : undefined, 'finalize_target'); |
| assert.equal(settlementRequested, true); |
| assert.equal( |
| (await readLocalHostDeploymentRecord(ROOT_ID, { authorityRoot }))?.state.kind, |
| 'handoff', |
| ); |
| }); |
| |
| test('local restart keeps service Hosts under operator authority', async () => { |
| assert.deepEqual( |
| await restartRuntimeHostNpmGlobalDeployment({ |
| rootPath: '/managed-root', |
| registration: hostRegistration({ lifecycleMode: 'service' }), |
| }), |
| { kind: 'operator_required', reason: 'service_host' }, |
| ); |
| assert.deepEqual( |
| await restartRuntimeHostNpmGlobalDeployment({ |
| rootPath: '/legacy-root', |
| registration: hostRegistration({ lifecycleMode: undefined }), |
| }), |
| { kind: 'operator_required', reason: 'unowned_host' }, |
| ); |
| }); |
| |
| test('committed target conflicting with the observed Host fails closed', async (t) => { |
| const base = await mkdtemp(join(tmpdir(), 'maka-local-restart-observation-conflict-')); |
| t.after(() => rm(base, { recursive: true, force: true })); |
| const authorityRoot = join(base, 'authority'); |
| const claimed = await applyLocalHostDeploymentTransition( |
| ROOT_ID, |
| { kind: 'claim', owner: CLI_OWNER, selected: TARGET }, |
| { authorityRoot }, |
| ); |
| assert.equal(claimed.kind, 'applied'); |
| |
| await assert.rejects( |
| restartRuntimeHostNpmGlobalDeployment( |
| { rootPath: join(base, 'root'), registration: hostRegistration() }, |
| { authorityRoot }, |
| { |
| resolveInstallation: async () => ({ |
| owner: CLI_OWNER, |
| observedRelease: { |
| version: TARGET.version, |
| packageRoot: base, |
| cliPath: join(base, 'dist', 'cli.js'), |
| }, |
| }), |
| resolveCandidate: async () => TARGET, |
| withPackage: async () => assert.fail('conflicting committed target must not be staged'), |
| }, |
| ), |
| (error: unknown) => |
| error instanceof RuntimeHostLocalHandoffError && |
| error.code === 'selected_target_observation_conflict', |
| ); |
| }); |
| |
| test('external npm replacement rejects a different durable source owner before staging', async (t) => { |
| const base = await mkdtemp(join(tmpdir(), 'maka-local-external-owner-mismatch-')); |
| t.after(() => rm(base, { recursive: true, force: true })); |
| const authorityRoot = join(base, 'authority'); |
| await applyLocalHostDeploymentTransition( |
| ROOT_ID, |
| { kind: 'claim', owner: DESKTOP_OWNER, selected: PREVIOUS }, |
| { authorityRoot }, |
| ); |
| |
| await assert.rejects( |
| restartRuntimeHostNpmGlobalDeployment( |
| { rootPath: join(base, 'root'), registration: hostRegistration() }, |
| { authorityRoot }, |
| { |
| resolveInstallation: async () => ({ |
| owner: CLI_OWNER, |
| observedRelease: { |
| version: TARGET.version, |
| packageRoot: base, |
| cliPath: join(base, 'dist', 'cli.js'), |
| }, |
| }), |
| resolveCandidate: async () => assert.fail('cross-owner replacement must not resolve'), |
| withPackage: async () => assert.fail('cross-owner replacement must not stage a target'), |
| connectExisting: async () => assert.fail('cross-owner replacement must not observe a Host'), |
| }, |
| ), |
| (error: unknown) => |
| error instanceof RuntimeHostLocalHandoffError && error.code === 'source_owner_mismatch', |
| ); |
| }); |
| |
| test('external npm downgrade is rejected before staging or retirement', async (t) => { |
| const base = await mkdtemp(join(tmpdir(), 'maka-local-external-downgrade-')); |
| t.after(() => rm(base, { recursive: true, force: true })); |
| const authorityRoot = join(base, 'authority'); |
| await applyLocalHostDeploymentTransition( |
| ROOT_ID, |
| { kind: 'claim', owner: CLI_OWNER, selected: PREVIOUS }, |
| { authorityRoot }, |
| ); |
| const downgrade = { |
| kind: 'npm_registry' as const, |
| version: '0.9.0', |
| integrity: `sha512-${Buffer.alloc(64, 9).toString('base64')}`, |
| }; |
| |
| await assert.rejects( |
| restartRuntimeHostNpmGlobalDeployment( |
| { rootPath: join(base, 'root'), registration: hostRegistration() }, |
| { authorityRoot }, |
| { |
| resolveInstallation: async () => ({ |
| owner: CLI_OWNER, |
| observedRelease: { |
| version: downgrade.version, |
| packageRoot: base, |
| cliPath: join(base, 'dist', 'cli.js'), |
| }, |
| }), |
| resolveCandidate: async () => downgrade, |
| withPackage: async () => assert.fail('a downgrade must not be staged'), |
| }, |
| ), |
| (error: unknown) => |
| error instanceof RuntimeHostLocalHandoffError && error.code === 'unsupported_downgrade', |
| ); |
| }); |
| |
| async function stageSelectedPackage(base: string, sourcePackageRoot: string): Promise<void> { |
| await stageRuntimeHostNpmGlobalDeploymentTarget( |
| { |
| rootId: ROOT_ID, |
| owner: CLI_OWNER, |
| target: PREVIOUS, |
| transactionId: 'selected-source', |
| }, |
| { platform: 'linux', homeDir: join(base, 'home') }, |
| { |
| withPackage: async (_candidate, use) => use(sourcePackageRoot), |
| prepareDeployment: prepareRuntimeHostPackageDeployment, |
| }, |
| ); |
| } |
| |
| async function selfContainedPackage( |
| base: string, |
| version: string, |
| options: { readonly sourceRetirementHelper?: boolean } = {}, |
| ): Promise<string> { |
| const root = join(base, `source-${version}`); |
| const runtimeHostRoot = join(root, 'node_modules', '@maka', 'runtime-host'); |
| await Promise.all([ |
| mkdir(join(root, 'dist'), { recursive: true }), |
| mkdir(join(runtimeHostRoot, 'dist'), { recursive: true }), |
| ]); |
| await Promise.all([ |
| writeFile(join(root, 'package.json'), JSON.stringify({ name: 'maka-agent', version })), |
| writeFile(join(root, 'dist', 'cli.js'), ''), |
| ...(options.sourceRetirementHelper |
| ? [writeFile(join(root, 'dist', 'runtime-host-local-source-retirement.js'), '')] |
| : []), |
| writeFile(join(runtimeHostRoot, 'package.json'), '{}'), |
| writeFile(join(runtimeHostRoot, 'dist', 'execution-candidate-main.js'), ''), |
| ]); |
| return root; |
| } |
| |
| function hostRegistration(overrides: Partial<HostRegistration> = {}): HostRegistration { |
| return { |
| kind: 'maka-runtime-host', |
| schemaVersion: RUNTIME_HOST_REGISTRATION_SCHEMA_VERSION, |
| rootId: ROOT_ID, |
| hostEpoch: 'old-host', |
| endpoint: '/tmp/maka-host.sock', |
| protocolMin: RUNTIME_HOST_PROTOCOL_VERSION, |
| protocolMax: RUNTIME_HOST_PROTOCOL_VERSION, |
| compatibilityEpoch: RUNTIME_HOST_COMPATIBILITY_EPOCH - 1, |
| compositionId: INTERACTIVE_RUNTIME_HOST_COMPOSITION_ID, |
| compositionRevision: 'legacy', |
| lifecycleMode: 'ephemeral', |
| state: 'ready', |
| pid: 42, |
| createdAt: new Date(0).toISOString(), |
| ...overrides, |
| }; |
| } |
| |
| function incompatibleHost(registration: HostRegistration) { |
| return { |
| kind: 'incompatible' as const, |
| registration, |
| handshake: { |
| kind: 'incompatible' as const, |
| hostEpoch: registration.hostEpoch, |
| protocolMin: 0, |
| protocolMax: 0, |
| compatibilityEpoch: registration.compatibilityEpoch, |
| compositionId: registration.compositionId, |
| compositionRevision: registration.compositionRevision, |
| state: 'ready' as const, |
| replacement: 'blocked_by_residency' as const, |
| }, |
| }; |
| } |
| |
| for (const mode of ['explicit', 'safe', 'identity_changed'] as const) { |
| test(`legacy npm restart preserves staged deployment authority during ${mode} recovery`, async (t) => { |
| const base = await mkdtemp(join(tmpdir(), 'maka-legacy-process-recovery-')); |
| t.after(() => rm(base, { recursive: true, force: true })); |
| const sourcePackageRoot = await selfContainedPackage(base, TARGET.version); |
| const authorityRoot = join(base, 'authority'); |
| const registration = hostRegistration(); |
| const processIdentity = { startIdentity: 'observed-process-start' }; |
| const events: string[] = []; |
| const result = await restartRuntimeHostNpmGlobalDeployment( |
| { |
| rootPath: join(base, 'root'), |
| registration, |
| processIdentity, |
| activeWorkPolicy: mode === 'safe' ? 'refuse_active_work' : 'interrupt_active_work', |
| deploymentPathOptions: { platform: 'linux', homeDir: join(base, 'home') }, |
| }, |
| { authorityRoot }, |
| { |
| resolveManagedAuthority: async () => undefined, |
| resolveInstallation: async () => ({ |
| owner: CLI_OWNER, |
| observedRelease: { |
| version: TARGET.version, |
| packageRoot: sourcePackageRoot, |
| cliPath: join(sourcePackageRoot, 'dist', 'cli.js'), |
| }, |
| }), |
| resolveCandidate: async () => TARGET, |
| withPackage: async (_candidate, use) => { |
| events.push('stage'); |
| return use(sourcePackageRoot); |
| }, |
| prepareDeployment: prepareRuntimeHostPackageDeployment, |
| connectExisting: async () => incompatibleHost(registration), |
| terminateObservedHost: async (observed, authority) => { |
| assert.equal(observed.registration, registration); |
| assert.equal(authority.processIdentity, processIdentity); |
| assert.equal(authority.isCurrent(), true); |
| events.push('stop'); |
| return mode !== 'identity_changed'; |
| }, |
| activateTarget: async () => { |
| events.push('activate'); |
| return mode === 'safe' |
| ? { kind: 'active_work', settle: async () => {} } |
| : { kind: 'ready', settle: async () => {} }; |
| }, |
| }, |
| ); |
| if (mode === 'explicit') { |
| assert.equal(result.kind, 'completed'); |
| assert.deepEqual(events, ['stage', 'stop', 'activate']); |
| assert.deepEqual((await readLocalHostDeploymentRecord(ROOT_ID, { authorityRoot }))?.state, { |
| kind: 'owned', |
| owner: CLI_OWNER, |
| selected: TARGET, |
| }); |
| } else if (mode === 'safe') { |
| assert.equal(result.kind, 'active_work'); |
| assert.deepEqual(events, ['stage', 'activate']); |
| assert.equal(await readLocalHostDeploymentRecord(ROOT_ID, { authorityRoot }), undefined); |
| } else { |
| assert.equal(result.kind, 'recovery_required'); |
| assert.deepEqual(events, ['stage', 'stop']); |
| } |
| }); |
| } |