Files
DongHyeonka 7289ce97bb fix: stop the smoke sweep reporting an expected 404 as a failure
A document with no preview yet answers 404 when the screen asks for its current
one, and the screen turns that into "make a preview". The sweep counted it as a
failure, so every run ended with the same red line under a healthy deployment.

A check that cries wolf on every run stops being read, and a real failure would
have sat unnoticed beside it. The session probe's 401 before sign-in is the
same kind of expected answer and is excluded on the same terms; every other
4xx and 5xx still fails the sweep.
2026-08-21 18:00:14 +09:00

112 lines
5.4 KiB
TypeScript

/**
* 배포본 전수 확인.
*
* 이 파일은 절차 실패에서 나왔다 — 고친 화면만 확인하고 배포해서, 나머지가 깨진 것은 매번
* 사용자가 먼저 발견했다. 운영 환경이므로 배포 전에 모든 화면을 한 번씩 열어 보는 것이 맞다.
*
* 각 화면에서 보는 것: main 이 그려졌는지, 콘솔 오류, 4xx/5xx API 응답, 그리고 화면에 뜬
* 오류 문구. 하나라도 있으면 그 화면을 실패로 적고 끝까지 진행한다.
*
* SPW=<비밀번호> node scripts/smoke/production-sweep.mjs [origin]
*/
import process from "node:process";
import { chromium, type Page } from "@playwright/test";
const ORIGIN = process.argv[2] ?? "https://hyeonworks.com";
const PW = process.env.SPW;
const ERROR_TEXT =
/요청을 처리하지 못했습니다|지원 정보 확인|표시할 수 없습니다|불러오지 못했습니다|화면을 찾을 수 없습니다|Not Found/;
const results: { label: string; path: string; problems: string[] }[] = [];
async function visit(
page: Page,
label: string,
path: string,
{ expectMain = true }: { expectMain?: boolean } = {},
) {
const problems: string[] = [];
const onConsole = (m: { type(): string; text(): string }) => { if (m.type() === "error" && !/401/.test(m.text())) problems.push(`console: ${m.text().slice(0, 120)}`); };
const onResponse = (r: {
url(): string;
status(): number;
request(): { method(): string };
}) => {
const u = new URL(r.url()).pathname;
if (r.status() < 400 || !u.startsWith("/api")) return;
// 두 가지는 화면이 다루는 정상 상태다: 로그인 전 세션 탐침의 401, 그리고 아직 미리보기를
// 만들지 않은 문서의 404. 이것들을 실패로 세면 매번 같은 줄이 뜨고, 진짜 실패가 그 사이에
// 묻힌다 — 늑대가 왔다고 매번 외치는 점검은 아무도 읽지 않는다.
if (u.includes("/studio/session")) return;
if (r.status() === 404 && r.request().method() === "GET" && u.endsWith("/preview")) return;
problems.push(`${r.status()} ${r.request().method()} ${u}`);
};
page.on("console", onConsole);
page.on("response", onResponse);
try {
await page.goto(ORIGIN + path, { waitUntil: "domcontentloaded", timeout: 45000 });
await page.waitForTimeout(3000);
const main = await page.locator("main").count();
const body = (await page.locator("body").innerText().catch(() => "")).replace(/\s+/g, " ");
if (expectMain && main === 0) problems.push("main 없음");
const shown = body.match(ERROR_TEXT);
if (shown) problems.push(`화면 문구: ${shown[0]}`);
} catch (error) {
problems.push(`이동 실패: ${String(error).slice(0, 90)}`);
} finally {
page.off("console", onConsole);
page.off("response", onResponse);
}
results.push({ label, path, problems });
console.log(`${problems.length ? "✗" : "✓"} ${label.padEnd(22)} ${path}`);
for (const p of problems) console.log(` ${p}`);
}
const browser = await chromium.launch();
const page = await (await browser.newContext()).newPage();
console.log("=== 공개 ===");
for (const [label, path] of [
["홈", "/"], ["탐색", "/explore"], ["Case 목록", "/explore/cases"],
["프로젝트", "/projects"], ["변경 기록", "/releases"], ["릴리즈 상세", "/releases/0.1.0"],
["검색", "/search"], ["프로필", "/profile"],
]) await visit(page, label, path);
if (!PW) { console.log("\n(SPW 없음 — Studio 생략)"); await browser.close(); process.exit(0); }
console.log("\n=== 로그인 ===");
await page.goto(ORIGIN + "/studio", { waitUntil: "domcontentloaded", timeout: 60000 });
await page.waitForTimeout(2500);
const start = page.getByRole("button", { name: /로그인 시작/ }).or(page.getByRole("link", { name: /로그인 시작/ }));
if (await start.count()) { await start.first().click(); await page.waitForTimeout(5000); }
await page.fill("#username", "hyeonworks");
await page.fill("#password", PW);
await page.click("#kc-login, input[type=submit], button[type=submit]");
await page.waitForTimeout(6000);
console.log(" 로그인 후:", page.url().replace(ORIGIN, "") || "/");
console.log("\n=== Studio ===");
for (const [label, path] of [
["대시보드", "/studio"], ["작업본", "/studio/documents"], ["새 문서", "/studio/documents/new"],
["게시 기록", "/studio/publications"], ["Asset", "/studio/assets"],
["주제·프로젝트", "/studio/taxonomy"], ["릴리즈", "/studio/releases"],
]) await visit(page, label, path);
// 작업본 하나를 골라 편집·검증·미리보기까지 연다
await page.goto(ORIGIN + "/studio/documents", { waitUntil: "domcontentloaded" });
await page.waitForTimeout(3000);
const href = await page.locator("a[href*='/studio/documents/'][href$='/edit']").first().getAttribute("href").catch(() => null);
if (href) {
const id = href.split("/")[3];
console.log("\n=== 문서 흐름 ===", id);
for (const [label, suffix] of [["편집", "/edit"], ["검증", "/validation"], ["미리보기", "/preview"], ["게시", "/publish"]])
await visit(page, label, `/studio/documents/${id}${suffix}`);
} else console.log("\n(편집 링크를 찾지 못해 문서 흐름 생략)");
await browser.close();
const failed = results.filter((r) => r.problems.length);
console.log(`\n=== 결과 === ${results.length - failed.length}/${results.length} 통과`);
for (const r of failed) console.log(` ✗ ${r.label} (${r.path}): ${r.problems.join(" | ").slice(0, 160)}`);
process.exit(failed.length ? 1 : 0);