Skip to content

Commit afe641b

Browse files
committed
fix: harden skills market delete flow
1 parent 820b029 commit afe641b

6 files changed

Lines changed: 76 additions & 0 deletions

File tree

tests/e2e/run.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ async function main() {
4040
...process.env,
4141
HOME: tmpHome,
4242
USERPROFILE: tmpHome,
43+
CODEX_HOME: '',
44+
CLAUDE_HOME: '',
45+
CLAUDE_CONFIG_DIR: '',
46+
XDG_CONFIG_HOME: '',
4347
CODEXMATE_FORCE_RESET_EXISTING_CONFIG: '1',
4448
CODEXMATE_NO_BROWSER: '1'
4549
};

tests/unit/skills-market-runtime.test.mjs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,3 +309,47 @@ test('skills import entrypoints return early while scan is in progress', async (
309309
assert.strictEqual(uploadCalls, 0);
310310
assert.deepStrictEqual(vm.messageLog, []);
311311
});
312+
313+
test('deleteSelectedSkills reports busy state while import sources scan is running', async () => {
314+
let confirmCalls = 0;
315+
let apiCalls = 0;
316+
const vm = buildVm(async () => {
317+
apiCalls += 1;
318+
return {};
319+
}, {
320+
skillsScanningImports: true,
321+
skillsSelectedNames: ['alpha'],
322+
requestConfirmDialog: async () => {
323+
confirmCalls += 1;
324+
return true;
325+
}
326+
});
327+
328+
await vm.deleteSelectedSkills();
329+
330+
assert.strictEqual(confirmCalls, 0);
331+
assert.strictEqual(apiCalls, 0);
332+
assert.deepStrictEqual(vm.messageLog, [{
333+
message: '正在扫描导入源,请稍后再试',
334+
type: 'error'
335+
}]);
336+
});
337+
338+
test('deleteSelectedSkills wires confirm dialog disabled state to live scan status', async () => {
339+
let confirmOptions = null;
340+
const vm = buildVm(async () => ({}), {
341+
skillsSelectedNames: ['alpha'],
342+
requestConfirmDialog: async (options) => {
343+
confirmOptions = options;
344+
return false;
345+
}
346+
});
347+
348+
await vm.deleteSelectedSkills();
349+
350+
assert(confirmOptions, 'confirm dialog should be opened');
351+
assert.strictEqual(typeof confirmOptions.confirmDisabled, 'function');
352+
assert.strictEqual(confirmOptions.confirmDisabled.call(vm), false);
353+
vm.skillsScanningImports = true;
354+
assert.strictEqual(confirmOptions.confirmDisabled.call(vm), true);
355+
});

