/** * /init/seed 必須把種子的 3.12 三層欄位原樣寫進 KV —— SDD: workflow-discovery task 3.12/3.13 * * 為什麼要有這個測試(別刪): * 3.12 給 `RecipeDefinition` 加了 body_template / response_map / auth / binding_name, * 但 `/init/seed` 當時是**列舉欄位重建** recipe record ⇒ 不在名單上的欄位被靜默吃掉。 * 症狀最惡劣的地方在於「哪裡都不會紅」:recipe 查得到、canonical_id 對、endpoint 對, * 只有跑起來像沒設定過(auth 掉了 ⇒ 走 HTTP 路徑去 fetch「@cf/…」這種不是網址的字串)。 * 這與 2026-08-02 `syncManifest()` 列舉式重建吃掉 `manifest.daemon` 欄是同一型事故—— * 當時的教訓寫著:「**東西還在不在**也要進機械閘」,本檔就是那道閘。 * * 範圍:只驗「種子 → KV」這段(純資料搬運)。真的呼叫 Workers AI 由實例端到端驗。 */ import { describe, it, expect } from 'vitest'; import { env, SELF } from 'cloudflare:test'; import { API_RECIPE_SEEDS } from '../src/lib/api-recipe-seeds'; type StoredRecipe = { canonical_id: string; endpoint: string; auth?: string; binding_name?: string; body_template?: Record; response_map?: { text_path?: string; answer_marker?: string; strip_prefixes?: string[] }; }; async function seedThenRead(canonicalId: string): Promise { const res = await SELF.fetch('https://example.com/init/seed', { method: 'POST' }); // 測試環境沒有 KBDB binding ⇒ portal template 那段必然失敗、整體回 207(誠實回報,非本測目標)。 // 本檔只管 API recipe 那半,所以驗它自己的計數,不驗整體 status。 const body = await res.json<{ api_recipes: { seeded: number; failed: number; errors: string[] } }>(); expect(body.api_recipes.errors).toEqual([]); expect(body.api_recipes.failed).toBe(0); const uuid = await env.RECIPES.get(`idx:installed:${canonicalId}`); expect(uuid, `${canonicalId} 沒有被 seed 進 KV`).toBeTruthy(); return JSON.parse((await env.RECIPES.get(`recipe:${uuid}`))!) as StoredRecipe; } describe('/init/seed 不得靜默吃掉 recipe 的 3.12 欄位', () => { it('workers_ai_chat 種子本身宣告齊四個欄位(種子端)', () => { const seed = API_RECIPE_SEEDS.find(s => s.canonical_id === 'workers_ai_chat'); expect(seed, 'workers_ai_chat 種子不存在=裝完不會有免金鑰問答').toBeDefined(); expect(seed!.auth).toBe('binding'); expect(seed!.binding_name).toBe('AI'); expect(seed!.endpoint.startsWith('@cf/'), 'binding 型的 endpoint=模型 id').toBe(true); expect(seed!.body_template).toBeDefined(); expect(seed!.response_map?.text_path).toBe('response'); }); it('seed 之後 KV 裡讀回來的仍帶 auth/binding_name/body_template/response_map(KV 端)', async () => { const stored = await seedThenRead('workers_ai_chat'); expect(stored.auth, 'auth 掉了 ⇒ 會被當成 HTTP recipe 去 fetch 一個不是網址的字串').toBe('binding'); expect(stored.binding_name).toBe('AI'); expect(stored.body_template, 'body_template 掉了 ⇒ 整包 ctx 被當 payload 送給模型').toBeDefined(); expect(stored.response_map?.text_path, 'response_map 掉了 ⇒ 下游拿不到 text').toBe('response'); expect(stored.response_map?.answer_marker).toBe('【答】'); }); it('既有 HTTP 種子不受影響:沒宣告新欄位就是 undefined,不憑空長出來', async () => { const stored = await seedThenRead('telegram_send'); expect(stored.auth).toBeUndefined(); expect(stored.binding_name).toBeUndefined(); expect(stored.body_template).toBeUndefined(); expect(stored.response_map).toBeUndefined(); expect(stored.endpoint).toContain('api.telegram.org'); }); });