diff --git a/packages/ql3-local-owner-cli/src/deployment/reconciliation/completion/coordinator.ts b/packages/ql3-local-owner-cli/src/deployment/reconciliation/completion/coordinator.ts index 6368d577..c5652219 100644 --- a/packages/ql3-local-owner-cli/src/deployment/reconciliation/completion/coordinator.ts +++ b/packages/ql3-local-owner-cli/src/deployment/reconciliation/completion/coordinator.ts @@ -86,6 +86,7 @@ interface AutomationProof { interface SecretConfigProof { readonly intent: Readonly; readonly receipt: Readonly; + readonly preparedHeadDigest: string; readonly paths: ReturnType; readonly storageState: 'applied' | 'completed'; } @@ -107,6 +108,7 @@ export interface LocalReconciliationCompletionDependencies async function runHistoryProof( command: Readonly, terminal: Readonly, + secretConfig: Readonly | null, uid: number, dependencies: LocalReconciliationCompletionDependencies, ): Promise | null> { @@ -153,7 +155,8 @@ async function runHistoryProof( command.request.runHistory.expectedPreservationDigest || history.receipt.applicationPlanDigest !== terminal.plan.applicationPlanDigest || - history.receipt.sourceHeadDigest !== command.request.expectedHeadDigest + history.receipt.sourceHeadDigest !== + (secretConfig?.preparedHeadDigest ?? command.request.expectedHeadDigest) ) { fail('run history preservation evidence is detached'); } @@ -571,6 +574,7 @@ async function secretConfigProof( selected, uid, ); + const targetSnapshotSha256 = decision.context.planHeader.targetSnapshotSha256; if ( intent.command.options.deploymentRoot !== command.options.deploymentRoot || intent.command.options.applicationRoot !== @@ -589,8 +593,9 @@ async function secretConfigProof( receipt.decisionId !== binding.decisionId || receipt.applyDigest !== binding.expectedApplyDigest || receipt.preparationDigest !== intent.preparationDigest || - (automation !== null && - intent.backup.sha256 !== automation.receipt.targetAfter.sha256) || + (automation === null + ? targetSnapshotSha256 !== null + : targetSnapshotSha256 !== automation.receipt.targetAfter.sha256) || fs.existsSync(selected.rollbackReceipt) ) { fail('secret config apply evidence is detached'); @@ -623,6 +628,7 @@ async function secretConfigProof( return Object.freeze({ intent, receipt, + preparedHeadDigest: decision.context.planHeader.preparedHeadDigest, paths: selected, storageState, }); @@ -820,6 +826,7 @@ export async function completeLocalReconciliation( const runHistory = await runHistoryProof( command, terminal, + secretConfig, uid, dependencies, ); @@ -1015,6 +1022,7 @@ export async function verifyLocalReconciliationCompletion( const runHistory = await runHistoryProof( syntheticCompleteCommand, terminal, + secretConfig, uid, dependencies, ); diff --git a/packages/ql3-local-owner-cli/test/reconciliationCapturePrepare.test.cjs b/packages/ql3-local-owner-cli/test/reconciliationCapturePrepare.test.cjs index 6fdd0765..f7eeb44b 100644 --- a/packages/ql3-local-owner-cli/test/reconciliationCapturePrepare.test.cjs +++ b/packages/ql3-local-owner-cli/test/reconciliationCapturePrepare.test.cjs @@ -4402,6 +4402,66 @@ test('Secret/Config plan follows applied Automation and preserved Run History on ); assert.equal(applied.state, 'reconciliation_secret_config_applied'); assert.equal(applied.activeBindingCount, 1); + + const completionRoot = path.join( + path.dirname(state.captureRoot), + 'cross-domain-completion', + ); + fs.mkdirSync(completionRoot, { mode: 0o700 }); + const completionCommand = { + schemaVersion: 3, + operation: 'local.deployment.reconciliation.complete', + options: { + deploymentRoot: state.deploymentRoot, + applicationRoot: state.applicationRoot, + completionRoot, + automation: { + automationRoot: state.automationRoot, + automationDecisionRoot: state.automationDecisionRoot, + automationApplyRoot: state.automationApplyRoot, + targetDatabasePath: state.targetDatabasePath, + }, + secretConfig: { + secretConfigRoot, + secretConfigDecisionRoot, + secretConfigApplyRoot, + targetDatabasePath: state.targetDatabasePath, + }, + runHistory: { + runHistoryRoot, + decisionFilePath: state.reviewFile.filePath, + }, + allowRootService: rootAcknowledgement(), + }, + request: { + completionId: '00000000-0000-4000-8000-000000000434', + applicationId: state.application.applicationId, + expectedApplicationPlanDigest: state.application.applicationPlanDigest, + expectedHeadDigest: applied.instanceHeadDigest, + automation: { + automationId: state.automationCommand.request.automationId, + decisionId: state.decisionId, + expectedApplyDigest: state.applied.applyDigest, + }, + secretConfig: { + secretConfigId, + decisionId: secretConfigDecisionId, + expectedApplyDigest: applied.applyDigest, + }, + runHistory: { + preservationId: preservationCommand.request.preservationId, + expectedPreservationDigest: preserved.preservationDigest, + }, + completedAtMs: appliedAtMs + 1, + }, + }; + const completed = await completeLocalReconciliation(completionCommand); + assert.equal(completed.state, 'reconciliation_completed'); + assert.equal(completed.adapterCount, 3); + assert.equal( + (await completeLocalReconciliation(completionCommand)).status, + 'existing', + ); }); test('Secret/Config decision reauthenticates the same reviewer, seals exact candidates and verifies content-free', async (t) => {