feat: add optimistic locking and improve type safety

- Implement optimistic locking for deposit balance updates
  - Prevent race conditions in concurrent deposit requests
  - Add automatic retry with exponential backoff (max 3 attempts)
  - Add version column to user_deposits table

- Improve type safety across codebase
  - Add explicit types for Namecheap API responses
  - Add typed function arguments (ManageDepositArgs, etc.)
  - Remove `any` types from deposit-agent and tool files

- Add reconciliation job for balance integrity verification
  - Compare user_deposits.balance vs SUM(confirmed transactions)
  - Alert admin on discrepancy detection

- Set up test environment with Vitest + Miniflare
  - Add 50+ test cases for deposit system
  - Add helper functions for test data creation

- Update documentation
  - Add migration guide for version columns
  - Document optimistic locking patterns

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
kappa
2026-01-19 23:23:09 +09:00
parent 8d0fe30722
commit f5df0c0ffe
21 changed files with 13448 additions and 169 deletions

View File

@@ -1,4 +1,11 @@
import type { Env } from '../types';
import type {
Env,
OpenAIResponse,
BraveSearchResponse,
BraveSearchResult,
Context7SearchResponse,
Context7DocsResponse
} from '../types';
import { retryWithBackoff, RetryError } from '../utils/retry';
import { createLogger } from '../utils/logger';
import { getOpenAIUrl } from '../utils/api-urls';
@@ -85,7 +92,7 @@ export async function executeSearchWeb(args: { query: string }, env?: Env): Prom
{ maxRetries: 2 } // 번역은 중요하지 않으므로 재시도 2회로 제한
);
if (translateRes.ok) {
const translateData = await translateRes.json() as any;
const translateData = await translateRes.json() as OpenAIResponse;
translatedQuery = translateData.choices?.[0]?.message?.content?.trim() || query;
logger.info('번역', { original: query, translated: translatedQuery });
}
@@ -112,7 +119,7 @@ export async function executeSearchWeb(args: { query: string }, env?: Env): Prom
if (!response.ok) {
return `🔍 검색 오류: ${response.status}`;
}
const data = await response.json() as any;
const data = await response.json() as BraveSearchResponse;
// Web 검색 결과 파싱
const webResults = data.web?.results || [];
@@ -120,7 +127,7 @@ export async function executeSearchWeb(args: { query: string }, env?: Env): Prom
return `🔍 "${query}"에 대한 검색 결과가 없습니다.`;
}
const results = webResults.slice(0, 3).map((r: any, i: number) =>
const results = webResults.slice(0, 3).map((r: BraveSearchResult, i: number) =>
`${i + 1}. <b>${r.title}</b>\n ${r.description}\n ${r.url}`
).join('\n\n');
@@ -149,7 +156,7 @@ export async function executeLookupDocs(args: { library: string; query: string }
() => fetch(searchUrl),
{ maxRetries: 3 }
);
const searchData = await searchResponse.json() as any;
const searchData = await searchResponse.json() as Context7SearchResponse;
if (!searchData.libraries?.length) {
return `📚 "${library}" 라이브러리를 찾을 수 없습니다.`;
@@ -163,7 +170,7 @@ export async function executeLookupDocs(args: { library: string; query: string }
() => fetch(docsUrl),
{ maxRetries: 3 }
);
const docsData = await docsResponse.json() as any;
const docsData = await docsResponse.json() as Context7DocsResponse;
if (docsData.error) {
return `📚 문서 조회 실패: ${docsData.message || docsData.error}`;