1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
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', { 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');
});
});
|