@oj-module/cli 0.2.2 → 0.2.5

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.
Files changed (231) hide show
  1. package/devkit/README.md +14 -3
  2. package/devkit/SKILL.md +16 -4
  3. package/devkit/framework-dev.md +8 -6
  4. package/devkit/manual.md +243 -23
  5. package/package.json +2 -2
  6. package/shell-dist/assets/runtime.js +143 -8
  7. package/shell-dist/index.html +2 -2
  8. package/shell-dist/versions.json +3 -3
  9. package/src/args.ts +67 -0
  10. package/src/build.ts +11 -3
  11. package/src/bundle.ts +197 -0
  12. package/src/contract/__tests__/handler-scan.test.ts +82 -0
  13. package/src/contract/check.ts +2 -1
  14. package/src/contract/handler-scan.ts +32 -0
  15. package/src/deterministic-tar.ts +120 -0
  16. package/src/dev.ts +4 -2
  17. package/src/enable.ts +28 -0
  18. package/src/index.ts +63 -2
  19. package/src/info.ts +3 -2
  20. package/src/init.ts +147 -14
  21. package/src/install.ts +268 -0
  22. package/src/keygen.ts +88 -0
  23. package/src/layout.ts +31 -0
  24. package/src/lifecycle/__tests__/authoring.test.ts +109 -0
  25. package/src/lifecycle/__tests__/bundle.test.ts +150 -0
  26. package/src/lifecycle/__tests__/deterministic-tar.test.ts +145 -0
  27. package/src/lifecycle/__tests__/fixtures/README.md +59 -0
  28. package/src/lifecycle/__tests__/fixtures/fixture-invalid/EXPECTED-VIOLATIONS.md +27 -0
  29. package/src/lifecycle/__tests__/fixtures/fixture-invalid/api/src/Demo/manifest.yaml +6 -0
  30. package/src/lifecycle/__tests__/fixtures/fixture-invalid/manifest.yaml +40 -0
  31. package/src/lifecycle/__tests__/fixtures/fixture-invalid/web/src/Demo/entry.ts +14 -0
  32. package/src/lifecycle/__tests__/fixtures/fixture-invalid/web/src/Demo/locales/en-US.json +12 -0
  33. package/src/lifecycle/__tests__/fixtures/fixture-invalid/web/src/Demo/locales/zh-CN.json +12 -0
  34. package/src/lifecycle/__tests__/fixtures/fixture-valid/api/src/demo-solution/manifest.yaml +9 -0
  35. package/src/lifecycle/__tests__/fixtures/fixture-valid/api/src/demo-solution/migrations/0001__init.sql +6 -0
  36. package/src/lifecycle/__tests__/fixtures/fixture-valid/api/src/demo-solution/ping/api.ts +9 -0
  37. package/src/lifecycle/__tests__/fixtures/fixture-valid/api/src/demo-solution/schema.yaml +9 -0
  38. package/src/lifecycle/__tests__/fixtures/fixture-valid/manifest.yaml +96 -0
  39. package/src/lifecycle/__tests__/fixtures/fixture-valid/web/src/demo-solution/entry.ts +23 -0
  40. package/src/lifecycle/__tests__/fixtures/fixture-valid/web/src/demo-solution/locales/en-US.json +29 -0
  41. package/src/lifecycle/__tests__/fixtures/fixture-valid/web/src/demo-solution/locales/zh-CN.json +29 -0
  42. package/src/lifecycle/__tests__/fixtures/sig/demo-solution-0.2.0.tgz +0 -0
  43. package/src/lifecycle/__tests__/fixtures/sig/demo-solution-0.2.0.tgz.bad.sig +7 -0
  44. package/src/lifecycle/__tests__/fixtures/sig/demo-solution-0.2.0.tgz.sig +7 -0
  45. package/src/lifecycle/__tests__/fixtures/sig/demo-solution-0.2.0.tgz.unknown.sig +7 -0
  46. package/src/lifecycle/__tests__/fixtures/sig/gen-signed-fixtures.d.mts +21 -0
  47. package/src/lifecycle/__tests__/fixtures/sig/gen-signed-fixtures.mjs +196 -0
  48. package/src/lifecycle/__tests__/helpers.ts +78 -0
  49. package/src/lifecycle/__tests__/install.test.ts +514 -0
  50. package/src/lifecycle/__tests__/keygen.test.ts +61 -0
  51. package/src/lifecycle/__tests__/manifest-contract.test.ts +228 -0
  52. package/src/lifecycle/__tests__/publish.test.ts +80 -0
  53. package/src/lifecycle/__tests__/sig-fixtures.test.ts +100 -0
  54. package/src/lifecycle/__tests__/sign.test.ts +131 -0
  55. package/src/lifecycle/__tests__/smoke.test.ts +61 -0
  56. package/src/lifecycle/__tests__/trust.test.ts +177 -0
  57. package/src/lifecycle/__tests__/uninstall.test.ts +87 -0
  58. package/src/lifecycle/keygen.ts +40 -0
  59. package/src/lifecycle/sign.ts +74 -0
  60. package/src/manifest-contract.ts +696 -0
  61. package/src/oj-cert.ts +19 -0
  62. package/src/oj-cmd.ts +3 -3
  63. package/src/preview.ts +2 -2
  64. package/src/publish.ts +150 -0
  65. package/src/sign.ts +128 -0
  66. package/src/site.ts +146 -0
  67. package/src/trust.ts +116 -0
  68. package/src/uninstall.ts +29 -0
  69. package/src/usage.ts +35 -1
  70. package/templates/README.md +1 -1
  71. package/templates/api/config.yaml +47 -0
  72. package/templates/api/seed.mysql.sql +1 -1
  73. package/templates/api/seed.pg.sql +1 -1
  74. package/templates/api/src/_platform/_lifecycle/atomic.ts +98 -0
  75. package/templates/api/src/_platform/_lifecycle/history.ts +31 -0
  76. package/templates/api/src/_platform/_lifecycle/installer.ts +1080 -0
  77. package/templates/api/src/_platform/_lifecycle/lock.ts +148 -0
  78. package/templates/api/src/_platform/_lifecycle/semver.ts +61 -0
  79. package/templates/api/src/_platform/_lifecycle/sha384.ts +187 -0
  80. package/templates/api/src/_platform/_lifecycle/state.ts +256 -0
  81. package/templates/api/src/_platform/_lifecycle/steps/deploy-web.ts +182 -0
  82. package/templates/api/src/_platform/_lifecycle/steps/download.ts +90 -0
  83. package/templates/api/src/_platform/_lifecycle/steps/extract.ts +239 -0
  84. package/templates/api/src/_platform/_lifecycle/steps/migrate.ts +363 -0
  85. package/templates/api/src/_platform/_lifecycle/steps/preflight.ts +590 -0
  86. package/templates/api/src/_platform/_lifecycle/steps/register.ts +211 -0
  87. package/templates/api/src/_platform/_lifecycle/steps/reload.ts +81 -0
  88. package/templates/api/src/_platform/_lifecycle/steps/smoke.ts +216 -0
  89. package/templates/api/src/_platform/_lifecycle/steps/verify.ts +207 -0
  90. package/templates/api/src/_platform/_lifecycle/tasks.ts +119 -0
  91. package/templates/api/src/_platform/_lifecycle/text-edit.ts +110 -0
  92. package/templates/api/src/_platform/_lifecycle/tgz-guard.ts +41 -0
  93. package/templates/api/src/_platform/_lifecycle/trust.ts +229 -0
  94. package/templates/api/src/_platform/_lifecycle/types.ts +94 -0
  95. package/templates/api/src/_platform/_lifecycle/uninstall.ts +283 -0
  96. package/templates/api/src/_platform/_lifecycle/verify-checks.ts +228 -0
  97. package/templates/api/src/_platform/_lifecycle/verify-report.ts +92 -0
  98. package/templates/api/src/_platform/_shared/service/access.ts +2 -2
  99. package/templates/api/src/_platform/_shared/service/settings.ts +10 -1
  100. package/templates/api/src/_platform/_shared/service/solution-perms.ts +14 -7
  101. package/templates/api/src/_platform/_shared/site-facade.ts +44 -0
  102. package/templates/api/src/_platform/manifest.yaml +1 -0
  103. package/templates/api/src/_platform/market/api.ts +883 -0
  104. package/templates/api/src/_platform/market/bootstrap.ts +39 -0
  105. package/templates/api/src/_platform/migrations/0010__sys_solution_route.mysql.sql +17 -0
  106. package/templates/api/src/_platform/migrations/0010__sys_solution_route.pg.sql +16 -0
  107. package/templates/api/src/_platform/migrations/0010__sys_solution_route.sql +18 -0
  108. package/templates/api/src/_platform/migrations/0011__sys_setting_group_label.mysql.sql +2 -0
  109. package/templates/api/src/_platform/migrations/0011__sys_setting_group_label.sql +4 -0
  110. package/templates/api/src/_platform/migrations/0012__market_history.mysql.sql +13 -0
  111. package/templates/api/src/_platform/migrations/0012__market_history.pg.sql +12 -0
  112. package/templates/api/src/_platform/migrations/0012__market_history.sql +14 -0
  113. package/templates/api/src/_platform/schema.yaml +21 -4
  114. package/templates/api/src/_platform/seed.sql +2 -2
  115. package/templates/api/src/_platform/solutions/api.ts +212 -0
  116. package/templates/api/src/demo/manifest.yaml +5 -0
  117. package/templates/api/src/demo/migrations/0001__create_todos.sql +9 -0
  118. package/templates/api/src/demo/schema.yaml +11 -0
  119. package/templates/api/src/demo/seed.sql +5 -0
  120. package/templates/api/src/demo/todos/api.ts +59 -0
  121. package/templates/api/src/home/line/api.ts +57 -0
  122. package/templates/api/src/home/manifest.yaml +6 -0
  123. package/templates/api/src/home/migrations/0001__create_home_tables.sql +13 -0
  124. package/templates/api/src/home/pie/api.ts +40 -0
  125. package/templates/api/src/home/schema.yaml +14 -0
  126. package/templates/api/src/home/seed.sql +70 -0
  127. package/templates/api/src/system/oplog/api.ts +44 -2
  128. package/templates/api/tests/fixture.ts +16 -0
  129. package/templates/api/tests/lifecycle/atomic-state.spec.ts +183 -0
  130. package/templates/api/tests/lifecycle/bootstrap.spec.ts +116 -0
  131. package/templates/api/tests/lifecycle/deploy-smoke.spec.ts +492 -0
  132. package/templates/api/tests/lifecycle/installer.spec.ts +1218 -0
  133. package/templates/api/tests/lifecycle/market-api.spec.ts +971 -0
  134. package/templates/api/tests/lifecycle/migrate.spec.ts +575 -0
  135. package/templates/api/tests/lifecycle/preflight.spec.ts +255 -0
  136. package/templates/api/tests/lifecycle/register.spec.ts +194 -0
  137. package/templates/api/tests/lifecycle/semver.spec.ts +43 -0
  138. package/templates/api/tests/lifecycle/sha384.spec.ts +48 -0
  139. package/templates/api/tests/lifecycle/sig-fixtures.spec.ts +65 -0
  140. package/templates/api/tests/lifecycle/solutions-api.spec.ts +98 -0
  141. package/templates/api/tests/lifecycle/steps-fs.spec.ts +623 -0
  142. package/templates/api/tests/lifecycle/text-edit.spec.ts +177 -0
  143. package/templates/api/tests/lifecycle/tgz-guard.spec.ts +103 -0
  144. package/templates/api/tests/lifecycle/trust.spec.ts +310 -0
  145. package/templates/api/tests/lifecycle/uninstall.spec.ts +386 -0
  146. package/templates/api/tests/lifecycle/verify-checks.spec.ts +385 -0
  147. package/templates/api/tests/lifecycle/verify-report.spec.ts +290 -0
  148. package/templates/api/tests/rbac-settings.spec.ts +8 -0
  149. package/templates/init.include.yaml +56 -0
  150. package/templates/openapi.json +527 -0
  151. package/templates/package.json +52 -0
  152. package/templates/web/src/_platform/__client/api.schemas.ts +8 -0
  153. package/templates/web/src/_platform/__client/api.ts +335 -0
  154. package/templates/web/src/about/entry.ts +42 -0
  155. package/templates/web/src/about/locales/en-US.json +16 -0
  156. package/templates/web/src/about/locales/zh-CN.json +16 -0
  157. package/templates/web/src/about/pages/constants.ts +20 -0
  158. package/templates/web/src/about/pages/index.tsx +135 -0
  159. package/templates/web/src/access/entry.ts +74 -0
  160. package/templates/web/src/access/locales/en-US.json +46 -0
  161. package/templates/web/src/access/locales/zh-CN.json +46 -0
  162. package/templates/web/src/access/pages/access-mode/index.tsx +16 -0
  163. package/templates/web/src/access/pages/admin-visible/index.tsx +16 -0
  164. package/templates/web/src/access/pages/button-control/index.tsx +181 -0
  165. package/templates/web/src/access/pages/common-visible/index.tsx +16 -0
  166. package/templates/web/src/access/pages/page-control/index.tsx +142 -0
  167. package/templates/web/src/demo/__client/api.schemas.ts +20 -0
  168. package/templates/web/src/demo/__client/api.ts +89 -0
  169. package/templates/web/src/exception/entry.ts +80 -0
  170. package/templates/web/src/exception/locales/en-US.json +13 -0
  171. package/templates/web/src/exception/locales/zh-CN.json +13 -0
  172. package/templates/web/src/exception/pages/403/index.tsx +30 -0
  173. package/templates/web/src/exception/pages/404/index.tsx +30 -0
  174. package/templates/web/src/exception/pages/500/index.tsx +30 -0
  175. package/templates/web/src/exception/pages/unknown-component/index.tsx +38 -0
  176. package/templates/web/src/home/__client/api.schemas.ts +22 -0
  177. package/templates/web/src/home/__client/api.ts +113 -0
  178. package/templates/web/src/login/locales/en-US.json +6 -0
  179. package/templates/web/src/login/locales/zh-CN.json +6 -0
  180. package/templates/web/src/login/pages/login.tsx +67 -18
  181. package/templates/web/src/oj-market/__tests__/CardGrid.test.tsx +93 -0
  182. package/templates/web/src/oj-market/__tests__/HistoryDrawer.test.tsx +28 -0
  183. package/templates/web/src/oj-market/__tests__/InstallDialog.test.tsx +69 -0
  184. package/templates/web/src/oj-market/__tests__/RollbackDialog.test.tsx +43 -0
  185. package/templates/web/src/oj-market/__tests__/UninstallConfirm.test.tsx +30 -0
  186. package/templates/web/src/oj-market/__tests__/marketClient.fake.ts +70 -0
  187. package/templates/web/src/oj-market/__tests__/poll.test.ts +24 -0
  188. package/templates/web/src/oj-market/__tests__/render.tsx +14 -0
  189. package/templates/web/src/oj-market/__tests__/runtime-stub.ts +14 -0
  190. package/templates/web/src/oj-market/__tests__/semver.test.ts +26 -0
  191. package/templates/web/src/oj-market/api/MarketClientProvider.tsx +12 -0
  192. package/templates/web/src/oj-market/api/client.ts +135 -0
  193. package/templates/web/src/oj-market/api/context.ts +11 -0
  194. package/templates/web/src/oj-market/api/defaultClient.ts +7 -0
  195. package/templates/web/src/oj-market/components/CardGrid.tsx +76 -0
  196. package/templates/web/src/oj-market/components/CardItem.tsx +102 -0
  197. package/templates/web/src/oj-market/components/DetailDrawer.tsx +53 -0
  198. package/templates/web/src/oj-market/components/HistoryDrawer.tsx +75 -0
  199. package/templates/web/src/oj-market/components/InstallDialog.tsx +112 -0
  200. package/templates/web/src/oj-market/components/RollbackDialog.tsx +79 -0
  201. package/templates/web/src/oj-market/components/UninstallConfirm.tsx +74 -0
  202. package/templates/web/src/oj-market/entry.ts +27 -0
  203. package/templates/web/src/oj-market/hooks/useMarket.ts +91 -0
  204. package/templates/web/src/oj-market/locales/en-US.json +100 -0
  205. package/templates/web/src/oj-market/locales/zh-CN.json +100 -0
  206. package/templates/web/src/oj-market/pages/MarketApp.tsx +136 -0
  207. package/templates/web/src/oj-market/types.ts +84 -0
  208. package/templates/web/src/oj-market/utils/poll.ts +36 -0
  209. package/templates/web/src/oj-market/utils/semver.ts +14 -0
  210. package/templates/web/src/outside/entry.ts +80 -0
  211. package/templates/web/src/outside/locales/en-US.json +10 -0
  212. package/templates/web/src/outside/locales/zh-CN.json +10 -0
  213. package/templates/web/src/personal-center/entry.ts +1 -10
  214. package/templates/web/src/personal-center/locales/en-US.json +1 -5
  215. package/templates/web/src/personal-center/locales/zh-CN.json +1 -5
  216. package/templates/web/src/route-nest/entry.ts +73 -0
  217. package/templates/web/src/route-nest/locales/en-US.json +9 -0
  218. package/templates/web/src/route-nest/locales/zh-CN.json +9 -0
  219. package/templates/web/src/route-nest/pages/menu1/menu1-1/index.tsx +5 -0
  220. package/templates/web/src/route-nest/pages/menu1/menu1-2/index.tsx +5 -0
  221. package/templates/web/src/route-nest/pages/menu2/index.tsx +5 -0
  222. package/templates/web/src/system/__client/api.schemas.ts +12 -0
  223. package/templates/web/src/system/__client/api.ts +25 -1
  224. package/templates/web/src/system/pages/oplog/index.tsx +1 -1
  225. package/templates/web/src/system/pages/roles/index.tsx +1 -0
  226. package/templates/web/src/system/pages/settings/index.tsx +1 -1
  227. package/templates/web/src/system/pages/users/index.tsx +1 -1
  228. package/templates/web.config.ts +32 -40
  229. package/vendor/host-versions.json +3 -3
  230. package/templates/pnpm-workspace.yaml +0 -14
  231. package/templates/web/src/personal-center/pages/settings/index.tsx +0 -5
