Skip to content

Commit 43961c1

Browse files
committed
브랜드 게이트: 드리프트와 조용한 파싱 실패 차단
브랜드가 파일 참조가 되면서 실패가 전부 조용해졌다. 셋 다 실제로 밟았다: SVG는 XML이라 주석의 연속 하이픈 하나로 파싱 불가가 되고(브라우저가 에러 없이 이미지를 통째로 버린다), CSS 주석이 조기에 닫히면 :root 블록이 무효가 되며(색만 사라진다), 서버 MIME이 틀리면 이미지가 안 그려진다. - tests/run.mjs [브랜드]: demo.css의 색이 마크 실측색과 일치하는지, 페이지가 마크 정본을 참조하는지(인라인 복제 금지), pages.yml이 assets를 배포하는지, logo.svg 주석에 연속 하이픈이 없는지, demo.css 주석이 조기 종료되지 않는지, var 참조가 전부 선언과 짝인지. - tests/browser/brandGate.html: 마크가 실제로 디코드되고(naturalWidth > 0) 팔레트가 실제로 적용되는지를 브라우저에서 확인한다. 정적 검사만으로는 파싱 실패를 끝까지 못 막는다. - examples.mjs가 예제 5쪽보다 먼저 이 게이트를 연다(파이썬을 안 띄우므로 몇 초면 끝난다).
1 parent 0f2143d commit 43961c1

3 files changed

Lines changed: 110 additions & 1 deletion

File tree

tests/browser/brandGate.html

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<!DOCTYPE html>
2+
<html lang="ko">
3+
<head>
4+
<meta charset="utf-8">
5+
<title>pyproc 브랜드 게이트</title>
6+
<link rel="stylesheet" href="../../examples/demo.css">
7+
</head>
8+
<body>
9+
<h1>pyproc 브랜드 게이트</h1>
10+
<p>브랜드 자산(마크 + 팔레트)이 <em>진짜로 그려지는지</em> 검증한다. 이 층의 실패는 전부 조용하다:
11+
SVG는 XML이라 주석 하나로 파싱 불가가 되고(브라우저가 에러 없이 이미지를 버린다), CSS 주석이
12+
조기에 닫히면 :root 블록이 통째로 무효가 되며(색만 사라진다), 서버 MIME이 틀리면 이미지가
13+
안 그려진다. 어느 것도 정적 검사로는 끝까지 못 막는다. 그래서 실제로 띄워서 본다.</p>
14+
<img id="mark" src="../../assets/logo.svg" width="64" height="64" alt="pyproc">
15+
<span class="badge ok">ok</span><span class="badge info">info</span>
16+
<pre id="out">실행 중...</pre>
17+
<script type="module">
18+
const out = document.getElementById("out");
19+
const checks = [];
20+
const check = (name, pass, info = "") => {
21+
checks.push({ name, pass: !!pass, info: String(info) });
22+
out.textContent += `\n${pass ? "PASS" : "FAIL"} ${name}${info ? " (" + info + ")" : ""}`;
23+
};
24+
const rgb = (el, prop) => getComputedStyle(el)[prop];
25+
26+
// 1) 마크가 이미지로 디코드된다 = SVG가 XML로 파싱됐고 MIME도 맞다.
27+
const mark = document.getElementById("mark");
28+
await mark.decode().then(() => {}, () => {});
29+
check("마크가 디코드된다(SVG 파싱 + MIME)", mark.naturalWidth > 0,
30+
`naturalWidth=${mark.naturalWidth}`);
31+
32+
// 2) 팔레트가 적용된다 = demo.css가 파싱됐고 :root 변수가 살아있다.
33+
// 기대값은 demo.css의 정의와 같아야 한다(--bg #0a0f1c, --teal #00d4c8, --blue #5b8cff).
34+
const bg = rgb(document.body, "backgroundColor");
35+
check("배경이 브랜드 네이비(--bg)", bg === "rgb(10, 15, 28)", bg);
36+
const ok = rgb(document.querySelector(".badge.ok"), "color");
37+
check("성공색이 브랜드 틸(--teal)", ok === "rgb(0, 212, 200)", ok);
38+
const info = rgb(document.querySelector(".badge.info"), "color");
39+
check("정보색이 브랜드 블루(--blue)", info === "rgb(91, 140, 255)", info);
40+
41+
const ok2 = checks.length > 0 && checks.every((c) => c.pass);
42+
out.textContent = (ok2 ? "게이트 GREEN\n" : "게이트 RED\n") + out.textContent;
43+
try {
44+
await fetch("/gateReport", {
45+
method: "POST", headers: { "Content-Type": "application/json" },
46+
body: JSON.stringify({ ok: ok2, checks }),
47+
});
48+
} catch (e) {}
49+
</script>
50+
</body>
51+
</html>

