@oj-module/cli 0.2.68 → 0.2.71
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 +10 -8
- package/devkit/framework-dev.md +1 -1
- package/devkit/manual.md +45 -19
- package/devkit/module-dev-and-cdn-playground.md +20 -14
- package/devkit/publish-walkthrough.md +132 -43
- package/devkit/wecom-customer-reauth-guide.md +110 -0
- package/package.json +2 -2
- package/shell-dist/assets/{runtime.1xzahckt10.js → runtime.7x99hmm020.js} +2 -2
- package/shell-dist/bundles/oj-market-0.2.0.tgz +0 -0
- package/shell-dist/bundles/oj-market-0.2.0.tgz.sig +8 -0
- package/shell-dist/index.html +4 -4
- package/shell-dist/versions.json +3 -3
- package/src/__tests__/list.test.ts +7 -7
- package/src/__tests__/oj-health-host.test.ts +19 -0
- package/src/__tests__/site-tenant-header.test.ts +2 -2
- package/src/__tests__/templates-builtin-list.test.ts +47 -0
- package/src/__tests__/templates-schema-columns.test.ts +11 -2
- package/src/__tests__/templates-tenant-scope.test.ts +2 -3
- package/src/__tests__/users-roles-display.test.ts +84 -0
- package/src/bundle.ts +1 -1
- package/src/contract/__tests__/market-client.integration.test.ts +23 -22
- package/src/enable.ts +2 -2
- package/src/init.ts +45 -0
- package/src/install.ts +5 -5
- package/src/lifecycle/__tests__/install.test.ts +29 -29
- package/src/lifecycle/__tests__/market-bundle.test.ts +87 -0
- package/src/lifecycle/__tests__/publish.test.ts +8 -10
- package/src/lifecycle/__tests__/trust.test.ts +14 -14
- package/src/lifecycle/__tests__/uninstall.test.ts +4 -4
- package/src/list.ts +3 -3
- package/src/market/__tests__/bootstrap.test.ts +24 -7
- package/src/market/__tests__/failure-playbook.test.ts +0 -1
- package/src/market/__tests__/remote-push.test.ts +228 -0
- package/src/market/__tests__/verify.test.ts +45 -207
- package/src/market/bootstrap.ts +12 -7
- package/src/market/command.ts +33 -35
- package/src/market/db-seed.ts +129 -0
- package/src/market/failure-playbook.ts +3 -9
- package/src/market/fixture.ts +2 -6
- package/src/market/publish.ts +145 -89
- package/src/market/remote-push.ts +236 -0
- package/src/market/trial.ts +2 -2
- package/src/market/verify.ts +17 -270
- package/src/market/walkthrough.ts +38 -63
- package/src/oj.ts +13 -2
- package/src/preview.ts +5 -3
- package/src/publish.ts +3 -2
- package/src/scaffold-guard.ts +4 -0
- package/src/site.ts +1 -1
- package/src/trust.ts +8 -8
- package/src/uninstall.ts +2 -2
- package/src/usage.ts +23 -21
- package/templates/config.yaml +22 -3
- package/templates/global.d.ts +6 -4
- package/templates/init.include.yaml +16 -11
- package/templates/openapi.json +3971 -3663
- package/templates/src/.ojm-builtin.json +13 -0
- package/templates/src/README.md +5 -2
- package/templates/src/app/api/_platform/README.md +3 -2
- package/templates/src/app/api/_platform/_lifecycle/atomic.ts +7 -2
- package/templates/src/app/api/_platform/_lifecycle/bundle-converge.ts +158 -0
- package/templates/src/app/api/_platform/_lifecycle/history.ts +3 -2
- package/templates/src/app/api/_platform/_lifecycle/installer.ts +25 -5
- package/templates/src/app/api/_platform/_lifecycle/local-archive.ts +61 -0
- package/templates/src/app/api/_platform/_lifecycle/manifest-of-upload.ts +62 -0
- package/templates/src/app/api/_platform/_lifecycle/steps/preflight.ts +12 -2
- package/templates/src/app/api/_platform/_lifecycle/steps/reload.ts +4 -1
- package/templates/src/app/api/_platform/_lifecycle/steps/smoke.ts +8 -2
- package/templates/src/app/api/_platform/_lifecycle/types.ts +1 -1
- package/templates/src/app/api/_platform/_lifecycle/uninstall.ts +14 -5
- package/templates/src/app/api/_platform/_shared/service/domain/users.ts +7 -1
- package/templates/src/app/api/_platform/_shared/site-facade.ts +31 -0
- package/templates/src/app/api/_platform/manifest.yaml +1 -2
- package/templates/src/app/api/_platform/migrations/0003__market_catalog.mysql.sql +6 -0
- package/templates/src/app/api/_platform/migrations/0003__market_catalog.pg.sql +6 -0
- package/templates/src/app/api/_platform/migrations/0003__market_catalog.sql +18 -0
- package/templates/src/app/api/_platform/migrations/0004__market_drop_source_seq.mysql.sql +6 -0
- package/templates/src/app/api/_platform/migrations/0004__market_drop_source_seq.pg.sql +6 -0
- package/templates/src/app/api/_platform/migrations/0004__market_drop_source_seq.sql +6 -0
- package/templates/src/app/api/_platform/migrations/0005__market_sig_registry.mysql.sql +8 -0
- package/templates/src/app/api/_platform/migrations/0005__market_sig_registry.pg.sql +8 -0
- package/templates/src/app/api/_platform/migrations/0005__market_sig_registry.sql +8 -0
- package/templates/src/app/api/_platform/schema.yaml +8 -11
- package/templates/src/app/api/_platform/seed.mysql.sql +1 -1
- package/templates/src/app/api/_platform/seed.pg.sql +1 -1
- package/templates/src/app/api/_platform/seed.sql +1 -1
- package/templates/src/app/api/auth/_shared/wecom/sync.ts +6 -0
- package/templates/src/app/api/system/users/api.ts +3 -0
- package/templates/src/app/web/_platform/__client/api.schemas.ts +0 -113
- package/templates/src/app/web/_platform/__client/index.ts +1 -333
- package/templates/src/app/web/system/__client/api.schemas.ts +2 -0
- package/templates/src/app/web/system/locales/en-US.json +15 -1
- package/templates/src/app/web/system/locales/zh-CN.json +15 -1
- package/templates/src/app/web/system/pages/departments/sync-bar.tsx +9 -3
- package/templates/src/app/web/system/pages/roles/index.tsx +4 -8
- package/templates/src/app/web/system/pages/roles/role-label.ts +26 -0
- package/templates/src/app/web/system/pages/solutions/index.tsx +110 -0
- package/templates/src/app/web/system/pages/solutions/market-entry.ts +46 -0
- package/templates/src/app/web/system/pages/users/index.tsx +12 -2
- package/templates/src/app/web/system/pages/users/match-keyword.ts +19 -0
- package/templates/src/app/web/system/pages/users/roles.tsx +43 -14
- package/templates/src/app/web/system/routes.ts +18 -1
- package/templates/src/{app/api/_platform/market → oj-market/api}/api.ts +475 -592
- package/templates/src/oj-market/api/backfill.ts +221 -0
- package/templates/src/{app/api/_platform/market → oj-market/api}/bootstrap.ts +2 -2
- package/templates/src/oj-market/api/cdn/api.ts +312 -0
- package/templates/src/oj-market/api/cdn/shared.ts +104 -0
- package/templates/src/oj-market/api/cdn/signed-auth.ts +75 -0
- package/templates/src/oj-market/api/manifest.yaml +8 -0
- package/templates/src/oj-market/solution.yaml +21 -0
- package/templates/src/oj-market/web/__client/api.schemas.ts +167 -0
- package/templates/src/oj-market/web/__client/index.ts +509 -0
- package/templates/src/oj-market/web/__tests__/marketClient.fake.ts +6 -6
- package/templates/src/oj-market/web/api/client.ts +6 -6
- package/templates/src/oj-market/web/api/defaultClient.ts +31 -25
- package/templates/src/oj-market/web/entry.ts +5 -3
- package/templates/src/oj-market/web/hooks/useMarket.ts +6 -6
- package/templates/src/oj-market/web/pages/MarketApp.tsx +20 -3
- package/templates/src/oj-market/web/publish/locales/en-US.json +1 -1
- package/templates/src/oj-market/web/publish/locales/zh-CN.json +1 -1
- package/templates/src/oj-market/web/publish/pages/history.tsx +2 -11
- package/templates/src/oj-market/web/routes.ts +12 -0
- package/templates/src/oj-market/web/types.ts +14 -8
- package/templates/tests/lifecycle/anonymous-wiring.spec.ts +65 -0
- package/templates/tests/lifecycle/backfill.spec.ts +392 -0
- package/templates/tests/lifecycle/bootstrap.spec.ts +1 -1
- package/templates/tests/lifecycle/bundle-converge.spec.ts +264 -0
- package/templates/tests/lifecycle/cdn-upload.spec.ts +661 -0
- package/templates/tests/lifecycle/deploy-smoke.spec.ts +25 -0
- package/templates/tests/lifecycle/exec-grant.spec.ts +38 -0
- package/templates/tests/lifecycle/installer.spec.ts +36 -0
- package/templates/tests/lifecycle/market-api.spec.ts +448 -803
- package/templates/tests/lifecycle/preflight.spec.ts +18 -0
- package/templates/tests/lifecycle/trust.spec.ts +2 -2
- package/templates/tests/lifecycle/uninstall.spec.ts +36 -2
- package/templates/tests/lifecycle/verify-checks.spec.ts +1 -1
- package/templates/tests/rbac-users.spec.ts +8 -0
- package/templates/tests/solutions-entry.spec.ts +55 -0
- package/templates/tsconfig.paths.json +1 -0
- package/templates/types/stream-probes/stream-handlers.ts +34 -0
- package/templates/web.config.ts +6 -6
- package/vendor/host-versions.json +3 -3
- package/bin/oj +0 -4
- package/src/market/__tests__/build-index.test.ts +0 -641
- package/src/market/__tests__/sharding.test.ts +0 -90
- package/src/market/__tests__/yank-ledger.test.ts +0 -60
- package/src/market/build-index.ts +0 -526
- package/src/market/index-builder.ts +0 -149
- package/src/market/sharding.ts +0 -94
- package/src/market/yank-ledger.ts +0 -91
- package/templates/src/app/api/_platform/_lifecycle/source-seq.ts +0 -88
- package/templates/src/app/api/_platform/market/index-source.ts +0 -112
- package/templates/tests/lifecycle/source-seq.spec.ts +0 -55
package/src/enable.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// ojm enable/disable(P1-13,D8):生命周期开关瘦客户端——POST /api/
|
|
1
|
+
// ojm enable/disable(P1-13,D8):生命周期开关瘦客户端——POST /api/oj-market/enable
|
|
2
2
|
// 触发站点侧 setEnabled 四处同写(锁/sidecar/route/modules.json,D12)。本地零状态,纯转发。
|
|
3
3
|
import process from "node:process";
|
|
4
4
|
import { callApi, login, resolveSite } from "./site";
|
|
@@ -21,7 +21,7 @@ export async function setEnabledCommand(name: string, enabled: boolean, opts: En
|
|
|
21
21
|
const fetchFn = opts.fetchFn ?? globalThis.fetch;
|
|
22
22
|
const { base, user, pass } = resolveSite(opts);
|
|
23
23
|
const token = await login(fetchFn, base, user, pass);
|
|
24
|
-
const result = await callApi<{ ok: boolean, message: string }>(fetchFn, base, token, "POST", "/api/
|
|
24
|
+
const result = await callApi<{ ok: boolean, message: string }>(fetchFn, base, token, "POST", "/api/oj-market/enable", { name: name.trim(), enabled });
|
|
25
25
|
console.log(`[ojm] ${result.message}`);
|
|
26
26
|
if (!result.ok)
|
|
27
27
|
process.exitCode = 1;
|
package/src/init.ts
CHANGED
|
@@ -38,6 +38,8 @@ export interface InitOptions {
|
|
|
38
38
|
ojInstaller?: (binDir: string) => Promise<void>
|
|
39
39
|
/** 注入桩密封(测试);缺省真跑 bin/oj secret keygen/seal */
|
|
40
40
|
seal?: Sealer
|
|
41
|
+
/** 随站第一方 bundle 源目录(测试注入);缺省 <cliRoot>/shell-dist/bundles(prepack 同步面) */
|
|
42
|
+
bundlesDir?: string
|
|
41
43
|
}
|
|
42
44
|
|
|
43
45
|
interface InitReport {
|
|
@@ -149,6 +151,12 @@ export async function initProject(destDir: string, opts: InitOptions = {}): Prom
|
|
|
149
151
|
else if (builtinStatus === "skipped")
|
|
150
152
|
report.skipped.push(`${BUILTIN_LIST_FILE}(已存在,不覆盖用户手改)`);
|
|
151
153
|
|
|
154
|
+
// 1c) 随站第一方 bundle 落位(Phase B T3b,仅 --yes 自动化路径;交互式零动作——
|
|
155
|
+
// 决策 #1 两路径并存):tgz+sig 拷入 cdn/ 并写 bundles.json 意图标记,
|
|
156
|
+
// 站点经 oj-market index 端点对账收敛(上拉不下拉,方案 §5)。
|
|
157
|
+
// bundle 由发布流水线 built+signed 后收进 shell-dist/bundles(scripts/sync-bundles.mjs)。
|
|
158
|
+
placeBundledSolutions(destDir, opts, report, cliRoot);
|
|
159
|
+
|
|
152
160
|
// 1b) tsconfig.paths.json:把 oj 别名 `#/<模块名>/…` 投影成 TS 的 paths。
|
|
153
161
|
// 必须**现场生成**而不是随模板拷贝——模板兼作 demo 工程(多出 demo/home 两个
|
|
154
162
|
// 模块),而用户工程只有白名单里的那些;照拷会让 paths 指向不存在的目录。
|
|
@@ -277,6 +285,43 @@ export async function initProject(destDir: string, opts: InitOptions = {}): Prom
|
|
|
277
285
|
}
|
|
278
286
|
|
|
279
287
|
/** 默认密封器:bin/oj secret keygen + seal(失败返回 null,不抛) */
|
|
288
|
+
/**
|
|
289
|
+
* 随站第一方 bundle 落位(Phase B T3b):`ojm init --yes` 把发布流水线收进
|
|
290
|
+
* shell-dist/bundles 的 <name>-<version>.tgz(+sig) 拷入工程 cdn/,并写
|
|
291
|
+
* bundles.json 意图标记(站点启动后经 oj-market index 端点对账收敛——上拉不下拉)。
|
|
292
|
+
* 仅 --yes 自动化路径执行(决策 #1:--yes = 无人值守授权;交互式保持手动口径);
|
|
293
|
+
* bundle 源缺失是「发布未内置 bundle」的合法态(如源码仓直跑)——warning 不失败。
|
|
294
|
+
*/
|
|
295
|
+
function placeBundledSolutions(destDir: string, opts: InitOptions, report: InitReport, cliRoot: string): void {
|
|
296
|
+
if (!opts.yes)
|
|
297
|
+
return;
|
|
298
|
+
const srcDir = opts.bundlesDir ?? path.join(cliRoot, "shell-dist", "bundles");
|
|
299
|
+
if (!fs.existsSync(srcDir)) {
|
|
300
|
+
report.warnings.push("随站方案包缺失(shell-dist/bundles 不在——发布流水线未内置 bundle):市场保持出厂未装,superadmin 可经 system 域「方案安装」首装");
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
const files = fs.readdirSync(srcDir).filter(f => f.endsWith(".tgz")).sort();
|
|
304
|
+
if (files.length === 0) {
|
|
305
|
+
report.warnings.push("随站方案包目录为空(shell-dist/bundles 无 tgz):市场保持出厂未装");
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
const cdnDir = path.join(destDir, "cdn");
|
|
309
|
+
fs.mkdirSync(cdnDir, { recursive: true });
|
|
310
|
+
const entries: { name: string, version: string, file: string }[] = [];
|
|
311
|
+
for (const file of files) {
|
|
312
|
+
fs.copyFileSync(path.join(srcDir, file), path.join(cdnDir, file));
|
|
313
|
+
const sig = `${file}.sig`;
|
|
314
|
+
if (fs.existsSync(path.join(srcDir, sig)))
|
|
315
|
+
fs.copyFileSync(path.join(srcDir, sig), path.join(cdnDir, sig));
|
|
316
|
+
// name/version 自文件名解析(bundle 命名纪律 <name>-<version>.tgz,同 download 段正名口径);
|
|
317
|
+
// version 以数字开头取最后一个连字符切分(name 本身可含连字符,如 oj-market)
|
|
318
|
+
const m = file.match(/^(.+)-(\d[0-9A-Za-z.+-]*)\.tgz$/);
|
|
319
|
+
entries.push({ name: m?.[1] ?? file.replace(/\.tgz$/, ""), version: m?.[2] ?? "", file });
|
|
320
|
+
}
|
|
321
|
+
fs.writeFileSync(path.join(cdnDir, "bundles.json"), `${JSON.stringify({ solutions: entries }, null, "\t")}\n`);
|
|
322
|
+
report.created.push(`cdn/ 随站方案包 ${files.length} 个 + 意图标记 bundles.json`);
|
|
323
|
+
}
|
|
324
|
+
|
|
280
325
|
const defaultSealer: Sealer = async (destDir, plainSecret) => {
|
|
281
326
|
const ojBin = resolveOjBin(destDir);
|
|
282
327
|
if (!fs.existsSync(ojBin))
|
package/src/install.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { TaskRecord } from "./site";
|
|
|
2
2
|
// ojm install(P1-13,D6):方案包安装瘦客户端。目标两种形态:
|
|
3
3
|
// · 本地 tgz 路径(<name>-<version>.tgz 文件名推导身份,--name/--version 可覆盖)→ file 源 multipart 上传;
|
|
4
4
|
// · name[@version] → cdn 源(站点侧 Form B 索引解析,安装器不拼 URL——市场侧职责,api.ts 同纪律)。
|
|
5
|
-
// 流程:登录 → POST /api/
|
|
5
|
+
// 流程:登录 → POST /api/oj-market/install → 轮询 task?id= 至终态 → 渲染。
|
|
6
6
|
// 终态渲染(契约附录 A 同表取用,R28/DRY):失败/partial 打 error.reason + retryable 标注 +
|
|
7
7
|
// hintKey 对应双语指引(--locale zh-CN|en-US,缺省 zh-CN;文案真源为契约附录 A 表)。
|
|
8
8
|
import fs from "node:fs";
|
|
@@ -285,11 +285,11 @@ export async function installCommand(target: string, opts: InstallOptions = {}):
|
|
|
285
285
|
if (opts.name !== undefined && opts.name !== slug)
|
|
286
286
|
throw new UsageError(`[ojm] --name「${opts.name}」与 registry 目标推导的物理名「${slug}」不一致——registry 源由目标推导身份,请二选一`);
|
|
287
287
|
// 旧站点拒:meta apiVersion < 0.2.0 不支持 registry 源(REG-R9② 升版反噬 F4 的人话面)
|
|
288
|
-
const meta = await callApi<{ apiVersion?: string }>(fetchFn, base, token, "GET", "/api/
|
|
288
|
+
const meta = await callApi<{ apiVersion?: string }>(fetchFn, base, token, "GET", "/api/oj-market/meta");
|
|
289
289
|
if (!isSiteApiAtLeast(meta.apiVersion, "0.2.0"))
|
|
290
290
|
throw new Error(`[ojm] 目标站点平台 API 版本过低(需 ≥ 0.2.0 才支持 --registry 源安装,实为 ${meta.apiVersion ?? "未知"})——请升级目标站点的 oj-module,或改用 CDN/file 源`);
|
|
291
291
|
const registryOrigin = originOf(src.url) ?? src.url;
|
|
292
|
-
response = await callApi<{ task_id: string }>(fetchFn, base, token, "POST", `/api/
|
|
292
|
+
response = await callApi<{ task_id: string }>(fetchFn, base, token, "POST", `/api/oj-market/install${forceQ}`, {
|
|
293
293
|
name: slug,
|
|
294
294
|
version: opts.version ?? version,
|
|
295
295
|
source: "registry",
|
|
@@ -321,7 +321,7 @@ export async function installCommand(target: string, opts: InstallOptions = {}):
|
|
|
321
321
|
else {
|
|
322
322
|
console.log(`[ojm] 未发现签名侧车(${path.basename(sigPath)})——宿主验签硬 fail-closed,未签名包将被拒(sig_missing);签名方法见 devkit「签名与信任」`);
|
|
323
323
|
}
|
|
324
|
-
const res = await fetchFn(`${base}/api/
|
|
324
|
+
const res = await fetchFn(`${base}/api/oj-market/install${forceQ}`, {
|
|
325
325
|
method: "POST",
|
|
326
326
|
headers: { authorization: `Bearer ${token}`, ...tenantHeaders() },
|
|
327
327
|
body: form,
|
|
@@ -331,7 +331,7 @@ export async function installCommand(target: string, opts: InstallOptions = {}):
|
|
|
331
331
|
else {
|
|
332
332
|
if (opts.name !== undefined && opts.name !== parsed.name)
|
|
333
333
|
throw new Error(`[ojm] --name「${opts.name}」与目标「${parsed.name}」不一致——请二选一`);
|
|
334
|
-
response = await callApi<{ task_id: string }>(fetchFn, base, token, "POST", `/api/
|
|
334
|
+
response = await callApi<{ task_id: string }>(fetchFn, base, token, "POST", `/api/oj-market/install${forceQ}`, {
|
|
335
335
|
name: parsed.name,
|
|
336
336
|
version: opts.version ?? parsed.version,
|
|
337
337
|
source: "cdn",
|
|
@@ -96,7 +96,7 @@ describe("pollTask · 轮询退避(800ms 起步指数退避、上限 5s、超
|
|
|
96
96
|
sleepLog.length = 0;
|
|
97
97
|
let n = 0;
|
|
98
98
|
const { fetchFn } = fetchStub({
|
|
99
|
-
"/api/
|
|
99
|
+
"/api/oj-market/task": () => ok(n++ < 2 ? rec({ status: "running", progress: 30 * n }) : rec()),
|
|
100
100
|
});
|
|
101
101
|
const r = await pollTask(fetchFn, "http://x", "tok", "t1", { sleep });
|
|
102
102
|
expect(r.status).toBe("done");
|
|
@@ -107,7 +107,7 @@ describe("pollTask · 轮询退避(800ms 起步指数退避、上限 5s、超
|
|
|
107
107
|
sleepLog.length = 0;
|
|
108
108
|
let n = 0;
|
|
109
109
|
const { fetchFn } = fetchStub({
|
|
110
|
-
"/api/
|
|
110
|
+
"/api/oj-market/task": () => ok(rec({ status: n++ < 5 ? "running" : "done", progress: 10 + n })),
|
|
111
111
|
});
|
|
112
112
|
const r = await pollTask(fetchFn, "http://x", "tok", "t1", { sleep, timeoutMs: 20_000 });
|
|
113
113
|
expect(r.status).toBe("done");
|
|
@@ -117,7 +117,7 @@ describe("pollTask · 轮询退避(800ms 起步指数退避、上限 5s、超
|
|
|
117
117
|
it("超时报错:总等待预算耗尽即人话退出", async () => {
|
|
118
118
|
sleepLog.length = 0;
|
|
119
119
|
const { fetchFn } = fetchStub({
|
|
120
|
-
"/api/
|
|
120
|
+
"/api/oj-market/task": () => ok(rec({ status: "running" })),
|
|
121
121
|
});
|
|
122
122
|
await expect(pollTask(fetchFn, "http://x", "tok", "t1", { sleep, timeoutMs: 1000 })).rejects.toThrow(/超时/);
|
|
123
123
|
expect(sleepLog).toEqual([800]);
|
|
@@ -125,7 +125,7 @@ describe("pollTask · 轮询退避(800ms 起步指数退避、上限 5s、超
|
|
|
125
125
|
|
|
126
126
|
it("查询参数形态:task?id=<id>(非路径段,P1-12 裁决)", async () => {
|
|
127
127
|
const { fetchFn, calls } = fetchStub({
|
|
128
|
-
"/api/
|
|
128
|
+
"/api/oj-market/task": () => ok(rec()),
|
|
129
129
|
});
|
|
130
130
|
await pollTask(fetchFn, "http://x", "tok", "t1", { sleep });
|
|
131
131
|
expect(calls[0]?.url).toContain("task?id=t1");
|
|
@@ -233,7 +233,7 @@ describe("schemaVersion 旧记录兼容(P2-10 升版纪律读方侧:高版
|
|
|
233
233
|
|
|
234
234
|
it("pollTask:高版本记录原样返回不炸(轮询消费面兼容)", async () => {
|
|
235
235
|
const { fetchFn } = fetchStub({
|
|
236
|
-
"/api/
|
|
236
|
+
"/api/oj-market/task": () => ok(rec({ status: "done", schemaVersion: 99 })),
|
|
237
237
|
});
|
|
238
238
|
const r = await pollTask(fetchFn, "http://x", "tok", "t1", { sleep: async () => {} });
|
|
239
239
|
expect(r.status).toBe("done");
|
|
@@ -245,8 +245,8 @@ describe("schemaVersion 旧记录兼容(P2-10 升版纪律读方侧:高版
|
|
|
245
245
|
writeFileSync(tgz, "fake-tgz-bytes");
|
|
246
246
|
const { fetchFn } = fetchStub({
|
|
247
247
|
"/api/auth/login": () => ok({ access_token: "tok", refresh_token: "r", expires_in: 1 }),
|
|
248
|
-
"/api/
|
|
249
|
-
"/api/
|
|
248
|
+
"/api/oj-market/install": () => ok({ task_id: "t1" }),
|
|
249
|
+
"/api/oj-market/task": () => ok(rec({ status: "done", schemaVersion: 99 })),
|
|
250
250
|
});
|
|
251
251
|
const exitBackup = process.exitCode;
|
|
252
252
|
process.exitCode = 0;
|
|
@@ -351,8 +351,8 @@ describe("installCommand · file 源端到端(stub fetch:登录 → multipar
|
|
|
351
351
|
writeFileSync(tgz, "fake-tgz-bytes");
|
|
352
352
|
const { fetchFn, calls } = fetchStub({
|
|
353
353
|
"/api/auth/login": () => ok({ access_token: "tok-1", refresh_token: "r", expires_in: 1 }),
|
|
354
|
-
"/api/
|
|
355
|
-
"/api/
|
|
354
|
+
"/api/oj-market/install": () => ok({ task_id: "t1" }),
|
|
355
|
+
"/api/oj-market/task": () => ok(rec()),
|
|
356
356
|
});
|
|
357
357
|
await installCommand(tgz, { siteUrl: "http://x", user: "u", pass: "p", fetchFn, sleep: async () => {} });
|
|
358
358
|
// 登录体
|
|
@@ -378,8 +378,8 @@ describe("installCommand · file 源端到端(stub fetch:登录 → multipar
|
|
|
378
378
|
writeFileSync(tgz, "fake-tgz-bytes");
|
|
379
379
|
const { fetchFn } = fetchStub({
|
|
380
380
|
"/api/auth/login": () => ok({ access_token: "tok", refresh_token: "r", expires_in: 1 }),
|
|
381
|
-
"/api/
|
|
382
|
-
"/api/
|
|
381
|
+
"/api/oj-market/install": () => ok({ task_id: "t1" }),
|
|
382
|
+
"/api/oj-market/task": () => ok(rec({
|
|
383
383
|
status: "failed",
|
|
384
384
|
stage: "migrating",
|
|
385
385
|
error: { reason: "迁移失败", retryable: true, hintKey: "lifecycle.stage.migrating.hint" },
|
|
@@ -403,8 +403,8 @@ describe("installCommand · file 源端到端(stub fetch:登录 → multipar
|
|
|
403
403
|
writeFileSync(`${tgz}.sig`, JSON.stringify({ alg: "Ed25519", key_id: "test.publisher", public_key: "a".repeat(64), value: "AAAA", canon: "v2", registry_id: "test.registry" }));
|
|
404
404
|
const { fetchFn, calls } = fetchStub({
|
|
405
405
|
"/api/auth/login": () => ok({ access_token: "tok", refresh_token: "r", expires_in: 1 }),
|
|
406
|
-
"/api/
|
|
407
|
-
"/api/
|
|
406
|
+
"/api/oj-market/install": () => ok({ task_id: "t1" }),
|
|
407
|
+
"/api/oj-market/task": () => ok(rec()),
|
|
408
408
|
});
|
|
409
409
|
await installCommand(tgz, { siteUrl: "http://x", user: "u", pass: "p", fetchFn, sleep: async () => {} });
|
|
410
410
|
const form = calls[1]?.init?.body as FormData;
|
|
@@ -418,8 +418,8 @@ describe("installCommand · file 源端到端(stub fetch:登录 → multipar
|
|
|
418
418
|
writeFileSync(tgz, "fake-tgz-bytes");
|
|
419
419
|
const { fetchFn, calls } = fetchStub({
|
|
420
420
|
"/api/auth/login": () => ok({ access_token: "tok", refresh_token: "r", expires_in: 1 }),
|
|
421
|
-
"/api/
|
|
422
|
-
"/api/
|
|
421
|
+
"/api/oj-market/install": () => ok({ task_id: "t1" }),
|
|
422
|
+
"/api/oj-market/task": () => ok(rec()),
|
|
423
423
|
});
|
|
424
424
|
await installCommand(tgz, { siteUrl: "http://x", user: "u", pass: "p", fetchFn, sleep: async () => {} });
|
|
425
425
|
const form = calls[1]?.init?.body as FormData;
|
|
@@ -441,8 +441,8 @@ describe("installCommand · cdn 源(JSON 体 name@version)", () => {
|
|
|
441
441
|
it("source=cdn 的 JSON 体 + 版本缺省 null", async () => {
|
|
442
442
|
const { fetchFn, calls } = fetchStub({
|
|
443
443
|
"/api/auth/login": () => ok({ access_token: "tok", refresh_token: "r", expires_in: 1 }),
|
|
444
|
-
"/api/
|
|
445
|
-
"/api/
|
|
444
|
+
"/api/oj-market/install": () => ok({ task_id: "t1" }),
|
|
445
|
+
"/api/oj-market/task": () => ok(rec()),
|
|
446
446
|
});
|
|
447
447
|
await installCommand("demo-solution", { siteUrl: "http://x", user: "u", pass: "p", fetchFn, sleep: async () => {} });
|
|
448
448
|
const installCall = calls[1]!;
|
|
@@ -453,8 +453,8 @@ describe("installCommand · cdn 源(JSON 体 name@version)", () => {
|
|
|
453
453
|
it("@version 透传", async () => {
|
|
454
454
|
const { fetchFn, calls } = fetchStub({
|
|
455
455
|
"/api/auth/login": () => ok({ access_token: "tok", refresh_token: "r", expires_in: 1 }),
|
|
456
|
-
"/api/
|
|
457
|
-
"/api/
|
|
456
|
+
"/api/oj-market/install": () => ok({ task_id: "t1" }),
|
|
457
|
+
"/api/oj-market/task": () => ok(rec()),
|
|
458
458
|
});
|
|
459
459
|
await installCommand("demo-solution@0.2.0", { siteUrl: "http://x", user: "u", pass: "p", fetchFn, sleep: async () => {} });
|
|
460
460
|
expect(JSON.parse(String(calls[1]?.init?.body)).version).toBe("0.2.0");
|
|
@@ -465,11 +465,11 @@ describe("setEnabledCommand · enable/disable 瘦客户端(D8)", () => {
|
|
|
465
465
|
it("pOST enable {name, enabled} → 打印服务端 message;ok:false 非零退出", async () => {
|
|
466
466
|
const { fetchFn, calls } = fetchStub({
|
|
467
467
|
"/api/auth/login": () => ok({ access_token: "tok", refresh_token: "r", expires_in: 1 }),
|
|
468
|
-
"/api/
|
|
468
|
+
"/api/oj-market/enable": () => ok({ ok: true, message: "启用完成:四处同写" }),
|
|
469
469
|
});
|
|
470
470
|
await setEnabledCommand("demo-solution", true, { siteUrl: "http://x", user: "u", pass: "p", fetchFn });
|
|
471
471
|
const call = calls[1]!;
|
|
472
|
-
expect(call.url).toContain("/api/
|
|
472
|
+
expect(call.url).toContain("/api/oj-market/enable");
|
|
473
473
|
expect(JSON.parse(String(call.init?.body))).toEqual({ name: "demo-solution", enabled: true });
|
|
474
474
|
expect(logs.join("\n")).toContain("启用完成:四处同写");
|
|
475
475
|
|
|
@@ -478,7 +478,7 @@ describe("setEnabledCommand · enable/disable 瘦客户端(D8)", () => {
|
|
|
478
478
|
try {
|
|
479
479
|
const { fetchFn: fetchFn2 } = fetchStub({
|
|
480
480
|
"/api/auth/login": () => ok({ access_token: "tok", refresh_token: "r", expires_in: 1 }),
|
|
481
|
-
"/api/
|
|
481
|
+
"/api/oj-market/enable": () => ok({ ok: false, message: "禁用失败:「x」未安装" }),
|
|
482
482
|
});
|
|
483
483
|
await setEnabledCommand("x", false, { siteUrl: "http://x", user: "u", pass: "p", fetchFn: fetchFn2 });
|
|
484
484
|
expect(process.exitCode).toBe(1);
|
|
@@ -521,13 +521,13 @@ describe("s5-2② REG-R9 install 源切换:registry(CLI 归一 + 旧站点
|
|
|
521
521
|
it("installCommand registry:新站点(apiVersion 0.2.0)POST source=registry + registry=origin(slug 归一)", async () => {
|
|
522
522
|
const { fetchFn, calls } = fetchStub({
|
|
523
523
|
"/api/auth/login": () => ok({ access_token: "tok" }),
|
|
524
|
-
"/api/
|
|
525
|
-
"/api/
|
|
526
|
-
"/api/
|
|
524
|
+
"/api/oj-market/meta": () => ok({ apiVersion: "0.2.0" }),
|
|
525
|
+
"/api/oj-market/install": () => ok({ task_id: "t1" }),
|
|
526
|
+
"/api/oj-market/task": () => ok(rec()),
|
|
527
527
|
});
|
|
528
528
|
const r = await installCommand("@acme/demo", { siteUrl: "http://site", user: "u", pass: "p", registry: "https://r.example.com/", fetchFn });
|
|
529
529
|
expect(r.status).toBe("done");
|
|
530
|
-
const call = calls.find(c => c.url.includes("/api/
|
|
530
|
+
const call = calls.find(c => c.url.includes("/api/oj-market/install"));
|
|
531
531
|
expect(call).toBeDefined();
|
|
532
532
|
const body = JSON.parse(String(call!.init!.body));
|
|
533
533
|
expect(body).toMatchObject({ name: "acme-demo", source: "registry", registry: "https://r.example.com" });
|
|
@@ -537,12 +537,12 @@ describe("s5-2② REG-R9 install 源切换:registry(CLI 归一 + 旧站点
|
|
|
537
537
|
it("installCommand registry:旧站点(apiVersion 0.1.0)→ 抛「站点版本过低」且不 POST install", async () => {
|
|
538
538
|
const { fetchFn, calls } = fetchStub({
|
|
539
539
|
"/api/auth/login": () => ok({ access_token: "tok" }),
|
|
540
|
-
"/api/
|
|
541
|
-
"/api/
|
|
540
|
+
"/api/oj-market/meta": () => ok({ apiVersion: "0.1.0" }),
|
|
541
|
+
"/api/oj-market/install": () => ok({ task_id: "t1" }),
|
|
542
542
|
});
|
|
543
543
|
await expect(installCommand("@acme/demo", { siteUrl: "http://site", user: "u", pass: "p", registry: "https://r.example.com", fetchFn }))
|
|
544
544
|
.rejects
|
|
545
545
|
.toThrow(/版本过低/);
|
|
546
|
-
expect(calls.some(c => c.url.includes("/api/
|
|
546
|
+
expect(calls.some(c => c.url.includes("/api/oj-market/install"))).toBe(false);
|
|
547
547
|
});
|
|
548
548
|
});
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
// BDD:oj-market 第一方方案包(Phase B T1)——市场 UI 以**纯 web 方案包**形态分发:
|
|
3
|
+
// 契约 src/oj-market/solution.yaml(web-only:api 编排面留在平台,随脚手架内置不随包),
|
|
4
|
+
// `ojm build --bundle oj-market` 产出 dist/oj-market-0.2.0.tgz(+ 发包人 ojm sign 平台签名)。
|
|
5
|
+
// 版本基线 0.2.0(20261106-2122 拍板);entry.ts 版本与契约版本同源漂移由本 spec 钉住。
|
|
6
|
+
import { cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { tmpdir } from "node:os";
|
|
8
|
+
import { dirname, join } from "node:path";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
10
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
11
|
+
import { parse } from "yaml";
|
|
12
|
+
import { buildBundle } from "../../bundle";
|
|
13
|
+
import { validateContract } from "../../manifest-contract";
|
|
14
|
+
|
|
15
|
+
const TEMPLATES = join(dirname(fileURLToPath(import.meta.url)), "../../../templates");
|
|
16
|
+
const CONTRACT = join(TEMPLATES, "src/oj-market/solution.yaml");
|
|
17
|
+
|
|
18
|
+
/** tar 成员清单(与 bundle.test 同原语) */
|
|
19
|
+
function tarList(tgz: string): string[] {
|
|
20
|
+
const out = execFileSync("tar", ["-tzf", tgz], { encoding: "utf-8" });
|
|
21
|
+
return out.split("\n").filter(Boolean);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
describe("oj-market 方案包契约(T1)", () => {
|
|
25
|
+
it("src/oj-market/solution.yaml 在位:name=oj-market、version=0.2.0、web-only(无 api 段)、契约零违规", () => {
|
|
26
|
+
expect(existsSync(CONTRACT)).toBe(true);
|
|
27
|
+
const m = parse(readFileSync(CONTRACT, "utf8")) as Record<string, unknown>;
|
|
28
|
+
expect(m).toMatchObject({ name: "oj-market", version: "0.2.0" });
|
|
29
|
+
expect(m.api).toBeUndefined(); // 安装编排面留在平台(src/oj-market/api 内置不随包)
|
|
30
|
+
const web = m.web as Record<string, unknown>;
|
|
31
|
+
expect(web.entry).toBe("modules/oj-market/0.2.0/entry.js");
|
|
32
|
+
expect(validateContract(m)).toEqual([]);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("模板工程 builtin 名单不含 oj-market(第一方打包放行;用户工程名单由 init 按白名单派生仍含——两侧语义分歧是本用例钉住的裁决)", () => {
|
|
36
|
+
const p = join(TEMPLATES, "src/.ojm-builtin.json");
|
|
37
|
+
const list = JSON.parse(readFileSync(p, "utf8")) as { modules: string[] };
|
|
38
|
+
expect(list.modules).not.toContain("oj-market");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("版本同源:web entry.ts 模块版本 = 契约版本(0.2.0 基线,双写漂移即红)", async () => {
|
|
42
|
+
const { readModuleDefinition } = await import("../../build/module-def");
|
|
43
|
+
const def = await readModuleDefinition(join(TEMPLATES, "src/oj-market/web/entry.ts"), TEMPLATES);
|
|
44
|
+
expect(def.version).toBe("0.2.0");
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe("oj-market 方案包组装(T1,scratch 工程钉布局)", () => {
|
|
49
|
+
let project: string;
|
|
50
|
+
let out: string;
|
|
51
|
+
|
|
52
|
+
beforeEach(() => {
|
|
53
|
+
project = mkdtempSync(join(tmpdir(), "ojm-market-bundle-"));
|
|
54
|
+
out = mkdtempSync(join(tmpdir(), "ojm-market-out-"));
|
|
55
|
+
// 契约原样入 scratch(唯一真源,勿在测试里另写一份)
|
|
56
|
+
mkdirSync(join(project, "src/oj-market"), { recursive: true });
|
|
57
|
+
cpSync(CONTRACT, join(project, "src/oj-market/solution.yaml"));
|
|
58
|
+
// 模块标记(布局探测)+ 无 category 路由的 entry 桩(derive 空 → 保留契约 routes)
|
|
59
|
+
mkdirSync(join(project, "src/oj-market/web/locales"), { recursive: true });
|
|
60
|
+
writeFileSync(join(project, "src/oj-market/web/entry.ts"), "export default { name: \"oj-market\", version: \"0.2.0\", routes: [] };\n");
|
|
61
|
+
writeFileSync(join(project, "src/oj-market/web/locales/zh-CN.json"), JSON.stringify({ menu: { main: "方案市场" } }));
|
|
62
|
+
writeFileSync(join(project, "src/oj-market/web/locales/en-US.json"), JSON.stringify({ menu: { main: "Market" } }));
|
|
63
|
+
// 站点构建产物桩(版本段 === 0.2.0)
|
|
64
|
+
mkdirSync(join(project, "dist/web/modules/oj-market/0.2.0"), { recursive: true });
|
|
65
|
+
writeFileSync(join(project, "dist/web/modules/oj-market/0.2.0/entry.js"), "export default {};\n");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
afterEach(() => {
|
|
69
|
+
rmSync(project, { recursive: true, force: true });
|
|
70
|
+
rmSync(out, { recursive: true, force: true });
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("buildBundle --bundle oj-market:纯 web 包(manifest + web/modules + 双语 locales,无 api/ 成员)", async () => {
|
|
74
|
+
const [r] = await buildBundle(project, { out, name: "oj-market" });
|
|
75
|
+
expect(r).toMatchObject({ name: "oj-market", version: "0.2.0" });
|
|
76
|
+
expect(r!.sha256).toMatch(/^[a-f0-9]{64}$/);
|
|
77
|
+
const names = tarList(r!.tgz);
|
|
78
|
+
expect(names).toContain("oj-market-0.2.0/manifest.yaml");
|
|
79
|
+
expect(names).toContain("oj-market-0.2.0/web/modules/oj-market/0.2.0/entry.js");
|
|
80
|
+
expect(names).toContain("oj-market-0.2.0/web/modules/oj-market/locales/zh-CN.json");
|
|
81
|
+
expect(names).toContain("oj-market-0.2.0/web/modules/oj-market/locales/en-US.json");
|
|
82
|
+
expect(names.some(n => n.includes("/api/"))).toBe(false);
|
|
83
|
+
// 确定性:同输入两次打包哈希一致(C2-32)
|
|
84
|
+
const [r2] = await buildBundle(project, { out, name: "oj-market" });
|
|
85
|
+
expect(r2!.sha256).toBe(r!.sha256);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
@@ -205,17 +205,15 @@ describe("ojm publish 索引重算 + 签名(P3-5/6/7,REG-CS5 三级化)",
|
|
|
205
205
|
it("索引写入只有 writeIndexFile 一条路径(源码级不变量,grep 可证)", () => {
|
|
206
206
|
const srcOf = (rel: string) => readFileSync(fileURLToPath(new URL(rel, import.meta.url)), "utf8");
|
|
207
207
|
const countOf = (text: string, re: RegExp) => text.split("\n").filter(line => re.test(line)).length;
|
|
208
|
-
// 生成侧:publish.ts 里唯一的 fs.writeFileSync 就在 writeIndexFile 体内(tmp+rename
|
|
209
|
-
// build-index.ts 零直写——索引文件全部经唯一写入口
|
|
208
|
+
// 生成侧:publish.ts 里唯一的 fs.writeFileSync 就在 writeIndexFile 体内(tmp+rename)
|
|
210
209
|
expect(countOf(srcOf("../../publish.ts"), /fs\.writeFileSync/)).toBe(1);
|
|
211
210
|
expect(countOf(srcOf("../../publish.ts"), /appendFileSync/)).toBe(0);
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
expect(countOf(walkthrough, /appendFileSync/)).toBe(
|
|
218
|
-
expect(countOf(walkthrough,
|
|
219
|
-
expect(countOf(walkthrough, /writeIndexFile\(catalog/)).toBe(1);
|
|
211
|
+
// A6:市场索引机制整建制退役——build-index/index-builder/sharding/yank-ledger 已删,
|
|
212
|
+
// 全仓不再有第二个索引写者(registry-* 文件面留档,唯一写入口仍是 writeIndexFile)
|
|
213
|
+
for (const rel of ["../../market/build-index.ts", "../../market/index-builder.ts", "../../market/sharding.ts", "../../market/yank-ledger.ts"])
|
|
214
|
+
expect(existsSync(fileURLToPath(new URL(rel, import.meta.url)))).toBe(false);
|
|
215
|
+
// walkthrough 的索引 tamper drill 随 neg-badindex 退役——不再有任何索引直写点
|
|
216
|
+
expect(countOf(srcOf("../../market/walkthrough.ts"), /appendFileSync/)).toBe(0);
|
|
217
|
+
expect(countOf(srcOf("../../market/walkthrough.ts"), /writeIndexFile/)).toBe(0);
|
|
220
218
|
});
|
|
221
219
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import process from "node:process";
|
|
2
2
|
// BDD:ojm trust 瘦客户端(P2-2,C2-38/39)——纯转发(uninstall.test.ts 同范式):
|
|
3
|
-
// 登录 → GET/POST/DELETE /api/
|
|
3
|
+
// 登录 → GET/POST/DELETE /api/oj-market/trust* → 打印人话;fetch 全 stub 不真发网络。
|
|
4
4
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
5
5
|
import { formatFingerprint, trustCommand } from "../../trust";
|
|
6
6
|
|
|
@@ -44,10 +44,10 @@ describe("trustCommand · ls(P2-2)", () => {
|
|
|
44
44
|
it("登录后 GET /market/trust → 逐行打印 key_id + 分组指纹", async () => {
|
|
45
45
|
const { fetchFn, calls } = fetchStub({
|
|
46
46
|
"/api/auth/login": () => ok({ access_token: "tok", refresh_token: "r", expires_in: 1 }),
|
|
47
|
-
"/api/
|
|
47
|
+
"/api/oj-market/trust": () => ok({ keys: [KEY_A], note: null }),
|
|
48
48
|
});
|
|
49
49
|
await trustCommand("ls", [], { ...SITE, fetchFn });
|
|
50
|
-
expect(calls[1]?.url).toContain("/api/
|
|
50
|
+
expect(calls[1]?.url).toContain("/api/oj-market/trust");
|
|
51
51
|
expect(calls[1]?.init?.method).toBe("GET");
|
|
52
52
|
expect((calls[1]?.init?.headers as Record<string, string>).authorization).toBe("Bearer tok");
|
|
53
53
|
const out = logs.join("\n");
|
|
@@ -58,7 +58,7 @@ describe("trustCommand · ls(P2-2)", () => {
|
|
|
58
58
|
it("空库 → 可读空态;note 在场(.bak 回落)→ 先打注意行", async () => {
|
|
59
59
|
const { fetchFn } = fetchStub({
|
|
60
60
|
"/api/auth/login": () => ok({ access_token: "tok", refresh_token: "r", expires_in: 1 }),
|
|
61
|
-
"/api/
|
|
61
|
+
"/api/oj-market/trust": () => ok({ keys: [], note: "x 损坏,已自动回落 x.bak" }),
|
|
62
62
|
});
|
|
63
63
|
await trustCommand("ls", [], { ...SITE, fetchFn });
|
|
64
64
|
expect(logs.join("\n")).toContain("已自动回落");
|
|
@@ -69,7 +69,7 @@ describe("trustCommand · add / revoke(P2-2)", () => {
|
|
|
69
69
|
it("add:POST {key_id, public_key};added / exists_same 两种回显", async () => {
|
|
70
70
|
const added = fetchStub({
|
|
71
71
|
"/api/auth/login": () => ok({ access_token: "tok", refresh_token: "r", expires_in: 1 }),
|
|
72
|
-
"/api/
|
|
72
|
+
"/api/oj-market/trust/key": () => ok({ result: "added" }),
|
|
73
73
|
});
|
|
74
74
|
await trustCommand("add", ["ojm.test", "a".repeat(64)], { ...SITE, fetchFn: added.fetchFn });
|
|
75
75
|
const call = added.calls[1]!;
|
|
@@ -79,7 +79,7 @@ describe("trustCommand · add / revoke(P2-2)", () => {
|
|
|
79
79
|
|
|
80
80
|
const idem = fetchStub({
|
|
81
81
|
"/api/auth/login": () => ok({ access_token: "tok", refresh_token: "r", expires_in: 1 }),
|
|
82
|
-
"/api/
|
|
82
|
+
"/api/oj-market/trust/key": () => ok({ result: "exists_same" }),
|
|
83
83
|
});
|
|
84
84
|
await trustCommand("add", ["ojm.test", "a".repeat(64)], { ...SITE, fetchFn: idem.fetchFn });
|
|
85
85
|
expect(logs.join("\n")).toContain("无需重复添加");
|
|
@@ -97,18 +97,18 @@ describe("trustCommand · add / revoke(P2-2)", () => {
|
|
|
97
97
|
try {
|
|
98
98
|
const yes = fetchStub({
|
|
99
99
|
"/api/auth/login": () => ok({ access_token: "tok", refresh_token: "r", expires_in: 1 }),
|
|
100
|
-
"/api/
|
|
100
|
+
"/api/oj-market/trust/key": () => ok({ revoked: true }),
|
|
101
101
|
});
|
|
102
102
|
await trustCommand("revoke", ["ojm.test"], { ...SITE, fetchFn: yes.fetchFn });
|
|
103
103
|
const call = yes.calls[1]!;
|
|
104
104
|
expect(call.init?.method).toBe("DELETE");
|
|
105
|
-
expect(call.url).toContain("/api/
|
|
105
|
+
expect(call.url).toContain("/api/oj-market/trust/key?key_id=ojm.test");
|
|
106
106
|
expect(process.exitCode).toBe(0);
|
|
107
107
|
|
|
108
108
|
process.exitCode = 0;
|
|
109
109
|
const no = fetchStub({
|
|
110
110
|
"/api/auth/login": () => ok({ access_token: "tok", refresh_token: "r", expires_in: 1 }),
|
|
111
|
-
"/api/
|
|
111
|
+
"/api/oj-market/trust/key": () => ok({ revoked: false }),
|
|
112
112
|
});
|
|
113
113
|
await trustCommand("revoke", ["nope.key"], { ...SITE, fetchFn: no.fetchFn });
|
|
114
114
|
expect(process.exitCode).toBe(1);
|
|
@@ -138,14 +138,14 @@ describe("trustCommand · confirm(P2-3 TOFU 确认链)", () => {
|
|
|
138
138
|
it("先展示指纹再调端点:GET task → 打印 key_id + 分组指纹 → POST /market/trust {task_id}", async () => {
|
|
139
139
|
const { fetchFn, calls } = fetchStub({
|
|
140
140
|
"/api/auth/login": () => ok({ access_token: "tok", refresh_token: "r", expires_in: 1 }),
|
|
141
|
-
"/api/
|
|
142
|
-
"/api/
|
|
141
|
+
"/api/oj-market/task": () => ok(needsTrustTask),
|
|
142
|
+
"/api/oj-market/trust": () => ok({ result: "added", key_id: "stranger.pub" }),
|
|
143
143
|
});
|
|
144
144
|
await trustCommand("confirm", [], { ...SITE, task: "install-1-abc", fetchFn });
|
|
145
145
|
// 调用序:登录 → GET task → POST trust
|
|
146
|
-
expect(calls[1]?.url).toContain("/api/
|
|
146
|
+
expect(calls[1]?.url).toContain("/api/oj-market/task?id=install-1-abc");
|
|
147
147
|
expect(calls[2]?.init?.method).toBe("POST");
|
|
148
|
-
expect(calls[2]?.url).toContain("/api/
|
|
148
|
+
expect(calls[2]?.url).toContain("/api/oj-market/trust");
|
|
149
149
|
expect(JSON.parse(String(calls[2]?.init?.body))).toEqual({ task_id: "install-1-abc" });
|
|
150
150
|
const out = logs.join("\n");
|
|
151
151
|
expect(out).toContain("stranger.pub");
|
|
@@ -157,7 +157,7 @@ describe("trustCommand · confirm(P2-3 TOFU 确认链)", () => {
|
|
|
157
157
|
it("任务非 needs_trust → 本地可读报错(不发确认请求)", async () => {
|
|
158
158
|
const { fetchFn, calls } = fetchStub({
|
|
159
159
|
"/api/auth/login": () => ok({ access_token: "tok", refresh_token: "r", expires_in: 1 }),
|
|
160
|
-
"/api/
|
|
160
|
+
"/api/oj-market/task": () => ok({ ...needsTrustTask, status: "done", trust: undefined }),
|
|
161
161
|
});
|
|
162
162
|
await expect(trustCommand("confirm", [], { ...SITE, task: "install-1-abc", fetchFn })).rejects.toThrow(/不在待信任确认状态/);
|
|
163
163
|
expect(calls).toHaveLength(2); // 登录 + GET task,无 POST
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import process from "node:process";
|
|
2
2
|
// BDD:ojm uninstall 瘦客户端(P1-15a 缺口 B,D6/D8 同款)——纯转发:
|
|
3
|
-
// 登录 → POST /api/
|
|
3
|
+
// 登录 → POST /api/oj-market/uninstall {name} → 打印服务端人话 message;
|
|
4
4
|
// ok:false(未装/空锁拒启)置 exitCode 1。fetch 全 stub 不真发网络(install.test.ts 同范式)。
|
|
5
5
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
6
6
|
import { uninstallCommand } from "../../uninstall";
|
|
@@ -42,13 +42,13 @@ describe("uninstallCommand · 卸载瘦客户端(P1-15a 缺口 B)", () => {
|
|
|
42
42
|
it("登录后 POST uninstall {name}(Bearer 注入)→ 打印服务端 message", async () => {
|
|
43
43
|
const { fetchFn, calls } = fetchStub({
|
|
44
44
|
"/api/auth/login": () => ok({ access_token: "tok", refresh_token: "r", expires_in: 1 }),
|
|
45
|
-
"/api/
|
|
45
|
+
"/api/oj-market/uninstall": () => ok({ ok: true, message: "卸载完成:四处同删(demo-solution@0.1.0)" }),
|
|
46
46
|
});
|
|
47
47
|
await uninstallCommand("demo-solution", { siteUrl: "http://x", user: "u", pass: "p", fetchFn });
|
|
48
48
|
// 登录体 + uninstall JSON 体 + Bearer
|
|
49
49
|
expect(JSON.parse(String(calls[0]?.init?.body))).toEqual({ username: "u", password: "p" });
|
|
50
50
|
const call = calls[1]!;
|
|
51
|
-
expect(call.url).toContain("/api/
|
|
51
|
+
expect(call.url).toContain("/api/oj-market/uninstall");
|
|
52
52
|
expect(call.init?.method).toBe("POST");
|
|
53
53
|
expect((call.init?.headers as Record<string, string>).authorization).toBe("Bearer tok");
|
|
54
54
|
expect(JSON.parse(String(call.init?.body))).toEqual({ name: "demo-solution" });
|
|
@@ -58,7 +58,7 @@ describe("uninstallCommand · 卸载瘦客户端(P1-15a 缺口 B)", () => {
|
|
|
58
58
|
it("ok:false(域逻辑拒绝)→ message 照打 + exitCode 1", async () => {
|
|
59
59
|
const { fetchFn } = fetchStub({
|
|
60
60
|
"/api/auth/login": () => ok({ access_token: "tok", refresh_token: "r", expires_in: 1 }),
|
|
61
|
-
"/api/
|
|
61
|
+
"/api/oj-market/uninstall": () => ok({ ok: false, message: "卸载失败:「x」未安装(锁与 sidecar 均无记录)" }),
|
|
62
62
|
});
|
|
63
63
|
const exitBackup = process.exitCode;
|
|
64
64
|
process.exitCode = 0;
|
package/src/list.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
// `ojm list`(站点已装方案的可查面):登录 → GET /api/
|
|
1
|
+
// `ojm list`(站点已装方案的可查面):登录 → GET /api/oj-market/index → 只挑已装的打表。
|
|
2
2
|
//
|
|
3
3
|
// 为什么需要它:到 0.3.2 为止,CLI **没有任何一条命令能回答「这个站点上装了什么」**——
|
|
4
4
|
// 装完只有一条终态文案,之后要核对版本/启用态得登站点翻市场页。运维与 CI 都需要一个
|
|
5
5
|
// 可脚本化的清单(升级前看谁落后、禁用后确认状态、卸载前确认名字)。
|
|
6
6
|
//
|
|
7
|
-
// 为什么复用 `GET /
|
|
7
|
+
// 为什么复用 `GET /oj-market/index` 而不是新增端点:这一条已经把三件事聚合好了——
|
|
8
8
|
// sidecar 里的 installed/enabled、索引里的 latest/available(算 upgradable)、信任库判定
|
|
9
9
|
// 出的 trust_state。新增一个「只列已装」的端点等于让两个真相源各自漂移,而站点侧那边
|
|
10
10
|
// 本来就要维护全量行(市场页在用)。
|
|
@@ -59,7 +59,7 @@ function asTrustState(raw: unknown): TrustState {
|
|
|
59
59
|
|
|
60
60
|
/** 取一份已装清单(`--all` 时含未装)。不打印、不 exit 的纯函数层,便于 CI 直接在进程内用。 */
|
|
61
61
|
export async function fetchInstalled(fetchFn: typeof fetch, base: string, token: string, all: boolean): Promise<SolutionRow[]> {
|
|
62
|
-
const rows = await callApi<MarketIndexRow[]>(fetchFn, base, token, "GET", "/api/
|
|
62
|
+
const rows = await callApi<MarketIndexRow[]>(fetchFn, base, token, "GET", "/api/oj-market/index");
|
|
63
63
|
const out: SolutionRow[] = [];
|
|
64
64
|
for (const raw of rows) {
|
|
65
65
|
const name = String(raw.name ?? "").trim();
|