// @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', { timeout: 15000 }, 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'); }); });