feat: add protected manual save records

This commit is contained in:
2026-07-29 08:49:29 +09:00
parent d04accc9e1
commit 624eb2c219
11 changed files with 4458 additions and 201 deletions

View File

@@ -43,6 +43,7 @@ const campaignGeneration = 4;
const staleGeneration = 3;
const campaignKey = 'heros-web:campaign-state';
const campaignSlotKey = `${campaignKey}:slot-1`;
const manualSaveSlotKey = 'heros-web:manual-save:slot-1';
const battleBaseKey = `heros-web:battle:${battleId}`;
const battleSlotKey = `${battleBaseKey}:slot-1`;
const legacyBattleKey = 'heros-web:first-battle-state';
@@ -373,9 +374,16 @@ try {
`${renderer}: removal refusal escaped the intended battle keys.`
);
await page.evaluate(() => {
window.__HEROS_BATTLE_GENERATION_FIXTURE__
?.restoreRemovalInterceptor?.();
});
const beforeControl = await page.evaluate(
({
requestedBattleSlotKey,
requestedCampaignSlotKey,
requestedManualSaveSlotKey,
requestedCampaignGeneration
}) => {
const scene = window.__HEROS_DEBUG__?.scene('BattleScene');
@@ -389,7 +397,28 @@ try {
requestedBattleSlotKey,
JSON.stringify(fixtureState.valid)
);
const campaign = JSON.parse(
window.localStorage.getItem(
requestedCampaignSlotKey
) ?? 'null'
);
if (!campaign) {
throw new Error('The campaign fixture is unavailable for the protected manual control.');
}
window.localStorage.setItem(
requestedManualSaveSlotKey,
JSON.stringify({
version: 1,
slot: 1,
savedAt: fixtureState.valid.savedAt,
context: 'battle',
campaign,
battle: fixtureState.valid
})
);
const accepted = scene.readBattleSaveState?.(1, true);
const acceptedManual =
scene.readManualBattleSaveState?.(1, true);
const loadDisabled =
scene.isMapMenuActionDisabled?.('load') === true;
scene.showSaveSlotPanel?.('load');
@@ -404,6 +433,8 @@ try {
accepted?.battleResumeGeneration ?? null,
acceptedTurnNumber:
accepted?.turnNumber ?? null,
acceptedManualTurnNumber:
acceptedManual?.turnNumber ?? null,
loadDisabled,
panelMode: panel?.saveSlotPanelMode ?? null,
confirmationMode:
@@ -415,6 +446,8 @@ try {
},
{
requestedBattleSlotKey: battleSlotKey,
requestedCampaignSlotKey: campaignSlotKey,
requestedManualSaveSlotKey: manualSaveSlotKey,
requestedCampaignGeneration: campaignGeneration
}
);
@@ -428,6 +461,10 @@ try {
beforeControl.acceptedTurnNumber,
fixture.validTurnNumber
);
assert.equal(
beforeControl.acceptedManualTurnNumber,
fixture.validTurnNumber
);
assert.equal(beforeControl.loadDisabled, false);
assert.equal(beforeControl.panelMode, 'load');
assert.equal(beforeControl.confirmationMode, 'load-confirm');
@@ -437,29 +474,70 @@ try {
const scene = window.__HEROS_DEBUG__?.scene('BattleScene');
scene?.activateTurnPromptPrimaryAction?.();
});
await page.waitForFunction(
({
expectedBattleId,
expectedTurnNumber,
expectedMarkerHp
}) => {
const battle = window.__HEROS_DEBUG__?.battle?.();
return (
battle?.battleId === expectedBattleId &&
battle?.turnNumber === expectedTurnNumber &&
battle?.activeFaction === 'ally' &&
battle?.phase === 'idle' &&
battle?.units?.find((unit) => unit.id === 'liu-bei')?.hp ===
expectedMarkerHp
);
},
{
expectedBattleId: battleId,
expectedTurnNumber: fixture.validTurnNumber,
expectedMarkerHp: fixture.validMarkerHp
},
{ timeout: 90000 }
);
try {
await page.waitForFunction(
({
expectedBattleId,
expectedTurnNumber,
expectedMarkerHp
}) => {
const battle = window.__HEROS_DEBUG__?.battle?.();
return (
battle?.battleId === expectedBattleId &&
battle?.turnNumber === expectedTurnNumber &&
battle?.activeFaction === 'ally' &&
battle?.phase === 'idle' &&
battle?.units?.find((unit) => unit.id === 'liu-bei')?.hp ===
expectedMarkerHp
);
},
{
expectedBattleId: battleId,
expectedTurnNumber: fixture.validTurnNumber,
expectedMarkerHp: fixture.validMarkerHp
},
{ timeout: 90000 }
);
} catch (error) {
const restoreDiagnostics = await page.evaluate(
({
requestedCampaignSlotKey,
requestedBattleSlotKey,
requestedManualSaveSlotKey
}) => ({
battle:
window.__HEROS_DEBUG__?.battle?.() ?? null,
campaign: JSON.parse(
window.localStorage.getItem(
requestedCampaignSlotKey
) ?? 'null'
),
battleSave: JSON.parse(
window.localStorage.getItem(
requestedBattleSlotKey
) ?? 'null'
),
manualSave: JSON.parse(
window.localStorage.getItem(
requestedManualSaveSlotKey
) ?? 'null'
)
}),
{
requestedCampaignSlotKey: campaignSlotKey,
requestedBattleSlotKey: battleSlotKey,
requestedManualSaveSlotKey: manualSaveSlotKey
}
);
throw new Error(
`${renderer}: protected manual restore did not become ready: ${JSON.stringify({
restoreDiagnostics,
pageErrors,
consoleErrors
})}`,
{ cause: error }
);
}
const restored = await page.evaluate(
({
@@ -687,6 +765,16 @@ try {
},
`${renderer}: ally action autosave`
);
assert.equal(
actionAutosave.autosave.indicatorVisible,
true,
`${renderer}: a successful ally-action autosave did not expose visible save feedback.`
);
assert.match(
actionAutosave.autosave.indicatorText ?? '',
/자동 저장 완료/,
`${renderer}: the success save indicator was not clearly labeled.`
);
const unsafeAutosaves = [];
for (const [
@@ -1099,6 +1187,16 @@ try {
rollbackProbe.autosave.lastSavedAt,
rollbackBaseline.state.savedAt
);
assert.equal(
rollbackProbe.autosave.indicatorVisible,
true,
`${renderer}: a failed autosave did not expose a persistent visible warning.`
);
assert.match(
rollbackProbe.autosave.indicatorText ?? '',
/자동 저장 실패/,
`${renderer}: the failed autosave warning was not clearly labeled.`
);
assert.equal(pageErrors.length, 0, pageErrors.join('\n'));
assert.equal(consoleErrors.length, 0, consoleErrors.join('\n'));
@@ -1313,6 +1411,15 @@ function assertAutosaveDebugState(
),
`${label}.lastSavedAt is invalid.`
);
assert(
typeof autosave.indicatorVisible === 'boolean',
`${label}.indicatorVisible must be boolean.`
);
assert(
autosave.indicatorText === null ||
typeof autosave.indicatorText === 'string',
`${label}.indicatorText is invalid.`
);
}
function assertAutosaveWrite(