Skip to content

Test Specs IV7

dogfootman edited this page Oct 16, 2026 · 9 revisions

Test Specs — IV7 Monitoring (에이전트 설치)

생성: 2026-10-16 · 기준: v07 설치 후 통합 검증(IV) · 원본: 여정별 계획서(Excel) ← Test Scenarios · IV 허브 · 기능 TC

Playwright(TypeScript) spec 소스입니다. 시나리오마다 ## <시나리오번호> 섹션이 있고 앵커는 #<시나리오번호 소문자> 입니다. 공용 헬퍼는 Test-Specs-Common. 비밀번호 등은 마스킹했습니다.

시나리오 파일 줄 해시
IV7_S01 tests/iv7/IV7_S01.spec.ts 47 21f3a7c6
IV7_S02 tests/iv7/IV7_S02.spec.ts 26 fba5559c
IV7_S03 tests/iv7/IV7_S03.spec.ts 40 3d70ed74
IV7_S04 tests/iv7/IV7_S04.spec.ts 53 4c18be18
IV7_S05 tests/iv7/IV7_S05.spec.ts 33 858f38fc
IV7_S06 tests/iv7/IV7_S06.spec.ts 57 ec132281
IV7_S07 tests/iv7/IV7_S07.spec.ts 65 b044fbb8
IV7_S08 tests/iv7/IV7_S08.spec.ts 30 a6b7ce95
IV7_S09 tests/iv7/IV7_S09.spec.ts 28 e4e51c4a
IV7_S10 tests/iv7/IV7_S10.spec.ts 46 98f7c330
IV7_S11 tests/iv7/IV7_S11.spec.ts 56 a4126fd6
IV7_S12 tests/iv7/IV7_S12.spec.ts 67 aa108960
IV7_S13 tests/iv7/IV7_S13.spec.ts 31 f0125ce5
IV7_S14 tests/iv7/IV7_S14.spec.ts 30 c1c4f52b
IV7_S15 tests/iv7/IV7_S15.spec.ts 34 db2d0c54
IV7_S16 tests/iv7/IV7_S16.spec.ts 30 e1851c70
IV7_S17 tests/iv7/IV7_S17.spec.ts 30 2f0355ad
IV7_S18 tests/iv7/IV7_S18.spec.ts 29 10c50ab9
IV7_S19 tests/iv7/IV7_S19.spec.ts 37 8df7af22
IV7_S20 tests/iv7/IV7_S20.spec.ts 33 9e224647
IV7_S21 tests/iv7/IV7_S21.spec.ts 44 a51ea5a9
IV7_S22 tests/iv7/IV7_S22.spec.ts 33 40bcbd88
IV7_S23 tests/iv7/IV7_S23.spec.ts 91 6c91f225
IV7_S24 tests/iv7/IV7_S24.spec.ts 38 eb5eb930
IV7_S25 tests/iv7/IV7_S25.spec.ts 63 16450ba8
IV7_S26 tests/iv7/IV7_S26.spec.ts 28 0a66fffe
IV7_S27 tests/iv7/IV7_S27.spec.ts 50 255e0125
IV7_S28 tests/iv7/IV7_S28.spec.ts 44 ee5cfc00
IV7_S29 tests/iv7/IV7_S29.spec.ts 35 1067e8f8
IV7_S30 tests/iv7/IV7_S30.spec.ts 32 211db6a2
IV7_S31 tests/iv7/IV7_S31.spec.ts 25 146ac1b8
IV7_S32 tests/iv7/IV7_S32.spec.ts 23 6e53aa33
IV7_S33 tests/iv7/IV7_S33.spec.ts 28 d211f549
IV7_S34 tests/iv7/IV7_S34.spec.ts 36 2b6c55dc

IV7_S01

tests/iv7/IV7_S01.spec.ts · 47줄 · 해시 21f3a7c6 · 시나리오 보기

// IV7_S01: Monitoring: Infra Monitoring Agent 설치 [정상, CSP별]
// 대상: Infra node (K8s 아님)
// 화면: Monitorings > Config(Monitor Setting, /webconsole/_view/mc011yconfig, iframe) → 대상 node 의 Monitoring Agent 열 Install 버튼
//   (Monitoring 화면에서는 node 옆 'Go to Config to install agent' 버튼으로 같은 화면으로 이동. 2026-10-07 프로브 확인)
// 시작 상태: admin(mcmp) 로그인 상태, Workspace=ws01 / Project=default 선택된 상태에서 시작. IV3 Infra Running
// 절차 (예: AWS 기준): 1.Monitorings > Config 진입 2.Workload mc1 의 첫 node(g1-1) Monitoring Agent 가 Not installed 인지 확인
//   3.Monitoring Agent 'Install' 클릭 → 확인창 확인(Log Agent 는 누르지 않음) 4.요청 접수 및 Installing → Installed(SUCCESS) 확인
// 기대결과: 설치 요청 접수, Monitoring Agent 가 설치된다
// 2026-10-12: 화면의 '첫 Install 버튼' 이 아니라 대상 node({ng}-1, MON_NODE 로 변경 가능)의 Monitoring Agent 칸을 지정해 누른다 — 다른 Infra/CSP 의 node 를 잘못 누르지 않도록(KT mc9 추가).
//   이미 설치돼 있으면(preexisting) 설치 단계는 건너뛰고 상태만 기록한다.
// 선행 Infra 가 Config 화면에 없으면 BLOCKED 로 skip 한다.

import { test, expect } from '../../lib/fixtures';
import { activeCsps, tid, infraName, infraNodegroups, DEFAULT_PROJECT } from '../../lib/data';
import { openConfig, waitNodeRunning, agentAction, cellText, classify, settleAgent } from './monitoring';

for (const c of activeCsps(["AWS","Azure","Alibaba","GCP","Tencent","NHN Cloud","NCP","IBM","KT"])) { // 엑셀에 있는 CSP: AWS, Azure, Alibaba, GCP, Tencent, NHN Cloud, NCP, IBM, KT
  test(`${tid("IV7_S01", c)} ` + "[Infra] Monitoring: Infra 에이전트 설치", async ({ page, dialogs }) => {
    test.setTimeout(30 * 60 * 1000);
    const infra = infraName(c);
    const node = process.env.MON_NODE || `${infraNodegroups(c)[0]}-1`;
    test.info().annotations.push({ type: 'target', description: `Infra node (K8s 아님): infra=${infra} node=${node}` });
    const first = await openConfig(page, DEFAULT_PROJECT);
    test.skip(!first.text.includes(infra), `BLOCKED: 선행 Infra ${infra} 가 Monitor Setting 화면에 없음 (IV3 Infra Running 필요)`);
    const fr = await waitNodeRunning(page, DEFAULT_PROJECT, node);
    const monBefore = await settleAgent(page, fr, node, 'Monitoring');
    const logBefore = await cellText(fr, node, 'Log');
    test.info().annotations.push({ type: 'before', description: `Monitoring: ${monBefore} / Log: ${logBefore}` });
    if (classify(monBefore) === 'installed' || classify(monBefore) === 'inactive') {
      test.info().annotations.push({ type: 'preexisting', description: `Monitoring Agent 이미 설치됨(${monBefore}) → 설치 단계 건너뜀` });
      return;
    }
    expect(classify(monBefore), `시작: Monitoring Agent Not installed(현재 ${monBefore})`).toMatch(/not-installed|failed/);
    if (classify(monBefore) === 'failed') { // 이전 시도 흔적(FAILED)은 Uninstall 로 정리한 뒤 다시 설치
      const u = await agentAction(page, fr, dialogs, node, 'Monitoring', 'Uninstall');
      test.info().annotations.push({ type: 'precondition', description: `Monitoring Agent(failed) Uninstall → ${u.state}` });
      expect(u.done, `Monitoring Agent Uninstall 후 Not installed — 마지막 상태: ${u.state}`).toBe(true);
    }
    const r = await agentAction(page, fr, dialogs, node, 'Monitoring', 'Install');
    test.info().annotations.push({ type: 'monitoring-result', description: `Monitoring Agent 설치 결과: ${r.cls} (${r.seen.join(' → ')})` });
    const logAfter = await cellText(fr, node, 'Log');
    test.info().annotations.push({ type: 'after', description: `Monitoring: ${r.state} / Log: ${logAfter}` });
    expect.soft(logAfter, 'Log Agent 상태 변화 없음').toBe(logBefore);
    expect(r.done, `Monitoring Agent 설치 완료(Installed) — 마지막 상태: ${r.state}`).toBe(true);
  });
}

IV7_S02

tests/iv7/IV7_S02.spec.ts · 26줄 · 해시 fba5559c · 시나리오 보기

// IV7_S02: Monitoring: K8s 모니터링 설치 [정상, CSP별]
// 대상: K8s cluster / K8s node (Infra 아님)
// 화면: Monitorings > Config(Monitor Setting, iframe, Namespace=Project) → K8s {cluster} 의 node → Monitoring Agent 'Install'
// 시작 상태: admin 로그인 상태, Workspace=ws01 / Project=p1 선택 상태에서 시작. IV3 K8s Ready
// 절차 (예: AWS 기준): 1.Monitoring(K8s 탭)에서 k1d 의 node 가 'agent not installed' 인지 확인 2.Config 화면 K8s k1d 의 node Monitoring Agent 'Install' 클릭
//   3.설치 요청 접수(알림 또는 상태 변화) 확인
// 기대결과: 설치 요청 접수
// 선행 K8s(k{n}) 가 Config 화면에 없으면 BLOCKED 로 skip 한다.
// 대상 K8s: 기본 k{n}(IV3_S13 Workflow 의 {prefix}-{csp} 이름은 k1-aws 라 다를 수 있음) — K8S_CLUSTER 로 변경(v07 AWS: k1d)

import { test } from '../../lib/fixtures';
import { activeCsps, tid, k8sName, k8sProject } from '../../lib/data';
import { openConfig, installFirstAgent } from './monitoring';

for (const c of activeCsps(["AWS","Azure","Alibaba","GCP","Tencent","NHN Cloud","NCP","IBM","KT"])) { // 엑셀에 있는 CSP: AWS, Azure, Alibaba, GCP, Tencent, NHN Cloud, NCP, IBM, KT
  test(`${tid("IV7_S02", c)} ` + "[K8s] Monitoring: K8s 모니터링 설치", async ({ page, dialogs }) => {
    test.setTimeout(10 * 60 * 1000);
    const cluster = process.env.K8S_CLUSTER || k8sName(c);
    test.info().annotations.push({ type: 'target', description: `K8s cluster/node (Infra 아님): cluster=${cluster}, project=${k8sProject(c)}` });
    const { fr, text } = await openConfig(page, k8sProject(c));
    test.skip(!text.includes(cluster), `BLOCKED: 선행 K8s ${cluster} 가 Monitor Setting 화면에 없음 (IV3 K8s Ready, Project ${k8sProject(c)} 필요)`);
    // 클러스터가 여러 개면(k1-aws 는 node 0) 대상 클러스터 구역의 Install 을 눌러야 한다 — 'No nodes' 인 클러스터를 건너뛰고 첫 Install 을 사용
    await installFirstAgent(page, fr, dialogs);
  });
}

IV7_S03

tests/iv7/IV7_S03.spec.ts · 40줄 · 해시 3d70ed74 · 시나리오 보기

// IV7_S03: [Infra] Monitoring: Log Agent 설치 [정상, CSP별]
// 대상: Infra node (K8s 아님)
// 화면: Monitorings > Config(Monitor Setting, /webconsole/_view/mc011yconfig, iframe) → 대상 node 의 Log Agent 열 Install 버튼
// 전제: admin(mcmp) 로그인 상태, Workspace=ws01 / Project=default. IV3 Infra mc{n} Running
// 절차(AWS): 1.Monitorings > Config 진입 2.Workload mc1 의 node g1-1 Log Agent 가 Not installed 인지 확인(FAILED 등 이전 시도 흔적이 있으면 Uninstall 로 정리)
//   3.Log Agent 'Install' 클릭(Monitoring Agent 는 누르지 않음) → 확인창 확인 4.요청 접수 및 Installing → Installed(SUCCESS) 확인 5.Monitoring Agent 상태는 시작 때와 같은지 확인
// 기대결과: Log Agent 설치 요청이 접수되고 Log Agent 만 설치된다(Monitoring Agent 상태 변화 없음)
// 상태 문구: Not installed / INSTALLING Installing… / SUCCESS(또는 Installed) / FAILED / SERVICE_INACTIVE (+ Uninstall 버튼). FAILED 는 설치 실패이며 Uninstall 로 정리 가능
// 선행 Infra 가 Config 화면에 없으면 BLOCKED 로 skip. 대상 node 는 첫 node({ng}-1), MON_NODE 환경변수로 변경 가능(NODE 는 node 실행 경로 환경변수라 쓰지 않는다)
import { test, expect } from '../../lib/fixtures';
import { activeCsps, tid, infraName, infraNodegroups, DEFAULT_PROJECT } from '../../lib/data';
import { openConfig, waitNodeRunning, agentAction, cellText, classify, settleAgent } from './monitoring';

for (const c of activeCsps(["AWS","Azure","Alibaba","GCP","Tencent","NHN Cloud","NCP","IBM","KT"])) {
  test(`${tid("IV7_S03", c)} ` + "[Infra] Monitoring: Log Agent 설치", async ({ page, dialogs }) => {
    test.setTimeout(30 * 60 * 1000);
    const infra = infraName(c);
    const node = process.env.MON_NODE ||`${infraNodegroups(c)[0]}-1`;
    test.info().annotations.push({ type: 'target', description: `Infra node (K8s 아님): infra=${infra} node=${node}` });
    const first = await openConfig(page, DEFAULT_PROJECT);
    test.skip(!first.text.includes(infra), `BLOCKED: 선행 Infra ${infra} 가 Monitor Setting 화면에 없음 (IV3 Infra Running 필요)`);
    const fr = await waitNodeRunning(page, DEFAULT_PROJECT, node);
    const monBefore = await cellText(fr, node, 'Monitoring');
    let logBefore = await settleAgent(page, fr, node, 'Log');
    test.info().annotations.push({ type: 'before', description: `Monitoring: ${monBefore} / Log: ${logBefore}` });
    // 전제 준비: 설치돼 있거나 실패 흔적이 있으면 Uninstall 로 Not installed 로 맞춘다
    if (!/not-installed/.test(classify(logBefore))) {
      const u = await agentAction(page, fr, dialogs, node, 'Log', 'Uninstall');
      test.info().annotations.push({ type: 'precondition', description: `Log Agent(${classify(logBefore)}) Uninstall → ${u.state}` });
      expect(u.done, `Log Agent Uninstall 후 Not installed — 마지막 상태: ${u.state}`).toBe(true);
    }
    const r = await agentAction(page, fr, dialogs, node, 'Log', 'Install');
    test.info().annotations.push({ type: 'log-result', description: `Log Agent 설치 결과: ${r.cls} (${r.seen.join(' → ')})` });
    const monAfter = await cellText(fr, node, 'Monitoring');
    test.info().annotations.push({ type: 'after', description: `Monitoring: ${monAfter} / Log: ${r.state}` });
    expect.soft(monAfter, 'Monitoring Agent 상태 변화 없음').toBe(monBefore);
    expect(r.done, `Log Agent 설치 완료(Installed) — 마지막 상태: ${r.state}`).toBe(true);
  });
}

IV7_S04

tests/iv7/IV7_S04.spec.ts · 53줄 · 해시 4c18be18 · 시나리오 보기

// IV7_S04: [K8s] Monitoring: Log Agent 설치 [정상, CSP별]
// 대상: K8s cluster / K8s node (Infra 아님)
// 화면: Monitorings > Config(Monitor Setting, iframe, Namespace=Project) → K8s {cluster} 의 node → Log Agent 열 Install 버튼
// 전제: admin 로그인 상태, Workspace=ws01 / Project=p{n}. IV3 K8s Active. AWS: Project p1 / k1d / NodeGroup g1s
// 절차(AWS): 1.K8s Workloads 에서 k1d NodeGroup g1s 의 node 확인 2.Monitorings > Config(p1) 의 K8s k1d 에서 그 node 행의 Log Agent 확인
//   3.Log Agent 가 Not installed 가 아니면(이전 시도/설치) Uninstall 로 정리(전제 준비) 4.Log Agent 'Install' 클릭(Monitoring Agent 는 누르지 않음) → 확인
//   5.Log Agent Installing → Installed 확인 6.Monitoring Agent 상태는 시작 때와 같은지 확인
// 기대결과: Log Agent 설치 요청이 접수되고 Log Agent 만 설치된다(Monitoring Agent 상태 변화 없음)
// 대상 클러스터/NodeGroup: K8S_CLUSTER(기본 k{n}), K8S_NG(기본 g{n}s) — v07 AWS: k1d / g1s
import { test, expect } from '../../lib/fixtures';
import { activeCsps, tid, k8sName, k8sProject } from '../../lib/data';
import { loginAsAdmin } from '../../lib/ui';
import { gotoK8s, selectNodeGroup } from '../../lib/iv3-k8s';
import { openConfig, waitNodeRunning, agentAction, cellText, classify, settleAgent } from './monitoring';

