Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import type {
import { assertRuntimeHostManagedOperatorConfig } from '../runtime-host-managed-deployment.js';
import {
applyRuntimeHostLifecycleTransition,
convergeRuntimeHostLifecycleControlProjection,
recoverRuntimeHostLifecycleTransition,
replaceRuntimeHostLifecycle,
resolveRecoverableRuntimeHostManagedDeployment,
Expand All @@ -56,6 +57,42 @@ import {
const INTEGRITY = `sha512-${Buffer.alloc(64, 7).toString('base64')}`;
const UPDATED_INTEGRITY = `sha512-${Buffer.alloc(64, 8).toString('base64')}`;

test('control projection repair leaves the running Host supervisor untouched', async () => {
const current = config('/workspace', 'a'.repeat(64), 1, 'launch_agent');
const calls: string[] = [];
const provider = new FakeLifecycleProvider('launch_agent', 'launch_agent_timer');
const originalActivate = provider.reconciliationTrigger.activate;
provider.reconciliationTrigger.activate = async () => {
calls.push('scheduler.activate');
await originalActivate();
};

await convergeRuntimeHostLifecycleControlProjection(current, {
convergeOperator: async (from, to) => {
assert.deepEqual(from, current);
assert.deepEqual(to, current);
calls.push('operator.converge');
},
verifyOperator: async () => undefined,
resolveProvider: () => ({
...provider,
supervisor: {
...provider.supervisor,
converge: async () => assert.fail('the running supervisor must not be replaced'),
},
reconciliationTrigger: {
...provider.reconciliationTrigger,
converge: async (definition) => {
calls.push('scheduler.converge');
await provider.reconciliationTrigger.converge(definition);
},
},
}),
});

assert.deepEqual(calls, ['operator.converge', 'scheduler.converge', 'scheduler.activate']);
});

test('one authority record recovers provider cutover failures without a journal', async (t) => {
const stateRoot = await mkdtemp(join(tmpdir(), 'maka-lifecycle-root-'));
const authorityRoot = await mkdtemp(join(tmpdir(), 'maka-lifecycle-authority-'));
Expand Down
10 changes: 9 additions & 1 deletion packages/cli/src/__tests__/runtime-host-selected-update.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ describe('managed Runtime Host selected update', () => {
let output = '';
let managedReads = 0;
let pruned = false;
const projection: string[] = [];
const exitCode = await runManagedRuntimeHostUpdateCli(
{
...OPTIONS,
Expand All @@ -106,7 +107,13 @@ describe('managed Runtime Host selected update', () => {
assert.deepEqual(options?.expectedOwner, expectedHost);
return { kind: 'active', config: current } as never;
},
verifyProjection: async () => {},
convergeControlProjection: async (config) => {
assert.equal(config, current);
projection.push('converge');
},
verifyProjection: async () => {
projection.push('verify');
},
assertOperatorConfig: () => {},
manageLifecycle: async () => {
managedReads += 1;
Expand All @@ -130,6 +137,7 @@ describe('managed Runtime Host selected update', () => {
assert.equal(exitCode, 0);
assert.equal(managedReads, 2);
assert.equal(pruned, true);
assert.deepEqual(projection, ['converge', 'verify']);
assert.doesNotMatch(output, /"phase":"staging"/u);
const result = decodeRuntimeHostServiceManagementFrame(output.trim().split('\n').at(-1) ?? '');
assert.equal(
Expand Down
19 changes: 19 additions & 0 deletions packages/cli/src/runtime-host-lifecycle-transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -806,6 +806,25 @@ export async function verifyRuntimeHostLifecycleReady(
);
}

/** Repairs update-control artifacts without interrupting the running Host supervisor. */
export async function convergeRuntimeHostLifecycleControlProjection(
config: RuntimeHostManagedDeploymentConfig,
deps: RuntimeHostLifecycleTransactionDeps,
): Promise<void> {
const canonical = decodeRuntimeHostManagedDeploymentConfig(config);
await deps.convergeOperator(canonical, canonical);
if (canonical.lifecycle.mode !== 'supervised') return;
const provider = deps.resolveProvider(canonical);
if (canonical.reconciliation.trigger === 'scheduled') {
await provider.reconciliationTrigger.converge(
runtimeHostReconciliationTriggerDefinition(canonical),
);
await provider.reconciliationTrigger.activate();
} else {
await provider.reconciliationTrigger.uninstall();
}
}

/** Verifies the durable operator and supervisor projection without requiring a compatible Host. */
export async function verifyRuntimeHostLifecycleProjection(
config: RuntimeHostManagedDeploymentConfig,
Expand Down
5 changes: 4 additions & 1 deletion packages/cli/src/runtime-host-update-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ import { truncateUtf8 } from '@maka/core/diagnostic-log';
import { activateRuntimeHostManagedDeployment } from '@maka/runtime-host/client';
import {
createRuntimeHostLegacyPosixOperatorCommand,
runtimeHostManagedOperatorCommand,
decodeRuntimeHostServiceManagementFrame,
encodeRuntimeHostServiceManagementFrame,
RUNTIME_HOST_SERVICE_ERROR_CODE_MAX_BYTES,
Expand Down Expand Up @@ -81,6 +80,7 @@ import {
import type { RuntimeHostExpectedHost, RuntimeHostUpdateSelector } from './runtime-host-cli.js';
import {
canDiscardRuntimeHostLifecycleDesiredArtifacts,
convergeRuntimeHostLifecycleControlProjection,
replaceRuntimeHostLifecycle,
resolveRecoverableRuntimeHostManagedDeployment,
RuntimeHostLifecycleTransactionError,
Expand Down Expand Up @@ -142,6 +142,7 @@ interface RuntimeHostUpdateCliDeps {
readonly createLifecycleDeps: (rootId: string) => RuntimeHostLifecycleTransactionDeps;
readonly assertOperatorDeployment: typeof assertRuntimeHostManagedOperatorDeployment;
readonly recoverDeployment: typeof resolveRecoverableRuntimeHostManagedDeployment;
readonly convergeControlProjection: typeof convergeRuntimeHostLifecycleControlProjection;
readonly verifyProjection: typeof verifyRuntimeHostLifecycleProjection;
readonly assertOperatorConfig: typeof assertRuntimeHostManagedOperatorConfig;
readonly manageLifecycle: typeof manageRuntimeHostManagedLifecycle;
Expand Down Expand Up @@ -237,6 +238,7 @@ export async function runManagedRuntimeHostUpdateCli(
}),
assertOperatorDeployment: assertRuntimeHostManagedOperatorDeployment,
recoverDeployment: resolveRecoverableRuntimeHostManagedDeployment,
convergeControlProjection: convergeRuntimeHostLifecycleControlProjection,
verifyProjection: verifyRuntimeHostLifecycleProjection,
assertOperatorConfig: assertRuntimeHostManagedOperatorConfig,
manageLifecycle: manageRuntimeHostManagedLifecycle,
Expand Down Expand Up @@ -651,6 +653,7 @@ async function runCanonicalRuntimeHostUpdate(
);
}
const current = recovered.config;
await deps.canonical.convergeControlProjection(current, lifecycleDeps);
await deps.canonical.verifyProjection(current, lifecycleDeps);
deps.canonical.assertOperatorConfig(
current,
Expand Down