feat: add per-profile reset defaults

This commit is contained in:
2026-08-09 14:43:10 +00:00
parent 0484a84927
commit 74ef723c7e
8 changed files with 618 additions and 28 deletions
+41
View File
@@ -464,6 +464,24 @@ const zInstallOptions = z.object({
gitRef: z.string().min(1).max(128).optional(),
});
const zOperationInstallOptions = zInstallOptions.omit({ gitRef: true });
const zProfileResetDefaults = zInstallOptions.omit({
scenarioId: true,
openAt: true,
preopenAt: true,
gitRef: true,
});
const SYSTEM_PROFILE_RESET_DEFAULTS: z.infer<typeof zProfileResetDefaults> = {
turnTermMinutes: 60,
sync: true,
fiction: 1,
extend: true,
blockGeneralCreate: 0,
npcMode: 0,
showImgLevel: 3,
tournamentTrig: true,
joinMode: 'full',
autorunUser: null,
};
const zSourceMode = z.enum(['BRANCH', 'COMMIT']);
const zResetSourceMode = z.enum(['CURRENT', 'BRANCH', 'COMMIT']);
@@ -533,6 +551,16 @@ const readMetaObject = (value: unknown): Record<string, unknown> => {
return value as Record<string, unknown>;
};
const readProfileResetDefaults = (
meta: Record<string, unknown>
): { defaults: z.infer<typeof zProfileResetDefaults>; source: 'SYSTEM' | 'PROFILE' } => {
const parsed = zProfileResetDefaults.partial().safeParse(meta.resetDefaults);
if (meta.resetDefaults === undefined || !parsed.success) {
return { defaults: { ...SYSTEM_PROFILE_RESET_DEFAULTS }, source: 'SYSTEM' };
}
return { defaults: { ...SYSTEM_PROFILE_RESET_DEFAULTS, ...parsed.data }, source: 'PROFILE' };
};
const applyMetaPatch = (
meta: Record<string, unknown>,
patch: Record<string, unknown | null | undefined>
@@ -1474,6 +1502,18 @@ export const adminRouter = router({
}),
}),
profiles: router({
getResetDefaults: adminProcedure
.input(z.object({ profileName: z.string().min(1) }))
.query(async ({ ctx, input }) => {
const adminAuth = requireAdminAuth(ctx);
assertAnyPermission(adminAuth, [ROLE_ADMIN_PROFILES, ROLE_ADMIN_SCENARIO_RESET], input.profileName);
const profile = await ctx.profiles.getProfile(input.profileName);
if (!profile) {
throw new TRPCError({ code: 'NOT_FOUND', message: 'Profile not found.' });
}
const meta = readMetaObject(profile.meta);
return readProfileResetDefaults(meta);
}),
listNavigation: adminProcedure.query(async ({ ctx }) => {
const adminAuth = requireAdminAuth(ctx);
return orderGatewayProfiles(await ctx.profiles.listProfiles())
@@ -1660,6 +1700,7 @@ export const adminRouter = router({
nextSeasonIdx: z.number().int().min(0).nullable().optional(),
localAccountAccessGraceDays: z.number().int().min(0).max(365).nullable().optional(),
localAccountGeneralCreationGraceDays: z.number().int().min(0).max(365).nullable().optional(),
resetDefaults: zProfileResetDefaults.nullable().optional(),
}),
reason: z.string().trim().min(3).max(200),
})
+98 -1
View File
@@ -29,6 +29,7 @@ const buildCaller = async (
initialNotice?: string;
initialProfileStatus?: GatewayProfileRecord['status'];
profileScenario?: string;
profileMeta?: GatewayProfileRecord['meta'];
releaseLogVisibilityAfterPolls?: number;
} = {}
) => {
@@ -75,7 +76,7 @@ const buildCaller = async (
status: options.initialProfileStatus ?? ('STOPPED' as const),
buildStatus: 'SUCCEEDED' as const,
buildCommitSha: 'HEAD',
meta: {},
meta: options.profileMeta ?? {},
createdAt: '2026-07-25T00:00:00.000Z',
updatedAt: '2026-07-25T00:00:00.000Z',
};
@@ -511,6 +512,102 @@ describe('admin operation API', () => {
});
});
it('returns validated profile reset defaults to a scenario-only operator', async () => {
const harness = await buildCaller(
async () => {
throw new Error('not used');
},
{
adminRoles: ['admin.scenarios.reset:che:2'],
firstUserIsAdmin: false,
profileMeta: {
resetDefaults: {
turnTermMinutes: 20,
sync: false,
fiction: 0,
extend: false,
blockGeneralCreate: 2,
npcMode: 1,
showImgLevel: 1,
tournamentTrig: false,
joinMode: 'onlyRandom',
autorunUser: { limitMinutes: 720, options: ['develop', 'train'] },
},
},
}
);
await expect(harness.caller.admin.profiles.getResetDefaults({ profileName: 'che:2' })).resolves.toEqual({
source: 'PROFILE',
defaults: {
turnTermMinutes: 20,
sync: false,
fiction: 0,
extend: false,
blockGeneralCreate: 2,
npcMode: 1,
showImgLevel: 1,
tournamentTrig: false,
joinMode: 'onlyRandom',
autorunUser: { limitMinutes: 720, options: ['develop', 'train'] },
},
});
});
it('falls back to system reset defaults when profile metadata is malformed', async () => {
const harness = await buildCaller(
async () => {
throw new Error('not used');
},
{
adminRoles: ['admin.scenarios.reset:che:2'],
firstUserIsAdmin: false,
profileMeta: { resetDefaults: { npcMode: 99 } },
}
);
await expect(harness.caller.admin.profiles.getResetDefaults({ profileName: 'che:2' })).resolves.toMatchObject({
source: 'SYSTEM',
defaults: { turnTermMinutes: 60, npcMode: 0, tournamentTrig: true, autorunUser: null },
});
});
it('stores reset defaults only after validating the complete metadata object', async () => {
const harness = await buildCaller(
async () => {
throw new Error('not used');
},
{ adminRoles: ['admin.profiles.settings:che:2'], firstUserIsAdmin: false }
);
const resetDefaults = {
turnTermMinutes: 10,
sync: true,
fiction: 1 as const,
extend: true,
blockGeneralCreate: 0 as const,
npcMode: 2 as const,
showImgLevel: 3 as const,
tournamentTrig: true,
joinMode: 'full' as const,
autorunUser: null,
};
await harness.caller.admin.profiles.updateMeta({
profileName: 'che:2',
patch: { resetDefaults },
reason: 'set server reset defaults',
});
expect(harness.updatedMetas.at(-1)).toMatchObject({ resetDefaults });
await expect(
harness.caller.admin.profiles.updateMeta({
profileName: 'che:2',
patch: { resetDefaults: { ...resetDefaults, turnTermMinutes: 7 } },
reason: 'reject invalid reset defaults',
})
).rejects.toBeDefined();
});
it('does not let a scenario-only operator combine a Git update with reset', async () => {
const harness = await buildCaller(
async () => {