feat: 예약 초기화 중간 버전 업데이트를 허용한다

미래 예약 RESET을 보존하면서 DB 유지 DEPLOY 한 건을 별도 queue lane에 등록한다. 예약 시각까지 시작되지 않은 중간 배포는 RESET claim 전에 자동 취소하고 관리자 화면에 이 경계를 안내한다.
This commit is contained in:
2026-08-24 13:22:19 +00:00
parent a51a9bfc04
commit ce0c9e257d
9 changed files with 350 additions and 22 deletions
@@ -10,6 +10,7 @@ type Operation = {
sourceMode?: 'BRANCH' | 'COMMIT';
sourceRef?: string;
resolvedCommitSha?: string;
scheduledAt?: string;
completedAt?: string;
error?: string;
payload: Record<string, unknown>;
@@ -382,7 +383,7 @@ const installFixture = async (page: Page, state: FixtureState) => {
createdAt: '2026-07-25T02:00:00.000Z',
updatedAt: '2026-07-25T02:00:00.000Z',
};
state.operations = [operation];
state.operations = [operation, ...state.operations];
return response(operation);
}
if (name === 'admin.operations.requestDeploy') {
@@ -398,7 +399,7 @@ const installFixture = async (page: Page, state: FixtureState) => {
createdAt: '2026-08-01T01:00:00.000Z',
updatedAt: '2026-08-01T01:00:00.000Z',
};
state.operations = [operation];
state.operations = [operation, ...state.operations];
return response(operation);
}
if (name === 'admin.operations.requestGameCancellation') {
@@ -817,6 +818,67 @@ test('separates DB-preserving profile deployment from DB reset', async ({ page }
expect(state.requestBodies.some((entry) => entry.operation === 'admin.operations.requestReset')).toBe(false);
});
test('keeps a future scenario reset reserved while submitting one interim DB-preserving deploy', async ({
page,
}, testInfo) => {
const scheduledReset: Operation = {
id: '99999999-9999-4999-8999-999999999999',
profileName: 'che:default',
type: 'RESET',
status: 'QUEUED',
sourceMode: 'BRANCH',
sourceRef: 'main',
scheduledAt: '2099-08-27T05:00:00.000Z',
payload: {},
requestedBy: 'admin',
createdAt: '2026-08-24T00:00:00.000Z',
updatedAt: '2026-08-24T00:00:00.000Z',
};
const state: FixtureState = {
operations: [scheduledReset],
gatewayOperations: [],
runtimeRunning: true,
requestBodies: [],
};
await installFixture(page, state);
const confirmations: string[] = [];
page.on('dialog', async (dialog) => {
confirmations.push(dialog.message());
await dialog.accept();
});
await page.goto('admin/servers/che%3Adefault/version');
const notice = page.getByTestId('interim-deploy-notice');
await expect(notice).toContainText('예약 초기화 유지');
await expect(notice).toContainText('시나리오 초기화 예약은 취소되지 않습니다.');
await expect(page.getByTestId('request-deploy')).toBeEnabled();
await page.getByTestId('request-deploy').click();
await expect(page.getByText('DB 보존 배포 작업을 등록했습니다.').first()).toBeVisible();
expect(confirmations).toHaveLength(1);
expect(confirmations[0]).toContain('예약된 시나리오 초기화');
expect(confirmations[0]).toContain('자동 취소됩니다.');
expect(state.operations).toHaveLength(2);
expect(state.operations).toContainEqual(scheduledReset);
expect(state.requestBodies.filter((entry) => entry.operation === 'admin.operations.requestDeploy')).toHaveLength(1);
expect(state.requestBodies.some((entry) => entry.operation === 'admin.operations.requestReset')).toBe(false);
await page.setViewportSize({ width: 390, height: 844 });
const mobileGeometry = await notice.evaluate((element) => {
const rect = element.getBoundingClientRect();
return {
left: rect.left,
right: rect.right,
viewportWidth: document.documentElement.clientWidth,
documentScrollWidth: document.documentElement.scrollWidth,
};
});
expect(mobileGeometry.left).toBeGreaterThanOrEqual(0);
expect(mobileGeometry.right).toBeLessThanOrEqual(mobileGeometry.viewportWidth);
expect(mobileGeometry.documentScrollWidth).toBeLessThanOrEqual(mobileGeometry.viewportWidth);
await page.screenshot({ path: testInfo.outputPath('interim-deploy-mobile.png'), fullPage: true });
});
test('submits a separately authorized destructive game cancellation on desktop and mobile', async ({
page,
}, testInfo) => {