tests/unit/skills-modal-ui.test.mjs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,10 @@ test('skills modal script is modularized and exposes computed/methods from skill
3232
assert.match(appScript, /\.\.\.createSkillsComputed\(\)/);
3333
assert.match(appScript, /\.\.\.createSkillsMethods\(\{ api \}\)/);
3434
assert.match(appScript, /showConfirmDialog:\s*false/);
35+
assert.match(appScript, /confirmDialogConfirmDisabled:\s*false/);
36+
assert.match(appScript, /confirmDialogDisableWhen:\s*null/);
3537
assert.match(appScript, /requestConfirmDialog\(/);
38+
assert.match(appScript, /isConfirmDialogDisabled\(\)/);
3639
assert.match(appScript, /resolveConfirmDialog\(/);
3740

3841
assert.match(skillsComputed, /skillsConfiguredCount\(\)/);
@@ -62,6 +65,7 @@ test('skills modal script is modularized and exposes computed/methods from skill
6265

6366
test('skills modal styles define summary and panel layout hooks', () => {
6467
const styles = readProjectFile('web-ui/styles.css');
68+
const html = readProjectFile('web-ui/index.html');
6569
assert.match(styles, /\.form-select/);
6670
assert.match(styles, /\.skills-summary-strip/);
6771
assert.match(styles, /\.skills-summary-item/);
@@ -75,4 +79,5 @@ test('skills modal styles define summary and panel layout hooks', () => {
7579
assert.match(styles, /\.market-target-chip/);
7680
assert.doesNotMatch(styles, /\.market-online-toolbar/);
7781
assert.doesNotMatch(styles, /\.market-ecosystem-card/);
82+
assert.match(html, /:disabled="isConfirmDialogDisabled\(\)"/);
7883
});

web-ui/app.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,8 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
144144
confirmDialogConfirmText: '确认',
145145
confirmDialogCancelText: '取消',
146146
confirmDialogDanger: false,
147+
confirmDialogConfirmDisabled: false,
148+
confirmDialogDisableWhen: null,
147149
confirmDialogResolver: null,
148150
configTemplateContent: '',
149151
configTemplateApplying: false,
@@ -3554,6 +3556,7 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
35543556
if (typeof this.confirmDialogResolver === 'function') {
35553557
this.confirmDialogResolver(false);
35563558
}
3559+
const confirmDisabled = options.confirmDisabled;
35573560
this.confirmDialogTitle = typeof options.title === 'string' && options.title.trim()
35583561
? options.title.trim()
35593562
: '请确认操作';
@@ -3565,11 +3568,23 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
35653568
? options.cancelText.trim()
35663569
: '取消';
35673570
this.confirmDialogDanger = !!options.danger;
3571+
this.confirmDialogConfirmDisabled = typeof confirmDisabled === 'function' ? false : !!confirmDisabled;
3572+
this.confirmDialogDisableWhen = typeof confirmDisabled === 'function' ? confirmDisabled : null;
35683573
this.showConfirmDialog = true;
35693574
return new Promise((resolve) => {
35703575
this.confirmDialogResolver = resolve;
35713576
});
35723577
},
3578+
isConfirmDialogDisabled() {
3579+
if (typeof this.confirmDialogDisableWhen === 'function') {
3580+
try {
3581+
return !!this.confirmDialogDisableWhen.call(this);
3582+
} catch (_) {
3583+
return false;
3584+
}
3585+
}
3586+
return !!this.confirmDialogConfirmDisabled;
3587+
},
35733588
resolveConfirmDialog(confirmed) {
35743589
const resolver = typeof this.confirmDialogResolver === 'function'
35753590
? this.confirmDialogResolver
@@ -3580,6 +3595,8 @@ import { createSkillsMethods } from './modules/skills.methods.mjs';
35803595
this.confirmDialogConfirmText = '确认';
35813596
this.confirmDialogCancelText = '取消';
35823597
this.confirmDialogDanger = false;
3598+
this.confirmDialogConfirmDisabled = false;
3599+
this.confirmDialogDisableWhen = null;
35833600
this.confirmDialogResolver = null;
35843601
if (resolver) {
35853602
resolver(!!confirmed);

web-ui/index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2326,6 +2326,7 @@ <h1 class="main-title">
23262326
<button class="btn btn-cancel" @click="closeConfirmDialog">{{ confirmDialogCancelText }}</button>
23272327
<button
23282328
:class="['btn', 'btn-confirm', confirmDialogDanger ? 'btn-danger' : '']"
2329+
:disabled="isConfirmDialogDisabled()"
23292330
@click="resolveConfirmDialog(true)">
23302331
{{ confirmDialogConfirmText }}
23312332
</button>

web-ui/modules/skills.methods.mjs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -413,6 +413,10 @@ export function createSkillsMethods({ api }) {
413413

414414
async deleteSelectedSkills() {
415415
if (this.skillsDeleting || this.skillsZipImporting || this.skillsExporting || this.skillsImporting) return;
416+
if (this.skillsScanningImports) {
417+
this.showMessage('正在扫描导入源,请稍后再试', 'error');
418+
return;
419+
}
416420
const selected = Array.isArray(this.skillsSelectedNames)
417421
? Array.from(new Set(this.skillsSelectedNames.map((item) => String(item || '').trim()).filter(Boolean)))
418422
: [];
@@ -425,6 +429,7 @@ export function createSkillsMethods({ api }) {
425429
message: `确认从 ${this.skillsTargetLabel} 删除 ${selected.length} 个 skill 吗?此操作不可撤销。`,
426430
confirmText: '删除',
427431
cancelText: '取消',
432+
confirmDisabled: () => this.skillsDeleting || this.skillsScanningImports,
428433
danger: true
429434
});
430435
if (!confirmed) {

0 commit comments

Comments
 (0)