test(ql3): qualify failed upgrades on edge devices

This commit is contained in:
whyour
2026-08-14 14:53:44 +08:00
parent 1c75ac0f45
commit 51145a8028
10 changed files with 1156 additions and 71 deletions
+2 -1
View File
@@ -36,6 +36,7 @@
- 2026-08-14 人工恢复更新(ADR-0397,已接受):上述唯一保留的 `executing + Job/receipt 均缺失` 不确定窗口现在具有显式 Cluster 产品处置路径。既有 Approval management mTLS/OIDC endpoint 新增 `approval.recover.inspect|resolve`,只接受五分钟内 `multi_factor|hardware` User、独立 `approval.recover` 权限、二次认证、exact execution version/digest 和外部 evidence SHA-256。只允许 Secret binding/transition action`confirm_failed` 写 failed`abandon_unknown` 写 blocked,永远禁止人工 succeeded、Job 重建或 execution 重置。PostgreSQL `pg-0065`/capability v64 新增不可变 resolution ledger 与单个 SECURITY DEFINER resolver,在同一事务内锁 Policy/execution fence、写 allowed audit、推进终态并写 receiptApproval manager 只有 dispatch/execution/resolution SELECT 与函数 EXECUTE,没有 execution UPDATE。通用 execution repository 与 Worker Credential 调用链保持不变。真实 PostgreSQL 18.4 已从空库完成 65 migration,证明原子提交、exact replay 不重复审计和 direct UPDATE `42501`;实现不新增 package、依赖、Pod、Pool、daemon、timer、watcher 或 Edge/Standalone 负担。18-package clean build/test 退出 0backend 1,194 pass/2 skip/0 failpackage/dependency/edge/deployment 审计零 finding;新 migration 与 repository 内聚到 `approved-action` 领域,migration ledger 直属源码保持审定上限 65。PostgreSQL 18.4 arm64 physical HA 125 项 gate、timeline `1→2` 通过,报告 SHA-256 为 `6d4921cba74475d15722a13c6a8034793c0ee25681bc7dcaf91024927c5752fe`,临时 Docker 资源已清理。
- 2026-08-14 升级失败收口(ADR-0398,进行中):已存在旧 active 的 `upgrade|reinstall|rollback` 不再先切 active pointer 再物化 Package 资源。共享 activation prerequisite sequence 固定为 Secret binding/transition receipt 就绪后,从 staged install 与 immutable lock 构建候选 generation,按既有字节上限完成 Manifest、Task、Workflow、Prompt、Tool 语义物化,并以 generation digest 预发布 immutable revision;只有成功后才进入 active pointer CAS。确定性候选错误把本次 install 写为 `failed(activation_fact_conflict)``activeLockDigest` 保持 `previousActiveLockDigest` 且 publisher 不被调用;瞬时文件/OCI/数据库故障保持 staged 重试。generation 1 没有旧版本可保留,Secret-aware 首次安装继续使用 ADR-0395 的 post-activation B1 binding ceremony,不被候选门错误拒绝。激活后的 publication recovery 只复用 revision 做 generation-fenced reconciliation。实现复用既有 18 个 package、双方言 repository、Local 单 SQLite authority 与 Cluster caller-driven recovery Job/单 Pool,不新增 migration、表、依赖、daemon、timer、watcher、listener、连接池或常驻 cache。定向 Runtime Core 21/21、Runtime Core 全量 548/548、Local Application 47 pass/4 条件 skip、Cluster Admin 347 pass/3 条件 skip18-package clean build/test 退出 0backend 1196 项为 1194 pass/2 条件 skip/0 failpackage boundary、cluster dependency、cluster deployment 与 edge import 审计均通过。PostgreSQL `18.4` arm64 physical HA 125 项、timeline `1→2` 通过,报告 SHA-256 `75d7a52be75c22b2aacf32f2d7e2c432a467ebaab4d639668ff3a4b98767a17e`。真实 Kubernetes 失败升级未移动 active pointer/head 的现场门与固定物理低配设备证据仍待闭合。
- 2026-08-14 失败升级现场门更新:既有 PostgreSQL/OCI/Kubernetes recovery E2E 已升级为 report v2。门先激活 signed OCI generation 1,再排入包含合法 Task 与循环 Workflow 的 generation 2;第一次 recovery 必须因 transition receipt 缺失而失败并保持 staged,提交 content-free receipt 后,第二次 recovery 必须确定性写入 `failed(activation_fact_conflict)`,且 generation 2 materialized revision 数量为 0。门在升级前后逐字比较 active ConfigMap UID、`resourceVersion` 与完整 `active.json`,从而排除“先切 pointer、再补偿”的假安全;OCI v1 六路径各取一次、v2 六路径各取两次,全部要求 HTTPS、exact Basic authentication 与零 redirect。runtime rollout 只绑定最终成功 recovery JobConfigMap-only RBAC 与 runtime 数据库隔离保持不变。证据链现强制使用 canonical absolute path 原子写入 `0600` no-replace 私有报告,绑定 40-hex source revision 与 admin/control OCI revision label;持久报告只保存 active JSON digest,不保存原始 pointer、credential、DSN、kubeconfig、证书或 Secret material。独立离线审计以 `O_NOFOLLOW` 和 inode/mode/size 复验文件,并 exact-shape 校验 provenance、ordering、数据库/OCI/RBAC/runtime 事实、11 个 gate 和 limitationCI 审计成功后上传固定 14 天的 evidence artifact。该链路只增加验收代码,不增加产品 package、依赖或低配/集群运行时常驻开销。本轮 producer/离线审计契约 14/14、18-package clean build/test 退出 0、backend 1201 pass/2 skip/0 failpackage/dependency/deployment/edge 审计均通过;PostgreSQL 18.4 arm64 physical HA 125 gate、timeline `1→2` 通过,报告 SHA-256 `8560469694c67776e5e4c70977f8bde8d4f5635f8e7d1c293ef449dc6da59f72`。本机 admin/control 镜像构建成功,但固定 Kind 1.32.8 节点镜像不在缓存且受限网络无法取得,门在创建节点前中止并确认无遗留集群/容器;因此远端 Kubernetes 成功记录与固定物理低配设备证据仍阻断 ADR Accepted。
- 2026-08-14 固定低配失败升级 workload 更新:新增 `plugin_package_failed_upgrade_edge_candidate`,在 fresh production migration SQLite 上用正式 install/materialized repositories、正式 recovery coordinator 和正式资源物化 prerequisite,先激活 generation 1,再恢复含循环 Workflow 的 generation 2。exact gate 要求候选从 staged 进入 `failed(activation_fact_conflict)`、旧 active/previous digest 均保留、stage/publisher/inspect 零调用、候选 revision 零行、byte source 四路径各一次、`integrity_check=ok`,并限制耗时、RSS delta 与数据库 logical/allocated growth。它已进入 128 MiB router stress、256 MiB Edge release 和统一 physical recorderCI cgroup 报告永久保持 `supported=false`,只有固定型号、无虚拟化设备在声明数据盘生成的统一私有总报告才是物理候选,且仍不证明断电。Node `v24.18.0` arm64 开发机本轮观测 14.390 ms、RSS delta 3,014,656 bytes、SQLite 增长 8,192 bytes,只作为可运行性样本。阶段完整验证为 18-package clean build/test 退出 0、backend 1,203 pass/2 skip/0 fail、相关 backend/发布契约 34/34package boundary 仍为 18 个且 `singleSourcePackages=[]``shallowSourcePackages=[]`cluster dependency/deployment、Edge import 和 service bridge import 审计零 finding。固定设备统一报告与远端 Kubernetes 成功记录仍是 ADR-0398 转 Accepted 的阻断项。
- D-302/ADR-0390(已接受)
Cluster operator context 增加无网络、无 mutation 的内建 `ql3-cluster-admin context validate` 预检。它先复用 owner-private context
reader,再让每个 entry 经过与真实请求相同的 production HTTPS/Kubernetes configuration preparation,验证精确 route、hostname、CA、
@@ -5911,7 +5912,7 @@ ADR-0088 将 Linux CI 拆为三个不能互相替代的档位:
三档都固定 Node `24.18.0` 和 Debian slim,只在原生 Linux x64/arm64 runner 上执行。`scripts/ql3-linux-resource-gate.cjs` 必须从容器内部证明 cgroup v2 的 memory/swap/cpu/PID 精确限额、工作前后零 `max`/OOM 事件、非 root、只读根与 workspace、有界可写 `/tmp`、seccomp 和 `NoNewPrivs`Docker CLI 参数、`os.totalmem()`、QEMU 或一次成功退出均不能替代这些事实。Workflow/Prompt crash report 永久保持 `physicalPowerLossProven=false`;固定设备受控断电前,它只能证明进程崩溃恢复。Docker tmpfs 上的 logical/allocated growth 也不能推导闪存 FTL 写放大。
当前 Edge 产品候选仍是 1 CPU、256 MiB RAM、1 GiB 可用持久空间,推荐 512 MiB,但正式支持阈值必须来自固定物理设备。`scripts/ql3-physical-edge-evidence.cjs` 已提供 fail-closed candidate recorder:绑定 exact-shape 设备 manifest、实际架构/内存/kernel/libc/文件系统/容量、Edge 与真实数据盘 Node SQLite 基准,拒绝容器/VM 指示、symlink/路径漂移和覆盖已有输出,以 `0600` + fsync 发布 SHA-256 绑定报告,并永久输出 `supported=false`。D-87 又增加了同设备同 boot 的进程 idle sampler 与专用文件系统 fault probeD-88 增加了通过正式 TaskDefinition Repository 在 fresh v14 SQLite 上写入并完整扫描 100/1000/10000 三档的规模记录器,同时记录 RSS、耗时与数据库 logical/allocated bytes。基础记录器只能导入私有、摘要有效且 scope 精确的同设备同 boot 报告。规模报告不证明 scheduler 吞吐,也不替代 2.x adopted migration 的时间与磁盘峰值。物理支持仍需补齐冷启动/首次 ready、整机指标、adopted migration、application recovery、断电和 release signature;规模协议虽已实现,仍待固定实机采集。Cluster 必须另行记录副本数、PostgreSQL/连接池、claim/ACK/completion 吞吐、重复率、队列深度和 failover;不得从 512 MiB 空载门禁推导生产规格。
当前 Edge 产品候选仍是 1 CPU、256 MiB RAM、1 GiB 可用持久空间,推荐 512 MiB,但正式支持阈值必须来自固定物理设备。`scripts/ql3-physical-edge-evidence.cjs` 已提供 fail-closed candidate recorder:绑定 exact-shape 设备 manifest、实际架构/内存/kernel/libc/文件系统/容量、Edge 与真实数据盘 Node SQLite 基准,拒绝容器/VM 指示、symlink/路径漂移和覆盖已有输出,以 `0600` + fsync 发布 SHA-256 绑定报告,并永久输出 `supported=false`。D-87 又增加了同设备同 boot 的进程 idle sampler 与专用文件系统 fault probeD-88 增加了通过正式 TaskDefinition Repository 在 fresh v14 SQLite 上写入并完整扫描 100/1000/10000 三档的规模记录器,同时记录 RSS、耗时与数据库 logical/allocated bytes。D-306B2/ADR-0398 进一步把真实 Plugin Package 失败升级加入基础 workload:在同一 scratch 数据盘的 fresh production migration SQLite 上先激活 generation 1,再以正式 recovery coordinator、候选物化 prerequisite 和 SQLite repositories 恢复含循环 Workflow 的 generation 2;只有 v2 进入 `failed(activation_fact_conflict)`、旧 active digest 双重保留、publisher 零调用、候选 materialized revision 零行、`integrity_check=ok` 且耗时/RSS/logical 与 allocated 增长均未越界才通过。该 workload 也进入 128/256 MiB cgroup 门,但容器结果仍只属于 stress;统一 physical recorder 在无虚拟化固定设备上采集的总报告才属于物理候选。基础记录器只能导入私有、摘要有效且 scope 精确的同设备同 boot 报告。规模报告不证明 scheduler 吞吐,也不替代 2.x adopted migration 的时间与磁盘峰值;失败升级报告也永久声明 `physical_power_loss_not_proven`。物理支持仍需补齐冷启动/首次 ready、整机指标、adopted migration、application recovery、断电和 release signature;规模与失败升级协议虽已实现,仍待固定实机采集。Cluster 必须另行记录副本数、PostgreSQL/连接池、claim/ACK/completion 吞吐、重复率、队列深度和 failover;不得从 512 MiB 空载门禁推导生产规格。
冷启动/首次 ready 只能对最终 application artifact 与冻结的 readiness contract 计时,不能用 Executor benchmark 或模块 import 代替。TaskDefinition schema、独立 execution revision digest、版本化 Trigger schema、Repository 与读取路径现已由 local SQLite capability v16 冻结,规模记录器只能调用正式端口;它生成的仍是物理 candidate,固定设备报告采集前不形成支持结论。fresh schema migration 计时只属于该规模报告的局部度量,2.x adopted database 仍须单独记录 migration 时间、替表额外磁盘峰值与采样精度。对应证据未完成前,candidate report 保留其余 required evidence。
@@ -152,6 +152,19 @@ pnpm evidence:physical-edge -- \
记录器拒绝 macOS、容器/VM 指示、架构/内存/文件系统漂移、symlink、根目录数据路径、已有输出和非 canonical 路径;SQLite 基准的临时库实际位于声明的数据文件系统。输出以 `0600`、no-replace、fsync 发布,并绑定 SHA-256。`storageMedium`、设备型号和 SoC 仍是 operator-declared provenanceSHA-256 也不是签名,因此报告始终为 `supported=false`
基础记录器现在还在同一真实数据文件系统的私有 scratch 中运行
`plugin_package_failed_upgrade_edge_candidate`:在 fresh production migration SQLite
上先激活 generation 1,再持久化一个包含循环 Workflow 的 generation 2 staged
升级,并通过正式 recovery coordinator、正式候选资源物化 prerequisite 和正式 SQLite
repositories 恢复。报告只有在 generation 2 精确进入
`failed(activation_fact_conflict)`、旧 `activeLockDigest`
`previousActiveLockDigest` 均保留、activation publisher 零调用、候选 materialized
revision 零行、SQLite `integrity_check=ok`,且耗时、RSS delta、logical/allocated
数据库增长均处于固定上限内时才通过。相同 workload 同时进入 128 MiB router stress
与 256 MiB Edge release cgroup 门,前者仍只是 CI stress;只有由本记录器在无虚拟化
指示的固定设备和声明数据盘上生成时,才属于物理候选证据。两条路径都明确保留
`physical_power_loss_not_proven`,不能推出断电安全或正式最低配置。
### 补充 idle 证据
idle sampler 只观察一个已经启动的 QingLong Node 进程,采样窗口为 30 至 3600 秒,间隔为 1 至 60 秒且必须整除窗口。manifest 必须绑定 device、PID 与期望 executable;采样期间 PID、boot ID、进程 start ticks、UID、executable 或命令摘要任一漂移都失败关闭:
@@ -48,7 +48,9 @@ ConfigMap 和数据库 head 之间会出现第二次分布式提交窗口,并
- Local 与 Cluster 组合测试证明恢复顺序一致,既有 active publication/reconciliation 不回归。
- 完整 18-package、backend、package/dependency/deployment/edge/import 审计通过。
- 真实 PostgreSQL/Kubernetes 门证明失败升级没有移动 active ConfigMap/headphysical HA 门通过。
- 固定物理低配设备证据仍由 ADR-0396 单独阻断,不能用开发机观测替代。
- 固定物理低配设备必须运行真实 SQLite 失败升级 workload,证明旧 active 保留、候选
revision 零写入、publisher 零调用、数据库完整性及耗时/RSS/数据盘增长上限;容器
cgroup 结果只能作为前置 stress,不能用开发机或虚拟化观测替代。
## 影响与替代方案
@@ -77,8 +79,8 @@ ConfigMap 和数据库 head 之间会出现第二次分布式提交窗口,并
镜像 provenance、六段单调 ordering、数据库精确计数、OCI 18 次认证请求、ConfigMap-only RBAC、双节点 runtime 绑定、全部
11 个 gate 与三项 limitation 做 exact-shape 校验,并以 `O_NOFOLLOW`/inode/mode/size 复验私有报告。CI 在独立 job 内先审计,
再上传固定 14 天的 source-bound evidence artifact;这一证据链只属于验收,不新增产品 package、依赖或运行时常驻开销。
- 18-package clean build/test 在允许 loopback TLS 的环境退出 0backend 1203 项为
1201 pass/2 条件 skip/0 fail。新增/更新的 recovery E2E producer/离线审计契约 14/14Runtime Core 定向 21/21。package boundary 保持 18 个 package 且
- 18-package clean build/test 在允许 loopback TLS 的环境退出 0backend 1205 项为
1203 pass/2 条件 skip/0 fail。新增/更新的 recovery E2E producer/离线审计契约 14/14Runtime Core 定向 21/21。package boundary 保持 18 个 package 且
`singleSourcePackages=[]``shallowSourcePackages=[]`cluster dependency、cluster deployment
与 edge import 审计均无 finding。
- PostgreSQL `18.4` arm64 physical HA 通过 125 项门,timeline `1→2`,报告 SHA-256
@@ -86,3 +88,18 @@ ConfigMap 和数据库 head 之间会出现第二次分布式提交窗口,并
资源已清理。本机已成功构建现场门所需 admin/control 镜像,但固定 `kindest/node:v1.32.8` 不在本地缓存,受限网络拉取数分钟无进度;
门在创建任何 Kind 节点前被中止,并确认没有遗留集群或容器。因此 v2 门、私有报告与离线审计代码已完成,但仍不能计为真实 Kubernetes
现场通过;远端 CI 成功记录与固定物理低配设备证据仍待完成,本 ADR 保持 Proposed。
- 固定低配资格门的产品工作负载已落地:`evidence:plugin-package-recovery-edge`
使用 fresh production migration SQLite、正式 install/materialized repositories、正式
recovery coordinator 与正式语义物化门,实测无效 generation 2 从 staged 进入 failed
旧 active generation 保留、publisher 0 次、候选 materialized revision 0 行、数据库
`integrity_check=ok`。该工作负载已接入 128/256 MiB Linux 资源门和统一 physical Edge
recorder;统一聚合器会 exact-shape 拒绝缺失、改名或失败报告,并把
`plugin_package_failed_upgrade_retains_active_generation` 列为独立 collected/remaining
evidence。当前本机 arm64 执行仅是开发验证,尚未产生固定型号、无虚拟化设备上的
owner-private 总报告,也未证明受控断电,因此不能把本 ADR 转为 Accepted。
本轮真实 workload 在 Node `v24.18.0` arm64 开发机观测为 14.390 ms、RSS delta
3,014,656 bytes、SQLite logical/allocated growth 各 8,192 bytes,均低于
10,000 ms、96 MiB、4 MiB 的候选上限;这些数值只证明脚本与门禁可运行,不是
固定设备支持结论。对应 backend 新增路径、物理聚合和 Linux release 契约 34/34
package boundary、cluster dependency、cluster deployment、Edge import 与 service
bridge import 审计均通过。
+1
View File
@@ -58,6 +58,7 @@
"benchmark:worker": "node scripts/ql3-worker-resource-benchmark.cjs",
"benchmark:ai-profile": "node scripts/ql3-ai-profile-benchmark.cjs",
"evidence:physical-edge": "node scripts/ql3-physical-edge-evidence.cjs",
"evidence:plugin-package-recovery-edge": "node scripts/ql3-plugin-package-recovery-edge-benchmark.cjs",
"evidence:physical-edge-idle": "node scripts/ql3-physical-edge-idle-sampler.cjs",
"evidence:physical-edge-fault": "node scripts/ql3-physical-edge-fault-probe.cjs",
"evidence:physical-edge-task-scale": "node scripts/ql3-physical-edge-task-scale.cjs",
+51 -41
View File
@@ -295,41 +295,41 @@ function createWorkloadPlans(root, tierName) {
...(tierName === 'edge-release-ci'
? ['edge', 'standalone'].map((profile) =>
Object.freeze({
name: `local-ai-prompt-durable-output-${profile}`,
format: 'node_test',
nodeArgs: Object.freeze([
'--test',
'--test-name-pattern=executes one active Package Prompt',
path.join(
root,
'packages/ql3-local-application/test/activation.test.cjs',
),
]),
env: Object.freeze({
QL3_PROMPT_RESOURCE_PROFILE: profile,
QL3_PROMPT_RESOURCE_OUTPUT_BYTES: String(
PROMPT_RESOURCE_OUTPUT_BYTES,
),
}),
maxProcessRssBytes: tier.promptMaxProcessRssMb * MIB,
contract: Object.freeze({
kind: 'durable_prompt_output_resource',
profile,
journalMode: profile === 'edge' ? 'delete' : 'wal',
durableOutputBytes: PROMPT_RESOURCE_OUTPUT_BYTES,
providerCalls: 2,
keyLoads: 1,
keyResolutions: 1,
exactReplay: true,
contentFree: true,
maxLogicalWriteAmplificationPermille: 3_000,
maxAllocatedWriteAmplificationPermille: 3_500,
maxWalWriteAmplificationPermille:
profile === 'standalone' ? 3_000 : 0,
requireWalGrowth: profile === 'standalone',
runAttempts: 0,
physicalPowerLossProven: false,
}),
name: `local-ai-prompt-durable-output-${profile}`,
format: 'node_test',
nodeArgs: Object.freeze([
'--test',
'--test-name-pattern=executes one active Package Prompt',
path.join(
root,
'packages/ql3-local-application/test/activation.test.cjs',
),
]),
env: Object.freeze({
QL3_PROMPT_RESOURCE_PROFILE: profile,
QL3_PROMPT_RESOURCE_OUTPUT_BYTES: String(
PROMPT_RESOURCE_OUTPUT_BYTES,
),
}),
maxProcessRssBytes: tier.promptMaxProcessRssMb * MIB,
contract: Object.freeze({
kind: 'durable_prompt_output_resource',
profile,
journalMode: profile === 'edge' ? 'delete' : 'wal',
durableOutputBytes: PROMPT_RESOURCE_OUTPUT_BYTES,
providerCalls: 2,
keyLoads: 1,
keyResolutions: 1,
exactReplay: true,
contentFree: true,
maxLogicalWriteAmplificationPermille: 3_000,
maxAllocatedWriteAmplificationPermille: 3_500,
maxWalWriteAmplificationPermille:
profile === 'standalone' ? 3_000 : 0,
requireWalGrowth: profile === 'standalone',
runAttempts: 0,
physicalPowerLossProven: false,
}),
}),
)
: []),
@@ -395,10 +395,7 @@ function createWorkloadPlans(root, tierName) {
profiles: Object.freeze(['edge', 'standalone']),
crashPointsPerProfile: 7,
scenarios: 14,
boundaries: Object.freeze([
'model_start',
'model_completion',
]),
boundaries: Object.freeze(['model_start', 'model_completion']),
mechanism: 'process_sigkill_then_database_reopen',
physicalPowerLossProven: false,
}),
@@ -426,6 +423,20 @@ function createWorkloadPlans(root, tierName) {
}),
]
: []),
Object.freeze({
name: 'plugin-package-failed-upgrade',
format: 'json',
script: path.join(
root,
'scripts/ql3-plugin-package-recovery-edge-benchmark.cjs',
),
args: Object.freeze([
'--json',
'--max-duration-ms=10000',
`--max-rss-delta-mb=${tier.edgeMaxRssDeltaMb}`,
`--max-database-growth-bytes=${4 * MIB}`,
]),
}),
]);
}
return Object.freeze([
@@ -489,8 +500,7 @@ function parseNodeTestReport(output, plan) {
evidence.exactReplay !== contract.exactReplay ||
evidence.contentFree !== contract.contentFree ||
evidence.durableFacts?.attempts !== contract.runAttempts ||
evidence.physicalPowerLossProven !==
contract.physicalPowerLossProven ||
evidence.physicalPowerLossProven !== contract.physicalPowerLossProven ||
!Number.isSafeInteger(
evidence.databaseLogicalWriteAmplificationPermille,
) ||
+88 -1
View File
@@ -5,6 +5,9 @@ const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { spawnSync } = require('node:child_process');
const {
validateReport: validatePluginPackageRecoveryEdgeReport,
} = require('./ql3-plugin-package-recovery-edge-benchmark.cjs');
const MIB = 1024 * 1024;
const GIB = 1024 * MIB;
@@ -710,12 +713,72 @@ function runEvidenceWorkloads(root, dataPath) {
],
{ TMPDIR: scratchPath },
),
runJsonWorkload(
root,
'plugin-package-failed-upgrade',
'scripts/ql3-plugin-package-recovery-edge-benchmark.cjs',
[
'--json',
'--max-duration-ms=10000',
'--max-rss-delta-mb=96',
`--max-database-growth-bytes=${4 * MIB}`,
],
{ TMPDIR: scratchPath },
),
]);
} finally {
fs.rmSync(scratchPath, { recursive: true, force: false });
}
}
function validateEvidenceWorkloads(workloads) {
if (!Array.isArray(workloads) || workloads.length !== 3) {
return Object.freeze(['physical evidence workloads are incomplete']);
}
const expectedNames = [
'edge-executor',
'node-sqlite-on-device-storage',
'plugin-package-failed-upgrade',
];
if (
workloads.some(
(workload, index) =>
!hasExactKeys(workload, ['name', 'report']) ||
workload.name !== expectedNames[index],
)
) {
return Object.freeze(['physical evidence workload identity is invalid']);
}
const violations = [];
const edge = workloads[0].report;
if (
edge?.schemaVersion !== 1 ||
edge?.profile !== 'edge' ||
edge?.gates?.passed !== true ||
!Array.isArray(edge?.gates?.violations) ||
edge.gates.violations.length !== 0
) {
violations.push('edge executor workload did not pass exactly');
}
const sqlite = workloads[1].report;
if (
sqlite?.journalMode !== 'delete' ||
sqlite?.synchronous !== 'full' ||
sqlite?.integrityCheck !== 'ok' ||
!Number.isSafeInteger(sqlite?.databaseBytes) ||
sqlite.databaseBytes < 1 ||
!isNonNegativeNumber(sqlite?.rssDeltaMb)
) {
violations.push('on-device SQLite workload is invalid');
}
violations.push(
...validatePluginPackageRecoveryEdgeReport(workloads[2].report).map(
(violation) => `Plugin Package recovery workload: ${violation}`,
),
);
return Object.freeze(violations);
}
function canonicalDigest(value) {
return crypto
.createHash('sha256')
@@ -1402,7 +1465,10 @@ function buildEvidenceReport({
supplementalEvidence = [],
generatedAt,
}) {
const violations = validateObservedPlatform(manifest, observed);
const violations = [
...validateObservedPlatform(manifest, observed),
...validateEvidenceWorkloads(workloads),
];
const idleEvidence = supplementalEvidence.find(
({ evidenceClass }) => evidenceClass === 'physical_edge_idle_candidate',
);
@@ -1491,6 +1557,19 @@ function buildEvidenceReport({
if (directServiceStopEvidence) {
collectedEvidence.push('init_managed_graceful_application_stop');
}
const pluginPackageRecoveryEvidence = Array.isArray(workloads)
? workloads.find(({ name }) => name === 'plugin-package-failed-upgrade')
: undefined;
const pluginPackageRecoveryQualified =
pluginPackageRecoveryEvidence !== undefined &&
validatePluginPackageRecoveryEdgeReport(
pluginPackageRecoveryEvidence.report,
).length === 0;
if (pluginPackageRecoveryQualified) {
collectedEvidence.push(
'plugin_package_failed_upgrade_retains_active_generation',
);
}
const remainingRequiredEvidence = [
...(directServiceStartEvidence
? ['firmware_and_bootloader_power_on_to_linux_kernel_clock']
@@ -1513,6 +1592,13 @@ function buildEvidenceReport({
'power_loss_restart',
'release_archive_signature',
];
if (!pluginPackageRecoveryQualified) {
remainingRequiredEvidence.splice(
1,
0,
'plugin_package_failed_upgrade_retains_active_generation',
);
}
if (!idleEvidence) {
remainingRequiredEvidence.splice(
1,
@@ -1713,6 +1799,7 @@ module.exports = {
validateServiceStartEvidenceReport,
validateDirectServiceStartEvidenceReport,
validateDirectServiceStopEvidenceReport,
validateEvidenceWorkloads,
validateTaskScaleWorkload,
validateObservedPlatform,
writeNoReplace,
@@ -0,0 +1,863 @@
#!/usr/bin/env node
const crypto = require('node:crypto');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { performance } = require('node:perf_hooks');
const MIB = 1024 * 1024;
const DEFAULT_MAX_DURATION_MS = 10_000;
const DEFAULT_MAX_RSS_DELTA_MB = 96;
const DEFAULT_MAX_DATABASE_GROWTH_BYTES = 4 * MIB;
const REPORT_KEYS = Object.freeze([
'calls',
'database',
'durable',
'evidenceClass',
'gates',
'identity',
'limitations',
'measurement',
'recovery',
'schemaVersion',
'supported',
'thresholds',
]);
const CALL_KEYS = Object.freeze([
'byteSourceClose',
'byteSourceOpen',
'byteSourceRead',
'publisherInspect',
'publisherPublish',
'stage',
]);
const DATABASE_KEYS = Object.freeze([
'allocatedBytes',
'allocatedGrowthBytes',
'files',
'integrityCheck',
'journalMode',
'logicalBytes',
'logicalGrowthBytes',
'materializedCandidateRevisions',
'synchronous',
]);
const DATABASE_FILE_KEYS = Object.freeze([
'allocatedBytes',
'logicalBytes',
'suffix',
]);
const DURABLE_KEYS = Object.freeze([
'activeLockDigestPreserved',
'failedFrom',
'failureReason',
'headInstallationIsCandidate',
'previousActiveLockDigestPreserved',
'state',
]);
const GATE_KEYS = Object.freeze(['passed', 'violations']);
const IDENTITY_KEYS = Object.freeze(['architecture', 'node', 'platform']);
const MEASUREMENT_KEYS = Object.freeze([
'durationMs',
'rssAfterBytes',
'rssBeforeBytes',
'rssDeltaBytes',
]);
const RECOVERY_KEYS = Object.freeze([
'deferred',
'manualRequired',
'pages',
'remaining',
'retry',
'safeToAdmit',
'scanned',
'settled',
'superseded',
]);
const THRESHOLD_KEYS = Object.freeze([
'maxDatabaseGrowthBytes',
'maxDurationMs',
'maxRssDeltaBytes',
]);
const EXPECTED_LIMITATIONS = Object.freeze([
'container_or_vm_resource_limits_are_not_physical_support_evidence',
'physical_power_loss_not_proven',
]);
class QingLong3PluginPackageRecoveryEdgeBenchmarkError extends Error {
constructor(message) {
super(
`QingLong 3.0 Plugin Package recovery Edge benchmark failed: ${message}`,
);
this.name = 'QingLong3PluginPackageRecoveryEdgeBenchmarkError';
}
}
function hasExactKeys(value, expected) {
return (
value !== null &&
typeof value === 'object' &&
!Array.isArray(value) &&
JSON.stringify(Object.keys(value).sort()) === JSON.stringify(expected)
);
}
function positiveNumber(value, label) {
const parsed = Number(value);
if (!Number.isFinite(parsed) || parsed <= 0) {
throw new QingLong3PluginPackageRecoveryEdgeBenchmarkError(
`${label} must be a positive number`,
);
}
return parsed;
}
function positiveSafeInteger(value, label) {
const parsed = Number(value);
if (!Number.isSafeInteger(parsed) || parsed < 1) {
throw new QingLong3PluginPackageRecoveryEdgeBenchmarkError(
`${label} must be a positive safe integer`,
);
}
return parsed;
}
function parseArguments(argv) {
const options = {
json: false,
maxDatabaseGrowthBytes: DEFAULT_MAX_DATABASE_GROWTH_BYTES,
maxDurationMs: DEFAULT_MAX_DURATION_MS,
maxRssDeltaBytes: DEFAULT_MAX_RSS_DELTA_MB * MIB,
};
for (const argument of argv) {
if (argument === '--') continue;
if (argument === '--json') {
options.json = true;
continue;
}
const [name, raw] = argument.split('=', 2);
if (raw === undefined) {
throw new QingLong3PluginPackageRecoveryEdgeBenchmarkError(
`unsupported argument ${argument}`,
);
}
if (name === '--max-duration-ms') {
options.maxDurationMs = positiveNumber(raw, name);
} else if (name === '--max-rss-delta-mb') {
options.maxRssDeltaBytes = positiveNumber(raw, name) * MIB;
} else if (name === '--max-database-growth-bytes') {
options.maxDatabaseGrowthBytes = positiveSafeInteger(raw, name);
} else {
throw new QingLong3PluginPackageRecoveryEdgeBenchmarkError(
`unsupported argument ${name}`,
);
}
}
return Object.freeze(options);
}
function round(value) {
return Math.round(value * 1_000) / 1_000;
}
function sha256(value) {
return crypto.createHash('sha256').update(value).digest('hex');
}
function storageSnapshot(databasePath) {
const files = [
['database', databasePath],
['-journal', `${databasePath}-journal`],
['-wal', `${databasePath}-wal`],
['-shm', `${databasePath}-shm`],
]
.filter(([, filePath]) => fs.existsSync(filePath))
.map(([suffix, filePath]) => {
const stat = fs.statSync(filePath);
return Object.freeze({
suffix,
logicalBytes: stat.size,
allocatedBytes: stat.blocks * 512,
});
});
return Object.freeze({
files: Object.freeze(files),
logicalBytes: files.reduce((sum, file) => sum + file.logicalBytes, 0),
allocatedBytes: files.reduce((sum, file) => sum + file.allocatedBytes, 0),
});
}
function packageFixture(runtime, generation, previous, invalidWorkflow) {
const packageArchitecture =
process.arch === 'x64'
? 'amd64'
: process.arch === 'arm'
? 'arm/v7'
: process.arch;
const task = (id) => ({
schema: 'qinglong/plugin-package-task-resource@v1',
id,
name: `Recovery ${id}`,
labels: { 'plugin.qinglong.io/source': 'edge-recovery-benchmark' },
enabled: true,
kind: 'command',
spec: {
schema: 'qinglong/command@v1',
config: {
command: {
kind: 'argv',
file: '/usr/bin/printf',
args: [id],
},
},
},
});
const workflow = {
schema: 'qinglong/plugin-package-workflow-resource@v1',
id: 'recovery',
name: 'Recovery qualification',
enabled: true,
steps: invalidWorkflow
? [
{ id: 'collect', task: 'collect', needs: ['report'] },
{ id: 'report', task: 'report', needs: ['collect'] },
]
: [
{ id: 'collect', task: 'collect', needs: [] },
{ id: 'report', task: 'report', needs: ['collect'] },
],
};
const values = {
'tasks/collect.json': task('collect'),
'tasks/report.json': task('report'),
'workflows/recovery.json': workflow,
};
const bytes = Object.fromEntries(
Object.entries(values).map(([resourcePath, value]) => [
resourcePath,
Buffer.from(JSON.stringify(value)),
]),
);
const descriptors = Object.entries(bytes)
.map(([resourcePath, material]) => ({
path: resourcePath,
bytes: material.byteLength,
digest: sha256(material),
}))
.sort((left, right) => left.path.localeCompare(right.path));
const manifest = {
apiVersion: runtime.PLUGIN_PACKAGE_API_VERSION,
kind: runtime.PLUGIN_PACKAGE_KIND,
metadata: {
name: 'edge-recovery-benchmark',
displayName: 'Edge recovery benchmark',
version: `${generation}.0.0`,
description: 'Bounded failed-upgrade qualification workload',
license: 'Apache-2.0',
},
spec: {
compatibility: {
qinglong: '>=3.0.0-0 <4.0.0',
architectures: [packageArchitecture],
deploymentProfiles: ['edge'],
},
runtimes: [],
resources: {
memory: { recommended: '16Mi' },
disk: { install: '4Mi', working: '8Mi' },
},
permissions: {
network: { allowedHosts: [] },
secrets: [],
tools: ['system.command'],
},
contents: {
tasks: ['tasks/collect.json', 'tasks/report.json'],
workflows: ['workflows/recovery.json'],
prompts: [],
tools: [],
},
},
};
const environment = {
qinglongVersion: '3.0.0-alpha.0',
architecture: packageArchitecture,
deploymentProfile: 'edge',
runtimes: [],
availableMemoryBytes: 128 * MIB,
availableDiskBytes: 256 * MIB,
};
const plan = runtime.planPluginPackageInstall(
manifest,
environment,
previous?.manifest,
);
const artifactDigest = sha256(`edge-recovery-artifact-${generation}`);
const action = {
lockId: `edge-recovery-lock-${generation}`,
projectId: 'default',
manifest,
plan,
environment,
...(previous ? { previousManifest: previous.manifest } : {}),
source: {
kind: 'offline',
locator: `offline:sha256:${artifactDigest}`,
artifactDigest,
artifactBytes: Object.values(bytes).reduce(
(sum, material) => sum + material.byteLength,
0,
),
contentDigest: runtime.pluginPackageContentTreeDigest(descriptors),
},
architecture: packageArchitecture,
deploymentProfile: 'edge',
targetGeneration: generation,
...(previous ? { previousLockDigest: previous.lock.lockDigest } : {}),
};
const lock = runtime.createPluginPackageLock({
...action,
approval: {
requestId: `edge-recovery-approval-${generation}`,
requestVersion: 1,
dispatchId: `edge-recovery-dispatch-${generation}`,
actionDigest: runtime.pluginPackageInstallActionDigest(action),
previewDigest: runtime.pluginPackageInstallPlanDigest(plan),
approvedBy: { type: 'user', id: 'edge-owner' },
approvedAtMs: 100 + generation,
expiresAtMs: 100_000,
fence: { projectVersion: 1, bindingVersion: 1 },
},
createdAtMs: 200 + generation,
});
return Object.freeze({
bytes: Object.freeze(bytes),
lock,
manifest,
manifestBytes: Buffer.from(
runtime.serializePluginPackageManifest(manifest),
),
});
}
function stage(runtime, value, record, occurredAtMs) {
return runtime.transitionPluginPackageInstall(value.lock, record, {
type: 'stage_completed',
mutationId: `edge-recovery-stage-${value.lock.targetGeneration}`,
occurredAtMs,
stageRef: `edge-recovery:${value.lock.lockDigest}`,
artifactDigest: value.lock.source.artifactDigest,
manifestDigest: value.lock.manifestDigest,
contentDigest: value.lock.source.contentDigest,
evidenceDigest: sha256(
`edge-recovery-stage-${value.lock.targetGeneration}`,
),
});
}
async function createActiveGeneration(runtime, repository, value) {
const queued = runtime.createPluginPackageInstall(value.lock, {
installationId: 'edge-recovery-install-1',
mutationId: 'edge-recovery-create-1',
occurredAtMs: 301,
});
await repository.create(
runtime.pluginPackageInstallCreate(value.lock, queued, null),
);
const staged = stage(runtime, value, queued, 302);
await repository.commit(runtime.pluginPackageInstallCommit(queued, staged));
const activating = runtime.transitionPluginPackageInstall(
value.lock,
staged,
{
type: 'activation_started',
mutationId: 'edge-recovery-activate-1',
occurredAtMs: 303,
},
);
await repository.commit(
runtime.pluginPackageInstallCommit(staged, activating),
);
const active = runtime.transitionPluginPackageInstall(
value.lock,
activating,
{
type: 'activation_committed',
mutationId: 'edge-recovery-commit-1',
occurredAtMs: 304,
activationRef: `edge-active:${value.lock.lockDigest}`,
intentDigest: runtime.pluginPackageActivationIntentDigest(
value.lock,
activating,
),
generation: 1,
contentDigest: value.lock.source.contentDigest,
},
);
await repository.commit(
runtime.pluginPackageInstallCommit(activating, active),
);
return active;
}
async function createStagedUpgrade(runtime, repository, value, active) {
const queued = runtime.createPluginPackageInstall(value.lock, {
installationId: 'edge-recovery-install-2',
mutationId: 'edge-recovery-create-2',
occurredAtMs: 401,
});
await repository.create(
runtime.pluginPackageInstallCreate(value.lock, queued, active),
);
const staged = stage(runtime, value, queued, 402);
await repository.commit(runtime.pluginPackageInstallCommit(queued, staged));
return staged;
}
function runtimeDependencies() {
const pluginPackage = require('../packages/ql3-runtime-core/dist/plugin-package/pluginPackage');
const install = require('../packages/ql3-runtime-core/dist/plugin-package/installation/pluginPackageInstall');
const bundle = require('../packages/ql3-runtime-core/dist/plugin-package/pluginPackageBundle');
const recovery = require('../packages/ql3-runtime-core/dist/plugin-package/installation/pluginPackageRecovery');
const materialization = require('../packages/ql3-runtime-core/dist/plugin-package/pluginPackageResourceMaterialization');
const generation = require('../packages/ql3-runtime-core/dist/plugin-package/pluginPackageResourceGeneration');
const semantics = require('../packages/ql3-runtime-core/dist/task-definition/taskSpecSemantic');
return {
...pluginPackage,
...install,
...bundle,
...recovery,
...materialization,
...generation,
...semantics,
};
}
async function runBenchmark(options) {
if (Number(process.versions.node.split('.')[0]) < 24) {
throw new QingLong3PluginPackageRecoveryEdgeBenchmarkError(
'Node.js 24 or newer is required',
);
}
const { DatabaseSync } = require('node:sqlite');
const runtime = runtimeDependencies();
const {
LocalSqlitePluginPackageInstallRepository,
} = require('../packages/ql3-local-sqlite/dist/plugin-package/pluginPackageInstallRepository');
const {
LocalSqlitePluginPackageMaterializedRevisionRepository,
} = require('../packages/ql3-local-sqlite/dist/plugin-package/pluginPackageMaterializedRevisionRepository');
const {
migrateLocalSqliteDatabase,
} = require('../packages/ql3-local-sqlite/dist/migration/migration');
const temporaryRoot = fs.mkdtempSync(
path.join(os.tmpdir(), 'ql3-package-recovery-edge-'),
);
fs.chmodSync(temporaryRoot, 0o700);
const databasePath = path.join(temporaryRoot, 'recovery.sqlite');
let database;
try {
database = new DatabaseSync(databasePath, {
allowExtension: false,
allowUnknownNamedParameters: false,
defensive: true,
enableDoubleQuotedStringLiterals: false,
enableForeignKeyConstraints: true,
timeout: 1_000,
});
await migrateLocalSqliteDatabase(database);
database.exec('PRAGMA journal_mode = DELETE; PRAGMA synchronous = FULL;');
const baseline = storageSnapshot(databasePath);
const repository = new LocalSqlitePluginPackageInstallRepository(database);
const registry = runtime.createBuiltInTaskSpecSemanticRegistry();
const materializedRepository =
new LocalSqlitePluginPackageMaterializedRevisionRepository(
database,
registry,
);
const first = packageFixture(runtime, 1, null, false);
const second = packageFixture(runtime, 2, first, true);
const calls = {
byteSourceClose: 0,
byteSourceOpen: 0,
byteSourceRead: 0,
publisherInspect: 0,
publisherPublish: 0,
stage: 0,
};
const rssBeforeBytes = process.memoryUsage().rss;
const startedAt = performance.now();
const active = await createActiveGeneration(runtime, repository, first);
const staged = await createStagedUpgrade(
runtime,
repository,
second,
active,
);
const prerequisite =
new runtime.PluginPackageResourceActivationPrerequisite({
byteSource: {
async open(generation) {
if (
generation.lockDigest !== second.lock.lockDigest ||
generation.installationId !== staged.installationId
) {
throw new Error('byte source received another generation');
}
calls.byteSourceOpen += 1;
return {
async read(resourcePath, maximumBytes) {
calls.byteSourceRead += 1;
const material =
resourcePath === 'package.json'
? second.manifestBytes
: second.bytes[resourcePath];
if (!material || material.byteLength > maximumBytes) {
throw new Error('byte source path or bound is invalid');
}
return new Uint8Array(material);
},
async close() {
calls.byteSourceClose += 1;
},
};
},
},
materializedRepository,
taskSpecSemanticRegistry: registry,
});
const coordinator = new runtime.PluginPackageRecoveryCoordinator({
repository,
stageProvider: {
async stage() {
calls.stage += 1;
throw new Error('staged upgrade must not be staged again');
},
},
publisher: {
async publish() {
calls.publisherPublish += 1;
throw new Error('invalid candidate must not be published');
},
async inspect() {
calls.publisherInspect += 1;
throw new Error('invalid candidate must not be inspected');
},
},
activationPrerequisite: prerequisite,
now: () => 500,
});
const recovery = await coordinator.recover({ pageSize: 1, maxPages: 2 });
const durationMs = performance.now() - startedAt;
const rssAfterBytes = process.memoryUsage().rss;
const durable = await repository.find(staged.projectId, staged.packageName);
const candidateGeneration =
runtime.createPluginPackageResourceGenerationFromReferences({
installationId: staged.installationId,
projectId: staged.projectId,
packageName: staged.packageName,
lockDigest: second.lock.lockDigest,
generation: second.lock.targetGeneration,
previousActiveLockDigest: first.lock.lockDigest,
contentDigest: second.lock.source.contentDigest,
resources: second.lock.resources,
});
const materializedCandidateRevisions = database
.prepare(
`SELECT count(*) AS count
FROM "QingLong3PluginPackageMaterializedRevisions"
WHERE generation_digest = ?`,
)
.get(candidateGeneration.generationDigest).count;
const integrityCheck = database
.prepare('PRAGMA integrity_check')
.get().integrity_check;
const finalStorage = storageSnapshot(databasePath);
const logicalGrowthBytes = Math.max(
0,
finalStorage.logicalBytes - baseline.logicalBytes,
);
const allocatedGrowthBytes = Math.max(
0,
finalStorage.allocatedBytes - baseline.allocatedBytes,
);
const rssDeltaBytes = Math.max(0, rssAfterBytes - rssBeforeBytes);
const violations = [];
if (
!durable ||
durable.installationId !== staged.installationId ||
durable.state !== 'failed' ||
durable.failure?.reason !== 'activation_fact_conflict' ||
durable.failure?.failedFrom !== 'staged'
) {
violations.push('candidate did not fail closed from staged');
}
if (
durable?.previousActiveLockDigest !== first.lock.lockDigest ||
durable?.activeLockDigest !== first.lock.lockDigest
) {
violations.push('previous active lock was not preserved');
}
if (
calls.stage !== 0 ||
calls.publisherPublish !== 0 ||
calls.publisherInspect !== 0
) {
violations.push(
'invalid candidate reached stage or activation publisher',
);
}
if (
calls.byteSourceOpen !== 1 ||
calls.byteSourceRead !== 4 ||
calls.byteSourceClose !== 1
) {
violations.push('candidate bytes were not read exactly once');
}
if (materializedCandidateRevisions !== 0) {
violations.push('invalid candidate materialized revision was published');
}
if (integrityCheck !== 'ok') {
violations.push(`SQLite integrity_check returned ${integrityCheck}`);
}
if (durationMs > options.maxDurationMs) {
violations.push('duration exceeded the configured bound');
}
if (rssDeltaBytes > options.maxRssDeltaBytes) {
violations.push('RSS delta exceeded the configured bound');
}
if (
Math.max(logicalGrowthBytes, allocatedGrowthBytes) >
options.maxDatabaseGrowthBytes
) {
violations.push('database growth exceeded the configured bound');
}
const report = Object.freeze({
schemaVersion: 1,
evidenceClass: 'plugin_package_failed_upgrade_edge_candidate',
supported: false,
identity: Object.freeze({
node: process.version,
architecture: process.arch,
platform: process.platform,
}),
measurement: Object.freeze({
durationMs: round(durationMs),
rssBeforeBytes,
rssAfterBytes,
rssDeltaBytes,
}),
thresholds: Object.freeze({
maxDurationMs: options.maxDurationMs,
maxRssDeltaBytes: options.maxRssDeltaBytes,
maxDatabaseGrowthBytes: options.maxDatabaseGrowthBytes,
}),
database: Object.freeze({
journalMode: 'delete',
synchronous: 'full',
integrityCheck,
logicalBytes: finalStorage.logicalBytes,
allocatedBytes: finalStorage.allocatedBytes,
logicalGrowthBytes,
allocatedGrowthBytes,
files: finalStorage.files,
materializedCandidateRevisions,
}),
durable: Object.freeze({
state: durable?.state ?? null,
failureReason: durable?.failure?.reason ?? null,
failedFrom: durable?.failure?.failedFrom ?? null,
headInstallationIsCandidate:
durable?.installationId === staged.installationId,
previousActiveLockDigestPreserved:
durable?.previousActiveLockDigest === first.lock.lockDigest,
activeLockDigestPreserved:
durable?.activeLockDigest === first.lock.lockDigest,
}),
recovery: Object.freeze(recovery),
calls: Object.freeze(calls),
limitations: EXPECTED_LIMITATIONS,
gates: Object.freeze({
passed: violations.length === 0,
violations: Object.freeze(violations),
}),
});
const reportViolations = validateReport(report);
if (reportViolations.length > 0) {
throw new QingLong3PluginPackageRecoveryEdgeBenchmarkError(
`report contract rejected: ${reportViolations.join('; ')}`,
);
}
if (!report.gates.passed) {
throw new QingLong3PluginPackageRecoveryEdgeBenchmarkError(
report.gates.violations.join('; '),
);
}
return report;
} finally {
database?.close();
fs.rmSync(temporaryRoot, { recursive: true, force: true });
}
}
function validateReport(report) {
const violations = [];
if (!hasExactKeys(report, REPORT_KEYS)) {
return Object.freeze(['report shape is invalid']);
}
if (
report.schemaVersion !== 1 ||
report.evidenceClass !== 'plugin_package_failed_upgrade_edge_candidate' ||
report.supported !== false
) {
violations.push('report identity is invalid');
}
if (
!hasExactKeys(report.identity, IDENTITY_KEYS) ||
typeof report.identity.node !== 'string' ||
!['arm64', 'x64', 'arm'].includes(report.identity.architecture) ||
typeof report.identity.platform !== 'string'
) {
violations.push('runtime identity is invalid');
}
if (
!hasExactKeys(report.measurement, MEASUREMENT_KEYS) ||
!Number.isFinite(report.measurement.durationMs) ||
report.measurement.durationMs < 0 ||
!Number.isSafeInteger(report.measurement.rssBeforeBytes) ||
report.measurement.rssBeforeBytes < 1 ||
!Number.isSafeInteger(report.measurement.rssAfterBytes) ||
report.measurement.rssAfterBytes < 1 ||
!Number.isSafeInteger(report.measurement.rssDeltaBytes) ||
report.measurement.rssDeltaBytes < 0
) {
violations.push('measurement is invalid');
}
if (
!hasExactKeys(report.thresholds, THRESHOLD_KEYS) ||
!Number.isFinite(report.thresholds.maxDurationMs) ||
report.thresholds.maxDurationMs <= 0 ||
!Number.isSafeInteger(report.thresholds.maxRssDeltaBytes) ||
report.thresholds.maxRssDeltaBytes < 1 ||
!Number.isSafeInteger(report.thresholds.maxDatabaseGrowthBytes) ||
report.thresholds.maxDatabaseGrowthBytes < 1
) {
violations.push('thresholds are invalid');
}
if (
!hasExactKeys(report.database, DATABASE_KEYS) ||
report.database.journalMode !== 'delete' ||
report.database.synchronous !== 'full' ||
report.database.integrityCheck !== 'ok' ||
report.database.materializedCandidateRevisions !== 0 ||
![
'logicalBytes',
'allocatedBytes',
'logicalGrowthBytes',
'allocatedGrowthBytes',
].every(
(key) =>
Number.isSafeInteger(report.database[key]) && report.database[key] >= 0,
) ||
!Array.isArray(report.database.files) ||
report.database.files.length < 1 ||
report.database.files.length > 4 ||
report.database.files.some(
(file) =>
!hasExactKeys(file, DATABASE_FILE_KEYS) ||
!['database', '-journal', '-wal', '-shm'].includes(file.suffix) ||
!Number.isSafeInteger(file.logicalBytes) ||
file.logicalBytes < 0 ||
!Number.isSafeInteger(file.allocatedBytes) ||
file.allocatedBytes < 0,
)
) {
violations.push('database evidence is invalid');
}
if (
!hasExactKeys(report.durable, DURABLE_KEYS) ||
report.durable.state !== 'failed' ||
report.durable.failureReason !== 'activation_fact_conflict' ||
report.durable.failedFrom !== 'staged' ||
report.durable.headInstallationIsCandidate !== true ||
report.durable.previousActiveLockDigestPreserved !== true ||
report.durable.activeLockDigestPreserved !== true
) {
violations.push('durable recovery facts are invalid');
}
if (
!hasExactKeys(report.recovery, RECOVERY_KEYS) ||
report.recovery.pages !== 1 ||
report.recovery.scanned !== 1 ||
report.recovery.settled !== 1 ||
report.recovery.retry !== 0 ||
report.recovery.manualRequired !== 0 ||
report.recovery.superseded !== 0 ||
report.recovery.deferred !== 0 ||
report.recovery.remaining !== false ||
report.recovery.safeToAdmit !== true
) {
violations.push('recovery outcome is invalid');
}
if (
!hasExactKeys(report.calls, CALL_KEYS) ||
report.calls.stage !== 0 ||
report.calls.publisherPublish !== 0 ||
report.calls.publisherInspect !== 0 ||
report.calls.byteSourceOpen !== 1 ||
report.calls.byteSourceRead !== 4 ||
report.calls.byteSourceClose !== 1
) {
violations.push('authority call evidence is invalid');
}
if (
JSON.stringify(report.limitations) !== JSON.stringify(EXPECTED_LIMITATIONS)
) {
violations.push('limitations are invalid');
}
if (
!hasExactKeys(report.gates, GATE_KEYS) ||
report.gates.passed !== true ||
!Array.isArray(report.gates.violations) ||
report.gates.violations.length !== 0
) {
violations.push('gates did not pass exactly');
}
if (
report.measurement?.durationMs > report.thresholds?.maxDurationMs ||
report.measurement?.rssDeltaBytes > report.thresholds?.maxRssDeltaBytes ||
Math.max(
report.database?.logicalGrowthBytes ?? Number.POSITIVE_INFINITY,
report.database?.allocatedGrowthBytes ?? Number.POSITIVE_INFINITY,
) > report.thresholds?.maxDatabaseGrowthBytes
) {
violations.push('measurement exceeded its declared threshold');
}
return Object.freeze(violations);
}
async function main() {
const options = parseArguments(process.argv.slice(2));
const report = await runBenchmark(options);
process.stdout.write(
`${JSON.stringify(report, null, options.json ? 0 : 2)}\n`,
);
}
module.exports = {
QingLong3PluginPackageRecoveryEdgeBenchmarkError,
parseArguments,
runBenchmark,
validateReport,
};
if (require.main === module) {
main().catch((error) => {
process.stderr.write(
`${error instanceof Error ? error.message : String(error)}\n`,
);
process.exitCode = 1;
});
}
+24 -23
View File
@@ -117,6 +117,7 @@ test('builds tier-specific workload plans without shell commands', () => {
'local-workflow-sqlite-lock',
'local-workflow-admission-crash-recovery',
'local-workflow-control-crash-recovery',
'plugin-package-failed-upgrade',
],
);
assert.match(edge[0].script, /ql3-edge-benchmark\.cjs$/);
@@ -135,10 +136,7 @@ test('builds tier-specific workload plans without shell commands', () => {
),
true,
);
const edgeRelease = createWorkloadPlans(
'/workspace',
'edge-release-ci',
);
const edgeRelease = createWorkloadPlans('/workspace', 'edge-release-ci');
assert.deepEqual(
edgeRelease.map(({ name }) => name),
[
@@ -152,6 +150,7 @@ test('builds tier-specific workload plans without shell commands', () => {
'local-workflow-control-crash-recovery',
'local-ai-prompt-model-invocation-crash-recovery',
'local-ai-prompt-outer-transaction-crash-recovery',
'plugin-package-failed-upgrade',
],
);
assert.equal(
@@ -185,17 +184,11 @@ test('builds tier-specific workload plans without shell commands', () => {
assert.equal(edgeRelease[3].contract.exactReplay, true);
assert.equal(edgeRelease[3].contract.contentFree, true);
assert.equal(edgeRelease[3].contract.durableOutputBytes, 512 * 1024);
assert.equal(
edgeRelease[3].contract.maxWalWriteAmplificationPermille,
0,
);
assert.equal(edgeRelease[3].contract.maxWalWriteAmplificationPermille, 0);
assert.equal(edgeRelease[4].contract.profile, 'standalone');
assert.equal(edgeRelease[4].contract.journalMode, 'wal');
assert.equal(edgeRelease[4].contract.requireWalGrowth, true);
assert.match(
edge[3].script,
/ql3-local-workflow-resource-benchmark\.cjs$/,
);
assert.match(edge[3].script, /ql3-local-workflow-resource-benchmark\.cjs$/);
assert.ok(edge[3].args.includes('--lock-samples=16'));
assert.ok(edge[3].args.includes('--max-lock-p95-ms=500'));
assert.equal(edge[4].contract.scenarios, 16);
@@ -221,6 +214,12 @@ test('builds tier-specific workload plans without shell commands', () => {
true,
);
assert.equal(edgeRelease[9].contract.physicalPowerLossProven, false);
assert.match(
edge.at(-1).script,
/ql3-plugin-package-recovery-edge-benchmark\.cjs$/,
);
assert.ok(edge.at(-1).args.includes('--max-rss-delta-mb=64'));
assert.ok(edgeRelease.at(-1).args.includes('--max-rss-delta-mb=96'));
const cluster = createWorkloadPlans('/workspace', 'cluster-control-ci');
assert.deepEqual(
cluster.map(({ name }) => name),
@@ -248,7 +247,9 @@ test('fails closed when durable Prompt resource evidence drifts', () => {
const [edgePlan, standalonePlan] = createWorkloadPlans(
'/workspace',
'edge-release-ci',
).filter(({ contract }) => contract?.kind === 'durable_prompt_output_resource');
).filter(
({ contract }) => contract?.kind === 'durable_prompt_output_resource',
);
const evidence = {
profile: 'edge',
journalMode: 'delete',
@@ -268,7 +269,9 @@ test('fails closed when durable Prompt resource evidence drifts', () => {
physicalPowerLossProven: false,
};
const output = (value) =>
`tests 1\npass 1\nfail 0\nskipped 0\nQL3_RESOURCE_EVIDENCE=${JSON.stringify(value)}\n`;
`tests 1\npass 1\nfail 0\nskipped 0\nQL3_RESOURCE_EVIDENCE=${JSON.stringify(
value,
)}\n`;
assert.equal(
parseNodeTestReport(output(evidence), edgePlan).evidence.profile,
'edge',
@@ -301,12 +304,8 @@ test('fails closed when durable Prompt resource evidence drifts', () => {
});
test('fails closed when authenticated Workflow cancellation evidence drifts', () => {
const plan = createWorkloadPlans(
'/workspace',
'edge-release-ci',
).find(
({ contract }) =>
contract?.kind === 'local_workflow_product_lifecycle',
const plan = createWorkloadPlans('/workspace', 'edge-release-ci').find(
({ contract }) => contract?.kind === 'local_workflow_product_lifecycle',
);
assert.ok(plan);
const completionEvidence = {
@@ -332,10 +331,12 @@ test('fails closed when authenticated Workflow cancellation evidence drifts', ()
physicalPowerLossProven: false,
};
const output = (value) =>
`tests 2\npass 2\nfail 0\nskipped 0\nQL3_RESOURCE_EVIDENCE=${JSON.stringify(completionEvidence)}\nQL3_RESOURCE_EVIDENCE=${JSON.stringify(value)}\n`;
`tests 2\npass 2\nfail 0\nskipped 0\nQL3_RESOURCE_EVIDENCE=${JSON.stringify(
completionEvidence,
)}\nQL3_RESOURCE_EVIDENCE=${JSON.stringify(value)}\n`;
assert.equal(
parseNodeTestReport(output(cancellationEvidence), plan)
.evidenceRecords[1].processExited,
parseNodeTestReport(output(cancellationEvidence), plan).evidenceRecords[1]
.processExited,
true,
);
assert.throws(
+45 -2
View File
@@ -24,6 +24,9 @@ const {
validateObservedPlatform,
writeNoReplace,
} = require('../../scripts/ql3-physical-edge-evidence.cjs');
const {
runBenchmark: runPluginPackageRecoveryEdgeBenchmark,
} = require('../../scripts/ql3-plugin-package-recovery-edge-benchmark.cjs');
const {
buildReport: buildAdoptionScaleReport,
normalizeManifest: normalizeAdoptionScaleManifest,
@@ -168,15 +171,49 @@ test('fails closed on architecture, memory, filesystem and virtualization drift'
);
});
test('produces digest-bound candidate evidence but never supported status', () => {
test('produces digest-bound candidate evidence but never supported status', async () => {
const pluginPackageRecovery = await runPluginPackageRecoveryEdgeBenchmark({
maxDatabaseGrowthBytes: 4 * 1024 * 1024,
maxDurationMs: 10_000,
maxRssDeltaBytes: 96 * 1024 * 1024,
});
const workloads = [
{
name: 'edge-executor',
report: {
schemaVersion: 1,
profile: 'edge',
gates: { passed: true, violations: [] },
},
},
{
name: 'node-sqlite-on-device-storage',
report: {
journalMode: 'delete',
synchronous: 'full',
integrityCheck: 'ok',
databaseBytes: 16_384,
rssDeltaMb: 1,
},
},
{
name: 'plugin-package-failed-upgrade',
report: pluginPackageRecovery,
},
];
const report = buildEvidenceReport({
manifest: manifest(),
observed: observed(),
workloads: [{ name: 'edge-executor', report: { gates: { passed: true } } }],
workloads,
generatedAt: '2026-07-22T00:00:00.000Z',
});
assert.equal(report.supported, false);
assert.equal(report.qualification.physicalCandidate, true);
assert.ok(
report.qualification.collectedEvidence.includes(
'plugin_package_failed_upgrade_retains_active_generation',
),
);
assert.equal(report.sha256.length, 64);
const changed = buildEvidenceReport({
manifest: manifest({ deviceId: 'router-a2' }),
@@ -184,6 +221,12 @@ test('produces digest-bound candidate evidence but never supported status', () =
workloads: [],
generatedAt: '2026-07-22T00:00:00.000Z',
});
assert.equal(changed.qualification.physicalCandidate, false);
assert.ok(
changed.qualification.remainingRequiredEvidence.includes(
'plugin_package_failed_upgrade_retains_active_generation',
),
);
assert.notEqual(changed.sha256, report.sha256);
});
@@ -0,0 +1,49 @@
const assert = require('node:assert/strict');
const { test } = require('node:test');
const {
parseArguments,
runBenchmark,
validateReport,
} = require('../../scripts/ql3-plugin-package-recovery-edge-benchmark.cjs');
test('parses bounded failed-upgrade benchmark thresholds', () => {
assert.deepEqual(
parseArguments([
'--json',
'--max-duration-ms=5000',
'--max-rss-delta-mb=64',
'--max-database-growth-bytes=2097152',
]),
{
json: true,
maxDatabaseGrowthBytes: 2 * 1024 * 1024,
maxDurationMs: 5_000,
maxRssDeltaBytes: 64 * 1024 * 1024,
},
);
assert.throws(
() => parseArguments(['--max-rss-delta-mb=0']),
/positive number/,
);
assert.throws(() => parseArguments(['--unknown=1']), /unsupported argument/);
});
test('uses real SQLite recovery to reject an invalid upgrade before publication', async () => {
const report = await runBenchmark({
maxDatabaseGrowthBytes: 4 * 1024 * 1024,
maxDurationMs: 10_000,
maxRssDeltaBytes: 96 * 1024 * 1024,
});
assert.deepEqual(validateReport(report), []);
assert.equal(report.durable.state, 'failed');
assert.equal(report.durable.activeLockDigestPreserved, true);
assert.equal(report.database.materializedCandidateRevisions, 0);
assert.equal(report.calls.publisherPublish, 0);
assert.equal(report.gates.passed, true);
assert.deepEqual(validateReport({ ...report, supported: true }), [
'report identity is invalid',
]);
});