feat(t135): 庫目錄可自主移除+標示還在不在同步
leo:「需要加移除按鈕。因為別人裝錯我沒辦法幫他弄,需要可以自主」
+「你應該要顯示這個庫沒有本地對應的 folder,那就不容易刪錯」
(兩態非三態——leo 二修:「分兩種沒意義」,那是內部狀態不是用戶分類)。
- DELETE /portal/admin/libraries/:id(登記簿)與 by-name/:name(auto 庫需輸入庫名確認)
- kbdb 加 deprecate-by-library(auto 庫移除=標 deprecated,資料保留可還原)
- daemon/libraries 存 active 清單 → 卡片標 🟢同步中/灰目前沒有在同步
- 不自動刪(daemon 可能沒開機);daemon 從未回報時整列不標
vitest 24 passed(1 紅=console HTML 搬遷陳舊測試,非本案)。
(實作=子 CC;驗證+commit=總管。含 t116/t117 先前未 commit 的 graph-executor/wasi-shim 修正)
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
// Cypher Executor 端到端測試
|
||||
import { SELF } from 'cloudflare:test';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { GraphExecutor } from '../src/graph-executor';
|
||||
import type { ComponentRunner, ExecutionGraph } from '../src/types';
|
||||
|
||||
describe('GET /', () => {
|
||||
it('回傳服務狀態', async () => {
|
||||
@@ -191,4 +193,60 @@ describe('POST /execute', () => {
|
||||
});
|
||||
expect(res.status).toBe(400);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
// t117: FOREACH 全部項目失敗 → 錯誤訊息含 status code(GraphExecutor 單元測試)
|
||||
describe('t117: FOREACH 全項失敗 → ExecutionError 含 status code', () => {
|
||||
it('FOREACH 所有項目 success:false(含 status 401)→ executor.execute() 拋出含 "401" 的錯誤', async () => {
|
||||
// mock loader:任何零件都回 {success:false, status:401, error:"HTTP 401"}
|
||||
const failLoader = async (_: string): Promise<ComponentRunner> =>
|
||||
async () => ({ success: false, status: 401, error: 'HTTP 401', data: { body: 'Unauthorized' } });
|
||||
|
||||
const executor = new GraphExecutor(failLoader);
|
||||
|
||||
const graph: ExecutionGraph = {
|
||||
id: 'foreach-fail-t117',
|
||||
name: 'FOREACH 全失敗',
|
||||
nodes: [
|
||||
{ id: 'input', type: 'Input', data: { items: ['a', 'b'] } },
|
||||
{ id: 'writer', type: 'Component', componentId: 'http_request' },
|
||||
],
|
||||
edges: [
|
||||
{ from: 'input', to: 'writer', type: 'FOREACH', iterator: 'item' },
|
||||
],
|
||||
};
|
||||
|
||||
// t117 核心驗證:全部失敗 → throw(不再靜默)
|
||||
await expect(executor.execute(graph, {})).rejects.toThrow(/401/);
|
||||
});
|
||||
|
||||
it('FOREACH 部分項目成功 → 不拋出(只有全部失敗才報錯)', async () => {
|
||||
let callCount = 0;
|
||||
// 第一次呼叫失敗,第二次成功(部分失敗不觸發 t117 all-fail 路徑)
|
||||
const mixedLoader = async (_: string): Promise<ComponentRunner> =>
|
||||
async () => {
|
||||
callCount++;
|
||||
if (callCount === 1) return { success: false, status: 401, error: 'HTTP 401' };
|
||||
return { success: true, data: { ok: true } };
|
||||
};
|
||||
|
||||
const executor = new GraphExecutor(mixedLoader);
|
||||
|
||||
const graph: ExecutionGraph = {
|
||||
id: 'foreach-mixed-t117',
|
||||
name: 'FOREACH 部分失敗',
|
||||
nodes: [
|
||||
{ id: 'input', type: 'Input', data: { items: ['a', 'b'] } },
|
||||
{ id: 'writer', type: 'Component', componentId: 'http_request' },
|
||||
],
|
||||
edges: [
|
||||
{ from: 'input', to: 'writer', type: 'FOREACH', iterator: 'item' },
|
||||
],
|
||||
};
|
||||
|
||||
// 部分失敗 → 不拋出,正常回傳 results 陣列
|
||||
const result = await executor.execute(graph, {});
|
||||
expect(result).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -395,6 +395,82 @@ describe('/portal/admin/libraries', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// ═══════════════ t135 庫目錄移除 ═══════════════
|
||||
|
||||
describe('DELETE /portal/admin/libraries(t135)', () => {
|
||||
it('DELETE /:id — 成功移除已登記庫;KBDB /records/:id DELETE 被呼叫', async () => {
|
||||
await seedAdminSession();
|
||||
mockGetRecord('rec_admin', adminValues());
|
||||
// 成員驗證:list by template 回有該 record
|
||||
mockListByTemplate('portal_library', [
|
||||
{ record_id: 'rec_lib1', values: { name: 'finance', display_name: '財務庫', status: 'active' } },
|
||||
]);
|
||||
let deleteCalled = false;
|
||||
fetchMock
|
||||
.get(KBDB)
|
||||
.intercept({ path: '/records/rec_lib1', method: 'DELETE' })
|
||||
.reply(200, () => { deleteCalled = true; return { success: true }; });
|
||||
const res = await json('DELETE', '/portal/admin/libraries/rec_lib1', undefined, { Authorization: 'Bearer tok-admin' });
|
||||
expect(res.status).toBe(200);
|
||||
const data = (await res.json()) as { success: boolean; name: string; message: string };
|
||||
expect(data.success).toBe(true);
|
||||
expect(data.name).toBe('finance');
|
||||
expect(deleteCalled).toBe(true);
|
||||
});
|
||||
|
||||
it('DELETE /:id — 庫不在目錄 → 404', async () => {
|
||||
await seedAdminSession();
|
||||
mockGetRecord('rec_admin', adminValues());
|
||||
mockListByTemplate('portal_library', []); // 空目錄
|
||||
const res = await json('DELETE', '/portal/admin/libraries/rec_lib_x', undefined, { Authorization: 'Bearer tok-admin' });
|
||||
expect(res.status).toBe(404);
|
||||
});
|
||||
|
||||
it('DELETE /:id — 非 admin → 403', async () => {
|
||||
await seedAdminSession('tok-user', 'rec_u1');
|
||||
mockGetRecord('rec_u1', userValues());
|
||||
const res = await json('DELETE', '/portal/admin/libraries/rec_lib1', undefined, { Authorization: 'Bearer tok-user' });
|
||||
expect(res.status).toBe(403);
|
||||
});
|
||||
|
||||
it('DELETE /by-name/:name — confirm 符合 → 呼叫 KBDB deprecate-by-library', async () => {
|
||||
await seedAdminSession();
|
||||
mockGetRecord('rec_admin', adminValues());
|
||||
let deprecateCalled = false;
|
||||
fetchMock
|
||||
.get(KBDB)
|
||||
.intercept({ path: '/entries/deprecate-by-library', method: 'PATCH' })
|
||||
.reply(200, () => { deprecateCalled = true; return { success: true, deprecated_count: 12 }; });
|
||||
const res = await json('DELETE', '/portal/admin/libraries/by-name/kb', { confirm: 'kb' }, { Authorization: 'Bearer tok-admin' });
|
||||
expect(res.status).toBe(200);
|
||||
const data = (await res.json()) as { success: boolean; deprecated_count: number };
|
||||
expect(data.success).toBe(true);
|
||||
expect(data.deprecated_count).toBe(12);
|
||||
expect(deprecateCalled).toBe(true);
|
||||
});
|
||||
|
||||
it('DELETE /by-name/:name — 無 confirm → 400', async () => {
|
||||
await seedAdminSession();
|
||||
mockGetRecord('rec_admin', adminValues());
|
||||
const res = await json('DELETE', '/portal/admin/libraries/by-name/kb', {}, { Authorization: 'Bearer tok-admin' });
|
||||
expect(res.status).toBe(400);
|
||||
});
|
||||
|
||||
it('DELETE /by-name/:name — confirm 不符 → 400', async () => {
|
||||
await seedAdminSession();
|
||||
mockGetRecord('rec_admin', adminValues());
|
||||
const res = await json('DELETE', '/portal/admin/libraries/by-name/kb', { confirm: 'wrong' }, { Authorization: 'Bearer tok-admin' });
|
||||
expect(res.status).toBe(400);
|
||||
});
|
||||
|
||||
it('DELETE /by-name/:name — 非 admin → 403', async () => {
|
||||
await seedAdminSession('tok-user', 'rec_u1');
|
||||
mockGetRecord('rec_u1', userValues());
|
||||
const res = await json('DELETE', '/portal/admin/libraries/by-name/kb', { confirm: 'kb' }, { Authorization: 'Bearer tok-user' });
|
||||
expect(res.status).toBe(403);
|
||||
});
|
||||
});
|
||||
|
||||
// ═══════════════ 6. t122 萃取引擎金鑰雲端下發 ═══════════════
|
||||
|
||||
describe('/portal/admin/extractor + /portal/daemon/config 萃取引擎(t122)', () => {
|
||||
|
||||
Reference in New Issue
Block a user