for (const c of activeCsps(["AWS","Azure","Alibaba","GCP","Tencent","NHN Cloud","NCP","IBM","KT"])) {
  test(`${tid("IV7_S04", c)} ` + "[K8s] Monitoring: Log Agent 설치", async ({ page, dialogs }) => {
    test.setTimeout(40 * 60 * 1000);
    const prj = k8sProject(c);
    const cluster = process.env.K8S_CLUSTER || k8sName(c);
    const ngName = process.env.K8S_NG || `g${c.n}s`;
    await loginAsAdmin(page);
    await gotoK8s(page, prj);
    const st = await selectNodeGroup(page, cluster, ngName);
    const node = st.nodes[0];
    test.info().annotations.push({ type: 'target', description: `K8s cluster/node (Infra 아님): ${prj} / ${cluster} / NodeGroup ${ngName} / node ${node ?? '(없음)'}` });
    test.skip(!node, `BLOCKED: NodeGroup ${ngName} 에 node 없음`);
    // K8s 목록은 화면 제목보다 늦게 채워진다 → 최대 3회 다시 열어 확인
    let seen = false;
    for (let i = 0; i < 3 && !seen; i++) {
      const o = await openConfig(page, prj);
      seen = o.text.includes(cluster) && o.text.includes(node);
      if (!seen) await page.waitForTimeout(15_000);
    }
    test.skip(!seen, `BLOCKED: ${cluster} / ${node} 가 Monitor Setting 화면에 없음`);
    const fr = await waitNodeRunning(page, prj, node);
    const monBefore = await settleAgent(page, fr, node, 'Monitoring');
    const logBefore = await settleAgent(page, fr, node, 'Log');
    test.info().annotations.push({ type: 'before', description: `Monitoring: ${monBefore} / Log: ${logBefore}` });
    if (classify(logBefore) !== 'not-installed') {
      const u = await agentAction(page, fr, dialogs, node, 'Log', 'Uninstall');
      test.info().annotations.push({ type: 'precondition', description: `Log Agent(${classify(logBefore)}) Uninstall → ${u.state}` });
      expect(u.done, `Log Agent Uninstall 후 Not installed — 마지막 상태: ${u.state}`).toBe(true);
    }
    const r = await agentAction(page, fr, dialogs, node, 'Log', 'Install');
    test.info().annotations.push({ type: 'log-result', description: `Log Agent 설치 결과: ${r.cls} (${r.seen.join(' → ')})` });
    const monAfter = await cellText(fr, node, 'Monitoring');
    test.info().annotations.push({ type: 'after', description: `Monitoring: ${monAfter} / Log: ${r.state}` });
    expect.soft(monAfter, 'Monitoring Agent 상태 변화 없음').toBe(monBefore);
    expect(r.done, `Log Agent 설치 완료(Installed) — 마지막 상태: ${r.state}`).toBe(true);
  });
}

IV7_S05

tests/iv7/IV7_S05.spec.ts · 33줄 · 해시 858f38fc · 시나리오 보기

// IV7_S05: [Infra] Monitoring: Monitoring Agent + Log Agent 둘 다 설치 [정상, CSP별]
// 대상: Infra node (K8s 아님)
// 화면: Monitorings > Config(Monitor Setting, iframe) → 대상 node 의 Monitoring Agent 열 Install, Log Agent 열 Install
// 전제: admin(mcmp) 로그인 상태, Workspace=ws01 / Project=default. IV3 Infra mc{n} Running, 두 에이전트가 모두 Not installed 인 node 필요(AWS: g1-2)
// 절차(AWS): 1.Monitorings > Config 진입 2.Workload mc1 의 두 에이전트가 모두 Not installed 인 node(g1-2) 확인 3.Monitoring Agent 'Install' → 확인 4.Log Agent 'Install' → 확인
//   5.두 에이전트 모두 Installing → Installed(Uninstall 버튼) 확인(약 3분)
// 기대결과: 두 에이전트 설치 요청이 각각 접수되고 모두 Installed 가 된다
// 대상 node 는 MON_NODE 환경변수로 지정(기본 {ng}-2). 두 에이전트가 이미 설치/일부 설치된 node 면 BLOCKED 로 skip
import { test, expect } from '../../lib/fixtures';
import { activeCsps, tid, infraName, infraNodegroups, DEFAULT_PROJECT } from '../../lib/data';
import { openConfig, waitNodeRunning, agentAction, cellText, classify, settleAgent } from './monitoring';

for (const c of activeCsps(["AWS","Azure","Alibaba","GCP","Tencent","NHN Cloud","NCP","IBM","KT"])) {
  test(`${tid("IV7_S05", c)} ` + "[Infra] Monitoring: Monitoring Agent + Log Agent 둘 다 설치", async ({ page, dialogs }) => {
    test.setTimeout(30 * 60 * 1000);
    const infra = infraName(c);
    const node = process.env.MON_NODE ||`${infraNodegroups(c)[0]}-2`;
    test.info().annotations.push({ type: 'target', description: `Infra node (K8s 아님): infra=${infra} node=${node}` });
    const first = await openConfig(page, DEFAULT_PROJECT);
    test.skip(!first.text.includes(infra), `BLOCKED: 선행 Infra ${infra} 가 Monitor Setting 화면에 없음 (IV3 Infra Running 필요)`);
    const fr = await waitNodeRunning(page, DEFAULT_PROJECT, node);
    const mon0 = await settleAgent(page, fr, node, 'Monitoring');
    const log0 = await settleAgent(page, fr, node, 'Log');
    test.info().annotations.push({ type: 'before', description: `Monitoring: ${mon0} / Log: ${log0}` });
    test.skip(!(classify(mon0) === 'not-installed' && classify(log0) === 'not-installed'), `BLOCKED: ${node} 의 두 에이전트가 모두 Not installed 가 아님`);
    const m = await agentAction(page, fr, dialogs, node, 'Monitoring', 'Install');
    const l = await agentAction(page, fr, dialogs, node, 'Log', 'Install');
    test.info().annotations.push({ type: 'after', description: `Monitoring: ${await cellText(fr, node, 'Monitoring')} / Log: ${await cellText(fr, node, 'Log')}` });
    expect.soft(m.done, `Monitoring Agent 설치 완료 — 마지막 상태: ${m.state}`).toBe(true);
    expect.soft(l.done, `Log Agent 설치 완료 — 마지막 상태: ${l.state}`).toBe(true);
  });
}

IV7_S06

tests/iv7/IV7_S06.spec.ts · 57줄 · 해시 ec132281 · 시나리오 보기

// IV7_S06: [K8s] Monitoring: Monitoring Agent + Log Agent 둘 다 설치 [정상, CSP별]
// 대상: K8s cluster / K8s node (Infra 아님)
// 화면: Monitorings > Config(Monitor Setting, iframe, Namespace=Project) → K8s {cluster} 의 node → Monitoring Agent 열 Install, Log Agent 열 Install
// 전제: admin 로그인 상태, Workspace=ws01 / Project=p{n}. IV3 K8s Active. AWS: Project p1 / k1d / NodeGroup g1s(사용자 지시 2026-10-08)
// 절차(AWS): 1.K8s Workloads 에서 k1d NodeGroup g1s 의 node 확인 2.Monitorings > Config(p1) 의 K8s k1d 에서 그 node 행 확인
//   3.Monitoring Agent 가 이미 설치돼 있으면(IV7_S02) 먼저 Uninstall 로 Not installed 상태를 만든다(전제 준비) 4.Monitoring Agent 'Install' → 확인 5.Log Agent 'Install' → 확인
//   6.두 에이전트 모두 Installing → Installed 확인
// 기대결과: 두 에이전트 설치 요청이 각각 접수되고 모두 Installed 가 된다
// 대상 클러스터/NodeGroup: K8S_CLUSTER(기본 k{n}), K8S_NG(기본 g{n}s) — v07 AWS: k1d / g1s
import { test, expect } from '../../lib/fixtures';
import { activeCsps, tid, k8sName, k8sProject } from '../../lib/data';
import { loginAsAdmin } from '../../lib/ui';
import { gotoK8s, selectNodeGroup } from '../../lib/iv3-k8s';
import { openConfig, waitNodeRunning, agentAction, cellText, classify, settleAgent } from './monitoring';

for (const c of activeCsps(["AWS","Azure","Alibaba","GCP","Tencent","NHN Cloud","NCP","IBM","KT"])) {
  test(`${tid("IV7_S06", c)} ` + "[K8s] Monitoring: Monitoring Agent + Log Agent 둘 다 설치", async ({ page, dialogs }) => {
    test.setTimeout(40 * 60 * 1000);
    const prj = k8sProject(c);
    const cluster = process.env.K8S_CLUSTER || k8sName(c);
    const ngName = process.env.K8S_NG || `g${c.n}s`;
    await loginAsAdmin(page);
    await gotoK8s(page, prj);
    const st = await selectNodeGroup(page, cluster, ngName);
    const node = st.nodes[0];
    test.info().annotations.push({ type: 'target', description: `K8s cluster/node (Infra 아님): ${prj} / ${cluster} / NodeGroup ${ngName} / node ${node ?? '(없음)'}` });
    test.skip(!node, `BLOCKED: NodeGroup ${ngName} 에 node 없음`);
    // K8s 목록은 화면 제목보다 늦게 채워진다(2026-10-08: 첫 로드에서 'Monitor Setting Namespace: p1 Refresh' 만 보임) → 최대 3회 다시 열어 확인
    let seen = false;
    for (let i = 0; i < 3 && !seen; i++) {
      const o = await openConfig(page, prj);
      seen = o.text.includes(cluster) && o.text.includes(node);
      if (!seen) await page.waitForTimeout(15_000);
    }
    test.skip(!seen, `BLOCKED: ${cluster} / ${node} 가 Monitor Setting 화면에 없음`);
    const fr = await waitNodeRunning(page, prj, node);
    const mon = await settleAgent(page, fr, node, 'Monitoring');
    const log = await settleAgent(page, fr, node, 'Log');
    test.info().annotations.push({ type: 'before', description: `Monitoring: ${mon} / Log: ${log}` });
    // 전제 준비: 이미 설치된 에이전트는 Uninstall 해서 둘 다 Not installed 로 맞춘다
    for (const kind of ['Monitoring', 'Log'] as const) {
      const t = kind === 'Monitoring' ? mon : log;
      // 'Failed Install'(K8s 설치 실패, 2026-10-15 GKE)처럼 Install 버튼만 있으면 Uninstall 없이 바로 Install 단계로
      if (classify(t) !== 'not-installed' && /Uninstall/.test(t)) {
        const u = await agentAction(page, fr, dialogs, node, kind, 'Uninstall');
        test.info().annotations.push({ type: 'precondition', description: `${kind} Agent Uninstall → ${u.state}` });
        expect(u.done, `${kind} Agent Uninstall 후 Not installed — 마지막 상태: ${u.state}`).toBe(true);
      }
    }
    const m = await agentAction(page, fr, dialogs, node, 'Monitoring', 'Install');
    const l = await agentAction(page, fr, dialogs, node, 'Log', 'Install');
    test.info().annotations.push({ type: 'after', description: `Monitoring: ${await cellText(fr, node, 'Monitoring')} / Log: ${await cellText(fr, node, 'Log')}` });
    expect.soft(m.done, `Monitoring Agent 설치 완료 — 마지막 상태: ${m.state}`).toBe(true);
    expect.soft(l.done, `Log Agent 설치 완료 — 마지막 상태: ${l.state}`).toBe(true);
  });
}

IV7_S07

tests/iv7/IV7_S07.spec.ts · 65줄 · 해시 b044fbb8 · 시나리오 보기

// IV7_S07: TC-OBS-007 모니터링 플러그인(item) 등록 (Monitoring Agent) [정상, 공통]
// v0.6 TC-ID: TC-OBS-AGENT-04
// 화면: Monitorings > Config → Agent 설치 완료 node → 메트릭(플러그인) 토글 표
// 시작 상태: admin(mcmp) 로그인, ws01/default. 대상 node Monitoring Agent SUCCESS(TC-OBS-AGENT-03), node Running
// 절차: 1.설치 완료 node 선택 2.등록 item(GET …/node/{nodeId}/item)·사용 가능 플러그인(GET /api/o11y/monitoring/plugins) 3.비활성 플러그인 ON 4.등록(POST …/node/{nodeId}/item) 5.item 목록 갱신 6.(원복) 켠 플러그인 OFF(DELETE …/item/{itemSeq})
// 기대결과: 플러그인 등록 2xx, item 목록(seq, pluginName, state)
// 실행 보류(사용자 결정 2026-10-14: 상태를 바꾸는 OBS TC 는 spec 만 작성). OBS_WRITE=1 일 때만 실행.
// 2026-10-16 수정(rf-obs2 탐색, 실행 전): 화면 확인 결과 node 행(<td>) 클릭 → 표 아래 'Monitoring Metrics — {node}' 패널(플러그인 11개 체크박스 cpu…system + 'Apply',
//   안내 'Enables the selected metrics and disables the rest.'). 행별 토글이 아니라 Apply 한 번에 선택 집합을 반영 → 이전 선택 상태를 기록하고 하나만 추가해 Apply, 원복은 이전 집합으로 다시 Apply.
// 2026-10-16 보완(사용자 지시): 비활성 플러그인이 없으면 하나를 끄고 Apply → 다시 켜서 Apply → item 목록에 등록 확인(끝 상태 = 원래대로 모두 켜짐)
//   node 가 Suspended 이면 item 조회가 없고 체크가 모두 비어 보여(Apply 시 전부 끄게 됨) 실행하지 않는다 — Running node(OBS_WRITE_NODE) 필요. Apply 이후 API(POST/DELETE item) 는 미검증
import { test, expect } from '../../lib/fixtures';
import { loginAsAdmin } from '../../lib/ui';
import { openObs, collectResponses, clean, OBS_NS, OBS_INFRA } from '../../lib/iv7-obs';

test('IV7_S07 TC-OBS-007 모니터링 플러그인(item) 등록', async ({ page }) => {
  test.skip(process.env.OBS_WRITE !== '1', '상태 변경 TC — OBS_WRITE=1 일 때만 실행(사용자 결정 2026-10-14)');
  const node = process.env.OBS_WRITE_NODE ?? '';
  test.skip(!node, 'OBS_WRITE_NODE 미지정(공용 node 보호)');
  const items = collectResponses(page, new RegExp(`/api/o11y/monitoring/${OBS_NS}/${OBS_INFRA}/node/${node}/item`));
  await loginAsAdmin(page);
  const f = await openObs(page, 'config');
  const row = f.locator('tr').filter({ has: f.locator('td', { hasText: new RegExp(`^${node}$`) }) }).first();
  const rowText = clean(await row.innerText());
  test.info().annotations.push({ type: 'row', description: rowText });
  test.skip(!/Running/.test(rowText), `BLOCKED: ${node} 가 Running 아님(${rowText}) — 플러그인 반영에 VM 접속 필요`);
  await row.locator('td').first().click();
  await expect(f.getByRole('button', { name: 'Apply', exact: true })).toBeVisible({ timeout: 30_000 });
  await expect.poll(() => items.filter((i) => i.method === 'GET').length, { message: 'item 목록 GET', timeout: 30_000 }).toBeGreaterThan(0);
  const before: any[] = items.filter((i) => i.method === 'GET').at(-1)!.json?.data ?? [];
  test.info().annotations.push({ type: 'items-before', description: JSON.stringify(before).slice(0, 600) });
  const boxes = f.locator('label').filter({ has: f.locator('input[type="checkbox"]') });
  const names = (await boxes.allInnerTexts()).map((s) => s.trim());
  const checked = await boxes.locator('input[type="checkbox"]').evaluateAll((els) => els.map((e) => (e as HTMLInputElement).checked));
  const apply = async (label: string) => {
    const n0 = items.length;
    await f.getByRole('button', { name: 'Apply', exact: true }).click();
    await expect.poll(() => items.slice(n0).some((i) => i.method !== 'GET' && i.status < 300), { message: `${label} 2xx`, timeout: 60_000 }).toBe(true);
    test.info().annotations.push({ type: label, description: items.slice(n0).filter((i) => i.method !== 'GET').map((i) => `${i.status} ${i.method} ${i.url.split('/api/')[1]}`).join(' | ').slice(0, 600) });
  };
  let offIdx = checked.findIndex((c, i) => !c && !/nvidia/.test(names[i]));
  // 2026-10-16 보완(사용자 지시): 모든 플러그인이 켜져 있으면(비활성 없음) 하나를 먼저 끄고 Apply → 그 플러그인을 다시 켜서 등록되는지 확인한다.
  //   끈 플러그인은 마지막에 켠 상태로 끝나므로 원래 상태(모두 켜짐)로 돌아간다.
  let restoreOff = true; // 원래 꺼져 있던 것을 켰으면 마지막에 다시 끈다
  if (offIdx < 0) {
    offIdx = names.findIndex((n) => !/nvidia/.test(n) && !/^cpu$/i.test(n)); // cpu 는 기본 수집이라 피함
    expect(offIdx, '끄고 다시 켤 플러그인').toBeGreaterThanOrEqual(0);
    await boxes.nth(offIdx).locator('input').uncheck();
    await apply('pre-off');
    restoreOff = false;
  }
  const name = names[offIdx];
  await boxes.nth(offIdx).locator('input').check();
  await apply('register');
  // 등록 확인: item 목록을 다시 읽어 그 플러그인이 있는지
  await f.locator('tr').filter({ has: f.locator('td', { hasText: new RegExp(`^${node}$`) }) }).first().locator('td').first().click();
  await expect.poll(() => (items.filter((i) => i.method === 'GET').at(-1)?.json?.data ?? []).some((x: any) => new RegExp(name, 'i').test(x.pluginName ?? x.name ?? JSON.stringify(x))),
    { message: `item 목록에 ${name}`, timeout: 60_000 }).toBe(true);
  test.info().annotations.push({ type: 'items-after', description: JSON.stringify(items.filter((i) => i.method === 'GET').at(-1)?.json?.data ?? []).slice(0, 600) });
  if (restoreOff) { // (원복) 원래 꺼져 있던 플러그인은 다시 끔
    await boxes.nth(offIdx).locator('input').uncheck();
    await apply('restore-off');
  }
});

