fix resource transfer failure log parity
This commit is contained in:
@@ -907,6 +907,7 @@ export const createReservedTurnHandler = async (options: {
|
|||||||
const constraints = definition.buildConstraints(constraintCtx, actionArgs);
|
const constraints = definition.buildConstraints(constraintCtx, actionArgs);
|
||||||
const result = evaluateConstraints(constraints, constraintCtx, view);
|
const result = evaluateConstraints(constraints, constraintCtx, view);
|
||||||
if (result.kind !== 'allow') {
|
if (result.kind !== 'allow') {
|
||||||
|
const failedCommandName = definition.name;
|
||||||
definition = fallbackDefinition;
|
definition = fallbackDefinition;
|
||||||
actionArgs = definition.parseArgs({}) ?? {};
|
actionArgs = definition.parseArgs({}) ?? {};
|
||||||
actionKey = definition.key;
|
actionKey = definition.key;
|
||||||
@@ -914,7 +915,7 @@ export const createReservedTurnHandler = async (options: {
|
|||||||
const reason = result.kind === 'deny' ? result.reason : '조건을 확인할 수 없습니다.';
|
const reason = result.kind === 'deny' ? result.reason : '조건을 확인할 수 없습니다.';
|
||||||
blockedReason = reason;
|
blockedReason = reason;
|
||||||
const meta = result.kind === 'deny' ? { constraintName: result.constraintName } : undefined;
|
const meta = result.kind === 'deny' ? { constraintName: result.constraintName } : undefined;
|
||||||
logs.push(createActionLog(reason, meta));
|
logs.push(createActionLog(`${reason} ${failedCommandName} 실패.`, meta));
|
||||||
}
|
}
|
||||||
if (!usedFallback && (kind === 'general' || currentNation)) {
|
if (!usedFallback && (kind === 'general' || currentNation)) {
|
||||||
const currentYearMonth = joinYearMonth(context.world.currentYear, context.world.currentMonth);
|
const currentYearMonth = joinYearMonth(context.world.currentYear, context.world.currentMonth);
|
||||||
|
|||||||
@@ -618,6 +618,7 @@ describe('Reserved Turn Execution Integration', () => {
|
|||||||
const dirty = world.consumeDirtyState();
|
const dirty = world.consumeDirtyState();
|
||||||
expect(world.getGeneralById(1)!.cityId).toBe(1);
|
expect(world.getGeneralById(1)!.cityId).toBe(1);
|
||||||
const denyLog = dirty.logs.find((log) => log.text.includes('같은 도시입니다.'));
|
const denyLog = dirty.logs.find((log) => log.text.includes('같은 도시입니다.'));
|
||||||
|
expect(denyLog?.text).toContain('이동 실패.');
|
||||||
expect(denyLog?.meta?.constraintName).toBe('notSameDestCity');
|
expect(denyLog?.meta?.constraintName).toBe('notSameDestCity');
|
||||||
expect(dirty.logs.some((log) => log.text.includes('아무것도 실행하지 않았습니다.'))).toBe(true);
|
expect(dirty.logs.some((log) => log.text.includes('아무것도 실행하지 않았습니다.'))).toBe(true);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -583,7 +583,7 @@ export const friendlyDestGeneral = (): Constraint => ({
|
|||||||
if (destGeneral.nationId === general.nationId) {
|
if (destGeneral.nationId === general.nationId) {
|
||||||
return allow();
|
return allow();
|
||||||
}
|
}
|
||||||
return { kind: 'deny', reason: '아군이 아닙니다.' };
|
return { kind: 'deny', reason: '아국 장수가 아닙니다.' };
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -3497,6 +3497,108 @@ integration('general command gift resource and target boundaries', () => {
|
|||||||
}, 120_000);
|
}, 120_000);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
interface ResourceTransferBoundaryCase {
|
||||||
|
name: string;
|
||||||
|
action: 'che_증여' | 'che_헌납' | 'che_군량매매';
|
||||||
|
args: Record<string, unknown>;
|
||||||
|
actorPatch?: Record<string, unknown>;
|
||||||
|
fixturePatches?: FixturePatches;
|
||||||
|
completed: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const resourceTransferBoundaryCases: ResourceTransferBoundaryCase[] = [
|
||||||
|
{
|
||||||
|
name: 'gift completes with a zero transferable gold amount at the exact minimum',
|
||||||
|
action: 'che_증여',
|
||||||
|
args: { isGold: true, amount: 100, destGeneralID: 3 },
|
||||||
|
actorPatch: { gold: 0 },
|
||||||
|
completed: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'gift rejects a general from another nation',
|
||||||
|
action: 'che_증여',
|
||||||
|
args: { isGold: true, amount: 100, destGeneralID: 2 },
|
||||||
|
completed: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'donation at the exact rice minimum may spend below that minimum',
|
||||||
|
action: 'che_헌납',
|
||||||
|
args: { isGold: false, amount: 100 },
|
||||||
|
actorPatch: { rice: 500, crew: 0 },
|
||||||
|
completed: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'donation completes with zero gold at the zero minimum',
|
||||||
|
action: 'che_헌납',
|
||||||
|
args: { isGold: true, amount: 100 },
|
||||||
|
actorPatch: { gold: 0 },
|
||||||
|
completed: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'buying rice with one gold preserves the fee and integer persistence boundary',
|
||||||
|
action: 'che_군량매매',
|
||||||
|
args: { buyRice: true, amount: 100 },
|
||||||
|
actorPatch: { gold: 1, rice: 0 },
|
||||||
|
completed: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'selling one rice preserves the fee and integer persistence boundary',
|
||||||
|
action: 'che_군량매매',
|
||||||
|
args: { buyRice: false, amount: 100 },
|
||||||
|
actorPatch: { gold: 0, rice: 1, crew: 0 },
|
||||||
|
completed: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'trade rate 137 preserves resource, tax, stat experience, and logs',
|
||||||
|
action: 'che_군량매매',
|
||||||
|
args: { buyRice: false, amount: 1_000 },
|
||||||
|
fixturePatches: { cities: { 3: { trade: 137 } } },
|
||||||
|
completed: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'a neutral general can trade in a neutral trader city without nation tax',
|
||||||
|
action: 'che_군량매매',
|
||||||
|
args: { buyRice: false, amount: 1_000 },
|
||||||
|
actorPatch: { nationId: 0, officerLevel: 0 },
|
||||||
|
fixturePatches: { cities: { 3: { nationId: 0 } } },
|
||||||
|
completed: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
integration('general resource transfer target, fee, state, and log parity', () => {
|
||||||
|
it.each(resourceTransferBoundaryCases)(
|
||||||
|
'$name',
|
||||||
|
async ({ action, args, actorPatch, fixturePatches, completed }) => {
|
||||||
|
const request = buildRequest(action, args, actorPatch, fixturePatches);
|
||||||
|
request.setup!.world!.hiddenSeed = `general-resource-transfer-${action}-${String(args.isGold ?? args.buyRice)}`;
|
||||||
|
request.observe!.includeGlobalHistoryLogs = true;
|
||||||
|
const reference = runReferenceTurnCommandTraceRequest(
|
||||||
|
workspaceRoot!,
|
||||||
|
request as unknown as Record<string, unknown>
|
||||||
|
);
|
||||||
|
const core = await runCoreTurnCommandTrace(request, reference.before);
|
||||||
|
|
||||||
|
expect(reference.execution.outcome).toMatchObject({ completed });
|
||||||
|
expect(core.execution.outcome).toMatchObject({
|
||||||
|
requestedAction: action,
|
||||||
|
actionKey: completed ? action : '휴식',
|
||||||
|
usedFallback: !completed,
|
||||||
|
});
|
||||||
|
expect(core.rng).toEqual(reference.rng);
|
||||||
|
expect(
|
||||||
|
compareTurnSnapshotDeltas(reference.before, reference.after, core.before, core.after, {
|
||||||
|
ignoredPathPatterns: ignoredLifecyclePaths,
|
||||||
|
})
|
||||||
|
).toEqual([]);
|
||||||
|
|
||||||
|
expect(semanticLogSignatures(core.after.logs)).toEqual(
|
||||||
|
semanticLogSignatures(addedReferenceLogs(reference.before, reference.after.logs))
|
||||||
|
);
|
||||||
|
},
|
||||||
|
120_000
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
type GeneralConstraintCase = {
|
type GeneralConstraintCase = {
|
||||||
name: string;
|
name: string;
|
||||||
action: string;
|
action: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user