diff options
| author | Somhairle H. Marisol <[email protected]> | 2026-09-17 14:32:37 +0800 |
|---|---|---|
| committer | Somhairle H. Marisol <[email protected]> | 2026-09-17 14:32:37 +0800 |
| commit | 5c0ba37eda80d39e6ceca59bb1d5f4942f858995 (patch) | |
| tree | 948723f9cedf7ccb0707fa6ee516bd30fe20fd10 /frontend/src/lib/strategyTab.test.ts | |
| download | strategy-lab-5c0ba37eda80d39e6ceca59bb1d5f4942f858995.tar.gz | |
chore: establish Strategy Lab source baseline (development, not release)
Diffstat (limited to 'frontend/src/lib/strategyTab.test.ts')
| -rw-r--r-- | frontend/src/lib/strategyTab.test.ts | 95 |
1 files changed, 95 insertions, 0 deletions
diff --git a/frontend/src/lib/strategyTab.test.ts b/frontend/src/lib/strategyTab.test.ts new file mode 100644 index 0000000..a5bf6d8 --- /dev/null +++ b/frontend/src/lib/strategyTab.test.ts @@ -0,0 +1,95 @@ +// @ts-nocheck +import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest'; +import { mount, unmount } from 'svelte'; +// NOTE: `resolve.conditions: ['browser']` lets vitest resolve svelte's client +// build so actual component mounting works in jsdom. +import { putDraft } from '../lib/client'; +import { ApiError } from '../lib/api'; + +// Live regression (artifacts/qa/supervisor-live/browser-strategy.json): +// ProjectStrategyTab rendered `c(...).slice is not a function` pageerror and a +// blank main because the backend sends draft_generation as a NUMBER (i64) while +// the frontend typed it as a string and called .slice on it. Backend keeps the +// integer; these tests exercise the ACTUAL numeric API shape through the real +// fetch client and the REAL component render in jsdom (no source-string-only +// assertions). + +const PROJECT_ID = '7630e296-0b2f-44c3-85f1-fe695e7a7e4b'; + +const json = (v, status = 200) => + new Response(JSON.stringify(v), { status, headers: { 'content-type': 'application/json' } }); + +function projectBody(generation) { + return { + id: PROJECT_ID, name: '端到端验收项目', description: null, + draft_code: 'import backtrader as bt\nclass Strategy(bt.Strategy):\n pass\n', + draft_generation: generation, // NUMERIC contract from the backend (i64) + created_at: '2026-01-01T00:00:00Z', updated_at: '2026-01-01T00:00:00Z', + }; +} + +async function mountStrategyTab(fetchImpl) { + vi.stubGlobal('fetch', fetchImpl); + const ProjectStrategyTab = (await import('../pages/ProjectStrategyTab.svelte')).default; + const host = document.createElement('div'); + document.body.appendChild(host); + const inst = mount(ProjectStrategyTab, { target: host, props: { projectId: PROJECT_ID } }); + // flush effects/microtasks + await new Promise((r) => setTimeout(r, 50)); + return { host, inst, cleanup: () => { try { unmount(inst); } catch {} host.remove(); vi.unstubAllGlobals(); } }; +} + +function handler() { + return (url, _opts = {}) => { + if (/\/api\/projects\/[0-9a-f-]+$/.test(String(url))) + return Promise.resolve(json(projectBody(3))); + if (/\/versions/.test(String(url))) + return Promise.resolve(json({ items: [] })); + return Promise.resolve(json({})); + }; +} + +describe('strategy tab numeric generation contract (pageerror regression)', () => { + beforeEach(() => { document.body.innerHTML = ''; }); + afterEach(() => vi.unstubAllGlobals()); + + it('full numeric flow renders editor + 草稿代 3 without .slice crash', async () => { + const { host, cleanup } = await mountStrategyTab(handler()); + try { + const text = host.textContent; + // editor content from the numeric flow (component reached rendering) + expect(text).toContain('class Strategy(bt.Strategy)'); + // generation renders as the actual number, never a truncated-slice crash + expect(text).toMatch(/草稿代\s*3(?:\s|$|·)/); + expect(text).not.toContain('slice is not a function'); + } finally { cleanup(); } + }); + + it('client PUT sends numeric expected_generation and accepts numeric response', async () => { + const seen = []; + vi.stubGlobal('fetch', (url, opts = {}) => { + seen.push([String(url), opts]); + return Promise.resolve(json({ id: PROJECT_ID, draft_code: 'x=1', draft_generation: 4 })); + }); + const res = await putDraft(PROJECT_ID, 'x=1', 3); + expect(res.draft_generation).toBe(4); + expect(typeof res.draft_generation).toBe('number'); + const [, sentOpts] = seen[0]; + const sentBody = typeof sentOpts.body === 'string' ? JSON.parse(sentOpts.body) : sentOpts.body; + expect(sentBody.expected_generation).toBe(3); + expect(typeof sentBody.expected_generation).toBe('number'); + }); + + it('409 conflict keeps stale_generation code (numeric retry flow preserved)', async () => { + vi.stubGlobal('fetch', (url) => { + if (/\/draft$/.test(String(url))) + return Promise.resolve(new Response(JSON.stringify({ error: { code: 'stale_generation', message: 'g3' } }), { status: 409, headers: { 'content-type': 'application/json' } })); + return Promise.resolve(json({})); + }); + let err; + await putDraft(PROJECT_ID, 'stale', 2).catch((e) => { err = e; }); + expect(err).toBeInstanceOf(ApiError); + expect(err.status).toBe(409); + expect(err.code).toBe('stale_generation'); + }); +}); |
