NPC 명령의 실제 검사와 준비 및 대체 실행 순서를 감사 기록에 연결

This commit is contained in:
2026-09-16 08:46:56 +00:00
parent eb680121c3
commit 028f985e7a
15 changed files with 391 additions and 24 deletions
@@ -11,6 +11,8 @@ const zTick = z
const zSummary = z.object({
schemaVersion: z.literal(1),
coverage: z.literal('PROCEDURES'),
executionCoverage: z.literal('ATTEMPTS').optional(),
executionStatus: z.enum(['PREPARING', 'BLOCKED', 'RESOLVED']).optional(),
clockRevision: z.number().int(),
codeVersion: z.string().nullable(),
policyRefs: z.object({
@@ -48,6 +50,27 @@ const zStep = z.intersection(
tick: z.number().nullable(),
}),
z.discriminatedUnion('kind', [
z.object({
kind: z.literal('EXECUTION_ATTEMPT'),
attempt: z.number().int().min(0).max(5),
requestedAction: z.string(),
resolvedAction: z.string(),
executedAction: z.string().nullable(),
completed: z.boolean(),
usedFallback: z.boolean(),
alternativeAction: z.string().nullable(),
preparation: z.object({ term: z.number().int().positive(), total: z.number().int().positive() }).nullable(),
checks: z
.array(
z.object({
stage: z.enum(['ARGS', 'CONSTRAINT', 'COOLDOWN', 'CONTEXT', 'BLOCK']),
action: z.string(),
result: z.enum(['allow', 'deny', 'unknown']),
reason: z.string().nullable(),
})
)
.max(5),
}),
z.object({ kind: z.literal('DECISION_START'), reservedAction: z.string() }),
z.object({ kind: z.literal('DECISION_END'), action: z.string().nullable(), reason: z.string().nullable() }),
z.object({ kind: z.literal('DECISION_ERROR') }),
@@ -2298,7 +2298,34 @@ integration('game API security over HTTP transport', () => {
ordinal: 0,
steps: Array.from({ length: 128 }, (_, sequence) => ({ ...step, sequence })),
},
{ decisionId: decisionIds[0]!, ordinal: 1, steps: [{ ...step, sequence: 128 }] },
{
decisionId: decisionIds[0]!,
ordinal: 1,
steps: [
{
...step,
sequence: 128,
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: '자원 부족',
secret: 'decision-secret',
},
],
},
],
},
],
});
const decisionInput = { generalId: decisionGeneral, month: { year: 190, month: 1 }, limit: 1 };
@@ -2351,8 +2378,27 @@ integration('game API security over HTTP transport', () => {
});
expect(JSON.stringify(decisionPage.body)).not.toContain('decision-secret');
expect((await get('decisionDetail', admin, { ...decisionDetailInput, cursor: 0 })).body).toMatchObject({
result: { data: { chunks: [{ ordinal: 1, steps: [{ sequence: 128 }] }], nextCursor: null } },
result: {
data: {
chunks: [
{
ordinal: 1,
steps: [
{
sequence: 128,
kind: 'EXECUTION_ATTEMPT',
checks: [{ stage: 'CONSTRAINT', result: 'deny', reason: '자원 부족' }],
},
],
},
],
nextCursor: null,
},
},
});
expect(
JSON.stringify((await get('decisionDetail', admin, { ...decisionDetailInput, cursor: 0 })).body)
).not.toContain('decision-secret');
expect((await get('decisionDetail', admin, { ...decisionDetailInput, generalId })).status).toBe(404);
expect((await get('decisionDetail', admin, { ...decisionDetailInput, limit: 5 })).status).toBe(400);
expect((await get('decisionHistory', admin, { ...decisionInput, limit: 201 })).status).toBe(400);