IV7_S08

tests/iv7/IV7_S08.spec.ts · 30줄 · 해시 a6b7ce95 · 시나리오 보기

// IV7_S08: TC-OBS-008 Monitoring Agent 제거 (Uninstall) [정상, 공통]
// v0.6 TC-ID: TC-OBS-AGENT-05
// 화면: Monitorings > Config → node 행 Monitoring Agent 의 Uninstall → 확인
// 시작 상태: admin(mcmp) 로그인, ws01/default. 설치된 node(OBS_WRITE_NODE — 다른 세션이 쓰는 node 는 지정 금지)
// 절차: 1.node 행 Monitoring Agent Uninstall → 확인 2.제거 호출(DELETE /api/o11y/monitoring/{ns}/{infra}/node/{node}) 3.미등록 상태 전환 4.(원복) 재설치는 IV7_S01 절차
// 기대결과: 제거 응답 2xx, node 미등록 상태
// 실행 보류(사용자 결정 2026-10-14: 상태를 바꾸는 OBS TC 는 spec 만 작성). OBS_WRITE=1 일 때만 실행. 셀렉터는 IV7_S03 과 같은 monitoring.ts agentAction(검증됨)
// 2026-10-16 수정(rf-obs2): node 가 Running 이 아니면 BLOCKED skip 추가 — Suspended node 에서 Uninstall 하면 VM 접속 실패로 FAILED 가 남는다(mc9 g9-1 사례).
//   2026-10-16 시점 대상 후보 mc1/g1-2(두 에이전트 SERVICE_INACTIVE=설치됨, Suspended)는 Resume 이 필요해 미실행
import { test, expect } from '../../lib/fixtures';
import { loginAsAdmin } from '../../lib/ui';
import { openObs, collectResponses, OBS_NS, OBS_INFRA } from '../../lib/iv7-obs';
import { agentAction, cellText, rowStatus } from './monitoring';

test('IV7_S08 TC-OBS-008 Monitoring Agent 제거 (Uninstall)', async ({ page, dialogs }) => {
  test.skip(process.env.OBS_WRITE !== '1', '상태 변경 TC — OBS_WRITE=1 일 때만 실행(사용자 결정 2026-10-14)');
  const node = process.env.OBS_WRITE_NODE ?? '';
  test.skip(!node, 'OBS_WRITE_NODE 미지정(공용 node 보호)');
  const del = collectResponses(page, new RegExp(`/api/o11y/monitoring/${OBS_NS}/${OBS_INFRA}/node/${node}`));
  await loginAsAdmin(page);
  const f = page.frameLocator('iframe').first();
  await openObs(page, 'config');
  const st = await rowStatus(f, node);
  test.info().annotations.push({ type: 'node-status', description: `${node}:${st || '행 없음'} / Monitoring: ${await cellText(f, node, 'Monitoring')} / Log: ${await cellText(f, node, 'Log')}` });
  test.skip(st !== 'Running', `BLOCKED: ${node} 가 Running 아님(${st || '행 없음'}) — 에이전트 제거는 VM 접속 필요('Cannot connect to the VM — it must be running')`);
  await agentAction(page, f, dialogs, node, 'Monitoring', 'Uninstall');
  test.info().annotations.push({ type: 'api', description: del.map((d) => `${d.status} ${d.method} ${d.url}`).join(' | ') });
  expect(await cellText(f, node, 'Monitoring')).toMatch(/NOT[_ ]INSTALLED|Install/i);
});

IV7_S09

tests/iv7/IV7_S09.spec.ts · 28줄 · 해시 e4e51c4a · 시나리오 보기

// IV7_S09: TC-OBS-009 Log Agent 제거 (Uninstall) [정상, 공통]
// v0.6 TC-ID: TC-OBS-AGENT-05(Log)
// 화면: Monitorings > Config → node 행 Log Agent 의 Uninstall → 확인
// 시작 상태: admin(mcmp) 로그인, ws01/default. Log Agent 설치 node(OBS_WRITE_NODE)
// 절차: 1.node 행 Log Agent Uninstall → 확인 2.제거 호출 3.미등록 상태 4.(원복) 재설치는 IV7_S03 절차
// 기대결과: 제거 응답 2xx, node 미등록 상태
// 실행 보류(사용자 결정 2026-10-14: 상태를 바꾸는 OBS TC 는 spec 만 작성). OBS_WRITE=1 일 때만 실행. 셀렉터는 IV7_S03 과 같은 monitoring.ts agentAction(검증됨)
// 2026-10-16 수정(rf-obs2): node 가 Running 이 아니면 BLOCKED skip 추가 — Suspended node 에서 Uninstall 하면 VM 접속 실패로 FAILED 가 남는다(mc9 g9-1 사례).
//   2026-10-16 시점 대상 후보 mc1/g1-2(두 에이전트 SERVICE_INACTIVE=설치됨, Suspended)는 Resume 이 필요해 미실행
import { test, expect } from '../../lib/fixtures';
import { loginAsAdmin } from '../../lib/ui';
import { openObs } from '../../lib/iv7-obs';
import { agentAction, cellText, rowStatus } from './monitoring';

test('IV7_S09 TC-OBS-009 Log Agent 제거 (Uninstall)', async ({ page, dialogs }) => {
  test.skip(process.env.OBS_WRITE !== '1', '상태 변경 TC — OBS_WRITE=1 일 때만 실행(사용자 결정 2026-10-14)');
  const node = process.env.OBS_WRITE_NODE ?? '';
  test.skip(!node, 'OBS_WRITE_NODE 미지정(공용 node 보호)');
  await loginAsAdmin(page);
  const f = page.frameLocator('iframe').first();
  await openObs(page, 'config');
  const st = await rowStatus(f, node);
  test.info().annotations.push({ type: 'node-status', description: `${node}:${st || '행 없음'} / Monitoring: ${await cellText(f, node, 'Monitoring')} / Log: ${await cellText(f, node, 'Log')}` });
  test.skip(st !== 'Running', `BLOCKED: ${node} 가 Running 아님(${st || '행 없음'}) — 에이전트 제거는 VM 접속 필요('Cannot connect to the VM — it must be running')`);
  await agentAction(page, f, dialogs, node, 'Log', 'Uninstall');
  expect(await cellText(f, node, 'Log')).toMatch(/NOT[_ ]INSTALLED|Install/i);
});

IV7_S10

tests/iv7/IV7_S10.spec.ts · 46줄 · 해시 98f7c330 · 시나리오 보기

// IV7_S10: TC-OBS-010 Trigger Policy 생성/목록/삭제 [정상, 공통]
// v0.6 TC-ID: TC-OBS-TRIG-01
// 화면: Monitorings > Alert → Policies → + New Policy
// 시작 상태: admin(mcmp) 로그인, ws01/default. mc-o11y-manager / Grafana(알림) 기동
// 절차: 1.Policies 탭 2.목록(GET /api/o11y/trigger/policy) 3.+ New Policy: title/description/resourceType CPU/aggregation AVG/holdDuration 5m/repeatInterval 1h/threshold info·warning·critical → 생성(POST) 4.목록 반영 5.삭제(202) → 목록에서 제거
// 기대결과: 생성 201(id), 목록(content[], totalElements), 삭제 202
// 실행 보류(사용자 결정 2026-10-14: 상태를 바꾸는 OBS TC 는 spec 만 작성). OBS_WRITE=1 일 때만 실행.
// 2026-10-16 수정(첫 실행, rf-obs2): 2026-10-14 추정 셀렉터(getByLabel)가 화면과 달라 교체 — 폼 라벨이 <label> 이 아닌 <div> 라서
//   lib/iv7-obs.ts policyField/createPolicy/deletePolicy 사용. 삭제 확인은 JS confirm('Delete this policy?')이라 fixtures dialogs 가 수락. 실패해도 finally 에서 정책 삭제(정리)
import { test, expect } from '../../lib/fixtures';
import { loginAsAdmin } from '../../lib/ui';
import { openObs, collectResponses, createPolicy, deletePolicy, policyRow, clean } from '../../lib/iv7-obs';

export const POLICY = 'iv7-s10-cpu-policy';
test('IV7_S10 TC-OBS-010 Trigger Policy 생성/목록/삭제', async ({ page }) => {
  test.skip(process.env.OBS_WRITE !== '1', '상태 변경 TC — OBS_WRITE=1 일 때만 실행(사용자 결정 2026-10-14)');
  const list = collectResponses(page, /\/api\/o11y\/trigger\/policy\?/);
  await loginAsAdmin(page);
  const f = await openObs(page, 'alerts');
  try {
    await test.step('1~2. Policies 목록', async () => {
      await expect.poll(() => list.length, { timeout: 30_000 }).toBeGreaterThan(0);
      const d = list.at(-1)!.json?.data;
      test.info().annotations.push({ type: 'list-before', description: `${list.at(-1)!.status} totalElements=${d?.totalElements} content=${(d?.content ?? []).map((p: any) => p.title).join(',')}` });
      expect(Array.isArray(d?.content)).toBe(true);
    });
    const c = await test.step('3. + New Policy → Create', async () =>
      createPolicy(page, f, { title: POLICY, description: 'IV7_S10 test policy', resource: 'CPU', aggregation: 'AVG', hold: '5m', repeat: '1h', info: '50', warning: '70', critical: '90' }));
    expect(c.status, '생성 201').toBe(201);
    expect(c.id, '생성 id').toBeTruthy();
    await test.step('4. 목록 반영', async () => {
      const row = clean(await policyRow(f, POLICY).innerText());
      test.info().annotations.push({ type: 'row', description: row });
      expect(row).toMatch(/cpu/i);
      expect(row).toContain('I:50 W:70 C:90');
      const d = list.at(-1)!.json?.data;
      expect((d?.content ?? []).some((p: any) => p.title === POLICY && p.holdDuration === '5m' && p.repeatInterval === '1h' && /avg/i.test(p.aggregationType))).toBe(true);
    });
    await test.step('5. 삭제 202 → 목록에서 제거', async () => {
      expect(await deletePolicy(page, f, POLICY)).toBe(202);
    });
  } finally {
    await deletePolicy(page, f, POLICY).catch(() => 0); // 정리(이미 지웠으면 아무것도 안 함)
  }
});

IV7_S11

tests/iv7/IV7_S11.spec.ts · 56줄 · 해시 a4126fd6 · 시나리오 보기

// IV7_S11: TC-OBS-011 정책 ↔ Node/Infra 연결 [정상, 공통]
// v0.6 TC-ID: TC-OBS-TRIG-02
// 화면: Monitorings > Alert → 정책 행 Manage → Alarm targets
// 시작 상태: admin(mcmp) 로그인, ws01/default. 정책 존재(IV7_S10 절차로 생성), 대상 node 존재
// 절차: 1.Manage → Alarm targets 2.Infra 선택 → 레벨 Node(단일 VM) 3.Add targets(POST /api/o11y/trigger/policy/{id}/node) 4.정책 vms 목록 갱신 5.(정리) 대상 제거 · 정책 삭제
// 기대결과: 202 ACCEPTED, 정책 vms 목록 갱신
// 실행 보류(사용자 결정 2026-10-14: 상태를 바꾸는 OBS TC 는 spec 만 작성). OBS_WRITE=1 일 때만 실행.
// 2026-10-16 수정(첫 실행, rf-obs2): 추정 셀렉터 교체 — 정책이 없으면 Manage 가 없으므로 이 spec 이 정책을 만들고(IV7_S10 절차) 끝에 지운다.
//   Alarm targets 패널은 'Infra / Cluster' select + node 체크박스(label 'g1-2 (g1-2)') + 'Add targets'. 레벨(Infra/Node) 선택 UI 는 없고 node 체크가 단일 VM 연결.
//   대상 node 는 OBS_TARGET_NODE(기본 OBS_NODE) — 연결만 하고 node 상태는 바꾸지 않음. 알림 채널은 비워 두어 발송 없음
import { test, expect } from '../../lib/fixtures';
import { loginAsAdmin } from '../../lib/ui';
import { openObs, collectResponses, createPolicy, deletePolicy, policyRow, clean, OBS_INFRA, OBS_NODE } from '../../lib/iv7-obs';

const POLICY = 'iv7-s11-target-policy';
const NODE = process.env.OBS_TARGET_NODE ?? OBS_NODE;
test('IV7_S11 TC-OBS-011 정책 ↔ Node/Infra 연결', async ({ page }) => {
  test.skip(process.env.OBS_WRITE !== '1', '상태 변경 TC — OBS_WRITE=1 일 때만 실행(사용자 결정 2026-10-14)');
  const api = collectResponses(page, /\/api\/o11y\/trigger\/policy\/\d+\/node/);
  const list = collectResponses(page, /\/api\/o11y\/trigger\/policy\?/);
  await loginAsAdmin(page);
  const f = await openObs(page, 'alerts');
  try {
    await test.step('0. 사전: 정책 생성(IV7_S10 절차)', async () => { await createPolicy(page, f, { title: POLICY, description: 'IV7_S11 target test' }); });
    await test.step('1~3. Manage → Alarm targets → Infra·Node → Add targets', async () => {
      await policyRow(f, POLICY).getByRole('button', { name: 'Manage', exact: true }).click();
      await expect(f.getByRole('button', { name: 'Add targets' })).toBeVisible();
      const sel = f.locator('select').filter({ has: f.locator('option', { hasText: new RegExp(`^${OBS_INFRA}$`) }) }).first();
      await sel.selectOption({ label: OBS_INFRA });
      await page.waitForTimeout(2000);
      await f.locator('label').filter({ hasText: new RegExp(`^\\s*${NODE} \\(`) }).first().locator('input[type="checkbox"]').check()
        .catch(async () => f.getByLabel(new RegExp(`^${NODE} \\(`)).first().check());
      await f.getByRole('button', { name: 'Add targets' }).click();
      await expect.poll(() => api.find((a) => a.method === 'POST')?.status ?? 0, { message: 'Add targets POST', timeout: 30_000 }).toBeGreaterThan(0);
      const p = api.find((a) => a.method === 'POST')!;
      test.info().annotations.push({ type: 'add-targets', description: `${p.status} POST ${p.url} ${JSON.stringify(p.json ?? '').slice(0, 300)}` });
      expect(p.status, '202 ACCEPTED').toBe(202);
    });
    await test.step('4. 정책 vms 목록 갱신', async () => {
      await expect.poll(() => (list.at(-1)?.json?.data?.content ?? []).find((x: any) => x.title === POLICY)?.vms?.length ?? 0, { timeout: 30_000 }).toBeGreaterThan(0);
      const vms = (list.at(-1)!.json.data.content as any[]).find((x) => x.title === POLICY).vms;
      test.info().annotations.push({ type: 'vms', description: JSON.stringify(vms).slice(0, 500) });
      expect(JSON.stringify(vms)).toContain(NODE);
      const row = clean(await policyRow(f, POLICY).innerText());
      test.info().annotations.push({ type: 'row', description: row });
      test.info().annotations.push({ type: 'panel', description: clean(await f.locator('body').innerText()).slice(0, 1200) });
    });
  } finally {
    await test.step('5. (정리) 정책 삭제 — 연결된 대상도 함께 제거', async () => {
      await page.keyboard.press('Escape').catch(() => {});
      const st = await deletePolicy(page, f, POLICY).catch((e) => { test.info().annotations.push({ type: 'cleanup-error', description: String(e).slice(0, 200) }); return -1; });
      test.info().annotations.push({ type: 'cleanup', description: `정책 삭제 ${st}` });
    });
  }
});

IV7_S12

tests/iv7/IV7_S12.spec.ts · 67줄 · 해시 aa108960 · 시나리오 보기

