Files
Arcrun/registry/tests/recordAnalytics.test.ts
uncle6me-web ea1c0571c1 步驟6 執行統計回寫:每顆零件跑完回寫 success_rate(task 29,design.md「執行統計設計」)
- registry 新增 POST /analytics/record:ANALYTICS_KV 計數器(stats:{hash_id}:{version})
  為唯一真相源,衍生值(success_rate/avg_duration_ms/call_count)回填 comp: 記錄
  ——查詢讀取端讀哪就寫哪,不開第二真相源。KV 無 CAS,誠實標註非原子。
- cypher-executor execution-evaluator 從 stub 改真實作:執行收尾(/cypher/execute
  成功與 ExecutionError 路徑+webhook 路徑)對 trace 裡每顆 Component 節點
  fire-and-forget 回寫,waitUntil 包、不增加執行同步延遲(仿 recordRecipeStats 慣例)。
  成敗判定=trace error 或 output.success===false(makeHttpRunner 非 2xx 不 throw)。
- registry 位置沿 search-nodes 慣例:REGISTRY_BASE_URL 覆蓋,未設走 wasmWorkerUrl。
- 新增單測 13 個全綠;本地雙 wrangler dev 端到端實測:http_request 跑 5 次
  (3 成功+2 失敗)→ success_rate 1→0.6、call_count 0→5,/cypher/search 同步可見。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 13:45:05 +08:00

100 lines
3.9 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.
// 單元測試:recordAnalytics — 執行統計回寫
// SDD: system-dev/docs/3-specs/arcrun-core-mvp/design.md「執行統計設計」
import { describe, it, expect, beforeEach } from 'vitest';
import { recordAnalytics } from '../src/actions/recordAnalytics';
import type { Bindings } from '../src/types';
// 最小 KV mockget/put/listIn-memory
function makeKv() {
const store = new Map<string, string>();
return {
store,
async get(key: string) { return store.get(key) ?? null; },
async put(key: string, value: string) { store.set(key, value); },
async list({ prefix }: { prefix: string }) {
return { keys: [...store.keys()].filter(k => k.startsWith(prefix)).map(name => ({ name })) };
},
};
}
describe('recordAnalytics', () => {
let submissions: ReturnType<typeof makeKv>;
let analytics: ReturnType<typeof makeKv>;
let env: Bindings;
beforeEach(() => {
submissions = makeKv();
analytics = makeKv();
env = { SUBMISSIONS_KV: submissions, ANALYTICS_KV: analytics } as unknown as Bindings;
// 種一顆零件(indexOnlyComponent 的記錄形狀)
submissions.store.set('idx:http_request', 'cmp_abc12345');
submissions.store.set('comp:cmp_abc12345:v1', JSON.stringify({
component_hash_id: 'cmp_abc12345',
canonical_id: 'http_request',
display_name: 'HTTP Request',
version: 'v1',
success_rate: 1,
avg_duration_ms: 0,
call_count: 0,
visibility: 'public',
status: 'active',
}));
});
it('N 次成功+M 次失敗 → success_rate = success_runs / total_runs,計數器與 comp 記錄同步', async () => {
// 3 成功 + 2 失敗
for (const success of [true, true, true, false, false]) {
const r = await recordAnalytics({ canonical_id: 'http_request', success, duration_ms: 100 }, env);
expect(r.ok).toBe(true);
}
// 真相源:ANALYTICS_KV 計數器
const counters = JSON.parse(analytics.store.get('stats:cmp_abc12345:v1')!);
expect(counters).toEqual({ total_runs: 5, success_runs: 3, total_ms: 500 });
// 讀取端:comp 記錄被回填衍生值
const record = JSON.parse(submissions.store.get('comp:cmp_abc12345:v1')!);
expect(record.success_rate).toBeCloseTo(3 / 5);
expect(record.avg_duration_ms).toBe(100);
expect(record.call_count).toBe(5);
});
it('接受 cmp_ hash_id 直接記錄', async () => {
const r = await recordAnalytics({ canonical_id: 'cmp_abc12345', success: true, duration_ms: 50 }, env);
expect(r.ok).toBe(true);
expect(r.canonical_id).toBe('http_request');
expect(r.total_runs).toBe(1);
});
it('指定 version 時記到該版本', async () => {
submissions.store.set('comp:cmp_abc12345:v2', JSON.stringify({
component_hash_id: 'cmp_abc12345',
canonical_id: 'http_request',
version: 'v2',
success_rate: 1, avg_duration_ms: 0, call_count: 0,
visibility: 'public', status: 'active',
}));
const r = await recordAnalytics({ canonical_id: 'http_request', version: 'v2', success: false, duration_ms: 30 }, env);
expect(r.ok).toBe(true);
expect(r.version).toBe('v2');
expect(analytics.store.has('stats:cmp_abc12345:v2')).toBe(true);
expect(analytics.store.has('stats:cmp_abc12345:v1')).toBe(false);
});
it('不在索引的零件回 ok:false(誠實 404,不假綠)', async () => {
const r = await recordAnalytics({ canonical_id: 'no_such_component', success: true, duration_ms: 1 }, env);
expect(r.ok).toBe(false);
expect(r.error).toContain('不在索引');
});
it('tombstone 版本不被記錄', async () => {
submissions.store.set('comp:cmp_abc12345:v1', JSON.stringify({
component_hash_id: 'cmp_abc12345', canonical_id: 'http_request', version: 'v1', status: 'tombstone',
}));
const r = await recordAnalytics({ canonical_id: 'http_request', success: true, duration_ms: 1 }, env);
expect(r.ok).toBe(false);
});
});