summaryrefslogtreecommitdiff
path: root/frontend/src/lib/strategyTab.test.ts
blob: a5bf6d8fff3ef00ec7042942193ca789d37a8826 (plain)
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', 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');
  });
});