// IV7_S12: TC-OBS-012 알림 채널 설정 [정상, 공통]
// v0.6 TC-ID: TC-OBS-TRIG-03
// 화면: Monitorings > Alert → 정책 Manage → Notification channels
// 시작 상태: admin(mcmp) 로그인, ws01/default. 정책 존재, Grafana contact point(SMTP 등)·채널별 자격 구성
// 절차: 1.Notification channels 2.지원 채널(GET /api/o11y/trigger/noti/channel) 3.Email 체크 · 수신자 입력 4.Test(POST /api/o11y/trigger/noti/test) 5.Save channels(PUT /api/o11y/trigger/policy/{id}/channel)
// 기대결과: 202, notiChannels 갱신, (테스트 시) 발송 결과
// 비고: 채널 자격(SMTP·Slack 토큰 등)이 없으면 SKIP(지침 4-0). 수신자는 OBS_NOTI_EMAIL
// 실행 보류(사용자 결정 2026-10-14: 상태를 바꾸는 OBS TC 는 spec 만 작성). OBS_WRITE=1 일 때만 실행.
// 2026-10-16 수정(첫 실행, rf-obs2): 추정 셀렉터 교체 — 이 spec 이 정책을 만들고(IV7_S10 절차, 대상 node 없음) Manage → Notification channels → Email 체크·수신자 → Save channels 후 정책 삭제.
//   수신자 기본값은 발송되지 않는 예약 도메인 mcmp-e2e@example.com(OBS_NOTI_EMAIL 로 변경). 'Test' 는 실제 메시지를 보내므로('Test delivers a real message through RabbitMQ')
//   수신자·발송 승인이 있을 때만(OBS_NOTI_TEST=1) 누른다. 정책에 대상 node 가 없어 저장만으로는 알림이 발송되지 않는다
import { test, expect } from '../../lib/fixtures';
import { loginAsAdmin } from '../../lib/ui';
import { openObs, collectResponses, createPolicy, deletePolicy, policyRow, clean } from '../../lib/iv7-obs';

const POLICY = 'iv7-s12-channel-policy';
test('IV7_S12 TC-OBS-012 알림 채널 설정', async ({ page }) => {
  test.skip(process.env.OBS_WRITE !== '1', '상태 변경 TC — OBS_WRITE=1 일 때만 실행(사용자 결정 2026-10-14)');
  const to = process.env.OBS_NOTI_EMAIL || 'mcmp-e2e@example.com';
  const api = collectResponses(page, /\/api\/o11y\/trigger\/(noti|policy\/\d+\/channel)/);
  const list = collectResponses(page, /\/api\/o11y\/trigger\/policy\?/);
  await loginAsAdmin(page);
  const f = await openObs(page, 'alerts');
  try {
    await test.step('2. 지원 채널 목록', async () => {
      await expect.poll(() => api.filter((a) => /noti\/channel/.test(a.url)).length, { timeout: 30_000 }).toBeGreaterThan(0);
      const ch = api.filter((a) => /noti\/channel/.test(a.url)).at(-1)!.json?.data?.notiChannels ?? [];
      test.info().annotations.push({ type: 'channels', description: ch.map((c: any) => `${c.id}:${c.name}(${c.type}/${c.provider}, active=${c.isActive})`).join(', ') });
      expect(ch.some((c: any) => c.type === 'email' && c.isActive), 'email 채널 활성').toBe(true);
    });
    await test.step('0. 사전: 정책 생성(IV7_S10 절차, 대상 없음)', async () => { await createPolicy(page, f, { title: POLICY, description: 'IV7_S12 channel test' }); });
    await test.step('1·3. Manage → Notification channels → Email 체크 · 수신자', async () => {
      await policyRow(f, POLICY).getByRole('button', { name: 'Manage', exact: true }).click();
      await expect(f.getByRole('button', { name: 'Save channels' })).toBeVisible();
      const email = f.locator('label').filter({ hasText: /^\s*Email\s*$/ }).first();
      const cb = email.locator('input[type="checkbox"]');
      if (await cb.count()) await cb.check(); else await email.click();
      await f.getByPlaceholder(/Email addresses/).first().fill(to);
      test.info().annotations.push({ type: 'recipient', description: to });
    });
    await test.step('4. Test 발송(OBS_NOTI_TEST=1 일 때만)', async () => {
      if (process.env.OBS_NOTI_TEST !== '1') { test.info().annotations.push({ type: 'test-send', description: '미실행 — 실제 메시지 발송(수신자·발송 승인 필요)' }); return; }
      const row = f.locator('div').filter({ has: f.getByPlaceholder(/Email addresses/) }).filter({ has: f.getByRole('button', { name: 'Test', exact: true }) }).last();
      await row.getByRole('button', { name: 'Test', exact: true }).click();
      await expect.poll(() => api.some((a) => /noti\/test/.test(a.url)), { timeout: 60_000 }).toBe(true);
      const t = api.find((a) => /noti\/test/.test(a.url))!;
      test.info().annotations.push({ type: 'test-send', description: `${t.status} ${t.method} ${t.url} ${JSON.stringify(t.json ?? '').slice(0, 300)}` });
    });
    await test.step('5. Save channels → 202, notiChannels 갱신', async () => {
      await f.getByRole('button', { name: 'Save channels' }).click();
      await expect.poll(() => api.find((a) => a.method === 'PUT')?.status ?? 0, { message: 'Save channels PUT', timeout: 30_000 }).toBeGreaterThan(0);
      const p = api.find((a) => a.method === 'PUT')!;
      test.info().annotations.push({ type: 'save-channels', description: `${p.status} PUT ${p.url} ${JSON.stringify(p.json ?? '').slice(0, 300)}` });
      expect(p.status, 'Save channels 202').toBe(202);
      await expect.poll(() => JSON.stringify((list.at(-1)?.json?.data?.content ?? []).find((x: any) => x.title === POLICY)?.notiChannels ?? []), { timeout: 30_000 }).toMatch(/email/i);
      const nc = (list.at(-1)!.json.data.content as any[]).find((x) => x.title === POLICY).notiChannels;
      test.info().annotations.push({ type: 'notiChannels', description: JSON.stringify(nc).slice(0, 500) });
      test.info().annotations.push({ type: 'row', description: clean(await policyRow(f, POLICY).innerText()) });
    });
  } finally {
    await test.step('(정리) 정책 삭제', async () => {
      const st = await deletePolicy(page, f, POLICY).catch((e) => { test.info().annotations.push({ type: 'cleanup-error', description: String(e).slice(0, 200) }); return -1; });
      test.info().annotations.push({ type: 'cleanup', description: `정책 삭제 ${st}` });
    });
  }
});

IV7_S13

tests/iv7/IV7_S13.spec.ts · 31줄 · 해시 f0125ce5 · 시나리오 보기

// IV7_S13: TC-OBS-013 Trigger / Notification History [정상, 공통]
// v0.6 TC-ID: TC-OBS-TRIG-04 — 기존 spec(mcmp-e2e deploy/scenarios/shared/step-handlers/obs/trigger.ts runObsTrig04)은 절차만 참고(사용자 결정 2026-10-14)
// 화면: Monitorings > Alert (iframe /embed/alerts/{ns}) → Policies(트리거 이력) · Notification History 탭
// 시작 상태: admin(mcmp) 로그인, ws01/default. TC-OBS-011·012 완료, 임계치 초과 트리거 발생
// 절차: 1.트리거 발생 후 History 확인 2.트리거 이력(GET /api/o11y/trigger/history) severity/value/threshold 3.(옵션) 코멘트 수정 4.알림 발송 이력(GET /api/o11y/trigger/noti/history) 5.Notification History 탭 발송 로그
// 기대결과: 트리거 이력 content[](severity, value, threshold), 알림 이력 content[](channelName, isSucceeded)
// 비고: 읽기 TC 로 먼저 진행(사용자 결정 2026-10-14). 정책·트리거가 없으면 SKIP(선행 TC-OBS-010~012 미실행)
import { test, expect } from '../../lib/fixtures';
import { loginAsAdmin } from '../../lib/ui';
import { openObs, collectResponses, clean } from '../../lib/iv7-obs';

test('IV7_S13 TC-OBS-013 Trigger / Notification History', async ({ page }) => {
  const policy = collectResponses(page, /\/api\/o11y\/trigger\/policy(\?|$)/);
  const notiHist = collectResponses(page, /\/api\/o11y\/trigger\/noti\/history/);
  const trigHist = collectResponses(page, /\/api\/o11y\/trigger\/history/);
  await loginAsAdmin(page);
  const f = await openObs(page, 'alerts');
  await expect.poll(() => policy.length, { timeout: 30_000 }).toBeGreaterThan(0);
  const policies = policy.at(-1)!.json?.data?.totalElements ?? 0;
  await test.step('4~5. Notification History 탭', async () => {
    await f.getByRole('button', { name: 'Notification History', exact: true }).click();
    await page.waitForTimeout(4000);
    test.info().annotations.push({ type: 'noti-history', description: `${notiHist.map((n) => `${n.status} ${n.url.slice(0, 120)} total=${n.json?.data?.totalElements}`).join(' | ') || '(호출 없음)'} / ${clean(await f.locator('body').innerText()).slice(0, 300)}` });
  });
  const trig = trigHist.at(-1)?.json?.data?.totalElements ?? 0;
  const noti = notiHist.at(-1)?.json?.data?.totalElements ?? 0;
  test.info().annotations.push({ type: 'counts', description: `정책 ${policies} / 트리거 이력 ${trig} / 알림 이력 ${noti}` });
  test.skip(!policies || (!trig && !noti), '정책·트리거 이력 없음 — 선행 TC-OBS-010~012(정책 생성·대상 연결·채널) 미실행');
  expect(noti + trig).toBeGreaterThan(0);
});

IV7_S14

tests/iv7/IV7_S14.spec.ts · 30줄 · 해시 c1c4f52b · 시나리오 보기

// IV7_S14: TC-OBS-014 NS 목록 조회 [정상, 공통]
// v0.6 TC-ID: TC-OBS-NS-01 — 기존 spec 없음(레거시는 구 UI DOM 존재만 확인). 2026-10-14 신규 작성
// 화면: Monitorings > Monitoring (iframe :18081/embed/monitoring/{ns})
// 시작 상태: admin(mcmp) 로그인, ws01/default. mc-o11y-manager · CB-Tumblebug 기동, NS 1개 이상
// 절차: 1.Monitoring 진입 2.화면이 부르는 NS 목록(GET /tumblebug/ns) 3.콘솔 Project(=NS) 목록이 NS 목록에 포함 4.iframe 상단 'Namespace — {선택 NS}'
// 기대결과: ns 배열(id, name), NS 목록 표시
// 비고: 콘솔 임베드 화면에는 NS 셀렉터가 없고 콘솔 상단 Project 선택이 NS 로 전달된다(2026-10-14) → Project 선택칸 = NS 목록으로 판정
import { test, expect } from '../../lib/fixtures';
import { loginAsAdmin } from '../../lib/ui';
import { openObs, collectResponses, OBS_NS, clean } from '../../lib/iv7-obs';

test('IV7_S14 TC-OBS-014 NS 목록 조회', async ({ page }) => {
  const res = collectResponses(page, /\/tumblebug\/ns(\?|$)/);
  await loginAsAdmin(page);
  const f = await openObs(page, 'monitoring');
  await test.step('1~2. NS 목록 응답(GET /tumblebug/ns)', async () => {
    await expect.poll(() => res.length, { timeout: 30_000 }).toBeGreaterThan(0);
    const ns: any[] = res.at(-1)!.json?.ns ?? [];
    test.info().annotations.push({ type: 'ns', description: ns.map((n) => `${n.id}(${n.name})`).join(', ') });
    expect(ns.length).toBeGreaterThan(0);
    expect(ns.every((n) => n.id && n.name)).toBe(true);
    const projects = (await page.locator('#select-current-project option').allInnerTexts()).map(clean).filter((p) => p && !/select/i.test(p));
    test.info().annotations.push({ type: 'console-projects', description: projects.join(', ') });
    for (const p of projects) expect(ns.map((n) => n.id), `Project ${p} 가 NS 목록에`).toContain(p);
  });
  await test.step('3. iframe 상단 Namespace 표시', async () => {
    await expect(f.getByText(new RegExp(`Namespace\\s*—\\s*${OBS_NS}`)).first()).toBeVisible();
  });
});

IV7_S15

tests/iv7/IV7_S15.spec.ts · 34줄 · 해시 db2d0c54 · 시나리오 보기

// IV7_S15: TC-OBS-015 NS 선택 → Infra 목록 [정상, 공통]
// v0.6 TC-ID: TC-OBS-NS-02 — 2026-10-14 신규 작성(레거시는 드롭다운 DOM 존재만 확인)
// 화면: Monitorings > Monitoring (Node 탭) / Logs 의 Workload(Infra/Cluster) 셀렉터
// 시작 상태: admin(mcmp) 로그인, ws01/default. 대상 NS 에 Infra 1개 이상
// 절차: 1.Project(=NS) default 선택 2.Infra 목록(GET /tumblebug/ns/{ns}/infra) 3.Monitoring 화면의 Infra 목록·'N Nodes / M Infras' 4.Logs Workload 셀렉터 목록
// 기대결과: infra 배열(id, name, status, node[]), Infra 셀렉터 표시
import { test, expect } from '../../lib/fixtures';
import { loginAsAdmin } from '../../lib/ui';
import { openObs, collectResponses, OBS_NS, clean } from '../../lib/iv7-obs';

test('IV7_S15 TC-OBS-015 NS 선택 → Infra 목록', async ({ page }) => {
  const res = collectResponses(page, new RegExp(`/tumblebug/ns/${OBS_NS}/infra(\\?|$)`));
  await loginAsAdmin(page);
  const f = await openObs(page, 'monitoring');
  let ids: string[] = [];
  await test.step('1~3. Infra 목록 응답 = Monitoring 화면', async () => {
    await expect.poll(() => res.length, { timeout: 30_000 }).toBeGreaterThan(0);
    const infra: any[] = res.at(-1)!.json?.infra ?? [];
    ids = infra.map((i) => i.id);
    const body = clean(await f.locator('body').innerText());
    test.info().annotations.push({ type: 'infra', description: infra.map((i) => `${i.id}(${i.status}, node ${(i.node ?? []).length})`).join(', ') });
    test.info().annotations.push({ type: 'summary', description: body.match(/\d+ Nodes \/ \d+ Infras/)?.[0] ?? '' });
    expect(ids.length).toBeGreaterThan(0);
    expect(Number(body.match(/(\d+) Infras/)?.[1])).toBe(ids.length);
    for (const id of ids) expect(body, `Infra ${id} 표시`).toContain(id);
  });
  await test.step('4. Logs 의 Workload(Infra/Cluster) 셀렉터', async () => {
    const g = await openObs(page, 'logs');
    const opts = (await g.locator('select').first().locator('option').allInnerTexts()).map(clean).filter((o) => !/^Select/.test(o));
    test.info().annotations.push({ type: 'logs-workload', description: opts.join(', ') });
    expect(opts.sort()).toEqual([...ids].sort());
  });
});

IV7_S16

tests/iv7/IV7_S16.spec.ts · 30줄 · 해시 e1851c70 · 시나리오 보기

// IV7_S16: TC-OBS-016 Infra 선택 → Node 목록 [정상, 공통]
// v0.6 TC-ID: TC-OBS-NS-03 — 2026-10-14 신규 작성(레거시는 DOM 존재만 확인)
// 화면: Monitorings > Config (iframe /embed/config/{ns}) — Infra 별 Node 표(Name/Node ID/Status/Monitoring Agent/Log Agent)
// 시작 상태: admin(mcmp) 로그인, ws01/default. 대상 Infra(OBS_INFRA, 기본 mc1) 존재
// 절차: 1.Config 진입 2.Infra 상세(GET /tumblebug/ns/{ns}/infra/{infra}) 또는 목록의 node 3.매니저 등록 node 상태(GET /api/o11y/monitoring/{ns}/{infra}/node) 4.화면의 node 행과 Agent 상태가 응답과 일치
// 기대결과: node 배열, 노드별 monitoring_agent_status / log_agent_status 표시
import { test, expect } from '../../lib/fixtures';
import { loginAsAdmin } from '../../lib/ui';
import { openObs, collectResponses, OBS_NS, OBS_INFRA, clean } from '../../lib/iv7-obs';

test('IV7_S16 TC-OBS-016 Infra 선택 → Node 목록', async ({ page }) => {
  const res = collectResponses(page, new RegExp(`/api/o11y/monitoring/${OBS_NS}/${OBS_INFRA}/node$`));
  await loginAsAdmin(page);
  const f = await openObs(page, 'config');
  await test.step(`1~4. ${OBS_INFRA} node 행 · Agent 상태 = 응답`, async () => {
    await expect.poll(() => res.length, { timeout: 30_000 }).toBeGreaterThan(0);
    const nodes: any[] = res.at(-1)!.json?.data ?? [];
    test.info().annotations.push({ type: 'nodes', description: nodes.map((n) => `${n.node_id}: mon=${n.monitoring_agent_status} log=${n.log_agent_status}`).join(', ') });
    expect(nodes.length).toBeGreaterThan(0);
    for (const n of nodes) {
      const row = f.locator('tr').filter({ has: f.locator('td', { hasText: new RegExp(`^${n.node_id}$`) }) }).first();
      const t = clean(await row.innerText());
      test.info().annotations.push({ type: `row-${n.node_id}`, description: t });
      expect(t).toContain(n.node_id);
      expect(t.toUpperCase()).toContain(String(n.monitoring_agent_status).toUpperCase());
      expect(t.toUpperCase()).toContain(String(n.log_agent_status).toUpperCase());
    }
  });
});

