SSOT 대시보드:
- 한신대 기술분석 PDF(19쪽) 정합성 분석 + 이번 세션 발견 섹션 추가
- 섹션 폴드아웃(접기)·상단 목차(드릴다운)·모두 펼치기/접기 — 내용 보존, 레이아웃만 정리
페르소나 반응 강화('저항·반응 조절' 핵심 차별):
- PersonaCard.triggers(역린) 필드 + CCD 핵심상처 파생 역린 블록
- L0에 무례·모욕·조롱 시 현실적 동맹 균열 반응 지침
버그·성능 수정(라이브/E2E로 포착):
- 게이트웨이 페르소나 격리: --append-system-prompt를 --system-prompt(교체)로 + --exclude-dynamic-system-prompt-sections (내담자 캐릭터 붕괴·개발맥락 누출 차단)
- RAG: 임베더 동기 로드(약 7-13초)를 _warm_rag_caches 백그라운드 warm으로(세션 생성 블로킹 회귀 수정)
- voice TTS RMS 데드힌트 제거, init_state OpennessParams 파라미터객체화
- 한국어 PII(날짜·금액·주소) 마스킹 보강
- 레이아웃 시각 게이트: 폼 컨트롤 값 스크롤 오탐 제외(7/7)
검증: 백엔드 84/84, E2E 42(데스크톱 27·모바일 11·아바타 4), 시각 게이트 7/7
101 lines
4.2 KiB
TypeScript
101 lines
4.2 KiB
TypeScript
import path from "node:path";
|
|
import { pathToFileURL } from "node:url";
|
|
import { expect, test } from "@playwright/test";
|
|
|
|
const PERSONA_CODES = ["p4", "p5", "p6", "p7"] as const;
|
|
|
|
test.describe("first-party Live2D assets", () => {
|
|
test("serves model3 manifests and exp3 expressions for every persona", async ({ request }) => {
|
|
const indexResponse = await request.get("/live2d/personas/index.json");
|
|
expect(indexResponse.ok(), await indexResponse.text()).toBeTruthy();
|
|
const index = (await indexResponse.json()) as {
|
|
personas: { code: string; model3: string; expressionCount: number }[];
|
|
};
|
|
|
|
expect(index.personas).toHaveLength(4);
|
|
|
|
for (const code of PERSONA_CODES) {
|
|
const modelResponse = await request.get(`/live2d/personas/${code}/${code}.model3.json`);
|
|
expect(modelResponse.ok(), await modelResponse.text()).toBeTruthy();
|
|
const model = (await modelResponse.json()) as {
|
|
Version: number;
|
|
Vignette: { ModelId: string; PersonaCode: string };
|
|
FileReferences: { Expressions: { Name: string; File: string }[] };
|
|
};
|
|
|
|
expect(model.Version).toBe(3);
|
|
expect(model.Vignette.ModelId).toBe(`vignette-${code}-live2d`);
|
|
expect(model.Vignette.PersonaCode.toLowerCase()).toBe(code);
|
|
expect(model.FileReferences.Expressions.length).toBeGreaterThanOrEqual(20);
|
|
expect(model.FileReferences.Expressions.map((expression) => expression.Name)).toEqual(
|
|
expect.arrayContaining(["joy", "sad", "angry", "rage"]),
|
|
);
|
|
|
|
const rage = model.FileReferences.Expressions.find((expression) => expression.Name === "rage");
|
|
expect(rage, `${code} rage expression`).toBeTruthy();
|
|
const expressionResponse = await request.get(`/live2d/personas/${code}/${rage!.File}`);
|
|
expect(expressionResponse.ok(), await expressionResponse.text()).toBeTruthy();
|
|
const expression = (await expressionResponse.json()) as {
|
|
Type: string;
|
|
Version: number;
|
|
FadeInTime: number;
|
|
FadeOutTime: number;
|
|
Parameters: { Id: string; Value: number; Blend: string }[];
|
|
};
|
|
|
|
expect(expression.Type).toBe("Live2D Expression");
|
|
expect(expression.Version).toBe(3);
|
|
expect(expression.FadeInTime).toBe(0.26);
|
|
expect(expression.FadeOutTime).toBe(0.32);
|
|
expect(expression.Parameters.length).toBeGreaterThanOrEqual(12);
|
|
expect(expression.Parameters.map((parameter) => parameter.Id)).toEqual(
|
|
expect.arrayContaining(["ParamMouthOpenY", "ParamMouthForm", "ParamEyeLOpen"]),
|
|
);
|
|
}
|
|
});
|
|
|
|
test("keeps legacy demo paths blocked while allowing first-party persona assets at the Pages function", async () => {
|
|
const functionPath = path.resolve("functions", "live2d", "[[path]].js");
|
|
const { onRequest } = (await import(pathToFileURL(functionPath).href)) as {
|
|
onRequest: (context: {
|
|
request: Request;
|
|
params: { path: string[] };
|
|
env: { ASSETS: { fetch: (request: Request) => Promise<Response> } };
|
|
}) => Promise<Response>;
|
|
};
|
|
|
|
const allowed = await onRequest({
|
|
request: new Request("https://vignette.example/live2d/personas/p4/p4.model3.json"),
|
|
params: { path: ["personas", "p4", "p4.model3.json"] },
|
|
env: {
|
|
ASSETS: {
|
|
fetch: async () => new Response("{}", { status: 200, headers: { "content-type": "application/json" } }),
|
|
},
|
|
},
|
|
});
|
|
expect(allowed.status).toBe(200);
|
|
expect(allowed.headers.get("x-robots-tag")).toBe("noindex");
|
|
|
|
const legacy = await onRequest({
|
|
request: new Request("https://vignette.example/live2d/mao/Mao.model3.json"),
|
|
params: { path: ["mao", "Mao.model3.json"] },
|
|
env: {
|
|
ASSETS: {
|
|
fetch: async () => new Response("should not be called", { status: 200 }),
|
|
},
|
|
},
|
|
});
|
|
expect(legacy.status).toBe(404);
|
|
|
|
const traversal = await onRequest({
|
|
request: new Request("https://vignette.example/live2d/personas/p4/../mao/Mao.model3.json"),
|
|
params: { path: ["personas", "p4", "..", "mao", "Mao.model3.json"] },
|
|
env: {
|
|
ASSETS: {
|
|
fetch: async () => new Response("should not be called", { status: 200 }),
|
|
},
|
|
},
|
|
});
|
|
expect(traversal.status).toBe(404);
|
|
});
|
|
});
|