mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-20 08:05:22 +08:00
fix(ql3): preserve target-native reconciliation domains
This commit is contained in:
@@ -58,10 +58,19 @@
|
||||
`Secret/Config backup.sha256 == Automation targetAfter.sha256`,再证明 current target 等于 Secret/Config `targetAfter`。lineage 只新增
|
||||
`reconciliation_secret_config_applied → reconciliation_completed` 一条合法边;receipt seal 与 completed head durable 后,才幂等删除两类数据库等量
|
||||
backup。Secret/Config 的 ciphertext material、intent 与 receipt 继续 `0400` 保留,backup/rollback root 封为 `0500`,不增加后台 GC、timer 或常驻内存。
|
||||
rolled-back、target drift、提前丢失 backup 与其余 manual 域继续失败关闭;完整迁移库当前仍可能因 Identity/Unknown 保持 manual,这不是 v3 可绕过的限制。
|
||||
rolled-back、target drift、提前丢失 backup 与其余 manual 域继续失败关闭。
|
||||
第九切片以 D-398/ADR-0493 修正 planner 对目标原生数据的两个架构误判,而没有新增 adapter 或放宽未知 schema。fresh v52 中
|
||||
`QingLong3LegacyAdoptions`、`QingLong3LegacyAdoptionTasks`、`QingLong3LocalExecutionContextRecipes`、`QingLong3LocalTaskExecutionRevisions`
|
||||
精确归入 Automation,`QingLong3RunAttemptLogArtifactTombstones`、`QingLong3RunAttemptLogRetentionState` 精确归入 Run History;不使用
|
||||
`QingLong3*` 宽泛通配,未来未登记表仍是 row-free、blocked 的 `unknown`。`identity_policy_audit` 按来源分离:没有 Legacy `Auths/Users`
|
||||
时,Target 原生 Project/RoleBinding/Identity/Credential/Pepper/Approval/SecurityAudit/LocalOwner 事实为 `target_only`,必须逐事实 signed
|
||||
`retain_target`,随后才以 `no_effect/application_summary` 进入 completion;只要存在 Legacy 身份事实,仍为
|
||||
`manual_required/identity_custody_required`,本切片不声称旧 credential、session、token、Policy 或 Audit 已迁移。Secret/Config v52 fixture
|
||||
现已用真实 `complete → replay → verify` 完成 v3 闭环,不再由测试伪造 receipt、直接推进 head 或手工回收 storage;真正未知表和 Legacy 身份回归
|
||||
继续失败关闭。
|
||||
全部 evidence 不含原 Env name/value、目标 ciphertext/key ID 或 row body。v52 Local SQLite 完整测试为 `247/247`,publisher 定向回归 `6/6`;fresh Edge
|
||||
readiness 为 contract v52、104 migrations、89 required tables、SQLite 3.53.3、`DELETE` journal。Local Admin 为 `96/96`,Local Owner 为
|
||||
`300 total / 293 pass / 7 conditional skip / 0 fail`;18-package clean build 与逐包顺序测试单次退出 0,完整 backend 为
|
||||
readiness 为 contract v52、104 migrations、89 required tables、SQLite 3.53.3、`DELETE` journal。Local Admin 为 `96/96`,ADR-0493 后 Local Owner 有效结果为
|
||||
`301 total / 294 pass / 7 conditional skip / 0 fail`;完整 backend 为
|
||||
`1567 total / 1565 pass / 2 conditional skip / 0 fail`。package boundary、精确 Cluster dependency/legacy boundary、122-module Edge import、本地镜像与
|
||||
`14/14` Local artifact audit 全部 compatible;基础 Edge/Standalone 为 `2,635,529 / 2,635,607 bytes`、323 files、58 loaded modules,距 4 MiB
|
||||
上限仍分别保留 `1,558,775 / 1,558,697 bytes`,且闭包只有 Local SQLite、runtime-core 与 SemVer,没有 Cluster/PostgreSQL 依赖。
|
||||
|
||||
@@ -143,6 +143,6 @@ Cluster 不得把 Legacy Env 明文写入 PostgreSQL、ConfigMap、Job command
|
||||
|
||||
D-397 当前八切片已经实现:absent、unsupported、Edge over-budget、2.x 顺序、同名连接、disabled preservation、保留前缀、异常状态、effective overflow、candidate digest、content-free diagnostics、私有有界 row plan、目标 Secret 冲突、Automation adoption projection、no-effect/manual outcome、durable no-replace publication、terminal seal、head CAS、逐候选独立 signed decision、同一强认证 reviewer、decision/authorization byte bound、`skip → manual_required`、prepare/commit response-loss exact replay、只读 terminal verify、v51 逐 Task/Trigger adoption provenance、v52 Local SQLite 原子 application publisher、Owner prepared/apply/rollback orchestration,以及 ADR-0492 completion v3。v52 在一个 `BEGIN IMMEDIATE` 内复验 Project/RoleBinding、外部 authority、逐 Task/Trigger provenance、当前 head、Plugin ownership 与 Trigger 数量,流式写入加密 Secret、content-free audit、Task rev2、dispatch、Trigger rev2、schedule 和四类 application ledger;deferred parent FK 允许最多 100,000 Task/500,000 Trigger 逐项发布而不在 JS 堆保留全集。Owner 在写前固定 backup 与 stopped proof,以有界 ciphertext-only material 连接 reviewed decision 和 publisher,并覆盖 apply/rollback 全部 response-loss 窗口。completion v3 保留 v1/v2 exact shape,验证 signed decision/apply/current target/head,只有 completed head durable 后才幂等回收 Secret/Config backup;rolled-back、target drift 与其余 manual 域继续失败关闭。commit response-loss 通过 durable receipt exact replay,并重新验证 Secret envelope、Task/Trigger head 与 schedule;目标占用、provenance 缺项、提交前 authority 漂移均回滚全部 DML,rollback 则恢复写前 snapshot。
|
||||
|
||||
本切片当前验证:Local SQLite `247/247`,其中 Secret/Config application publisher 定向回归 `6/6`;fresh Edge readiness 为 contract v52、104 migrations、89 required tables、SQLite 3.53.3、`DELETE` journal。Local Admin 为 `96/96`,Local Owner 为 `300 total / 293 pass / 7 conditional skip / 0 fail`;18-package clean build 与逐包顺序测试单次退出 0,完整 backend 为 `1567 total / 1565 pass / 2 conditional skip / 0 fail`。package boundary、精确 Cluster dependency/legacy boundary、122-module Edge import、service-manager bridge import、本地镜像与 `14/14` Local artifact audit 全部 compatible;Local Admin 为 49 source / 48 nested / 1 root export,Local Owner 为 188/187/1,workspace 仍为 18 packages 且没有单文件或浅层 package。基础 Edge/Standalone 为 `2,635,529 / 2,635,607 bytes`、323 files、58 loaded modules,且没有 Cluster/PostgreSQL 闭包。本切片不改 PostgreSQL schema、连接、role、Pool、容器或 Kubernetes 拓扑,因此不重跑且不重新占有 PostgreSQL HA 证明。
|
||||
本切片当前验证:Local SQLite `247/247`,其中 Secret/Config application publisher 定向回归 `6/6`;fresh Edge readiness 为 contract v52、104 migrations、89 required tables、SQLite 3.53.3、`DELETE` journal。Local Admin 为 `96/96`;ADR-0493 后 Local Owner 有效结果为 `301 total / 294 pass / 7 conditional skip / 0 fail`。完整 backend 为 `1567 total / 1565 pass / 2 conditional skip / 0 fail`。package boundary、精确 Cluster dependency/legacy boundary、122-module Edge import、service-manager bridge import、本地镜像与 `14/14` Local artifact audit 全部 compatible;Local Admin 为 49 source / 48 nested / 1 root export,Local Owner 为 188/187/1,workspace 仍为 18 packages 且没有单文件或浅层 package。基础 Edge/Standalone 为 `2,635,529 / 2,635,607 bytes`、323 files、58 loaded modules,且没有 Cluster/PostgreSQL 闭包。本切片不改 PostgreSQL schema、连接、role、Pool、容器或 Kubernetes 拓扑,因此不重跑且不重新占有 PostgreSQL HA 证明。
|
||||
|
||||
转为 Accepted 前仍必须完成:真实 Edge 空间预算证据与 Cluster Secret provider live gate;ADR-0492 已完成本机 completion schema 演进和 completed-head 后 rollback material 回收,但本切片的 Local Owner 编排或 PostgreSQL HA 证据不得冒充外部密钥托管,也不得把仍含 Identity/Unknown manual 域的迁移冒充跨领域 completion 已完成。
|
||||
转为 Accepted 前仍必须完成:真实 Edge 空间预算证据与 Cluster Secret provider live gate;ADR-0492 已完成本机 completion schema 演进和 completed-head 后 rollback material 回收,ADR-0493 又让没有 Legacy 身份输入的 fresh v52 目标身份经 signed `retain_target` 正确形成 no-effect,并精确消除六张已知目标表的 `unknown` 误判。Legacy `Auths/Users` 或真正未知表仍保持 manual;本切片的 Local Owner 编排或 PostgreSQL HA 证据不得冒充外部密钥托管。
|
||||
|
||||
@@ -56,7 +56,7 @@ Cluster 不复用本机 receipt、POSIX storage 或 instance head。Cluster comp
|
||||
|
||||
- completion receipt 的消费者必须按 `schemaVersion` 解析,未知版本继续失败关闭。
|
||||
- `advanceLocalCutoverInstanceHead` 是 CRITICAL 共享状态机;本 ADR 只增加一条 source-state 边,不改任何旧 transition。
|
||||
- 完整迁移库当前仍可能因 `identity_policy_audit` 或 `unknown` 保持 manual;这是正确的全局围栏,不应为了演示 v3 成功而放宽。
|
||||
- 本 ADR 验收时完整迁移库仍会因目标原生 `identity_policy_audit` 和六张未登记 v52 表落入 `unknown` 而保持 manual;ADR-0493 已通过精确分类与 signed `retain_target` 修正这两个本地 planner 缺口。Legacy `Auths/Users` 和真正未知表仍保持全局围栏。
|
||||
- v3 verify 要求 completed storage,不能在 head durable 之前充当修复命令。
|
||||
|
||||
## 被拒绝的替代方案
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
# ADR-0493:目标原生 Reconciliation 域分类与身份保留
|
||||
|
||||
- 状态:Accepted
|
||||
- 日期:2026-08-24
|
||||
- 决策:D-398
|
||||
- 关联:ADR-0483、ADR-0484、ADR-0485、ADR-0488、ADR-0492
|
||||
|
||||
## 背景
|
||||
|
||||
Reconciliation 必须同时处理两类不同事实:从 2.x 迁入的数据,以及已经属于 QingLong 3.0 目标库的原生数据。此前实现把 `identity_policy_audit` 整域无条件标为 `manual_required/identity_custody_required`,因此即使 Legacy 完全没有 `Auths/Users`,fresh v52 目标库自带的 Project、RoleBinding、Credential、Pepper、SecurityAudit 与 LocalOwner 表也会阻止全局 completion。
|
||||
|
||||
同一盘点还发现 fresh v52 的六张已知表没有进入稳定领域,被误归为 `unknown`:
|
||||
|
||||
- Automation:`QingLong3LegacyAdoptions`、`QingLong3LegacyAdoptionTasks`、`QingLong3LocalExecutionContextRecipes`、`QingLong3LocalTaskExecutionRevisions`;
|
||||
- Run History:`QingLong3RunAttemptLogArtifactTombstones`、`QingLong3RunAttemptLogRetentionState`。
|
||||
|
||||
这不是缺少新 adapter,而是现有 planner 对目标原生 schema 的知识不完整。为绕过门禁而伪造 completion receipt 或把整个 `QingLong3*` 前缀视为可信都会破坏 fail-closed 边界。
|
||||
|
||||
## 决策
|
||||
|
||||
### 1. 已知目标表使用精确领域目录
|
||||
|
||||
上述六张表按精确表名分别归入 Automation 与 Run History。Schema object 继续按其 `tableName` 继承同一领域,因此相关 index/trigger 不需要单独的宽泛名称规则。
|
||||
|
||||
不引入 `QingLong3*`、`LegacyAdoption*` 或 `Local*` 的整体通配。未来新增但尚未登记的目标表仍进入 `unknown`,不读取行数,并要求人工外部处理。
|
||||
|
||||
### 2. 目标原生身份是保留事实,不是迁移事实
|
||||
|
||||
`identity_policy_audit` 的 disposition 改为按来源判定:
|
||||
|
||||
- Legacy 存在 `Auths/Users` 时继续 `manual_required`;
|
||||
- Legacy 没有身份事实、Target 有已知身份事实时为 `target_only`;
|
||||
- 两端都没有时为 `aligned`。
|
||||
|
||||
Target 身份诊断是 `required/reviewable_fact`,必须在 signed review 中逐事实选择 `retain_target` 才能形成 `no_effect` application action。它不能选择 `adopt_legacy` 或 `exclude_legacy`。Legacy 身份诊断仍是 `blocked/identity_custody_required`,只允许 `defer|manual_external`,本 ADR 不声明 2.x credential、session、password hash、token、Policy 或 Audit 已迁移。
|
||||
|
||||
### 3. Completion 只消费既有闭合证据
|
||||
|
||||
本 ADR 不增加 completion schema、adapter、instance state 或绕过规则。目标原生身份经过 canonical diagnostics、signed decision 和 application summary 后,以既有 `no_effect/application_summary` 进入 completion;真正未知表和任何 Legacy 身份事实仍阻止 completion。
|
||||
|
||||
完整 v52 + Legacy Secret/Config fixture 必须通过真实 v3 completion,而不是测试直接构造 receipt、推进 head 或调用 storage collector。只有 completed head durable 后,Secret/Config backup 才能由既有流程回收。
|
||||
|
||||
### 4. 部署与资源边界不变
|
||||
|
||||
实现只修改既有 Local Owner 一次性 reconciliation planner/reviewer,没有新增 workspace package、production dependency、SQL migration、daemon、listener、timer、Pool、容器或 Kubernetes workload。Edge/Standalone 常驻闭包、SQLite cache 上限和 artifact 预算不变;Cluster 仍使用自己的 PostgreSQL、Secret provider 与 HA authority。
|
||||
|
||||
## 被拒绝的替代方案
|
||||
|
||||
### 为目标原生身份增加 migration adapter
|
||||
|
||||
拒绝。没有 Legacy 身份输入时不存在要迁移的数据;额外 adapter 只会复制已经由目标数据库拥有的事实并扩大权限面。
|
||||
|
||||
### 信任全部 QingLong3 前缀
|
||||
|
||||
拒绝。名称前缀不是 schema ownership 证明,会让未来未知表自动越过人工审查。
|
||||
|
||||
### 自动跳过目标身份评审
|
||||
|
||||
拒绝。`target_only` 仍必须形成逐事实 signed `retain_target` 决策,不能仅凭分类器直接授权 completion。
|
||||
|
||||
### 放行 Legacy Auths/Users
|
||||
|
||||
拒绝。旧身份材料的 custody、hash/token 兼容、撤销、Policy 与 Audit 语义尚未建立独立 adapter;本 ADR 只消除目标原生数据被误当成迁移输入的问题。
|
||||
|
||||
## 验证
|
||||
|
||||
- 定向回归 `4/4`:fresh v52 `unknown=0`、目标身份 `retain_target`、Legacy 身份继续 blocked、真正未知表继续 row-free/manual;
|
||||
- Secret/Config completion v3 使用真实 `complete → replay → verify` 链路,首调 `completed`、重放 `existing`,receipt 为 v3 且 Identity/Unknown 均为 `no_effect`;
|
||||
- completed head durable 后 Secret/Config backup 被回收,加密 material 与 receipt 保持只读;
|
||||
- Local Owner 受限沙箱为 `301 total / 291 pass / 7 conditional skip / 3 loopback EPERM`,两个 loopback 文件在沙箱外 `15/15`,有效结果 `301/294/7/0`;
|
||||
- 完整 backend 为 `1567 total / 1565 pass / 2 conditional skip / 0 fail`;18-package clean build 与逐包顺序测试除 Worker 三条 sandbox loopback 外全部通过,对应 Worker 文件在沙箱外 `8/8`;
|
||||
- package boundary、精确 Cluster dependency、122-module Edge import、service-manager bridge import、本地镜像与 `14/14` Local artifact audit 全部 compatible;workspace 保持 18 packages,`singleSourcePackages=[]`、`shallowSourcePackages=[]`;
|
||||
- 基础 Edge/Standalone 保持 `2,635,529 / 2,635,607 bytes`、323 files、58 loaded modules,没有 Cluster/PostgreSQL 闭包。
|
||||
@@ -496,6 +496,7 @@
|
||||
| [ADR-0490](./ADR-0490-run-history-terminal-preservation.md) | Run History 终态保全与跨领域完成证明 | Accepted |
|
||||
| [ADR-0491](./ADR-0491-bounded-secret-config-reconciliation-and-task-binding.md) | 有界 Secret/Config Reconciliation 与任务环境绑定 | Proposed(D-397 local apply/rollback/completion complete;Edge/Cluster gate pending) |
|
||||
| [ADR-0492](./ADR-0492-compatible-secret-config-completion-v3.md) | 兼容 Secret/Config 的 Reconciliation Completion v3 | Accepted |
|
||||
| [ADR-0493](./ADR-0493-target-native-reconciliation-domain-preservation.md) | 目标原生 Reconciliation 域分类与身份保留 | Accepted |
|
||||
|
||||
## 规则
|
||||
|
||||
|
||||
@@ -16,6 +16,16 @@ import {
|
||||
|
||||
const MAX_SCHEMA_OBJECTS = 4_096;
|
||||
const MAX_TABLES = 512;
|
||||
const TARGET_AUTOMATION_TABLES = new Set([
|
||||
'QingLong3LegacyAdoptions',
|
||||
'QingLong3LegacyAdoptionTasks',
|
||||
'QingLong3LocalExecutionContextRecipes',
|
||||
'QingLong3LocalTaskExecutionRevisions',
|
||||
]);
|
||||
const TARGET_RUN_HISTORY_TABLES = new Set([
|
||||
'QingLong3RunAttemptLogArtifactTombstones',
|
||||
'QingLong3RunAttemptLogRetentionState',
|
||||
]);
|
||||
|
||||
export interface LocalReconciliationDomainInventory {
|
||||
readonly domain: LocalReconciliationPlanDomain;
|
||||
@@ -85,6 +95,7 @@ function targetDomain(name: string): LocalReconciliationPlanDomain {
|
||||
return 'schema_lineage';
|
||||
}
|
||||
if (
|
||||
TARGET_AUTOMATION_TABLES.has(name) ||
|
||||
name.includes('TaskDefinition') ||
|
||||
name.includes('Trigger') ||
|
||||
name.includes('Automation')
|
||||
@@ -95,6 +106,7 @@ function targetDomain(name: string): LocalReconciliationPlanDomain {
|
||||
return 'secret_and_config';
|
||||
}
|
||||
if (
|
||||
TARGET_RUN_HISTORY_TABLES.has(name) ||
|
||||
name === 'Runs' ||
|
||||
name.startsWith('Run') ||
|
||||
name.startsWith('StepRun') ||
|
||||
|
||||
@@ -175,10 +175,11 @@ function domainDisposition(
|
||||
) {
|
||||
return 'unsupported';
|
||||
}
|
||||
if (domain === 'identity_policy_audit' && legacyFacts > 0) {
|
||||
return 'manual_required';
|
||||
}
|
||||
if (
|
||||
(domain === 'secret_and_config' ||
|
||||
domain === 'run_history' ||
|
||||
domain === 'identity_policy_audit') &&
|
||||
(domain === 'secret_and_config' || domain === 'run_history') &&
|
||||
legacyFacts + targetFacts > 0
|
||||
) {
|
||||
return 'manual_required';
|
||||
|
||||
@@ -297,10 +297,14 @@ export function assertLocalReconciliationReviewDecisionMatchesFact(
|
||||
!['retain_target', 'retain_both', 'defer', 'manual_external'].includes(
|
||||
selected.disposition,
|
||||
)) ||
|
||||
((fact.domain === 'secret_and_config' ||
|
||||
fact.domain === 'identity_policy_audit' ||
|
||||
fact.domain === 'unknown') &&
|
||||
!['defer', 'manual_external'].includes(selected.disposition))
|
||||
((fact.domain === 'secret_and_config' || fact.domain === 'unknown') &&
|
||||
!['defer', 'manual_external'].includes(selected.disposition)) ||
|
||||
(fact.domain === 'identity_policy_audit' &&
|
||||
!(
|
||||
fact.database === 'target'
|
||||
? ['retain_target', 'defer', 'manual_external']
|
||||
: ['defer', 'manual_external']
|
||||
).includes(selected.disposition))
|
||||
) {
|
||||
configurationError(
|
||||
'decision disposition is not allowed for canonical fact',
|
||||
|
||||
@@ -195,6 +195,12 @@ function requirement(
|
||||
});
|
||||
}
|
||||
if (domain === 'identity_policy_audit') {
|
||||
if (database === 'target') {
|
||||
return Object.freeze({
|
||||
decisionRequirement: 'required' as const,
|
||||
reason: 'reviewable_fact' as const,
|
||||
});
|
||||
}
|
||||
return Object.freeze({
|
||||
decisionRequirement: 'blocked' as const,
|
||||
reason: 'identity_custody_required' as const,
|
||||
|
||||
@@ -74,17 +74,6 @@ const {
|
||||
const {
|
||||
targetStoppedEvidence,
|
||||
} = require('../dist/deployment/cutover/targetStopRecordEvidence.js');
|
||||
const {
|
||||
collectLocalReconciliationSecretConfigCompletedStorage,
|
||||
localReconciliationSecretConfigApplyPaths,
|
||||
readLocalReconciliationSecretConfigApplyIntent,
|
||||
validateLocalReconciliationSecretConfigCompletedStorage,
|
||||
} = require('../dist/deployment/reconciliation/application/secret-and-config/application/storage.js');
|
||||
const {
|
||||
buildLocalReconciliationCompletionReceipt,
|
||||
normalizeLocalReconciliationCompletionReceipt,
|
||||
} = require('../dist/deployment/reconciliation/completion/evidence.js');
|
||||
|
||||
function digest(value) {
|
||||
return crypto
|
||||
.createHash('sha256')
|
||||
@@ -2974,6 +2963,55 @@ test('review diagnostics keep secret and unknown facts blocked and row-free', (t
|
||||
);
|
||||
});
|
||||
|
||||
test('review diagnostics preserve target-native identity but keep legacy identity fail-closed', (t) => {
|
||||
const initializeDatabases = (paths) => {
|
||||
planningDatabaseInitializer()(paths);
|
||||
const legacy = new DatabaseSync(paths.legacySourcePath);
|
||||
legacy.exec('CREATE TABLE "Auths" (id INTEGER PRIMARY KEY)');
|
||||
legacy.close();
|
||||
fs.copyFileSync(paths.legacySourcePath, paths.recoveryPath);
|
||||
fs.chmodSync(paths.recoveryPath, 0o600);
|
||||
const target = new DatabaseSync(paths.targetDatabasePath);
|
||||
target.exec(
|
||||
'CREATE TABLE "QingLong3IdentityRecords" (id INTEGER PRIMARY KEY)',
|
||||
);
|
||||
target.close();
|
||||
};
|
||||
const state = preparedReview(t, {
|
||||
initializeDatabases,
|
||||
planId: '00000000-0000-4000-8000-000000000333',
|
||||
reviewId: '00000000-0000-4000-8000-000000000334',
|
||||
reviewSuffix: 'identity-custody',
|
||||
});
|
||||
assert.equal(state.planned.outcome, 'manual_required');
|
||||
const prepared = prepareLocalReconciliationReview(state.reviewCommand);
|
||||
const legacyCommand = diagnosticCommand(state, prepared, {
|
||||
database: 'legacy',
|
||||
domain: 'identity_policy_audit',
|
||||
outputName: 'legacy-identity.json',
|
||||
});
|
||||
writeLocalReconciliationReviewDiagnostics(legacyCommand);
|
||||
const legacy = JSON.parse(
|
||||
fs.readFileSync(legacyCommand.request.outputPath, 'utf8'),
|
||||
);
|
||||
assert.equal(legacy.records[0].name, 'Auths');
|
||||
assert.equal(legacy.records[0].decisionRequirement, 'blocked');
|
||||
assert.equal(legacy.records[0].reason, 'identity_custody_required');
|
||||
|
||||
const targetCommand = diagnosticCommand(state, prepared, {
|
||||
database: 'target',
|
||||
domain: 'identity_policy_audit',
|
||||
outputName: 'target-identity.json',
|
||||
});
|
||||
writeLocalReconciliationReviewDiagnostics(targetCommand);
|
||||
const target = JSON.parse(
|
||||
fs.readFileSync(targetCommand.request.outputPath, 'utf8'),
|
||||
);
|
||||
assert.equal(target.records[0].name, 'QingLong3IdentityRecords');
|
||||
assert.equal(target.records[0].decisionRequirement, 'required');
|
||||
assert.equal(target.records[0].reason, 'reviewable_fact');
|
||||
});
|
||||
|
||||
test('review diagnostics page at sixty-four and CLI output stays content-free', (t) => {
|
||||
const initializeDatabases = (paths) => {
|
||||
planningDatabaseInitializer()(paths);
|
||||
@@ -4352,7 +4390,7 @@ test('Secret/Config apply publishes encrypted material atomically and recovers e
|
||||
assert.equal(databaseCloses, authentications);
|
||||
});
|
||||
|
||||
test('completion v3 proves Secret/Config apply but preserves rollback authority while other domains remain manual', async (t) => {
|
||||
test('completion v3 completes a classified v52 target and collects Secret/Config rollback authority', async (t) => {
|
||||
const state = await appliedSecretConfigFixture(t, {
|
||||
suffix: 'completion-v3',
|
||||
});
|
||||
@@ -4402,116 +4440,71 @@ test('completion v3 proves Secret/Config apply but preserves rollback authority
|
||||
const backupPath = path.join(backupRoot, 'before.sqlite');
|
||||
const materialPath = path.join(applyRoot, 'materials.ndjson');
|
||||
assert.equal(fs.existsSync(backupPath), true);
|
||||
|
||||
await assert.rejects(
|
||||
completeLocalReconciliation(command),
|
||||
/identity_policy_audit is not terminally reconciled/,
|
||||
const plan = JSON.parse(
|
||||
fs.readFileSync(
|
||||
path.join(
|
||||
state.planRoot,
|
||||
state.prepareCommand.request.planId,
|
||||
'plan.json',
|
||||
),
|
||||
'utf8',
|
||||
),
|
||||
);
|
||||
assert.equal(fs.existsSync(backupPath), true);
|
||||
assert.deepEqual(
|
||||
plan.domains.find((domain) => domain.domain === 'unknown'),
|
||||
{
|
||||
domain: 'unknown',
|
||||
legacySchemaObjects: 0,
|
||||
targetSchemaObjects: 0,
|
||||
legacyTables: 0,
|
||||
targetTables: 0,
|
||||
legacyRows: 0,
|
||||
targetRows: 0,
|
||||
rowCountsComplete: true,
|
||||
inventoryDigest: plan.domains.find(
|
||||
(domain) => domain.domain === 'unknown',
|
||||
).inventoryDigest,
|
||||
disposition: 'aligned',
|
||||
},
|
||||
);
|
||||
assert.equal(
|
||||
plan.domains.find((domain) => domain.domain === 'identity_policy_audit')
|
||||
.disposition,
|
||||
'target_only',
|
||||
);
|
||||
const targetIdentityDecisions = state.reviewFile.decisions.filter(
|
||||
(decision) =>
|
||||
decision.database === 'target' &&
|
||||
decision.domain === 'identity_policy_audit',
|
||||
);
|
||||
assert.ok(targetIdentityDecisions.length > 0);
|
||||
assert.equal(
|
||||
targetIdentityDecisions.every(
|
||||
(decision) => decision.disposition === 'retain_target',
|
||||
),
|
||||
true,
|
||||
);
|
||||
|
||||
const completed = await completeLocalReconciliation(command);
|
||||
assert.equal(completed.status, 'completed');
|
||||
assert.equal(completed.state, 'reconciliation_completed');
|
||||
assert.equal(completed.adapterCount, 1);
|
||||
assert.equal((await completeLocalReconciliation(command)).status, 'existing');
|
||||
assert.equal(
|
||||
readLocalCutoverInstanceHead(
|
||||
state.deploymentRoot,
|
||||
state.captureCommand.request.instanceId,
|
||||
state.uid,
|
||||
).state,
|
||||
'reconciliation_secret_config_applied',
|
||||
);
|
||||
assert.equal(
|
||||
fs.existsSync(path.join(completionRoot, command.request.completionId)),
|
||||
false,
|
||||
);
|
||||
|
||||
const domainNames = [
|
||||
'schema_lineage',
|
||||
'automation',
|
||||
'secret_and_config',
|
||||
'run_history',
|
||||
'plugin_package',
|
||||
'ai_and_tool',
|
||||
'identity_policy_audit',
|
||||
'unknown',
|
||||
];
|
||||
const domains = domainNames.map((domain) =>
|
||||
domain === 'secret_and_config'
|
||||
? {
|
||||
domain,
|
||||
action: 'adapter_required',
|
||||
evidenceKind: 'secret_config_application',
|
||||
evidenceDigest: state.applied.applyDigest,
|
||||
}
|
||||
: {
|
||||
domain,
|
||||
action: 'no_effect',
|
||||
evidenceKind: 'application_summary',
|
||||
evidenceDigest: 'a'.repeat(64),
|
||||
},
|
||||
);
|
||||
const receipt = buildLocalReconciliationCompletionReceipt({
|
||||
completionId: command.request.completionId,
|
||||
applicationId: command.request.applicationId,
|
||||
profile: state.captureCommand.request.profile,
|
||||
instanceId: state.captureCommand.request.instanceId,
|
||||
cutoverId: state.captureCommand.request.cutoverId,
|
||||
generation: 1,
|
||||
activationDigest: state.captureCommand.request.expectedActivationDigest,
|
||||
applicationPlanDigest: state.application.applicationPlanDigest,
|
||||
sourceHeadDigest: state.applied.instanceHeadDigest,
|
||||
domains,
|
||||
adapterCount: 1,
|
||||
completedAtMs: command.request.completedAtMs,
|
||||
});
|
||||
assert.equal(receipt.schemaVersion, 3);
|
||||
assert.deepEqual(
|
||||
normalizeLocalReconciliationCompletionReceipt(receipt),
|
||||
receipt,
|
||||
);
|
||||
|
||||
const currentHead = readLocalCutoverInstanceHead(
|
||||
state.deploymentRoot,
|
||||
state.captureCommand.request.instanceId,
|
||||
state.uid,
|
||||
);
|
||||
const durableHead = advanceLocalCutoverInstanceHead(
|
||||
{
|
||||
options: { deploymentRoot: state.deploymentRoot },
|
||||
request: {
|
||||
cutoverId: state.captureCommand.request.cutoverId,
|
||||
profile: state.captureCommand.request.profile,
|
||||
instanceId: state.captureCommand.request.instanceId,
|
||||
expectedActivationDigest:
|
||||
state.captureCommand.request.expectedActivationDigest,
|
||||
requestedAtMs: command.request.completedAtMs,
|
||||
},
|
||||
},
|
||||
state.uid,
|
||||
'reconciliation_completed',
|
||||
currentHead.generation,
|
||||
receipt.completionDigest,
|
||||
);
|
||||
assert.equal(durableHead.state, 'reconciliation_completed');
|
||||
const selected = localReconciliationSecretConfigApplyPaths(
|
||||
state.secretConfigApplyRoot,
|
||||
state.secretConfigId,
|
||||
);
|
||||
const intent = readLocalReconciliationSecretConfigApplyIntent(
|
||||
selected,
|
||||
state.uid,
|
||||
);
|
||||
collectLocalReconciliationSecretConfigCompletedStorage(
|
||||
selected,
|
||||
intent,
|
||||
state.uid,
|
||||
);
|
||||
collectLocalReconciliationSecretConfigCompletedStorage(
|
||||
selected,
|
||||
intent,
|
||||
state.uid,
|
||||
);
|
||||
validateLocalReconciliationSecretConfigCompletedStorage(
|
||||
selected,
|
||||
intent,
|
||||
state.uid,
|
||||
const receipt = JSON.parse(
|
||||
fs.readFileSync(
|
||||
path.join(completionRoot, command.request.completionId, 'receipt.json'),
|
||||
'utf8',
|
||||
),
|
||||
);
|
||||
assert.equal(receipt.schemaVersion, 3);
|
||||
assert.equal(fs.existsSync(backupPath), false);
|
||||
assert.deepEqual(fs.readdirSync(backupRoot), []);
|
||||
assert.deepEqual(fs.readdirSync(path.join(applyRoot, 'rollback-work')), []);
|
||||
@@ -4527,6 +4520,26 @@ test('completion v3 proves Secret/Config apply but preserves rollback authority
|
||||
evidenceDigest: state.applied.applyDigest,
|
||||
},
|
||||
);
|
||||
for (const domainName of ['identity_policy_audit', 'unknown']) {
|
||||
assert.equal(
|
||||
receipt.domains.find((domain) => domain.domain === domainName).action,
|
||||
'no_effect',
|
||||
);
|
||||
}
|
||||
const verified = await verifyLocalReconciliationCompletion({
|
||||
schemaVersion: 3,
|
||||
operation: 'local.deployment.reconciliation.complete.verify',
|
||||
options: command.options,
|
||||
request: {
|
||||
completionId: command.request.completionId,
|
||||
applicationId: command.request.applicationId,
|
||||
expectedCompletionDigest: completed.completionDigest,
|
||||
automation: null,
|
||||
secretConfig,
|
||||
runHistory: null,
|
||||
},
|
||||
});
|
||||
assert.equal(verified.status, 'verified');
|
||||
});
|
||||
|
||||
test('completion v3 rejects rolled-back Secret/Config evidence', async (t) => {
|
||||
|
||||
Reference in New Issue
Block a user