dsh-config-manager 0.1.39 → 0.1.41
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/lib/client.d.ts +90 -8
- package/lib/client.js +1904 -1086
- package/lib/client.js.map +1 -1
- package/lib/core/cache-cleaner.d.ts +39 -0
- package/lib/core/cache-cleaner.js +164 -0
- package/lib/core/cache-cleaner.js.map +1 -0
- package/lib/index.js +172 -3
- package/lib/index.js.map +1 -1
- package/lib/market/github-repos.d.ts +19 -8
- package/lib/market/github-repos.js +62 -33
- package/lib/market/github-repos.js.map +1 -1
- package/lib/market/my-repo.d.ts +111 -3
- package/lib/market/my-repo.js +309 -26
- package/lib/market/my-repo.js.map +1 -1
- package/lib/market/prepare.js +3 -2
- package/lib/market/prepare.js.map +1 -1
- package/lib/market/types.d.ts +2 -0
- package/lib/security/secret-scanner.d.ts +18 -3
- package/lib/security/secret-scanner.js +144 -20
- package/lib/security/secret-scanner.js.map +1 -1
- package/package.json +1 -1
- package/src/client/common/ConfirmDialog.tsx +135 -0
- package/src/client/config-manager.module.css +101 -0
- package/src/client/market/MarketPanel.tsx +138 -45
- package/src/client/market/MyConfigsView.tsx +515 -132
- package/src/client/market/disclaimer.test.ts +57 -0
- package/src/client/market/disclaimer.ts +43 -0
- package/src/client/market/market-api.test.ts +3 -0
- package/src/client/market/market-locales.ts +37 -13
- package/src/client/market/my-configs-api.ts +20 -2
- package/src/client/market/my-configs-view.test.ts +137 -2
- package/src/client/market/my-configs-view.ts +174 -2
- package/src/client/run-store.test.ts +40 -2
- package/src/client/run-store.ts +70 -10
- package/src/client/sync/SyncConfirmView.tsx +28 -22
- package/src/client/sync/SyncSettingsView.tsx +175 -65
- package/src/client/sync/sync-api.ts +19 -0
- package/src/client/sync/sync-locales.ts +14 -2
- package/src/core/cache-cleaner.test.ts +333 -0
- package/src/core/cache-cleaner.ts +196 -0
- package/src/index.ts +165 -5
- package/src/market/github-repos.test.ts +99 -8
- package/src/market/github-repos.ts +67 -35
- package/src/market/my-repo.test.ts +218 -27
- package/src/market/my-repo.ts +403 -30
- package/src/market/prepare.test.ts +71 -1
- package/src/market/prepare.ts +3 -2
- package/src/market/types.ts +2 -0
- package/src/security/secret-scanner.ts +139 -22
- package/src/security/security.test.ts +111 -2
|
@@ -17,7 +17,7 @@ import assert from 'node:assert/strict';
|
|
|
17
17
|
import { GitHubApiError, MARKET_UPSTREAM_OWNER, MARKET_UPSTREAM_REPO } from './github-repos.ts';
|
|
18
18
|
import type { GitHubForkInfo, GitHubPullRequestInfo, GitHubPullRequestParams, GitHubRepoInfo, GitHubUserInfo } from './github-repos.ts';
|
|
19
19
|
import type { GitHubRestLike, MyItemEntry, MyRepoForm } from './my-repo.ts';
|
|
20
|
-
import { MyRepoService, prBranchFor, slugifyItemId, uniqueItemId, bumpVersion } from './my-repo.ts';
|
|
20
|
+
import { MyRepoError, MyRepoService, delistBranchFor, prBranchFor, slugifyItemId, uniqueItemId, bumpVersion } from './my-repo.ts';
|
|
21
21
|
import { MarketPrepareError } from './prepare.ts';
|
|
22
22
|
import type { MarketPrepareInput, MarketPrepareResult } from './prepare.ts';
|
|
23
23
|
import type { GitFileWriter, GitFileWriteCall, GitFileWriterResult } from './git-file-writer.ts';
|
|
@@ -54,6 +54,7 @@ interface RestOverrides {
|
|
|
54
54
|
readFile?: (owner: string, repo: string, path: string, ref?: string) => Promise<string | null>;
|
|
55
55
|
openPullRequest?: (params: GitHubPullRequestParams) => Promise<GitHubPullRequestInfo>;
|
|
56
56
|
listOpenPullRequests?: (owner: string, repo: string, head?: string) => Promise<GitHubPullRequestInfo[]>;
|
|
57
|
+
closePullRequest?: (owner: string, repo: string, number: number) => Promise<GitHubPullRequestInfo>;
|
|
57
58
|
}
|
|
58
59
|
|
|
59
60
|
interface Harness {
|
|
@@ -90,10 +91,22 @@ function makePrepare() {
|
|
|
90
91
|
return { prepare, prepareInputs };
|
|
91
92
|
}
|
|
92
93
|
|
|
93
|
-
function makeHarness(overrides: RestOverrides = {}): Harness {
|
|
94
|
+
function makeHarness(overrides: RestOverrides = {}, opts: { userIndex?: string; officialIndex?: string } = {}): Harness {
|
|
94
95
|
const gitCalls: GitFileWriteCall[] = [];
|
|
96
|
+
/** 动态用户仓库 index(初始=opts.userIndex ?? 空;gitWriter 写入后更新,readFile 读取最新——后台收录要读回条目) */
|
|
97
|
+
let liveUserIndex = opts.userIndex ?? USER_INDEX_EMPTY;
|
|
98
|
+
/** 官方市场 index(opts.officialIndex 可覆盖;后台收录读它) */
|
|
99
|
+
const liveOfficialIndex = opts.officialIndex ?? OFFICIAL_INDEX;
|
|
95
100
|
const gitWriter: GitFileWriter = {
|
|
96
|
-
writeFiles: async (call): Promise<GitFileWriterResult> => {
|
|
101
|
+
writeFiles: async (call): Promise<GitFileWriterResult> => {
|
|
102
|
+
gitCalls.push(call);
|
|
103
|
+
// 写用户仓库时同步内存 index(后台收录/下架经 readFile 读最新)
|
|
104
|
+
if (call.repoUrl === USER_REPO_CLONE) {
|
|
105
|
+
const idx = call.entries.find((e) => e.path === 'index.json');
|
|
106
|
+
if (idx !== undefined) liveUserIndex = String(idx.content);
|
|
107
|
+
}
|
|
108
|
+
return { pushed: true, branch: 'default' };
|
|
109
|
+
},
|
|
97
110
|
};
|
|
98
111
|
const openPrCalls: GitHubPullRequestParams[] = [];
|
|
99
112
|
const restCalls: string[] = [];
|
|
@@ -117,8 +130,8 @@ function makeHarness(overrides: RestOverrides = {}): Harness {
|
|
|
117
130
|
},
|
|
118
131
|
readFile: async (owner, repo, p) => {
|
|
119
132
|
restCalls.push(`readFile:${owner}/${repo}/${p}`);
|
|
120
|
-
if (owner === LOGIN) return
|
|
121
|
-
return
|
|
133
|
+
if (owner === LOGIN) return liveUserIndex; // 用户仓库 index(动态:反映最近一次写入)
|
|
134
|
+
return liveOfficialIndex; // 官方市场 index
|
|
122
135
|
},
|
|
123
136
|
openPullRequest: async (params) => {
|
|
124
137
|
restCalls.push('openPullRequest');
|
|
@@ -133,6 +146,7 @@ function makeHarness(overrides: RestOverrides = {}): Harness {
|
|
|
133
146
|
};
|
|
134
147
|
},
|
|
135
148
|
listOpenPullRequests: async () => { restCalls.push('listOpenPullRequests'); return []; },
|
|
149
|
+
closePullRequest: async () => { restCalls.push('closePullRequest'); throw new Error('closePullRequest 默认不调用'); },
|
|
136
150
|
...overrides,
|
|
137
151
|
};
|
|
138
152
|
const { prepare, prepareInputs } = makePrepare();
|
|
@@ -159,18 +173,20 @@ function indexJsonOf(call: GitFileWriteCall): { schemaVersion: number; items: Ar
|
|
|
159
173
|
|
|
160
174
|
/* ---------------------------------------------------------------- 首次上传全流程 */
|
|
161
175
|
|
|
162
|
-
test('my-repo: 首次上传全流程 → prepare 元数据自动生成 + 建仓 +
|
|
176
|
+
test('my-repo: 首次上传全流程 → prepare 元数据自动生成 + 建仓 + 写用户仓库(同步)+ 后台收录(fork + 建 PR)', async () => {
|
|
163
177
|
const h = makeHarness();
|
|
164
178
|
const result = await h.service.upload({ zipBytes: ZIP_BYTES, form: form() });
|
|
165
179
|
|
|
180
|
+
// 同步段:推用户仓库立即返回(listing='pending',prUrl 尚不可用)
|
|
166
181
|
assert.equal(result.ok, true);
|
|
167
182
|
assert.equal(result.itemId, 'my-config');
|
|
168
183
|
assert.equal(result.version, '1.0.0');
|
|
169
184
|
assert.equal(result.sha256, 'sha256-abc');
|
|
170
185
|
assert.deepEqual(result.sections, ['settings']);
|
|
171
186
|
assert.equal(result.repoUrl, USER_REPO_URL);
|
|
172
|
-
assert.equal(result.
|
|
173
|
-
assert.equal(result.
|
|
187
|
+
assert.equal(result.listing, 'pending', '上传立即返回 pending(收录后台进行)');
|
|
188
|
+
assert.equal(result.prNumber, null);
|
|
189
|
+
assert.equal(result.prUrl, null);
|
|
174
190
|
assert.ok(result.warnings.length >= 1);
|
|
175
191
|
|
|
176
192
|
// prepare 输入:author=login、version=1.0.0、repoUrl=用户仓库
|
|
@@ -182,14 +198,10 @@ test('my-repo: 首次上传全流程 → prepare 元数据自动生成 + 建仓
|
|
|
182
198
|
assert.equal(pi.repoUrl, USER_REPO_URL);
|
|
183
199
|
assert.equal(pi.now, '2026-08-20T00:00:00.000Z');
|
|
184
200
|
|
|
185
|
-
//
|
|
201
|
+
// 同步段仓库操作:getUser → 读用户 index → repoExists → createPublicRepo → 写用户仓库
|
|
186
202
|
assert.ok(h.restCalls.includes('createPublicRepo'), '首次上传必须自动创建公开仓库');
|
|
187
|
-
assert.
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
// gitWriter 两次调用:用户仓库 + fork 分支
|
|
191
|
-
assert.equal(h.gitCalls.length, 2);
|
|
192
|
-
const [userCall, forkCall] = h.gitCalls as [GitFileWriteCall, GitFileWriteCall];
|
|
203
|
+
assert.equal(h.gitCalls.length, 1, '同步段只写用户仓库(收录在后台)');
|
|
204
|
+
const userCall = h.gitCalls[0]!;
|
|
193
205
|
assert.equal(userCall.repoUrl, USER_REPO_CLONE);
|
|
194
206
|
assert.deepEqual(userCall.entries.map((e) => e.path), [
|
|
195
207
|
'items/my-config/manifest.json', 'items/my-config/config.zip', 'index.json',
|
|
@@ -203,7 +215,17 @@ test('my-repo: 首次上传全流程 → prepare 元数据自动生成 + 建仓
|
|
|
203
215
|
assert.equal(userIndex.items[0]!.version, '1.0.0');
|
|
204
216
|
assert.equal(userIndex.items[0]!.author, LOGIN);
|
|
205
217
|
|
|
206
|
-
//
|
|
218
|
+
// 后台收录(waitForListing 等终态)→ fork + 写官方 index 分支 + 建 PR
|
|
219
|
+
const listing = await h.service.waitForListing('my-config');
|
|
220
|
+
assert.equal(listing.listing, 'done');
|
|
221
|
+
assert.equal(listing.prNumber, 42);
|
|
222
|
+
assert.equal(listing.prUrl, `${OFFICIAL_URL}/pull/42`);
|
|
223
|
+
assert.ok(h.restCalls.includes('ensureFork'), '收录阶段必须 ensureFork 官方仓库');
|
|
224
|
+
assert.ok(h.restCalls.some((c) => c.startsWith('readFile:xiajiajun516/dsh-config-market/index.json')), '必须读官方最新 index');
|
|
225
|
+
|
|
226
|
+
// gitWriter 两次调用:用户仓库 + fork 分支
|
|
227
|
+
assert.equal(h.gitCalls.length, 2);
|
|
228
|
+
const forkCall = h.gitCalls[1]!;
|
|
207
229
|
assert.equal(forkCall.repoUrl, FORK_CLONE);
|
|
208
230
|
assert.equal(forkCall.branch, 'dsh-market-sync/my-config');
|
|
209
231
|
assert.equal(forkCall.baseRef, 'upstream/main');
|
|
@@ -226,9 +248,7 @@ test('my-repo: 首次上传全流程 → prepare 元数据自动生成 + 建仓
|
|
|
226
248
|
|
|
227
249
|
test('my-repo: 更新 → id 不变、version 纯自动 +1、updatedAt 刷新', async () => {
|
|
228
250
|
const existing = USER_INDEX_WITH([{ id: 'my-config', name: 'My Config', version: '1.0.1', author: LOGIN }]);
|
|
229
|
-
const h = makeHarness({
|
|
230
|
-
readFile: async (owner, repo, p) => (owner === LOGIN ? existing : OFFICIAL_INDEX),
|
|
231
|
-
});
|
|
251
|
+
const h = makeHarness({}, { userIndex: existing });
|
|
232
252
|
const result = await h.service.update({ zipBytes: ZIP_BYTES, form: form() });
|
|
233
253
|
|
|
234
254
|
assert.equal(result.ok, true);
|
|
@@ -243,11 +263,44 @@ test('my-repo: 更新 → id 不变、version 纯自动 +1、updatedAt 刷新',
|
|
|
243
263
|
const userIndex = indexJsonOf(h.gitCalls[0]!);
|
|
244
264
|
const entry = userIndex.items.find((it) => it.id === 'my-config')!;
|
|
245
265
|
assert.equal(entry.version, '1.0.2');
|
|
246
|
-
//
|
|
266
|
+
// 后台收录:fork 分支同步新版本
|
|
267
|
+
await h.service.waitForListing('my-config');
|
|
247
268
|
const forkIndex = indexJsonOf(h.gitCalls[1]!);
|
|
248
269
|
assert.equal(forkIndex.items.find((it) => it.id === 'my-config')!.version, '1.0.2');
|
|
249
270
|
});
|
|
250
271
|
|
|
272
|
+
test('my-repo: update 显式 form.id → 按 id 定位(name→slug 失配时不再误为新建)', async () => {
|
|
273
|
+
// 模拟中文名条目:id=config-abc12345(name slug 折叠为空 → hash 兜底),更新时 name 可随意改
|
|
274
|
+
const existing = USER_INDEX_WITH([
|
|
275
|
+
{ id: 'config-abc12345', name: '我的配置', version: '2.1.0', author: LOGIN },
|
|
276
|
+
]);
|
|
277
|
+
const h = makeHarness({
|
|
278
|
+
readFile: async (owner, repo, p) => (owner === LOGIN ? existing : OFFICIAL_INDEX),
|
|
279
|
+
});
|
|
280
|
+
// form.id 显式指向条目 id;name 被用户改了也不影响定位
|
|
281
|
+
const result = await h.service.update({ zipBytes: ZIP_BYTES, form: form({ id: 'config-abc12345', name: '我的配置 v2' }) });
|
|
282
|
+
|
|
283
|
+
assert.equal(result.ok, true);
|
|
284
|
+
assert.equal(result.itemId, 'config-abc12345', '必须按显式 id 更新');
|
|
285
|
+
assert.equal(result.version, '2.1.1', 'version 基于显式 id 条目的旧版本 +1');
|
|
286
|
+
|
|
287
|
+
const userIndex = indexJsonOf(h.gitCalls[0]!);
|
|
288
|
+
assert.ok(userIndex.items.some((it) => it.id === 'config-abc12345' && it.version === '2.1.1'), '显式 id 条目被更新');
|
|
289
|
+
assert.equal(userIndex.items.filter((it) => it.id === 'config-abc12345').length, 1, '不得新增重复条目');
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
test('my-repo: update 显式 form.id 但条目不存在 → 明确报错(不静默新建)', async () => {
|
|
293
|
+
const existing = USER_INDEX_WITH([{ id: 'other-item', name: 'Other', version: '1.0.0', author: LOGIN }]);
|
|
294
|
+
const h = makeHarness({
|
|
295
|
+
readFile: async (owner, repo, p) => (owner === LOGIN ? existing : OFFICIAL_INDEX),
|
|
296
|
+
});
|
|
297
|
+
const result = await h.service.update({ zipBytes: ZIP_BYTES, form: form({ id: 'gone-item', name: 'Gone' }) });
|
|
298
|
+
|
|
299
|
+
assert.equal(result.ok, false);
|
|
300
|
+
assert.equal(result.errorCode, 'item_not_found');
|
|
301
|
+
assert.equal(h.gitCalls.length, 0, '未找到条目必须零推送');
|
|
302
|
+
});
|
|
303
|
+
|
|
251
304
|
/* ---------------------------------------------------------------- PR 复用与重开 */
|
|
252
305
|
|
|
253
306
|
test('my-repo: PR 复用(open PR 未合并)→ 不重复创建', async () => {
|
|
@@ -260,15 +313,19 @@ test('my-repo: PR 复用(open PR 未合并)→ 不重复创建', async () =>
|
|
|
260
313
|
});
|
|
261
314
|
const result = await h.service.upload({ zipBytes: ZIP_BYTES, form: form() });
|
|
262
315
|
|
|
263
|
-
|
|
264
|
-
|
|
316
|
+
// 后台收录完成:复用已有 open PR(head 匹配)
|
|
317
|
+
const listing = await h.service.waitForListing('my-config');
|
|
318
|
+
assert.equal(listing.prNumber, 7);
|
|
319
|
+
assert.equal(listing.prUrl, `${OFFICIAL_URL}/pull/7`);
|
|
265
320
|
assert.equal(h.openPrCalls.length, 0, '已有 open PR 必须复用,不重复创建');
|
|
266
321
|
});
|
|
267
322
|
|
|
268
323
|
test('my-repo: PR 重开(无 open PR / 已合并)→ 新建 PR', async () => {
|
|
269
324
|
const h = makeHarness(); // listOpenPullRequests 默认返回 []
|
|
270
325
|
const result = await h.service.upload({ zipBytes: ZIP_BYTES, form: form() });
|
|
271
|
-
assert.equal(result.
|
|
326
|
+
assert.equal(result.ok, true);
|
|
327
|
+
const listing = await h.service.waitForListing('my-config');
|
|
328
|
+
assert.equal(listing.prNumber, 42);
|
|
272
329
|
assert.equal(h.openPrCalls.length, 1, '无 open PR 必须新建');
|
|
273
330
|
});
|
|
274
331
|
|
|
@@ -287,9 +344,7 @@ test('my-repo: 远端 index 前进防覆盖 → 用户/官方既有条目全部
|
|
|
287
344
|
{ id: 'my-config', name: 'My Config', version: '0.9.0', repo: USER_REPO_URL },
|
|
288
345
|
],
|
|
289
346
|
});
|
|
290
|
-
const h = makeHarness({
|
|
291
|
-
readFile: async (owner, repo, p) => (owner === LOGIN ? userIndexText : officialText),
|
|
292
|
-
});
|
|
347
|
+
const h = makeHarness({}, { userIndex: userIndexText, officialIndex: officialText });
|
|
293
348
|
|
|
294
349
|
// update 同名:旧条目已在,直接 bump 而非新建
|
|
295
350
|
const result = await h.service.update({ zipBytes: ZIP_BYTES, form: form() });
|
|
@@ -303,7 +358,8 @@ test('my-repo: 远端 index 前进防覆盖 → 用户/官方既有条目全部
|
|
|
303
358
|
assert.equal(userIndex.items.find((it) => it.id === 'old-item')!.version, '2.0.0');
|
|
304
359
|
assert.equal(userIndex.items.find((it) => it.id === 'my-config')!.version, '0.9.1');
|
|
305
360
|
|
|
306
|
-
// 官方 index
|
|
361
|
+
// 官方 index(后台收录完成):keep-me 保留 + my-config 更新
|
|
362
|
+
await h.service.waitForListing('my-config');
|
|
307
363
|
const forkIndex = indexJsonOf(h.gitCalls[1]!);
|
|
308
364
|
assert.deepEqual(forkIndex.items.map((it) => it.id), ['keep-me', 'my-config']);
|
|
309
365
|
assert.equal(forkIndex.items.find((it) => it.id === 'my-config')!.version, '0.9.1');
|
|
@@ -330,6 +386,7 @@ test('my-repo: 校验失败(prepare 抛错)→ 零推送、零仓库操作',
|
|
|
330
386
|
readFile: async () => { restCalls.push('readFile'); return null; },
|
|
331
387
|
openPullRequest: async () => { restCalls.push('openPullRequest'); throw new Error('不应调用'); },
|
|
332
388
|
listOpenPullRequests: async () => { restCalls.push('listOpenPullRequests'); return []; },
|
|
389
|
+
closePullRequest: async () => { restCalls.push('closePullRequest'); throw new Error('不应调用'); },
|
|
333
390
|
};
|
|
334
391
|
const service = new MyRepoService({
|
|
335
392
|
prepare: failingPrepare,
|
|
@@ -450,6 +507,140 @@ test('my-repo: listItems 从未上传(用户仓库无 index)→ 空数组',
|
|
|
450
507
|
assert.deepEqual(items, []);
|
|
451
508
|
});
|
|
452
509
|
|
|
510
|
+
/* ---------------------------------------------------------------- 删除条目(关 PR / 下架) */
|
|
511
|
+
|
|
512
|
+
test('my-repo: 删除未收录条目(有 open PR)→ 移除索引与文件 + 关闭收录 PR', async () => {
|
|
513
|
+
const existing = USER_INDEX_WITH([{ id: 'my-config', name: 'My Config', version: '1.0.0', author: LOGIN }]);
|
|
514
|
+
const openPrs = [
|
|
515
|
+
{ number: 7, htmlUrl: `${OFFICIAL_URL}/pull/7`, title: 't', head: prBranchFor('my-config'), state: 'open', merged: false },
|
|
516
|
+
];
|
|
517
|
+
const closedCalls: number[] = [];
|
|
518
|
+
const h = makeHarness({
|
|
519
|
+
readFile: async (owner, repo, p) => (owner === LOGIN ? existing : OFFICIAL_INDEX),
|
|
520
|
+
listOpenPullRequests: async (owner, repo, head) => {
|
|
521
|
+
// 删除场景:先查官方 index(未收录)→ 再查 open PR(命中)
|
|
522
|
+
return openPrs.filter((p) => head === `${LOGIN}:${p.head}`);
|
|
523
|
+
},
|
|
524
|
+
closePullRequest: async (owner, repo, number) => {
|
|
525
|
+
closedCalls.push(number);
|
|
526
|
+
return { number, htmlUrl: `${OFFICIAL_URL}/pull/${number}`, title: 't', head: 'x', state: 'closed', merged: false };
|
|
527
|
+
},
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
const result = await h.service.deleteItem('my-config');
|
|
531
|
+
assert.equal(result.ok, true);
|
|
532
|
+
assert.equal(result.delisted, false, '未收录不触发下架');
|
|
533
|
+
assert.equal(result.prNumber, 7);
|
|
534
|
+
assert.deepEqual(closedCalls, [7], '必须关闭待审核的收录 PR');
|
|
535
|
+
|
|
536
|
+
// 用户仓库写回:index.json 移除条目(删除 items/ 目录由调用方在 writeFiles 前 fs.rm 完成)
|
|
537
|
+
assert.equal(h.gitCalls.length, 1);
|
|
538
|
+
const call = h.gitCalls[0]!;
|
|
539
|
+
assert.equal(call.repoUrl, `${USER_REPO_URL}.git`);
|
|
540
|
+
assert.deepEqual(call.entries.map((e) => e.path), ['index.json']);
|
|
541
|
+
const userIndex = indexJsonOf(call);
|
|
542
|
+
assert.equal(userIndex.items.length, 0, '删除后 index 必须为空');
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
test('my-repo: 删除已收录条目 → 移除本地内容 + 后台异步提下架 PR(独立 delist 分支)', async () => {
|
|
546
|
+
const existing = USER_INDEX_WITH([{ id: 'my-config', name: 'My Config', version: '1.0.0', author: LOGIN }]);
|
|
547
|
+
const officialText = JSON.stringify({
|
|
548
|
+
schemaVersion: 1,
|
|
549
|
+
name: 'DSH 配置市场',
|
|
550
|
+
items: [{ id: 'my-config', name: 'My Config', version: '1.0.0', repo: USER_REPO_URL }],
|
|
551
|
+
});
|
|
552
|
+
const h = makeHarness({
|
|
553
|
+
readFile: async (owner, repo, p) => (owner === LOGIN ? existing : officialText),
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
const result = await h.service.deleteItem('my-config');
|
|
557
|
+
assert.equal(result.ok, true);
|
|
558
|
+
assert.equal(result.delisted, true, '已收录必须触发下架');
|
|
559
|
+
assert.ok(result.warnings.some((w) => w.includes('下架')), '必须提示已提交下架 PR');
|
|
560
|
+
|
|
561
|
+
// 本地删除:index.json 移除条目
|
|
562
|
+
const userIndex = indexJsonOf(h.gitCalls[0]!);
|
|
563
|
+
assert.equal(userIndex.items.length, 0);
|
|
564
|
+
|
|
565
|
+
// 后台下架:独立 delist 分支 + 从官方 index 移除
|
|
566
|
+
const listing = await h.service.waitForListing('my-config');
|
|
567
|
+
assert.equal(listing.listing, 'done');
|
|
568
|
+
assert.equal(h.gitCalls.length, 2);
|
|
569
|
+
const forkCall = h.gitCalls[1]!;
|
|
570
|
+
assert.equal(forkCall.branch, delistBranchFor('my-config'), '下架必须用独立 delist 分支');
|
|
571
|
+
assert.equal(forkCall.force, true);
|
|
572
|
+
const officialIndex = indexJsonOf(forkCall);
|
|
573
|
+
assert.equal(officialIndex.items.length, 0, '官方 index 必须移除该条目');
|
|
574
|
+
// 下架 PR 标题/正文走下架语义
|
|
575
|
+
assert.equal(h.openPrCalls.length, 1);
|
|
576
|
+
assert.ok(h.openPrCalls[0]!.title.includes('下架'), '下架 PR 标题必须区分收录');
|
|
577
|
+
assert.equal(h.openPrCalls[0]!.head, `${LOGIN}:${delistBranchFor('my-config')}`);
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
test('my-repo: 删除不存在的条目 → item_not_found 零推送', async () => {
|
|
581
|
+
const h = makeHarness({
|
|
582
|
+
readFile: async (owner, repo, p) => (owner === LOGIN ? USER_INDEX_EMPTY : OFFICIAL_INDEX),
|
|
583
|
+
});
|
|
584
|
+
const result = await h.service.deleteItem('gone-item');
|
|
585
|
+
assert.equal(result.ok, false);
|
|
586
|
+
assert.equal(result.errorCode, 'item_not_found');
|
|
587
|
+
assert.equal(h.gitCalls.length, 0, '删除不存在的条目必须零推送');
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
/* ---------------------------------------------------------------- relist / listingStatus */
|
|
591
|
+
|
|
592
|
+
test('my-repo: relist 幂等(上传失败后重新提交收录)→ 复用已有 fork + open PR', async () => {
|
|
593
|
+
const openPrs = [
|
|
594
|
+
{ number: 7, htmlUrl: `${OFFICIAL_URL}/pull/7`, title: 't', head: prBranchFor('my-config'), state: 'open', merged: false },
|
|
595
|
+
];
|
|
596
|
+
const h = makeHarness({
|
|
597
|
+
listOpenPullRequests: async (owner, repo, head) => openPrs.filter((p) => head === `${LOGIN}:${p.head}`),
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
// 上传(后台收录完成)
|
|
601
|
+
const up = await h.service.upload({ zipBytes: ZIP_BYTES, form: form() });
|
|
602
|
+
assert.equal(up.ok, true);
|
|
603
|
+
assert.equal(up.itemId, 'my-config');
|
|
604
|
+
await h.service.waitForListing('my-config');
|
|
605
|
+
|
|
606
|
+
// relist:任务已 done → 直接返回当前状态,不重复启动
|
|
607
|
+
const again = await h.service.relist('my-config');
|
|
608
|
+
assert.equal(again.listing, 'done');
|
|
609
|
+
assert.equal(again.prNumber, 7);
|
|
610
|
+
assert.equal(h.openPrCalls.length, 0, 'relist 复用 open PR 不新建');
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
test('my-repo: relist 条目不存在 → item_not_found', async () => {
|
|
614
|
+
const h = makeHarness({
|
|
615
|
+
readFile: async (owner, repo, p) => (owner === LOGIN ? USER_INDEX_EMPTY : OFFICIAL_INDEX),
|
|
616
|
+
});
|
|
617
|
+
await assert.rejects(h.service.relist('gone-item'), (err: unknown) => {
|
|
618
|
+
assert.ok(err instanceof MyRepoError);
|
|
619
|
+
assert.equal(err.code, 'item_not_found');
|
|
620
|
+
return true;
|
|
621
|
+
});
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
test('my-repo: listingStatus 任务表未命中 → 回退 GitHub 实况推导(已收录 → done)', async () => {
|
|
625
|
+
const officialText = JSON.stringify({
|
|
626
|
+
schemaVersion: 1,
|
|
627
|
+
name: 'DSH 配置市场',
|
|
628
|
+
items: [{ id: 'my-config', name: 'My Config', version: '1.0.0', repo: USER_REPO_URL }],
|
|
629
|
+
});
|
|
630
|
+
const h = makeHarness({
|
|
631
|
+
readFile: async (owner, repo, p) => (owner === LOGIN ? USER_INDEX_EMPTY : officialText),
|
|
632
|
+
});
|
|
633
|
+
const status = await h.service.listingStatus('my-config');
|
|
634
|
+
assert.equal(status?.listing, 'done');
|
|
635
|
+
assert.equal(status?.prUrl, null, '官方 index 命中时无 PR 链接');
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
test('my-repo: listingStatus 无任务且无实况 → null', async () => {
|
|
639
|
+
const h = makeHarness();
|
|
640
|
+
const status = await h.service.listingStatus('never-existed');
|
|
641
|
+
assert.equal(status, null);
|
|
642
|
+
});
|
|
643
|
+
|
|
453
644
|
/* ---------------------------------------------------------------- 纯函数 */
|
|
454
645
|
|
|
455
646
|
test('my-repo: slugifyItemId 稳定 slug(中文/符号折叠、字母数字开头、长度上限)', () => {
|