test(ql3): lock legacy core api contracts

This commit is contained in:
whyour
2026-08-20 15:41:39 +08:00
parent c0ef9a969e
commit 0c503932cc
4 changed files with 478 additions and 1 deletions
+15 -1
View File
@@ -11,6 +11,20 @@
最新增量证据(2026-08-20):
- D-378/ADR-0471(已接受):把孵化重点从连续 Cluster Console 增量转回 3.0 首发兼容闭环,新增直接运行生产 Cron/Subscription
Express Router 与 Celebrate validator 的 2.x 核心执行 API 基线。Cron 覆盖 list/create/update/disable/enable/run/stop、单日志、日志列表和
单实例 stopSubscription 覆盖 list/create/update/disable/enable/run/stop/log。确定性 TypeDI spy 同时锁定服务参数与现行 JSON envelope,尤其
保持 run/stop/enable/disable 的 `{code:200}` 异步接受语义,不把 v3 Run 字段静默塞回 2.x;两个非法 body 均在 service dispatch 前返回
HTTP 400。聚焦真实回环 Router contract `18/18`。本切片不修改生产 function/class/method,不新增 package、dependency、binary、schema、
migration、服务、部署对象或常驻资源;它只是首个核心样本,不代表完整 2.x 兼容与升级/回滚 Gate 已关闭。System、Script、Open API、鉴权错误码、
真实 SQLite 升级、Primary 双态与目标实例 rollback rehearsal 仍须后续完成。完整 backend 为
`1,523 total / 1,521 pass / 2 conditional skip / 0 fail`18-package clean build/逐包测试在允许既有 Worker TLS 回环门后单次退出 0。
package boundary、Cluster dependency、Edge import、Cluster/Worker deployment、Console 与 Console distribution 七项审计全部
compatible/passedworkspace 仍为 18 packages、`singleSourcePackages=[]``shallowSourcePackages=[]`Cluster Admin 保持
`129 source / 128 nested`。14 档 Local artifact audit 全部 compatible;基础 Edge/Standalone 保持
`2,598,669 / 2,598,747` bytes、57 loaded modulesApplication+AI 为 `4,501,822 / 4,501,954`MCP 为
`7,324,601 / 7,324,709`,证明测试基线不进入低配路由设备闭包。本切片不改变 PostgreSQL schema、ACL、repository、role、Pool、连接或
failover 语义,因此不重跑且不重新占有 HA 证明;D-373/D-374 PostgreSQL 18.6 arm64 HA `146/146`、timeline `1→2` 仅作为相邻既有基线。
- D-377/ADR-0470(已接受):修复可选 Console authority 已默认关闭、浏览器却仍展示全部二十个操作的产品与授权边界漂移。Console CLI 现在把
完成私有文件校验后的唯一 `allowedOperations` 集合注入 server;十三个 Project/Copilot 基础只读操作必须完整存在,Run、Worker 与 Package
三组可选操作只能整组启用或整组关闭,未知、重复、缺失基础操作或半组配置均在监听前失败。认证后的同源
@@ -9463,7 +9477,7 @@ flowchart LR
| 切片 | 当前状态 | 已具备 | 尚未接入/门禁 |
| --- | --- | --- | --- |
| PR-0 Migration/Cluster 基线 | In progress | SQLite/PostgreSQL reviewed migration、checksum/rollback/schema ownership/readiness 与 Node 20/24 × x64/arm64 CI 基线;`runtime-core` 提供共享领域 contract`cluster-postgres` 提供 exact-pinned Pool、四角色、21 条 migration、capability v20/35 表、可写主库、受审 function/role activation check、完整 cluster repository 及显式 admin-only Plugin Package installation和 Approved Action authority`cluster-admin` 提供短生命周期发布 authority、POSIX/Kubernetes Secret adapter、标准 OCI stage verifier、exact-registry credential provider、有界恢复与 caller-driven Package Approved Action dispatcher`local-admin` 以相同 handler 组合 edge/standalone 单 SQLite authority`cluster-control` 提供 readiness-first HTTP/mTLS admission、startup recovery、数据库时钟 row-lease scheduler 和 Remote Worker 控制面。Package admission 已在两个方言的同一事务中强绑定 immutable proposal、exact executing snapshot、Project Policy fence 与数据库观察时钟,并以 receipt 收敛 COMMIT response lossdispatcher 固定 start 前 retry、start 后 durable inspectionedge/standalone/cluster 默认单批 1/4/16 且无 timer。PostgreSQL 18 physical-promotion 门禁已在本机 arm64 完成 streaming、`remote_apply`、受控复制链分区、fence-before-promote、timeline 1→2、新 generation 同步策略、新 slot、旧主 `pg_rewind`/只读 sync rejoin、端点延迟切换、双 control 摘流/激活,以及 scheduler、credential v1/v2/v3/v4、completion、cancellation 和通用事务不确定窗口的精确收敛。ADR-0126/0127 进一步完成独立 process/migration binary、三 workspace package/五 production root 的 builder/production 双 lock、实际非 root/只读 Docker smoke、双副本 QL3 Kustomize、migration/runtime authority 分离与有界私有 CA 文件绑定;本地 PostgreSQL 18 TLS 1.3 门禁已验证旧 CA、overlap、新 CA、SIGHUP 端点证书切换和逆序回滚,并接入 x64/arm64 CIADR-0128 已完成 control 43 外部+3 内部与 admin 85 外部+3 内部 component 的精确 CycloneDX、两种真实 arm64 镜像 inventory 对账、control 真实本地 amd64/arm64 OCI + 逐平台 SPDX/SLSA digest graph、双镜像原生 x64/arm64 image/OCI CI matrix、两个独立 GHCR repository 的 digest-bound Cosign/GitHub attestation 发布契约与发布后 manifest/signature/provenance/CycloneDX 四重自验证门;ADR-0129 已完成 CloudNativePG 1.30.0/PostgreSQL 18.4 digest lock、三实例同步 HA、四 DatabaseRole/Database owner、离散凭据、`-rw` endpoint、`ca.crt` 投影、Kustomize/audit 门及四节点 Kind worker-loss live contract/CI 定义;ADR-0137 至 ADR-0141 已完成 Plugin Package PostgreSQL parity、Approved Action 双方言 durable authority、immutable Package proposal、execution/start-barrier、Package 双方言原子 admission receipt、单用途 Project/Policy lock authority、跨 adapter 合同、标准 OCI resolver、exact-registry credential provider、ConfigMap CAS publisher、独立 admin image、最小权限 Job/RBAC 与真实 Kind 双 Pod resourceVersion/RBAC live gate,并由 21 项本机物理 HA 门复验 PostgreSQL authority。ADR-0230 把 Workflow frontier 与 Task Attempt admission 接入既有 production Scheduler cadenceADR-0231 又在同一 production process 中以独立 mTLS listener/Worker Pool 和内部 runtime capability port 装入 Remote Worker 全套无 Secret transport,保持 20 包、单 Scheduler timer 与显式权限 subpath | PostgreSQL 18 × x64/arm64 远端 CI 成功记录;取得一次 CloudNativePG live job 成功证据并继续完成 CA overlap rotation/rollback、Barman/WAL archive restore、Kubernetes HA control-plane/failover、基础设施 STONITH、真实 Pod/raw-wire 分区、多 Pod 与 Session replacement 竞争;stale lock/temp 修复与 pepper retire/reference/GCPostgreSQL Package admission 过期租约真库门、资源 generation consumer 和受认证管理入口;admin 真实双架构 OCI 记录、两个 image 的实际远端 GHCR manifest digest、Cosign/GitHub attestation 独立 verify 记录、漏洞/容量证明;Cluster Secret provider、Remote Worker expiry/retry lifecycle 与启动装配;管理 CLI/API/UI、审批与 rate limitschedule 指标/告警;正式停写与双数据库 backup manifest;固定物理 edge 基线和完整 2.x API 契约样本 |
| PR-0 Migration/Cluster 基线 | In progress | SQLite/PostgreSQL reviewed migration、checksum/rollback/schema ownership/readiness 与 Node 20/24 × x64/arm64 CI 基线;`runtime-core` 提供共享领域 contract`cluster-postgres` 提供 exact-pinned Pool、四角色、21 条 migration、capability v20/35 表、可写主库、受审 function/role activation check、完整 cluster repository 及显式 admin-only Plugin Package installation和 Approved Action authority`cluster-admin` 提供短生命周期发布 authority、POSIX/Kubernetes Secret adapter、标准 OCI stage verifier、exact-registry credential provider、有界恢复与 caller-driven Package Approved Action dispatcher`local-admin` 以相同 handler 组合 edge/standalone 单 SQLite authority`cluster-control` 提供 readiness-first HTTP/mTLS admission、startup recovery、数据库时钟 row-lease scheduler 和 Remote Worker 控制面。Package admission 已在两个方言的同一事务中强绑定 immutable proposal、exact executing snapshot、Project Policy fence 与数据库观察时钟,并以 receipt 收敛 COMMIT response lossdispatcher 固定 start 前 retry、start 后 durable inspectionedge/standalone/cluster 默认单批 1/4/16 且无 timer。PostgreSQL 18 physical-promotion 门禁已在本机 arm64 完成 streaming、`remote_apply`、受控复制链分区、fence-before-promote、timeline 1→2、新 generation 同步策略、新 slot、旧主 `pg_rewind`/只读 sync rejoin、端点延迟切换、双 control 摘流/激活,以及 scheduler、credential v1/v2/v3/v4、completion、cancellation 和通用事务不确定窗口的精确收敛。ADR-0126/0127 进一步完成独立 process/migration binary、三 workspace package/五 production root 的 builder/production 双 lock、实际非 root/只读 Docker smoke、双副本 QL3 Kustomize、migration/runtime authority 分离与有界私有 CA 文件绑定;本地 PostgreSQL 18 TLS 1.3 门禁已验证旧 CA、overlap、新 CA、SIGHUP 端点证书切换和逆序回滚,并接入 x64/arm64 CIADR-0128 已完成 control 43 外部+3 内部与 admin 85 外部+3 内部 component 的精确 CycloneDX、两种真实 arm64 镜像 inventory 对账、control 真实本地 amd64/arm64 OCI + 逐平台 SPDX/SLSA digest graph、双镜像原生 x64/arm64 image/OCI CI matrix、两个独立 GHCR repository 的 digest-bound Cosign/GitHub attestation 发布契约与发布后 manifest/signature/provenance/CycloneDX 四重自验证门;ADR-0129 已完成 CloudNativePG 1.30.0/PostgreSQL 18.4 digest lock、三实例同步 HA、四 DatabaseRole/Database owner、离散凭据、`-rw` endpoint、`ca.crt` 投影、Kustomize/audit 门及四节点 Kind worker-loss live contract/CI 定义;ADR-0137 至 ADR-0141 已完成 Plugin Package PostgreSQL parity、Approved Action 双方言 durable authority、immutable Package proposal、execution/start-barrier、Package 双方言原子 admission receipt、单用途 Project/Policy lock authority、跨 adapter 合同、标准 OCI resolver、exact-registry credential provider、ConfigMap CAS publisher、独立 admin image、最小权限 Job/RBAC 与真实 Kind 双 Pod resourceVersion/RBAC live gate,并由 21 项本机物理 HA 门复验 PostgreSQL authority。ADR-0230 把 Workflow frontier 与 Task Attempt admission 接入既有 production Scheduler cadenceADR-0231 又在同一 production process 中以独立 mTLS listener/Worker Pool 和内部 runtime capability port 装入 Remote Worker 全套无 Secret transport,保持 20 包、单 Scheduler timer 与显式权限 subpathADR-0471 新增 Cron/Subscription 真实 Router 的首个 2.x 核心执行 API envelope/validation 基线 `18/18` | PostgreSQL 18 × x64/arm64 远端 CI 成功记录;取得一次 CloudNativePG live job 成功证据并继续完成 CA overlap rotation/rollback、Barman/WAL archive restore、Kubernetes HA control-plane/failover、基础设施 STONITH、真实 Pod/raw-wire 分区、多 Pod 与 Session replacement 竞争;stale lock/temp 修复与 pepper retire/reference/GCPostgreSQL Package admission 过期租约真库门、资源 generation consumer 和受认证管理入口;admin 真实双架构 OCI 记录、两个 image 的实际远端 GHCR manifest digest、Cosign/GitHub attestation 独立 verify 记录、漏洞/容量证明;Cluster Secret provider、Remote Worker expiry/retry lifecycle 与启动装配;管理 CLI/API/UI、审批与 rate limitschedule 指标/告警;正式停写与双数据库 backup manifest;固定物理 edge 基线和 System/Script/Open API/鉴权错误码/Primary 双态的剩余 2.x 契约、真实 SQLite 升级及目标实例回滚演练 |
| PR-1 Run Schema | Incubating | Run/RunAttempt/RunEvent schema、nullable cancel request 与 Attempt deadline 字段及恢复索引、CancellationDispatch 状态/version/lease/backoff schema、Repository port、临时 Sequelize adapter、统一事件大小/分页上限、跨 adapter RunRepository contract suite(原子事务、回滚、Run/Attempt/RetryPolicy CAS、唯一错误、分页与取消恢复);ADR-0041 的 `pg-0003-run-retry-policy`、capability v2、driver-neutral PostgreSQL Run Repository 与真实 `pg.Pool` 上的共享 Repository/rollback/SQLSTATE contractADR-0063/0069/0071/0073/0074/0076 的独立 Node 24 local-sqlite typed schema、十二条 reviewed migration、capability v6、共享 operation authority、readiness/RunRepository/API credential repository/receipt journal/dispatch plan/encrypted Secret envelope/Project Policy/security audit/authorized mutation/stable Identity catalog、Drizzle↔真实 catalog table/column/index/CHECK/FK lockstep、base/adopted/application edge/standalone 产物门禁;ADR-0064 的 legacy baseline/plan digest、Online Backup recovery、side-by-side target migration、staged manifest、双库栅栏 activation、source 生命周期写栅栏、target stable identity 和重启语义;ADR-0065 的独立 cutover authority、外部副作用停机 evidence、append-only journal、start/restart/stop barrier 与 unknown→manual_required 收敛;ADR-0066 的 adopted storage→Run reconciliation→receipt maintenance→domain recovery→lifecycle→admission application gate、严格有界 recovery summary 与 admission-first reverse stopADR-0067 的 SQLite 事实驱动 Run 候选源、256 条硬上限、截断失败关闭和唯一 Repository authorityADR-0068 的 receipt-first Reconciler、callback token/sequence fence、exact local-process identity、Attempt/Run/双 Event 原子终态推进和最终 verifierADR-0069 的 local-process 单向包边界、pre-spawn journal、受审 POSIX launcher、immutable receipt、exact identity 和 Profile-aware cleanup lifecycleADR-0070 的独立 local-execution、spawn 前后双 transaction CAS、callback digest、exact stop 补偿与 fail-closed starting 保留;ADR-0071 的独立 local-dispatch、不可变 revision/context、Secret-first materializer、Profile Artifact admission、4/64 MiB output hard quota 和窄 application facadeADR-0073/0074 的 Project-bound SecretRef、AES-256-GCM、外置 keyring 生命周期、双 SQLite authority CAS、application preflight、强 Principal/Policy 和 envelope+audit 原子提交;ADR-0086 的本机 Owner provisioning/challenge/claim/delivery acknowledgement/credential recovery CLIADR-0377 的 Local/Cluster 同构、Profile-aware、Project-scoped Artifact range read | fresh database/pepper setup、credential rotation/GC 运维编排与 Secret/Project/Role/Approval 管理 CLI/API/UI、备份/rekey、2.x/target process controller、人工 recovery、target 写后 reconciliation 与完整 cutover/rollback 演练;retry 产品策略、Artifact retention/tombstone stack、具体本机 lifecycle 和 target executableLinux x64/arm64、PID namespace、断电与固定路由设备门禁;PostgreSQL 16/18 双连接并发与 failover integrationTask revision/context 跨方言 contract/并发压力与引用感知 retention、Keyv 数据迁移 |
| PR-2 Run 状态机 | Incubating | 纯转换表、终态/时间/错误/执行器元数据规则、Run version 与 event sequence CAS、事务性 RunCommandService、回滚测试 | 重复 Worker callback/fencing、并发数据库压力测试、Primary 执行链接入 |
| PR-3 Executor 端口 | Incubating | ADR-0003、ExecutionSpec/Context/Handle/Result、Executor port、LocalProcessExecutor、进程组取消/超时升级、流式背压、Legacy Cron spec builder、真实进程 contract tests、可复现 edge 基准入口 | 固定 edge/多架构设备基线、Legacy builder 与 makeCommand 差异审计、Primary 生产流量接入 |
@@ -0,0 +1,65 @@
# ADR-0471Legacy 核心执行 API 兼容基线
- 状态:Accepted
- 日期:2026-08-20
- 关联 RFCQL-RFC-0001 D-378、PR-0、Runtime Milestone Gate
- 关联 ADRADR-0002、ADR-0445、ADR-0454、ADR-0470
## 上下文
QingLong 3.0 已经具备 Shadow Run、受门禁的 Manual Primary、Run/Event、取消恢复和本地/集群执行协议,但这些新能力不能以破坏 2.x
Cron 与 Subscription API 为代价。现有路由仍是 Web UI、Shell、Open API 适配层和大量部署脚本的产品契约;仅验证 Runtime 内部状态机,不能证明
3.0 开关两侧仍保留相同的 HTTP method、path、请求校验、服务参数和响应 envelope。
首个兼容基线应从最靠近现有用户执行闭环的路径开始:Cron 与 Subscription 的列表、创建、更新、启停、手动运行、停止和日志。测试必须执行真实
Express Router 与 Celebrate 校验器,而不是复制一份路径清单或只搜索源码。它也不能为了测试方便启动完整 QingLong master/worker、数据库、gRPC、
Scheduler 或任何 3.0 后台 lifecycle。
## 决策
1. 增加独立的真实回环 HTTP 契约测试,直接注册生产 `back/api/cron.ts``back/api/subscription.ts` Router,并挂载在现行 `/api` 前缀。
2. 服务层使用 TypeDI 注入的确定性 spy,因此测试只裁决 Router 责任:HTTP method/path、Celebrate request contract、参数透传、2.x response envelope
与错误请求在 service dispatch 前拒绝。它不伪造数据库、进程或 Scheduler 集成证据。
3. Cron 基线覆盖 list、create、update、disable、enable、run、stop、单日志、日志列表和单实例 stop。`run|stop|enable|disable` 继续返回
`{code: 200}`,不得因为 3.0 内部拥有 Run ID 而向 2.x 成功 envelope 强塞 v3 字段。
4. Subscription 基线覆盖 list、create、update、disable、enable、run、stop 和日志;`searchValue``ids` 与 numeric ID 的现有转换语义保持不变。
5. 非法 Cron run 与 Subscription stop body 必须由真实校验器返回 HTTP 400,且 service spy 调用数保持不变,证明无副作用 dispatch。
6. 本切片只增加测试、ADR 和 RFC 状态,不修改任何生产 function/class/method,不新增依赖、package、binary、schema、migration、端口、服务、timer、
queue、cache、数据库连接或部署对象。测试结束时关闭临时回环 listener 并清理 TypeDI token。
7. 本基线不是“完整 2.x 兼容已完成”的声明。System、Script、Open API、鉴权/错误码、真实 SQLite 升级、Primary 开关双态、目标实例回滚和 UI/Shell
仍须后续独立 contract 与 rehearsal,完成前 Runtime Milestone Gate 保持未关闭。
## 被拒绝的替代方案
### 只用正则扫描路由源码
拒绝。文本存在不能证明路由注册顺序、校验器转换、TypeDI 调用和实际 JSON 序列化行为。
### 启动完整 QingLong 服务作为每次后端单测前置条件
拒绝。它会把数据库、gRPC、cluster fork、文件系统和调度器故障混入 Router contract,增加低配开发机和 CI 的成本,也无法精确定位兼容漂移。
### 让 2.x run 响应直接返回 QingLong 3.0 Run 对象
拒绝。现有客户端依赖 code-only 的异步接受语义;Run/Event 应通过 `/api/v3` 或后续显式兼容扩展发现,不能静默改变 2.x envelope。
### 将全部 2.x API 一次性冻结在一个超大测试中
拒绝。首个阶段优先覆盖执行主路径,并明确登记剩余域;后续 contract 应按 Identity/Open API、System/Script、文件与日志等稳定产品边界扩展,避免难以维护的
单体 fixture。
## 升级与回滚
- 本切片没有生产行为和持久化变更。升级只增加 CI 回归门,运行中实例不加载测试文件。
- 若契约基线本身错误,可回滚测试、ADR 与 RFC 增量;不得通过删除测试掩盖真实 2.x 兼容回归,应先由 Maintainer 明确批准兼容变更。
## 验证与证据
- 聚焦真实 Router 契约为 `18/18`,覆盖 16 个成功路径/子路径和 2 个校验前拒绝路径;每个成功路径同时校验 HTTP 状态、JSON envelope 与 service 参数。
- 完整 backend 为 `1,523 total / 1,521 pass / 2 conditional skip / 0 fail`18-package clean build 与逐包测试在允许既有 Worker TLS
回环门后单次退出 0。
- package boundary、Cluster dependency、Edge import、Cluster/Worker deployment、Console 与 Console distribution 七项审计全部
compatible/passedworkspace 仍为 18 packages、`singleSourcePackages=[]``shallowSourcePackages=[]`
- 14 档 Local artifact audit 全部 compatible;基础 Edge/Standalone 为 `2,598,669 / 2,598,747` bytes、57 loaded modules
Application+AI 为 `4,501,822 / 4,501,954`MCP 为 `7,324,601 / 7,324,709`。新增测试不进入任何运行制品。
- PostgreSQL HA 不重跑且不重新占有既有证明,因为本切片没有 schema、ACL、repository、role、Pool、连接或 failover 变化。
+1
View File
@@ -474,6 +474,7 @@
| [ADR-0468](./ADR-0468-optional-console-worker-observation.md) | 可选 Console Worker 只读观察 | Accepted |
| [ADR-0469](./ADR-0469-optional-console-package-installation-observation.md) | 可选 Console Package Installation 只读观察 | Accepted |
| [ADR-0470](./ADR-0470-session-scoped-console-capability-discovery.md) | Console 会话级能力发现与服务端操作围栏 | Accepted |
| [ADR-0471](./ADR-0471-legacy-core-api-compatibility-baseline.md) | Legacy 核心执行 API 兼容基线 | Accepted |
## 规则
@@ -0,0 +1,397 @@
require('ts-node/register/transpile-only');
require('reflect-metadata');
const assert = require('node:assert/strict');
const http = require('node:http');
const { after, before, test } = require('node:test');
const express = require('express');
const { errors } = require('celebrate');
const { Container } = require('typedi');
const CronService = require('../../back/services/cron').default;
const SubscriptionService = require('../../back/services/subscription').default;
const registerCronRoutes = require('../../back/api/cron').default;
const registerSubscriptionRoutes =
require('../../back/api/subscription').default;
const calls = [];
let origin;
let server;
let serverListening = false;
function record(domain, operation, args, result) {
calls.push({ domain, operation, args });
return Promise.resolve(result);
}
const cronService = {
crontabs: (...args) =>
record('cron', 'list', args, {
data: [{ id: 11, name: 'legacy-task', status: 0 }],
total: 1,
}),
create: (...args) =>
record('cron', 'create', args, {
id: 11,
...args[0],
status: 0,
}),
update: (...args) =>
record('cron', 'update', args, {
...args[0],
status: 0,
}),
disabled: (...args) => record('cron', 'disable', args, undefined),
enabled: (...args) => record('cron', 'enable', args, undefined),
run: (...args) => record('cron', 'run', args, undefined),
stop: (...args) => record('cron', 'stop', args, undefined),
log: (...args) =>
record('cron', 'log', args, {
content: 'legacy log\n',
status: 'running',
}),
logs: (...args) => record('cron', 'logs', args, ['one.log', 'two.log']),
stopInstance: (...args) =>
record('cron', 'stop-instance', args, {
code: 200,
message: '实例已停止',
}),
};
const subscriptionService = {
list: (...args) =>
record('subscription', 'list', args, [
{ id: 21, name: 'legacy-subscription', status: 0 },
]),
create: (...args) =>
record('subscription', 'create', args, {
id: 21,
...args[0],
status: 0,
}),
update: (...args) =>
record('subscription', 'update', args, {
...args[0],
status: 0,
}),
disabled: (...args) => record('subscription', 'disable', args, undefined),
enabled: (...args) => record('subscription', 'enable', args, undefined),
run: (...args) => record('subscription', 'run', args, undefined),
stop: (...args) => record('subscription', 'stop', args, undefined),
log: (...args) => record('subscription', 'log', args, 'subscription log\n'),
};
const logger = {
debug() {},
error() {},
info() {},
warn() {},
};
async function request(method, pathname, body) {
const response = await fetch(`${origin}${pathname}`, {
method,
headers: body === undefined ? {} : { 'content-type': 'application/json' },
body: body === undefined ? undefined : JSON.stringify(body),
});
const text = await response.text();
return {
status: response.status,
body: text.length === 0 ? undefined : JSON.parse(text),
};
}
function lastCall() {
return calls.at(-1);
}
before(async () => {
Container.set('logger', logger);
Container.set(CronService, cronService);
Container.set(SubscriptionService, subscriptionService);
const app = express();
app.set('case sensitive routing', true);
app.set('strict routing', true);
app.use(express.json());
const api = express.Router();
registerCronRoutes(api);
registerSubscriptionRoutes(api);
app.use('/api', api);
app.use(errors());
app.use((error, _request, response, _next) => {
response.status(error.status || 500).json({
code: error.status || 500,
message: error.message,
});
});
server = http.createServer(app);
await new Promise((resolve, reject) => {
server.once('error', reject);
server.listen(0, '127.0.0.1', resolve);
});
serverListening = true;
const address = server.address();
origin = `http://127.0.0.1:${address.port}`;
});
after(async () => {
Container.remove(CronService);
Container.remove(SubscriptionService);
Container.remove('logger');
if (serverListening) {
await new Promise((resolve, reject) =>
server.close((error) => (error ? reject(error) : resolve())),
);
}
});
test('preserves the 2.x Cron API envelope across the QingLong 3.0 runtime boundary', async (t) => {
await t.test(
'lists tasks with the existing query and response shape',
async () => {
const response = await request(
'GET',
'/api/crons?searchValue=legacy&page=1&size=20',
);
assert.deepEqual(response, {
status: 200,
body: {
code: 200,
data: {
data: [{ id: 11, name: 'legacy-task', status: 0 }],
total: 1,
},
},
});
assert.deepEqual(lastCall(), {
domain: 'cron',
operation: 'list',
args: [
{
searchValue: 'legacy',
page: '1',
size: '20',
},
],
});
},
);
const createBody = {
name: 'legacy-task',
command: 'echo compatible',
schedule: '0 0 * * *',
};
await t.test(
'creates and updates tasks without adding a v3-only envelope',
async () => {
const created = await request('POST', '/api/crons', createBody);
assert.equal(created.status, 200);
assert.deepEqual(created.body, {
code: 200,
data: { id: 11, ...createBody, status: 0 },
});
assert.deepEqual(lastCall(), {
domain: 'cron',
operation: 'create',
args: [createBody],
});
const updateBody = { id: 11, ...createBody, command: 'echo updated' };
const updated = await request('PUT', '/api/crons', updateBody);
assert.equal(updated.status, 200);
assert.deepEqual(updated.body, {
code: 200,
data: { ...updateBody, status: 0 },
});
assert.deepEqual(lastCall(), {
domain: 'cron',
operation: 'update',
args: [updateBody],
});
},
);
for (const operation of ['disable', 'enable', 'run', 'stop']) {
await t.test(
`${operation} keeps the code-only success response`,
async () => {
const response = await request('PUT', `/api/crons/${operation}`, [11]);
assert.deepEqual(response, { status: 200, body: { code: 200 } });
assert.deepEqual(lastCall(), {
domain: 'cron',
operation,
args: [[11]],
});
},
);
}
await t.test(
'keeps task log and instance-stop response contracts',
async () => {
const log = await request('GET', '/api/crons/11/log');
assert.deepEqual(log, {
status: 200,
body: {
code: 200,
data: 'legacy log\n',
logStatus: 'running',
},
});
assert.deepEqual(lastCall(), {
domain: 'cron',
operation: 'log',
args: [11],
});
const logs = await request('GET', '/api/crons/11/logs');
assert.deepEqual(logs, {
status: 200,
body: { code: 200, data: ['one.log', 'two.log'] },
});
assert.deepEqual(lastCall(), {
domain: 'cron',
operation: 'logs',
args: [11],
});
const stopped = await request('POST', '/api/crons/11/instances/101/stop');
assert.deepEqual(stopped, {
status: 200,
body: { code: 200, message: '实例已停止' },
});
assert.deepEqual(lastCall(), {
domain: 'cron',
operation: 'stop-instance',
args: [101],
});
},
);
await t.test(
'rejects an invalid execution request before service dispatch',
async () => {
const callCount = calls.length;
const response = await request('PUT', '/api/crons/run', ['x']);
assert.equal(response.status, 400);
assert.equal(calls.length, callCount);
},
);
});
test('preserves the 2.x Subscription API envelope across the QingLong 3.0 runtime boundary', async (t) => {
await t.test('lists subscriptions with both legacy filters', async () => {
const response = await request(
'GET',
'/api/subscriptions?searchValue=legacy&ids=%5B21%5D',
);
assert.deepEqual(response, {
status: 200,
body: {
code: 200,
data: [{ id: 21, name: 'legacy-subscription', status: 0 }],
},
});
assert.deepEqual(lastCall(), {
domain: 'subscription',
operation: 'list',
args: ['legacy', '[21]'],
});
});
const createBody = {
type: 'public-repo',
url: 'https://example.invalid/repository.git',
schedule_type: 'cron',
alias: 'legacy-subscription',
};
await t.test(
'creates and updates subscriptions with the legacy envelope',
async () => {
const created = await request('POST', '/api/subscriptions', createBody);
assert.deepEqual(created, {
status: 200,
body: {
code: 200,
data: { id: 21, ...createBody, status: 0 },
},
});
assert.deepEqual(lastCall(), {
domain: 'subscription',
operation: 'create',
args: [createBody],
});
const updateBody = {
id: 21,
type: 'public-repo',
url: 'https://example.invalid/updated.git',
alias: 'legacy-subscription',
};
const updated = await request('PUT', '/api/subscriptions', updateBody);
assert.deepEqual(updated, {
status: 200,
body: {
code: 200,
data: { ...updateBody, status: 0 },
},
});
assert.deepEqual(lastCall(), {
domain: 'subscription',
operation: 'update',
args: [updateBody],
});
},
);
for (const operation of ['disable', 'enable', 'run', 'stop']) {
await t.test(
`${operation} keeps the code-only success response`,
async () => {
const response = await request(
'PUT',
`/api/subscriptions/${operation}`,
[21],
);
assert.deepEqual(response, { status: 200, body: { code: 200 } });
assert.deepEqual(lastCall(), {
domain: 'subscription',
operation,
args: [[21]],
});
},
);
}
await t.test('keeps the subscription log response contract', async () => {
const response = await request('GET', '/api/subscriptions/21/log');
assert.deepEqual(response, {
status: 200,
body: { code: 200, data: 'subscription log\n' },
});
assert.deepEqual(lastCall(), {
domain: 'subscription',
operation: 'log',
args: [21],
});
});
await t.test(
'rejects an invalid stop request before service dispatch',
async () => {
const callCount = calls.length;
const response = await request('PUT', '/api/subscriptions/stop', [
21,
'x',
]);
assert.equal(response.status, 400);
assert.equal(calls.length, callCount);
},
);
});