@oj-module/cli 0.2.6 → 0.2.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/devkit/README.md +70 -24
- package/devkit/framework-dev.md +14 -5
- package/devkit/manual.md +28 -23
- package/devkit/module-dev-and-cdn-playground.md +197 -0
- package/devkit/sql-builder-missing-capabilities-pr.md +268 -0
- package/package.json +2 -2
- package/shell-dist/assets/runtime.js +130 -45
- package/shell-dist/index.html +2 -2
- package/shell-dist/versions.json +3 -3
- package/src/__tests__/build-backend.test.ts +64 -0
- package/src/__tests__/versions.test.ts +26 -0
- package/src/build.ts +9 -0
- package/src/bundle.ts +8 -5
- package/src/contract/__tests__/handler-scan.test.ts +29 -0
- package/src/contract/__tests__/market-client.integration.test.ts +170 -0
- package/src/contract/handler-scan.ts +35 -4
- package/src/dev.ts +10 -2
- package/src/index.ts +9 -3
- package/src/init.ts +24 -0
- package/src/lifecycle/__tests__/bundle.test.ts +3 -3
- package/src/market/__tests__/bootstrap.test.ts +204 -0
- package/src/market/__tests__/build-index.test.ts +154 -0
- package/src/market/__tests__/cdn-layout.test.ts +96 -0
- package/src/market/__tests__/failure-playbook.test.ts +33 -0
- package/src/market/__tests__/fixture.test.ts +48 -0
- package/src/market/__tests__/publish.test.ts +109 -0
- package/src/market/__tests__/report.test.ts +51 -0
- package/src/market/__tests__/setup.test.ts +114 -0
- package/src/market/__tests__/verify.test.ts +111 -0
- package/src/market/bootstrap.ts +255 -0
- package/src/market/build-index.ts +159 -0
- package/src/market/cdn-layout.ts +122 -0
- package/src/market/command.ts +139 -0
- package/src/market/failure-playbook.ts +57 -0
- package/src/market/fixture.ts +138 -0
- package/src/market/index-builder.ts +76 -0
- package/src/market/publish.ts +156 -0
- package/src/market/report.ts +67 -0
- package/src/market/scan.ts +99 -0
- package/src/market/setup.ts +144 -0
- package/src/market/trial.ts +121 -0
- package/src/market/verify.ts +298 -0
- package/src/market/walkthrough.ts +506 -0
- package/src/oj.ts +3 -1
- package/src/preview-config.ts +177 -0
- package/src/preview.ts +106 -56
- package/src/sign.ts +21 -6
- package/src/usage.ts +22 -2
- package/src/vendor.ts +4 -2
- package/src/versions.ts +28 -0
- package/templates/api/.ojm-api-exempt.json +6 -2
- package/templates/api/config.yaml +24 -2
- package/templates/api/src/_platform/README.md +31 -0
- package/templates/api/src/_platform/_lifecycle/atomic.ts +1 -1
- package/templates/api/src/_platform/_lifecycle/history.ts +10 -5
- package/templates/api/src/_platform/_lifecycle/installer.ts +4 -4
- package/templates/api/src/_platform/_lifecycle/sha384.ts +1 -1
- package/templates/api/src/_platform/_lifecycle/steps/download.ts +1 -1
- package/templates/api/src/_platform/_lifecycle/steps/extract.ts +8 -8
- package/templates/api/src/_platform/_lifecycle/steps/register.ts +57 -29
- package/templates/api/src/_platform/_lifecycle/trust.ts +1 -1
- package/templates/api/src/_platform/_lifecycle/uninstall.ts +29 -56
- package/templates/api/src/_platform/_lifecycle/verify-checks.ts +2 -2
- package/templates/api/src/_platform/_shared/service/{contacts.ts → domain/contacts.ts} +20 -13
- package/templates/api/src/_platform/_shared/service/{roles.ts → domain/roles.ts} +18 -19
- package/templates/api/src/_platform/_shared/service/{settings.ts → domain/settings.ts} +6 -6
- package/templates/api/src/_platform/_shared/service/{solution-perms.ts → domain/solution-perms.ts} +25 -22
- package/templates/api/src/_platform/_shared/service/{users.ts → domain/users.ts} +20 -26
- package/templates/api/src/_platform/_shared/service/{access.ts → infra/access.ts} +5 -8
- package/templates/api/src/_platform/_shared/service/{audit.ts → infra/audit.ts} +26 -26
- package/templates/api/src/_platform/_shared/service/infra/oplog.ts +40 -0
- package/templates/api/src/_platform/_shared/service/{query.ts → infra/query.ts} +18 -0
- package/templates/api/src/_platform/_shared/service/{rate.ts → infra/rate.ts} +16 -13
- package/templates/api/src/_platform/_shared/site-facade.ts +1 -1
- package/templates/api/src/_platform/contacts/api.ts +2 -2
- package/templates/api/src/_platform/contacts/detail/api.ts +2 -2
- package/templates/api/src/_platform/fixtures/l1-test-user.sql +13 -0
- package/templates/api/src/_platform/manifest.yaml +4 -1
- package/templates/api/src/_platform/market/api.ts +54 -16
- package/templates/api/src/_platform/migrations/0013__users_profile.sql +4 -0
- package/templates/api/src/_platform/schema.yaml +26 -0
- package/templates/api/src/_platform/solutions/api.ts +13 -12
- package/templates/api/src/auth/_shared/ldap-login.ts +11 -7
- package/templates/api/src/auth/_shared/login-throttle.ts +2 -2
- package/templates/api/src/auth/_shared/session.ts +26 -22
- package/templates/api/src/auth/api.ts +1 -1
- package/templates/api/src/demo/todos/api.ts +8 -8
- package/templates/api/src/home/line/api.ts +4 -4
- package/templates/api/src/home/pie/api.ts +4 -4
- package/templates/api/src/notification/_shared/notify.ts +97 -57
- package/templates/api/src/notification/api.ts +1 -1
- package/templates/api/src/notification/manifest.yaml +1 -0
- package/templates/api/src/notification/migrations/0004__notifications_broadcast_hidden.sql +12 -0
- package/templates/api/src/notification/schema.yaml +7 -0
- package/templates/api/src/notification/seed.sql +5 -4
- package/templates/api/src/personal-center/profile/api.ts +63 -0
- package/templates/api/src/personal-center/upload/api.ts +1 -1
- package/templates/api/src/system/login-methods/api.ts +1 -1
- package/templates/api/src/system/oplog/api.ts +3 -3
- package/templates/api/src/system/permissions/api.ts +2 -2
- package/templates/api/src/system/roles/api.ts +2 -2
- package/templates/api/src/system/roles/bindings/api.ts +2 -2
- package/templates/api/src/system/settings/api.ts +2 -2
- package/templates/api/src/system/users/api.ts +2 -2
- package/templates/api/src/system/users/forbidden/api.ts +2 -2
- package/templates/api/src/system/users/password/api.ts +2 -2
- package/templates/api/src/system/users/roles/api.ts +2 -2
- package/templates/api/src/web/user-info/api.ts +4 -5
- package/templates/api/tests/auth-session.spec.ts +1 -1
- package/templates/api/tests/auth-throttle.spec.ts +1 -1
- package/templates/api/tests/fixture.ts +8 -16
- package/templates/api/tests/guard.spec.ts +1 -1
- package/templates/api/tests/l1-builder.test.ts +91 -0
- package/templates/api/tests/lifecycle/installer.spec.ts +1 -4
- package/templates/api/tests/lifecycle/market-api.spec.ts +2 -4
- package/templates/api/tests/lifecycle/steps-fs.spec.ts +5 -8
- package/templates/api/tests/lifecycle/trust.spec.ts +1 -1
- package/templates/api/tests/lifecycle/uninstall.spec.ts +29 -89
- package/templates/api/tests/query-utils.spec.ts +85 -1
- package/templates/api/tests/rbac-audit.spec.ts +1 -1
- package/templates/api/tests/rbac-contacts.spec.ts +2 -2
- package/templates/api/tests/rbac-guard.spec.ts +1 -1
- package/templates/api/tests/rbac-roles.spec.ts +2 -2
- package/templates/api/tests/rbac-settings.spec.ts +66 -21
- package/templates/api/tests/rbac-solution-perms.spec.ts +1 -1
- package/templates/api/tests/rbac-users.spec.ts +1 -1
- package/templates/api/tsconfig.json +8 -0
- package/templates/cdn/README.md +48 -0
- package/templates/env.d.ts +2 -0
- package/templates/global.d.ts +850 -0
- package/templates/init.include.yaml +14 -2
- package/templates/logs/task-events.jsonl +0 -0
- package/templates/openapi.json +580 -23
- package/templates/package.json +2 -1
- package/templates/web/src/_platform/__client/api.schemas.ts +97 -0
- package/templates/web/src/_platform/__client/api.ts +152 -39
- package/templates/web/src/notification/entry.ts +46 -9
- package/templates/web/src/notification/locales/en-US.json +19 -0
- package/templates/web/src/notification/locales/zh-CN.json +19 -0
- package/templates/web/src/notification/pages/center/index.tsx +131 -0
- package/templates/web/src/oj-market/__tests__/CardGrid.test.tsx +19 -0
- package/templates/web/src/oj-market/__tests__/InstallDialog.test.tsx +3 -0
- package/templates/web/src/oj-market/__tests__/RollbackDialog.test.tsx +1 -0
- package/templates/web/src/oj-market/__tests__/marketClient.fake.ts +1 -0
- package/templates/web/src/oj-market/api/client.ts +5 -105
- package/templates/web/src/oj-market/api/defaultClient.ts +111 -5
- package/templates/web/src/oj-market/components/CardGrid.tsx +4 -1
- package/templates/web/src/oj-market/components/CardItem.tsx +9 -5
- package/templates/web/src/oj-market/components/DetailDrawer.tsx +7 -3
- package/templates/web/src/oj-market/components/InstallDialog.tsx +1 -1
- package/templates/web/src/oj-market/entry.ts +8 -2
- package/templates/web/src/oj-market/hooks/useMarket.ts +4 -1
- package/templates/web/src/oj-market/locales/en-US.json +2 -1
- package/templates/web/src/oj-market/locales/zh-CN.json +2 -1
- package/templates/web/src/oj-market/pages/MarketApp.tsx +16 -1
- package/templates/web/src/personal-center/__client/api.schemas.ts +9 -0
- package/templates/web/src/personal-center/__client/api.ts +25 -1
- package/templates/web/src/personal-center/entry.ts +5 -0
- package/templates/web/src/personal-center/pages/my-profile/index.tsx +25 -3
- package/templates/web/src/system/entry.ts +2 -1
- package/templates/web.config.ts +13 -10
- package/vendor/host-versions.json +3 -3
- package/templates/api/src/_platform/_shared/service/oplog.ts +0 -47
- package/templates/api/src/notifications/api.ts +0 -32
- package/templates/api/src/notifications/manifest.yaml +0 -5
- package/templates/web/src/notifications/__client/api.schemas.ts +0 -8
- package/templates/web/src/notifications/__client/api.ts +0 -80
- /package/templates/api/src/_platform/_shared/service/{guard.ts → infra/guard.ts} +0 -0
package/shell-dist/versions.json
CHANGED
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
"react-router": "7.18.2",
|
|
8
8
|
"react-router/dom": "7.18.2",
|
|
9
9
|
"@tanstack/react-query": "5.102.8",
|
|
10
|
-
"@oj-module/runtime": "0.2.
|
|
11
|
-
"@oj-module/runtime/contract": "0.2.
|
|
12
|
-
"@oj-module/runtime/contract/errors": "0.2.
|
|
10
|
+
"@oj-module/runtime": "0.2.14",
|
|
11
|
+
"@oj-module/runtime/contract": "0.2.14",
|
|
12
|
+
"@oj-module/runtime/contract/errors": "0.2.14",
|
|
13
13
|
"@plane/kit": "0.1.20",
|
|
14
14
|
"@plane/editor": "0.1.20",
|
|
15
15
|
"antd": "6.6.1",
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// buildBackend spec:宿主 versions.json 双落点(方案安装的 C2-09 前置)。
|
|
2
|
+
//
|
|
3
|
+
// 安装器按 B-11 布局读 `<api>/dist/versions.json`(与 manifests.yaml 同层)做 peerRuntime
|
|
4
|
+
// 强制比对;而站点壳的 versions.json 落在 web/dist。两者不同源时,新工程 `ojm build`
|
|
5
|
+
// 完仍会在 preflight 被 C2-09 拒装——这条用例钉住「同源双写」,防止回归。
|
|
6
|
+
import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { tmpdir } from "node:os";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import { describe, expect, it } from "vitest";
|
|
10
|
+
|
|
11
|
+
import { buildBackend } from "../build";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 假 oj 二进制:只需成功退出——本 spec 验的是 buildBackend 在 oj build 之后的收尾动作
|
|
15
|
+
* (versions.json 同源落地),oj build 自身的产物由用例预先放好模拟。
|
|
16
|
+
*/
|
|
17
|
+
function fakeOj(projectRoot: string): void {
|
|
18
|
+
mkdirSync(join(projectRoot, "bin"), { recursive: true });
|
|
19
|
+
const bin = join(projectRoot, "bin", "oj");
|
|
20
|
+
writeFileSync(bin, "#!/bin/sh\nexit 0\n");
|
|
21
|
+
chmodSync(bin, 0o755);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function newProject(): string {
|
|
25
|
+
const root = mkdtempSync(join(tmpdir(), "ojm-build-backend-"));
|
|
26
|
+
mkdirSync(join(root, "api", "src", "demo"), { recursive: true });
|
|
27
|
+
writeFileSync(join(root, "api", "src", "demo", "manifest.yaml"), "name: demo\nversion: 0.1.0\n", "utf-8");
|
|
28
|
+
writeFileSync(join(root, "api", "config.yaml"), "server:\n port: 9778\n", "utf-8");
|
|
29
|
+
// 模拟 oj build 已产出的 manifests.yaml(验证收尾动作不破坏既有产物)
|
|
30
|
+
mkdirSync(join(root, "api", "dist"), { recursive: true });
|
|
31
|
+
writeFileSync(join(root, "api", "dist", "manifests.yaml"), "demo: 0.1.0\n", "utf-8");
|
|
32
|
+
fakeOj(root);
|
|
33
|
+
return root;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
describe("given 宿主工程有 api/src When ojm build 后端", () => {
|
|
37
|
+
it("then api/dist/versions.json 与 shell-dist 同源落地(安装器 C2-09 前置)", async () => {
|
|
38
|
+
const root = newProject();
|
|
39
|
+
|
|
40
|
+
await buildBackend(root);
|
|
41
|
+
|
|
42
|
+
const hostVersions = join(root, "api", "dist", "versions.json");
|
|
43
|
+
expect(existsSync(hostVersions)).toBe(true);
|
|
44
|
+
const parsed = JSON.parse(readFileSync(hostVersions, "utf-8")) as Record<string, string>;
|
|
45
|
+
expect(typeof parsed["@oj-module/runtime"]).toBe("string");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("then 既有 oj build 产物不被覆盖(manifests.yaml 原样保留)", async () => {
|
|
49
|
+
const root = newProject();
|
|
50
|
+
|
|
51
|
+
await buildBackend(root);
|
|
52
|
+
|
|
53
|
+
expect(readFileSync(join(root, "api", "dist", "manifests.yaml"), "utf-8")).toBe("demo: 0.1.0\n");
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe("given 纯前端工程(无 api/src)When ojm build 后端", () => {
|
|
58
|
+
it("then 跳过构建、不产出 api/dist", async () => {
|
|
59
|
+
const root = mkdtempSync(join(tmpdir(), "ojm-build-backend-fe-"));
|
|
60
|
+
|
|
61
|
+
expect(await buildBackend(root)).toBe(false);
|
|
62
|
+
expect(existsSync(join(root, "api", "dist"))).toBe(false);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { hostArtifactFreshnessError } from "../versions";
|
|
4
|
+
|
|
5
|
+
describe("hostArtifactFreshnessError(源码形态宿主产物过期守卫)", () => {
|
|
6
|
+
it("版本一致 → 通过", () => {
|
|
7
|
+
expect(hostArtifactFreshnessError("0.2.11", "0.2.11")).toBeUndefined();
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it("任一侧缺失 → 跳过守卫", () => {
|
|
11
|
+
expect(hostArtifactFreshnessError(undefined, "0.2.11")).toBeUndefined();
|
|
12
|
+
expect(hostArtifactFreshnessError("0.2.11", undefined)).toBeUndefined();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("宿主产物落后于 workspace runtime → 报真因并指向 build:shell", () => {
|
|
16
|
+
const err = hostArtifactFreshnessError("0.2.10", "0.2.11");
|
|
17
|
+
expect(err).toContain("shell-dist/versions.json)过期");
|
|
18
|
+
expect(err).toContain("0.2.10");
|
|
19
|
+
expect(err).toContain("0.2.11");
|
|
20
|
+
expect(err).toContain("build:shell");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("宿主产物领先(跨仓消费旧版 runtime)→ 同样拦截", () => {
|
|
24
|
+
expect(hostArtifactFreshnessError("0.2.12", "0.2.11")).toContain("过期");
|
|
25
|
+
});
|
|
26
|
+
});
|
package/src/build.ts
CHANGED
|
@@ -511,6 +511,15 @@ export async function buildBackend(projectRoot: string): Promise<boolean> {
|
|
|
511
511
|
// SyntaxError: Invalid left-hand side in assignment(端点 500)。
|
|
512
512
|
// 0.1.32 / 0.1.50 均复现。产物体积换正确性,等上游修好再撤这个旗标。
|
|
513
513
|
execFileSync(oj, ["build", ...configArgs, "-d", apiSrc, "-o", apiDist, "--no-minify"], { stdio: "inherit" });
|
|
514
|
+
// 宿主 versions.json 双落点:站点壳从 shell-dist 拷进 web/dist(mergeShellSite),
|
|
515
|
+
// 而方案安装器读的是 `<api>/dist/versions.json`(B-11:与 manifests.yaml 同层,用于
|
|
516
|
+
// peerRuntime 强制比对)。两处必须同源,否则新工程 `ojm build` 完仍装不上方案——
|
|
517
|
+
// preflight 直接 C2-09 fail-closed「无法读取宿主 runtime 版本」。
|
|
518
|
+
const versionsSrc = path.join(resolveShellDist(), "versions.json");
|
|
519
|
+
if (fs.existsSync(versionsSrc)) {
|
|
520
|
+
fs.mkdirSync(apiDist, { recursive: true });
|
|
521
|
+
fs.copyFileSync(versionsSrc, path.join(apiDist, "versions.json"));
|
|
522
|
+
}
|
|
514
523
|
return true;
|
|
515
524
|
}
|
|
516
525
|
|
package/src/bundle.ts
CHANGED
|
@@ -116,8 +116,14 @@ export async function buildBundle(projectRoot: string, opts: { out?: string } =
|
|
|
116
116
|
}
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
-
/**
|
|
120
|
-
|
|
119
|
+
/**
|
|
120
|
+
* C2-31 分层核对:模块级 manifest 仅 oj 原生键,且 name/version 与 bundle 级逐一相等
|
|
121
|
+
* (oj 启动强校验同源)。
|
|
122
|
+
*
|
|
123
|
+
* 变更(2026-10-21):`desc === summary` 的等值要求已移除(与安装侧 extract.ts 同步)——
|
|
124
|
+
* 那是构建期映射的产物而不是安全边界,保留它会让 desc 与 summary 一字之差的包直接装不上。
|
|
125
|
+
*/
|
|
126
|
+
function checkModuleManifest(built: string, name: string, version: string, _summary: string): string[] {
|
|
121
127
|
const p = path.join(built, "manifest.yaml");
|
|
122
128
|
let doc: unknown;
|
|
123
129
|
try {
|
|
@@ -137,9 +143,6 @@ function checkModuleManifest(built: string, name: string, version: string, summa
|
|
|
137
143
|
if (mm.version !== version) {
|
|
138
144
|
out.push(`模块级 version「${String(mm.version)}」必须 === bundle version「${version}」`);
|
|
139
145
|
}
|
|
140
|
-
if (mm.desc !== summary) {
|
|
141
|
-
out.push(`模块级 desc「${String(mm.desc)}」必须 === bundle summary「${summary}」(安装器写 desc 即取 summary)`);
|
|
142
|
-
}
|
|
143
146
|
return out;
|
|
144
147
|
}
|
|
145
148
|
|
|
@@ -51,6 +51,35 @@ export default createMarketApi({} as Env);
|
|
|
51
51
|
]);
|
|
52
52
|
});
|
|
53
53
|
|
|
54
|
+
it("标识符默认导出指回工厂调用:const x = createXxx(env); export default x(market/api.ts 真形态)", () => {
|
|
55
|
+
// 真实模板形态:工厂调用赋给 const,再 export default 该标识符。
|
|
56
|
+
// 此前 scanHandlerFile 只认「default 导出 = 调用表达式」,认不出
|
|
57
|
+
// 「default 导出 = 标识符 → 指向一次工厂调用」,导致 market 整族 route 漏登。
|
|
58
|
+
const rows = scan("api.ts", `
|
|
59
|
+
import type { Env } from "./types";
|
|
60
|
+
export function createMarketApi(env: Env) {
|
|
61
|
+
return {
|
|
62
|
+
"install": {
|
|
63
|
+
async post() { return {}; },
|
|
64
|
+
},
|
|
65
|
+
"verify": {
|
|
66
|
+
async post() { return {}; },
|
|
67
|
+
last: {
|
|
68
|
+
async get() { return {}; },
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
const marketApi = createMarketApi({} as Env);
|
|
74
|
+
export default marketApi;
|
|
75
|
+
`);
|
|
76
|
+
expect(rows.map(r => `${r.method} ${r.full}`).sort()).toEqual([
|
|
77
|
+
"GET _platform/market/verify/last",
|
|
78
|
+
"POST _platform/market/install",
|
|
79
|
+
"POST _platform/market/verify",
|
|
80
|
+
]);
|
|
81
|
+
});
|
|
82
|
+
|
|
54
83
|
it("无 return 对象 / 未知 callee / 点分 callee 不可解析 → 零行(不炸不猜)", () => {
|
|
55
84
|
expect(scan("a.ts", "export default createMissingApi({});")).toEqual([]);
|
|
56
85
|
expect(scan("b.ts", `
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
// ScopedRequestLike 真源在 runtime contract(生成物 __client/api.ts 也从这里 import,
|
|
2
|
+
// 自身不再 re-export——旧写法 type import 自 __client 会在再生后 TS2305)。
|
|
3
|
+
import type { ScopedRequestLike } from "@oj-module/runtime/contract/errors";
|
|
4
|
+
|
|
5
|
+
// 集成回归:真实 httpMarketClient(统一后委托 ojm 生成 client)的委托行为。
|
|
6
|
+
// 经 bindRequest 注入 mock request,验证每条 MarketClient 方法 → 正确 URL/body/query + 返回形状;
|
|
7
|
+
// 并验证 index 的 503「未配置市场地址」→ [] 空态(PRD §5.3)。
|
|
8
|
+
//
|
|
9
|
+
// 不加载 @oj-module/runtime 重依赖(antd / pro-components 在 node 环境无法 import),仅桩 request、
|
|
10
|
+
// 透传真实 zod(供生成 client 的 DEV 响应校验走真实路径)。ContractApiError 由轻量子路径提供。
|
|
11
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
12
|
+
import { bindRequest } from "../../../templates/web/src/_platform/__client/api";
|
|
13
|
+
import { httpMarketClient } from "../../../templates/web/src/oj-market/api/defaultClient";
|
|
14
|
+
|
|
15
|
+
// index() 直连 request(不走生成 client),故把桩 request 做成可变实现,测试内统一指向同一 mock。
|
|
16
|
+
const { requestImpl } = vi.hoisted(() => ({
|
|
17
|
+
requestImpl: {
|
|
18
|
+
get: () => { throw new Error("placeholder"); },
|
|
19
|
+
post: () => { throw new Error("placeholder"); },
|
|
20
|
+
put: () => { throw new Error("placeholder"); },
|
|
21
|
+
delete: () => { throw new Error("placeholder"); },
|
|
22
|
+
patch: () => { throw new Error("placeholder"); },
|
|
23
|
+
head: () => { throw new Error("placeholder"); },
|
|
24
|
+
},
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
vi.mock("@oj-module/runtime", async () => {
|
|
28
|
+
const { z } = await import("zod");
|
|
29
|
+
return {
|
|
30
|
+
z,
|
|
31
|
+
// defaultClient 顶层 bindRequest(request) 用到;测试内立即被 mock 覆盖,不会真发请求。
|
|
32
|
+
request: requestImpl,
|
|
33
|
+
};
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
interface Call {
|
|
37
|
+
method: string
|
|
38
|
+
url: string
|
|
39
|
+
opts?: { searchParams?: Record<string, string | number | boolean>, json?: unknown }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function mockRequest(responder: (c: Call) => { status?: number, data?: unknown }) {
|
|
43
|
+
const calls: Call[] = [];
|
|
44
|
+
const make = (method: string) => (url: string, opts?: Call["opts"]) => {
|
|
45
|
+
calls.push({ method, url, opts });
|
|
46
|
+
const r = responder({ method, url, opts });
|
|
47
|
+
if (r.status && r.status >= 400) {
|
|
48
|
+
// ky 形态:HTTPError 携带 response.status(httpMarketClient.isUnconfigured 依赖它)
|
|
49
|
+
throw Object.assign(new Error(`HTTP ${r.status}`), { response: { status: r.status } });
|
|
50
|
+
}
|
|
51
|
+
return { json: () => Promise.resolve({ code: 0, data: r.data ?? null }) };
|
|
52
|
+
};
|
|
53
|
+
const req = {
|
|
54
|
+
get: make("get"),
|
|
55
|
+
post: make("post"),
|
|
56
|
+
put: make("put"),
|
|
57
|
+
delete: make("delete"),
|
|
58
|
+
patch: make("patch"),
|
|
59
|
+
head: make("head"),
|
|
60
|
+
} as unknown as ScopedRequestLike;
|
|
61
|
+
return { calls, req };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// 让 index() 直连的 request 与生成 client 的 bindRequest 指向同一 mock,行为一致。
|
|
65
|
+
// 桥接:占位方法零参签名 ↔ ScopedRequestLike 真实签名,经 any 双向打通(测试桩,无运行时差异)。
|
|
66
|
+
function useMock(m: ReturnType<typeof mockRequest>) {
|
|
67
|
+
bindRequest(m.req);
|
|
68
|
+
const req = m.req as unknown as Record<keyof typeof requestImpl, () => never>;
|
|
69
|
+
for (const k of Object.keys(requestImpl) as Array<keyof typeof requestImpl>)
|
|
70
|
+
requestImpl[k] = req[k];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
describe("httpMarketClient · 统一委托生成 client 集成回归", () => {
|
|
74
|
+
let mock: ReturnType<typeof mockRequest>;
|
|
75
|
+
|
|
76
|
+
beforeEach(() => {
|
|
77
|
+
mock = mockRequest(() => ({ data: {} }));
|
|
78
|
+
useMock(mock);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("meta() → GET _platform/market/meta,返回 { apiVersion }", async () => {
|
|
82
|
+
mock = mockRequest(() => ({ data: { apiVersion: "1.2.3" } }));
|
|
83
|
+
useMock(mock);
|
|
84
|
+
const r = await httpMarketClient.meta();
|
|
85
|
+
expect(mock.calls).toEqual([{ method: "get", url: "_platform/market/meta", opts: undefined }]);
|
|
86
|
+
expect(r).toEqual({ apiVersion: "1.2.3" });
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("index() → GET _platform/market/index,返回 IndexRow[]", async () => {
|
|
90
|
+
const rows = [{ name: "a", latest: "1.0", installed: "1.0", enabled: true, upgradable: false }];
|
|
91
|
+
mock = mockRequest(() => ({ data: rows }));
|
|
92
|
+
useMock(mock);
|
|
93
|
+
const r = await httpMarketClient.index();
|
|
94
|
+
expect(mock.calls[0]).toMatchObject({ method: "get", url: "_platform/market/index" });
|
|
95
|
+
expect(r).toEqual(rows);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("index() 遇 503(未配置市场地址)→ [] 空态,不抛(PRD §5.3)", async () => {
|
|
99
|
+
mock = mockRequest(c => (c.url === "_platform/market/index" ? { status: 503 } : { data: {} }));
|
|
100
|
+
useMock(mock);
|
|
101
|
+
const r = await httpMarketClient.index();
|
|
102
|
+
expect(r).toEqual([]);
|
|
103
|
+
// 503 是配置态而非故障:silentError 抑制全局 toast + retry:0 不重试(原
|
|
104
|
+
// market-client-unconfigured 用例的断言并入此处——工厂形态退役后唯一活口)
|
|
105
|
+
expect(mock.calls).toHaveLength(1);
|
|
106
|
+
expect(mock.calls[0].opts).toMatchObject({ silentError: true, retry: 0 });
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("install(input) → POST _platform/market/install,body 含 name/version/source", async () => {
|
|
110
|
+
mock = mockRequest(() => ({ data: { task_id: "t-1" } }));
|
|
111
|
+
useMock(mock);
|
|
112
|
+
const r = await httpMarketClient.install({ name: "alpha", version: "2.0", source: "file" });
|
|
113
|
+
expect(mock.calls[0]).toMatchObject({ method: "post", url: "_platform/market/install" });
|
|
114
|
+
expect(mock.calls[0].opts?.json).toEqual({ name: "alpha", version: "2.0", source: "file" });
|
|
115
|
+
expect(r).toEqual({ task_id: "t-1" });
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("install(input) 缺省 version/source → null / cdn 兜底", async () => {
|
|
119
|
+
mock = mockRequest(() => ({ data: { task_id: "t-2" } }));
|
|
120
|
+
useMock(mock);
|
|
121
|
+
await httpMarketClient.install({ name: "beta" });
|
|
122
|
+
expect(mock.calls[0].opts?.json).toEqual({ name: "beta", version: null, source: "cdn" });
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("task(id) → GET _platform/market/task? 带 id", async () => {
|
|
126
|
+
const rec = { id: "t-1", name: "a", version: "1.0", status: "done", stage: "done", progress: 100, message: "ok", stages: [], startedAt: 1 };
|
|
127
|
+
mock = mockRequest(() => ({ data: rec }));
|
|
128
|
+
useMock(mock);
|
|
129
|
+
const r = await httpMarketClient.task("t-1");
|
|
130
|
+
expect(mock.calls[0]).toMatchObject({ method: "get", url: "_platform/market/task" });
|
|
131
|
+
expect(mock.calls[0].opts?.searchParams).toEqual({ id: "t-1" });
|
|
132
|
+
expect(r).toEqual(rec);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("enable(input) → POST _platform/market/enable,body {name,enabled}", async () => {
|
|
136
|
+
mock = mockRequest(() => ({ data: { ok: true, message: "done" } }));
|
|
137
|
+
useMock(mock);
|
|
138
|
+
const r = await httpMarketClient.enable({ name: "a", enabled: false });
|
|
139
|
+
expect(mock.calls[0]).toMatchObject({ method: "post", url: "_platform/market/enable" });
|
|
140
|
+
expect(mock.calls[0].opts?.json).toEqual({ name: "a", enabled: false });
|
|
141
|
+
expect(r).toEqual({ ok: true, message: "done" });
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("history() → GET _platform/market/history,返回 MarketHistory[]", async () => {
|
|
145
|
+
const hist = [{ id: 1, solution: "a", version: "1.0", action: "install", status: "ok", operator: null, message: null, created_at: 1 }];
|
|
146
|
+
mock = mockRequest(() => ({ data: hist }));
|
|
147
|
+
useMock(mock);
|
|
148
|
+
const r = await httpMarketClient.history();
|
|
149
|
+
expect(mock.calls[0]).toMatchObject({ method: "get", url: "_platform/market/history" });
|
|
150
|
+
expect(r).toEqual(hist);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("uninstallImpact(name) → GET _platform/market/uninstall-impact? 带 name", async () => {
|
|
154
|
+
mock = mockRequest(() => ({ data: { name: "a", affectedRoles: 2, affectedUsers: 5 } }));
|
|
155
|
+
useMock(mock);
|
|
156
|
+
const r = await httpMarketClient.uninstallImpact("a");
|
|
157
|
+
expect(mock.calls[0]).toMatchObject({ method: "get", url: "_platform/market/uninstall-impact" });
|
|
158
|
+
expect(mock.calls[0].opts?.searchParams).toEqual({ name: "a" });
|
|
159
|
+
expect(r).toEqual({ name: "a", affectedRoles: 2, affectedUsers: 5 });
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("uninstall(name) → POST _platform/market/uninstall,body {name}", async () => {
|
|
163
|
+
mock = mockRequest(() => ({ data: { ok: true, message: "removed" } }));
|
|
164
|
+
useMock(mock);
|
|
165
|
+
const r = await httpMarketClient.uninstall("a");
|
|
166
|
+
expect(mock.calls[0]).toMatchObject({ method: "post", url: "_platform/market/uninstall" });
|
|
167
|
+
expect(mock.calls[0].opts?.json).toEqual({ name: "a" });
|
|
168
|
+
expect(r).toEqual({ ok: true, message: "removed" });
|
|
169
|
+
});
|
|
170
|
+
});
|
|
@@ -94,6 +94,20 @@ function collectObjectDecls(sf: ts.SourceFile): Map<string, ts.ObjectLiteralExpr
|
|
|
94
94
|
return decls;
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
/** 顶层 `const x = createXxx(env)` 调用声明表:工厂形态经标识符引用(`export default x`)时的解析依据 */
|
|
98
|
+
function collectCallDecls(sf: ts.SourceFile): Map<string, ts.CallExpression> {
|
|
99
|
+
const decls = new Map<string, ts.CallExpression>();
|
|
100
|
+
for (const stmt of sf.statements) {
|
|
101
|
+
if (!ts.isVariableStatement(stmt))
|
|
102
|
+
continue;
|
|
103
|
+
for (const decl of stmt.declarationList.declarations) {
|
|
104
|
+
if (ts.isIdentifier(decl.name) && decl.initializer && ts.isCallExpression(decl.initializer))
|
|
105
|
+
decls.set(decl.name.text, decl.initializer);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return decls;
|
|
109
|
+
}
|
|
110
|
+
|
|
97
111
|
/** verb 键 → 实现函数名(供 `.route` 语句按函数名查找;内联写法回落键名) */
|
|
98
112
|
function implNameOf(prop: ts.ObjectLiteralElementLike): string | undefined {
|
|
99
113
|
if (ts.isShorthandPropertyAssignment(prop))
|
|
@@ -141,9 +155,16 @@ function defaultExportObject(sf: ts.SourceFile, decls: Map<string, ts.ObjectLite
|
|
|
141
155
|
* 是调用表达式,此前扫描器不认,route-unregistered 守卫对 market/solutions 全族失效):
|
|
142
156
|
* 解析同文件同名函数声明体内 return 的对象字面量(运行时 oj 内省执行工厂拿到该对象,静态
|
|
143
157
|
* 扫描镜像此语义)。支持多个 return;对象内标识符引用仍解析文件级顶层 const。
|
|
158
|
+
* 补全(P2-14 形态遗漏):`export default createXxxApi(marketEnv())` 中 `call.expression`
|
|
159
|
+
* 是**带实参的调用表达式**,函数名落在 `call.expression.expression`(createXxxApi);
|
|
160
|
+
* 旧逻辑直接 dottedNameOf(call.expression) 对 CallExpression 返回 undefined → 扫成 0 路由,
|
|
161
|
+
* 致使 ojm api --check 把 market 全族端点判为「未实现 / stub 缺失」。此处穿透一层取被调函数名。
|
|
144
162
|
*/
|
|
145
163
|
function factoryReturnObjects(sf: ts.SourceFile, call: ts.CallExpression): ts.ObjectLiteralExpression[] {
|
|
146
|
-
|
|
164
|
+
// `export default createXxxApi(marketEnv())`:`call.expression` 为带实参的调用表达式,
|
|
165
|
+
// 函数名在其 `.expression`;`export default obj.createXxxApi(marketEnv())` 同样穿透。
|
|
166
|
+
const target = ts.isCallExpression(call.expression) ? call.expression.expression : call.expression;
|
|
167
|
+
const callee = dottedNameOf(target);
|
|
147
168
|
if (callee === undefined)
|
|
148
169
|
return [];
|
|
149
170
|
for (const stmt of sf.statements) {
|
|
@@ -189,6 +210,7 @@ export function scanHandlerFile(filePath: string, apiSrcDir: string): HandlerRow
|
|
|
189
210
|
const sf = ts.createSourceFile(filePath, text, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
|
190
211
|
const routes = collectRouteDecls(sf);
|
|
191
212
|
const decls = collectObjectDecls(sf);
|
|
213
|
+
const callDecls = collectCallDecls(sf);
|
|
192
214
|
const relDir = relative(apiSrcDir, dirname(filePath)).split(sep).join("/");
|
|
193
215
|
const rows: HandlerRow[] = [];
|
|
194
216
|
const seen = new Set<ts.Node>();
|
|
@@ -251,10 +273,19 @@ export function scanHandlerFile(filePath: string, apiSrcDir: string): HandlerRow
|
|
|
251
273
|
walk(root, "", filePrefix, 1);
|
|
252
274
|
}
|
|
253
275
|
else {
|
|
254
|
-
// P2-14
|
|
276
|
+
// 工厂形态(P2-14 + 补全):export default 后接
|
|
277
|
+
// - CallExpression 直调:createXxx(env) / createXxx(marketEnv())
|
|
278
|
+
// - Identifier 引用:const api = createXxx(env); export default api
|
|
279
|
+
// 组前缀从目录镜像起算(同内联字面量形态)。
|
|
255
280
|
for (const stmt of sf.statements) {
|
|
256
|
-
if (ts.isExportAssignment(stmt)
|
|
257
|
-
|
|
281
|
+
if (!ts.isExportAssignment(stmt) || stmt.isExportEquals)
|
|
282
|
+
continue;
|
|
283
|
+
const expr = stmt.expression;
|
|
284
|
+
const callExpr = ts.isCallExpression(expr)
|
|
285
|
+
? expr
|
|
286
|
+
: (ts.isIdentifier(expr) ? callDecls.get(expr.text) : undefined);
|
|
287
|
+
if (callExpr) {
|
|
288
|
+
for (const obj of factoryReturnObjects(sf, callExpr))
|
|
258
289
|
walk(obj, "", "", 1);
|
|
259
290
|
}
|
|
260
291
|
}
|
package/src/dev.ts
CHANGED
|
@@ -183,11 +183,19 @@ export async function devServer(projectRoot: string, opts: DevOptions = {}): Pro
|
|
|
183
183
|
|
|
184
184
|
// 终端 Ctrl+C / `kill`(SIGINT / SIGTERM)终止 dev 进程时,后端 oj 必须随之消亡:
|
|
185
185
|
// 起一个统一优雅停机,先停 oj 再退出;并打印后端终止信息。
|
|
186
|
+
// 终端 Ctrl-C 是进程组信号:外层 bin 转发一份 + 组送达一份,内层会收到
|
|
187
|
+
// 两个 SIGINT——once 首次触发即摘除,第二个走默认动作杀死本进程,
|
|
188
|
+
// shutdownDev 跑不完(与 preview 同根因,2026-10-19 冒烟实测)。on +
|
|
189
|
+
// 幂等守卫吞掉重复信号。
|
|
190
|
+
let shuttingDown = false;
|
|
186
191
|
const shutdown = () => {
|
|
192
|
+
if (shuttingDown)
|
|
193
|
+
return;
|
|
194
|
+
shuttingDown = true;
|
|
187
195
|
void shutdownDev(server, oj, hub);
|
|
188
196
|
};
|
|
189
|
-
process.
|
|
190
|
-
process.
|
|
197
|
+
process.on("SIGINT", shutdown);
|
|
198
|
+
process.on("SIGTERM", shutdown);
|
|
191
199
|
|
|
192
200
|
// 兜底:dev 进程以任何非优雅路径退出(未走 SIGINT/SIGTERM,如未捕获异常)时,
|
|
193
201
|
// 同步强杀 oj,确保它不孤儿化继续占端口(shutdownDev 已停则 kill 为 no-op)。
|
package/src/index.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { buildBundle } from "./bundle";
|
|
|
7
7
|
import { devServer } from "./dev";
|
|
8
8
|
import { mergeManifests, printInfo } from "./info";
|
|
9
9
|
import { initProject } from "./init";
|
|
10
|
-
import {
|
|
10
|
+
import { runPreview } from "./preview";
|
|
11
11
|
import { usageText } from "./usage";
|
|
12
12
|
import { vendorCommand } from "./vendor";
|
|
13
13
|
import { readCliVersion } from "./versions";
|
|
@@ -51,8 +51,8 @@ async function main() {
|
|
|
51
51
|
}
|
|
52
52
|
case "preview": {
|
|
53
53
|
const previewPortArg = Number(process.argv[3]);
|
|
54
|
-
const previewPort = Number.
|
|
55
|
-
await
|
|
54
|
+
const previewPort = Number.isInteger(previewPortArg) && previewPortArg > 0 ? previewPortArg : undefined;
|
|
55
|
+
await runPreview(projectRoot, { port: previewPort });
|
|
56
56
|
break;
|
|
57
57
|
}
|
|
58
58
|
case "dev": {
|
|
@@ -161,6 +161,12 @@ async function main() {
|
|
|
161
161
|
publishCommand(positionals[0] ?? "", { id, key, check });
|
|
162
162
|
break;
|
|
163
163
|
}
|
|
164
|
+
case "market": {
|
|
165
|
+
// 本地模拟 CDN 与市场链路(setup / publish / build-index)
|
|
166
|
+
const { runMarket } = await import("./market/command");
|
|
167
|
+
await runMarket(process.argv.slice(3));
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
164
170
|
default: {
|
|
165
171
|
if (command && OJ_PASSTHROUGH.has(command)) {
|
|
166
172
|
const { runOjSubcommand } = await import("./oj-cmd");
|
package/src/init.ts
CHANGED
|
@@ -98,6 +98,13 @@ export async function initProject(destDir: string, opts: InitOptions = {}): Prom
|
|
|
98
98
|
fs.mkdirSync(skillDir, { recursive: true });
|
|
99
99
|
for (const name of ["SKILL.md", "api-manual.md"])
|
|
100
100
|
copyIfAbsent(path.join(devkit, name), path.join(skillDir, name), report);
|
|
101
|
+
// v0.1.82 起 devkit 新增两份专题指南(api-manual/SKILL 均引用它们):
|
|
102
|
+
// SQL 构造器能力矩阵/方言差异、oj test L1 测试指南。它们是**增量文档**、
|
|
103
|
+
// 不影响 ojm 运行,故「有则拷、无则记跳过」——bin/devkit 来自用户本地
|
|
104
|
+
// oj 版本(MIN_OJ_VERSION 只钉到 0.1.73),旧版 devkit 没有这两个文件,
|
|
105
|
+
// 为两份文档把整个 init 打挂不值当(用户升 oj 后重跑 init 即补上)。
|
|
106
|
+
for (const name of ["sql-builder-guide.md", "oj-test-guide.md"])
|
|
107
|
+
copyIfPresent(path.join(devkit, name), path.join(skillDir, name), report);
|
|
101
108
|
copyIfAbsent(path.join(devkit, "global.d.ts"), path.join(destDir, "global.d.ts"), report);
|
|
102
109
|
}
|
|
103
110
|
else {
|
|
@@ -367,6 +374,21 @@ function copyIfAbsent(src: string, dest: string, report: InitReport): void {
|
|
|
367
374
|
report.created.push(path.relative(process.cwd(), dest) || path.basename(dest));
|
|
368
375
|
}
|
|
369
376
|
|
|
377
|
+
/**
|
|
378
|
+
* 同 copyIfAbsent,但**源缺失**时记一条跳过而非抛错。
|
|
379
|
+
*
|
|
380
|
+
* 用于从 bin/devkit 拷第三方版本化内容(两份专题指南):devkit 随用户本地 oj
|
|
381
|
+
* 版本而变,旧版没有新文件属正常版本 skew,不是安装包损坏——缺了只少份文档,
|
|
382
|
+
* 不该让 init 失败。源在而目标已存在仍走 copyIfAbsent 的幂等跳过。
|
|
383
|
+
*/
|
|
384
|
+
function copyIfPresent(src: string, dest: string, report: InitReport): void {
|
|
385
|
+
if (!fs.existsSync(src)) {
|
|
386
|
+
report.skipped.push(`${path.basename(src)}(bin/devkit 无此文件,跳过)`);
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
copyIfAbsent(src, dest, report);
|
|
390
|
+
}
|
|
391
|
+
|
|
370
392
|
/**
|
|
371
393
|
* 新工程 .gitignore:不存在则生成;已存在则幂等跳过(用户规则永不覆盖)。
|
|
372
394
|
*
|
|
@@ -378,6 +400,8 @@ const SCAFFOLD_GITIGNORE = `# 由 ojm init 生成。
|
|
|
378
400
|
node_modules/
|
|
379
401
|
bin/
|
|
380
402
|
.ojm-tmp-*
|
|
403
|
+
# ojm preview 的派生 config(临时产物,进程退出即删;见 preview-config.ts)
|
|
404
|
+
api/.ojm-preview.yaml
|
|
381
405
|
# 本地 sqlite 与运行日志
|
|
382
406
|
api/db.sqlite
|
|
383
407
|
api/db.sqlite-shm
|
|
@@ -33,7 +33,7 @@ afterEach(() => {
|
|
|
33
33
|
|
|
34
34
|
/** 预置「已构建」产物:oj build(api/dist/<name>-<version>/)与站点构建(web/dist/modules/) */
|
|
35
35
|
function makePrebuiltArtifacts(): void {
|
|
36
|
-
// oj build 产物(模块级 manifest 仅 oj 原生七键;name/version
|
|
36
|
+
// oj build 产物(模块级 manifest 仅 oj 原生七键;name/version 与 bundle 级逐一相等,C2-31)
|
|
37
37
|
const apiDist = join(project, "api/dist/demo-solution-0.1.0");
|
|
38
38
|
mkdirSync(join(apiDist, "ping"), { recursive: true });
|
|
39
39
|
mkdirSync(join(apiDist, "migrations"), { recursive: true });
|
|
@@ -101,9 +101,9 @@ describe("buildBundle · 校验先于打包(fail-fast)", () => {
|
|
|
101
101
|
await expect(pack()).rejects.toThrow(/manifest\.yaml/);
|
|
102
102
|
});
|
|
103
103
|
|
|
104
|
-
it("c2-31 分层核对:模块级 desc ≠ bundle summary →
|
|
104
|
+
it("c2-31 分层核对:模块级 desc ≠ bundle summary → 放行(等值要求已移除)", async () => {
|
|
105
105
|
writeFileSync(join(project, "api/dist/demo-solution-0.1.0/manifest.yaml"), "name: demo-solution\ndesc: 不一致描述\nversion: 0.1.0\n");
|
|
106
|
-
await expect(pack()).
|
|
106
|
+
await expect(pack()).resolves.toMatchObject({ name: "demo-solution" });
|
|
107
107
|
});
|
|
108
108
|
|
|
109
109
|
it("c2-31 分层核对:api/dist 缺 <name>-<version>/ → 可读报错指明期望产物路径", async () => {
|