@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.
- package/devkit/README.md +14 -3
- package/devkit/SKILL.md +16 -4
- package/devkit/framework-dev.md +8 -6
- package/devkit/manual.md +243 -23
- package/package.json +2 -2
- package/shell-dist/assets/runtime.js +143 -8
- package/shell-dist/index.html +2 -2
- package/shell-dist/versions.json +3 -3
- package/src/args.ts +67 -0
- package/src/build.ts +11 -3
- package/src/bundle.ts +197 -0
- package/src/contract/__tests__/handler-scan.test.ts +82 -0
- package/src/contract/check.ts +2 -1
- package/src/contract/handler-scan.ts +32 -0
- package/src/deterministic-tar.ts +120 -0
- package/src/dev.ts +4 -2
- package/src/enable.ts +28 -0
- package/src/index.ts +63 -2
- package/src/info.ts +3 -2
- package/src/init.ts +147 -14
- package/src/install.ts +268 -0
- package/src/keygen.ts +88 -0
- package/src/layout.ts +31 -0
- package/src/lifecycle/__tests__/authoring.test.ts +109 -0
- package/src/lifecycle/__tests__/bundle.test.ts +150 -0
- package/src/lifecycle/__tests__/deterministic-tar.test.ts +145 -0
- package/src/lifecycle/__tests__/fixtures/README.md +59 -0
- package/src/lifecycle/__tests__/fixtures/fixture-invalid/EXPECTED-VIOLATIONS.md +27 -0
- package/src/lifecycle/__tests__/fixtures/fixture-invalid/api/src/Demo/manifest.yaml +6 -0
- package/src/lifecycle/__tests__/fixtures/fixture-invalid/manifest.yaml +40 -0
- package/src/lifecycle/__tests__/fixtures/fixture-invalid/web/src/Demo/entry.ts +14 -0
- package/src/lifecycle/__tests__/fixtures/fixture-invalid/web/src/Demo/locales/en-US.json +12 -0
- package/src/lifecycle/__tests__/fixtures/fixture-invalid/web/src/Demo/locales/zh-CN.json +12 -0
- package/src/lifecycle/__tests__/fixtures/fixture-valid/api/src/demo-solution/manifest.yaml +9 -0
- package/src/lifecycle/__tests__/fixtures/fixture-valid/api/src/demo-solution/migrations/0001__init.sql +6 -0
- package/src/lifecycle/__tests__/fixtures/fixture-valid/api/src/demo-solution/ping/api.ts +9 -0
- package/src/lifecycle/__tests__/fixtures/fixture-valid/api/src/demo-solution/schema.yaml +9 -0
- package/src/lifecycle/__tests__/fixtures/fixture-valid/manifest.yaml +96 -0
- package/src/lifecycle/__tests__/fixtures/fixture-valid/web/src/demo-solution/entry.ts +23 -0
- package/src/lifecycle/__tests__/fixtures/fixture-valid/web/src/demo-solution/locales/en-US.json +29 -0
- package/src/lifecycle/__tests__/fixtures/fixture-valid/web/src/demo-solution/locales/zh-CN.json +29 -0
- package/src/lifecycle/__tests__/fixtures/sig/demo-solution-0.2.0.tgz +0 -0
- package/src/lifecycle/__tests__/fixtures/sig/demo-solution-0.2.0.tgz.bad.sig +7 -0
- package/src/lifecycle/__tests__/fixtures/sig/demo-solution-0.2.0.tgz.sig +7 -0
- package/src/lifecycle/__tests__/fixtures/sig/demo-solution-0.2.0.tgz.unknown.sig +7 -0
- package/src/lifecycle/__tests__/fixtures/sig/gen-signed-fixtures.d.mts +21 -0
- package/src/lifecycle/__tests__/fixtures/sig/gen-signed-fixtures.mjs +196 -0
- package/src/lifecycle/__tests__/helpers.ts +78 -0
- package/src/lifecycle/__tests__/install.test.ts +514 -0
- package/src/lifecycle/__tests__/keygen.test.ts +61 -0
- package/src/lifecycle/__tests__/manifest-contract.test.ts +228 -0
- package/src/lifecycle/__tests__/publish.test.ts +80 -0
- package/src/lifecycle/__tests__/sig-fixtures.test.ts +100 -0
- package/src/lifecycle/__tests__/sign.test.ts +131 -0
- package/src/lifecycle/__tests__/smoke.test.ts +61 -0
- package/src/lifecycle/__tests__/trust.test.ts +177 -0
- package/src/lifecycle/__tests__/uninstall.test.ts +87 -0
- package/src/lifecycle/keygen.ts +40 -0
- package/src/lifecycle/sign.ts +74 -0
- package/src/manifest-contract.ts +696 -0
- package/src/oj-cert.ts +19 -0
- package/src/oj-cmd.ts +3 -3
- package/src/preview.ts +2 -2
- package/src/publish.ts +150 -0
- package/src/sign.ts +128 -0
- package/src/site.ts +146 -0
- package/src/trust.ts +116 -0
- package/src/uninstall.ts +29 -0
- package/src/usage.ts +35 -1
- package/templates/README.md +1 -1
- package/templates/api/config.yaml +47 -0
- package/templates/api/seed.mysql.sql +1 -1
- package/templates/api/seed.pg.sql +1 -1
- package/templates/api/src/_platform/_lifecycle/atomic.ts +98 -0
- package/templates/api/src/_platform/_lifecycle/history.ts +31 -0
- package/templates/api/src/_platform/_lifecycle/installer.ts +1080 -0
- package/templates/api/src/_platform/_lifecycle/lock.ts +148 -0
- package/templates/api/src/_platform/_lifecycle/semver.ts +61 -0
- package/templates/api/src/_platform/_lifecycle/sha384.ts +187 -0
- package/templates/api/src/_platform/_lifecycle/state.ts +256 -0
- package/templates/api/src/_platform/_lifecycle/steps/deploy-web.ts +182 -0
- package/templates/api/src/_platform/_lifecycle/steps/download.ts +90 -0
- package/templates/api/src/_platform/_lifecycle/steps/extract.ts +239 -0
- package/templates/api/src/_platform/_lifecycle/steps/migrate.ts +363 -0
- package/templates/api/src/_platform/_lifecycle/steps/preflight.ts +590 -0
- package/templates/api/src/_platform/_lifecycle/steps/register.ts +211 -0
- package/templates/api/src/_platform/_lifecycle/steps/reload.ts +81 -0
- package/templates/api/src/_platform/_lifecycle/steps/smoke.ts +216 -0
- package/templates/api/src/_platform/_lifecycle/steps/verify.ts +207 -0
- package/templates/api/src/_platform/_lifecycle/tasks.ts +119 -0
- package/templates/api/src/_platform/_lifecycle/text-edit.ts +110 -0
- package/templates/api/src/_platform/_lifecycle/tgz-guard.ts +41 -0
- package/templates/api/src/_platform/_lifecycle/trust.ts +229 -0
- package/templates/api/src/_platform/_lifecycle/types.ts +94 -0
- package/templates/api/src/_platform/_lifecycle/uninstall.ts +283 -0
- package/templates/api/src/_platform/_lifecycle/verify-checks.ts +228 -0
- package/templates/api/src/_platform/_lifecycle/verify-report.ts +92 -0
- package/templates/api/src/_platform/_shared/service/access.ts +2 -2
- package/templates/api/src/_platform/_shared/service/settings.ts +10 -1
- package/templates/api/src/_platform/_shared/service/solution-perms.ts +14 -7
- package/templates/api/src/_platform/_shared/site-facade.ts +44 -0
- package/templates/api/src/_platform/manifest.yaml +1 -0
- package/templates/api/src/_platform/market/api.ts +883 -0
- package/templates/api/src/_platform/market/bootstrap.ts +39 -0
- package/templates/api/src/_platform/migrations/0010__sys_solution_route.mysql.sql +17 -0
- package/templates/api/src/_platform/migrations/0010__sys_solution_route.pg.sql +16 -0
- package/templates/api/src/_platform/migrations/0010__sys_solution_route.sql +18 -0
- package/templates/api/src/_platform/migrations/0011__sys_setting_group_label.mysql.sql +2 -0
- package/templates/api/src/_platform/migrations/0011__sys_setting_group_label.sql +4 -0
- package/templates/api/src/_platform/migrations/0012__market_history.mysql.sql +13 -0
- package/templates/api/src/_platform/migrations/0012__market_history.pg.sql +12 -0
- package/templates/api/src/_platform/migrations/0012__market_history.sql +14 -0
- package/templates/api/src/_platform/schema.yaml +21 -4
- package/templates/api/src/_platform/seed.sql +2 -2
- package/templates/api/src/_platform/solutions/api.ts +212 -0
- package/templates/api/src/demo/manifest.yaml +5 -0
- package/templates/api/src/demo/migrations/0001__create_todos.sql +9 -0
- package/templates/api/src/demo/schema.yaml +11 -0
- package/templates/api/src/demo/seed.sql +5 -0
- package/templates/api/src/demo/todos/api.ts +59 -0
- package/templates/api/src/home/line/api.ts +57 -0
- package/templates/api/src/home/manifest.yaml +6 -0
- package/templates/api/src/home/migrations/0001__create_home_tables.sql +13 -0
- package/templates/api/src/home/pie/api.ts +40 -0
- package/templates/api/src/home/schema.yaml +14 -0
- package/templates/api/src/home/seed.sql +70 -0
- package/templates/api/src/system/oplog/api.ts +44 -2
- package/templates/api/tests/fixture.ts +16 -0
- package/templates/api/tests/lifecycle/atomic-state.spec.ts +183 -0
- package/templates/api/tests/lifecycle/bootstrap.spec.ts +116 -0
- package/templates/api/tests/lifecycle/deploy-smoke.spec.ts +492 -0
- package/templates/api/tests/lifecycle/installer.spec.ts +1218 -0
- package/templates/api/tests/lifecycle/market-api.spec.ts +971 -0
- package/templates/api/tests/lifecycle/migrate.spec.ts +575 -0
- package/templates/api/tests/lifecycle/preflight.spec.ts +255 -0
- package/templates/api/tests/lifecycle/register.spec.ts +194 -0
- package/templates/api/tests/lifecycle/semver.spec.ts +43 -0
- package/templates/api/tests/lifecycle/sha384.spec.ts +48 -0
- package/templates/api/tests/lifecycle/sig-fixtures.spec.ts +65 -0
- package/templates/api/tests/lifecycle/solutions-api.spec.ts +98 -0
- package/templates/api/tests/lifecycle/steps-fs.spec.ts +623 -0
- package/templates/api/tests/lifecycle/text-edit.spec.ts +177 -0
- package/templates/api/tests/lifecycle/tgz-guard.spec.ts +103 -0
- package/templates/api/tests/lifecycle/trust.spec.ts +310 -0
- package/templates/api/tests/lifecycle/uninstall.spec.ts +386 -0
- package/templates/api/tests/lifecycle/verify-checks.spec.ts +385 -0
- package/templates/api/tests/lifecycle/verify-report.spec.ts +290 -0
- package/templates/api/tests/rbac-settings.spec.ts +8 -0
- package/templates/init.include.yaml +56 -0
- package/templates/openapi.json +527 -0
- package/templates/package.json +52 -0
- package/templates/web/src/_platform/__client/api.schemas.ts +8 -0
- package/templates/web/src/_platform/__client/api.ts +335 -0
- package/templates/web/src/about/entry.ts +42 -0
- package/templates/web/src/about/locales/en-US.json +16 -0
- package/templates/web/src/about/locales/zh-CN.json +16 -0
- package/templates/web/src/about/pages/constants.ts +20 -0
- package/templates/web/src/about/pages/index.tsx +135 -0
- package/templates/web/src/access/entry.ts +74 -0
- package/templates/web/src/access/locales/en-US.json +46 -0
- package/templates/web/src/access/locales/zh-CN.json +46 -0
- package/templates/web/src/access/pages/access-mode/index.tsx +16 -0
- package/templates/web/src/access/pages/admin-visible/index.tsx +16 -0
- package/templates/web/src/access/pages/button-control/index.tsx +181 -0
- package/templates/web/src/access/pages/common-visible/index.tsx +16 -0
- package/templates/web/src/access/pages/page-control/index.tsx +142 -0
- package/templates/web/src/demo/__client/api.schemas.ts +20 -0
- package/templates/web/src/demo/__client/api.ts +89 -0
- package/templates/web/src/exception/entry.ts +80 -0
- package/templates/web/src/exception/locales/en-US.json +13 -0
- package/templates/web/src/exception/locales/zh-CN.json +13 -0
- package/templates/web/src/exception/pages/403/index.tsx +30 -0
- package/templates/web/src/exception/pages/404/index.tsx +30 -0
- package/templates/web/src/exception/pages/500/index.tsx +30 -0
- package/templates/web/src/exception/pages/unknown-component/index.tsx +38 -0
- package/templates/web/src/home/__client/api.schemas.ts +22 -0
- package/templates/web/src/home/__client/api.ts +113 -0
- package/templates/web/src/login/locales/en-US.json +6 -0
- package/templates/web/src/login/locales/zh-CN.json +6 -0
- package/templates/web/src/login/pages/login.tsx +67 -18
- package/templates/web/src/oj-market/__tests__/CardGrid.test.tsx +93 -0
- package/templates/web/src/oj-market/__tests__/HistoryDrawer.test.tsx +28 -0
- package/templates/web/src/oj-market/__tests__/InstallDialog.test.tsx +69 -0
- package/templates/web/src/oj-market/__tests__/RollbackDialog.test.tsx +43 -0
- package/templates/web/src/oj-market/__tests__/UninstallConfirm.test.tsx +30 -0
- package/templates/web/src/oj-market/__tests__/marketClient.fake.ts +70 -0
- package/templates/web/src/oj-market/__tests__/poll.test.ts +24 -0
- package/templates/web/src/oj-market/__tests__/render.tsx +14 -0
- package/templates/web/src/oj-market/__tests__/runtime-stub.ts +14 -0
- package/templates/web/src/oj-market/__tests__/semver.test.ts +26 -0
- package/templates/web/src/oj-market/api/MarketClientProvider.tsx +12 -0
- package/templates/web/src/oj-market/api/client.ts +135 -0
- package/templates/web/src/oj-market/api/context.ts +11 -0
- package/templates/web/src/oj-market/api/defaultClient.ts +7 -0
- package/templates/web/src/oj-market/components/CardGrid.tsx +76 -0
- package/templates/web/src/oj-market/components/CardItem.tsx +102 -0
- package/templates/web/src/oj-market/components/DetailDrawer.tsx +53 -0
- package/templates/web/src/oj-market/components/HistoryDrawer.tsx +75 -0
- package/templates/web/src/oj-market/components/InstallDialog.tsx +112 -0
- package/templates/web/src/oj-market/components/RollbackDialog.tsx +79 -0
- package/templates/web/src/oj-market/components/UninstallConfirm.tsx +74 -0
- package/templates/web/src/oj-market/entry.ts +27 -0
- package/templates/web/src/oj-market/hooks/useMarket.ts +91 -0
- package/templates/web/src/oj-market/locales/en-US.json +100 -0
- package/templates/web/src/oj-market/locales/zh-CN.json +100 -0
- package/templates/web/src/oj-market/pages/MarketApp.tsx +136 -0
- package/templates/web/src/oj-market/types.ts +84 -0
- package/templates/web/src/oj-market/utils/poll.ts +36 -0
- package/templates/web/src/oj-market/utils/semver.ts +14 -0
- package/templates/web/src/outside/entry.ts +80 -0
- package/templates/web/src/outside/locales/en-US.json +10 -0
- package/templates/web/src/outside/locales/zh-CN.json +10 -0
- package/templates/web/src/personal-center/entry.ts +1 -10
- package/templates/web/src/personal-center/locales/en-US.json +1 -5
- package/templates/web/src/personal-center/locales/zh-CN.json +1 -5
- package/templates/web/src/route-nest/entry.ts +73 -0
- package/templates/web/src/route-nest/locales/en-US.json +9 -0
- package/templates/web/src/route-nest/locales/zh-CN.json +9 -0
- package/templates/web/src/route-nest/pages/menu1/menu1-1/index.tsx +5 -0
- package/templates/web/src/route-nest/pages/menu1/menu1-2/index.tsx +5 -0
- package/templates/web/src/route-nest/pages/menu2/index.tsx +5 -0
- package/templates/web/src/system/__client/api.schemas.ts +12 -0
- package/templates/web/src/system/__client/api.ts +25 -1
- package/templates/web/src/system/pages/oplog/index.tsx +1 -1
- package/templates/web/src/system/pages/roles/index.tsx +1 -0
- package/templates/web/src/system/pages/settings/index.tsx +1 -1
- package/templates/web/src/system/pages/users/index.tsx +1 -1
- package/templates/web.config.ts +32 -40
- package/vendor/host-versions.json +3 -3
- package/templates/pnpm-workspace.yaml +0 -14
- package/templates/web/src/personal-center/pages/settings/index.tsx +0 -5
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import type { SolutionState, StateStore } from "../../src/_platform/_lifecycle/state";
|
|
2
|
+
import type { InstallContext } from "../../src/_platform/_lifecycle/steps/preflight";
|
|
3
|
+
// BDD:安装预检规则引擎(P1-5,R7/R8/R9/R24,十阶段状态机 stage 1)。
|
|
4
|
+
// 规则真源:docs/prd/solution/202610171254-lifecycle-manifest-contract.md(C2-01~33 安装列);
|
|
5
|
+
// 期望真源:packages/cli/src/lifecycle/__tests__/fixtures/(fixture-valid 零违规、fixture-invalid 七码齐报)。
|
|
6
|
+
// 实现纪律(契约 §0/夹具 README):收集全部违规后统一报出,不在首条违规停。
|
|
7
|
+
// 表驱动:每条规则一拒例 + 一过例;过例复用 fixture-valid 全字段合法清单,拒例仅做单点变异。
|
|
8
|
+
import { readFileSync } from "node:fs";
|
|
9
|
+
|
|
10
|
+
import { dirname, join } from "node:path";
|
|
11
|
+
import { fileURLToPath } from "node:url";
|
|
12
|
+
|
|
13
|
+
import { describe, expect, it } from "vitest";
|
|
14
|
+
import { parse } from "yaml";
|
|
15
|
+
import { preflight } from "../../src/_platform/_lifecycle/steps/preflight";
|
|
16
|
+
|
|
17
|
+
const FIXTURES = join(dirname(fileURLToPath(import.meta.url)), "../../../../src/lifecycle/__tests__/fixtures");
|
|
18
|
+
const VALID = parse(readFileSync(join(FIXTURES, "fixture-valid/manifest.yaml"), "utf8")) as any;
|
|
19
|
+
|
|
20
|
+
/** 宿主 versions.json 假数据(C2-09 真源键:@oj-module/runtime,shell/src/manifest.ts P7.6) */
|
|
21
|
+
const HOST_OK = { "@oj-module/runtime": "0.1.80" };
|
|
22
|
+
|
|
23
|
+
function stateStore(solutions: SolutionState["solutions"] = {}): StateStore {
|
|
24
|
+
return { read: () => ({ solutions: { ...solutions } }), write: async () => {} };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function ctx(overrides: Partial<InstallContext> = {}): InstallContext {
|
|
28
|
+
return {
|
|
29
|
+
manifest: structuredClone(VALID),
|
|
30
|
+
name: "demo-solution",
|
|
31
|
+
version: "0.1.0",
|
|
32
|
+
state: stateStore(),
|
|
33
|
+
hostVersions: HOST_OK,
|
|
34
|
+
hostApiVersion: "0.1.0", // C2-11 真源默认值(= market/api.ts API_VERSION;C2-11 describe 内按用例覆盖)
|
|
35
|
+
...overrides,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface RuleCase {
|
|
40
|
+
rule: string
|
|
41
|
+
clause: string
|
|
42
|
+
/** message 中除条款编号外必须出现的关键词(防「编号出现但语义报错」的假绿) */
|
|
43
|
+
keyword: string
|
|
44
|
+
mutate?: (m: any) => void
|
|
45
|
+
/** 拒例专用站点状态(占用检测注入已装方案) */
|
|
46
|
+
state?: SolutionState["solutions"]
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const RULES: RuleCase[] = [
|
|
50
|
+
{ rule: "name 白名单(大写拒绝)", clause: "C2-01", keyword: "name", mutate: (m) => { m.name = "Demo"; } },
|
|
51
|
+
{ rule: "name 保留名(market,R9)", clause: "C2-01", keyword: "保留名", mutate: (m) => { m.name = "market"; } },
|
|
52
|
+
{ rule: "version 禁 +(oj 相容排除,附录 B-1)", clause: "C2-02", keyword: "version", mutate: (m) => { m.version = "1.0.0+build.5"; } },
|
|
53
|
+
{
|
|
54
|
+
rule: "方案名已被已装方案占用(拒装)",
|
|
55
|
+
clause: "C2-13/14",
|
|
56
|
+
keyword: "占用",
|
|
57
|
+
state: { "demo-solution": { version: "0.1.0", enabled: true, installedAt: 1730000000000 } },
|
|
58
|
+
},
|
|
59
|
+
{ rule: "api.prefix 保留段(system)", clause: "C2-13", keyword: "保留", mutate: (m) => { m.api.prefix = "/api/system"; } },
|
|
60
|
+
{ rule: "web.route_prefix 保留段(system)", clause: "C2-14", keyword: "保留", mutate: (m) => { m.web.route_prefix = "/system"; } },
|
|
61
|
+
{ rule: "peerRuntime 宿主 runtime 不满足", clause: "C2-09", keyword: "不满足", mutate: (m) => { m.peerRuntime = ">=9.9.9"; } },
|
|
62
|
+
{ rule: "anonymous 越段(R8 段内收敛)", clause: "C2-26", keyword: "首段", mutate: (m) => { m.api.anonymous = ["system/health"]; } },
|
|
63
|
+
{ rule: "anonymous _system 禁入(语法层拒绝)", clause: "C2-26", keyword: "语法", mutate: (m) => { m.api.anonymous = ["_system/health"]; } },
|
|
64
|
+
{ rule: "anonymous 登录端点禁令(R8/S1)", clause: "C2-26", keyword: "登录", mutate: (m) => { m.api.anonymous = ["demo-solution/login"]; } },
|
|
65
|
+
{ rule: "路由 category 三值(D4,personal 已废弃)", clause: "C2-17", keyword: "category", mutate: (m) => { m.routes[0].category = "personal"; } },
|
|
66
|
+
{ rule: "路由 path 语法(参数段拒绝)", clause: "C2-17", keyword: "path", mutate: (m) => { m.routes[0].path = "/demo-solution/:id"; } },
|
|
67
|
+
{ rule: "required_permission 悬空(C2-21)", clause: "C2-21", keyword: "required_permission", mutate: (m) => { m.routes[1].required_permission = "demo-solution.absent"; } },
|
|
68
|
+
{ rule: "permission code 强制 <name>. 前缀", clause: "C2-15", keyword: "code", mutate: (m) => { m.permissions[0].code = "other.view"; } },
|
|
69
|
+
{ rule: "role permissions 悬空引用", clause: "C2-16", keyword: "悬空", mutate: (m) => { m.roles[0].permissions = ["demo-solution.absent"]; } },
|
|
70
|
+
{ rule: "设置组 label_key 必填", clause: "C2-22", keyword: "label_key", mutate: (m) => { delete m.settings.groups[0].label_key; } },
|
|
71
|
+
{ rule: "设置键所有权 moduleOf(key)===name(R24)", clause: "C2-23", keyword: "所有权", mutate: (m) => { m.settings.groups[0].items[0].key = "framework.site_title"; } },
|
|
72
|
+
{
|
|
73
|
+
rule: "type=enum 缺 options",
|
|
74
|
+
clause: "C2-24",
|
|
75
|
+
keyword: "options",
|
|
76
|
+
mutate: (m) => {
|
|
77
|
+
m.settings.groups[0].items[0].type = "enum";
|
|
78
|
+
delete m.settings.groups[0].items[0].options;
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
{ rule: "health 须为本方案路由或其子路径", clause: "C2-33", keyword: "路由", mutate: (m) => { m.health = "/other-thing"; } },
|
|
82
|
+
{ rule: "web.entry 版本段必须 === version", clause: "C2-27", keyword: "entry", mutate: (m) => { m.web.entry = "modules/demo-solution/9.9.9/entry.js"; } },
|
|
83
|
+
{ rule: "api.module 存在时必须 === name", clause: "C2-12", keyword: "api.module", mutate: (m) => { m.api.module = "other-solution"; } },
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
describe("preflight 规则表(每规则一拒例 + 一过例)", () => {
|
|
87
|
+
for (const c of RULES) {
|
|
88
|
+
it(`拒例【${c.clause}】${c.rule}`, async () => {
|
|
89
|
+
const m = structuredClone(VALID);
|
|
90
|
+
c.mutate?.(m);
|
|
91
|
+
const r = await preflight(ctx({ manifest: m, state: stateStore(c.state) }));
|
|
92
|
+
expect(r.stage).toBe("preflight");
|
|
93
|
+
expect(r.ok).toBe(false);
|
|
94
|
+
expect(r.retryable).toBe(false);
|
|
95
|
+
expect(r.message).toContain(c.clause);
|
|
96
|
+
expect(r.message).toContain(c.keyword);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it(`过例【${c.clause}】${c.rule}`, async () => {
|
|
100
|
+
const r = await preflight(ctx({ state: stateStore(c.state ? {} : c.state) }));
|
|
101
|
+
expect(r.ok).toBe(true);
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
describe("preflight · C2-09 peerRuntime 范围算子语义", () => {
|
|
107
|
+
it("过例:^ 同主版本且 ≥(宿主 0.1.80 满足 ^0.1.0)", async () => {
|
|
108
|
+
const m = structuredClone(VALID);
|
|
109
|
+
m.peerRuntime = "^0.1.0";
|
|
110
|
+
expect((await preflight(ctx({ manifest: m }))).ok).toBe(true);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("拒例:^ 主版本相同但低于基线(宿主 0.1.80 不满足 ^0.2.0)", async () => {
|
|
114
|
+
const m = structuredClone(VALID);
|
|
115
|
+
m.peerRuntime = "^0.2.0";
|
|
116
|
+
const r = await preflight(ctx({ manifest: m }));
|
|
117
|
+
expect(r.ok).toBe(false);
|
|
118
|
+
expect(r.message).toContain("C2-09");
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("拒例:精确声明与宿主不符(=0.9.9)", async () => {
|
|
122
|
+
const m = structuredClone(VALID);
|
|
123
|
+
m.peerRuntime = "=0.9.9";
|
|
124
|
+
const r = await preflight(ctx({ manifest: m }));
|
|
125
|
+
expect(r.ok).toBe(false);
|
|
126
|
+
expect(r.message).toContain("C2-09");
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("拒例:暂不支持的范围算子(<,P1 语义集外)", async () => {
|
|
130
|
+
const m = structuredClone(VALID);
|
|
131
|
+
m.peerRuntime = "<0.9.0";
|
|
132
|
+
const r = await preflight(ctx({ manifest: m }));
|
|
133
|
+
expect(r.ok).toBe(false);
|
|
134
|
+
expect(r.message).toContain("C2-09");
|
|
135
|
+
expect(r.message).toContain("暂不支持");
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("拒例:语法非法(非范围算子字符串)", async () => {
|
|
139
|
+
const m = structuredClone(VALID);
|
|
140
|
+
m.peerRuntime = "latest";
|
|
141
|
+
const r = await preflight(ctx({ manifest: m }));
|
|
142
|
+
expect(r.ok).toBe(false);
|
|
143
|
+
expect(r.message).toContain("C2-09");
|
|
144
|
+
expect(r.message).toContain("语法");
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("拒例:宿主 versions.json 缺失 → 强制比对 fail-closed", async () => {
|
|
148
|
+
const r = await preflight(ctx({ hostVersions: undefined }));
|
|
149
|
+
expect(r.ok).toBe(false);
|
|
150
|
+
expect(r.message).toContain("C2-09");
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("过例:peerRuntime 缺省 → 不做比对(C2-09 可选字段)", async () => {
|
|
154
|
+
const m = structuredClone(VALID);
|
|
155
|
+
delete m.peerRuntime;
|
|
156
|
+
expect((await preflight(ctx({ manifest: m, hostVersions: undefined }))).ok).toBe(true);
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
describe("preflight · C2-11 peerPlatform 范围算子语义(^ 遵循 0.x 口径)", () => {
|
|
161
|
+
const HOST = "0.1.80";
|
|
162
|
+
it("过例:^ 同主版本且 ≥(宿主 0.1.80 满足 ^0.1.0)", async () => {
|
|
163
|
+
const m = structuredClone(VALID);
|
|
164
|
+
m.peerPlatform = "^0.1.0";
|
|
165
|
+
expect((await preflight(ctx({ manifest: m, hostApiVersion: HOST }))).ok).toBe(true);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("拒例:^ 主版本相同但低于基线(宿主 0.1.80 不满足 ^0.2.0)", async () => {
|
|
169
|
+
const m = structuredClone(VALID);
|
|
170
|
+
m.peerPlatform = "^0.2.0";
|
|
171
|
+
const r = await preflight(ctx({ manifest: m, hostApiVersion: HOST }));
|
|
172
|
+
expect(r.ok).toBe(false);
|
|
173
|
+
expect(r.message).toContain("C2-11");
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("拒例:0.x 口径——^0.1.0 不兼容 0.2.x(minor 不同即不兼容)", async () => {
|
|
177
|
+
const m = structuredClone(VALID);
|
|
178
|
+
m.peerPlatform = "^0.1.0";
|
|
179
|
+
const r = await preflight(ctx({ manifest: m, hostApiVersion: "0.2.3" }));
|
|
180
|
+
expect(r.ok).toBe(false);
|
|
181
|
+
expect(r.message).toContain("C2-11");
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it("过例:0.x 口径——^0.2.0 兼容 0.2.5(minor 相同,patch 更高)", async () => {
|
|
185
|
+
const m = structuredClone(VALID);
|
|
186
|
+
m.peerPlatform = "^0.2.0";
|
|
187
|
+
expect((await preflight(ctx({ manifest: m, hostApiVersion: "0.2.5" }))).ok).toBe(true);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("过例:>= 跨 minor 满足(宿主 0.2.3 满足 >=0.1.0)", async () => {
|
|
191
|
+
const m = structuredClone(VALID);
|
|
192
|
+
m.peerPlatform = ">=0.1.0";
|
|
193
|
+
expect((await preflight(ctx({ manifest: m, hostApiVersion: "0.2.3" }))).ok).toBe(true);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it("拒例:精确声明与宿主不符(=0.1.0 vs 宿主 0.1.80)", async () => {
|
|
197
|
+
const m = structuredClone(VALID);
|
|
198
|
+
m.peerPlatform = "=0.1.0";
|
|
199
|
+
const r = await preflight(ctx({ manifest: m, hostApiVersion: HOST }));
|
|
200
|
+
expect(r.ok).toBe(false);
|
|
201
|
+
expect(r.message).toContain("C2-11");
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("拒例:暂不支持的范围算子(<,P1 语义集外)", async () => {
|
|
205
|
+
const m = structuredClone(VALID);
|
|
206
|
+
m.peerPlatform = "<0.9.0";
|
|
207
|
+
const r = await preflight(ctx({ manifest: m, hostApiVersion: HOST }));
|
|
208
|
+
expect(r.ok).toBe(false);
|
|
209
|
+
expect(r.message).toContain("C2-11");
|
|
210
|
+
expect(r.message).toContain("暂不支持");
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it("拒例:语法非法(非范围算子字符串)", async () => {
|
|
214
|
+
const m = structuredClone(VALID);
|
|
215
|
+
m.peerPlatform = "latest";
|
|
216
|
+
const r = await preflight(ctx({ manifest: m, hostApiVersion: HOST }));
|
|
217
|
+
expect(r.ok).toBe(false);
|
|
218
|
+
expect(r.message).toContain("C2-11");
|
|
219
|
+
expect(r.message).toContain("语法");
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it("拒例:宿主 API 版本未注入 → 强制比对 fail-closed", async () => {
|
|
223
|
+
const m = structuredClone(VALID);
|
|
224
|
+
m.peerPlatform = "^0.1.0";
|
|
225
|
+
const r = await preflight(ctx({ manifest: m, hostApiVersion: undefined }));
|
|
226
|
+
expect(r.ok).toBe(false);
|
|
227
|
+
expect(r.message).toContain("C2-11");
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it("过例:peerPlatform 缺省 → 不做比对(C2-11 可选字段)", async () => {
|
|
231
|
+
const m = structuredClone(VALID);
|
|
232
|
+
delete m.peerPlatform;
|
|
233
|
+
expect((await preflight(ctx({ manifest: m, hostApiVersion: undefined }))).ok).toBe(true);
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
describe("preflight · C2 夹具(双端唯一真源)", () => {
|
|
238
|
+
it("fixture-valid:零违规通过", async () => {
|
|
239
|
+
const r = await preflight(ctx({}));
|
|
240
|
+
expect(r.ok).toBe(true);
|
|
241
|
+
expect(r.stage).toBe("preflight");
|
|
242
|
+
expect(r.message).toContain("预检通过");
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it("fixture-invalid:七码齐报且全量列出(C2-01/02/16/17/22/23/26)", async () => {
|
|
246
|
+
const m = parse(readFileSync(join(FIXTURES, "fixture-invalid/manifest.yaml"), "utf8"));
|
|
247
|
+
const r = await preflight(ctx({ manifest: m, name: "Demo", version: "1.0.0+build.5" }));
|
|
248
|
+
expect(r.ok).toBe(false);
|
|
249
|
+
expect(r.retryable).toBe(false);
|
|
250
|
+
// 七码同现于同一条 message = 「收集全部违规后统一报出」纪律的实现证据
|
|
251
|
+
for (const code of ["C2-01", "C2-02", "C2-16", "C2-17", "C2-22", "C2-23", "C2-26"]) {
|
|
252
|
+
expect(r.message).toContain(code);
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
});
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
// BDD:注册步骤(P1-8,十阶段 stage registering,D10 唯一出口)。
|
|
2
|
+
// 安装 = registerSolution 一次性把方案声明的 RBAC/设置/路由 upsert 进宿主库:
|
|
3
|
+
// sys_role/sys_permission 经 upsertSolutionPerms 出口(AC2-1~6),sys_setting/sys_solution_route
|
|
4
|
+
// 在本步收敛;声明消失 → 行保留标 deprecated=1(镜像 solution-perms),管理员改过的
|
|
5
|
+
// value 永不被声明覆写(AC1-15 同纪律)。DB 级路由占用检查承接 P1-5 备案③(首段比较)。
|
|
6
|
+
// queryRegistered 归一化快照(过滤 deprecated)供 P1-9 冒烟对账;auditOp 落 solution.register。
|
|
7
|
+
// 夹具即真实库:真实迁移建表(0010 重构 sys_solution_route + sys_setting 补 deprecated)。
|
|
8
|
+
/* eslint-disable test/prefer-lowercase-title -- 任务/AC 编号是验收锚点,保留大写(DB/P1-5) */
|
|
9
|
+
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
|
|
10
|
+
|
|
11
|
+
import { checkOccupancy, queryAllRegistered, queryRegistered, registerSolution } from "../../src/_platform/_lifecycle/steps/register";
|
|
12
|
+
import { installDb } from "../fixture";
|
|
13
|
+
|
|
14
|
+
let dbo: ReturnType<typeof installDb>;
|
|
15
|
+
const g = globalThis as any;
|
|
16
|
+
|
|
17
|
+
beforeAll(() => {
|
|
18
|
+
dbo = installDb();
|
|
19
|
+
g.http = {}; // 安装器系统侧:无 http 会话 → audit username='system'
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
g.http = {};
|
|
24
|
+
dbo.exec("DELETE FROM sys_role_permission");
|
|
25
|
+
dbo.exec("DELETE FROM sys_user_role");
|
|
26
|
+
dbo.exec("DELETE FROM sys_permission");
|
|
27
|
+
dbo.exec("DELETE FROM sys_role");
|
|
28
|
+
dbo.exec("DELETE FROM sys_solution_route");
|
|
29
|
+
dbo.exec("DELETE FROM sys_setting");
|
|
30
|
+
dbo.exec("DELETE FROM sys_oplog");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
function rows(sql: string) {
|
|
34
|
+
return dbo.prepare(sql).all() as Record<string, unknown>[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function counts() {
|
|
38
|
+
const c = (t: string) => Number((rows(`SELECT COUNT(*) AS n FROM ${t}`)[0]).n);
|
|
39
|
+
return { role: c("sys_role"), perm: c("sys_permission"), setting: c("sys_setting"), route: c("sys_solution_route") };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** wiki 1.0.0 声明切片:两角色一(含两权限点)、两设置项、两路由 */
|
|
43
|
+
const WIKI_V1 = {
|
|
44
|
+
module: "wiki",
|
|
45
|
+
version: "1.0.0",
|
|
46
|
+
roles: [
|
|
47
|
+
{ name: "wiki.admin", title: "管理员", permissions: ["wiki.doc.create", "wiki.space.create"] },
|
|
48
|
+
],
|
|
49
|
+
settings: [
|
|
50
|
+
{ key: "wiki.home.page", group: "wiki", type: "string", default_value: "start", label_key: "wiki:settings.home" },
|
|
51
|
+
{ key: "wiki.readonly", group: "wiki", type: "bool", default_value: "0" },
|
|
52
|
+
],
|
|
53
|
+
routes: [
|
|
54
|
+
{ path: "/wiki/docs", category: "main", order: 200, title_key: "wiki:menu.docs" },
|
|
55
|
+
{ path: "/wiki/admin", category: "admin", order: 300, title_key: "wiki:menu.admin", required_permission: "wiki.doc.create" },
|
|
56
|
+
],
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
describe("注册后四表行存在", () => {
|
|
60
|
+
it("注册一次落齐 sys_role/sys_permission/sys_setting/sys_solution_route,deprecated 为空", async () => {
|
|
61
|
+
const r = await registerSolution(WIKI_V1);
|
|
62
|
+
expect(r.ok).toBe(true);
|
|
63
|
+
expect(r.deprecated).toEqual([]);
|
|
64
|
+
expect(rows("SELECT * FROM sys_role WHERE name = 'wiki.admin'")).toHaveLength(1);
|
|
65
|
+
expect(rows("SELECT * FROM sys_permission")).toHaveLength(2);
|
|
66
|
+
const setting = rows("SELECT * FROM sys_setting WHERE key = 'wiki.home.page'")[0];
|
|
67
|
+
expect(setting.value).toBe("start"); // 首装 value = default_value
|
|
68
|
+
expect(setting.module).toBe("wiki");
|
|
69
|
+
expect(setting.label_key).toBe("wiki:settings.home");
|
|
70
|
+
const route = rows("SELECT * FROM sys_solution_route WHERE path = '/wiki/admin'")[0];
|
|
71
|
+
expect(route.module).toBe("wiki");
|
|
72
|
+
expect(route.category).toBe("admin");
|
|
73
|
+
expect(route.required_permission).toBe("wiki.doc.create");
|
|
74
|
+
expect(Number(route.deprecated)).toBe(0);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe("重跑幂等", () => {
|
|
79
|
+
it("同 decl 跑两遍:四表行数不变;管理员改过的 value 不被覆写(AC1-15 同纪律),定义列照常更新", async () => {
|
|
80
|
+
await registerSolution(WIKI_V1);
|
|
81
|
+
dbo.exec("UPDATE sys_setting SET value = 'admin-changed' WHERE key = 'wiki.home.page'");
|
|
82
|
+
const before = counts();
|
|
83
|
+
const r = await registerSolution({
|
|
84
|
+
...WIKI_V1,
|
|
85
|
+
settings: [{ ...WIKI_V1.settings[0], default_value: "start-v2" }, WIKI_V1.settings[1]],
|
|
86
|
+
});
|
|
87
|
+
expect(r.ok).toBe(true);
|
|
88
|
+
expect(r.deprecated).toEqual([]);
|
|
89
|
+
expect(counts()).toEqual(before);
|
|
90
|
+
expect(rows("SELECT value, default_value FROM sys_setting WHERE key = 'wiki.home.page'")[0]).toEqual({ value: "admin-changed", default_value: "start-v2" });
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe("deprecated 语义(镜像 solution-perms AC2-3/4)", () => {
|
|
95
|
+
it("声明消失的 route/setting 旧行保留标 deprecated=1 并汇入清单;queryRegistered 快照不再含", async () => {
|
|
96
|
+
await registerSolution(WIKI_V1);
|
|
97
|
+
const r = await registerSolution({
|
|
98
|
+
...WIKI_V1,
|
|
99
|
+
version: "1.1.0",
|
|
100
|
+
roles: [{ name: "wiki.admin", title: "管理员", permissions: ["wiki.doc.create"] }], // wiki.space.create 消失
|
|
101
|
+
settings: [WIKI_V1.settings[0]], // wiki.readonly 消失
|
|
102
|
+
routes: [WIKI_V1.routes[0]], // /wiki/admin 消失
|
|
103
|
+
});
|
|
104
|
+
expect(r.ok).toBe(true);
|
|
105
|
+
expect(r.deprecated).toEqual(["wiki.space.create", "wiki.readonly", "/wiki/admin"]);
|
|
106
|
+
// 旧行全保留且标 deprecated(判决不突变)
|
|
107
|
+
expect(Number(rows("SELECT deprecated FROM sys_permission WHERE code = 'wiki.space.create'")[0].deprecated)).toBe(1);
|
|
108
|
+
expect(rows("SELECT * FROM sys_setting WHERE key = 'wiki.readonly'")).toHaveLength(1);
|
|
109
|
+
expect(Number(rows("SELECT deprecated FROM sys_setting WHERE key = 'wiki.readonly'")[0].deprecated)).toBe(1);
|
|
110
|
+
expect(rows("SELECT * FROM sys_solution_route WHERE path = '/wiki/admin'")).toHaveLength(1);
|
|
111
|
+
expect(Number(rows("SELECT deprecated FROM sys_solution_route WHERE path = '/wiki/admin'")[0].deprecated)).toBe(1);
|
|
112
|
+
// 快照只含现行声明行
|
|
113
|
+
const snap = await queryRegistered("wiki");
|
|
114
|
+
expect(snap?.routes).toEqual(["/wiki/docs"]);
|
|
115
|
+
expect(snap?.settings).toEqual(["wiki.home.page"]);
|
|
116
|
+
expect(snap?.permissions).toEqual(["wiki.doc.create"]);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
describe("DB 级路由占用(P1-5 备案③承接)", () => {
|
|
121
|
+
it("module B 声明 module A 已注册的首段 → ok=false 含占用者,A 的行原样", async () => {
|
|
122
|
+
await registerSolution({ module: "alpha", roles: [], routes: [{ path: "/alpha/x", category: "main", order: 200, title_key: "alpha:menu.x" }] });
|
|
123
|
+
const before = rows("SELECT * FROM sys_solution_route");
|
|
124
|
+
const r = await registerSolution({ module: "beta", roles: [], routes: [{ path: "/alpha/y", category: "main", order: 200, title_key: "beta:menu.y" }] });
|
|
125
|
+
expect(r.ok).toBe(false);
|
|
126
|
+
expect(r.message).toContain("alpha");
|
|
127
|
+
expect(rows("SELECT * FROM sys_solution_route")).toEqual(before);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("checkOccupancy:同 module 重注册不报自占;他 module 首段冲突逐条报告", async () => {
|
|
131
|
+
await registerSolution({ module: "alpha", roles: [], routes: [{ path: "/alpha/x", category: "main", order: 200, title_key: "alpha:menu.x" }] });
|
|
132
|
+
expect(await checkOccupancy("alpha", ["/alpha/x", "/alpha/z"])).toEqual([]);
|
|
133
|
+
expect(await checkOccupancy("beta", ["/alpha/y", "/beta/own"])).toEqual(["/alpha/y"]);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
describe("queryRegistered:冒烟对账快照", () => {
|
|
138
|
+
it("未注册返回 null;注册后四清单全排序、module 归一", async () => {
|
|
139
|
+
expect(await queryRegistered("nosuch")).toBeNull();
|
|
140
|
+
await registerSolution({
|
|
141
|
+
module: "sort",
|
|
142
|
+
roles: [{ name: "sort.b", title: "B" }, { name: "sort.a", title: "A" }],
|
|
143
|
+
settings: [
|
|
144
|
+
{ key: "sort.k2", group: "sort", type: "string", default_value: "2" },
|
|
145
|
+
{ key: "sort.k1", group: "sort", type: "string", default_value: "1" },
|
|
146
|
+
],
|
|
147
|
+
routes: [
|
|
148
|
+
{ path: "/sort/z", category: "hidden", order: 200 },
|
|
149
|
+
{ path: "/sort/m", category: "main", order: 201, title_key: "sort:menu.m" },
|
|
150
|
+
],
|
|
151
|
+
});
|
|
152
|
+
expect(await queryRegistered("sort")).toEqual({
|
|
153
|
+
module: "sort",
|
|
154
|
+
routes: ["/sort/m", "/sort/z"],
|
|
155
|
+
settings: ["sort.k1", "sort.k2"],
|
|
156
|
+
roles: ["sort.a", "sort.b"],
|
|
157
|
+
permissions: [],
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
describe("queryAllRegistered:全方案快照清单(P1-14 读端点辅助)", () => {
|
|
163
|
+
it("四表并集取模块名,按 module 排序逐个快照;无任何行 → 空数组", async () => {
|
|
164
|
+
expect(await queryAllRegistered()).toEqual([]);
|
|
165
|
+
await registerSolution({ module: "b-sol", roles: [], settings: [], routes: [{ path: "/b", category: "main", order: 200 }] });
|
|
166
|
+
await registerSolution({ module: "a-sol", roles: [], settings: [{ key: "a.k", group: "g", type: "string" }], routes: [] });
|
|
167
|
+
const all = await queryAllRegistered();
|
|
168
|
+
expect(all.map(s => s.module)).toEqual(["a-sol", "b-sol"]);
|
|
169
|
+
expect(all.find(s => s.module === "a-sol")).toMatchObject({ settings: ["a.k"], routes: [] });
|
|
170
|
+
expect(all.find(s => s.module === "b-sol")).toMatchObject({ routes: ["/b"] });
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("deprecated 行不计入存在性:唯一路由被声明消失标 deprecated 后方案不再列出", async () => {
|
|
174
|
+
await registerSolution({ module: "gone-sol", roles: [], settings: [], routes: [{ path: "/gone", category: "main", order: 200 }] });
|
|
175
|
+
// 重注册去掉 /gone → 旧行 deprecated=1,方案再无任何非 deprecated 行
|
|
176
|
+
await registerSolution({ module: "gone-sol", roles: [], settings: [], routes: [] });
|
|
177
|
+
const all = await queryAllRegistered();
|
|
178
|
+
expect(all.map(s => s.module)).toEqual([]);
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
describe("审计:auditOp 落 solution.register", () => {
|
|
183
|
+
it("注册成功落 oplog 一条(系统侧 username=system,含版本与 deprecated 明细)", async () => {
|
|
184
|
+
await registerSolution(WIKI_V1);
|
|
185
|
+
await vi.waitFor(() => expect(rows("SELECT * FROM sys_oplog")).toHaveLength(1));
|
|
186
|
+
const r = rows("SELECT * FROM sys_oplog")[0];
|
|
187
|
+
expect(r.action).toBe("solution.register");
|
|
188
|
+
expect(r.module).toBe("wiki");
|
|
189
|
+
expect(r.version).toBe("1.0.0");
|
|
190
|
+
expect(r.target).toBe("wiki");
|
|
191
|
+
expect(r.username).toBe("system");
|
|
192
|
+
expect(JSON.parse(String(r.detail))).toEqual({ routes: 2, settings: 2, deprecated: [] });
|
|
193
|
+
});
|
|
194
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// BDD:semver 比较器(R25,P1-1)。纯字符串解析、无依赖。
|
|
2
|
+
// 语义边界:数字段数值比较;prerelease 标识符数字按数值、字母按字典序、数字 < 字母;
|
|
3
|
+
// 无 prerelease > 有 prerelease;「1.0」「1.0.0」「1.0.0.0」视为相等(安装器容错口径)。
|
|
4
|
+
import { describe, expect, it } from "vitest";
|
|
5
|
+
|
|
6
|
+
import { compareVersions } from "../../src/_platform/_lifecycle/semver";
|
|
7
|
+
|
|
8
|
+
const cases: Array<[a: string, b: string, expected: -1 | 0 | 1]> = [
|
|
9
|
+
// 主版本数值比较(非字典序)
|
|
10
|
+
["0.1.10", "0.1.9", 1],
|
|
11
|
+
["0.1.9", "0.1.10", -1],
|
|
12
|
+
["2.0.0", "10.0.0", -1],
|
|
13
|
+
// 相等
|
|
14
|
+
["1.0.0", "1.0.0", 0],
|
|
15
|
+
// 长度容错:缺省段视为 0
|
|
16
|
+
["1.0", "1.0.0", 0],
|
|
17
|
+
["1.0.0", "1.0.0.0", 0],
|
|
18
|
+
["1", "1.0.0", 0],
|
|
19
|
+
// prerelease:有 prerelease < 无 prerelease
|
|
20
|
+
["1.0.0-alpha", "1.0.0", -1],
|
|
21
|
+
["1.0.0", "1.0.0-alpha", 1],
|
|
22
|
+
// prerelease 数字标识符按数值比较
|
|
23
|
+
["1.0.0-alpha.2", "1.0.0-alpha.10", -1],
|
|
24
|
+
// prerelease 字母标识符按字典序
|
|
25
|
+
["1.0.0-alpha", "1.0.0-beta", -1],
|
|
26
|
+
// prerelease 数字 < 字母
|
|
27
|
+
["1.0.0-1", "1.0.0-alpha", -1],
|
|
28
|
+
// prerelease 标识符多的 > 标识符少的(前缀相同)
|
|
29
|
+
["1.0.0-alpha", "1.0.0-alpha.1", -1],
|
|
30
|
+
// 标识符内部数字段仍按数值:alpha.9 < alpha.10
|
|
31
|
+
["1.0.0-alpha.9", "1.0.0-alpha.10", -1],
|
|
32
|
+
// 多连字符 prerelease:首个 - 后整体为标识符串,不得截断静默判等
|
|
33
|
+
["1.0.0-alpha-beta", "1.0.0-alpha", 1],
|
|
34
|
+
["1.0.0-alpha-beta", "1.0.0-alpha.2", 1],
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
describe("compareVersions(R25)", () => {
|
|
38
|
+
for (const [a, b, expected] of cases) {
|
|
39
|
+
it(`${a} vs ${b} → ${expected}`, () => {
|
|
40
|
+
expect(compareVersions(a, b)).toBe(expected);
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// BDD:纯 JS SHA-384(B-11 真机补面)。oj isolate 无 sha384 原语而契约 C2-28 钉死
|
|
2
|
+
// `sha384-<base64>`——安装器(isolate 内)必须与 ojm build(Node 侧)同算法重算比对。
|
|
3
|
+
// 向量真源 = FIPS 180-4 官方样例 + node:crypto 交叉验证(长度跨 0/128 字节块边界)。
|
|
4
|
+
import { Buffer } from "node:buffer";
|
|
5
|
+
import { createHash } from "node:crypto";
|
|
6
|
+
import { describe, expect, it } from "vitest";
|
|
7
|
+
import { bytesToBase64, sha384B64, sha384Bytes } from "../../src/_platform/_lifecycle/sha384";
|
|
8
|
+
|
|
9
|
+
const hex = (bytes: Uint8Array): string => Array.from(bytes, b => b.toString(16).padStart(2, "0")).join("");
|
|
10
|
+
|
|
11
|
+
describe("sha384Bytes(FIPS 180-4 向量)", () => {
|
|
12
|
+
it("空串", () => {
|
|
13
|
+
expect(hex(sha384Bytes(new Uint8Array(0)))).toBe("38b060a751ac96384cd9327eb1b1e36a21fdb71114be07434c0cc7bf63f6e1da274edebfe76f65fbd51ad2f14898b95b");
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("\"abc\"(官方样例)", () => {
|
|
17
|
+
expect(hex(sha384Bytes(new TextEncoder().encode("abc")))).toBe("cb00753f45a35e8bb5a03d699ac65007272c32ab0eded1631a8b605a43ff5bed8086072ba1e7cc2358baeca134c825a7");
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("双块以上长输入(1129 字节伪随机)与 node:crypto 逐字一致", () => {
|
|
21
|
+
const data = new Uint8Array(1129);
|
|
22
|
+
for (let i = 0; i < data.length; i++) {
|
|
23
|
+
data[i] = (i * 131 + 7) & 0xFF;
|
|
24
|
+
}
|
|
25
|
+
const expected = createHash("sha384").update(data).digest("hex");
|
|
26
|
+
expect(hex(sha384Bytes(data))).toBe(expected);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("块边界邻域(127/128/129/255/256 字节)与 node:crypto 交叉一致", () => {
|
|
30
|
+
for (const len of [1, 63, 64, 111, 112, 113, 127, 128, 129, 255, 256, 257]) {
|
|
31
|
+
const data = new Uint8Array(len).fill(0xA5);
|
|
32
|
+
expect(hex(sha384Bytes(data)), `len=${len}`).toBe(createHash("sha384").update(data).digest("hex"));
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe("sha384B64(C2-28 integrity 形态)", () => {
|
|
38
|
+
it("与 node:crypto 的 base64 摘要拼接一致:sha384-<b64>", () => {
|
|
39
|
+
const data = new TextEncoder().encode("demo-solution entry.js");
|
|
40
|
+
const expected = `sha384-${createHash("sha384").update(data).digest("base64")}`;
|
|
41
|
+
expect(sha384B64(data)).toBe(expected);
|
|
42
|
+
expect(sha384B64(data)).toMatch(/^sha384-[A-Za-z0-9+/]{64}$/);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("bytesToBase64 与 Buffer base64 一致", () => {
|
|
46
|
+
expect(bytesToBase64(new Uint8Array([0, 1, 2, 250, 251]))).toBe(Buffer.from([0, 1, 2, 250, 251]).toString("base64"));
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// 签名链夹具 templates 面断言(P2-0,契约 v1.3 §2.11/§2.12 + 附录 A.2)。
|
|
2
|
+
// 与 CLI 面(packages/cli/src/lifecycle/__tests__/sig-fixtures.test.ts)共用同一组提交产物:
|
|
3
|
+
// 本 spec 证明夹具从 templates 测试 root 可达、可再生且字节一致(双端三同一的夹具侧锚点)。
|
|
4
|
+
// isolate 生产面(steps/verify.ts + CryptoFace.verifyEd25519)对同组夹具的六码/TOFU 断言属 P2-3。
|
|
5
|
+
import { createHash } from "node:crypto";
|
|
6
|
+
import { readFileSync } from "node:fs";
|
|
7
|
+
import os from "node:os";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
10
|
+
import { describe, expect, it } from "vitest";
|
|
11
|
+
|
|
12
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
const SIG_DIR = path.resolve(HERE, "../../../../src/lifecycle/__tests__/fixtures/sig");
|
|
14
|
+
const GEN = path.join(SIG_DIR, "gen-signed-fixtures.mjs");
|
|
15
|
+
|
|
16
|
+
const HEX64 = /^[0-9a-f]{64}$/;
|
|
17
|
+
const STD_B64 = /^[A-Z0-9+/]+={0,2}$/i;
|
|
18
|
+
const KEY_ID = /^[a-z][a-z0-9-]*\.[a-z0-9-]+$/;
|
|
19
|
+
|
|
20
|
+
const gen = (await import(GEN)) as {
|
|
21
|
+
FIXTURE_NAME: string
|
|
22
|
+
FIXTURE_VERSION: string
|
|
23
|
+
CANON: string
|
|
24
|
+
canonicalString: (name: string, version: string, sha256: string) => string
|
|
25
|
+
fingerprintOf: (publicKeyHex: string) => string
|
|
26
|
+
generate: (outDir: string) => { sha256: string, canonical: string, paths: Record<string, string> }
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
describe("签名链夹具 · templates 面可达性与再生一致性(P2-0)", () => {
|
|
30
|
+
const name = gen.FIXTURE_NAME;
|
|
31
|
+
const version = gen.FIXTURE_VERSION;
|
|
32
|
+
const tgzPath = path.join(SIG_DIR, `${name}-${version}.tgz`);
|
|
33
|
+
const tgz = readFileSync(tgzPath);
|
|
34
|
+
const sha256 = createHash("sha256").update(tgz).digest("hex");
|
|
35
|
+
|
|
36
|
+
it("三枚 .sig 从 templates 测试 root 可达且字段形态合法", () => {
|
|
37
|
+
for (const suffix of [".sig", ".bad.sig", ".unknown.sig"]) {
|
|
38
|
+
const sig = JSON.parse(readFileSync(`${tgzPath}${suffix}`, "utf8")) as Record<string, string>;
|
|
39
|
+
expect(sig.alg).toBe("Ed25519");
|
|
40
|
+
expect(sig.canon).toBe(gen.CANON);
|
|
41
|
+
expect(sig.public_key).toMatch(HEX64);
|
|
42
|
+
expect(sig.value).toMatch(STD_B64);
|
|
43
|
+
expect(sig.value).not.toMatch(/[-_]/);
|
|
44
|
+
expect(sig.key_id).toMatch(KEY_ID);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("规范串与指纹从 templates 面计算与提交产物一致(三同一的夹具侧)", () => {
|
|
49
|
+
const canonical = gen.canonicalString(name, version, sha256);
|
|
50
|
+
expect(canonical).toBe(`${name}@${version}:${sha256}`);
|
|
51
|
+
const sig = JSON.parse(readFileSync(`${tgzPath}.sig`, "utf8")) as { public_key: string };
|
|
52
|
+
expect(gen.fingerprintOf(sig.public_key)).toMatch(HEX64);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("生成器跨 root 再生到 tmp 与提交产物字节一致(双端共用同一生成逻辑)", () => {
|
|
56
|
+
const out = path.join(os.tmpdir(), `ojm-sig-fixtures-tpl-${process.pid}`);
|
|
57
|
+
const r = gen.generate(out);
|
|
58
|
+
expect(r.sha256).toBe(sha256);
|
|
59
|
+
expect(r.canonical).toBe(`${name}@${version}:${sha256}`);
|
|
60
|
+
expect(readFileSync(r.paths.tgz).equals(tgz)).toBe(true);
|
|
61
|
+
expect(readFileSync(r.paths.good, "utf8")).toBe(readFileSync(`${tgzPath}.sig`, "utf8"));
|
|
62
|
+
expect(readFileSync(r.paths.bad, "utf8")).toBe(readFileSync(`${tgzPath}.bad.sig`, "utf8"));
|
|
63
|
+
expect(readFileSync(r.paths.unknown, "utf8")).toBe(readFileSync(`${tgzPath}.unknown.sig`, "utf8"));
|
|
64
|
+
});
|
|
65
|
+
});
|