diff --git a/app/api/util.spec.ts b/app/api/util.spec.ts index 1c3f97fb23..51fdf260ab 100644 --- a/app/api/util.spec.ts +++ b/app/api/util.spec.ts @@ -7,7 +7,7 @@ */ import { describe, expect, it, test } from 'vitest' -import { genName, parsePortRange, synthesizeData } from './util' +import { diskCan, genName, instanceCan, parsePortRange, synthesizeData } from './util' describe('parsePortRange', () => { describe('parses', () => { @@ -136,3 +136,22 @@ describe('synthesizeData', () => { ]) }) }) + +test('instanceCan', () => { + expect(instanceCan.start({ runState: 'running' })).toBe(false) + expect(instanceCan.start({ runState: 'stopped' })).toBe(true) + + // @ts-expect-error typechecker rejects actions that don't exist + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + instanceCan.abc +}) + +test('diskCan', () => { + expect(diskCan.delete({ state: { state: 'creating' } })).toBe(false) + expect(diskCan.delete({ state: { state: 'attached', instance: 'xyz' } })).toBe(false) + expect(diskCan.delete({ state: { state: 'detached' } })).toBe(true) + + // @ts-expect-error typechecker rejects actions that don't exist + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + diskCan.abc +}) diff --git a/app/api/util.ts b/app/api/util.ts index d2a42750dd..954cd77b0e 100644 --- a/app/api/util.ts +++ b/app/api/util.ts @@ -89,7 +89,7 @@ export const genName = (...parts: [string, ...string[]]) => { ) } -const instanceActions: Record = { +const instanceActions = { // NoVmm maps to to Stopped: // https://github.com/oxidecomputer/omicron/blob/6dd9802/nexus/db-model/src/instance_state.rs#L55 @@ -120,12 +120,12 @@ const instanceActions: Record = { updateNic: ['stopped'], // https://github.com/oxidecomputer/omicron/blob/6dd9802/nexus/src/app/instance.rs#L1520-L1522 serialConsole: ['running', 'rebooting', 'migrating', 'repairing'], -} +} satisfies Record // setting .states is a cute way to make it ergonomic to call the test function // while also making the states available directly -export const instanceCan = R.mapValues(instanceActions, (states) => { +export const instanceCan = R.mapValues(instanceActions, (states: InstanceState[]) => { const test = (i: { runState: InstanceState }) => states.includes(i.runState) test.states = states return test @@ -140,7 +140,7 @@ export function instanceTransitioning({ runState }: Instance) { ) } -const diskActions: Record = { +const diskActions = { // this is a weird one because the list of states is dynamic and it includes // 'creating' in the unwind of the disk create saga, but does not include // 'creating' in the disk delete saga, which is what we care about @@ -154,9 +154,9 @@ const diskActions: Record = { detach: ['attached'], // https://github.com/oxidecomputer/omicron/blob/3093818/nexus/db-queries/src/db/datastore/instance.rs#L1077-L1081 setAsBootDisk: ['attached'], -} +} satisfies Record -export const diskCan = R.mapValues(diskActions, (states) => { +export const diskCan = R.mapValues(diskActions, (states: DiskState['state'][]) => { // only have to Pick because we want this to work for both Disk and // Json, which we pass to it in the MSW handlers const test = (d: Pick) => states.includes(d.state.state)