tests/browser/examples.mjs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@ import { createStaticServer } from "../../examples/serve.mjs";
1111
import { findBrowser, headlessArgs } from "./harness.mjs";
1212

1313
const TIMEOUT_MS = Number(process.env.PYPROC_GATE_TIMEOUT || 240000);
14-
const PAGES = ["examples/basic.html", "examples/agentSandbox.html", "examples/terminal.html", "examples/machine.html", "examples/processOs.html"];
14+
// brandGate: 예제가 쓰는 브랜드 자산(마크 SVG + demo.css 팔레트)이 실제로 그려지는지 먼저 본다.
15+
// 이 층의 실패는 조용하다(파싱 실패 = 이미지가 사라지고, 색만 초기값으로 돌아간다). 파이썬을
16+
// 안 띄우므로 몇 초면 끝난다: 예제 5쪽을 다 돌리기 전에 진열장이 깨졌는지부터 알려준다.
17+
const PAGES = ["tests/browser/brandGate.html", "examples/basic.html", "examples/agentSandbox.html", "examples/terminal.html", "examples/machine.html", "examples/processOs.html"];
1518

1619
const browser = findBrowser();
1720
let resolveReport = null;

tests/run.mjs

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,61 @@ for (const f of collect(join(ROOT, "examples"), [".html"], [])) {
194194
});
195195
}
196196

