g01-watch-runonce.spec.ts 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127
  1. import { test, expect } from '../fixtures/auth';
  2. import type { Page } from '@playwright/test';
  3. /**
  4. * G-01 自动监控主链单页验证(手动触发分支)
  5. * 触发模型已澄清:定时(30 min)+ 手动 debug API 并存。本 spec 仅走 debug API。
  6. *
  7. * 主链断言(与 memory/project_s8_g01_mvp.md 对齐):
  8. * - 入口 POST /api/aidop/s8/watch-debug/run-once 返回 200 + 结构化结果
  9. * - 命中产出的新单:SourceType=AUTO_WATCH / SceneCode=S2S6_PRODUCTION / ExceptionType=EQUIP_FAULT / ModuleCode 为空
  10. * - 立即第二次 run-once:相同命中应进入"未闭环去重"分支(skipped 比例上升 / created=0),证明 EvaluateDedupAsync 生效
  11. */
  12. async function getToken(page: Page): Promise<string | null> {
  13. return page.evaluate(() => {
  14. for (let i = 0; i < localStorage.length; i++) {
  15. const k = localStorage.key(i)!;
  16. if (/access-token$/i.test(k) && !/x-access-token$/i.test(k)) {
  17. const raw = localStorage.getItem(k)!;
  18. try {
  19. const v = JSON.parse(raw);
  20. return typeof v === 'string' ? v : v?.value ?? raw;
  21. } catch {
  22. return raw;
  23. }
  24. }
  25. }
  26. return null;
  27. });
  28. }
  29. async function runOnce(page: Page) {
  30. const token = await getToken(page);
  31. expect(token, '未取到 access-token').toBeTruthy();
  32. const baseURL = new URL(page.url()).origin;
  33. const resp = await page.request.post(
  34. `${baseURL}/api/aidop/s8/watch-debug/run-once?tenantId=1&factoryId=1`,
  35. { headers: { Authorization: `Bearer ${token}` } },
  36. );
  37. return { status: resp.status(), body: resp.ok() ? ((await resp.json()) as any) : null };
  38. }
  39. async function fetchException(page: Page, id: number) {
  40. const token = await getToken(page);
  41. const baseURL = new URL(page.url()).origin;
  42. const resp = await page.request.get(
  43. `${baseURL}/api/aidop/s8/exceptions/${id}?tenantId=1&factoryId=1`,
  44. { headers: { Authorization: `Bearer ${token!}` } },
  45. );
  46. expect(resp.ok(), `详情接口非 200`).toBeTruthy();
  47. const j = (await resp.json()) as any;
  48. return j?.data ?? j;
  49. }
  50. test.describe.configure({ mode: 'serial' });
  51. test.describe('S8-G01 自动监控主链(手动触发)', () => {
  52. let firstRun: any | null = null;
  53. test('G01-RUN-1 调用 run-once,主链返回结构化结果', async ({ authedPage }) => {
  54. await authedPage.goto('/#/aidop/s8/exceptions', { waitUntil: 'domcontentloaded' });
  55. const { status, body } = await runOnce(authedPage);
  56. test.info().annotations.push({
  57. type: 'run1',
  58. description: `HTTP ${status}; count=${body?.count}; created=${
  59. body?.results?.filter((r: any) => r.created).length ?? 0
  60. }; skipped=${body?.results?.filter((r: any) => r.skipped).length ?? 0}`,
  61. });
  62. expect(status).toBe(200);
  63. expect(body, 'run-once 应返回 JSON').toBeTruthy();
  64. expect(typeof body.count, 'count 字段缺失').toBe('number');
  65. expect(Array.isArray(body.results), 'results 应为数组').toBeTruthy();
  66. for (const r of body.results) {
  67. expect(r).toHaveProperty('created');
  68. expect(r).toHaveProperty('skipped');
  69. expect(r).toHaveProperty('sourceRuleId');
  70. expect(r).toHaveProperty('relatedObjectCode');
  71. }
  72. firstRun = body;
  73. });
  74. test('G01-RUN-1-VERIFY 新建单据满足 G-01 口径', async ({ authedPage }) => {
  75. test.skip(!firstRun, '依赖 RUN-1 结果');
  76. const created = firstRun.results.filter((r: any) => r.created && r.createdExceptionId);
  77. test.skip(
  78. created.length === 0,
  79. '本轮 run-once 未产生新建(可能数据无命中或全部已去重),口径验证只能在有创建样本时进行',
  80. );
  81. const sample = created[0];
  82. const detail = await fetchException(authedPage, Number(sample.createdExceptionId));
  83. test.info().annotations.push({
  84. type: 'sample',
  85. description: JSON.stringify({
  86. id: detail.id,
  87. code: detail.code ?? detail.exceptionCode,
  88. sourceType: detail.sourceType,
  89. sceneCode: detail.sceneCode,
  90. exceptionType: detail.exceptionType,
  91. moduleCode: detail.moduleCode,
  92. }),
  93. });
  94. expect(String(detail.sourceType ?? '')).toBe('AUTO_WATCH');
  95. expect(String(detail.sceneCode ?? '')).toMatch(/S2S6_PRODUCTION|S2S6Production/);
  96. expect(String(detail.exceptionType ?? '')).toBe('EQUIP_FAULT');
  97. // ModuleCode 首版置空
  98. expect(detail.moduleCode == null || detail.moduleCode === '').toBeTruthy();
  99. });
  100. test('G01-RUN-2 紧随其后再触发,应命中未闭环去重', async ({ authedPage }) => {
  101. test.skip(!firstRun, '依赖 RUN-1 结果');
  102. const { status, body } = await runOnce(authedPage);
  103. test.info().annotations.push({
  104. type: 'run2',
  105. description: `HTTP ${status}; count=${body?.count}; created=${
  106. body?.results?.filter((r: any) => r.created).length ?? 0
  107. }; skipped=${body?.results?.filter((r: any) => r.skipped).length ?? 0}`,
  108. });
  109. expect(status).toBe(200);
  110. const created2 = body.results.filter((r: any) => r.created).length;
  111. const skipped2 = body.results.filter((r: any) => r.skipped).length;
  112. // 两次之间没有人为闭单 → 第二轮 created 应为 0;如有规则首次运行,至少 skipped 应 ≥ 第一次的 created 数
  113. expect(
  114. created2 === 0 || skipped2 >= firstRun.results.filter((r: any) => r.created).length,
  115. `第二次 run-once 仍有大量创建,去重未生效:created=${created2} skipped=${skipped2}`,
  116. ).toBeTruthy();
  117. });
  118. });