also looking at this
test: retain execution lifetime failure observations
#62
10 files
+174
-64
Review threads live on the whole diff, not on one commit, so none are shown here - a thread's line means something in the branch's final form, and painting it into an intermediate step would put it on code it is not about.
| @@ -1,4 +1,5 @@ | ||
| 1 | 1 | import assert from 'node:assert/strict' |
| 2 | import type { FixturePhaseObserver } from './handover-setup' | |
| 2 | 3 | import { setupHandoverFixture } from './handover-setup' |
| 3 | 4 | import { examplePublicSafetyConfig } from '../../../resources/functions/drone/public-safety/fixtures' |
| 4 | 5 | |
| @@ -8,8 +9,8 @@ export interface LifetimeSlot { | ||
| 8 | 9 | recording: string | null, completion: unknown, latestEvent: unknown, |
| 9 | 10 | candidate: unknown, pendingEvent: unknown, worker: Worker | null, |
| 10 | 11 | } |
| 11 | export async function setupExecutionLifetime(mode: string) { | |
| 12 | const f = await setupHandoverFixture() | |
| 12 | export async function setupExecutionLifetime(mode: string, observe?: FixturePhaseObserver) { | |
| 13 | const f = await setupHandoverFixture(observe) | |
| 13 | 14 | const core = await import('../../../resources/functions/drone/public-safety/execution/service') |
| 14 | 15 | const { executionOwner } = await import('../../../resources/functions/drone/public-safety/execution/state') |
| 15 | 16 | const { executionWords } = await import('../../../resources/functions/drone/public-safety/execution/protocol') |
| @@ -15,7 +15,7 @@ async function observe<T>(phase: string, work: () => Promise<T>): Promise<T> { | ||
| 15 | 15 | try { const result = await work(); emit('settled'); return result } |
| 16 | 16 | catch (error) { emit('rejected'); throw error } |
| 17 | 17 | } |
| 18 | const h = await observe('setup', () => setupExecutionLifetime(mode)) | |
| 18 | const h = await observe('setup', () => setupExecutionLifetime(mode, observe)) | |
| 19 | 19 | let attachment: Awaited<ReturnType<typeof h.core.attachIncidentExecution>> | undefined |
| 20 | 20 | let expiredClose: string | null = null |
| 21 | 21 | try { |
| @@ -29,7 +29,7 @@ try { | ||
| 29 | 29 | assert.equal(value.latestEvent.sequence, ++last) |
| 30 | 30 | }))) |
| 31 | 31 | await observe('activate', () => attachment!.activate()) |
| 32 | await h.wait(() => run.slot.phase === 'completed', 'The real eight-second producer must complete', 20000) | |
| 32 | await observe('producer-completed', () => h.wait(() => run.slot.phase === 'completed', 'The real eight-second producer must complete', 20000)) | |
| 33 | 33 | assert(ack.event) |
| 34 | 34 | assert.equal(ack.event.data.outcome, 'completed') |
| 35 | 35 | assert.equal(ack.event.data.transferred, true) |
| @@ -39,7 +39,7 @@ try { | ||
| 39 | 39 | attachment.release() |
| 40 | 40 | } |
| 41 | 41 | else { |
| 42 | await h.wait(() => Atomics.load(run.slot.control, h.executionWords.emitted) === 1, 'The real producer must reach its first blocked callback') | |
| 42 | await observe('producer-first-callback', () => h.wait(() => Atomics.load(run.slot.control, h.executionWords.emitted) === 1, 'The real producer must reach its first blocked callback')) | |
| 43 | 43 | assert.equal(Atomics.load(run.slot.control, h.executionWords.credit), 0) |
| 44 | 44 | assert.equal(Atomics.load(run.slot.control, h.executionWords.returned), 0) |
| 45 | 45 | const stoppingAt = performance.now() |
| @@ -2,8 +2,13 @@ import { randomUUID } from 'node:crypto' | ||
| 2 | 2 | import { setupIncidentDatabase } from './setup' |
| 3 | 3 | import { examplePublicSafetyConfig } from '../../../resources/functions/drone/public-safety/fixtures' |
| 4 | 4 | import { simulatePublicSafety } from '../../../resources/functions/drone/public-safety/runtime' |
| 5 | export async function setupHandoverFixture() { | |
| 6 | const { database, User } = await setupIncidentDatabase() | |
| 5 | export type FixturePhaseObserver = <T>(phase: string, work: () => Promise<T>) => Promise<T> | |
| 6 | ||
| 7 | export async function setupHandoverFixture(observe: FixturePhaseObserver = (phase, work) => { | |
| 8 | void phase | |
| 9 | return work() | |
| 10 | }) { | |
| 11 | const { database, User } = await observe('fixture-database', () => setupIncidentDatabase()) | |
| 7 | 12 | const access = await import('../../../resources/functions/drone/public-safety/access/service') |
| 8 | 13 | const records = await import('../../../resources/functions/drone/public-safety/access/records') |
| 9 | 14 | const handover = await import('../../../resources/functions/drone/public-safety/access/handover') |
| @@ -20,14 +25,18 @@ export async function setupHandoverFixture() { | ||
| 20 | 25 | for (const ability of incidentAbilities) Gate.define(`public-safety:${ability}`, (user, key) => publicSafetyIncidentGate(user, key, ability)) |
| 21 | 26 | for (const ability of ['read', 'write'] as const) Gate.define(`public-safety:handover-${ability}`, (user, key) => policy.publicSafetyHandoverGate(user, key, ability)) |
| 22 | 27 | const as = <T>(token: string, work: () => Promise<T>) => runWithRequest(enhanceRequest(new Request('https://incident.invalid/api/public-safety/incidents', { headers: { authorization: `Bearer ${token}` } }) as Parameters<typeof enhanceRequest>[0]), work) |
| 23 | const sender = await User.create({ name: 'Outgoing Coordinator', email: 'handover-sender@example.invalid', password: 'temporary-fixture-password' }) | |
| 24 | const recipient = await User.create({ name: 'Incoming Coordinator', email: 'handover-recipient@example.invalid', password: 'temporary-fixture-password' }) | |
| 25 | const unrelated = await User.create({ name: 'Unrelated Observer', email: 'handover-unrelated@example.invalid', password: 'temporary-fixture-password' }) | |
| 26 | for (const permission of ['public-safety:access', 'public-safety:create']) await Rbac.createPermission(permission, 'web') | |
| 27 | for (const user of [sender, recipient, unrelated]) await Rbac.givePermission(user.id, 'public-safety:access', 'web') | |
| 28 | for (const user of [sender, recipient]) await Rbac.givePermission(user.id, 'public-safety:create', 'web') | |
| 28 | const sender = await observe('fixture-sender-account', async () => User.create({ name: 'Outgoing Coordinator', email: 'handover-sender@example.invalid', password: 'temporary-fixture-password' })) | |
| 29 | const recipient = await observe('fixture-recipient-account', async () => User.create({ name: 'Incoming Coordinator', email: 'handover-recipient@example.invalid', password: 'temporary-fixture-password' })) | |
| 30 | const unrelated = await observe('fixture-unrelated-account', async () => User.create({ name: 'Unrelated Observer', email: 'handover-unrelated@example.invalid', password: 'temporary-fixture-password' })) | |
| 31 | await observe('fixture-rbac', async () => { | |
| 32 | for (const permission of ['public-safety:access', 'public-safety:create']) await Rbac.createPermission(permission, 'web') | |
| 33 | for (const user of [sender, recipient, unrelated]) await Rbac.givePermission(user.id, 'public-safety:access', 'web') | |
| 34 | for (const user of [sender, recipient]) await Rbac.givePermission(user.id, 'public-safety:create', 'web') | |
| 35 | }) | |
| 29 | 36 | const credential = async (userId: number, scopes = ['public-safety:read', 'public-safety:coordinate']) => (await tokens.createToken(userId, 'handover fixture', scopes, { withRefreshToken: false })).plainTextToken |
| 30 | const senderToken = await credential(sender.id), recipientToken = await credential(recipient.id), unrelatedToken = await credential(unrelated.id) | |
| 37 | const senderToken = await observe('fixture-sender-token', () => credential(sender.id)) | |
| 38 | const recipientToken = await observe('fixture-recipient-token', () => credential(recipient.id)) | |
| 39 | const unrelatedToken = await observe('fixture-unrelated-token', () => credential(unrelated.id)) | |
| 31 | 40 | const seed = async (key: string, lifetime = 86400000) => as(senderToken, async () => { |
| 32 | 41 | const config = examplePublicSafetyConfig() |
| 33 | 42 | config.id = key |