/** * POST /console/triage-check route 測試(分流台勾掉/還原,leo 2026-07-08 拍板) * * 驗證 IO 接線(改寫邏輯本身在 console-triage-model.test.ts 的 applyTriageCheck 純函式測試): * 1. session 閘:無/壞 Bearer → 401(沿用 triage-data 同款 validateConsoleSession) * 2. 整串回寫:先 GET 原 entry → PATCH content 帶完整 JSON(text/marker/owner_tier 等 * 原樣保留 + status:done + checked_via:console + checked_at)——防 PATCH 蓋掉別的欄位 * 3. 邊界:owner_id 非 console 租戶 → 404 不 PATCH;entry_type 非 todo/inbox → 400 不 PATCH * 4. restore:status 回 new、checked_via/checked_at 移除 * * KBDB 打 fetchMock 假 host(wrangler.test.toml KBDB_BASE_URL=https://kbdb.test)+ * disableNetConnect——測試絕不外連(更不會碰官方 uncle6 fallback)。 */ import { SELF, env, fetchMock } from 'cloudflare:test'; import { beforeAll, beforeEach, afterEach, describe, it, expect } from 'vitest'; const TOKEN = 'test-session-token'; const AUTH = { Authorization: `Bearer ${TOKEN}` }; beforeAll(() => { fetchMock.activate(); fetchMock.disableNetConnect(); }); afterEach(() => fetchMock.assertNoPendingInterceptors()); beforeEach(async () => { // isolatedStorage:每個測試自己 seed session(console-auth.ts 的 key 規約 console_sess:) await env.SESSIONS_KV.put(`console_sess:${TOKEN}`, JSON.stringify({ created_at: Date.now() })); }); function post(body: unknown, headers: Record = AUTH) { return SELF.fetch('http://localhost/console/triage-check', { method: 'POST', headers: { 'Content-Type': 'application/json', ...headers }, body: JSON.stringify(body), }); } /** mock GET /entries/:id(KBDB base 回應形狀 {success, entry}) */ function mockGet(id: string, entry: Record) { fetchMock .get('https://kbdb.test') .intercept({ path: `/entries/${id}`, method: 'GET' }) .reply(200, { success: true, entry }); } describe('POST /console/triage-check — session 閘', () => { it('無 Bearer → 401,不碰 KBDB', async () => { const res = await post({ entry_id: 'e1' }, {}); expect(res.status).toBe(401); }); it('壞 token → 401', async () => { const res = await post({ entry_id: 'e1' }, { Authorization: 'Bearer wrong-token' }); expect(res.status).toBe(401); }); it('entry_id 缺 → 400', async () => { const res = await post({}); expect(res.status).toBe(400); }); }); describe('POST /console/triage-check — check(勾掉=console 端終局銷案)', () => { it('先 GET 原 entry 再整串回寫:其餘欄位原樣保留 + status/checked_via/checked_at', async () => { mockGet('e1', { id: 'e1', owner_id: 'leo', entry_type: 'todo', content: JSON.stringify({ text: '把憑證輪替', marker: 'TODO', owner_tier: 'ai', project: 'Arcrun', source: 'notes', status: 'new' }), }); let patched = ''; fetchMock .get('https://kbdb.test') .intercept({ path: '/entries/e1', method: 'PATCH' }) .reply(200, (opts) => { patched = String(opts.body); return { success: true }; }); const res = await post({ entry_id: 'e1' }); const data = (await res.json()) as Record; expect(res.status).toBe(200); expect(data.success).toBe(true); expect(data.status).toBe('done'); const sent = JSON.parse(patched) as { content: string }; const content = JSON.parse(sent.content) as Record; expect(content.text).toBe('把憑證輪替'); expect(content.marker).toBe('TODO'); expect(content.owner_tier).toBe('ai'); expect(content.project).toBe('Arcrun'); expect(content.source).toBe('notes'); expect(content.status).toBe('done'); expect(content.checked_via).toBe('console'); // 終局標記:萃取端看到絕不復活 expect(typeof content.checked_at).toBe('string'); }); it('owner_id 非 console 租戶 → 404(不洩漏他租戶存在性),不發 PATCH', async () => { mockGet('e2', { id: 'e2', owner_id: 'someone-else', entry_type: 'todo', content: '{}' }); const res = await post({ entry_id: 'e2' }); expect(res.status).toBe(404); }); it('entry_type 非 todo/inbox → 400(不是泛用 entry 改寫器),不發 PATCH', async () => { mockGet('e3', { id: 'e3', owner_id: 'leo', entry_type: 'wiki_card', content: '{}' }); const res = await post({ entry_id: 'e3' }); expect(res.status).toBe(400); }); it('KBDB 找不到 entry → 404', async () => { fetchMock .get('https://kbdb.test') .intercept({ path: '/entries/gone', method: 'GET' }) .reply(404, { success: false, error: 'not found' }); const res = await post({ entry_id: 'gone' }); expect(res.status).toBe(404); }); }); describe('POST /console/triage-check — restore(誤勾救濟)', () => { it('status 回 new、checked_via/checked_at 移除、其餘欄位保留(inbox 也適用)', async () => { mockGet('e4', { id: 'e4', owner_id: 'leo', entry_type: 'inbox', content: JSON.stringify({ text: '誤勾的', from: 'leo', status: 'done', checked_via: 'console', checked_at: '2026-07-08T11:00:00.000Z' }), }); let patched = ''; fetchMock .get('https://kbdb.test') .intercept({ path: '/entries/e4', method: 'PATCH' }) .reply(200, (opts) => { patched = String(opts.body); return { success: true }; }); const res = await post({ entry_id: 'e4', action: 'restore' }); const data = (await res.json()) as Record; expect(res.status).toBe(200); expect(data.status).toBe('new'); const content = JSON.parse((JSON.parse(patched) as { content: string }).content) as Record; expect(content).toEqual({ text: '誤勾的', from: 'leo', status: 'new' }); }); });