@@ -0,0 +1,492 @@
1
+ // BDD:部署/热更/冒烟三步(P1-9,十阶段 stage 8~10)。
2
+ // deploy-web = staging/web 产物树真复制到站点 modules 目录 + modules.json 读-改-原子写 upsert
3
+ // (D13:读者永不见半截 JSON;R30:entry/css 路径 POSIX 分隔符序列化;同名整体替换、他条目原样保留);
4
+ // reload = 注入 reloadConfig 面(platform 特权 API),unchanged = 改写未生效判失败(R18 终态断言),
5
+ // expected 段键(缺省 ["vars"],B-11 reload 触发面)须逐一 ∈ applied;
6
+ // smoke = 探活(health 2xx / 缺省首个非匿名路由连通即可)+ integrity(部署落位后重算 sha384 vs
7
+ // modules.json 条目,R10 包内容口径;缺失 fail-closed)+ 注册一致性(升级态「包含非相等」:
8
+ // 快照逐数组 ⊇ 声明,少 = 注册丢失判失败)collect-all 全败齐报。
9
+ // entry 浏览器加载不在安装器验(Node import 必假绿——设计 §4.1),注释钉口径。
10
+ // fetch/sha384/query/reload 面经 ctx 注入 stub(同 steps-fs 范式),fs 为 globalThis.fs 面。
11
+ import type { FsFace } from "../../src/_platform/_lifecycle/atomic";
12
+ import type { StateStore } from "../../src/_platform/_lifecycle/state";
13
+ import type { DeployWebContext, ModuleEntry } from "../../src/_platform/_lifecycle/steps/deploy-web";
14
+ import type { ReloadContext, ReloadFace, ReloadResult } from "../../src/_platform/_lifecycle/steps/reload";
15
+ import type { SmokeContext } from "../../src/_platform/_lifecycle/steps/smoke";
16
+ import { createHash } from "node:crypto";
17
+ import * as fsp from "node:fs/promises";
18
+ import { tmpdir } from "node:os";
19
+ import { join } from "node:path";
20
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
21
+ import { deployWeb, posixPath, upsertModulesJson } from "../../src/_platform/_lifecycle/steps/deploy-web";
22
+ import { reloadAndAssert } from "../../src/_platform/_lifecycle/steps/reload";
23
+ import { probeUrlOf, smoke } from "../../src/_platform/_lifecycle/steps/smoke";
24
+
25
+ const g = globalThis as any;
26
+
27
+ const ENTRY = new TextEncoder().encode("console.log(\"demo entry\");\n");
28
+ const CSS = new TextEncoder().encode(".demo{color:red}\n");
29
+ const sha384b64 = (d: Uint8Array) => createHash("sha384").update(d).digest("base64");
30
+ const INT = `sha384-${sha384b64(ENTRY)}`;
31
+
32
+ let root: string;
33
+ let staging: string;
34
+ let dist: string;
35
+ let siteModulesDir: string;
36
+ let modulesJsonPath: string;
37
+
38
+ beforeEach(async () => {
39
+ root = await fsp.mkdtemp(join(tmpdir(), "ojm-deploy-smoke-"));
40
+ staging = join(root, "staging");
41
+ dist = join(root, "dist");
42
+ siteModulesDir = join(dist, "modules");
43
+ modulesJsonPath = join(dist, "modules.json");
44
+ g.fs = nodeFsFace();
45
+ });
46
+
47
+ afterEach(async () => {
48
+ g.fs = undefined;
49
+ await fsp.rm(root, { recursive: true, force: true });
50
+ });
51
+
52
+ function nodeFsFace(): FsFace {
53
+ return {
54
+ writeFile: (path, data) => fsp.writeFile(path, data),
55
+ readFile: path => fsp.readFile(path),
56
+ mkdir: (path, opts) => fsp.mkdir(path, opts),
57
+ remove: (path, opts) => fsp.rm(path, opts),
58
+ rename: (oldPath, newPath) => fsp.rename(oldPath, newPath),
59
+ copyFile: (src, dest) => fsp.copyFile(src, dest),
60
+ readdir: path => fsp.readdir(path),
61
+ };
62
+ }
63
+
64
+ function stateStore(): StateStore {
65
+ return { read: () => ({ solutions: {} }), write: async () => {} };
66
+ }
67
+
68
+ function solManifest(overrides: Record<string, unknown> = {}): any {
69
+ return {
70
+ name: "demo-solution",
71
+ version: "0.1.0",
72
+ peerRuntime: ">=0.0.0",
73
+ api: { prefix: "/api/demo-solution", anonymous: ["demo-solution/ping"] },
74
+ web: {
75
+ entry: "modules/demo-solution/0.1.0/entry.js",
76
+ css: ["modules/demo-solution/0.1.0/main.css"],
77
+ },
78
+ routes: [
79
+ { path: "/demo-solution/ping", category: "hidden", order: 205, title_key: "demo-solution:menu.ping" },
80
+ { path: "/demo-solution", category: "main", order: 200, title_key: "demo-solution:menu.main" },
81
+ { path: "/demo-solution/report", category: "hidden", order: 210, title_key: "demo-solution:menu.report" },
82
+ ],
83
+ permissions: [{ code: "demo-solution.view", title_key: "demo-solution:perm.view" }],
84
+ roles: [{ name: "demo-solution.admin", title_key: "demo-solution:role.admin", permissions: ["demo-solution.view"] }],
85
+ settings: {
86
+ groups: [{
87
+ group: "display",
88
+ label_key: "demo-solution:settings.group.display",
89
+ items: [{ key: "demo-solution.display.theme", type: "enum" }],
90
+ }],
91
+ },
92
+ ...overrides,
93
+ };
94
+ }
95
+
96
+ function dwCtx(overrides: Partial<DeployWebContext> = {}): DeployWebContext {
97
+ return {
98
+ manifest: solManifest(),
99
+ name: "demo-solution",
100
+ version: "0.1.0",
101
+ state: stateStore(),
102
+ stagingDir: staging,
103
+ siteModulesDir,
104
+ modulesJsonPath,
105
+ integrity: INT,
106
+ ...overrides,
107
+ };
108
+ }
109
+
110
+ function smCtx(overrides: Partial<SmokeContext> = {}): SmokeContext {
111
+ return {
112
+ manifest: solManifest(),
113
+ name: "demo-solution",
114
+ version: "0.1.0",
115
+ state: stateStore(),
116
+ apiPrefix: "/api/demo-solution",
117
+ fetch: async () => ({ ok: true, status: 200 }),
118
+ sha384B64: sha384b64,
119
+ modulesJsonPath,
120
+ query: async () => ({
121
+ module: "demo-solution",
122
+ routes: ["/demo-solution", "/demo-solution/ping", "/demo-solution/report"],
123
+ settings: ["demo-solution.display.theme"],
124
+ roles: ["demo-solution.admin"],
125
+ permissions: ["demo-solution.view"],
126
+ }),
127
+ ...overrides,
128
+ };
129
+ }
130
+
131
+ /** 种出「已部署落位」状态:产物文件 + modules.json 条目(smoke integrity 比对基准;withIntegrity=false 验缺 integrity fail-closed) */
132
+ async function seedDeployed(withIntegrity = true): Promise<void> {
133
+ const out = join(siteModulesDir, "demo-solution", "0.1.0");
134
+ await fsp.mkdir(out, { recursive: true });
135
+ await fsp.writeFile(join(out, "entry.js"), ENTRY);
136
+ await fsp.writeFile(join(out, "main.css"), CSS);
137
+ await fsp.mkdir(dist, { recursive: true });
138
+ const entry: Record<string, unknown> = {
139
+ name: "demo-solution",
140
+ entry: "modules/demo-solution/0.1.0/entry.js",
141
+ enabled: true,
142
+ css: ["modules/demo-solution/0.1.0/main.css"],
143
+ };
144
+ if (withIntegrity) {
145
+ entry.integrity = INT;
146
+ }
147
+ await fsp.writeFile(modulesJsonPath, `${JSON.stringify([entry])}\n`);
148
+ }
149
+
150
+ /** 种出 staging web 产物树(deployWeb 复制来源;含子目录验证递归复制) */
151
+ async function seedStagingWeb(): Promise<string> {
152
+ const src = join(staging, "web", "modules", "demo-solution", "0.1.0");
153
+ await fsp.mkdir(join(src, "assets"), { recursive: true });
154
+ await fsp.writeFile(join(src, "entry.js"), ENTRY);
155
+ await fsp.writeFile(join(src, "main.css"), CSS);
156
+ await fsp.writeFile(join(src, "assets", "logo.svg"), new TextEncoder().encode("<svg/>"));
157
+ return src;
158
+ }
159
+
160
+ describe("upsertModulesJson(R30 POSIX + D13 读-改-原子写)", () => {
161
+ it("反斜杠路径序列化为 POSIX(R30):entry/css 统一 / 落盘", async () => {
162
+ await upsertModulesJson({
163
+ name: "demo-solution",
164
+ entry: "modules\\demo-solution\\0.1.0\\entry.js",
165
+ enabled: true,
166
+ css: ["modules\\demo-solution\\0.1.0\\main.css"],
167
+ }, { modulesJsonPath });
168
+ const list = JSON.parse(await fsp.readFile(modulesJsonPath, "utf8"));
169
+ expect(list[0].entry).toBe("modules/demo-solution/0.1.0/entry.js");
170
+ expect(list[0].css).toEqual(["modules/demo-solution/0.1.0/main.css"]);
171
+ expect(posixPath("a\\b\\c.js")).toBe("a/b/c.js");
172
+ });
173
+
174
+ it("首次 upsert 创建清单:JSON 完整解析(无半截),条目字段齐", async () => {
175
+ await upsertModulesJson({ name: "demo-solution", entry: "modules/demo-solution/0.1.0/entry.js", enabled: true }, { modulesJsonPath });
176
+ const list = JSON.parse(await fsp.readFile(modulesJsonPath, "utf8"));
177
+ expect(list).toHaveLength(1);
178
+ expect(list[0]).toEqual({ name: "demo-solution", entry: "modules/demo-solution/0.1.0/entry.js", enabled: true });
179
+ });
180
+
181
+ it("同名整体替换 + 他模块条目原样保留;重复 upsert 幂等", async () => {
182
+ const other = { name: "other", entry: "modules/other/1.0.0/entry.js", enabled: true, chunks: [{ url: "modules/other/1.0.0/entry.js", integrity: "sha384-x", lazy: false }] };
183
+ await fsp.mkdir(dist, { recursive: true });
184
+ await fsp.writeFile(modulesJsonPath, `${JSON.stringify([other], null, 2)}\n`);
185
+ const first: ModuleEntry = { name: "demo-solution", entry: "modules/demo-solution/0.1.0/entry.js", enabled: true, integrity: INT };
186
+ await upsertModulesJson(first, { modulesJsonPath });
187
+ await upsertModulesJson({ ...first, entry: "modules/demo-solution/9.9.9/entry.js" }, { modulesJsonPath });
188
+ const list = JSON.parse(await fsp.readFile(modulesJsonPath, "utf8"));
189
+ expect(list).toHaveLength(2);
190
+ expect(list.find((e: ModuleEntry) => e.name === "other")).toEqual(other);
191
+ const mine = list.filter((e: ModuleEntry) => e.name === "demo-solution");
192
+ expect(mine).toHaveLength(1);
193
+ expect(mine[0].entry).toBe("modules/demo-solution/9.9.9/entry.js");
194
+ });
195
+
196
+ it("损坏 modules.json 拒绝 upsert(不覆盖丢失他模块条目)", async () => {
197
+ await fsp.mkdir(dist, { recursive: true });
198
+ await fsp.writeFile(modulesJsonPath, "{\"name\": \"other\", \"entr");
199
+ await expect(upsertModulesJson(
200
+ { name: "demo-solution", entry: "modules/demo-solution/0.1.0/entry.js", enabled: true },
201
+ { modulesJsonPath },
202
+ )).rejects.toThrow("拒绝 upsert");
203
+ });
204
+ });
205
+
206
+ describe("deployWeb(stage=deploying:staging/web 树 → 站点 modules 目录 + 清单 upsert)", () => {
207
+ it("真复制:落位文件树与来源一致(含子目录),modules.json 条目 entry POSIX + integrity 透传", async () => {
208
+ const src = await seedStagingWeb();
209
+ const r = await deployWeb(dwCtx());
210
+ expect(r.stage).toBe("deploying");
211
+ expect(r.ok).toBe(true);
212
+ const out = join(siteModulesDir, "demo-solution", "0.1.0");
213
+ expect(new Uint8Array(await fsp.readFile(join(out, "entry.js")))).toEqual(ENTRY);
214
+ expect(new Uint8Array(await fsp.readFile(join(out, "main.css")))).toEqual(CSS);
215
+ expect(new Uint8Array(await fsp.readFile(join(out, "assets", "logo.svg")))).toEqual(new TextEncoder().encode("<svg/>"));
216
+ expect(await fsp.readdir(join(out, "assets"))).toEqual(await fsp.readdir(join(src, "assets")));
217
+ const list = JSON.parse(await fsp.readFile(modulesJsonPath, "utf8"));
218
+ expect(list[0]).toEqual({
219
+ name: "demo-solution",
220
+ entry: "modules/demo-solution/0.1.0/entry.js",
221
+ enabled: true,
222
+ version: "0.1.0", // P2-15②:完整条目 extras
223
+ chunks: [],
224
+ css: ["modules/demo-solution/0.1.0/main.css"],
225
+ integrity: INT,
226
+ peerRuntime: ">=0.0.0",
227
+ });
228
+ });
229
+
230
+ it("ctx.integrity 优先于 manifest.web.integrity(P1-10 验签后重算口径,R10)", async () => {
231
+ await seedStagingWeb();
232
+ await deployWeb(dwCtx({
233
+ manifest: solManifest({ web: { entry: "modules/demo-solution/0.1.0/entry.js", integrity: "sha384-STALE" } }),
234
+ }));
235
+ const list = JSON.parse(await fsp.readFile(modulesJsonPath, "utf8"));
236
+ expect(list[0].integrity).toBe(INT);
237
+ });
238
+
239
+ it("manifest 无 web 段:api-only no-op ok,零落盘零清单变更", async () => {
240
+ const r = await deployWeb(dwCtx({ manifest: solManifest({ web: undefined }) }));
241
+ expect(r.ok).toBe(true);
242
+ await expect(fsp.readdir(siteModulesDir)).rejects.toThrow();
243
+ await expect(fsp.readFile(modulesJsonPath)).rejects.toThrow();
244
+ });
245
+
246
+ it("web.entry 已声明而 staging/web 缺失:ok=false retryable=false(包结构违约,C2-27)", async () => {
247
+ const r = await deployWeb(dwCtx());
248
+ expect(r.ok).toBe(false);
249
+ expect(r.retryable).toBe(false);
250
+ expect(r.message).toContain("web");
251
+ });
252
+
253
+ it("modules.json 损坏:部署失败且清单不被半写", async () => {
254
+ await seedStagingWeb();
255
+ await fsp.mkdir(dist, { recursive: true });
256
+ await fsp.writeFile(modulesJsonPath, "not-json");
257
+ const r = await deployWeb(dwCtx());
258
+ expect(r.ok).toBe(false);
259
+ expect(await fsp.readFile(modulesJsonPath, "utf8")).toBe("not-json");
260
+ });
261
+ });
262
+
263
+ describe("reloadAndAssert(stage=reloading:R18 unchanged=失败)", () => {
264
+ function rlCtx(result?: ReloadResult, throws = false): ReloadContext {
265
+ const face: ReloadFace = {
266
+ reloadConfig: throws
267
+ ? async () => {
268
+ throw new Error("server.reloadConfig: reload in progress (busy)");
269
+ }
270
+ : async () => result ?? { generation: 8, unchanged: false, applied: ["vars"], restart_pending: [], kept_listener: [] },
271
+ };
272
+ return { manifest: solManifest(), name: "demo-solution", version: "0.1.0", state: stateStore(), reload: face };
273
+ }
274
+
275
+ it("成功:applied 含 vars(缺省 expected)→ ok=true,message 含 generation", async () => {
276
+ const r = await reloadAndAssert(rlCtx());
277
+ expect(r.stage).toBe("reloading");
278
+ expect(r.ok).toBe(true);
279
+ expect(r.message).toContain("8");
280
+ });
281
+
282
+ it("unchanged → ok=false(R18:幂等回显 = config 改写未生效)", async () => {
283
+ const r = await reloadAndAssert(rlCtx({ generation: 7, unchanged: true, applied: [] }));
284
+ expect(r.ok).toBe(false);
285
+ expect(r.message).toContain("unchanged");
286
+ });
287
+
288
+ it("face 抛错(并发 busy)→ ok=false retryable=true", async () => {
289
+ const r = await reloadAndAssert(rlCtx(undefined, true));
290
+ expect(r.ok).toBe(false);
291
+ expect(r.retryable).toBe(true);
292
+ expect(r.message).toContain("busy");
293
+ });
294
+
295
+ it("expected 段键未生效:applied 缺 vars → ok=false(R18 以实际生效值断言)", async () => {
296
+ const r = await reloadAndAssert({ ...rlCtx({ generation: 8, unchanged: false, applied: ["mounts"], restart_pending: [], kept_listener: [] }), expected: ["mounts", "vars"] });
297
+ expect(r.ok).toBe(false);
298
+ expect(r.message).toContain("vars");
299
+ });
300
+
301
+ it("返回形状违约(applied 非数组)→ ok=false retryable=false", async () => {
302
+ const r = await reloadAndAssert(rlCtx({ unchanged: false, applied: "mounts" } as unknown as ReloadResult));
303
+ expect(r.ok).toBe(false);
304
+ expect(r.retryable).toBe(false);
305
+ });
306
+ });
307
+
308
+ describe("probeUrlOf(探针 URL 形态:apiPrefix/<path 去前导斜杠>,与 C2-26 段内收敛一致)", () => {
309
+ it("health /demo-solution/ping → /api/demo-solution/demo-solution/ping", () => {
310
+ expect(probeUrlOf("/api/demo-solution", "/demo-solution/ping")).toBe("/api/demo-solution/demo-solution/ping");
311
+ });
312
+ });
313
+
314
+ describe("smoke 探活(D14/Q-A:health 不强制)", () => {
315
+ it("health 声明:探 <apiPrefix>/<health>,2xx 过", async () => {
316
+ await seedDeployed();
317
+ const urls: string[] = [];
318
+ const r = await smoke(smCtx({
319
+ manifest: solManifest({ health: "/demo-solution/ping" }),
320
+ fetch: async (url) => {
321
+ urls.push(url);
322
+ return { ok: true, status: 200 };
323
+ },
324
+ }));
325
+ expect(urls).toEqual(["/api/demo-solution/demo-solution/ping"]);
326
+ expect(r.ok).toBe(true);
327
+ });
328
+
329
+ it("health 非 2xx → 探活失败", async () => {
330
+ await seedDeployed();
331
+ const r = await smoke(smCtx({
332
+ manifest: solManifest({ health: "/demo-solution/ping" }),
333
+ fetch: async () => ({ ok: false, status: 500 }),
334
+ }));
335
+ expect(r.ok).toBe(false);
336
+ expect(r.message).toContain("探活");
337
+ expect(r.message).toContain("500");
338
+ });
339
+
340
+ it("无 health:首个非匿名路由连通即可(401 亦过),匿名路由被跳过", async () => {
341
+ await seedDeployed();
342
+ const urls: string[] = [];
343
+ const r = await smoke(smCtx({
344
+ fetch: async (url) => {
345
+ urls.push(url);
346
+ return { ok: false, status: 401 };
347
+ },
348
+ }));
349
+ expect(urls).toEqual(["/api/demo-solution/demo-solution"]);
350
+ expect(r.ok).toBe(true);
351
+ });
352
+
353
+ it("无 health 且 fetch 抛错 → 探活失败", async () => {
354
+ await seedDeployed();
355
+ const r = await smoke(smCtx({
356
+ fetch: async () => {
357
+ throw new Error("ECONNREFUSED");
358
+ },
359
+ }));
360
+ expect(r.ok).toBe(false);
361
+ expect(r.message).toContain("探活");
362
+ });
363
+
364
+ it("无 health 无路由:无事可验视为过", async () => {
365
+ await seedDeployed();
366
+ const r = await smoke(smCtx({
367
+ manifest: solManifest({ routes: undefined, api: undefined, roles: undefined, permissions: undefined, settings: undefined }),
368
+ query: async () => null,
369
+ }));
370
+ expect(r.ok).toBe(true);
371
+ });
372
+ });
373
+
374
+ describe("smoke integrity(R10:部署落位后重算 sha384 vs modules.json)", () => {
375
+ it("落位产物与清单一致 → 过", async () => {
376
+ await seedDeployed();
377
+ const r = await smoke(smCtx());
378
+ expect(r.ok).toBe(true);
379
+ });
380
+
381
+ it("篡改一字节 → 失败,message 含期望与实得摘要", async () => {
382
+ await seedDeployed();
383
+ await fsp.writeFile(join(siteModulesDir, "demo-solution", "0.1.0", "entry.js"), new TextEncoder().encode("console.log(\"demo entrY\");\n"));
384
+ const r = await smoke(smCtx());
385
+ expect(r.ok).toBe(false);
386
+ expect(r.message).toContain("integrity");
387
+ expect(r.message).toContain(INT);
388
+ expect(r.message).toContain(`sha384-${sha384b64(new TextEncoder().encode("console.log(\"demo entrY\");\n"))}`);
389
+ });
390
+
391
+ it("modules.json 条目缺 integrity → 失败(fail-closed,P1-10 必传)", async () => {
392
+ await seedDeployed(false);
393
+ const r = await smoke(smCtx());
394
+ expect(r.ok).toBe(false);
395
+ expect(r.message).toContain("fail-closed");
396
+ });
397
+
398
+ it("声明 css 未落位 → 失败", async () => {
399
+ await seedDeployed();
400
+ await fsp.rm(join(siteModulesDir, "demo-solution", "0.1.0", "main.css"));
401
+ const r = await smoke(smCtx());
402
+ expect(r.ok).toBe(false);
403
+ expect(r.message).toContain("main.css");
404
+ });
405
+
406
+ it("manifest 无 web 段:integrity 项不适用(api-only 不因清单缺条目失败)", async () => {
407
+ await seedDeployed();
408
+ const r = await smoke(smCtx({ manifest: solManifest({ web: undefined, api: undefined, routes: undefined, settings: undefined, roles: undefined, permissions: undefined }) }));
409
+ expect(r.ok).toBe(true);
410
+ });
411
+ });
412
+
413
+ describe("smoke 注册一致性(升级态「包含非相等」)", () => {
414
+ it("快照超集(含已废弃遗留行)→ 过", async () => {
415
+ await seedDeployed();
416
+ const r = await smoke(smCtx({
417
+ query: async () => ({
418
+ module: "demo-solution",
419
+ routes: ["/demo-solution", "/demo-solution/ping", "/demo-solution/report", "/demo-solution/legacy"],
420
+ settings: ["demo-solution.display.theme", "demo-solution.display.legacy"],
421
+ roles: ["demo-solution.admin"],
422
+ permissions: ["demo-solution.view"],
423
+ }),
424
+ }));
425
+ expect(r.ok).toBe(true);
426
+ });
427
+
428
+ it("快照缺声明项 → 失败(注册丢失判失败,少 = 不可)", async () => {
429
+ await seedDeployed();
430
+ const r = await smoke(smCtx({
431
+ query: async () => ({
432
+ module: "demo-solution",
433
+ routes: ["/demo-solution"],
434
+ settings: [],
435
+ roles: ["demo-solution.admin"],
436
+ permissions: ["demo-solution.view"],
437
+ }),
438
+ }));
439
+ expect(r.ok).toBe(false);
440
+ expect(r.message).toContain("demo-solution.display.theme");
441
+ expect(r.message).toContain("demo-solution/report");
442
+ });
443
+
444
+ it("快照 null + 声明全空 → 过(首装无 RBAC/设置/路由声明)", async () => {
445
+ await seedDeployed();
446
+ const r = await smoke(smCtx({
447
+ manifest: solManifest({ routes: undefined, roles: undefined, permissions: undefined, settings: undefined, api: undefined, web: undefined }),
448
+ query: async () => null,
449
+ }));
450
+ expect(r.ok).toBe(true);
451
+ });
452
+
453
+ it("快照 null + 声明非空 → 失败", async () => {
454
+ await seedDeployed();
455
+ const r = await smoke(smCtx({ query: async () => null }));
456
+ expect(r.ok).toBe(false);
457
+ expect(r.message).toContain("注册");
458
+ });
459
+
460
+ it("快照查询抛错 → 失败", async () => {
461
+ await seedDeployed();
462
+ const r = await smoke(smCtx({ query: async () => {
463
+ throw new Error("db locked");
464
+ } }));
465
+ expect(r.ok).toBe(false);
466
+ expect(r.message).toContain("db locked");
467
+ });
468
+ });
469
+
470
+ describe("smoke collect-all(失败全量齐报)", () => {
471
+ it("探活与 integrity 同败:两项原因齐入 message", async () => {
472
+ await seedDeployed();
473
+ await fsp.writeFile(join(siteModulesDir, "demo-solution", "0.1.0", "entry.js"), new TextEncoder().encode("tampered"));
474
+ const r = await smoke(smCtx({
475
+ manifest: solManifest({ health: "/demo-solution/ping" }),
476
+ fetch: async () => ({ ok: false, status: 503 }),
477
+ }));
478
+ expect(r.ok).toBe(false);
479
+ expect(r.message).toContain("【探活】");
480
+ expect(r.message).toContain("【integrity】");
481
+ });
482
+
483
+ it("全过 → ok=true,三项齐名;entry 浏览器加载口径入 message(设计 §4.1)", async () => {
484
+ await seedDeployed();
485
+ const r = await smoke(smCtx());
486
+ expect(r.ok).toBe(true);
487
+ expect(r.message).toContain("探活");
488
+ expect(r.message).toContain("integrity");
489
+ expect(r.message).toContain("注册一致性");
490
+ expect(r.message).toContain("浏览器");
491
+ });
492
+ });