Test Specs IV8 - MZC-CSC/mc-web-console GitHub Wiki

Test Specs โ€” IV8 Data Migrations

์ƒ์„ฑ: 2026-10-15 ยท ๊ธฐ์ค€: v07 ์„ค์น˜ ํ›„ ํ†ตํ•ฉ ๊ฒ€์ฆ(IV) ยท ์›๋ณธ: ์—ฌ์ •๋ณ„ ๊ณ„ํš์„œ(Excel) โ† Test Scenarios ยท IV ํ—ˆ๋ธŒ ยท ๊ธฐ๋Šฅ TC

Playwright(TypeScript) spec ์†Œ์Šค์ž…๋‹ˆ๋‹ค. ์‹œ๋‚˜๋ฆฌ์˜ค๋งˆ๋‹ค ## <์‹œ๋‚˜๋ฆฌ์˜ค๋ฒˆํ˜ธ> ์„น์…˜์ด ์žˆ๊ณ  ์•ต์ปค๋Š” #<์‹œ๋‚˜๋ฆฌ์˜ค๋ฒˆํ˜ธ ์†Œ๋ฌธ์ž> ์ž…๋‹ˆ๋‹ค. ๊ณต์šฉ ํ—ฌํผ๋Š” Test-Specs-Common. ๋น„๋ฐ€๋ฒˆํ˜ธ ๋“ฑ์€ ๋งˆ์Šคํ‚นํ–ˆ์Šต๋‹ˆ๋‹ค.

์‹œ๋‚˜๋ฆฌ์˜ค ํŒŒ์ผ ์ค„ ํ•ด์‹œ
IV8_S01 tests/iv8/IV8_S01.spec.ts 18 4ea3ecd6
IV8_S02 tests/iv8/IV8_S02.spec.ts 15 8f0d208d
IV8_S03 tests/iv8/IV8_S03.spec.ts 29 68f09f08
IV8_S04 tests/iv8/IV8_S04.spec.ts 70 0053808b
IV8_S05 tests/iv8/IV8_S05.spec.ts 77 c0646226
IV8_S06 tests/iv8/IV8_S06.spec.ts 67 7e508888
IV8_S07 tests/iv8/IV8_S07.spec.ts 81 36d69f1e
IV8_S08 tests/iv8/IV8_S08.spec.ts 51 4f16b484
IV8_S09 tests/iv8/IV8_S09.spec.ts 52 c1b0949f
IV8_S10 tests/iv8/IV8_S10.spec.ts 48 e1d80c9b
IV8_S11 tests/iv8/IV8_S11.spec.ts 37 7d592bd5
IV8_S12 tests/iv8/IV8_S12.spec.ts 98 43e4bb43
IV8_S13 tests/iv8/IV8_S13.spec.ts 40 f76297ec
IV8_S14 tests/iv8/IV8_S14.spec.ts 59 e3b90577
IV8_S15 tests/iv8/IV8_S15.spec.ts 34 aebfcf7f
IV8_S16 tests/iv8/IV8_S16.spec.ts 40 a1b5437c
IV8_S17 tests/iv8/IV8_S17.spec.ts 126 b125a4ea
IV8_S18 tests/iv8/IV8_S18.spec.ts 42 785837a0
IV8_S19 tests/iv8/IV8_S19.spec.ts 61 71e29ab0
IV8_S20 tests/iv8/IV8_S20.spec.ts 65 83d022b3

IV8_S01

tests/iv8/IV8_S01.spec.ts ยท 18์ค„ ยท ํ•ด์‹œ 4ea3ecd6 ยท ์‹œ๋‚˜๋ฆฌ์˜ค ๋ณด๊ธฐ

import { test, expect } from '../../lib/fixtures';
import { loginAsAdmin } from '../../lib/ui';

// IV8_S01 : Data Migrations ์ง„์ž… (iframe ํ•ธ๋“œ์…ฐ์ดํฌ). ๋งํฌ: /webconsole/_view/mcdatamigration
test('IV8_S01 Data Migrations ์ง„์ž… (iframe ํ•ธ๋“œ์…ฐ์ดํฌ)', async ({ page }) => {
  const t0 = Date.now();
  await loginAsAdmin(page);
  test.info().annotations.push({ type: 'login-ms', description: String(Date.now() - t0) });
  await page.getByText('Data Migrations', { exact: true }).first().click();
  await page.getByRole('link', { name: 'Migration', exact: true }).first().click();
  // stage ๋™์‹œ ๋ถ€ํ•˜ ์‹œ ๋ฉ”๋‰ด ํด๋ฆญ ํ›„ 15์ดˆ ๋„˜๊ฒŒ ๋นˆ ํ™”๋ฉด์ด ์œ ์ง€๋œ ์  ์žˆ์Œ(2026-10-02) โ†’ URL ์ „ํ™˜ ํ™•์ธ ํ›„ iframe ์„ ๋„‰๋„‰ํžˆ ๋Œ€๊ธฐ
  await page.waitForURL(/mcdatamigration/, { timeout: 60_000 });
  await expect(page.locator('iframe').first()).toBeVisible({ timeout: 60_000 });
  const frame = page.frameLocator('iframe').first();
  await expect(frame.locator('#root, body').first()).toBeVisible({ timeout: 60_000 });
  test.info().annotations.push({ type: 'iframe-src', description: (await page.locator('iframe').first().getAttribute('src')) ?? '' });
});

IV8_S02

tests/iv8/IV8_S02.spec.ts ยท 15์ค„ ยท ํ•ด์‹œ 8f0d208d ยท ์‹œ๋‚˜๋ฆฌ์˜ค ๋ณด๊ธฐ

import { test, expect } from '../../lib/fixtures';
import { logout } from '../../lib/ui';

// IV8_S02 : ๋ฏธ๋กœ๊ทธ์ธ ์ƒํƒœ์—์„œ Data Migrations ์ง์ ‘ ์ ‘๊ทผ (์˜ˆ์ƒ ์‹คํŒจ)
test('IV8_S02 ๋ฏธ๋กœ๊ทธ์ธ ์ƒํƒœ์—์„œ Data Migrations ์ง์ ‘ ์ ‘๊ทผ (์˜ˆ์ƒ์‹คํŒจ)', async ({ page }) => {
  await logout(page);
  const res = await page.goto('/webconsole/_view/mcdatamigration');
  test.info().annotations.push({ type: 'landed', description: `${res?.status()} ${page.url()}` });
  // ๊ธฐ๋Œ€: ๋กœ๊ทธ์ธ/Unauthorized ํ™”๋ฉด์œผ๋กœ ์ด๋™ํ•˜๊ณ  ์ฝ˜ํ…์ธ (iframe) ๋ฏธ๋…ธ์ถœ.
  // 2026-10-01 Playwright ์‹คํ–‰: 1ํšŒ์ฐจ /auth/unauthorized#cookieError, ์ดํ›„ 3ํšŒ '502 Bad Gateway'(๊ฐ™์€ URL) โ†’ ํ˜„์žฌ ์ด ๋‹จ์–ธ์€ ์‹คํŒจํ•œ๋‹ค(์ œํ’ˆ ํ™•์ธ ํ•„์š”).
  await expect(page).toHaveURL(/\/auth\/(login|unauthorized)/);
  await expect(page.getByRole('heading', { name: 'Unauthorized' }).or(page.getByPlaceholder('Your ID'))).toBeVisible();
  await expect(page.locator('iframe')).toHaveCount(0);
});

IV8_S03

tests/iv8/IV8_S03.spec.ts ยท 29์ค„ ยท ํ•ด์‹œ 68f09f08 ยท ์‹œ๋‚˜๋ฆฌ์˜ค ๋ณด๊ธฐ

import { test, expect } from '../../lib/fixtures';
import { activeCsps, tid } from '../../lib/data';
import { loginAsAdmin } from '../../lib/ui';
import { apiRecorder, openDm, selectCred, ensureBucket, bucketName, dmBlocker, dmRegion } from '../../lib/iv8-os';

// IV8_S03 : Object Storage: Bucket ์ƒ์„ฑ โ†’ Bucket List ํ‘œ์‹œ [์ •์ƒ, CSP๋ณ„]
// Data Migrations > Generate > Object Storage โ†’ Target credential ์„ ํƒ โ†’ + Bucket(mcmp-e2e-v07-os{n}) โ†’ Bucket ListยทTarget Bucket ๋“œ๋กญ๋‹ค์šด ํ™•์ธ
// ์žฌ์‹คํ–‰ ๊ฐ€๋Šฅ: ์ด๋ฏธ ์žˆ์œผ๋ฉด ์ƒ์„ฑ์€ ๊ฑด๋„ˆ๋›ฐ๊ณ  ๋ชฉ๋ก ํ‘œ์‹œ๋งŒ ํ™•์ธํ•œ๋‹ค.
for (const c of activeCsps(['AWS', 'Azure', 'Alibaba', 'GCP', 'Tencent', 'NHN Cloud', 'NCP', 'IBM', 'KT'])) {
  test(`${tid('IV8_S03', c)} Object Storage: Bucket ์ƒ์„ฑ โ†’ Bucket List ํ‘œ์‹œ`, async ({ page }) => {
    const why = dmBlocker(c);
    test.skip(!!why, why);
    const api = apiRecorder(page);
    await loginAsAdmin(page);
    const f = await openDm(page, 'Generate');
    await selectCred(page, f, c);
    api();
    const name = bucketName(c);
    const row = await ensureBucket(page, f, name, api);
    expect(row, 'Bucket List ํ–‰').toContain(name);
    // ๋ชฉ๋ก์— ์˜ฌ๋ผ์™€๋„ Status ๊ฐ€ Failed ๋ฉด CSP ์ƒ์„ฑ ์‹คํŒจ(2026-10-12 KT: MCMP ์— Failed ๋กœ ๋“ฑ๋ก, cspResourceId ๋นˆ ๊ฐ’)
    expect(row, 'Bucket Status').not.toMatch(/\bFailed\b/i);
    // Region ์ง€์ • CSP ๋Š” ์ง€์ • ๋ฆฌ์ „์— ์ƒ์„ฑ๋๋Š”์ง€ ํ™•์ธ(2026-10-12: ์ž๋™ ์„ ํƒ af-south-1 ๋กœ ์ƒ์„ฑ๋œ ์ด๋ ฅ)
    if (dmRegion(c)) expect(row, 'Bucket Region').toContain(dmRegion(c));
    // Target Bucket ๋“œ๋กญ๋‹ค์šด์—๋„ ํ‘œ์‹œ
    await expect.poll(() => f.locator('select#newBucket option').allTextContents().then((o) => o.join(',')), { timeout: 30_000 }).toContain(name);
  });
}

IV8_S04

tests/iv8/IV8_S04.spec.ts ยท 70์ค„ ยท ํ•ด์‹œ 0053808b ยท ์‹œ๋‚˜๋ฆฌ์˜ค ๋ณด๊ธฐ

import { test, expect } from '../../lib/fixtures';
import { activeCsps, tid } from '../../lib/data';
import { loginAsAdmin } from '../../lib/ui';
import { apiRecorder, openDm, selectCred, selectBucket, bucketName, bucketRowText, readObjects, logHistory, dmBlocker, dmRegion } from '../../lib/iv8-os';

// IV8_S04 : Object Storage: ์ƒ˜ํ”Œ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ (Generate) [์ •์ƒ, CSP๋ณ„]
// Generate > Object Storage โ†’ Target credentialยทRegion โ†’ Target Bucket(mcmp-e2e-v07-os{n}, IV8_S03) โ†’ TXT ๋งŒ 1GB โ†’ Submit
// โ†’ ๊ฒฐ๊ณผ(Result)ยทAPI ์‘๋‹ต, bucket Objects, Log Management(Object Storage / Generate) ํ™•์ธ
const TYPES = ['SQL', 'CSV', 'TXT', 'PNG', 'GIF', 'ZIP', 'JSON', 'XML', 'ServerJSON', 'ServerSQL'];
const SIZE_GB = '1'; // ๊ธฐ๋ณธ 10GB โ†’ 1GB(์‹œ๊ฐ„ยท์ „์†ก ๋น„์šฉ)

