Files
Arcrun/cypher-executor/tests/init-seed-recipe-fields.test.ts
Leo 47c6aaea03 feat(t152): workers_ai_chat 種子(auth: binding,免金鑰)+ 修 /init/seed 吃掉 3.12 欄位+ 修 D1 LIKE 長查詢 500
SDD: workflow-discovery 3.12/3.13(不是新規格;3.12 已 confirmed 並實作完成)

## 1) workers_ai_chat 種子(新)
Cloudflare Workers AI 走 env.AI binding ⇒ 用戶不必填任何 API 金鑰就能問答。
放種子表而非產品安裝器:「裝好後預設有哪些 recipe」是平台能力(rule 07 薄殼原則)。
換模型/換供應商=改這一筆 recipe,workflow 不動。

選型實測(1.4.4 實例,真實長度 RAG prompt,每個模型連跑 2 次):
  llama-4-scout-17b        2373/2173 ms   答案最完整、引用正確 ← 選它
  llama-3.3-70b-fp8-fast   3261/2147 ms   可用但波動較大
  mistral-small-3.1-24b    3560/3631 ms
  qwen2.5-coder-32b        3572/3353 ms
  gpt-oss-120b             1971/2295 ms   回應形狀不同,response 取不到文字
  gemma-3-12b-it            5018 帳號無權限
對照舊路徑 Gemini gemma-4-31b-it:同型提問 16.87 s,且吐整段英文思考草稿。

## 2) 修 /init/seed 靜默吃掉 3.12 欄位
3.12 給 RecipeDefinition 加了 body_template/response_map/auth/binding_name,
但 /init/seed 是**列舉欄位重建** recipe record ⇒ 不在名單上的欄位被丟掉。
最惡劣的地方是「哪裡都不會紅」:recipe 查得到、endpoint 對,只有跑起來像沒設定過。
與 08-02 syncManifest 吃掉 manifest.daemon 欄同型(教訓:東西還在不在也要進機械閘)。
加 tests/init-seed-recipe-fields.test.ts:拿掉修復會紅、補回會綠(已實測會擋)。

## 3) 修 D1 LIKE pattern 50 bytes 上限造成的 500
/entries/search?q=… 只要 q 超過 48 bytes 就回 HTTP 500,沒有錯誤訊息。
逐 byte 二分:48→200/49→500;中文 16 字→200/17 字→500。
判別實驗:q 固定 48 bytes、其他 filter 全塞滿讓 SQL 變很長 → 仍 200
⇒ 爆的是 LIKE 的 pattern('%'+q+'%' = 50),不是 statement 長度。
中文問句超過 16 字是常態,而 rag_chat 用整句問題當 q ⇒ 聊天對正常問句等於不能用。
(=InkStoneCo status.md 待辦第 1 條「KBDB keyword 長查詢會炸」的根因。)
修法:q ≤ 48 bytes 走原路(行為逐字不變),超過才拆詞/切 UTF-8 邊界片段。
kbdb 全套 83 測全綠(含新增 8 項)。

## 4) 順手
- 移除被 commit 進 repo 的 node_modules 壞 symlink(指向 leo Mac 的絕對路徑,
  害任何 fresh clone 裝不起來、切分支還會把裝好的蓋掉——本次撞了兩次)。
- pending-changes.md 加 P2 提案(fan-out 並行執行)+等裁決,未動引擎。

驗證:cypher-executor 新增測試 17/17 綠;tsc 與基線逐字相同;
全套測試失敗集合與基線**逐字相同**(基線 14 個失敗,本分支 t173 既有,非本次引入)。
2026-08-03 02:56:53 +00:00

68 lines
3.8 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* /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<string, unknown>;
response_map?: { text_path?: string; answer_marker?: string; strip_prefixes?: string[] };
};
async function seedThenRead(canonicalId: string): Promise<StoredRecipe> {
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_mapKV 端)', 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');
});
});