IV7_S17

tests/iv7/IV7_S17.spec.ts · 30줄 · 해시 2f0355ad · 시나리오 보기

// IV7_S17: TC-OBS-017 모니터링 화면 진입 [정상, 공통]
// v0.6 TC-ID: TC-OBS-MON-DATA-01 — 기존 spec(mcmp-e2e mc-web-console/specs/obs/TC-OBS-MON-DATA-01-monitoring-data-screen.spec.ts)은 절차만 참고(사용자 결정 2026-10-14)
// 화면: Monitorings > Monitoring → node 대시보드(iframe /embed/monitoring/{ns}/{infra}/{node})
// 시작 상태: admin(mcmp) 로그인, ws01/default. 대상 node(OBS_INFRA/OBS_NODE, 기본 mc1/g1-1)
// 절차: 1.Monitoring 진입 → node 대시보드 경로 2.측정항목 메타(GET /api/o11y/monitoring/influxdb/measurement) 3.좌측 컨트롤(Agent/API, Workload/Server/Measurement/Metric/Aggregation/Range/Period, Start Monitoring)과 차트 영역
// 기대결과: 모니터링 대시보드 화면, 측정항목 목록·차트 컨트롤
// 비고: Infra 개요의 node 카드 클릭으로는 대시보드가 열리지 않아 시트 경로로 iframe 이동(2026-10-14)
import { test, expect } from '../../lib/fixtures';
import { loginAsAdmin } from '../../lib/ui';
import { openObs, obsGoto, collectResponses, OBS_NS, OBS_INFRA, OBS_NODE } from '../../lib/iv7-obs';

test('IV7_S17 TC-OBS-017 모니터링 화면 진입', async ({ page }) => {
  const meas = collectResponses(page, /\/api\/o11y\/monitoring\/influxdb\/measurement$/);
  await loginAsAdmin(page);
  let f = await openObs(page, 'monitoring');
  f = await test.step(`1. node 대시보드 /embed/monitoring/${OBS_NS}/${OBS_INFRA}/${OBS_NODE}`, () => obsGoto(page, f, `/embed/monitoring/${OBS_NS}/${OBS_INFRA}/${OBS_NODE}`));
  await test.step('2. 측정항목 메타 응답', async () => {
    await expect.poll(() => meas.length, { timeout: 30_000 }).toBeGreaterThan(0);
    const m: any[] = meas.at(-1)!.json?.data ?? [];
    test.info().annotations.push({ type: 'measurements', description: m.map((x) => x.measurement).join(', ') });
    expect(m.map((x) => x.measurement)).toEqual(expect.arrayContaining(['cpu', 'mem', 'disk']));
  });
  await test.step('3. 컨트롤 · 차트 영역', async () => {
    await expect(f.getByText('Monitoring Trend').first()).toBeVisible();
    for (const l of ['Workload', 'Server', 'Measurement', 'Metric', 'Aggregation', 'Range', 'Period']) await expect(f.getByText(l, { exact: true }).first()).toBeVisible();
    await expect(f.getByRole('button', { name: 'Start Monitoring' })).toBeVisible();
    await expect(f.getByText(`Node: ${OBS_NODE}`).first()).toBeVisible();
  });
});

IV7_S18

tests/iv7/IV7_S18.spec.ts · 29줄 · 해시 10c50ab9 · 시나리오 보기

// IV7_S18: TC-OBS-018 콘솔 iframe 모니터링 진입 [정상, 공통]
// v0.6 TC-ID: TC-OBS-IFRAME-01 — 기존 spec(mcmp-e2e mc-web-console/specs/obs/TC-OBS-IFRAME-01-console-iframe.spec.ts)은 절차만 참고(사용자 결정 2026-10-14)
// 화면: mc-web-console Monitorings 메뉴 6개(Monitoring/Logs/Config/Insight/Alert/Tracing) → iframe :18081/embed/...
// 시작 상태: admin(mcmp) 로그인, ws01/default. mc-web-console 연동 설정
// 절차: 1.각 메뉴 진입 → iframe src 가 /embed/{monitoring|logs|config|insight|alerts|trace}/{ns} 2.EmbedLayout(헤더/사이드바 없는 화면)으로 로드 3.화면별 핵심 요소 동작
// 기대결과: 네비게이션 크롬 없는 임베드 화면, iframe 내 기능 정상
import { test, expect } from '../../lib/fixtures';
import { loginAsAdmin } from '../../lib/ui';
import { openObs, OBS_NS, clean, type ObsMenu } from '../../lib/iv7-obs';

const EXPECT: Record<ObsMenu, RegExp> = {
  monitoring: /Namespace\s*—/, logs: /Log Manage/, config: /Monitor Setting/, insight: /Anomaly Detection/, alerts: /Trigger Policies|Policies/, trace: /Trace Manage/,
};
test('IV7_S18 TC-OBS-018 콘솔 iframe 모니터링 진입', async ({ page }) => {
  await loginAsAdmin(page);
  for (const menu of Object.keys(EXPECT) as ObsMenu[]) {
    await test.step(`${menu}: /embed/${menu}/${OBS_NS} · 임베드 레이아웃 · 핵심 요소`, async () => {
      const f = await openObs(page, menu);
      const path = new URL(f.url()).pathname;
      const body = clean(await f.locator('body').innerText());
      const chrome = await f.locator('nav, aside, header .navbar, .sidebar').count();
      test.info().annotations.push({ type: menu, description: `${path} / nav·sidebar 요소 ${chrome} / ${body.slice(0, 120)}` });
      expect(path).toBe(`/embed/${menu}/${OBS_NS}`);
      expect(body).toMatch(EXPECT[menu]);
      expect(body, '독립 실행 화면의 상단 네비 메뉴 없음').not.toMatch(/MC-Observability\s+(Monitoring\s+Logs|Dashboard)/);
    });
  }
});

IV7_S19

tests/iv7/IV7_S19.spec.ts · 37줄 · 해시 8df7af22 · 시나리오 보기

// IV7_S19: TC-OBS-019 Agent 기반 시계열 메트릭 (InfluxDB) [정상, 공통]
// v0.6 TC-ID: TC-OBS-METRIC-01 — 기존 spec(mcmp-e2e mc-web-console/specs/obs/TC-OBS-METRIC-01-agent-timeseries.spec.ts)은 절차만 참고(사용자 결정 2026-10-14)
// 화면: Monitorings > Monitoring → node 대시보드(/embed/monitoring/{ns}/{infra}/{node}, 데이터소스 Agent)
// 시작 상태: admin(mcmp) 로그인, ws01/default. 대상 node 에 Monitoring Agent SUCCESS · 수집 데이터 존재
// 절차: 1.node 대시보드(Agent) 2.Measurement cpu · Metric usage_idle · Aggregation Mean · Range 1H · Period 1m 3.Start Monitoring 4.노드 메트릭(POST /api/o11y/monitoring/influxdb/metric/{ns}/{infra}/{node}) 5.시계열 차트
// 기대결과: MetricDTO[](name, columns, tags, values[[time,value]]), 시계열 차트 표시
import { test, expect } from '../../lib/fixtures';
import { loginAsAdmin } from '../../lib/ui';
import { openObs, obsGoto, collectResponses, selectNear, OBS_NS, OBS_INFRA, OBS_NODE } from '../../lib/iv7-obs';

test('IV7_S19 TC-OBS-019 Agent 기반 시계열 메트릭 (InfluxDB)', async ({ page }) => {
  const metric = collectResponses(page, new RegExp(`/influxdb/metric/${OBS_NS}/${OBS_INFRA}/${OBS_NODE}$`));
  await loginAsAdmin(page);
  let f = await openObs(page, 'monitoring');
  f = await obsGoto(page, f, `/embed/monitoring/${OBS_NS}/${OBS_INFRA}/${OBS_NODE}`);
  await test.step('2. cpu · usage_idle · Mean · 1H · 1m', async () => {
    await selectNear(f, 'Measurement', 'cpu');
    await page.waitForTimeout(1500);
    await selectNear(f, 'Metric', 'usage_idle');
    await selectNear(f, 'Aggregation', 'Mean');
    await selectNear(f, 'Range', '1H');
    await selectNear(f, 'Period', '1m');
  });
  await test.step('3~5. Start Monitoring → 메트릭 응답 · 차트', async () => {
    const n0 = metric.length;
    await f.getByRole('button', { name: 'Start Monitoring' }).click();
    await expect.poll(() => metric.slice(n0).find((m) => m.json?.data?.[0]?.columns?.includes('usage_idle')), { timeout: 30_000 }).toBeTruthy();
    const d = metric.slice(n0).find((m) => m.json?.data?.[0]?.columns?.includes('usage_idle'))!.json.data[0];
    const vals = (d.values ?? []).filter((v: any[]) => v[1] !== null);
    test.info().annotations.push({ type: 'metric', description: `${d.name} ${JSON.stringify(d.tags)} columns=${d.columns} 값 ${vals.length}개 예: ${JSON.stringify(vals.slice(0, 2))}` });
    expect(vals.length, '수집된 시계열 값').toBeGreaterThan(0);
    await expect(f.locator('svg:visible, canvas:visible').first()).toBeVisible();
    await expect(f.getByText(/Download CSV/).first(), '차트 영역(다운로드 메뉴)').toBeVisible();
    test.info().annotations.push({ type: 'chart', description: (await f.locator('body').innerText()).replace(/\s+/g, ' ').match(/Node: [^\n]{0,200}/)?.[0] ?? '' });
  });
});

IV7_S20

tests/iv7/IV7_S20.spec.ts · 33줄 · 해시 9e224647 · 시나리오 보기

// IV7_S20: TC-OBS-020 CSP API 메트릭 (cb-spider) [정상, AWS]
// v0.6 TC-ID: TC-OBS-METRIC-02 — 기존 spec(mcmp-e2e mc-web-console/specs/obs/TC-OBS-METRIC-02-csp-api-metrics.spec.ts)은 절차만 참고(사용자 결정 2026-10-14)
// 화면: Monitorings > Monitoring → node 대시보드 → 'API' 데이터소스(?source=csp)
// 시작 상태: admin(mcmp) 로그인, ws01/default. 대상 node Running · CSP Credential 유효(Agent 불필요)
// 절차: 1.node 대시보드에서 API(CSP) 선택 2.connectionName·cspResourceName 3.API Metric CPU Usage Percent 4.CSP 메트릭(GET /api/o11y/monitoring/csp/node/{cspResourceName}/{metricType}?ConnectionName=…) 5.CSP 시계열 차트
// 기대결과: SpiderMonitoringInfo.Data(metricName, metricUnit, timestampValues[]), CSP 메트릭 차트
// 비고: AWS 는 memory_usage 미지원(unsupported:true, 2026-10-14) — 판정은 CPU 로
import { test, expect } from '../../lib/fixtures';
import { loginAsAdmin } from '../../lib/ui';
import { openObs, obsGoto, collectResponses, OBS_NS, OBS_INFRA, OBS_NODE } from '../../lib/iv7-obs';

