mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-20 16:07:11 +08:00
test(ql3): lock legacy core api contracts
This commit is contained in:
@@ -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、单日志、日志列表和
|
||||
单实例 stop;Subscription 覆盖 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/passed;workspace 仍为 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 modules,Application+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 loss;dispatcher 固定 start 前 retry、start 后 durable inspection,edge/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 CI;ADR-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 cadence;ADR-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/GC;PostgreSQL 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 limit;schedule 指标/告警;正式停写与双数据库 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 loss;dispatcher 固定 start 前 retry、start 后 durable inspection,edge/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 CI;ADR-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 cadence;ADR-0231 又在同一 production process 中以独立 mTLS listener/Worker Pool 和内部 runtime capability port 装入 Remote Worker 全套无 Secret transport,保持 20 包、单 Scheduler timer 与显式权限 subpath;ADR-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/GC;PostgreSQL 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 limit;schedule 指标/告警;正式停写与双数据库 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 contract;ADR-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 stop;ADR-0067 的 SQLite 事实驱动 Run 候选源、256 条硬上限、截断失败关闭和唯一 Repository authority;ADR-0068 的 receipt-first Reconciler、callback token/sequence fence、exact local-process identity、Attempt/Run/双 Event 原子终态推进和最终 verifier;ADR-0069 的 local-process 单向包边界、pre-spawn journal、受审 POSIX launcher、immutable receipt、exact identity 和 Profile-aware cleanup lifecycle;ADR-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 facade;ADR-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 CLI;ADR-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 executable;Linux x64/arm64、PID namespace、断电与固定路由设备门禁;PostgreSQL 16/18 双连接并发与 failover integration;Task 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-0471:Legacy 核心执行 API 兼容基线
|
||||
|
||||
- 状态:Accepted
|
||||
- 日期:2026-08-20
|
||||
- 关联 RFC:QL-RFC-0001 D-378、PR-0、Runtime Milestone Gate
|
||||
- 关联 ADR:ADR-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/passed;workspace 仍为 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 变化。
|
||||
@@ -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);
|
||||
},
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user