Files
aiturk-hermes-ide/apps/desktop/electron/profile-delete-routing.test.ts
T

326 lines
11 KiB
TypeScript

import assert from 'node:assert/strict'
import { test } from 'vitest'
import {
assertLocalProfileCanStart,
decideProfileDeleteAction,
dispatchConnectionScopedProfileDelete,
localProfilePoolKeys,
ProfileDeletionGate,
profileNameFromDeleteRequest,
resolveRouteProfile
} from './profile-delete-routing'
// ---------------------------------------------------------------------------
// profileNameFromDeleteRequest
// ---------------------------------------------------------------------------
test('profileNameFromDeleteRequest parses a DELETE /api/profiles/<name> path', () => {
assert.equal(profileNameFromDeleteRequest({ method: 'DELETE', path: '/api/profiles/worker' }), 'worker')
})
test('profileNameFromDeleteRequest lowercases the profile name', () => {
assert.equal(profileNameFromDeleteRequest({ method: 'DELETE', path: '/api/profiles/Worker' }), 'worker')
})
test('profileNameFromDeleteRequest returns null for non-DELETE methods', () => {
assert.equal(profileNameFromDeleteRequest({ method: 'GET', path: '/api/profiles/worker' }), null)
})
test('profileNameFromDeleteRequest returns null when the path does not match', () => {
assert.equal(profileNameFromDeleteRequest({ method: 'DELETE', path: '/api/sessions' }), null)
})
test('profileNameFromDeleteRequest returns null for an empty/whitespace name', () => {
assert.equal(profileNameFromDeleteRequest({ method: 'DELETE', path: '/api/profiles/%20' }), null)
})
test('profileNameFromDeleteRequest returns null for an undecodable path segment', () => {
assert.equal(profileNameFromDeleteRequest({ method: 'DELETE', path: '/api/profiles/%E0%A4%A' }), null)
})
// ---------------------------------------------------------------------------
// decideProfileDeleteAction
// ---------------------------------------------------------------------------
const deps = {
isDefaultProfile: p => p === 'default',
isValidProfileName: p => /^[a-z0-9][a-z0-9_-]{0,63}$/.test(p),
primaryProfileKey: () => 'primary-profile'
}
test('decideProfileDeleteAction is a noop for the default profile', () => {
assert.deepEqual(decideProfileDeleteAction('default', deps), { action: 'noop', profile: null })
})
test('decideProfileDeleteAction is a noop for null (no profile parsed)', () => {
assert.deepEqual(decideProfileDeleteAction(null, deps), { action: 'noop', profile: null })
})
test('decideProfileDeleteAction is a noop for an invalid profile name', () => {
assert.deepEqual(decideProfileDeleteAction('Not Valid!', deps), { action: 'noop', profile: null })
})
test('decideProfileDeleteAction tears down the primary backend for the primary profile', () => {
assert.deepEqual(decideProfileDeleteAction('primary-profile', deps), {
action: 'teardown-primary',
profile: 'primary-profile'
})
})
test('decideProfileDeleteAction tears down the pool backend for any other valid profile', () => {
assert.deepEqual(decideProfileDeleteAction('worker', deps), { action: 'teardown-pool', profile: 'worker' })
})
// ---------------------------------------------------------------------------
// resolveRouteProfile
// ---------------------------------------------------------------------------
test('resolveRouteProfile routes to the primary backend (null) when a profile was torn down', () => {
assert.equal(resolveRouteProfile('worker', 'other-profile'), null)
})
test('resolveRouteProfile passes the requested profile through when nothing was torn down', () => {
assert.equal(resolveRouteProfile(null, 'other-profile'), 'other-profile')
})
test('resolveRouteProfile passes through undefined when nothing was torn down and no profile was requested', () => {
assert.equal(resolveRouteProfile(null, undefined), undefined)
})
// ---------------------------------------------------------------------------
// ProfileDeletionGate / localProfilePoolKeys
// ---------------------------------------------------------------------------
test('ProfileDeletionGate blocks concurrent starts until deletion releases', () => {
const gate = new ProfileDeletionGate()
const release = gate.acquire('Selena')
assert.equal(gate.blocks('selena'), true)
assert.equal(gate.blocks('trina'), false)
release()
assert.equal(gate.blocks('selena'), false)
})
test('ProfileDeletionGate keeps overlapping deletion leases blocked', () => {
const gate = new ProfileDeletionGate()
const releaseFirst = gate.acquire('selena')
const releaseSecond = gate.acquire('selena')
releaseFirst()
assert.equal(gate.blocks('selena'), true)
releaseSecond()
assert.equal(gate.blocks('selena'), false)
})
test('ProfileDeletionGate rejects a deferred start when deletion begins while it waits', async () => {
const gate = new ProfileDeletionGate()
let continueStart = () => undefined
const waiting = new Promise<void>(resolve => {
continueStart = resolve
})
const start = (async () => {
await waiting
gate.assertCanStart('selena')
})()
const release = gate.acquire('selena')
continueStart()
await assert.rejects(start, /Profile "selena" is being deleted/)
release()
})
test('assertLocalProfileCanStart rejects a delayed retry after the profile directory is removed', () => {
const gate = new ProfileDeletionGate()
assert.throws(() => assertLocalProfileCanStart('selena', gate, () => false), /Profile "selena" no longer exists/)
assert.doesNotThrow(() => assertLocalProfileCanStart('default', gate, () => false))
assert.doesNotThrow(() => assertLocalProfileCanStart('selena', gate, profile => profile === 'selena'))
})
test('localProfilePoolKeys returns every local process scope for one profile', () => {
assert.deepEqual(localProfilePoolKeys('Selena'), ['selena', 'conn:local::selena'])
assert.deepEqual(localProfilePoolKeys(''), [])
})
test('resolveRouteProfile preserves a primary-backend route from another routing policy', () => {
assert.equal(resolveRouteProfile(null, null), null)
})
test('explicit registered local DELETE holds one gate through teardown and dispatch', async () => {
const events: string[] = []
const gate = new ProfileDeletionGate()
const result = await dispatchConnectionScopedProfileDelete(
{ connectionId: 'local', method: 'DELETE', path: '/api/profiles/worker', profile: 'worker' },
{
...deps,
acquire: profile => {
events.push(`gate:${profile}`)
const release = gate.acquire(profile)
return () => {
events.push(`release:${profile}`)
release()
}
},
connectionKind: () => 'local',
dispatch: async routeProfile => {
assert.equal(gate.blocks('worker'), true)
events.push(`dispatch:${routeProfile ?? 'primary'}`)
return 'deleted'
},
prepareLocal: async request => {
assert.equal(gate.blocks('worker'), true)
events.push(`prepare:${request.connectionId}:worker`)
},
teardownConnection: async () => assert.fail('local deletion must use prepareProfileDeleteRequest')
}
)
assert.equal(result, 'deleted')
assert.deepEqual(events, ['gate:worker', 'prepare:local:worker', 'dispatch:primary', 'release:worker'])
})
test('explicit registered SSH DELETE tears down its source backend before dispatch', async () => {
const events: string[] = []
const result = await dispatchConnectionScopedProfileDelete(
{ connectionId: 'build-host', method: 'DELETE', path: '/api/profiles/worker', profile: 'worker' },
{
...deps,
acquire: profile => {
events.push(`gate:${profile}`)
return () => events.push(`release:${profile}`)
},
connectionKind: () => 'ssh',
dispatch: async routeProfile => {
events.push(`dispatch:${routeProfile ?? 'primary'}`)
return 'deleted'
},
prepareLocal: async () => assert.fail('SSH deletion must not use local profile teardown'),
teardownConnection: async (connectionId, profile) => {
events.push(`teardown:${connectionId}:${profile}`)
}
}
)
assert.equal(result, 'deleted')
assert.deepEqual(events, ['gate:worker', 'teardown:build-host:worker', 'dispatch:primary', 'release:worker'])
})
test('non-identity connection DELETE tears down the logical pool and deletes the backend target', async () => {
const events: string[] = []
const request = {
connectionId: 'source-a',
method: 'DELETE',
path: '/api/profiles/backend-worker',
profile: 'worker'
}
await dispatchConnectionScopedProfileDelete(request, {
...deps,
acquire: profile => {
events.push(`gate:${profile}`)
return () => events.push(`release:${profile}`)
},
connectionKind: () => 'ssh',
dispatch: async routeProfile => {
events.push(`delete:${request.path}:${routeProfile ?? 'primary'}`)
return 'deleted'
},
prepareLocal: async () => assert.fail('remote deletion must not use local profile teardown'),
teardownConnection: async (connectionId, profile) => {
events.push(`teardown:${connectionId}:${profile}`)
assert.notEqual(profile, 'backend-worker', 'must not stop the backend-target pool')
}
})
assert.deepEqual(events, [
'gate:backend-worker',
'teardown:source-a:worker',
'delete:/api/profiles/backend-worker:primary',
'release:backend-worker'
])
})
test('connection-scoped default DELETE rejects before gate, preparation, teardown, or dispatch', async () => {
const events: string[] = []
await assert.rejects(
dispatchConnectionScopedProfileDelete(
{ connectionId: 'build-host', method: 'DELETE', path: '/api/profiles/default', profile: 'default' },
{
acquire: profile => {
events.push(`gate:${profile}`)
return () => events.push(`release:${profile}`)
},
connectionKind: () => 'ssh',
dispatch: async () => {
events.push('dispatch')
return 'deleted'
},
isDefaultProfile: profile => profile === 'default',
isValidProfileName: profile => /^[a-z0-9][a-z0-9_-]{0,63}$/.test(profile),
prepareLocal: async () => {
events.push('prepare')
},
teardownConnection: async () => {
events.push('teardown')
}
}
),
/default profile cannot be deleted/i
)
assert.deepEqual(events, [])
})
test('connection-scoped invalid-name DELETE rejects before gate, preparation, teardown, or dispatch', async () => {
const events: string[] = []
await assert.rejects(
dispatchConnectionScopedProfileDelete(
{ connectionId: 'local', method: 'DELETE', path: '/api/profiles/bad%20name', profile: 'bad name' },
{
acquire: profile => {
events.push(`gate:${profile}`)
return () => events.push(`release:${profile}`)
},
connectionKind: () => 'local',
dispatch: async () => {
events.push('dispatch')
return 'deleted'
},
isDefaultProfile: profile => profile === 'default',
isValidProfileName: profile => /^[a-z0-9][a-z0-9_-]{0,63}$/.test(profile),
prepareLocal: async () => {
events.push('prepare')
},
teardownConnection: async () => {
events.push('teardown')
}
}
),
/invalid profile name/i
)
assert.deepEqual(events, [])
})