197+
// 3.7) 브랜드: 마크 정본은 assets/logo.svg 하나다. 파비콘·헤더 로고·색이 여기서만 나온다.
198+
// 마크를 인라인으로 복제하거나(6쪽이 갈라진다), 마크와 CSS 색이 어긋나는 드리프트를 차단한다.
199+
console.log("\n[브랜드]");
200+
const logoSvg = readFileSync(join(ROOT, "assets", "logo.svg"), "utf8");
201+
const cssSrc = readFileSync(join(ROOT, "examples", "demo.css"), "utf8");
202+
const markColors = {
203+
// 마크의 그라디언트 양 끝과 터미널 패널 색 = 브랜드 색의 출처.
204+
markFrom: logoSvg.match(/<stop offset="0%" stop-color="(#[0-9a-f]{6})"\/>/)?.[1],
205+
markTo: logoSvg.match(/<stop offset="100%" stop-color="(#[0-9a-f]{6})"\/>\s*<\/linearGradient>/)?.[1],
206+
ink: logoSvg.match(/<path [^>]*fill="(#[0-9a-f]{6})"\/>/g)?.map((m) => m.match(/fill="(#[0-9a-f]{6})"/)[1])[0],
207+
};
208+
for (const [name, color] of Object.entries(markColors)) {
209+
check(`demo.css --${name}이 마크 실측색(${color})과 일치`, () => {
210+
if (!color) throw new Error("logo.svg에서 색을 못 읽음(마크 구조 변경?)");
211+
const declared = cssSrc.match(new RegExp(`--${name}:\\s*(#[0-9a-f]{6})`))?.[1];
212+
if (declared !== color) throw new Error(`demo.css는 ${declared}, 마크는 ${color}`);
213+
});
214+
}
215+
const landing = readFileSync(join(ROOT, "examples", "index.html"), "utf8");
216+
for (const f of collect(join(ROOT, "examples"), [".html"], [])) {
217+
const html = readFileSync(f, "utf8");
218+
const prefix = html === landing ? "assets/" : "../assets/"; // 랜딩만 배포 루트로 승격된다
219+
check(`마크 참조 고정: ${rel(f)}`, () => {
220+
if (!html.includes(`<link rel="icon" href="${prefix}logo.svg">`)) throw new Error("파비콘이 마크 정본을 안 씀");
221+
if (!html.includes(`<img class="logoMark" src="${prefix}logo.svg"`)) throw new Error("헤더 로고가 마크 정본을 안 씀");
222+
if (/<svg[^>]*class="logoMark"/.test(html)) throw new Error("마크 인라인 복제(SSOT 우회)");
223+
if (/rel="icon" href="data:/.test(html)) throw new Error("파비콘 data URI 복제(SSOT 우회)");
224+
});
225+
}
226+
check("pages.yml이 assets를 배포(안 그러면 파비콘·로고가 404)", () => {
227+
const pages = readFileSync(join(ROOT, ".github", "workflows", "pages.yml"), "utf8");
228+
if (!/cp -r [^\n]*\bassets\b/.test(pages)) throw new Error("assets 복사 없음");
229+
});
230+
// SVG는 XML이다: 주석 안의 연속 하이픈은 XML이 금지한다. 어기면 마크가 파싱 불가가 되어
231+
// 브라우저가 에러 한 줄 없이 이미지를 통째로 버린다(파비콘·헤더 로고가 동시에 사라진다).
232+
check("logo.svg 주석에 연속 하이픈 없음(XML 위반 = 마크 소멸)", () => {
233+
for (const c of logoSvg.match(/<!--[\s\S]*?-->/g) || []) {
234+
if (c.slice(4, -3).includes("--")) throw new Error("주석 본문에 연속 하이픈: XML 파싱 불가");
235+
}
236+
});
237+
// 주석 본문에 종료 기호가 섞이면 주석이 거기서 닫히고, 뒤따르는 문장이 선택자로 먹혀
238+
// :root 블록이 통째로 무효가 된다(색이 전부 사라지는데 에러는 없다). CSS 파서와 같은 방식으로
239+
// (여는 기호부터 첫 종료 기호까지) 주석을 걷어낸 뒤, 코드에 종료 기호가 남으면 조기 종료다.
240+
check("demo.css 주석 무결성(조기 종료가 시트를 무력화)", () => {
241+
const code = cssSrc.replace(/\/\*[\s\S]*?\*\//g, "");
242+
if (code.includes("*/")) throw new Error("주석 밖에 종료 기호가 남음: 주석 본문이 주석을 조기에 닫았다");
243+
if (code.includes("/*")) throw new Error("닫히지 않은 주석");
244+
});
245+
// 이름을 바꾼 변수를 어딘가 놓치면 그 자리만 색이 사라진다(계산 시점 무효 -> 초기값). 참조는 전부 해석돼야 한다.
246+
check("demo.css의 var(--x) 참조가 전부 선언과 짝", () => {
247+
const declared = new Set([...cssSrc.matchAll(/(--[a-zA-Z][\w-]*)\s*:/g)].map((m) => m[1]));
248+
const missing = [...new Set([...cssSrc.matchAll(/var\((--[\w-]+)/g)].map((m) => m[1]))].filter((v) => !declared.has(v));
249+
if (missing.length) throw new Error("선언 없는 변수 참조: " + missing.join(", "));
250+
});
251+
197252
// 4) 타입 선언: 소비자(TypeScript)용 index.d.ts가 공개 표면을 전부 덮는가.
198253
console.log("\n[타입]");
199254
const dts = readFileSync(join(ROOT, "index.d.ts"), "utf8");

0 commit comments

Comments
 (0)