test('IV7_S20_AWS TC-OBS-020 CSP API 메트릭 (cb-spider)', async ({ page }) => {
  const csp = collectResponses(page, /\/api\/o11y\/monitoring\/csp\/node\//);
  await loginAsAdmin(page);
  let f = await openObs(page, 'monitoring');
  f = await obsGoto(page, f, `/embed/monitoring/${OBS_NS}/${OBS_INFRA}/${OBS_NODE}`);
  await test.step('1. 데이터소스 API(CSP) 선택', async () => {
    await f.getByRole('button', { name: 'API', exact: true }).click();
    await page.waitForTimeout(4000);
    await expect(f.getByText('API Metric').first()).toBeVisible();
  });
  await test.step('3~5. CPU Usage Percent → CSP 메트릭 응답 · 차트', async () => {
    await f.getByRole('button', { name: 'CPU Usage Percent' }).click();
    await expect.poll(() => csp.find((c) => /cpu_usage/.test(c.url)), { timeout: 30_000 }).toBeTruthy();
    const r = csp.filter((c) => /cpu_usage/.test(c.url)).at(-1)!;
    const unsupported = csp.filter((c) => c.json?.unsupported).map((c) => c.url.match(/node\/[^/]+\/(\w+)/)?.[1]);
    test.info().annotations.push({ type: 'csp', description: `${r.url} → ${r.json?.metricName} (${r.json?.metricUnit}) 값 ${(r.json?.timestampValues ?? []).length}개 / unsupported: ${unsupported.join(', ') || '없음'}` });
    expect(r.url).toMatch(/ConnectionName=[\w-]+/);
    expect((r.json?.timestampValues ?? []).length).toBeGreaterThan(0);
    await expect(f.getByText(/CPU Usage Percent \(Percent\)/).first()).toBeVisible();
  });
});

IV7_S21

tests/iv7/IV7_S21.spec.ts · 44줄 · 해시 a51ea5a9 · 시나리오 보기

// IV7_S21: TC-OBS-021 Infra / NS 레벨 오버뷰 [정상, 공통]
// v0.6 TC-ID: TC-OBS-METRIC-04 — 기존 spec(mcmp-e2e mc-web-console/specs/obs/TC-OBS-METRIC-04-infra-ns-overview.spec.ts)은 절차만 참고(사용자 결정 2026-10-14)
// 화면: Monitorings > Monitoring (NS 오버뷰 /embed/monitoring/{ns}, Infra 오버뷰 /embed/monitoring/{ns}/{infra})
// 시작 상태: admin(mcmp) 로그인, ws01/default. 수집 데이터 존재(TC-OBS-019 또는 020)
// 절차: 1.NS 오버뷰 2.노드별 메트릭 병렬 조회(agent: influxdb/metric) 3.Node 탭 노드 카드(CPU/Memory/Disk) 4.K8s 탭 5.데이터소스 Agent/API 토글 → csp/node 조회
// 기대결과: 노드/클러스터 메트릭 요약 카드(CPU/Memory/Disk)
import { test, expect } from '../../lib/fixtures';
import { loginAsAdmin } from '../../lib/ui';
import { openObs, obsGoto, collectResponses, OBS_NS, OBS_INFRA, clean } from '../../lib/iv7-obs';

test('IV7_S21 TC-OBS-021 Infra / NS 레벨 오버뷰', async ({ page }) => {
  const agent = collectResponses(page, /\/influxdb\/metric\//);
  const csp = collectResponses(page, /\/api\/o11y\/monitoring\/csp\/node\//);
  await loginAsAdmin(page);
  let f = await openObs(page, 'monitoring');
  await test.step('1~3. NS 오버뷰 · 노드 카드 CPU/Memory/Disk', async () => {
    await expect.poll(() => agent.length, { timeout: 30_000 }).toBeGreaterThan(0);
    const body = clean(await f.locator('body').innerText());
    test.info().annotations.push({ type: 'ns-overview', description: `agent 조회 ${agent.length}건 / ${body.slice(0, 300)}` });
    // 2026-10-14 관찰: mc1/g1-1 카드가 'No data' — 화면은 mc1/g1-1 대신 같은 node 이름의 mt1/g1-1 을 조회(이름 충돌 의심). 기록만 한다
    const asked = [...new Set(agent.map((a) => a.url.replace(/.*influxdb\/metric\//, '')))];
    test.info().annotations.push({ type: 'requested-nodes', description: asked.join(', ') });
    for (const k of ['CPU Used', 'Memory Used', 'Disk Used']) expect(body).toContain(k);
  });
  await test.step(`Infra 오버뷰 /embed/monitoring/${OBS_NS}/${OBS_INFRA}: 값 있는 카드`, async () => {
    f = await obsGoto(page, f, `/embed/monitoring/${OBS_NS}/${OBS_INFRA}`);
    await expect.poll(async () => /CPU Used [\d.]+%/.test(clean(await f.locator('body').innerText())), { timeout: 60_000 }).toBe(true);
    const body = clean(await f.locator('body').innerText());
    test.info().annotations.push({ type: 'infra-overview', description: body.slice(0, 500) });
    expect(body).toContain(`Infra Overview — ${OBS_INFRA}`);
  });
  await test.step('4. K8s 탭', async () => {
    await f.getByRole('button', { name: 'K8s', exact: true }).click();
    await page.waitForTimeout(3000);
    test.info().annotations.push({ type: 'k8s', description: clean(await f.locator('body').innerText()).match(/\d+ Nodes \/ \d+ Clusters[^.]*/)?.[0] ?? '' });
    await f.getByRole('button', { name: 'Node', exact: true }).click();
  });
  await test.step('5. 데이터소스 API 토글 → csp/node 조회', async () => {
    await f.getByRole('button', { name: 'API', exact: true }).click();
    await expect.poll(() => csp.length, { timeout: 60_000 }).toBeGreaterThan(0);
    test.info().annotations.push({ type: 'csp', description: `csp/node 조회 ${csp.length}건: ${csp.slice(0, 3).map((c) => c.url.split('?')[0]).join(', ')}` });
  });
});

IV7_S22

tests/iv7/IV7_S22.spec.ts · 33줄 · 해시 40bcbd88 · 시나리오 보기

// IV7_S22: TC-OBS-022 K8s Cluster 노드 메트릭 [정상, AWS]
// v0.6 TC-ID: TC-OBS-METRIC-03 — 기존 spec(mcmp-e2e mc-web-console/specs/obs/TC-OBS-METRIC-03-k8s-node-metrics.spec.ts)은 절차만 참고(사용자 결정 2026-10-14)
// 화면: Monitorings > Monitoring → K8s 탭 → 클러스터 → NodeGroup → Node 대시보드(/embed/monitoring/{ns}/k8s/{connection}/{cluster}/{nodeGroup}/{n})
// 시작 상태: admin(mcmp) 로그인, ws01/{K8s 가 있는 Project}. mc-infra-manager 로 K8s 클러스터 생성, CSP Credential 유효
// 절차: 1.K8s 탭(데이터소스 API) 2.클러스터 목록(GET /tumblebug/ns/{ns}/k8sCluster, /api/o11y/monitoring/csp/clusters?ConnectionName=…) 3.Cluster → NodeGroup → Node 4.노드 메트릭(GET /api/o11y/monitoring/csp/cluster/{cluster}/{ng}/{n}/{metricType}) 5.차트
// 기대결과: SpiderMonitoringInfo.Data, 클러스터 노드 메트릭 차트
// 비고: 클러스터가 없으면 SKIP(선행 미충족). 대상 Project 는 OBS_K8S_NS(기본 OBS_NS)
import { test, expect } from '../../lib/fixtures';
import { loginAsAdmin } from '../../lib/ui';
import { openObs, collectResponses, OBS_NS, clean } from '../../lib/iv7-obs';

const NS = process.env.OBS_K8S_NS ?? OBS_NS;
test('IV7_S22_AWS TC-OBS-022 K8s Cluster 노드 메트릭', async ({ page }) => {
  const clusters = collectResponses(page, /\/k8sCluster$|\/csp\/clusters\?/);
  const metrics = collectResponses(page, /\/api\/o11y\/monitoring\/csp\/cluster\//);
  await loginAsAdmin(page);
  const f = await openObs(page, 'monitoring', NS);
  await f.getByRole('button', { name: 'K8s', exact: true }).click();
  await page.waitForTimeout(6000);
  const body = clean(await f.locator('body').innerText());
  const tb = clusters.find((c) => /k8sCluster$/.test(c.url))?.json?.K8sClusterInfo ?? [];
  test.info().annotations.push({ type: 'k8s', description: `${body.match(/\d+ Nodes \/ \d+ Clusters[^.]*/)?.[0] ?? ''} / tumblebug ${tb.length}건 / csp ${clusters.filter((c) => /csp\/clusters/.test(c.url)).map((c) => (c.json ?? []).length).join(',')}` });
  test.skip(!tb.length, `NS ${NS} 에 K8s 클러스터 없음(선행 미충족)`);
  // 데이터소스 기본값은 Agent(노드 에이전트 수집값) — CSP 노드 메트릭은 'API' 토글에서 조회된다(2026-10-15 p1/k1s 확인: Agent 는 '0/1 node(s) reporting', API 는 csp/cluster/{cspId}/{ng}/{n}/{metric} 7종)
  await test.step('3~5. 데이터소스 API → 클러스터 NodeGroup 노드 메트릭', async () => {
    await f.getByRole('button', { name: 'API', exact: true }).click();
    await expect.poll(() => metrics.length, { timeout: 60_000 }).toBeGreaterThan(0);
    const m = metrics.find((x) => (x.json?.timestampValues ?? []).length > 0);
    test.info().annotations.push({ type: 'metric', description: metrics.map((x) => `${x.url.split('?')[0]} ${(x.json?.timestampValues ?? []).length}`).join(', ') });
    expect(m, '값이 있는 클러스터 노드 메트릭').toBeTruthy();
  });
});

IV7_S23

tests/iv7/IV7_S23.spec.ts · 91줄 · 해시 6c91f225 · 시나리오 보기

// IV7_S23: TC-OBS-023 Anomaly Detection 설정 [정상, 공통]
// v0.6 TC-ID: TC-OBS-INSIGHT-01
// 화면: Monitorings > Insight → Anomaly Detection → + New Setting
// 시작 상태: admin(mcmp) 로그인, ws01/default. 수집 데이터(TC-OBS-AGENT-04), insight 엔진 기동
// 절차: 1.Anomaly Detection 탭 2.옵션·측정항목(GET …/anomaly-detection/options, /measurement) 3.측정항목 mem · 대상 node · 주기 5m → 생성(POST …/settings) 4.설정 목록 반영 5.(정리) 삭제(DELETE …/settings/{seq})
// 기대결과: 생성된 설정(settingSeq), 설정 목록 표시
// 실행 보류(사용자 결정 2026-10-14: 상태를 바꾸는 OBS TC 는 spec 만 작성). OBS_WRITE=1 일 때만 실행.
// 2026-10-16 수정(첫 실행, rf-obs2): 추정 셀렉터 교체 — '+ New Setting' 폼의 select 는 <label>(Scope — Infra / Cluster *, Scope — Node *, Measurement *, Execution Interval *) 다음 select.
//   Node select 는 Infra 선택 전 disabled. 목록 행 'default/{infra}/{node} | mem | 5m | Last Run | Delete'. 대상 node 는 OBS_TARGET_NODE(기본 OBS_NODE) —
//   IV7_S24(이력 조회)가 쓰는 기존 설정(mc1/g1-1 cpu)과 겹치지 않게 다른 node 를 지정한다. 실패해도 finally 에서 만든 설정 삭제
import { test, expect } from '../../lib/fixtures';
import { loginAsAdmin } from '../../lib/ui';
import { openObs, collectResponses, clean, OBS_NS, OBS_INFRA, OBS_NODE } from '../../lib/iv7-obs';

const NODE = process.env.OBS_TARGET_NODE ?? OBS_NODE;
test('IV7_S23 TC-OBS-023 Anomaly Detection 설정', async ({ page }) => {
  test.skip(process.env.OBS_WRITE !== '1', '상태 변경 TC — OBS_WRITE=1 일 때만 실행(사용자 결정 2026-10-14)');
  const api = collectResponses(page, /\/anomaly-detection\/(settings|options|measurement)/);
  await loginAsAdmin(page);
  let f0 = await openObs(page, 'insight');
  // iframe 이 프로젝트 선택 뒤 다시 로드되어 Frame 이 detach 되는 경우가 있어(2026-10-16 2차 실행) 매번 현재 insight frame 을 찾는다
  const cur = () => page.frames().filter((x) => !x.isDetached() && x.url().includes(':18081') && x.url().includes('/embed/insight')).at(-1) ?? f0;
  const sel = (label: string) => cur().locator(`xpath=//label[starts-with(normalize-space(),"${label}")]/following::select[1]`).first();
  const rowOf = () => cur().locator('tr').filter({ hasText: `${OBS_NS}/${OBS_INFRA}/${NODE}` }).filter({ has: cur().locator('td', { hasText: /^\s*mem\s*$/ }) }).filter({ has: cur().locator('td', { hasText: /^\s*5m\s*$/ }) }); // 셀 단위로 비교(textContent 는 셀 사이 공백이 없어 \b 정규식이 안 맞음)
  const settingsList = () => (api.filter((a) => a.method === 'GET' && /settings$/.test(a.url)).at(-1)?.json?.data ?? []) as any[];
  let created = false;
  try {
    await test.step('1~2. Anomaly Detection 탭 · 옵션/측정항목', async () => {
      await cur().getByRole('button', { name: 'Anomaly Detection', exact: true }).click();
      await expect.poll(() => api.filter((a) => /options|measurement/.test(a.url)).length, { timeout: 30_000 }).toBeGreaterThan(1);
      await expect.poll(() => api.filter((a) => a.method === 'GET' && /settings$/.test(a.url)).length, { timeout: 30_000 }).toBeGreaterThan(0);
      for (const a of api.filter((x) => /options|measurement/.test(x.url))) test.info().annotations.push({ type: 'api', description: `${a.status} ${a.url} ${JSON.stringify(a.json?.data ?? '').slice(0, 300)}` });
      test.info().annotations.push({ type: 'settings-before', description: settingsList().map((s) => `${s.seq}:${s.ns_id}/${s.infra_id}/${s.node_id} ${s.measurement} ${s.execution_interval}`).join(', ') });
      // 이전 실행이 남긴 같은 설정(이 spec 전용 대상)은 먼저 지운다 — 표가 응답보다 늦게 그려지므로 API 목록으로 판단 후 행을 기다린다
      if (settingsList().some((s) => s.infra_id === OBS_INFRA && s.node_id === NODE && s.measurement === 'mem')) {
        test.info().annotations.push({ type: 'frames', description: page.frames().map((x) => x.url().replace(/^https?:\/\/[^/]+/, '')).join(' , ') + ' / rows: ' + (await cur().locator('tr').allInnerTexts()).map(clean).join(' || ') });
        await expect(rowOf().first()).toBeVisible({ timeout: 30_000 });
        await rowOf().first().getByRole('button', { name: 'Delete' }).click();
        await expect(rowOf()).toHaveCount(0, { timeout: 30_000 });
        test.info().annotations.push({ type: 'precondition', description: `이전 실행이 남긴 ${OBS_INFRA}/${NODE} mem 5m 설정 삭제` });
      }
    });
    await test.step('3. + New Setting → mem · node · 5m → Create', async () => {
      await cur().getByRole('button', { name: '+ New Setting' }).click();
      await sel('Scope — Infra').selectOption(OBS_INFRA);
      await expect(sel('Scope — Node')).toBeEnabled({ timeout: 30_000 });
      await expect.poll(async () => (await sel('Scope — Node').locator('option').allInnerTexts()).join('|'), { timeout: 30_000 }).toContain(NODE);
      const opt = (await sel('Scope — Node').locator('option').allInnerTexts()).map((s) => s.trim()).find((s) => s === NODE || s.startsWith(`${NODE} `))!;
      await sel('Scope — Node').selectOption({ label: opt });
      await sel('Measurement').selectOption('mem');
      await sel('Execution Interval').selectOption('5m');
      await cur().getByRole('button', { name: 'Create', exact: true }).click();
      await expect.poll(() => api.find((a) => a.method === 'POST')?.status ?? 0, { message: '설정 생성 POST', timeout: 30_000 }).toBeGreaterThan(0);
      const p = api.find((a) => a.method === 'POST')!;
      created = p.status < 300;
      test.info().annotations.push({ type: 'create', description: `${p.status} POST ${p.url} ${JSON.stringify(p.json ?? '').slice(0, 300)}` });
      expect(p.status, '생성 2xx').toBeLessThan(300);
    });
    await test.step('4. 설정 목록 반영', async () => {
      if (!(await rowOf().first().isVisible({ timeout: 15_000 }).catch(() => false))) {
        // 2026-10-16 1차 실행: 생성 200('Target Registered Successfully') 후 목록이 바로 갱신되지 않음 → 화면을 다시 열어 확인
        test.info().annotations.push({ type: 'list-refresh', description: `생성 후 15초 안에 목록 미반영(화면: ${clean(await cur().locator('table').first().innerText().catch(() => '')).slice(0, 200)}) → 화면 다시 열기` });
        f0 = await openObs(page, 'insight');
      }
      await expect(rowOf().first()).toBeVisible({ timeout: 30_000 });
      test.info().annotations.push({ type: 'row', description: clean(await rowOf().first().innerText()) });
      const s = settingsList().find((x) => x.infra_id === OBS_INFRA && x.node_id === NODE && x.measurement === 'mem');
      test.info().annotations.push({ type: 'setting', description: JSON.stringify(s ?? null) });
      expect(s?.seq, 'settingSeq').toBeTruthy();
    });
    await test.step('5. (정리) 삭제', async () => {
      const seq = settingsList().find((x) => x.infra_id === OBS_INFRA && x.node_id === NODE && x.measurement === 'mem')?.seq;
      await rowOf().first().getByRole('button', { name: 'Delete' }).click();
      const mine = () => api.find((a) => a.method === 'DELETE' && a.url.endsWith(`/settings/${seq}`));
      await expect.poll(() => mine()?.status ?? 0, { message: `설정 #${seq} 삭제 DELETE`, timeout: 30_000 }).toBeGreaterThan(0);
      const d = mine()!;
      test.info().annotations.push({ type: 'delete', description: `${d.status} DELETE ${d.url}` });
      expect(d.status).toBeLessThan(300);
      await expect(rowOf()).toHaveCount(0, { timeout: 30_000 });
      created = false;
    });
  } finally {
    if (created && !(await rowOf().first().isVisible().catch(() => false))) f0 = await openObs(page, 'insight').catch(() => f0);
    if (created && await rowOf().first().isVisible().catch(() => false)) { // 실패 시 정리
      await rowOf().first().getByRole('button', { name: 'Delete' }).click().catch(() => {});
      await page.waitForTimeout(5000);
      test.info().annotations.push({ type: 'cleanup', description: `finally 삭제 시도 → 남은 행 ${await rowOf().count()}` });
    }
  }
});

IV7_S24

tests/iv7/IV7_S24.spec.ts · 38줄 · 해시 eb5eb930 · 시나리오 보기

// IV7_S24: TC-OBS-024 Anomaly History 조회 [정상, 공통]
// v0.6 TC-ID: TC-OBS-INSIGHT-02 — 기존 spec(mcmp-e2e deploy/scenarios/shared/step-handlers/obs/insight.ts runObsInsight02)은 절차만 참고(사용자 결정 2026-10-14)
// 화면: Monitorings > Insight → Anomaly Detection 탭 → Anomaly Detection History
// 시작 상태: admin(mcmp) 로그인, ws01/default. Anomaly Detection 설정 존재(설정 후 데이터 누적) — 2026-10-14 기존 설정 default/mc1/g1-1 cpu 30m
// 절차: 1.Insight 진입 2.History 에서 Infra · Node · Measurement 선택 3.Load History 4.이력 조회(GET /api/o11y/insight/anomaly-detection/ns/{ns}/infra/{infra}/node/{node}/history?measurement=…) 5.anomaly_score 차트
// 기대결과: {values:[{timestamp, anomaly_score}]}, 이상치 점수 차트
// 비고: 설정을 새로 만들지 않고 기존 설정의 이력을 읽는다(읽기 전용)
import { test, expect } from '../../lib/fixtures';
import { loginAsAdmin } from '../../lib/ui';
import { openObs, collectResponses, selectNear, OBS_INFRA, OBS_NODE, clean } from '../../lib/iv7-obs';

test('IV7_S24 TC-OBS-024 Anomaly History 조회', async ({ page }) => {
  const settings = collectResponses(page, /\/anomaly-detection\/settings$/);
  const hist = collectResponses(page, /\/anomaly-detection\/ns\/.+\/history/);
  await loginAsAdmin(page);
  const f = await openObs(page, 'insight');
  await expect.poll(() => settings.length, { timeout: 30_000 }).toBeGreaterThan(0);
  const s = (settings.at(-1)!.json?.data ?? []).find((x: any) => x.infra_id === OBS_INFRA && x.node_id === OBS_NODE);
  test.info().annotations.push({ type: 'setting', description: JSON.stringify(s ?? null) });
  test.skip(!s, `${OBS_INFRA}/${OBS_NODE} Anomaly Detection 설정 없음(선행 TC-OBS-023)`);
  await test.step(`2~4. ${OBS_INFRA} / ${OBS_NODE} / ${s.measurement} → Load History`, async () => {
    await f.locator('select').filter({ has: f.locator('option', { hasText: 'Select Infra / Cluster' }) }).first().selectOption({ label: OBS_INFRA });
    await page.waitForTimeout(2000);
    await f.locator('select').filter({ has: f.locator('option', { hasText: 'Select Node' }) }).first().selectOption({ label: OBS_NODE });
    await f.locator('select').filter({ has: f.locator('option', { hasText: 'Select Measurement' }) }).first().selectOption({ label: s.measurement });
    await f.getByRole('button', { name: 'Load History' }).click();
    await expect.poll(() => hist.length, { timeout: 30_000 }).toBeGreaterThan(0);
  });
  await test.step('5. anomaly_score 값 · 차트', async () => {
    const vals = (hist.at(-1)!.json?.data?.values ?? hist.at(-1)!.json?.values ?? []).filter((v: any) => v.anomaly_score !== null && !Number.isNaN(Number(v.anomaly_score)));
    test.info().annotations.push({ type: 'history', description: `${hist.at(-1)!.status} ${hist.at(-1)!.url.slice(0, 160)} / 값 ${vals.length}개 예: ${JSON.stringify(vals.slice(0, 2))}` });
    expect(hist.at(-1)!.status).toBe(200);
    expect(vals.length, 'anomaly_score 값').toBeGreaterThan(0);
    await expect(f.locator('svg:visible, canvas:visible').first()).toBeVisible();
    test.info().annotations.push({ type: 'screen', description: clean(await f.locator('body').innerText()).match(/Anomaly Detection History.{0,300}/)?.[0] ?? '' });
  });
});

IV7_S25

tests/iv7/IV7_S25.spec.ts · 63줄 · 해시 16450ba8 · 시나리오 보기

// IV7_S25: TC-OBS-025 Prediction (예측) [정상, 공통]
// v0.6 TC-ID: TC-OBS-INSIGHT-03
// 화면: Monitorings > Insight → Prediction 탭
// 시작 상태: admin(mcmp) 로그인, ws01/default. 수집 데이터, insight 엔진 기동
// 절차: 1.Prediction 탭 2.옵션(GET …/predictions/options) 3.Infra·Node·측정항목 cpu 선택 → 예측 실행(POST …/predictions/ns/{ns}/infra/{infra}/node/{node}) 4.예측 이력(GET …/history) 5.predicted_value 차트(과거+미래, CPU 는 100-usage_idle)
// 기대결과: {values:[{timestamp, predicted_value}]}, 예측 차트
// 실행 보류(사용자 결정 2026-10-14: 상태를 바꾸는 OBS TC 는 spec 만 작성). OBS_WRITE=1 일 때만 실행.
// 2026-10-16 수정(첫 실행, rf-obs2): 추정 셀렉터 교체 — Prediction 탭 폼은 <label>(Scope — Infra / Cluster *, Scope — Node *, Measurement *, Prediction Range *) 다음 select,
//   버튼 'Run Prediction' / 'Load Saved'. 화면 안내: 'Predictions become available about one day after the monitoring agent is installed'. 예측 범위는 OBS_PRED_RANGE(기본 '1 day').
//   예측 POST 응답·이력 응답을 모두 기록(오류 응답이면 그 내용으로 판정). 실제 응답 값 필드는 'value'(시트 기대 'predicted_value') — 둘 다 허용
import { test, expect } from '../../lib/fixtures';
import { loginAsAdmin } from '../../lib/ui';
import { openObs, collectResponses, clean, OBS_INFRA, OBS_NODE } from '../../lib/iv7-obs';

const NODE = process.env.OBS_TARGET_NODE ?? OBS_NODE;
test('IV7_S25 TC-OBS-025 Prediction (예측)', async ({ page }) => {
  test.skip(process.env.OBS_WRITE !== '1', '상태 변경 TC — OBS_WRITE=1 일 때만 실행(사용자 결정 2026-10-14)');
  const api = collectResponses(page, /\/insight\/predictions/);
  await loginAsAdmin(page);
  const f = await openObs(page, 'insight');
  const sel = (label: string) => f.locator(`xpath=//label[starts-with(normalize-space(),"${label}")]/following::select[1]`).first();
  await test.step('1~2. Prediction 탭 · 옵션', async () => {
    await f.getByRole('button', { name: 'Prediction', exact: true }).click();
    await expect.poll(() => api.filter((a) => /options/.test(a.url)).length, { timeout: 30_000 }).toBeGreaterThan(0);
    const o = api.find((a) => /options/.test(a.url))!;
    test.info().annotations.push({ type: 'options', description: `${o.status} ${JSON.stringify(o.json?.data ?? '').slice(0, 300)}` });
    test.info().annotations.push({ type: 'notice', description: clean(await f.locator('body').innerText()).match(/Predictions become available[^.]*\./)?.[0] ?? '' });
  });
  await test.step('3. Infra·Node·cpu 선택 → Run Prediction', async () => {
    await sel('Scope — Infra').selectOption(OBS_INFRA);
    await expect(sel('Scope — Node')).toBeEnabled({ timeout: 30_000 });
    await expect.poll(async () => (await sel('Scope — Node').locator('option').allInnerTexts()).join('|'), { timeout: 30_000 }).toContain(NODE);
    const opt = (await sel('Scope — Node').locator('option').allInnerTexts()).map((s) => s.trim()).find((s) => s === NODE || s.startsWith(`${NODE} `))!;
    await sel('Scope — Node').selectOption({ label: opt });
    await sel('Measurement').selectOption('cpu');
    await sel('Prediction Range').selectOption({ label: process.env.OBS_PRED_RANGE ?? '1 day' });
    await f.getByRole('button', { name: 'Run Prediction' }).click();
    await expect.poll(() => api.find((a) => a.method === 'POST')?.status ?? 0, { message: '예측 POST', timeout: 180_000 }).toBeGreaterThan(0);
    const p = api.find((a) => a.method === 'POST')!;
    test.info().annotations.push({ type: 'predict', description: `${p.status} POST ${p.url} ${JSON.stringify(p.json ?? '').slice(0, 600)}` });
    await page.waitForTimeout(5000);
    test.info().annotations.push({ type: 'screen', description: clean(await f.locator('body').innerText()).slice(0, 800) });
    expect(p.status, '예측 실행 2xx').toBeLessThan(300);
  });
  await test.step('4~5. 예측 값 · 차트', async () => {
    const p = api.find((a) => a.method === 'POST')!;
    let vals: any[] = p.json?.data?.values ?? [];
    if (!vals.length) { // 응답에 값이 없으면 저장된 예측(이력) 조회
      await f.getByRole('button', { name: 'Load Saved' }).click();
      await expect.poll(() => api.filter((a) => /history/.test(a.url)).length, { timeout: 60_000 }).toBeGreaterThan(0);
      const h = api.filter((a) => /history/.test(a.url)).at(-1)!;
      test.info().annotations.push({ type: 'history', description: `${h.status} GET ${h.url} ${JSON.stringify(h.json ?? '').slice(0, 600)}` });
      vals = h.json?.data?.values ?? [];
    }
    test.info().annotations.push({ type: 'values', description: `${vals.length}건 ${JSON.stringify(vals.slice(0, 3))}` });
    // 2026-10-16: 실제 응답 필드는 predicted_value 가 아니라 value({timestamp, value}) — 둘 다 허용하고 차이는 비고에 기록
    const pv = (v: any) => v.predicted_value ?? v.value;
    test.info().annotations.push({ type: 'field', description: `값 필드: ${Object.keys(vals[0] ?? {}).join(',')}` });
    expect(vals.filter((v) => pv(v) !== null && pv(v) !== undefined && !Number.isNaN(Number(pv(v)))).length, '예측 값(predicted_value/value)').toBeGreaterThan(0);
    await expect(f.locator('svg:visible, canvas:visible').first()).toBeVisible();
  });
});

IV7_S26

tests/iv7/IV7_S26.spec.ts · 28줄 · 해시 0a66fffe · 시나리오 보기

// IV7_S26: TC-OBS-026 Loki 라벨 조회 [정상, 공통]
// v0.6 TC-ID: TC-OBS-LOG-01 — 기존 spec(mcmp-e2e deploy/scenarios/shared/step-handlers/obs/log.ts runObsLog01)은 절차만 참고(사용자 결정 2026-10-14)
// 화면: Monitorings > Logs (iframe /embed/logs/{ns}) — Workload(Infra/Cluster)·Server 셀렉터
// 시작 상태: admin(mcmp) 로그인, ws01/default. Loki/fluent-bit 기동·로그 수집(대상 node Log Agent SUCCESS)
// 절차: 1.Logs 진입 2.라벨 키·값(GET /api/o11y/log/labels, /labels/{label}/values — 화면이 부르는 경우) 3.Workload 에서 대상 Infra 선택 → Server 셀렉터에 해당 Infra 의 node 반영
// 기대결과: {labels:[NS_ID, INFRA_ID, NODE_ID, level, service ...]}, 셀렉터에 라벨/값 반영
// 비고: 2026-10-14 v0.7 화면은 Workload 목록을 Tumblebug Infra 목록으로 채우고 labels API 를 부르지 않을 수 있다 → 호출 여부는 기록하고, 셀렉터 값 반영으로 판정
import { test, expect } from '../../lib/fixtures';
import { loginAsAdmin } from '../../lib/ui';
import { openObs, collectResponses, OBS_INFRA, clean } from '../../lib/iv7-obs';

test('IV7_S26 TC-OBS-026 Loki 라벨 조회', async ({ page }) => {
  const labels = collectResponses(page, /\/api\/o11y\/log\/labels/);
  const infra = collectResponses(page, new RegExp(`/tumblebug/ns/[^/]+/infra/${OBS_INFRA}$`));
  await loginAsAdmin(page);
  const f = await openObs(page, 'logs');
  await test.step(`1~3. Workload ${OBS_INFRA} → Server 셀렉터`, async () => {
    await f.locator('select').first().selectOption({ label: OBS_INFRA });
    await page.waitForTimeout(3000);
    const servers = (await f.locator('select').nth(1).locator('option').allInnerTexts()).map(clean).filter((o) => o !== 'All');
    const nodes = (infra.at(-1)?.json?.node ?? []).map((n: any) => n.id);
    test.info().annotations.push({ type: 'servers', description: `화면 ${servers.join(', ')} / Infra node ${nodes.join(', ')}` });
    test.info().annotations.push({ type: 'labels-api', description: labels.map((l) => `${l.status} ${l.url} ${JSON.stringify(l.json?.data ?? l.json).slice(0, 200)}`).join(' | ') || '(호출 없음)' });
    expect(servers.length).toBeGreaterThan(0);
    if (nodes.length) expect(servers.sort()).toEqual([...nodes].sort());
  });
});

IV7_S27

tests/iv7/IV7_S27.spec.ts · 50줄 · 해시 255e0125 · 시나리오 보기

// IV7_S27: TC-OBS-027 Loki LogQL 키워드 검색 [정상, 공통]
// v0.6 TC-ID: TC-OBS-LOG-02 — 기존 spec(mcmp-e2e deploy/scenarios/shared/step-handlers/obs/log.ts runObsLog02)은 절차만 참고(사용자 결정 2026-10-14)
// 화면: Monitorings > Logs → Workload · Server · Keyword → Search → List of Log
// 시작 상태: admin(mcmp) 로그인, ws01/default. TC-OBS-026(IV7_S26)
// 절차: 1.Workload(OBS_INFRA)·Server All 선택 후 Keyword 입력 → Search 2.LogQL {NS_ID="…",INFRA_ID="…"} |~ "(?i)keyword" 3.범위 조회(GET /api/o11y/log/query_range?query=…) 4.로그 표(Timestamp/Level/Node/Service/Message, 시간은 로컬)
// 입력값(시트): Keyword error (결과가 없으면 키워드 없이 검색해 표·시간 표시를 확인)
// 기대결과: LogSummaryDto.ResultDto(data[], stats), 로그 테이블(시간은 로컬)
// 비고: 2026-10-14 관찰 — 표 Timestamp 연도가 2001 로 표시됨(응답 시각과 비교해 판정)
import { test, expect } from '../../lib/fixtures';
import { loginAsAdmin } from '../../lib/ui';
import { openObs, collectResponses, OBS_INFRA, clean } from '../../lib/iv7-obs';

test('IV7_S27 TC-OBS-027 Loki LogQL 키워드 검색', async ({ page }) => {
  const q = collectResponses(page, /\/api\/o11y\/log\/query_range/);
  await loginAsAdmin(page);
  const f = await openObs(page, 'logs');
  await f.locator('select').first().selectOption({ label: OBS_INFRA });
  await page.waitForTimeout(2000);
  const search = async (kw: string) => {
    const n0 = q.length;
    const box = f.getByPlaceholder(/keyword/i).first().or(f.locator('input[type="text"], input[type="search"]').first());
    await box.fill(kw);
    await f.getByRole('button', { name: 'Search', exact: true }).click();
    await expect.poll(() => q.length, { timeout: 30_000 }).toBeGreaterThan(n0);
    await page.waitForTimeout(2000);
    return q.at(-1)!;
  };
  let r = await test.step('1~3. Keyword error → Search → LogQL query_range', () => search('error'));
  const query = decodeURIComponent(r.url.match(/query=([^&]+)/)?.[1] ?? '').replace(/\+/g, ' ');
  test.info().annotations.push({ type: 'logql', description: `${query} / 결과 ${(r.json?.data?.data ?? []).length}건` });
  expect(query).toContain(`INFRA_ID="${OBS_INFRA}"`);
  expect(query).toMatch(/\|~ "\(\?i\)error"/);
  if (!(r.json?.data?.data ?? []).length) {
    r = await test.step('(error 결과 없음) 키워드 없이 검색 → 표 확인', () => search(''));
  }
  await test.step('4. 로그 표 · Timestamp(로컬)', async () => {
    const rows: any[] = r.json?.data?.data ?? [];
    test.info().annotations.push({ type: 'rows', description: `${rows.length}건 예: ${JSON.stringify(rows[0] ?? {}).slice(0, 300)}` });
    expect(rows.length).toBeGreaterThan(0);
    const body = clean(await f.locator('body').innerText());
    for (const h of ['Timestamp', 'Level', 'Node', 'Service', 'Message']) expect(body).toContain(h);
    const shown = body.match(/List of Log.*?Message (\d{4})-(\d{2})-(\d{2}) (\d{2}:\d{2}:\d{2})/);
    const first = rows[0] ?? {};
    const apiTs = first.timestamp ?? first.time ?? first.ts ?? first.values?.[0]?.[0];
    const apiDate = apiTs ? new Date(/^\d{16,19}$/.test(String(apiTs)) ? Number(String(apiTs).slice(0, 13)) : apiTs) : undefined;
    test.info().annotations.push({ type: 'timestamp', description: `화면 첫 행 ${shown ? shown.slice(1, 5).join('-') : '(없음)'} / 응답 첫 행 ${apiTs} → ${apiDate?.toISOString?.() ?? '?'}` });
    expect(Number(shown?.[1]), '표시 연도 = 응답 시각의 연도').toBe(apiDate ? apiDate.getFullYear() : new Date().getFullYear());
  });
});

IV7_S28

tests/iv7/IV7_S28.spec.ts · 44줄 · 해시 ee5cfc00 · 시나리오 보기

// IV7_S28: TC-OBS-028 manager trace 조회 (Tempo) [정상, 공통]
// v0.6 TC-ID: TC-OBS-TRACE-01 — 기존 spec(mcmp-e2e deploy/scenarios/shared/step-handlers/obs/trace.ts runObsTrace01)은 절차만 참고(사용자 결정 2026-10-14)
// 화면: Monitorings > Tracing (iframe /embed/trace/{ns}) — Scope Framework / VM, Service, Keyword, Range → Search → List of Trace
// 시작 상태: admin(mcmp) 로그인, ws01/default. Tempo 기동·트레이스 수집(Framework 는 o11y manager/insight 자체 계측)
// 절차: 1.Tracing 진입 2.Scope Framework 3.Service 목록(GET /api/o11y/trace/services?scope=framework) · Service mc-observability-manager · Range 1H → Search 4.트레이스 검색(GET /api/o11y/trace/search?scope=…&service=…) 5.List of Trace 표(Start Time/Root Service/Root Name/Duration/Trace ID)
// 기대결과: TraceSummary[](traceId, rootService, rootName, startTimeMs, durationMs), List of Trace 표·페이저
// 비고: VM scope 는 대상 VM 에 Beyla/OTel(trace agent) 이 있어야 함(2026-10-14 전 node trace_agent NOT_INSTALLED) → Framework 로 판정, VM 은 기록
import { test, expect } from '../../lib/fixtures';
import { loginAsAdmin } from '../../lib/ui';
import { openObs, collectResponses, clean } from '../../lib/iv7-obs';

test('IV7_S28 TC-OBS-028 manager trace 조회 (Tempo)', async ({ page }) => {
  const svc = collectResponses(page, /\/api\/o11y\/trace\/services/);
  const search = collectResponses(page, /\/api\/o11y\/trace\/search/);
  await loginAsAdmin(page);
  const f = await openObs(page, 'trace');
  await test.step('2~3. Framework · Service 목록 · mc-observability-manager · 1H → Search', async () => {
    await f.getByRole('button', { name: 'Framework', exact: true }).click();
    await expect.poll(() => svc.length, { timeout: 30_000 }).toBeGreaterThan(0);
    test.info().annotations.push({ type: 'services', description: JSON.stringify(svc.at(-1)!.json?.data) });
    await f.locator('select').first().selectOption({ label: 'mc-observability-manager' });
    await f.locator('select').nth(1).selectOption({ label: '1H' });
    await f.getByRole('button', { name: 'Search', exact: true }).click();
    await expect.poll(() => search.length, { timeout: 30_000 }).toBeGreaterThan(0);
  });
  await test.step('4~5. 검색 응답 · List of Trace 표', async () => {
    const r = search.at(-1)!;
    const list: any[] = r.json?.data ?? [];
    test.info().annotations.push({ type: 'search', description: `${r.url.slice(0, 180)} → ${list.length}건 예: ${JSON.stringify(list[0] ?? {}).slice(0, 250)}` });
    expect(r.url).toMatch(/scope=framework/);
    expect(r.url).toMatch(/service=mc-observability-manager/);
    expect(list.length).toBeGreaterThan(0);
    expect(list.every((t) => t.rootService === 'mc-observability-manager')).toBe(true);
    const body = clean(await f.locator('body').innerText());
    for (const h of ['Start Time', 'Root Service', 'Root Name', 'Duration', 'Trace ID']) expect(body).toContain(h);
    expect(body).toMatch(/List of Trace · \d+ traces/);
  });
  await test.step('(기록) VM scope', async () => {
    await f.getByRole('button', { name: 'VM', exact: true }).click();
    await page.waitForTimeout(3000);
    test.info().annotations.push({ type: 'vm-scope', description: clean(await f.locator('body').innerText()).slice(0, 300) });
  });
});

IV7_S29

tests/iv7/IV7_S29.spec.ts · 35줄 · 해시 1067e8f8 · 시나리오 보기

// IV7_S29: TC-OBS-029 Grafana Explore 트레이싱 (트레이스 상세 Call Sequence) [정상, 공통]
// v0.6 TC-ID: TC-OBS-TRACE-02 — 기존 spec(mcmp-e2e deploy/scenarios/shared/step-handlers/obs/trace.ts runObsTrace02)은 절차만 참고(사용자 결정 2026-10-14)
// 화면: Monitorings > Tracing → List of Trace 행 클릭 → 같은 행 아래 API Call Sequence 인라인 펼침
// 시작 상태: admin(mcmp) 로그인, ws01/default. TC-OBS-028(IV7_S28) — 목록에서 선택
// 절차: 1.Framework 검색 2.목록 첫 행 클릭 3.단일 트레이스(GET /api/o11y/trace/{traceId}) → Call Sequence(Span/Service/Kind/Offset/Duration/Timeline) 4.행 다시 클릭 → 접힘
// 기대결과: TraceDetail(spans[]), 인라인 Call Sequence
// 비고: TC 이름은 'Grafana Explore' 이나 시트 절차는 o11y 화면의 인라인 Call Sequence(2026-10-14 시트 기준)
import { test, expect } from '../../lib/fixtures';
import { loginAsAdmin } from '../../lib/ui';
import { openObs, collectResponses, clean } from '../../lib/iv7-obs';

test('IV7_S29 TC-OBS-029 Grafana Explore 트레이싱 (Call Sequence)', async ({ page }) => {
  const detail = collectResponses(page, /\/api\/o11y\/trace\/[0-9a-f]{16,}$/);
  await loginAsAdmin(page);
  const f = await openObs(page, 'trace');
  await f.getByRole('button', { name: 'Search', exact: true }).click();
  const row = f.locator('tbody tr').first();
  await expect(row).toBeVisible({ timeout: 30_000 });
  const traceIdShort = (clean(await row.innerText()).match(/([0-9a-f]{16})…?$/) ?? [])[1] ?? '';
  await test.step('2~3. 첫 행 클릭 → 단일 트레이스 · Call Sequence', async () => {
    await row.click();
    await expect.poll(() => detail.length, { timeout: 30_000 }).toBeGreaterThan(0);
    const d = detail.at(-1)!.json?.data;
    test.info().annotations.push({ type: 'detail', description: `${d?.traceId} spans ${(d?.spans ?? []).length}: ${(d?.spans ?? []).slice(0, 4).map((s: any) => `${s.service}/${s.name}/${s.kind}`).join(' | ')}` });
    expect(d?.traceId?.startsWith(traceIdShort)).toBe(true);
    expect((d?.spans ?? []).length).toBeGreaterThan(0);
    await expect(f.getByText('API Call Sequence').first()).toBeVisible();
    for (const h of ['Span (call order)', 'Service', 'Kind', 'Offset', 'Duration', 'Timeline']) await expect(f.getByText(h, { exact: true }).first()).toBeVisible();
  });
  await test.step('4. 다시 클릭 → 접힘', async () => {
    await row.click();
    await expect(f.getByText('API Call Sequence')).toHaveCount(0, { timeout: 10_000 });
  });
});

IV7_S30

tests/iv7/IV7_S30.spec.ts · 32줄 · 해시 211db6a2 · 시나리오 보기

// IV7_S30: TC-OBS-030 Grafana 임베드 (Trace 임베드 화면) [정상, 공통]
// v0.6 TC-ID: TC-OBS-TRACE-03 — 기존 spec(mcmp-e2e mc-web-console/specs/obs/TC-OBS-TRACE-03-trace-iframe.spec.ts)은 절차만 참고(사용자 결정 2026-10-14)
// 화면: mc-web-console Monitorings > Tracing → iframe /embed/trace/{ns}
// 시작 상태: admin(mcmp) 로그인, ws01/default. TC-OBS-028, mc-web-console 연동 설정
// 절차: 1./embed/trace/{ns} 로드(콘솔 iframe) 2.EmbedLayout(네비/사이드바 없는 전체화면) 3.임베드 화면에서 Scope/Service/검색 · Call Sequence 동작
// 기대결과: 네비 없는 임베드 Trace 화면, iframe 내 검색/상세 정상 동작
import { test, expect } from '../../lib/fixtures';
import { loginAsAdmin } from '../../lib/ui';
import { openObs, OBS_NS, clean } from '../../lib/iv7-obs';

test('IV7_S30 TC-OBS-030 Grafana 임베드 (Trace 임베드 화면)', async ({ page }) => {
  await loginAsAdmin(page);
  const f = await openObs(page, 'trace');
  await test.step('1~2. /embed/trace 경로 · 임베드 레이아웃', async () => {
    const src = await page.locator('iframe').first().getAttribute('src');
    test.info().annotations.push({ type: 'iframe', description: `src=${src} / frame=${f.url()} / nav·sidebar ${await f.locator('nav, aside, .sidebar').count()}` });
    expect(new URL(f.url()).pathname).toBe(`/embed/trace/${OBS_NS}`);
    expect(await f.locator('nav, aside, .sidebar').count()).toBe(0);
  });
  await test.step('3. Scope 전환 · 검색 · Call Sequence', async () => {
    await f.getByRole('button', { name: 'VM', exact: true }).click();
    await page.waitForTimeout(1500);
    await f.getByRole('button', { name: 'Framework', exact: true }).click();
    await f.getByRole('button', { name: 'Search', exact: true }).click();
    const row = f.locator('tbody tr').first();
    await expect(row).toBeVisible({ timeout: 30_000 });
    await row.click();
    await expect(f.getByText('API Call Sequence').first()).toBeVisible({ timeout: 30_000 });
    test.info().annotations.push({ type: 'screen', description: clean(await f.locator('body').innerText()).match(/List of Trace.{0,250}/)?.[0] ?? '' });
  });
});

IV7_S31

tests/iv7/IV7_S31.spec.ts · 25줄 · 해시 146ac1b8 · 시나리오 보기

// IV7_S31: TC-OBS-031 Infra / NS 레벨 키워드 없이 전체 로그 조회 [정상, 공통]
// v0.6 TC-ID: TC-OBS-LOG-01 — '테스트 시나리오' 시트의 로그 TC(원래 TC-OBS-026). Framework 시트 TC-OBS-026(Loki 라벨 조회)과 정의가 달라 TC-OBS-031 로 별도 부여(사용자 결정 2026-10-15)
// 화면: Monitorings > Logs (iframe /embed/logs/{ns}) → Workload(Infra) · Server All · Keyword 비움 → Search
// 시작 상태: admin(mcmp) 로그인, ws01/default. 대상 Infra(OBS_INFRA, 기본 mc1) Log Agent 수집
// 절차: 1.Logs 진입 2.Workload mc1 · Server All · Keyword 없이 Search 3.LogQL {NS_ID,INFRA_ID} (키워드 필터 없음) 4.로그 표(Timestamp/Level/Node/Service/Message) · 여러 node 로그
// 기대결과: Infra 범위의 전체 로그가 키워드 필터 없이 표시된다
import { test, expect } from '../../lib/fixtures';
import { loginAsAdmin } from '../../lib/ui';
import { openObs, OBS_NS, OBS_INFRA } from '../../lib/iv7-obs';
import { logSearch } from '../../lib/iv7-obs-logs';

test('IV7_S31 TC-OBS-031 Infra / NS 레벨 키워드 없이 전체 로그 조회', async ({ page }) => {
  await loginAsAdmin(page);
  const f = await openObs(page, 'logs');
  const { query, rows, body } = await logSearch(page, f, OBS_INFRA, '');
  expect(query).toContain(`NS_ID="${OBS_NS}"`);
  expect(query).toContain(`INFRA_ID="${OBS_INFRA}"`);
  expect(query, '키워드 필터 없음').not.toMatch(/\|~/);
  expect(rows.length).toBeGreaterThan(0);
  for (const h of ['Timestamp', 'Level', 'Node', 'Service', 'Message']) expect(body).toContain(h);
  const nodes = [...new Set(rows.map((x) => x.labels?.NODE_ID).filter(Boolean))];
  test.info().annotations.push({ type: 'nodes', description: nodes.join(', ') });
  expect(rows.every((x) => x.labels?.INFRA_ID === OBS_INFRA), '모두 대상 Infra 로그').toBe(true);
});

IV7_S32

tests/iv7/IV7_S32.spec.ts · 23줄 · 해시 6e53aa33 · 시나리오 보기

// IV7_S32: TC-OBS-032 Infra / NS 레벨 키워드 기반 로그 조회 [정상, 공통]
// v0.6 TC-ID: TC-OBS-LOG-02 — '테스트 시나리오' 시트의 로그 TC(원래 TC-OBS-027). Framework 시트 TC-OBS-027(Loki LogQL 키워드 검색)과 정의가 달라 TC-OBS-032 로 별도 부여(사용자 결정 2026-10-15)
// 화면: Monitorings > Logs → Workload(Infra) · Server All · Keyword → Search
// 시작 상태: admin(mcmp) 로그인, ws01/default. 대상 Infra Log Agent 수집
// 절차: 1.Workload mc1 · Server All · Keyword error → Search 2.LogQL |~ "(?i)error" 3.결과 로그가 모두 키워드를 포함
// 기대결과: Infra 범위 로그 중 키워드를 포함한 로그만 표시된다
import { test, expect } from '../../lib/fixtures';
import { loginAsAdmin } from '../../lib/ui';
import { openObs, OBS_INFRA } from '../../lib/iv7-obs';
import { logSearch } from '../../lib/iv7-obs-logs';

const KW = process.env.OBS_LOG_KEYWORD ?? 'error';
test('IV7_S32 TC-OBS-032 Infra / NS 레벨 키워드 기반 로그 조회', async ({ page }) => {
  await loginAsAdmin(page);
  const f = await openObs(page, 'logs');
  const { query, rows } = await logSearch(page, f, OBS_INFRA, KW);
  expect(query).toMatch(new RegExp(`\\|~ "\\(\\?i\\)${KW}"`));
  expect(rows.length, `키워드 ${KW} 결과`).toBeGreaterThan(0);
  const miss = rows.filter((x) => !new RegExp(KW, 'i').test(JSON.stringify(x.value ?? x.line ?? x)));
  test.info().annotations.push({ type: 'keyword', description: `${rows.length}건 중 키워드 미포함 ${miss.length}건` });
  expect(miss.length, '모든 결과가 키워드 포함').toBe(0);
});

IV7_S33

tests/iv7/IV7_S33.spec.ts · 28줄 · 해시 d211f549 · 시나리오 보기

// IV7_S33: TC-OBS-033 K8s Cluster 키워드 없이 전체 로그 조회 [정상, 공통]
// v0.6 TC-ID: TC-OBS-LOG-03 — '테스트 시나리오' 시트의 로그 TC. Framework 시트와 번호가 겹쳐 TC-OBS-033 으로 별도 부여(사용자 결정 2026-10-15)
// 화면: Monitorings > Logs → Workload 에서 K8s Cluster 선택 · Keyword → Search
// 시작 상태: admin(mcmp) 로그인, ws01/{K8s Project}. K8s 클러스터 존재 · 로그 수집(K8s Log Agent, TC-OBS-005)
// 절차: 1.Logs 진입 2.Workload 에 K8s Cluster(OBS_K8S_CLUSTER) 선택 → Search 3.LogQL · 로그 표
// 기대결과: 클러스터 범위 로그가 표시된다
// 비고: K8s 클러스터가 없으면 SKIP(선행 미충족, 2026-10-15 stage 클러스터 0개)
import { test, expect } from '../../lib/fixtures';
import { loginAsAdmin } from '../../lib/ui';
import { openObs, clean } from '../../lib/iv7-obs';
import { logSearch } from '../../lib/iv7-obs-logs';

const NS = process.env.OBS_K8S_NS ?? 'p1';
const CLUSTER = process.env.OBS_K8S_CLUSTER ?? '';
test('IV7_S33 TC-OBS-033 K8s Cluster 키워드 없이 전체 로그 조회', async ({ page }) => {
  await loginAsAdmin(page);
  const f = await openObs(page, 'logs', NS);
  const opts = (await f.locator('select').first().locator('option').allInnerTexts()).map(clean).filter((o) => !/^Select/.test(o));
  test.info().annotations.push({ type: 'workloads', description: `${NS}: ${opts.join(', ') || '(없음)'}` });
  const target = CLUSTER || opts[0];
  test.skip(!target, `${NS} 에 K8s 클러스터 없음(선행 미충족)`);
  const { query, rows } = await logSearch(page, f, target, '');
  // K8s 클러스터도 Infra 와 같은 라벨 체계: {NS_ID, INFRA_ID=<cluster>, NODE_ID=<node>} (2026-10-15 AWS k1s 확인)
  expect(query).toContain(`INFRA_ID="${target}"`);
  expect(rows.length).toBeGreaterThan(0);
  expect(JSON.stringify(rows[0])).toContain(`"INFRA_ID":"${target}"`);
});

IV7_S34

tests/iv7/IV7_S34.spec.ts · 36줄 · 해시 2b6c55dc · 시나리오 보기

// IV7_S34: TC-OBS-034 K8s Cluster 키워드 기반 로그 조회 [정상, 공통]
// v0.6 TC-ID: TC-OBS-LOG-04 — '테스트 시나리오' 시트의 로그 TC. Framework 시트와 번호가 겹쳐 TC-OBS-034 으로 별도 부여(사용자 결정 2026-10-15)
// 화면: Monitorings > Logs → Workload 에서 K8s Cluster 선택 · Keyword error → Search
// 시작 상태: admin(mcmp) 로그인, ws01/{K8s Project}. K8s 클러스터 존재 · 로그 수집(K8s Log Agent, TC-OBS-005)
// 절차: 1.Logs 진입 2.Workload 에 K8s Cluster(OBS_K8S_CLUSTER) 선택 · Keyword error → Search 3.LogQL · 로그 표
// 기대결과: 클러스터 범위 로그가 키워드 필터로 표시된다
// 비고: K8s 클러스터가 없으면 SKIP(선행 미충족, 2026-10-15 stage 클러스터 0개)
import { test, expect } from '../../lib/fixtures';
import { loginAsAdmin } from '../../lib/ui';
import { openObs, clean } from '../../lib/iv7-obs';
import { logSearch } from '../../lib/iv7-obs-logs';

const NS = process.env.OBS_K8S_NS ?? 'p1';
const CLUSTER = process.env.OBS_K8S_CLUSTER ?? '';
test('IV7_S34 TC-OBS-034 K8s Cluster 키워드 기반 로그 조회', async ({ page }) => {
  await loginAsAdmin(page);
  const f = await openObs(page, 'logs', NS);
  const opts = (await f.locator('select').first().locator('option').allInnerTexts()).map(clean).filter((o) => !/^Select/.test(o));
  test.info().annotations.push({ type: 'workloads', description: `${NS}: ${opts.join(', ') || '(없음)'}` });
  const target = CLUSTER || opts[0];
  test.skip(!target, `${NS} 에 K8s 클러스터 없음(선행 미충족)`);
  // 키워드 기본 error(OBS_LOG_KEYWORD 로 변경). 클러스터 로그에 error 가 없으면(2026-10-15 GKE k4s 0건) 표본 로그에 있는 단어로 다시 검색
  let kw = process.env.OBS_LOG_KEYWORD ?? 'error';
  let { query, rows } = await logSearch(page, f, target, kw);
  if (!rows.length && !process.env.OBS_LOG_KEYWORD) {
    const sample = await logSearch(page, f, target, '');
    kw = JSON.stringify(sample.rows[0] ?? {}).match(/\b(stderr|stdout|warn\w*|info)\b/i)?.[1] ?? '';
    test.info().annotations.push({ type: 'keyword-fallback', description: `error 0건 → 표본 로그 단어 '${kw}' 로 재검색` });
    if (kw) ({ query, rows } = await logSearch(page, f, target, kw));
  }
  // K8s 클러스터도 Infra 와 같은 라벨 체계: {NS_ID, INFRA_ID=<cluster>, NODE_ID=<node>} (2026-10-15 AWS k1s 확인)
  expect(query).toContain(`INFRA_ID="${target}"`);
  expect(rows.length).toBeGreaterThan(0);
  expect(JSON.stringify(rows[0])).toContain(`"INFRA_ID":"${target}"`);
});

Clone this wiki locally