fix(gateway): finish lifecycle action audits

This commit is contained in:
2026-07-31 18:02:03 +00:00
parent c29b40ba5a
commit a2d4346d60
2 changed files with 64 additions and 6 deletions
+17 -5
View File
@@ -1332,11 +1332,6 @@ export const adminRouter = router({
SHUTDOWN: 'DISABLED',
} as const;
const mappedStatus = statusMap[input.action as keyof typeof statusMap];
if (mappedStatus) {
await ctx.profiles.updateStatus(input.profileName, mappedStatus);
await ctx.orchestrator.reconcileNow();
}
const meta = readMetaObject(profile.meta);
const actionLog = Array.isArray(meta.adminActions)
? meta.adminActions.filter((entry) => entry && typeof entry === 'object')
@@ -1354,6 +1349,23 @@ export const adminRouter = router({
adminActions: [...actionLog, actionRecord],
};
await ctx.profiles.updateMeta(input.profileName, nextMeta);
if (mappedStatus) {
await ctx.profiles.updateStatus(input.profileName, mappedStatus);
await ctx.orchestrator.reconcileNow();
const appliedActionRecord = {
...actionRecord,
status: 'APPLIED',
handledAt: new Date().toISOString(),
handler: 'gateway-api',
detail: `profile status reconciled as ${mappedStatus}`,
};
await ctx.profiles.updateMeta(input.profileName, {
...nextMeta,
adminActions: [...actionLog, appliedActionRecord],
adminActionsUpdatedAt: appliedActionRecord.handledAt,
});
return { ok: true, action: appliedActionRecord };
}
return { ok: true, action: actionRecord };
}),
requestBuild: profileAdminProcedure
+47 -1
View File
@@ -42,6 +42,7 @@ const buildCaller = async (
const createdRuntimeActions: Array<Record<string, unknown>> = [];
const flushes: Array<{ userId: string; reason?: string; iconRevision?: string }> = [];
const updatedStatuses: GatewayProfileRecord['status'][] = [];
const updatedMetas: Record<string, unknown>[] = [];
let reconcileCount = 0;
let storedNotice = options.initialNotice ?? '';
const profile = {
@@ -65,7 +66,10 @@ const buildCaller = async (
return { ...profile, status };
},
updateBuildStatus: async () => profile,
updateMeta: async () => profile,
updateMeta: async (_profileName, meta) => {
updatedMetas.push(meta);
return profile;
},
listReservedToStart: async () => [],
findQueuedBuild: async () => null,
updateLastError: async () => {},
@@ -171,6 +175,7 @@ const buildCaller = async (
admin,
flushes,
updatedStatuses,
updatedMetas,
getReconcileCount: () => reconcileCount,
getStoredNotice: () => storedNotice,
setStoredNotice: (notice: string) => {
@@ -360,6 +365,47 @@ describe('admin runtime clock action API', () => {
).resolves.toMatchObject({ ok: true });
expect(harness.updatedStatuses).toEqual(['RUNNING']);
expect(harness.getReconcileCount()).toBe(1);
expect(harness.updatedMetas).toHaveLength(2);
expect(harness.updatedMetas.at(-1)).toMatchObject({
adminActions: [
{
action: 'RESUME',
status: 'APPLIED',
handler: 'gateway-api',
detail: 'profile status reconciled as RUNNING',
},
],
});
});
it.each([
['STOP', 'STOPPED'],
['SHUTDOWN', 'DISABLED'],
] as const)('records %s as terminal only after runtime reconciliation', async (action, expectedStatus) => {
const harness = await buildCaller(unusedCreateOperation, { initialProfileStatus: 'RUNNING' });
const result = await harness.caller.admin.profiles.requestAction({
profileName: 'che:2',
action,
});
expect(harness.updatedStatuses).toEqual([expectedStatus]);
expect(harness.getReconcileCount()).toBe(1);
expect(harness.updatedMetas).toHaveLength(2);
expect(harness.updatedMetas[0]).toMatchObject({
adminActions: [{ action, status: 'REQUESTED' }],
});
expect(harness.updatedMetas[1]).toMatchObject({
adminActions: [
{
action,
status: 'APPLIED',
handler: 'gateway-api',
detail: `profile status reconciled as ${expectedStatus}`,
},
],
});
expect(result.action).toMatchObject({ action, status: 'APPLIED', handledAt: expect.any(String) });
});
it('rejects resume outside stopped and paused states without reconciliation', async () => {