NPC 명령의 실제 검사와 준비 및 대체 실행 순서를 감사 기록에 연결
This commit is contained in:
@@ -59,6 +59,7 @@ const decision = {
|
||||
summary: {
|
||||
schemaVersion: 1,
|
||||
coverage: 'PROCEDURES',
|
||||
executionCoverage: 'ATTEMPTS',
|
||||
clockRevision: 1,
|
||||
codeVersion: null,
|
||||
policyRefs: { DEFENCE: 'a'.repeat(64) },
|
||||
@@ -71,7 +72,12 @@ const decision = {
|
||||
blockedReason: '자원 부족',
|
||||
},
|
||||
};
|
||||
const install = async (page: Page, denied = false, baseline: boolean | 'document' | 'created' | 'removed' = false) => {
|
||||
const install = async (
|
||||
page: Page,
|
||||
denied = false,
|
||||
baseline: boolean | 'document' | 'created' | 'removed' = false,
|
||||
executionStatus?: 'PREPARING' | 'BLOCKED'
|
||||
) => {
|
||||
const requests: { operation: string; input: Record<string, unknown> }[] = [];
|
||||
await page.addInitScript((profile) => {
|
||||
localStorage.setItem('sammo-game-token', 'ga_audit');
|
||||
@@ -109,6 +115,7 @@ const install = async (page: Page, denied = false, baseline: boolean | 'document
|
||||
items: [
|
||||
{
|
||||
...decision,
|
||||
summary: { ...decision.summary, executionStatus },
|
||||
id: input.cursor ? 'c'.repeat(64) : decision.id,
|
||||
phase: input.cursor ? 'nation' : 'general',
|
||||
},
|
||||
@@ -118,7 +125,7 @@ const install = async (page: Page, denied = false, baseline: boolean | 'document
|
||||
case 'playAudit.decisionDetail':
|
||||
return result({
|
||||
...world,
|
||||
decision,
|
||||
decision: { ...decision, summary: { ...decision.summary, executionStatus } },
|
||||
chunks: [
|
||||
{
|
||||
ordinal: input.cursor === undefined ? 0 : 1,
|
||||
@@ -135,7 +142,25 @@ const install = async (page: Page, denied = false, baseline: boolean | 'document
|
||||
sequence: input.cursor === undefined ? 0 : 128,
|
||||
...(input.cursor === undefined
|
||||
? { kind: 'PROCEDURE_START', procedure: '<b>징병판정</b>' }
|
||||
: { kind: 'DECISION_END', action: 'che_징병', reason: '징병 선택' }),
|
||||
: {
|
||||
kind: 'EXECUTION_ATTEMPT',
|
||||
attempt: 0,
|
||||
requestedAction: 'che_징병',
|
||||
resolvedAction: 'che_징병',
|
||||
executedAction: '휴식',
|
||||
completed: true,
|
||||
usedFallback: true,
|
||||
alternativeAction: null,
|
||||
preparation: null,
|
||||
checks: [
|
||||
{
|
||||
stage: 'CONSTRAINT',
|
||||
action: 'che_징병',
|
||||
result: 'deny',
|
||||
reason: '<b>자원 부족</b>',
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -1106,7 +1131,15 @@ test('NPC decisions are explicit, paginated, independently addressable and escap
|
||||
await expect(page.getByRole('list', { name: '판단 절차' })).toContainText('<b>징병판정</b>');
|
||||
await expect(page.getByRole('list', { name: '판단 절차' }).locator('b')).toHaveCount(0);
|
||||
await page.getByRole('button', { name: '판단 절차 더 불러오기', exact: true }).click();
|
||||
await expect(page.getByRole('list', { name: '판단 절차' })).toContainText('최종 선택');
|
||||
await expect(page.getByRole('list', { name: '판단 절차' })).toContainText('실행 시도 1');
|
||||
await expect(page.getByRole('list', { name: '판단 절차' })).toContainText(
|
||||
'조건 · che_징병 · 차단 · <b>자원 부족</b>'
|
||||
);
|
||||
await expect(page.getByRole('list', { name: '판단 절차' }).locator('b')).toHaveCount(0);
|
||||
await expect(page.getByRole('list', { name: '판단 절차' }).locator(':scope > li').last()).toHaveAttribute(
|
||||
'value',
|
||||
'129'
|
||||
);
|
||||
expect(requests.filter((r) => r.operation === 'playAudit.decisionDetail').at(-1)?.input).toMatchObject({
|
||||
id: decision.id,
|
||||
generalId: 1,
|
||||
@@ -1181,3 +1214,17 @@ test('NPC decision opens its immutable policy without querying policy history',
|
||||
expect(requests.filter((r) => r.operation === 'playAudit.policyVersion')).toHaveLength(policyReads);
|
||||
await expect(page.getByRole('heading', { name: /선택 정책 버전/ })).toHaveCount(0);
|
||||
});
|
||||
|
||||
for (const [status, label] of [
|
||||
['PREPARING', '준비 중'],
|
||||
['BLOCKED', '실행 차단'],
|
||||
] as const) {
|
||||
test(`NPC execution ${status} is distinguished from a failed execution`, async ({ page }) => {
|
||||
await install(page, false, false, status);
|
||||
await page.goto(gamePath(`/play-audit?tab=generals&general=1&decision=${decision.id}`));
|
||||
const region = page.getByRole('region', { name: 'NPC 결정 기록', exact: true });
|
||||
await expect(region.getByRole('table')).toContainText(label);
|
||||
await expect(region.getByRole('region', { name: '선택 결정 상세', exact: true })).toContainText(label);
|
||||
await expect(region).not.toContainText('실행 실패');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -79,15 +79,34 @@ const loadDetail = async (more = false) => {
|
||||
}
|
||||
};
|
||||
const select = (id: string | null) => router.push({ query: { ...route.query, decision: id ?? undefined } });
|
||||
const outcome = (done: boolean | null) => (done === null ? '결과 미관측' : done ? '실행 완료' : '실행 실패');
|
||||
const outcome = (done: boolean | null, status?: 'PREPARING' | 'BLOCKED' | 'RESOLVED') =>
|
||||
status === 'PREPARING'
|
||||
? '준비 중'
|
||||
: status === 'BLOCKED'
|
||||
? '실행 차단'
|
||||
: done === null
|
||||
? '결과 미관측'
|
||||
: done
|
||||
? '실행 완료'
|
||||
: '실행 실패';
|
||||
const rngValue = (value: Extract<Step, { kind: 'RNG' }>['result']): string => {
|
||||
if (Array.isArray(value)) return value.map(rngValue).join(', ');
|
||||
if (value === null) return '없음';
|
||||
if (typeof value === 'object') return 'entityId' in value ? `대상 #${value.entityId}` : '상세 값 미수집';
|
||||
return String(value);
|
||||
};
|
||||
const checkLabels = {
|
||||
ARGS: '인자',
|
||||
CONSTRAINT: '조건',
|
||||
COOLDOWN: '재사용 대기',
|
||||
CONTEXT: '실행 문맥',
|
||||
BLOCK: '실행 제한',
|
||||
};
|
||||
const checkResultLabels = { allow: '통과', deny: '차단', unknown: '미확인' };
|
||||
const stepText = (step: Step): string => {
|
||||
switch (step.kind) {
|
||||
case 'EXECUTION_ATTEMPT':
|
||||
return `실행 시도 ${step.attempt + 1} · 요청 ${step.requestedAction} · 처리 ${step.resolvedAction} → 실행 ${step.executedAction ?? '미실행'} · ${step.preparation ? '준비 중' : outcome(step.completed)}${step.usedFallback ? ' · 대체 실행' : ''}${step.alternativeAction ? ` · 다음 대안 ${step.alternativeAction}` : ''}${step.preparation ? ` · 준비 ${step.preparation.term}/${step.preparation.total}` : ''}`;
|
||||
case 'DECISION_START':
|
||||
return `판단 시작 · 예약 ${step.reservedAction}`;
|
||||
case 'DECISION_END':
|
||||
@@ -165,7 +184,8 @@ watch(
|
||||
<td>{{ item.npcState < 2 ? '유저 자동턴' : item.npcState === 5 ? '부대장 NPC' : 'NPC' }}</td>
|
||||
<td>{{ item.summary.selectedAction ?? '선택 없음' }} → {{ item.summary.executedAction }}</td>
|
||||
<td>
|
||||
{{ outcome(item.summary.completed) }}{{ item.summary.usedFallback ? ' · 대체 실행' : '' }}
|
||||
{{ outcome(item.summary.completed, item.summary.executionStatus)
|
||||
}}{{ item.summary.usedFallback ? ' · 대체 실행' : '' }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@@ -182,6 +202,9 @@ watch(
|
||||
<button class="legacy-button" @click="loadDetail(Boolean(detail))">결정 상세 다시 조회</button>
|
||||
</p>
|
||||
<template v-if="detail">
|
||||
<p v-if="!detail.decision.summary.executionCoverage">
|
||||
이 기록에는 실행 단계의 시도 이력이 수집되지 않았습니다.
|
||||
</p>
|
||||
<p>
|
||||
{{ detail.decision.year }}년 {{ detail.decision.month }}월 · 국가 #{{ detail.decision.nationId }} ·
|
||||
도시 #{{ detail.decision.cityId }} · tick {{ detail.decision.tick }}
|
||||
@@ -193,7 +216,7 @@ watch(
|
||||
</p>
|
||||
<p>
|
||||
선택 사유: {{ detail.decision.summary.selectedReason ?? '미관측' }} ·
|
||||
{{ outcome(detail.decision.summary.completed) }}
|
||||
{{ outcome(detail.decision.summary.completed, detail.decision.summary.executionStatus) }}
|
||||
</p>
|
||||
<p v-if="detail.decision.summary.blockedReason">
|
||||
차단 사유: {{ detail.decision.summary.blockedReason }}
|
||||
@@ -222,6 +245,12 @@ watch(
|
||||
<template v-for="chunk in detail.chunks" :key="chunk.ordinal"
|
||||
><li v-for="step in chunk.steps" :key="step.sequence" :value="step.sequence + 1">
|
||||
{{ stepText(step) }}
|
||||
<ul v-if="step.kind === 'EXECUTION_ATTEMPT'">
|
||||
<li v-for="(check, index) in step.checks" :key="index">
|
||||
{{ checkLabels[check.stage] }} · {{ check.action }} ·
|
||||
{{ checkResultLabels[check.result] }}{{ check.reason ? ` · ${check.reason}` : '' }}
|
||||
</li>
|
||||
</ul>
|
||||
</li></template
|
||||
>
|
||||
</ol>
|
||||
|
||||
Reference in New Issue
Block a user