for (const c of activeCsps(['AWS', 'Azure', 'Alibaba', 'GCP', 'Tencent', 'NHN Cloud', 'NCP', 'IBM', 'KT'])) {
  test(`${tid('IV8_S04', c)} Object Storage: ์ƒ˜ํ”Œ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ (Generate)`, async ({ page }) => {
    test.setTimeout(45 * 60_000);
    const why = dmBlocker(c);
    test.skip(!!why, why);
    const api = apiRecorder(page);
    await loginAsAdmin(page);
    let f = await openDm(page, 'Generate');
    await selectCred(page, f, c);
    const name = bucketName(c);
    const row = await bucketRowText(f, name);
    test.skip(!row || /Failed/i.test(row), `์„ ํ–‰ IV8_S03_${c.code} bucket ${name} ์—†์Œ/์‹คํŒจ: ${row || '(์—†์Œ)'}`);
    if (dmRegion(c)) expect(row, 'bucket Region').toContain(dmRegion(c));
    await selectBucket(f, name);

    // TXT ๋งŒ ์ฒดํฌ, ํฌ๊ธฐ 1GB
    for (const t of TYPES) {
      const cb = f.locator(`#check${t}`);
      if (await cb.count()) await cb.setChecked(t === 'TXT', { force: true });
    }
    await f.locator('#sizeTXT').fill(SIZE_GB);
    const checked = await f.evaluate((ts) => ts.filter((t) => (document.getElementById(`check${t}`) as HTMLInputElement | null)?.checked), TYPES);
    test.info().annotations.push({ type: 'generate-input', description: `bucket=${name}, checked=${checked.join(',')}, TXT=${SIZE_GB}GB` });
    expect(checked).toEqual(['TXT']);

    api();
    let genResp = '';
    page.on('response', async (r) => {
      if (/:3300\/generate\//i.test(r.url()) && r.request().method() !== 'GET') genResp = `${r.status()} ${r.url().replace(/^https?:\/\/[^/]+/, '')}`;
    });
    const t0 = Date.now();
    await f.locator('#submitBtn').click();
    // ์™„๋ฃŒ ํŒ๋‹จ: generate API(POST) ์‘๋‹ต ๋˜๋Š” Result ์˜์—ญ์˜ ์™„๋ฃŒ/์‹คํŒจ ๋ฌธ๊ตฌ. 1GB ์ƒ์„ฑยท์—…๋กœ๋“œ๋Š” ์ˆ˜ ๋ถ„ ๊ฑธ๋ฆด ์ˆ˜ ์žˆ์Œ
    const result = f.locator('#resultText');
    await expect.poll(async () => {
      const v = await result.inputValue().catch(() => '');
      return genResp || /(success|fail|error|complete|finished|done)/i.test(v) ? 'done' : '';
    }, { timeout: 40 * 60_000, intervals: [5000, 10_000, 30_000] }).toBe('done');
    await page.waitForTimeout(3000); // ์‘๋‹ต ํ›„ Result ๊ฐฑ์‹ 
    test.info().annotations.push({ type: 'generate-response', description: genResp || '(POST ์‘๋‹ต ๋ฏธํ™•์ธ, Result ๋ฌธ๊ตฌ๋กœ ํŒ๋‹จ)' });
    const text = (await result.inputValue()).trim();
    test.info().annotations.push({ type: 'generate-sec', description: String(Math.round((Date.now() - t0) / 1000)) });
    test.info().annotations.push({ type: 'generate-result', description: text.slice(-1200) });
    test.info().annotations.push({ type: 'generate-api', description: api().slice(0, 1500) || '(API ๊ธฐ๋ก ์—†์Œ)' });
    expect(text, 'Generate ๊ฒฐ๊ณผ').not.toMatch(/\b(fail(ed)?|error)\b/i);

    // bucket ๊ฐ์ฒด ํ™•์ธ
    f = await openDm(page, 'Generate');
    await selectCred(page, f, c);
    const objs = await readObjects(page, f, name);
    if (objs) expect(objs.length, 'bucket ๊ฐ์ฒด ์ˆ˜').toBeGreaterThan(0);

    // Log Management ์ด๋ ฅ
    const logs = await logHistory(page, 'Object Storage', 'Generate');
    // ์ตœ์‹  ์ด๋ ฅ(์ฒซ ํ–‰)์ด ์ด bucket ์˜ generate success ์—ฌ์•ผ ํ•œ๋‹ค
    expect(logs[0] ?? '', 'Log Management ์ตœ์‹  Generate ์ด๋ ฅ').toMatch(new RegExp(`generate .*${dmRegion(c) ? dmRegion(c) + ' ' : ''}${name} .*success`, 'i'));
  });
}

IV8_S05

tests/iv8/IV8_S05.spec.ts ยท 77์ค„ ยท ํ•ด์‹œ c0646226 ยท ์‹œ๋‚˜๋ฆฌ์˜ค ๋ณด๊ธฐ

import { test, expect } from '../../lib/fixtures';
import { activeCsps, tid, CSPS } from '../../lib/data';
import { loginAsAdmin } from '../../lib/ui';
import {
  apiRecorder, openDm, selectCred, selectBucket, ensureBucket, bucketName, bucketRowText, readObjects, logHistory,
  nextStep, diagnose, dmBlocker, dmRegion,
} from '../../lib/iv8-os';

// IV8_S05 : Object Storage ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ (Migration) [์ •์ƒ, CSP๋ณ„]
// Migration > Object Storage 4๋‹จ๊ณ„: 1 Source(ํ•ด๋‹น CSP, mcmp-e2e-v07-os{n}) โ†’ 2 Diagnose โ†’ 3 Target(AWS os1m, AWS ํ–‰์€ GCP os4m. ์—†์œผ๋ฉด + Bucket) โ†’ 4 Diagnose โ†’ Submit
// โ†’ ResultยทAPI, ๋Œ€์ƒ bucket ๊ฐ์ฒด ์ˆ˜ = ์›๋ณธ Diagnose Object Count, Log Management(Migrate) ํ™•์ธ
for (const c of activeCsps(['AWS', 'Azure', 'Alibaba', 'GCP', 'Tencent', 'NHN Cloud', 'NCP', 'IBM', 'KT'])) {
  test(`${tid('IV8_S05', c)} Object Storage ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ (Migration)`, async ({ page }) => {
    test.setTimeout(60 * 60_000);
    const why = dmBlocker(c);
    test.skip(!!why, why);
    const tc = CSPS.find((x) => x.code === (c.code === 'AWS' ? 'GCP' : 'AWS'))!; // ๋Œ€์ƒ CSP
    const src = bucketName(c);
    const dst = bucketName(tc, 'm');
    test.info().annotations.push({ type: 'route', description: `${c.code} ${src} โ†’ ${tc.code} ${dst}` });
    const api = apiRecorder(page);
    await loginAsAdmin(page);
    let f = await openDm(page, 'Migration');

    // 1. Source
    await selectCred(page, f, c, 'sourceCredentialSelect');
    const srow = await bucketRowText(f, src);
    test.skip(!srow || /Failed/i.test(srow), `์„ ํ–‰ IV8_S03/S04_${c.code} ์›๋ณธ bucket ${src} ์—†์Œ/์‹คํŒจ: ${srow || '(์—†์Œ)'}`);
    await selectBucket(f, src, 'sourceBucket');
    await nextStep(page, f, '#collapseOne');
    // 2. Diagnose Source
    const sd = await diagnose(page, f, 'src');
    expect(Number(sd.count), '์›๋ณธ ๊ฐ์ฒด ์ˆ˜(IV8_S04 ์„ ํ–‰)').toBeGreaterThan(0);
    await nextStep(page, f, '#collapseTwo');
    // 3. Target (์—†์œผ๋ฉด + Bucket ์œผ๋กœ ์ƒ์„ฑ)
    await selectCred(page, f, tc, 'targetCredentialSelect');
    api();
    const trow = await ensureBucket(page, f, dst, api, '#tgt-bkt-createBtn');
    expect(trow, '๋Œ€์ƒ bucket').not.toMatch(/\bFailed\b/i);
    if (dmRegion(tc)) expect(trow, '๋Œ€์ƒ bucket Region').toContain(dmRegion(tc));
    await selectBucket(f, dst, 'targetBucket');
    await nextStep(page, f, '#collapseThree');
    // 4. Diagnose Target (์‹คํ–‰ ์ „)
    const td = await diagnose(page, f, 'tgt');

    // Submit
    api();
    let migResp = '';
    page.on('response', (r) => { if (/:3300\/migrat/i.test(r.url()) && r.request().method() !== 'GET') migResp = `${r.status()} ${r.url().replace(/^https?:\/\/[^/]+/, '')}`; });
    const t0 = Date.now();
    await f.locator('#submitBtn').click();
    const result = f.locator('#resultText');
    await expect.poll(async () => {
      const v = await result.inputValue().catch(() => '');
      return migResp || /(success|fail|error|complete|finished)/i.test(v) ? 'done' : '';
    }, { timeout: 55 * 60_000, intervals: [5000, 10_000, 30_000] }).toBe('done');
    await page.waitForTimeout(3000);
    const text = (await result.inputValue()).trim();
    test.info().annotations.push({ type: 'migrate-sec', description: String(Math.round((Date.now() - t0) / 1000)) });
    test.info().annotations.push({ type: 'migrate-response', description: migResp || '(POST ์‘๋‹ต ๋ฏธํ™•์ธ)' });
    test.info().annotations.push({ type: 'migrate-result', description: text.slice(-1500) });
    test.info().annotations.push({ type: 'migrate-api', description: api().slice(0, 1500) || '(API ๊ธฐ๋ก ์—†์Œ)' });
    expect(text, 'Migration ๊ฒฐ๊ณผ').not.toMatch(/\b(fail(ed)?|error)\b/i);

    // ๋Œ€์ƒ bucket ๊ฐ์ฒด ํ™•์ธ (Generate ํ™”๋ฉด Browse objects)
    f = await openDm(page, 'Generate');
    await selectCred(page, f, tc);
    const objs = await readObjects(page, f, dst);
    if (objs) expect(objs.length, '๋Œ€์ƒ bucket ๊ฐ์ฒด(ํด๋”) ์ˆ˜').toBeGreaterThan(0);
    test.info().annotations.push({ type: 'count-compare', description: `์›๋ณธ Diagnose count=${sd.count}, ๋Œ€์ƒ Diagnose(์‹คํ–‰ ์ „) count=${td.count}` });

    // Log Management ์ตœ์‹  Migrate ์ด๋ ฅ
    const logs = await logHistory(page, 'Object Storage', 'Migrate');
    expect(logs[0] ?? '', 'Log Management ์ตœ์‹  Migrate ์ด๋ ฅ').toMatch(/migrat.*success/i);
  });
}

IV8_S06

tests/iv8/IV8_S06.spec.ts ยท 67์ค„ ยท ํ•ด์‹œ 7e508888 ยท ์‹œ๋‚˜๋ฆฌ์˜ค ๋ณด๊ธฐ

import { test, expect } from '../../lib/fixtures';
import { activeCsps, tid } from '../../lib/data';
import { loginAsAdmin } from '../../lib/ui';
import {
  apiRecorder, openDm, selectCred, selectBucket, bucketName, bucketRowText, backupName, backupRowText, logHistory, dmBlocker, dmRegion,
} from '../../lib/iv8-os';

// IV8_S06 : Object Storage ๋ฐฑ์—… (Back Up) [์ •์ƒ, CSP๋ณ„]
// Back Up > Object Storage โ†’ Source credentialยทRegionยทBucket(mcmp-e2e-v07-os{n}) โ†’ Backup Name(mcmp-e2e-v07-bk{n}) โ†’ Submit
// โ†’ ResultยทAPI, Backup List(Back UpยทRestore ํ™”๋ฉด) ํ‘œ์‹œ, Log Management(Backup) ํ™•์ธ
// ์žฌ์‹คํ–‰ ๊ฐ€๋Šฅ: ๊ฐ™์€ ์ด๋ฆ„์˜ ๋ฐฑ์—…์ด ์ด๋ฏธ ์žˆ์œผ๋ฉด ์ƒ์„ฑ์€ ๊ฑด๋„ˆ๋›ฐ๊ณ  ๋ชฉ๋ก ํ‘œ์‹œ๋งŒ ํ™•์ธํ•œ๋‹ค.
for (const c of activeCsps(['AWS', 'Azure', 'Alibaba', 'GCP', 'Tencent', 'NHN Cloud', 'NCP', 'IBM', 'KT'])) {
  test(`${tid('IV8_S06', c)} Object Storage ๋ฐฑ์—… (Back Up)`, async ({ page }) => {
    test.setTimeout(60 * 60_000);
    const why = dmBlocker(c);
    test.skip(!!why, why);
    const src = bucketName(c);
    const bk = backupName(c);
    const api = apiRecorder(page);
    await loginAsAdmin(page);
    let f = await openDm(page, 'Back Up');
    await selectCred(page, f, c, 'sourceCredentialSelect');
    const srow = await bucketRowText(f, src);
    test.skip(!srow || /Failed/i.test(srow), `์„ ํ–‰ IV8_S03/S04_${c.code} ์›๋ณธ bucket ${src} ์—†์Œ/์‹คํŒจ: ${srow || '(์—†์Œ)'}`);
    if (dmRegion(c)) expect(srow, '์›๋ณธ bucket Region').toContain(dmRegion(c));

    const before = await backupRowText(f, bk);
    if (before) {
      test.info().annotations.push({ type: 'backup-exists', description: `${bk} ์ด๋ฏธ ์žˆ์Œ โ†’ ์ƒ์„ฑ ๊ฑด๋„ˆ๋œ€: ${before}` });
    } else {
      await selectBucket(f, src, 'sourceBucket');
      await f.locator('#backup-name').fill(bk);
      api();
      let bkResp = '';
      page.on('response', (r) => { if (/:3300\/backup/i.test(r.url()) && r.request().method() === 'POST') bkResp = `${r.status()} ${r.url().replace(/^https?:\/\/[^/]+/, '')}`; });
      const t0 = Date.now();
      await f.locator('#submitBtn').click();
      const result = f.locator('#resultText');
      await expect.poll(async () => {
        const v = await result.inputValue().catch(() => '');
        return bkResp || /(success|fail|error|complete|finished)/i.test(v) ? 'done' : '';
      }, { timeout: 55 * 60_000, intervals: [5000, 10_000, 30_000] }).toBe('done');
      await page.waitForTimeout(3000);
      const text = (await result.inputValue()).trim();
      test.info().annotations.push({ type: 'backup-sec', description: String(Math.round((Date.now() - t0) / 1000)) });
      test.info().annotations.push({ type: 'backup-response', description: bkResp || '(POST ์‘๋‹ต ๋ฏธํ™•์ธ)' });
      test.info().annotations.push({ type: 'backup-result', description: text.slice(-1500) });
      test.info().annotations.push({ type: 'backup-api', description: api().slice(0, 1500) || '(API ๊ธฐ๋ก ์—†์Œ)' });
      expect(text, 'Back Up ๊ฒฐ๊ณผ').not.toMatch(/\b(fail(ed)?|error)\b/i);
    }

    // Backup List: Back Up ํ™”๋ฉด(์ƒˆ๋กœ ์—ด๊ธฐ) / Restore ํ™”๋ฉด
    f = await openDm(page, 'Back Up');
    await expect.poll(() => backupRowText(f, bk), { timeout: 60_000 }).not.toBe('');
    const r1 = await backupRowText(f, bk);
    test.info().annotations.push({ type: 'backup-list(backup)', description: r1 });
    expect(r1, '๋ฐฑ์—… ์ƒํƒœ').not.toMatch(/\bFailed\b/i);
    f = await openDm(page, 'Restore');
    await expect.poll(() => backupRowText(f, bk), { timeout: 60_000 }).not.toBe('');
    test.info().annotations.push({ type: 'backup-list(restore)', description: await backupRowText(f, bk) });

    // Log Management ์ตœ์‹  Backup ์ด๋ ฅ
    const logs = await logHistory(page, 'Object Storage', 'Backup');
    expect(logs[0] ?? '', 'Log Management ์ตœ์‹  Backup ์ด๋ ฅ').toMatch(/backup.*success/i);
  });
}

IV8_S07

tests/iv8/IV8_S07.spec.ts ยท 81์ค„ ยท ํ•ด์‹œ 36d69f1e ยท ์‹œ๋‚˜๋ฆฌ์˜ค ๋ณด๊ธฐ

import { test, expect } from '../../lib/fixtures';
import { activeCsps, tid } from '../../lib/data';
import { loginAsAdmin } from '../../lib/ui';
import {
  apiRecorder, openDm, selectCred, selectBucket, ensureBucket, bucketName, backupName, backupRowText, readObjects, listFolder, logHistory, dmBlocker, dmRegion,
} from '../../lib/iv8-os';

// IV8_S07 : Object Storage ๋ณต๊ตฌ (Restore) [์ •์ƒ, CSP๋ณ„]
// Restore > Object Storage โ†’ Source Backup List ์—์„œ mcmp-e2e-v07-bk{n}(IV8_S06) ํ–‰ ์„ ํƒ โ†’ Target credentialยทRegion โ†’ + Bucket mcmp-e2e-v07-os{n}r โ†’ Submit
// โ†’ ResultยทAPI, ๋Œ€์ƒ bucket ๊ฐ์ฒด(์›๋ณธ os{n} ๊ณผ ๊ฐ™์€ datamold ํด๋”), Log Management(Restore) ํ™•์ธ
for (const c of activeCsps(['AWS', 'Azure', 'Alibaba', 'GCP', 'Tencent', 'NHN Cloud', 'NCP', 'IBM', 'KT'])) {
  test(`${tid('IV8_S07', c)} Object Storage ๋ณต๊ตฌ (Restore)`, async ({ page }) => {
    test.setTimeout(60 * 60_000);
    const why = dmBlocker(c);
    test.skip(!!why, why);
    const bk = backupName(c);
    const dst = bucketName(c, 'r');
    const api = apiRecorder(page);
    await loginAsAdmin(page);
    let f = await openDm(page, 'Restore');

    // Source: Backup List ํ–‰ ์„ ํƒ
    await expect.poll(() => backupRowText(f, bk), { timeout: 60_000 }).not.toBe('');
    const brow = await backupRowText(f, bk);
    test.skip(!/completed/i.test(brow), `์„ ํ–‰ IV8_S06_${c.code} ๋ฐฑ์—… ${bk} ์—†์Œ/๋ฏธ์™„๋ฃŒ: ${brow}`);
    const row = f.locator(`tr.backup-row[data-name="${bk}"]`).first();
    await row.locator('td', { hasText: bk }).first().click();
    await page.waitForTimeout(1500);
    const sel = await row.evaluate((r) => ({ cls: r.className, checked: (r.querySelector('input[type=checkbox]') as HTMLInputElement | null)?.checked ?? null, id: (r as HTMLElement).dataset.id ?? '' }));
    const label = await f.locator('#sourcePointLabel').inputValue().catch(() => '');
    test.info().annotations.push({ type: 'backup-select', description: `${brow} | row class=${sel.cls} checked=${sel.checked} | Source label=${label}` });

    // Target: credentialยทRegion โ†’ + Bucket(res) โ†’ Target Bucket
    await selectCred(page, f, c, 'targetCredentialSelect');
    api();
    const trow = await ensureBucket(page, f, dst, api, '#res-bkt-createBtn');
    expect(trow, '๋Œ€์ƒ bucket').not.toMatch(/\bFailed\b/i);
    if (dmRegion(c)) expect(trow, '๋Œ€์ƒ bucket Region').toContain(dmRegion(c));
    await selectBucket(f, dst, 'targetBucket');

    // Submit
    api();
    let rsResp = '';
    page.on('response', (r) => { if (/:3300\/restore/i.test(r.url()) && r.request().method() === 'POST') rsResp = `${r.status()} ${r.url().replace(/^https?:\/\/[^/]+/, '')}`; });
    const t0 = Date.now();
    await f.locator('#submitBtn').click();
    const result = f.locator('#resultText');
    await expect.poll(async () => {
      const v = await result.inputValue().catch(() => '');
      return rsResp || /(success|fail|error|complete|finished)/i.test(v) ? 'done' : '';
    }, { timeout: 55 * 60_000, intervals: [5000, 10_000, 30_000] }).toBe('done');
    await page.waitForTimeout(3000);
    const text = (await result.inputValue()).trim();
    test.info().annotations.push({ type: 'restore-sec', description: String(Math.round((Date.now() - t0) / 1000)) });
    test.info().annotations.push({ type: 'restore-response', description: rsResp || '(POST ์‘๋‹ต ๋ฏธํ™•์ธ)' });
    test.info().annotations.push({ type: 'restore-result', description: text.slice(-1500) });
    test.info().annotations.push({ type: 'restore-api', description: api().slice(0, 1500) || '(API ๊ธฐ๋ก ์—†์Œ)' });
    expect(text, 'Restore ๊ฒฐ๊ณผ').not.toMatch(/\b(fail(ed)?|error)\b/i);

    // ๋Œ€์ƒ bucket ๊ฐ์ฒด: ์›๋ณธ(os{n})๊ณผ ๊ฐ™์€ datamold ํด๋”๊ฐ€ ์žˆ์–ด์•ผ ํ•œ๋‹ค.
    // 2026-10-12 AWS ํ™•์ธ: ๋ณต๊ตฌ ๋ฐ์ดํ„ฐ๋Š” ์›๋ž˜ ๊ฒฝ๋กœ๊ฐ€ ์•„๋‹ˆ๋ผ '{๋ฐฑ์—… id}/' ์ ‘๋‘ ํด๋” ์•„๋ž˜์— ์›๋ณธ ๊ตฌ์กฐ ๊ทธ๋Œ€๋กœ ๋“ค์–ด๊ฐ„๋‹ค โ†’ ์ ‘๋‘ ํด๋”๊ฐ€ ์žˆ์œผ๋ฉด ๊ทธ ์•ˆ์„ ๋น„๊ต
    f = await openDm(page, 'Generate');
    await selectCred(page, f, c);
    const srcObjs = await readObjects(page, f, bucketName(c));
    let dstObjs = await readObjects(page, f, dst);
    const top = (dstObjs ?? []).map((t) => t.split(' ')[0]);
    if (sel.id && top.some((k) => k.startsWith(sel.id))) {
      test.info().annotations.push({ type: 'restore-prefix', description: `๋ณต๊ตฌ ์œ„์น˜: ${dst}/${sel.id}/ (์›๋ž˜ ๊ฒฝ๋กœ ์•„๋‹˜)` });
      dstObjs = await listFolder(page, f, dst, sel.id);
    }
    const folders = (o: string[] | null) => (o ?? []).filter((t) => /datamold/.test(t)).sort();
    test.info().annotations.push({ type: 'compare', description: `์›๋ณธ ${folders(srcObjs).join(', ')} / ๋ณต๊ตฌ ${folders(dstObjs).join(', ')}` });
    expect(folders(dstObjs), '๋ณต๊ตฌ๋œ datamold ํด๋”(์ด๋ฆ„ยทํ•ญ๋ชฉ ์ˆ˜) = ์›๋ณธ').toEqual(folders(srcObjs));
    expect(folders(dstObjs).length, '๋ณต๊ตฌ๋œ ํด๋” ์ˆ˜').toBeGreaterThan(0);

    // Log Management ์ตœ์‹  Restore ์ด๋ ฅ
    const logs = await logHistory(page, 'Object Storage', 'Restore');
    expect(logs[0] ?? '', 'Log Management ์ตœ์‹  Restore ์ด๋ ฅ').toMatch(/restore.*success/i);
  });
}

IV8_S08

tests/iv8/IV8_S08.spec.ts ยท 51์ค„ ยท ํ•ด์‹œ 4f16b484 ยท ์‹œ๋‚˜๋ฆฌ์˜ค ๋ณด๊ธฐ

import { test, expect } from '../../lib/fixtures';
import { activeCsps, tid, CSPS } from '../../lib/data';
import { loginAsAdmin } from '../../lib/ui';
import { apiRecorder, openDm, selectCred, deleteBucket, deleteBackup, bucketName, backupName, bucketRowText, backupRowText, dmBlocker } from '../../lib/iv8-os';

// IV8_S08 : Object Storage: ํ…Œ์ŠคํŠธ ์ž์› ์ •๋ฆฌ โ€” Bucketยท๋ฐฑ์—… ์‚ญ์ œ [์ •์ƒ, CSP๋ณ„]
// ์‚ญ์ œ ๋Œ€์ƒ(์ด ํ–‰์ด S03~S07 ์—์„œ ๋งŒ๋“  ๊ฒƒ):
//   โ‘  ์ž๊ธฐ CSP bucket mcmp-e2e-v07-os{n}(S03), os{n}r(S07)
//   โ‘ก S05 Migration ๋Œ€์ƒ bucket: AWS ํ–‰ โ†’ GCP os4m, ๋‹ค๋ฅธ ํ–‰ โ†’ AWS os1m (2026-10-12 ์‚ฌ์šฉ์ž ์ง€์‹œ๋กœ ์ •๋ฆฌ ๋ฒ”์œ„ ํ™•์žฅ)
//   โ‘ข S06 ๋ฐฑ์—… mcmp-e2e-v07-bk{n} (Restore ํ™”๋ฉด Backup List)
// Generate > Object Storage ์˜ Bucket List ์—์„œ ์ฒดํฌ โ†’ Delete โ†’ ์ด๋ฆ„ ์ž…๋ ฅ ํ™•์ธ. ์—†๋Š” ์ž์›์€ ๊ฑด๋„ˆ๋›ด๋‹ค(์ตœ์†Œ 1๊ฐœ๋Š” ์‚ญ์ œ๋ผ์•ผ PASS).
for (const c of activeCsps(['AWS', 'Azure', 'Alibaba', 'GCP', 'Tencent', 'NHN Cloud', 'NCP', 'IBM', 'KT'])) {
  test(`${tid('IV8_S08', c)} Object Storage: Bucket ์‚ญ์ œ (ํ…Œ์ŠคํŠธ ์ž์› ์ •๋ฆฌ)`, async ({ page }) => {
    const why = dmBlocker(c);
    test.skip(!!why, why);
    const tc = CSPS.find((x) => x.code === (c.code === 'AWS' ? 'GCP' : 'AWS'))!; // S05 ๋Œ€์ƒ CSP
    const api = apiRecorder(page);
    await loginAsAdmin(page);
    const deleted: string[] = [];

    // โ‘ข ๋ฐฑ์—… ๋จผ์ €(์›๋ณธ bucket ์ •๋ณด๋ฅผ ์ฐธ์กฐํ•˜๋ฏ€๋กœ bucket ๋ณด๋‹ค ๋จผ์ € ์ง€์šด๋‹ค)
    let f = await openDm(page, 'Restore');
    await page.waitForTimeout(3000);
    const bk = backupName(c);
    if (await deleteBackup(page, f, bk, api)) deleted.push(`backup ${bk}`);

    // โ‘  ์ž๊ธฐ CSP bucket
    f = await openDm(page, 'Generate');
    await selectCred(page, f, c);
    api();
    for (const n of [bucketName(c), bucketName(c, 'r')]) if (await deleteBucket(page, f, n, api)) deleted.push(`${c.code} ${n}`);
    // โ‘ก S05 ๋Œ€์ƒ bucket(๋‹ค๋ฅธ CSP)
    f = await openDm(page, 'Generate');
    await selectCred(page, f, tc);
    api();
    const mig = bucketName(tc, 'm');
    if (await deleteBucket(page, f, mig, api)) deleted.push(`${tc.code} ${mig}`);

    test.info().annotations.push({ type: 'deleted', description: deleted.join(', ') || '(์—†์Œ)' });
    expect(deleted.length, '์‚ญ์ œํ•œ ์ž์› ์ˆ˜(ํ•˜๋‚˜๋„ ์—†์œผ๋ฉด IV8_S03~S07 ์„ ํ–‰ ํ•„์š”)').toBeGreaterThan(0);
    expect(await bucketRowText(f, mig), `${mig} ๋ชฉ๋ก์—์„œ ์ œ๊ฑฐ`).toBe('');
    // ์ตœ์ข… ํ™•์ธ: ๋‚จ์€ ์ž์› ์—†์Œ
    f = await openDm(page, 'Generate');
    await selectCred(page, f, c);
    for (const n of [bucketName(c), bucketName(c, 'r')]) expect(await bucketRowText(f, n), `${n} ๋ชฉ๋ก์—์„œ ์ œ๊ฑฐ`).toBe('');
    f = await openDm(page, 'Restore');
    await page.waitForTimeout(3000);
    expect(await backupRowText(f, bk), `${bk} Backup List ์—์„œ ์ œ๊ฑฐ`).toBe('');
  });
}

IV8_S09

tests/iv8/IV8_S09.spec.ts ยท 52์ค„ ยท ํ•ด์‹œ c1b0949f ยท ์‹œ๋‚˜๋ฆฌ์˜ค ๋ณด๊ธฐ

import { test, expect } from '../../lib/fixtures';
import { activeCsps, tid } from '../../lib/data';
import { loginAsAdmin } from '../../lib/ui';
import { apiRecorder } from '../../lib/iv8-os';
import { openSql, ensureRdb, rdbsOf, refreshRow, PENDING, RDB_CSPS } from '../../lib/iv8-rdb';

// IV8_S09 : SQL Database: RDBMS ์ธ์Šคํ„ด์Šค ์ƒ์„ฑ โ†’ Instance List ํ‘œ์‹œ [์ •์ƒ, CSP๋ณ„ AWSยทGCP] (TC-DATA-018)
// Data Migrations > Generate > SQL Database ํƒญ โ†’ credential ์„ ํƒ(Region ์ง€์ •) โ†’ + Instance โ†’ ์—”์ง„/๋ฒ„์ „/Class/ID/๊ณ„์ • ์ž…๋ ฅ โ†’ Create โ†’ Instance List ์— ํ‘œ์‹œ
// ์ž…๋ ฅ๊ฐ’์€ Drive mc-data-manager ์‹œํŠธ ๊ธฐ์ค€(2026-10-14): AWS mysql 8.4.11 / db.m5.large / mcmp โ€” rdb1aws, rdb3aws 2๊ฐœ.
// GCP MYSQL 8_4 / db-g1-small / root(ํ™”๋ฉด์—์„œ root ๋งŒ ํ—ˆ์šฉ) โ€” rdb2gpc. ์žฌ์‹คํ–‰ ๊ฐ€๋Šฅ(์ด๋ฏธ ์žˆ์œผ๋ฉด ์ƒ์„ฑ ์ƒ๋žต).
// CSP ์˜ ์ธ์Šคํ„ด์Šค๋ฅผ ๋ชจ๋‘ ์ƒ์„ฑ ์š”์ฒญํ•œ ๋’ค ํ•จ๊ป˜ ์™„๋ฃŒ๋ฅผ ๊ธฐ๋‹ค๋ฆฐ๋‹ค(AWS 1๊ฐœ ์•ฝ 12๋ถ„).
// 2026-10-15: ๋Œ€์ƒํ‘œ RDB = AWSยทGCPยทAlibabaยทNCP (AlibabaยทNCP ์ตœ์†Œ ์‚ฌ์–‘, ์‚ฌ์šฉ์ž ์ง€์‹œ)
for (const c of activeCsps(RDB_CSPS)) {
  test(`${tid('IV8_S09', c)} SQL Database: RDBMS ์ธ์Šคํ„ด์Šค ์ƒ์„ฑ โ†’ Instance List ํ‘œ์‹œ`, async ({ page }) => {
    test.setTimeout(50 * 60_000);
    const list = rdbsOf(c);
    const api = apiRecorder(page);
    await loginAsAdmin(page);
    let f = await openSql(page, c);
    api();
    for (const r of list) {
      // Alibaba API ํ˜ธ์ถœ ์ œํ•œ("user flow control")์œผ๋กœ Class ๋ชฉ๋ก์ด ๋น„๋Š” ๊ฒฝ์šฐ๊ฐ€ ์žˆ์–ด ํ™”๋ฉด์„ ๋‹ค์‹œ ์—ด๊ณ  ์ตœ๋Œ€ 3ํšŒ ์‹œ๋„
      let row = '';
      for (let i = 1; ; i++) {
        try { row = await ensureRdb(page, f, c, r, api); break; } catch (e) {
          if (i >= 3) throw e;
          test.info().annotations.push({ type: 'rdb-retry', description: `${r.id} ${i}ํšŒ์ฐจ ์‹คํŒจ โ†’ 60s ํ›„ ์žฌ์‹œ๋„: ${String(e).slice(0, 300)}` });
          await page.waitForTimeout(60_000);
          f = await openSql(page, c);
        }
      }
      expect(row, `${r.id} Instance List ํ–‰`).toContain(r.id);
      expect(row, `${r.id} Instance Class ํ‘œ์‹œ`).toContain(r.classText);
      f = await openSql(page, c); // ์ƒ์„ฑ ํŒจ๋„์„ ๋‹ซ๊ณ  ๋ชฉ๋ก์„ ์ƒˆ๋กœ ์ฝ๋Š”๋‹ค
    }
    // ์ƒ์„ฑ ์ง„ํ–‰ ์ƒํƒœ(creating ๋“ฑ)๊ฐ€ ๋๋‚  ๋•Œ๊นŒ์ง€ ๊ฐฑ์‹ ํ•˜๋ฉฐ ๋Œ€๊ธฐ(์ตœ๋Œ€ 30๋ถ„)
    const cur: Record<string, string> = {};
    const t0 = Date.now();
    const pending = () => list.filter((r) => !cur[r.id] || PENDING.test(cur[r.id])); // ๋นˆ ๊ฐ’(๋ชฉ๋ก ์žฌ์กฐํšŒ ์ค‘)์€ ์™„๋ฃŒ๋กœ ๋ณด์ง€ ์•Š๋Š”๋‹ค
    while (pending().length && Date.now() - t0 < 30 * 60_000) {
      await page.waitForTimeout(30_000);
      for (const r of pending()) cur[r.id] = (await refreshRow(page, c, r.id)).text || cur[r.id] || '';
    }
    for (const r of list) {
      test.info().annotations.push({ type: 'rdb-final', description: `${r.id} โ€” ${Math.round((Date.now() - t0) / 1000)}s ๋Œ€๊ธฐ ํ›„: ${cur[r.id]}` });
      expect(cur[r.id], `${r.id} ์ตœ์ข… Instance List ํ–‰`).toContain(r.id);
      expect(cur[r.id], `${r.id} ์ธ์Šคํ„ด์Šค ์ƒํƒœ`).not.toMatch(/fail|error/i);
      expect(cur[r.id], `${r.id} ์ƒ์„ฑ ์™„๋ฃŒ ์ƒํƒœ(creating ์ข…๋ฃŒ)`).not.toMatch(PENDING);
    }
  });
}

IV8_S10

tests/iv8/IV8_S10.spec.ts ยท 48์ค„ ยท ํ•ด์‹œ e1d80c9b ยท ์‹œ๋‚˜๋ฆฌ์˜ค ๋ณด๊ธฐ

import { test, expect } from '../../lib/fixtures';
import { activeCsps, tid } from '../../lib/data';
import { loginAsAdmin } from '../../lib/ui';
import { apiRecorder, openDm, deleteBackup, backupRowText } from '../../lib/iv8-os';
import { openSql, deleteRdb, rdbsOf, refreshRow, rdbFlowOf, RDB_CSPS } from '../../lib/iv8-rdb';

// IV8_S10 : SQL Database: RDBMS ์ธ์Šคํ„ด์Šค ์‚ญ์ œ (ํ…Œ์ŠคํŠธ ์ž์› ์ •๋ฆฌ) [์ •์ƒ, CSP๋ณ„ AWSยทGCP] (TC-DATA-019)
// 1.Restore > SQL Database โ†’ Backup List ์—์„œ 'mcmp-e2e-v07-bk2'(IV8_S13) ์ฒดํฌ โ†’ Delete โ†’ ํ™•์ธ ํŒ์—…์— ๋ฐฑ์—… ์ด๋ฆ„ ์ž…๋ ฅ โ†’ Delete (AWS ์‹คํ–‰์—์„œ 1ํšŒ)
// 2.Generate > SQL Database โ†’ credential ์„ ํƒ โ†’ Instance List ์—์„œ S09 ์ธ์Šคํ„ด์Šค ์ฒดํฌ โ†’ Delete โ†’ ํ™•์ธ ํŒ์—…์— ์ธ์Šคํ„ด์Šค ์ด๋ฆ„ ์ž…๋ ฅ โ†’ Delete
// 3.Instance ListยทBackup List ์—์„œ ์‚ฌ๋ผ์ง ํ™•์ธ. ๋Œ€์ƒ: AWS rdb1awsยทrdb3aws / GCP rdb2gpc (Drive mc-data-manager ์‹œํŠธ ๊ธฐ์ค€, 2026-10-14)
// 2026-10-15: ๋Œ€์ƒํ‘œ RDB = AWSยทGCPยทAlibabaยทNCP, ๋ฐฑ์—…์€ CSP ๋ณ„(rdbFlowOf(c).backup)
for (const c of activeCsps(RDB_CSPS)) {
  test(`${tid('IV8_S10', c)} SQL Database: RDBMS ์ธ์Šคํ„ด์Šค ์‚ญ์ œ (ํ…Œ์ŠคํŠธ ์ž์› ์ •๋ฆฌ)`, async ({ page }) => {
    test.setTimeout(40 * 60_000);
    const list = rdbsOf(c);
    const api = apiRecorder(page);
    await loginAsAdmin(page);
    // AWS ๋Š” 2026-10-14 ํ๋ฆ„์˜ ์˜› ๋ฐฑ์—…(mcmp-e2e-v07-bk2)๋„ ๋‚จ์•„ ์žˆ์œผ๋ฉด ํ•จ๊ป˜ ์ •๋ฆฌ
    for (const RDB_BACKUP of [rdbFlowOf(c).backup, ...(c.code === 'AWS' ? ['mcmp-e2e-v07-bk2'] : [])]) {
      const r = await openDm(page, 'Restore');
      await r.getByText(/SQL Database/i).first().click();
      await page.waitForTimeout(3000);
      api();
      if (await backupRowText(r, RDB_BACKUP)) {
        expect(await deleteBackup(page, r, RDB_BACKUP, api), `${RDB_BACKUP} ๋ฐฑ์—… ์‚ญ์ œ`).toBe(true);
      } else test.info().annotations.push({ type: 'backup-missing', description: `${RDB_BACKUP} Backup List ์— ์—†์Œ(์ด๋ฏธ ์ •๋ฆฌ๋จ)` });
      expect(await backupRowText(r, RDB_BACKUP), `${RDB_BACKUP} Backup List ์—์„œ ์ œ๊ฑฐ`).toBe('');
    }
    for (const r of list) {
      const f = await openSql(page, c);
      api();
      const deleted = await deleteRdb(page, f, r, api);
      expect(deleted, `${r.id} ๊ฐ€ Instance List ์— ์žˆ์–ด์•ผ ํ•œ๋‹ค(IV8_S09 ์„ ํ–‰)`).toBe(true);
    }
    // ์‚ญ์ œ๋Š” CSP ์—์„œ ์ˆ˜ ๋ถ„ ๊ฑธ๋ฆฐ๋‹ค: ๋ชฉ๋ก์„ ๊ฐฑ์‹ ํ•˜๋ฉฐ ํ–‰์ด ์‚ฌ๋ผ์งˆ ๋•Œ๊นŒ์ง€ ๋Œ€๊ธฐ(์ตœ๋Œ€ 30๋ถ„)
    const t0 = Date.now();
    const cur: Record<string, string> = Object.fromEntries(list.map((r) => [r.id, 'pending']));
    while (list.some((r) => cur[r.id]) && Date.now() - t0 < 30 * 60_000) {
      await page.waitForTimeout(30_000);
      for (const r of list.filter((x) => cur[x.id])) {
        cur[r.id] = (await refreshRow(page, c, r.id)).text;
        test.info().annotations.push({ type: 'rdb-poll', description: `${r.id} ${Math.round((Date.now() - t0) / 1000)}s: ${cur[r.id] || '(ํ–‰ ์—†์Œ)'}` });
      }
    }
    for (const r of list) expect(cur[r.id], `${r.id} ๋ชฉ๋ก์—์„œ ์ œ๊ฑฐ`).toBe('');
  });
}

IV8_S11

tests/iv8/IV8_S11.spec.ts ยท 37์ค„ ยท ํ•ด์‹œ 7d592bd5 ยท ์‹œ๋‚˜๋ฆฌ์˜ค ๋ณด๊ธฐ

import { test, expect } from '../../lib/fixtures';
import { activeCsps, tid } from '../../lib/data';
import { loginAsAdmin } from '../../lib/ui';
import { apiRecorder, openDm, selectCred } from '../../lib/iv8-os';
import { openSql, pickRdbRow, createSampleData, loadDatabases, hasRdbDb, rdbFlowOf, RDB_CSPS, RDB_DB } from '../../lib/iv8-rdb';

// IV8_S11 : SQL Database: RDBMS ์ƒ˜ํ”Œ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ (Generate) [์ •์ƒ, CSP๋ณ„ AWS] (TC-DATA-008)
// 1.Data Migrations > Generate โ†’ SQL Database ํƒญ 2.credential 'AWS - aws' / Region 'ap-northeast-2' โ†’ Instance List ์—์„œ mcmp-e2e-v07-rdb1aws ์„ ํƒ
// 3.Sample Data โ†’ Username 'mcmp' / Password ์ž…๋ ฅ โ†’ Submit 4.ํ•˜๋‹จ Result ์„ฑ๊ณต ๋ฉ”์‹œ์ง€
// 5.Back Up โ†’ SQL Database ํƒญ์—์„œ ๊ฐ™์€ ์ธ์Šคํ„ด์Šค ์„ ํƒ โ†’ ์ ‘์†์ •๋ณด โ†’ Load Databases ์— LibraryManagement_0 ํ‘œ์‹œ
// ์„ ํ–‰: IV8_S09_AWS(rdb1aws available)
// 2026-10-15: ๋Œ€์ƒํ‘œ(RDB = AWSยทGCPยทAlibabaยทNCP)์— ๋งž์ถฐ CSP ๋ณ„ ์›๋ณธ(rdbFlowOf(c).src)์—์„œ ์‹คํ–‰
for (const c of activeCsps(RDB_CSPS)) {
  test(`${tid('IV8_S11', c)} SQL Database: RDBMS ์ƒ˜ํ”Œ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ (Generate)`, async ({ page }) => {
    test.setTimeout(30 * 60_000);
    const r = rdbFlowOf(c).src;
    const api = apiRecorder(page);
    await loginAsAdmin(page);
    const f = await openSql(page, c);
    api();
    await pickRdbRow(page, f, r.id);
    const result = await createSampleData(page, f, r, api);
    expect(result, 'Result ์„ฑ๊ณต ๋ฉ”์‹œ์ง€').toMatch(/success|complete/i);
    expect(result, 'Result ์˜ค๋ฅ˜ ์—†์Œ').not.toMatch(/fail|error/i);

    // Back Up > SQL Database ์—์„œ ์ƒ์„ฑ๋œ DB ์กฐํšŒ
    const b = await openDm(page, 'Back Up');
    await b.getByText(/SQL Database/i).first().click();
    await selectCred(page, b, c, 'sourceCredentialSelect');
    await page.waitForTimeout(3000);
    api();
    await pickRdbRow(page, b, r.id, '#mysql-srcHost');
    const dbs = await loadDatabases(page, b, r, '#rdbms-load-db-btn', api);
    expect(hasRdbDb(dbs), `Back Up ํ™”๋ฉด DB ๋ชฉ๋ก์— ${RDB_DB}(๋Œ€์†Œ๋ฌธ์ž ๋ฌด์‹œ)`).toBe(true);
  });
}

IV8_S12

tests/iv8/IV8_S12.spec.ts ยท 98์ค„ ยท ํ•ด์‹œ 43e4bb43 ยท ์‹œ๋‚˜๋ฆฌ์˜ค ๋ณด๊ธฐ

import { test, expect } from '../../lib/fixtures';
import { activeCsps, tid } from '../../lib/data';
import { loginAsAdmin } from '../../lib/ui';
import { apiRecorder, openDm, selectCred, dmCred, dmRegion } from '../../lib/iv8-os';
import {
  pickRdbRow, loadDatabases, resultText, waitResult, stepSummary, clickIn, diagnoseIn, DIAG_ERR,
  hasRdbDb, selectRdbDb, rdbFlowOf, RDB_CSPS, RDB_DB, RDB_PW,
} from '../../lib/iv8-rdb';

// IV8_S12 : SQL Database: RDBMS ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ [์ •์ƒ, CSP๋ณ„ AWS] (TC-DATA-009)
// 1.Data Migrations > Migration โ†’ SQL Database ํƒญ
// 2.Source: credential 'AWS - aws' / Region 'ap-northeast-2' โ†’ mcmp-e2e-v07-rdb1aws ์„ ํƒ โ†’ Username 'mcmp' / Password โ†’ Load Databases โ†’ LibraryManagement_0 ์„ ํƒ โ†’ Verify โ†’ Next
// 3.Diagnose Source โ†’ Next 4.Target: 'AWS - aws' / 'ap-northeast-2' โ†’ mcmp-e2e-v07-rdb3aws ์„ ํƒ โ†’ ์ ‘์†์ •๋ณด โ†’ Verify โ†’ Next
// 5.Diagnose Target โ†’ Submit 6.ํ•˜๋‹จ Result ์„ฑ๊ณต ๋ฉ”์‹œ์ง€ 7.Back Up โ†’ SQL Database ์—์„œ rdb3aws ์˜ DB ๋ชฉ๋ก์— LibraryManagement_0
// ์„ ํ–‰: IV8_S09_AWS(rdb1awsยทrdb3aws), IV8_S11_AWS(rdb1aws ์ƒ˜ํ”Œ ๋ฐ์ดํ„ฐ)
const P1 = '#collapseOne', P2 = '#collapseTwo', P3 = '#collapseThree', P4 = '#collapseFour';
// 2026-10-15: CSP ๋ณ„ srcโ†’mig(rdbFlowOf) โ€” ๋Œ€์ƒํ‘œ RDB = AWSยทGCPยทAlibabaยทNCP
for (const c of activeCsps(RDB_CSPS)) {
  test(`${tid('IV8_S12', c)} SQL Database: RDBMS ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜`, async ({ page }) => {
    test.setTimeout(40 * 60_000);
    const note = (t: string, d: string) => test.info().annotations.push({ type: t, description: d.slice(0, 1500) });
    const { src: RDB_SRC, mig: RDB_MIG } = rdbFlowOf(c);
    const api = apiRecorder(page);
    await loginAsAdmin(page);
    const f = await openDm(page, 'Migration');
    await f.getByText(/SQL Database/i).first().click();

    await test.step('1. Source ์ ‘์†์ •๋ณด โ†’ Database ์„ ํƒ โ†’ Verify โ†’ Next', async () => {
      await selectCred(page, f, c, 'sourceCredentialSelect');
      await page.waitForTimeout(3000);
      api();
      await pickRdbRow(page, f, RDB_SRC.id, '#mysql-srcHost');
      await loadDatabases(page, f, RDB_SRC, '#src-load-db-btn', api);
      await selectRdbDb(f, '#mysql-srcDatabase');
      await f.locator('#sourceVerifyBtn').click();
      await page.waitForTimeout(5000);
      note('source-verify', `${await stepSummary(f, P1)} | api: ${api()}`);
      await f.locator(`button[data-current="${P1}"]`).click();
      await expect(f.locator(P2)).toBeVisible({ timeout: 30_000 });
    });

    await test.step('2. Diagnose Source โ†’ Next', async () => {
      note('step2', await stepSummary(f, P2));
      const d = await diagnoseIn(page, f, P2, api);
      expect(d, 'Diagnose Source ๊ฒฐ๊ณผ ์˜ค๋ฅ˜ ์—†์Œ').not.toMatch(DIAG_ERR);
      await f.locator(`button[data-current="${P2}"]`).click();
      await expect(f.locator(P3)).toBeVisible({ timeout: 30_000 });
    });

    await test.step('3. Target ์ ‘์†์ •๋ณด โ†’ Verify โ†’ Next', async () => {
      note('step3-before', await stepSummary(f, P3));
      const cred = f.locator(`${P3} select[id*="Credential"]`).first();
      await cred.selectOption(dmCred(c)!);
      const region = f.locator(`${P3} select[id*="Region"]`).first();
      await page.waitForTimeout(2000);
      await region.selectOption(dmRegion(c));
      await page.waitForTimeout(4000);
      const row = f.locator(P3).locator('table tbody tr').filter({ has: f.locator('td', { hasText: new RegExp(`^\\s*${RDB_MIG.id}\\s*$`) }) }).first();
      await expect(row, `${RDB_MIG.id} Target Instance List ํ–‰`).toBeVisible({ timeout: 60_000 });
      await row.locator('td').nth(2).click();
      await page.waitForTimeout(2000);
      const user = f.locator(P3).getByPlaceholder('Username').first();
      const pw = f.locator(P3).getByPlaceholder('Password').first();
      await user.fill(RDB_MIG.user);
      await pw.fill(RDB_PW);
      api();
      await clickIn(f, P3, /verify/i);
      await page.waitForTimeout(5000);
      note('target-verify', `${await stepSummary(f, P3)} | api: ${api()}`);
      await f.locator(`button[data-current="${P3}"]`).click();
      await expect(f.locator(P4)).toBeVisible({ timeout: 30_000 });
    });

    await test.step('4. Diagnose Target โ†’ Submit โ†’ Result', async () => {
      note('step4', await stepSummary(f, P4));
      const d = await diagnoseIn(page, f, P4, api);
      expect(d, 'Diagnose Target ๊ฒฐ๊ณผ ์˜ค๋ฅ˜ ์—†์Œ').not.toMatch(DIAG_ERR);
      const before = await resultText(page, f);
      await f.locator('#submitBtn').click();
      const result = await waitResult(page, f, before, 30 * 60_000);
      note('submit-api', api() || '(API ๊ธฐ๋ก ์—†์Œ)');
      expect(result, 'Result ์„ฑ๊ณต ๋ฉ”์‹œ์ง€').toMatch(/success|complete/i);
      expect(result, 'Result ์˜ค๋ฅ˜ ์—†์Œ').not.toMatch(/\bfail|\berror/i);
    });

    await test.step('5. Back Up > SQL Database ์—์„œ Target(rdb3aws) DB ๋ชฉ๋ก ํ™•์ธ', async () => {
      const b = await openDm(page, 'Back Up');
      await b.getByText(/SQL Database/i).first().click();
      await selectCred(page, b, c, 'sourceCredentialSelect');
      await page.waitForTimeout(3000);
      api();
      await pickRdbRow(page, b, RDB_MIG.id, '#mysql-srcHost');
      const dbs = await loadDatabases(page, b, RDB_MIG, '#rdbms-load-db-btn', api);
      expect(hasRdbDb(dbs), `Target(${RDB_MIG.id}) DB ๋ชฉ๋ก์— ${RDB_DB}(๋Œ€์†Œ๋ฌธ์ž ๋ฌด์‹œ)`).toBe(true);
    });
  });
}

IV8_S13

tests/iv8/IV8_S13.spec.ts ยท 40์ค„ ยท ํ•ด์‹œ f76297ec ยท ์‹œ๋‚˜๋ฆฌ์˜ค ๋ณด๊ธฐ

import { test, expect } from '../../lib/fixtures';
import { activeCsps, tid } from '../../lib/data';
import { loginAsAdmin } from '../../lib/ui';
import { apiRecorder, openDm, selectCred, backupRowText } from '../../lib/iv8-os';
import { pickRdbRow, loadDatabases, submitBackup, stepSummary, hasRdbDb, selectRdbDb, rdbFlowOf, RDB_CSPS, RDB_DB } from '../../lib/iv8-rdb';

// IV8_S13 : SQL Database: RDBMS ๋ฐฑ์—… [์ •์ƒ, CSP๋ณ„ AWS] (TC-DATA-010)
// 1.Data Migrations > Back Up โ†’ SQL Database ํƒญ 2.Source: 'AWS - aws' / 'ap-northeast-2' โ†’ mcmp-e2e-v07-rdb1aws ์„ ํƒ โ†’ Username 'mcmp' / Password
// โ†’ Load Databases(๊ฒ€์ฆ) 3.Database 'LibraryManagement_0' ์„ ํƒ 4.Backup Name 'mcmp-e2e-v07-bk2' โ†’ Submit 5.Result ์„ฑ๊ณต ยท Backup List ์— ํ‘œ์‹œ
// ์„ ํ–‰: IV8_S11_AWS(rdb1aws ์ƒ˜ํ”Œ ๋ฐ์ดํ„ฐ). ์ด๋ฏธ ๊ฐ™์€ ์ด๋ฆ„ ๋ฐฑ์—…์ด ์žˆ์œผ๋ฉด ์‹คํŒจ๋กœ ๋‚จ๊ธด๋‹ค(IV8_S10 ์—์„œ ์ •๋ฆฌ ํ›„ ์žฌ์‹คํ–‰).
// 2026-10-15: CSP ๋ณ„ ์›๋ณธยท๋ฐฑ์—… ์ด๋ฆ„(rdbFlowOf) โ€” ๋Œ€์ƒํ‘œ RDB = AWSยทGCPยทAlibabaยทNCP
for (const c of activeCsps(RDB_CSPS)) {
  test(`${tid('IV8_S13', c)} SQL Database: RDBMS ๋ฐฑ์—…`, async ({ page }) => {
    test.setTimeout(40 * 60_000);
    const note = (t: string, d: string) => test.info().annotations.push({ type: t, description: d.slice(0, 1500) });
    const { src: RDB_SRC, backup: RDB_BACKUP } = rdbFlowOf(c);
    const api = apiRecorder(page);
    await loginAsAdmin(page);
    const f = await openDm(page, 'Back Up');
    await f.getByText(/SQL Database/i).first().click();
    await selectCred(page, f, c, 'sourceCredentialSelect');
    await page.waitForTimeout(3000);
    expect(await backupRowText(f, RDB_BACKUP), `${RDB_BACKUP} ๊ฐ€ ์ด๋ฏธ Backup List ์— ์žˆ์Œ(์ •๋ฆฌ ํ•„์š”)`).toBe('');
    api();
    await pickRdbRow(page, f, RDB_SRC.id, '#mysql-srcHost');
    const dbs = await loadDatabases(page, f, RDB_SRC, '#rdbms-load-db-btn', api);
    expect(hasRdbDb(dbs), `DB ๋ชฉ๋ก์— ${RDB_DB}(๋Œ€์†Œ๋ฌธ์ž ๋ฌด์‹œ)`).toBe(true);
    const dbSel = f.locator('select[name$="[databaseName]"], select[id*="Database"]').first();
    const dbId = await dbSel.getAttribute('id');
    await selectRdbDb(f, dbId ? `select#${dbId}` : 'select[id*="Database"]');
    note('form', await stepSummary(f, 'body'));
    const { result, row } = await submitBackup(page, f, RDB_BACKUP, api);
    expect(result, 'Result ์„ฑ๊ณต ๋ฉ”์‹œ์ง€').toMatch(/success|complete/i);
    expect(result, 'Result ์˜ค๋ฅ˜ ์—†์Œ').not.toMatch(/\bfail|\berror/i);
    expect(row, 'Backup List ์— ๋ฐฑ์—… ํ‘œ์‹œ').toContain(RDB_BACKUP);
    expect(row.toLowerCase(), 'Backup List ํ–‰์˜ DB(๋Œ€์†Œ๋ฌธ์ž ๋ฌด์‹œ)').toContain(RDB_DB.toLowerCase());
    expect(row, '๋ฐฑ์—… ์ƒํƒœ').not.toMatch(/fail|error/i);
  });
}

IV8_S14

tests/iv8/IV8_S14.spec.ts ยท 59์ค„ ยท ํ•ด์‹œ e3b90577 ยท ์‹œ๋‚˜๋ฆฌ์˜ค ๋ณด๊ธฐ

import { test, expect } from '../../lib/fixtures';
import { activeCsps, tid } from '../../lib/data';
import { loginAsAdmin } from '../../lib/ui';
import { apiRecorder, openDm, selectCred, backupRowText, logHistory } from '../../lib/iv8-os';
import { rdbRow, pickRdbRow, loadDatabases, resultText, waitResult, stepSummary, hasRdbDb, rdbFlowOf, RDB_CSPS, RDB_DB, RDB_PW } from '../../lib/iv8-rdb';

// IV8_S14 : SQL Database: RDBMS ๋ณต๊ตฌ [์ •์ƒ, Target GCP] (TC-DATA-011)
// 1.Data Migrations > Restore โ†’ SQL Database ํƒญ 2.Backup List ์—์„œ 'mcmp-e2e-v07-bk2'(IV8_S13, AWS rdb1aws ๋ฐฑ์—…) ์„ ํƒ
// 3.Target: 'GCP - gcp' / 'asia-northeast3' โ†’ mcmp-e2e-v07-rdb2gpc ์„ ํƒ โ†’ Username 'root' / Password โ†’ Submit 4.ํ•˜๋‹จ Result ์„ฑ๊ณต
// 5.Back Up โ†’ SQL Database ์—์„œ rdb2gpc ์˜ DB ๋ชฉ๋ก์— LibraryManagement_0. ์„ ํ–‰: IV8_S09_GCP, IV8_S13_AWS
// 2026-10-15: CSP ๋‹จ๋… ํ๋ฆ„ โ€” ๊ทธ CSP ์›๋ณธ ๋ฐฑ์—…(rdbFlowOf(c).backup) โ†’ ๊ฐ™์€ CSP ์˜ rst ๋กœ ๋ณต๊ตฌ (2026-10-14 GCP PASS ๋Š” AWS ๋ฐฑ์—… โ†’ rdb2gpc)
for (const c of activeCsps(RDB_CSPS)) {
  test(`${tid('IV8_S14', c)} SQL Database: RDBMS ๋ณต๊ตฌ`, async ({ page }) => {
    test.setTimeout(40 * 60_000);
    const { rst: RDB_RST, backup: RDB_BACKUP } = rdbFlowOf(c);
    const note = (t: string, d: string) => test.info().annotations.push({ type: t, description: d.slice(0, 1500) });
    const api = apiRecorder(page);
    await loginAsAdmin(page);
    const f = await openDm(page, 'Restore');
    await f.getByText(/SQL Database/i).first().click();
    await page.waitForTimeout(3000);
    await expect.poll(() => backupRowText(f, RDB_BACKUP), { timeout: 60_000 }).not.toBe('');
    const brow = await backupRowText(f, RDB_BACKUP);
    expect(brow, `${RDB_BACKUP} ๋ฐฑ์—… ์™„๋ฃŒ ์ƒํƒœ(IV8_S13 ์„ ํ–‰)`).toMatch(/completed/i);
    const row = f.locator(`tr.backup-row[data-name="${RDB_BACKUP}"]`).first();
    await (await row.count() ? row : f.locator('table tbody tr').filter({ hasText: RDB_BACKUP }).first()).locator('td', { hasText: RDB_BACKUP }).first().click();
    await page.waitForTimeout(1500);
    note('backup-select', `${brow} | Source=${await f.locator('#sourcePointLabel').inputValue().catch(() => '')}`);

    await selectCred(page, f, c, 'targetCredentialSelect');
    await page.waitForTimeout(3000);
    await expect(rdbRow(f, RDB_RST.id), `${RDB_RST.id} Target Instance List ํ–‰`).toBeVisible({ timeout: 60_000 });
    await rdbRow(f, RDB_RST.id).locator('td').nth(2).click();
    await page.waitForTimeout(2000);
    await f.locator('input[placeholder="Username"]:visible').first().fill(RDB_RST.user);
    await f.locator('input[placeholder="Password"]:visible').first().fill(RDB_PW);
    note('form', await stepSummary(f, 'body'));
    api();
    const before = await resultText(page, f);
    await f.locator('#submitBtn').click();
    const result = await waitResult(page, f, before, 30 * 60_000);
    note('submit-api', api() || '(API ๊ธฐ๋ก ์—†์Œ)');
    expect(result, 'Result ์„ฑ๊ณต ๋ฉ”์‹œ์ง€').toMatch(/success|complete/i);
    expect(result, 'Result ์˜ค๋ฅ˜ ์—†์Œ').not.toMatch(/\bfail|\berror/i);

    // ๋ณต๊ตฌ ๋Œ€์ƒ์—์„œ DB ์กฐํšŒ
    const b = await openDm(page, 'Back Up');
    await b.getByText(/SQL Database/i).first().click();
    await selectCred(page, b, c, 'sourceCredentialSelect');
    await page.waitForTimeout(3000);
    api();
    await pickRdbRow(page, b, RDB_RST.id, '#mysql-srcHost');
    const dbs = await loadDatabases(page, b, RDB_RST, '#rdbms-load-db-btn', api);
    expect(hasRdbDb(dbs), `Target(${RDB_RST.id}) DB ๋ชฉ๋ก์— ${RDB_DB}(๋Œ€์†Œ๋ฌธ์ž ๋ฌด์‹œ)`).toBe(true);
    const log = await logHistory(page, 'SQL Database', 'Restore').catch(() => [] as string[]);
    note('log', log.slice(0, 3).join(' / ') || '(์ด๋ ฅ ์กฐํšŒ ์•ˆ ๋จ)');
  });
}

IV8_S15

tests/iv8/IV8_S15.spec.ts ยท 34์ค„ ยท ํ•ด์‹œ aebfcf7f ยท ์‹œ๋‚˜๋ฆฌ์˜ค ๋ณด๊ธฐ

import { test, expect } from '../../lib/fixtures';
import { activeCsps, tid } from '../../lib/data';
import { loginAsAdmin } from '../../lib/ui';
import { apiRecorder } from '../../lib/iv8-os';
import { PENDING } from '../../lib/iv8-rdb';
import { openNosql, ensureNrdb, waitNrdbReady, nrdbsOf, NRDB_CSPS } from '../../lib/iv8-nrdb';

// IV8_S15 : No-SQL: NRDB ์ธ์Šคํ„ด์Šค ์ƒ์„ฑ โ†’ Instance List ํ‘œ์‹œ [์ •์ƒ, CSP๋ณ„ GCP] (TC-DATA-020)
// Data Migrations > Generate โ†’ No-SQL ํƒญ โ†’ credential 'GCP - gcp' / Region 'asia-northeast3' โ†’ + Instance โ†’ Instance ID ์ž…๋ ฅ โ†’ Create
// โ†’ Instance List ์— ํ‘œ์‹œ. ์ธ์Šคํ„ด์Šค mcmp-e2e-v07-nrdb1gcp(์›๋ณธ), mcmp-e2e-v07-nrdb2gcp(Migration ๋Œ€์ƒ). ์žฌ์‹คํ–‰ ๊ฐ€๋Šฅ(์ด๋ฏธ ์žˆ์œผ๋ฉด ์ƒ์„ฑ ์ƒ๋žต).
// 2026-10-15: ๋Œ€์ƒํ‘œ(NRDB) ์ค‘ ์ธ์Šคํ„ด์Šค๊ฐ€ ์žˆ๋Š” GCPยทAlibabaยทNCP. AlibabaยทNCP MongoDB ์ตœ์†Œ ์‚ฌ์–‘(์‚ฌ์šฉ์ž ์ง€์‹œ) โ€” Alibaba ์ƒค๋”ฉ ํด๋Ÿฌ์Šคํ„ฐ๋Š” ์ƒ์„ฑ์— ์ˆ˜์‹ญ ๋ถ„
for (const c of activeCsps(NRDB_CSPS)) {
  test(`${tid('IV8_S15', c)} No-SQL: NRDB ์ธ์Šคํ„ด์Šค ์ƒ์„ฑ โ†’ Instance List ํ‘œ์‹œ`, async ({ page }) => {
    test.setTimeout(70 * 60_000);
    const list = nrdbsOf(c);
    const NRDB_IDS = list.map((s) => s.id);
    const api = apiRecorder(page);
    await loginAsAdmin(page);
    for (const s of list) {
      const id = s.id;
      const f = await openNosql(page, c);
      api();
      const row = await ensureNrdb(page, f, s, api);
      expect(row, `${id} Instance List ํ–‰`).toContain(id);
    }
    const cur = await waitNrdbReady(page, c, NRDB_IDS, 55 * 60_000);
    for (const id of NRDB_IDS) {
      expect(cur[id], `${id} ์ตœ์ข… Instance List ํ–‰`).toContain(id);
      expect(cur[id], `${id} ์ธ์Šคํ„ด์Šค ์ƒํƒœ`).not.toMatch(/fail|error/i);
      expect(cur[id], `${id} ์ƒ์„ฑ ์™„๋ฃŒ ์ƒํƒœ`).not.toMatch(PENDING);
    }
  });
}

IV8_S16

tests/iv8/IV8_S16.spec.ts ยท 40์ค„ ยท ํ•ด์‹œ a1b5437c ยท ์‹œ๋‚˜๋ฆฌ์˜ค ๋ณด๊ธฐ

import { test, expect } from '../../lib/fixtures';
import { activeCsps, tid } from '../../lib/data';
import { loginAsAdmin } from '../../lib/ui';
import { apiRecorder, logHistory } from '../../lib/iv8-os';
import { openNosql, pickNrdbRow, createNosqlSample, createMongoSample, createDynamoSample, nrdbFlowOf, nrdbKind, NRDB_FLOW_CSPS } from '../../lib/iv8-nrdb';

// IV8_S16 : No-SQL: NRDBMS ์ƒ˜ํ”Œ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ (Generate) [์ •์ƒ, CSP๋ณ„] (TC-DATA-012)
// 1.Data Migrations > Generate โ†’ No-SQL ํƒญ 2.credential / Region ์„ ํƒ โ†’ Instance List ์—์„œ ์›๋ณธ ์„ ํƒ
// 3.Sample Data โ†’ ์ž…๋ ฅ โ†’ Submit 4.ํ•˜๋‹จ Result ์„ฑ๊ณต ๋ฉ”์‹œ์ง€ 5.Log Management(No-SQL / Generate) ์„ฑ๊ณต ์ด๋ ฅ
//   GCP(Firestore): mcmp-e2e-v07-nrdb1gcp ์„ ํƒ โ†’ Database ID(= Instance ID, ์ž๋™ ์ž…๋ ฅ) ํ™•์ธ โ†’ Submit
//   AlibabaยทNCP(MongoDB, 2026-10-15): nrdb1ali / mcmpndb1 ์„ ํƒ โ†’ UsernameยทPasswordยทDatabase Name(mcmpe2e) โ†’ Submit
//   AWS(DynamoDB, 2026-10-15): ์ธ์Šคํ„ด์Šค ๋ชฉ๋ก ์—†์Œ โ€” credentialยทRegion ์„ ํƒ ํ›„ 'Sample Data'(#nrdb-aws-sampleBtn)
// ํด๋ผ์šฐ๋“œ ์ฝ˜์†” ๋ฐ์ดํ„ฐ ํ™•์ธ์€ ์ด spec ๋ฒ”์œ„ ๋ฐ– โ€” ์ƒ˜ํ”Œ ๋ฐ์ดํ„ฐ๋Š” S17(Migration)ยทS18(Backup) ๊ฒฐ๊ณผ๋กœ ๊ฐ„์ ‘ ํ™•์ธ
// ์„ ํ–‰: IV8_S15_<CSP>(์›๋ณธ available, AWS ์ œ์™ธ)
// 2026-10-15: ์‚ฌ์šฉ์ž ๋Œ€์ƒํ‘œ(NRDB = AWSยทGCPยทAlibabaยทNCP)
for (const c of activeCsps(NRDB_FLOW_CSPS)) {
  test(`${tid('IV8_S16', c)} No-SQL: NRDBMS ์ƒ˜ํ”Œ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ (Generate)`, async ({ page }) => {
    test.setTimeout(30 * 60_000);
    const kind = nrdbKind(c);
    // NRDB_SAMPLE_ON=mig: ๋Œ€์ƒ(mig)์— ์ƒ˜ํ”Œ ๋ฐ์ดํ„ฐ โ€” MongoDB MigrationยทRestore ์‚ฌ์ „ ์กฐ๊ฑด(๋Œ€์ƒ์— Database ๊ฐ€ ์žˆ์–ด์•ผ ๋Œ€์ƒ DB ๋ฅผ ๊ณ ๋ฅผ ์ˆ˜ ์žˆ์Œ).
    // Sample Data ์ƒ์„ฑ์ด ๋Œ€์ƒ Database ๋ฅผ ๋งŒ๋“œ๋Š” ์ž‘์—…์„ ๋Œ€์‹ ํ•œ๋‹ค(2026-10-15 ์‚ฌ์šฉ์ž ํ™•์ธ: ์šฐํšŒ ์•„๋‹˜)
    const src = process.env.NRDB_SAMPLE_ON === 'mig' ? nrdbFlowOf(c).mig : nrdbFlowOf(c).src;
    if (process.env.NRDB_SAMPLE_ON === 'mig') test.info().annotations.push({ type: 'sample-target', description: `์‚ฌ์ „ ์กฐ๊ฑด: ๋Œ€์ƒ ${src.id} ์— Database ์ƒ์„ฑ(Sample Data)` });
    const api = apiRecorder(page);
    await loginAsAdmin(page);
    const f = await openNosql(page, c);
    api();
    let result = '';
    if (kind === 'dynamo') result = await createDynamoSample(page, f, '#nrdb-aws-sampleBtn', api);
    else {
      await pickNrdbRow(page, f, src.id);
      result = kind === 'mongo' ? await createMongoSample(page, f, src, api) : await createNosqlSample(page, f, api);
    }
    expect(result, 'Result ์„ฑ๊ณต ๋ฉ”์‹œ์ง€').toMatch(/success|complete/i);
    expect(result, 'Result ์˜ค๋ฅ˜ ์—†์Œ').not.toMatch(/fail|error/i);
    const log = await logHistory(page, 'No-SQL', 'Generate');
    expect(log.join(' / '), 'Log Management ์— Generate ์ด๋ ฅ').toMatch(/generate/i);
  });
}

IV8_S17

tests/iv8/IV8_S17.spec.ts ยท 126์ค„ ยท ํ•ด์‹œ b125a4ea ยท ์‹œ๋‚˜๋ฆฌ์˜ค ๋ณด๊ธฐ

import { test, expect } from '../../lib/fixtures';
import { activeCsps, tid } from '../../lib/data';
import { loginAsAdmin, watchToasts } from '../../lib/ui';
import { apiRecorder, openDm, selectCred, logHistory, dmCred, dmRegion } from '../../lib/iv8-os';
import { resultText, waitResult, stepSummary, diagnoseIn, DIAG_ERR, RDB_PW } from '../../lib/iv8-rdb';
import { pickNrdbRow, mongoLoadDb, nrdbFlowOf, nrdbKind, NRDB_FLOW_CSPS, NRDB_DB } from '../../lib/iv8-nrdb';

// IV8_S17 : No-SQL: NRDBMS ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ [์ •์ƒ, CSP๋ณ„] (TC-DATA-013)
// 1.Data Migrations > Migration โ†’ No-SQL ํƒญ 2.Source: credential / Region โ†’ ์›๋ณธ ์„ ํƒ โ†’ Next
// 3.Diagnose Source โ†’ Next 4.Target: ๊ฐ™์€ CSP credential / Region โ†’ ๋Œ€์ƒ ์„ ํƒ โ†’ Next 5.Diagnose Target โ†’ Submit 6.ํ•˜๋‹จ Result ์„ฑ๊ณต 7.Log Management(No-SQL) ์ด๋ ฅ
//   GCP(Firestore): nrdb1gcp โ†’ nrdb2gcp, Instance IDยทDatabase ID ์ž๋™ ์ž…๋ ฅ. Diagnose ๋Š” GCP ๋ฏธ์ง€์›(๋ฒ„ํŠผ ๋น„ํ™œ์„ฑ)
//   AlibabaยทNCP(MongoDB, 2026-10-15): ์›๋ณธ ํ–‰ ์„ ํƒ โ†’ UsernameยทPassword โ†’ Load Databases โ†’ mcmpe2e ์„ ํƒ. ๋Œ€์ƒ ํ–‰ ์„ ํƒ โ†’ UsernameยทPassword
//   AWS(DynamoDB, 2026-10-15): ์ธ์Šคํ„ด์Šค ์—†์ด SourceยทTarget ๋ชจ๋‘ aws / ap-northeast-2 (๊ฐ™์€ Region ์˜ DynamoDB)
// ์„ ํ–‰: IV8_S15_<CSP>(์›๋ณธยท๋Œ€์ƒ, AWS ์ œ์™ธ), IV8_S16_<CSP>(์›๋ณธ ์ƒ˜ํ”Œ ๋ฐ์ดํ„ฐ)
const P1 = '#collapseOne', P2 = '#collapseTwo', P3 = '#collapseThree', P4 = '#collapseFour';
// 2026-10-15: ์‚ฌ์šฉ์ž ๋Œ€์ƒํ‘œ(NRDB = AWSยทGCPยทAlibabaยทNCP)
for (const c of activeCsps(NRDB_FLOW_CSPS)) {
  test(`${tid('IV8_S17', c)} No-SQL: NRDBMS ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜`, async ({ page }) => {
    test.setTimeout(40 * 60_000);
    const note = (t: string, d: string) => test.info().annotations.push({ type: t, description: d.slice(0, 1500) });
    const kind = nrdbKind(c);
    const { src, mig } = nrdbFlowOf(c);
    const api = apiRecorder(page);
    // Diagnose: ๋ฒ„ํŠผ์ด ๋น„ํ™œ์„ฑ(GCP ๋ฏธ์ง€์›)ยท์—†์Œ์ด๋ฉด ์ƒํƒœ๋งŒ ๊ธฐ๋ก, ํ™œ์„ฑ์ด๋ฉด ์‹คํ–‰ํ•˜๊ณ  ๊ฒฐ๊ณผ์— ์˜ค๋ฅ˜๊ฐ€ ์—†์–ด์•ผ ํ•œ๋‹ค
    const diag = async (panel: string) => {
      const b = f.locator(panel).getByRole('button', { name: /diagnose/i }).first();
      const state = !(await b.isVisible().catch(() => false)) ? '๋ฒ„ํŠผ ์—†์Œ' : (await b.isEnabled()) ? 'ํ™œ์„ฑ' : '๋น„ํ™œ์„ฑ';
      note(`diagnose-state ${panel}`, `${state} | ${await stepSummary(f, panel)}`);
      if (state === 'ํ™œ์„ฑ') expect(await diagnoseIn(page, f, panel, api), `${panel} Diagnose ๊ฒฐ๊ณผ ์˜ค๋ฅ˜ ์—†์Œ`).not.toMatch(DIAG_ERR);
    };
    const clickIfVisible = async (scope: string, name: RegExp, tag: string) => {
      const b = f.locator(scope).getByRole('button', { name }).first();
      if (await b.isVisible().catch(() => false)) { await b.click(); await page.waitForTimeout(5000); note(tag, `${await stepSummary(f, scope)} | api: ${api() || '(์—†์Œ)'}`); }
    };
    await loginAsAdmin(page);
    const f = await openDm(page, 'Migration');
    await f.getByText(/No.?SQL/i).first().click();

    await test.step('1. Source ์„ ํƒ โ†’ Next', async () => {
      await selectCred(page, f, c, 'sourceCredentialSelect');
      await page.waitForTimeout(3000);
      api();
      if (kind === 'firestore') await pickNrdbRow(page, f, src.id, '#src-nrdb-gcp-databaseId');
      if (kind === 'mongo') {
        await pickNrdbRow(page, f, src.id, '#mig-source-mongodb-host');
        expect(await mongoLoadDb(page, f, src, 'mig-source-mongodb-', '#src-nrdb-load-db-btn', api, P1), 'Source Database ์„ ํƒ').not.toBe('');
        await clickIfVisible(P1, /verify/i, 'source-verify');
      }
      note('step1', await stepSummary(f, P1));
      await f.locator(`button[data-current="${P1}"]`).click();
      await expect(f.locator(P2)).toBeVisible({ timeout: 30_000 });
    });

    await test.step('2. Diagnose Source โ†’ Next', async () => {
      await diag(P2);
      await f.locator(`button[data-current="${P2}"]`).click();
      await expect(f.locator(P3)).toBeVisible({ timeout: 30_000 });
    });

    await test.step('3. Target ์„ ํƒ โ†’ Next', async () => {
      await f.locator(`${P3} select[id*="Credential"]`).first().selectOption(dmCred(c)!);
      await page.waitForTimeout(2000);
      // AWS: ๊ฐ™์€ Region ์˜ ๊ฐ™์€ ํ…Œ์ด๋ธ”๋กœ ์˜ฎ๊ธฐ๋ฉด 'BatchWriteItem โ€ฆ key element does not match the schema'(500, 2026-10-15) โ†’ ๋Œ€์ƒ Region ์„ ap-northeast-1 ๋กœ ๋ถ„๋ฆฌ
      await f.locator(`${P3} select[id*="Region"]`).first().selectOption(kind === 'dynamo' ? 'ap-northeast-1' : dmRegion(c));
      await page.waitForTimeout(4000);
      if (kind !== 'dynamo') {
        const row = f.locator(P3).locator('table tbody tr').filter({ has: f.locator('td', { hasText: new RegExp(`^\\s*${mig.id}\\s*$`) }) }).first();
        await expect(row, `${mig.id} Target Instance List ํ–‰`).toBeVisible({ timeout: 60_000 });
        await row.locator('td').nth(2).click();
        await page.waitForTimeout(2000);
      }
      if (kind === 'mongo') {
        note('step3-after-pick', await stepSummary(f, P3));
        const user = f.locator(`${P3} input[id*="sername"]:visible`).first();
        const pw = f.locator(`${P3} input[type="password"]:visible`).first();
        if (await user.count()) await user.fill(mig.user!);
        if (await pw.count()) await pw.fill(RDB_PW);
        await clickIfVisible(P3, /load databases/i, 'target-load-db');
        // Load Databases ๋’ค ๋Œ€์ƒ DB select(#mig-target-โ€ฆ)๊ฐ€ ์ƒ๊ธด๋‹ค โ€” ๋น„์›Œ ๋‘๋ฉด Submit ์ด ์š”์ฒญ์„ ๋ณด๋‚ด์ง€ ์•Š์Œ(2026-10-15 AlibabaยทNCP).
        // ์‚ฌ์ „ ์กฐ๊ฑด: ๋Œ€์ƒ์— Database ๊ฐ€ ์žˆ์–ด์•ผ ํ•œ๋‹ค โ†’ IV8_S16 ์„ NRDB_SAMPLE_ON=mig ๋กœ ๋Œ€์ƒ์—๋„ ์‹คํ–‰(Sample Data ๊ฐ€ Database ์ƒ์„ฑ์„ ๋Œ€์‹ )
        note('target-db-options', await f.locator(P3).evaluate((p) => [...p.querySelectorAll('select')].filter((e) => (e as HTMLElement).offsetParent)
          .map((s) => `#${s.id}=${(s as HTMLSelectElement).value} [${[...(s as HTMLSelectElement).options].map((o) => `${o.value}:${(o.textContent || '').trim()}`).join('|')}]`).join(' / ')));
        const tsel = f.locator(`${P3} select[id^="mig-target"]`).first();
        if (await tsel.count()) {
          const opts = await tsel.locator('option').evaluateAll((os) => os.map((o) => (o as HTMLOptionElement).value));
          if (opts.includes(NRDB_DB)) await tsel.selectOption(NRDB_DB);
          note('target-db', `${await tsel.getAttribute('id')} = ${await tsel.inputValue()} (์˜ต์…˜ ${opts.join('|')})`);
        }
        await clickIfVisible(P3, /verify/i, 'target-verify');
      }
      note('step3', await stepSummary(f, P3));
      await f.locator(`button[data-current="${P3}"]`).click();
      await expect(f.locator(P4)).toBeVisible({ timeout: 30_000 });
    });

    await test.step('4. Diagnose Target โ†’ Submit โ†’ Result', async () => {
      await diag(P4);
      const before = await resultText(page, f);
      api();
      const toasts = await watchToasts(page);
      note('submit-state', `#submitBtn enabled=${await f.locator('#submitBtn').isEnabled()} visible=${await f.locator('#submitBtn').isVisible()}`);
      // Result ์ƒ์ž์— ์˜ค๋ฅ˜๊ฐ€ ์•ˆ ๋‚˜์˜ค๋Š” ๊ฒฝ์šฐ(2026-10-15 AWS 500)๊ฐ€ ์žˆ์–ด /migrate ์‘๋‹ต์„ ํ•จ๊ป˜ ๊ธฐ๋‹ค๋ฆฐ๋‹ค
      const resp = page.waitForResponse((r) => /\/migrate\/nrdbms/i.test(r.url()) && r.request().method() === 'POST', { timeout: 30 * 60_000 }).catch(() => null);
      await f.locator('#submitBtn').click();
      await page.waitForTimeout(5000);
      const early = await toasts();
      note('submit-toasts', early.join(' | ') || '(์—†์Œ)');
      note('submit-invalid', await f.evaluate(() => [...document.querySelectorAll('.is-invalid, .invalid-feedback, .text-danger')].filter((e) => (e as HTMLElement).offsetParent).map((e) => `#${(e as HTMLElement).id} ${(e.textContent || '').trim()}`).join(' | ').slice(0, 600)) || '(์—†์Œ)');
      const first = await Promise.race([resp, page.waitForTimeout(60_000).then(() => 'none' as const)]);
      if (first === 'none') note('submit-api-wait', '60์ดˆ ์•ˆ์— /migrate/nrdbms ์š”์ฒญ ์—†์Œ');
      const r = first === 'none' ? null : first; // ์š”์ฒญ์ด ์•ˆ ๋‚˜๊ฐ€๋ฉด 30๋ถ„์„ ๊ธฐ๋‹ค๋ฆฌ์ง€ ์•Š๋Š”๋‹ค
      expect(r, 'Submit ํ›„ /migrate/nrdbms ์š”์ฒญ ์ „์†ก(๋Œ€์ƒ MongoDB ์— DB ๊ฐ€ ์—†์œผ๋ฉด ๋Œ€์ƒ DB ๋ฅผ ๊ณ ๋ฅผ ์ˆ˜ ์—†์–ด ์š”์ฒญ์ด ๋‚˜๊ฐ€์ง€ ์•Š์Œ)').not.toBeNull();
      if (r) { note('submit-response', `${r.status()} ${(await r.text().catch(() => '')).slice(0, 800)}`); expect(r.status(), '/migrate/nrdbms ์‘๋‹ต ์ƒํƒœ').toBeLessThan(400); }
      const result = await waitResult(page, f, before, r ? 5 * 60_000 : 60_000);
      note('submit-api', api() || '(API ๊ธฐ๋ก ์—†์Œ)');
      expect(result, 'Result ์„ฑ๊ณต ๋ฉ”์‹œ์ง€').toMatch(/success|complete/i);
      expect(result, 'Result ์˜ค๋ฅ˜ ์—†์Œ').not.toMatch(/\bfail|\berror/i);
    });

    await test.step('5. Log Management(No-SQL) ์ด๋ ฅ', async () => {
      const rows = await logHistory(page, 'No-SQL', 'Migrate').catch(async () => logHistory(page, 'No-SQL', 'Migration').catch(() => [] as string[]));
      note('log', rows.slice(0, 3).join(' / ') || '(์ด๋ ฅ ์กฐํšŒ ์•ˆ ๋จ)');
    });
  });
}

IV8_S18

tests/iv8/IV8_S18.spec.ts ยท 42์ค„ ยท ํ•ด์‹œ 785837a0 ยท ์‹œ๋‚˜๋ฆฌ์˜ค ๋ณด๊ธฐ

import { test, expect } from '../../lib/fixtures';
import { activeCsps, tid } from '../../lib/data';
import { loginAsAdmin } from '../../lib/ui';
import { apiRecorder, openDm, selectCred, backupRowText } from '../../lib/iv8-os';
import { submitBackup, stepSummary } from '../../lib/iv8-rdb';
import { pickNrdbRow, mongoLoadDb, nrdbFlowOf, nrdbKind, NRDB_FLOW_CSPS } from '../../lib/iv8-nrdb';

// IV8_S18 : No-SQL: NRDBMS ๋ฐฑ์—… [์ •์ƒ, CSP๋ณ„] (TC-DATA-014)
// 1.Data Migrations > Back Up โ†’ No-SQL ํƒญ 2.Source: credential / Region โ†’ ์›๋ณธ ์„ ํƒ 3.Backup Name โ†’ Submit 4.Result ์„ฑ๊ณต ยท Backup List ์— ํ‘œ์‹œ
//   GCP(Firestore): nrdb1gcp ์„ ํƒ(Instance IDยทDatabase ID ์ž๋™) โ†’ 'mcmp-e2e-v07-bk3'
//   AlibabaยทNCP(MongoDB, 2026-10-15): ์›๋ณธ ์„ ํƒ โ†’ UsernameยทPassword โ†’ Load Databases โ†’ mcmpe2e ์„ ํƒ โ†’ 'mcmp-e2e-v07-bk3ali' / 'bk3ncp'
//   AWS(DynamoDB, 2026-10-15): ์ธ์Šคํ„ด์Šค ์—†์ด aws / ap-northeast-2 โ†’ 'mcmp-e2e-v07-bk3aws'
// ์„ ํ–‰: IV8_S16_<CSP>(์›๋ณธ ์ƒ˜ํ”Œ ๋ฐ์ดํ„ฐ). ๊ฐ™์€ ์ด๋ฆ„ ๋ฐฑ์—…์ด ์žˆ์œผ๋ฉด ์‹คํŒจ๋กœ ๋‚จ๊ธด๋‹ค(IV8_S20 ์—์„œ ์ •๋ฆฌ ํ›„ ์žฌ์‹คํ–‰).
// 2026-10-15: ์‚ฌ์šฉ์ž ๋Œ€์ƒํ‘œ(NRDB = AWSยทGCPยทAlibabaยทNCP), ๋ฐฑ์—… ์ด๋ฆ„์€ CSP ๋ณ„(nrdbFlowOf(c).backup)
for (const c of activeCsps(NRDB_FLOW_CSPS)) {
  test(`${tid('IV8_S18', c)} No-SQL: NRDBMS ๋ฐฑ์—…`, async ({ page }) => {
    test.setTimeout(40 * 60_000);
    const note = (t: string, d: string) => test.info().annotations.push({ type: t, description: d.slice(0, 1500) });
    const kind = nrdbKind(c);
    const { src, backup } = nrdbFlowOf(c);
    const api = apiRecorder(page);
    await loginAsAdmin(page);
    const f = await openDm(page, 'Back Up');
    await f.getByText(/No.?SQL/i).first().click();
    await selectCred(page, f, c, 'sourceCredentialSelect');
    await page.waitForTimeout(3000);
    expect(await backupRowText(f, backup), `${backup} ๊ฐ€ ์ด๋ฏธ Backup List ์— ์žˆ์Œ(์ •๋ฆฌ ํ•„์š”)`).toBe('');
    api();
    if (kind === 'firestore') await pickNrdbRow(page, f, src.id, '#nrdbms-gcp-databaseId');
    if (kind === 'mongo') {
      await pickNrdbRow(page, f, src.id, '#nosql-srcHost');
      expect(await mongoLoadDb(page, f, src, 'nosql-src', '#nrdb-load-db-btn', api), 'Backup ๋Œ€์ƒ Database ์„ ํƒ').not.toBe('');
    }
    note('form', await stepSummary(f, 'body'));
    const { result, row } = await submitBackup(page, f, backup, api);
    expect(result, 'Result ์„ฑ๊ณต ๋ฉ”์‹œ์ง€').toMatch(/success|complete/i);
    expect(result, 'Result ์˜ค๋ฅ˜ ์—†์Œ').not.toMatch(/\bfail|\berror/i);
    expect(row, 'Backup List ์— ๋ฐฑ์—… ํ‘œ์‹œ').toContain(backup);
    expect(row, '๋ฐฑ์—… ์ƒํƒœ').not.toMatch(/fail|error/i);
  });
}

IV8_S19

tests/iv8/IV8_S19.spec.ts ยท 61์ค„ ยท ํ•ด์‹œ 71e29ab0 ยท ์‹œ๋‚˜๋ฆฌ์˜ค ๋ณด๊ธฐ

import { test, expect } from '../../lib/fixtures';
import { activeCsps, tid } from '../../lib/data';
import { loginAsAdmin } from '../../lib/ui';
import { apiRecorder, openDm, selectCred, backupRowText, logHistory } from '../../lib/iv8-os';
import { resultText, waitResult, stepSummary, RDB_PW } from '../../lib/iv8-rdb';
import { pickNrdbRow, mongoLoadDb, nrdbFlowOf, nrdbKind, NRDB_FLOW_CSPS } from '../../lib/iv8-nrdb';

// IV8_S19 : No-SQL: NRDBMS ๋ณต๊ตฌ [์ •์ƒ, CSP๋ณ„] (TC-DATA-015)
// 1.Data Migrations > Restore โ†’ No-SQL ํƒญ 2.Backup List ์—์„œ ๊ทธ CSP ๋ฐฑ์—…(IV8_S18) ์„ ํƒ
// 3.Target: ๊ฐ™์€ CSP credential / Region โ†’ ๋ณต๊ตฌ ๋Œ€์ƒ ์„ ํƒ โ†’ Submit 4.ํ•˜๋‹จ Result ์„ฑ๊ณต 5.Log Management(No-SQL / Restore) ์ด๋ ฅ
//   AWS(DynamoDB): ์ธ์Šคํ„ด์Šค ์—†์ด credentialยทRegion ๋งŒ (2026-10-14 PASS ๋Š” GCP bk3 โ†’ AWS, 2026-10-15 ๋ถ€ํ„ฐ AWS bk3aws โ†’ AWS)
//   GCP(Firestore): bk3 โ†’ nrdb2gcp
//   AlibabaยทNCP(MongoDB, 2026-10-15): bk3ali โ†’ nrdb2ali / bk3ncp โ†’ mcmpndb2, ๋Œ€์ƒ ์„ ํƒ ํ›„ UsernameยทPassword
// ํด๋ผ์šฐ๋“œ ์ฝ˜์†” ๋ฐ์ดํ„ฐ ํ™•์ธ์€ ์ด spec ๋ฒ”์œ„ ๋ฐ–. ์„ ํ–‰: IV8_S18_<CSP>
// 2026-10-15: ์‚ฌ์šฉ์ž ๋Œ€์ƒํ‘œ(NRDB = AWSยทGCPยทAlibabaยทNCP) โ€” CSP ๋‹จ๋… ํ๋ฆ„
for (const c of activeCsps(NRDB_FLOW_CSPS)) {
  test(`${tid('IV8_S19', c)} No-SQL: NRDBMS ๋ณต๊ตฌ`, async ({ page }) => {
    test.setTimeout(40 * 60_000);
    const note = (t: string, d: string) => test.info().annotations.push({ type: t, description: d.slice(0, 1500) });
    const kind = nrdbKind(c);
    const { rst, backup } = nrdbFlowOf(c);
    const api = apiRecorder(page);
    await loginAsAdmin(page);
    const f = await openDm(page, 'Restore');
    await f.getByText(/No.?SQL/i).first().click();
    await page.waitForTimeout(3000);
    await expect.poll(() => backupRowText(f, backup), { timeout: 60_000 }).not.toBe('');
    const brow = await backupRowText(f, backup);
    expect(brow, `${backup} ๋ฐฑ์—… ์™„๋ฃŒ ์ƒํƒœ(IV8_S18 ์„ ํ–‰)`).toMatch(/completed/i);
    const row = f.locator(`tr.backup-row[data-name="${backup}"]`).first();
    await (await row.count() ? row : f.locator('table tbody tr').filter({ hasText: backup }).first()).locator('td', { hasText: backup }).first().click();
    await page.waitForTimeout(1500);
    note('backup-select', `${brow} | Source=${await f.locator('#sourcePointLabel').inputValue().catch(() => '')}`);
    await selectCred(page, f, c, 'targetCredentialSelect');
    await page.waitForTimeout(3000);
    if (kind !== 'dynamo') {
      await pickNrdbRow(page, f, rst.id, kind === 'mongo' ? '#nosql-srcHost' : undefined);
      if (kind === 'mongo') {
        // ๋Œ€์ƒ ๊ณ„์ • โ†’ Load Databases โ†’ mcmpe2e ์„ ํƒ. DB ๋ฅผ ๊ณ ๋ฅด์ง€ ์•Š์œผ๋ฉด Submit ํ•ด๋„ ์š”์ฒญ์ด ๋‚˜๊ฐ€์ง€ ์•Š๋Š”๋‹ค(2026-10-15, S17 ๊ณผ ๊ฐ™์€ ๋™์ž‘)
        expect(await mongoLoadDb(page, f, rst, 'nosql-src', '#nrdb-load-db-btn', api), '๋ณต๊ตฌ ๋Œ€์ƒ Database ์„ ํƒ').not.toBe('');
      }
    }
    note('form', await stepSummary(f, 'body'));
    api();
    const before = await resultText(page, f);
    const resp = page.waitForResponse((r) => /\/restore\/nrdbms/i.test(r.url()) && r.request().method() === 'POST', { timeout: 30 * 60_000 }).catch(() => null);
    await f.locator('#submitBtn').click();
    const first = await Promise.race([resp, page.waitForTimeout(60_000).then(() => 'none' as const)]);
    const r = first === 'none' ? null : first;
    expect(r, 'Submit ํ›„ /restore/nrdbms ์š”์ฒญ ์ „์†ก').not.toBeNull();
    note('submit-response', `${r!.status()} ${(await r!.text().catch(() => '')).slice(0, 800)}`);
    expect(r!.status(), '/restore/nrdbms ์‘๋‹ต ์ƒํƒœ').toBeLessThan(400);
    const result = await waitResult(page, f, before, 5 * 60_000);
    note('submit-api', api() || '(API ๊ธฐ๋ก ์—†์Œ)');
    expect(result, 'Result ์„ฑ๊ณต ๋ฉ”์‹œ์ง€').toMatch(/success|complete/i);
    expect(result, 'Result ์˜ค๋ฅ˜ ์—†์Œ').not.toMatch(/\bfail|\berror/i);
    const log = await logHistory(page, 'No-SQL', 'Restore').catch(() => [] as string[]);
    note('log', log.slice(0, 3).join(' / ') || '(์ด๋ ฅ ์กฐํšŒ ์•ˆ ๋จ)');
  });
}

IV8_S20

tests/iv8/IV8_S20.spec.ts ยท 65์ค„ ยท ํ•ด์‹œ 83d022b3 ยท ์‹œ๋‚˜๋ฆฌ์˜ค ๋ณด๊ธฐ

import { test, expect } from '../../lib/fixtures';
import { activeCsps, tid } from '../../lib/data';
import { loginAsAdmin } from '../../lib/ui';
import { apiRecorder, openDm, deleteBackup, backupRowText } from '../../lib/iv8-os';
import { deleteRdb, rdbRowText } from '../../lib/iv8-rdb';
import { openNosql, refreshNrdb, nrdbsOf, nrdbFlowOf, NRDB_CSPS } from '../../lib/iv8-nrdb';

// AWS(DynamoDB)๋Š” ์ธ์Šคํ„ด์Šค๊ฐ€ ์—†์–ด TC-DATA-021 ๋น„๋Œ€์ƒ โ€” IV8_S18_AWS ๋ฐฑ์—…(bk3aws)๋งŒ ์ •๋ฆฌํ•œ๋‹ค(๊ณ„ํš ์—‘์…€ ํ–‰ ์—†์Œ)
for (const c of activeCsps(['AWS'])) {
  test(`${tid('IV8_S20', c)} No-SQL: ๋ฐฑ์—… ์ •๋ฆฌ(AWS DynamoDB, ์ธ์Šคํ„ด์Šค ์—†์Œ)`, async ({ page }) => {
    const backup = nrdbFlowOf(c).backup;
    const api = apiRecorder(page);
    await loginAsAdmin(page);
    const r = await openDm(page, 'Restore');
    await r.getByText(/No.?SQL/i).first().click();
    await page.waitForTimeout(3000);
    if (await backupRowText(r, backup)) expect(await deleteBackup(page, r, backup, api), `${backup} ๋ฐฑ์—… ์‚ญ์ œ`).toBe(true);
    else test.info().annotations.push({ type: 'backup-missing', description: `${backup} Backup List ์— ์—†์Œ(์ด๋ฏธ ์ •๋ฆฌ๋จ)` });
    expect(await backupRowText(r, backup), `${backup} Backup List ์—์„œ ์ œ๊ฑฐ`).toBe('');
  });
}

// IV8_S20 : No-SQL: NRDB ์ธ์Šคํ„ด์Šค ์‚ญ์ œ (ํ…Œ์ŠคํŠธ ์ž์› ์ •๋ฆฌ) [์ •์ƒ, CSP๋ณ„ GCP] (TC-DATA-021)
// 1.Restore > No-SQL โ†’ Backup List ์—์„œ 'mcmp-e2e-v07-bk3'(IV8_S18) ์ฒดํฌ โ†’ Delete โ†’ ํ™•์ธ ํŒ์—…์— ๋ฐฑ์—… ์ด๋ฆ„ ์ž…๋ ฅ โ†’ Delete
// 2.Generate > No-SQL โ†’ 'GCP - gcp' / 'asia-northeast3' โ†’ Instance List ์—์„œ mcmp-e2e-v07-nrdb1gcp, nrdb2gcp ๊ฐ๊ฐ ์ฒดํฌ โ†’ Delete โ†’ ์ด๋ฆ„ ์ž…๋ ฅ โ†’ Delete
// 3.Instance ListยทBackup List ์—์„œ ์‚ฌ๋ผ์ง ํ™•์ธ
// (Drive ์ ˆ์ฐจ 2 ์˜ 'Generate > SQL Database' ๋Š” No-SQL ์ธ์Šคํ„ด์Šค๋ผ No-SQL ํƒญ์—์„œ ์‚ญ์ œํ•œ๋‹ค)
// 2026-10-15: ๋Œ€์ƒํ‘œ(NRDB) ์ค‘ ์ธ์Šคํ„ด์Šค๊ฐ€ ์žˆ๋Š” GCPยทAlibabaยทNCP, ๋ฐฑ์—… ์ด๋ฆ„์€ CSP ๋ณ„(nrdbFlowOf(c).backup)
for (const c of activeCsps(NRDB_CSPS)) {
  test(`${tid('IV8_S20', c)} No-SQL: NRDB ์ธ์Šคํ„ด์Šค ์‚ญ์ œ (ํ…Œ์ŠคํŠธ ์ž์› ์ •๋ฆฌ)`, async ({ page }) => {
    test.setTimeout(45 * 60_000);
    const NRDB_IDS = nrdbsOf(c).map((s) => s.id);
    const NRDB_BACKUP = nrdbFlowOf(c).backup;
    const api = apiRecorder(page);
    await loginAsAdmin(page);
    const r = await openDm(page, 'Restore');
    await r.getByText(/No.?SQL/i).first().click();
    await page.waitForTimeout(3000);
    api();
    if (await backupRowText(r, NRDB_BACKUP)) {
      expect(await deleteBackup(page, r, NRDB_BACKUP, api), `${NRDB_BACKUP} ๋ฐฑ์—… ์‚ญ์ œ`).toBe(true);
    } else test.info().annotations.push({ type: 'backup-missing', description: `${NRDB_BACKUP} Backup List ์— ์—†์Œ(์ด๋ฏธ ์ •๋ฆฌ๋จ)` });
    expect(await backupRowText(r, NRDB_BACKUP), `${NRDB_BACKUP} Backup List ์—์„œ ์ œ๊ฑฐ`).toBe('');

    for (const id of NRDB_IDS) {
      const f = await openNosql(page, c);
      api();
      // ๋ชฉ๋ก์ด ๋Šฆ๊ฒŒ ๊ทธ๋ ค์งˆ ์ˆ˜ ์žˆ์–ด ํ–‰์„ ์ตœ๋Œ€ 30์ดˆ ๊ธฐ๋‹ค๋ฆฐ๋‹ค(2026-10-14: 3์ดˆ๋งŒ ๋ณด๊ณ  '๋ชฉ๋ก์— ์—†์Œ'์œผ๋กœ ํŒ๋‹จํ•œ ์‚ฌ๋ก€). ์ด๋ฏธ ์ง€์›Œ์กŒ์œผ๋ฉด ๊ธฐ๋ก๋งŒ ๋‚จ๊ธด๋‹ค
      const seen = await expect.poll(() => rdbRowText(f, id), { timeout: 30_000, intervals: [3000] }).not.toBe('').then(() => true, () => false);
      if (seen) expect(await deleteRdb(page, f, { id } as any, api), `${id} ์‚ญ์ œ ์š”์ฒญ`).toBe(true);
      else test.info().annotations.push({ type: 'nrdb-missing', description: `${id} Instance List ์— ์—†์Œ(์ด๋ฏธ ์‚ญ์ œ๋จ)` });
    }
    const t0 = Date.now();
    const cur: Record<string, string> = Object.fromEntries(NRDB_IDS.map((id) => [id, 'pending']));
    while (NRDB_IDS.some((id) => cur[id]) && Date.now() - t0 < 35 * 60_000) {
      await page.waitForTimeout(20_000);
      for (const id of NRDB_IDS.filter((x) => cur[x])) {
        cur[id] = await refreshNrdb(page, c, id);
        test.info().annotations.push({ type: 'nrdb-poll', description: `${id} ${Math.round((Date.now() - t0) / 1000)}s: ${cur[id] || '(ํ–‰ ์—†์Œ)'}` });
      }
    }
    for (const id of NRDB_IDS) expect(cur[id], `${id} ๋ชฉ๋ก์—์„œ ์ œ๊ฑฐ`).toBe('');
  });
}
โš ๏ธ **GitHub.com Fallback** โš ๏ธ