mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
test: remove duplicate capability coverage and strengthen boundary assertions (#33197)
* test: remove duplicate capability coverage and strengthen boundary assertions * test: preserve background task name assertion across mock resets --------- Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
This commit is contained in:
@@ -20,13 +20,16 @@ beforeEach(() => {
|
||||
})
|
||||
|
||||
describe('ensureDesktopNotificationChannel', () => {
|
||||
it('creates the channel the gateway payload names', async () => {
|
||||
it('creates the audible and silent channels the gateway payload names', async () => {
|
||||
await ensureDesktopNotificationChannel()
|
||||
|
||||
expect(Notifications.setNotificationChannelAsync).toHaveBeenCalledWith(
|
||||
'orca-desktop',
|
||||
expect.objectContaining({ importance: 'high' })
|
||||
)
|
||||
expect(vi.mocked(Notifications.setNotificationChannelAsync).mock.calls).toEqual([
|
||||
[
|
||||
'orca-desktop-silent',
|
||||
expect.objectContaining({ importance: 'high', sound: null, enableVibrate: false })
|
||||
],
|
||||
['orca-desktop', expect.objectContaining({ importance: 'high' })]
|
||||
])
|
||||
})
|
||||
|
||||
it('does nothing on iOS, which has no notification channels', () => {
|
||||
|
||||
@@ -7,10 +7,10 @@ const remove = vi.fn()
|
||||
vi.mock('react-native', () => ({
|
||||
AppState: {
|
||||
currentState: 'active',
|
||||
addEventListener: (_: string, callback: typeof onChange) => {
|
||||
addEventListener: vi.fn((_: string, callback: typeof onChange) => {
|
||||
onChange = callback
|
||||
return { remove }
|
||||
}
|
||||
})
|
||||
}
|
||||
}))
|
||||
afterEach(() => {
|
||||
@@ -23,6 +23,7 @@ it('renews only while mobile is foregrounded, resumes on return, and tears down'
|
||||
AppState.currentState = 'active'
|
||||
const renew = vi.fn(async () => {})
|
||||
const stop = startMobilePushLeaseRenewal(renew)
|
||||
expect(AppState.addEventListener).toHaveBeenCalledExactlyOnceWith('change', expect.any(Function))
|
||||
await vi.advanceTimersByTimeAsync(15 * 60_000)
|
||||
expect(renew).toHaveBeenCalledTimes(1)
|
||||
AppState.currentState = 'background'
|
||||
|
||||
@@ -7,7 +7,7 @@ vi.mock('../storage/preferences', () => ({
|
||||
}))
|
||||
|
||||
describe('notification opt-in gate', () => {
|
||||
it('asks for push-service consent when no choice is saved, regardless of OS permission', async () => {
|
||||
it('asks for push-service consent when no choice is saved', async () => {
|
||||
vi.mocked(readPushNotificationsPreference).mockResolvedValue({ value: null, loaded: true })
|
||||
await expect(shouldPresentNotificationOptIn()).resolves.toBe(true)
|
||||
})
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import { expect, it, vi } from 'vitest'
|
||||
const state = vi.hoisted(() => ({ task: null as null | ((input: unknown) => Promise<void>) }))
|
||||
const state = vi.hoisted(
|
||||
(): { taskName: string; task: null | ((input: unknown) => Promise<void>) } => ({
|
||||
taskName: '',
|
||||
task: null
|
||||
})
|
||||
)
|
||||
vi.mock('expo-task-manager', () => ({
|
||||
defineTask: (_name: string, task: typeof state.task) => {
|
||||
defineTask: (name: string, task: typeof state.task) => {
|
||||
state.taskName = name
|
||||
state.task = task
|
||||
},
|
||||
isAvailableAsync: async () => true
|
||||
@@ -24,6 +30,8 @@ import { registerPushDismissalTask } from './push-background-dismissal'
|
||||
|
||||
it('handles native background JSON and scopes dismissal to the originating host', async () => {
|
||||
await registerPushDismissalTask()
|
||||
expect(state.taskName).toBe('orca-push-dismissal')
|
||||
expect(Notifications.registerTaskAsync).toHaveBeenCalledExactlyOnceWith('orca-push-dismissal')
|
||||
await state.task!({
|
||||
data: {
|
||||
data: {
|
||||
|
||||
@@ -63,7 +63,8 @@ it('joins an overtaking JavaScript write before retrying a delayed negative snap
|
||||
return snapshot
|
||||
})
|
||||
const pending = wasPushDismissed(payload)
|
||||
await vi.waitFor(() => expect(finish).toBeDefined())
|
||||
await Promise.resolve()
|
||||
expect(finish).toBeDefined()
|
||||
await rememberPushDismissal(payload)
|
||||
finish()
|
||||
expect(await pending).toBe(true)
|
||||
|
||||
@@ -1,14 +1,6 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { sha256 } from '@noble/hashes/sha256'
|
||||
import { deriveHostFingerprint, resolveHostIdForFingerprint } from './push-host-fingerprint'
|
||||
|
||||
// Why Buffer here: it computes the same value through a completely different
|
||||
// base64 path than the module's btoa/replace, so the vector is a real cross-check
|
||||
// of the derivation the desktop and gateway independently perform.
|
||||
function expectedFingerprint(publicKey: Uint8Array): string {
|
||||
return Buffer.from(sha256(publicKey)).toString('base64url').slice(0, 16)
|
||||
}
|
||||
|
||||
const publicKey = Uint8Array.from({ length: 32 }, (_, index) => index)
|
||||
const publicKeyB64 = Buffer.from(publicKey).toString('base64')
|
||||
|
||||
@@ -16,16 +8,16 @@ describe('deriveHostFingerprint', () => {
|
||||
it('matches base64url(sha256(publicKey)) truncated to 16 chars', () => {
|
||||
const fingerprint = deriveHostFingerprint(publicKeyB64)
|
||||
|
||||
expect(fingerprint).toBe(expectedFingerprint(publicKey))
|
||||
expect(fingerprint).toBe('Yw3NKWbEM2aRElRI')
|
||||
expect(fingerprint).toHaveLength(16)
|
||||
})
|
||||
|
||||
it('produces url-safe characters only, so a fingerprint survives a JSON payload', () => {
|
||||
// 0xff bytes are what push '+' and '/' into a standard base64 digest.
|
||||
const dense = new Uint8Array(32).fill(0xff)
|
||||
// This digest prefix contains both '/' and '+' before URL normalization.
|
||||
const dense = new Uint8Array(32).fill(87)
|
||||
const fingerprint = deriveHostFingerprint(Buffer.from(dense).toString('base64'))
|
||||
|
||||
expect(fingerprint).toBe(expectedFingerprint(dense))
|
||||
expect(fingerprint).toBe('3G2G1n4fGgJbM_r-')
|
||||
expect(fingerprint).toMatch(/^[A-Za-z0-9_-]{16}$/)
|
||||
})
|
||||
|
||||
@@ -47,16 +39,14 @@ describe('resolveHostIdForFingerprint', () => {
|
||||
]
|
||||
|
||||
it('maps a push fingerprint back to the paired host id', () => {
|
||||
expect(resolveHostIdForFingerprint(expectedFingerprint(publicKey), hosts)).toBe('host-1')
|
||||
expect(resolveHostIdForFingerprint('Yw3NKWbEM2aRElRI', hosts)).toBe('host-1')
|
||||
})
|
||||
|
||||
it('returns null for a fingerprint no paired host derives', () => {
|
||||
expect(resolveHostIdForFingerprint('0123456789abcdef', hosts)).toBeNull()
|
||||
})
|
||||
|
||||
it('rejects a fingerprint of the wrong length before hashing anything', () => {
|
||||
expect(
|
||||
resolveHostIdForFingerprint(expectedFingerprint(publicKey).slice(0, 8), hosts)
|
||||
).toBeNull()
|
||||
it('rejects a fingerprint of the wrong length', () => {
|
||||
expect(resolveHostIdForFingerprint('Yw3NKWbEM2aRElRI'.slice(0, 8), hosts)).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -6,20 +6,31 @@ import {
|
||||
|
||||
it('reads a bounded individual notification identity', () => {
|
||||
const identity: PushNotificationIdentity = {
|
||||
notificationId: 'agent:one',
|
||||
notificationEpoch: 'epoch-1',
|
||||
notificationSeq: 7
|
||||
notificationId: 'n'.repeat(2048),
|
||||
notificationEpoch: 'e'.repeat(128),
|
||||
notificationSeq: Number.MAX_SAFE_INTEGER
|
||||
}
|
||||
expect(readPushNotificationIdentity(identity)).toEqual(identity)
|
||||
})
|
||||
|
||||
it('rejects incomplete or non-integral notification identities', () => {
|
||||
it('rejects incomplete or out-of-bounds notification identities', () => {
|
||||
const identity = {
|
||||
notificationId: 'agent:one',
|
||||
notificationEpoch: 'epoch-1',
|
||||
notificationSeq: 7
|
||||
}
|
||||
expect(readPushNotificationIdentity({ notificationId: 'agent:one' })).toBeNull()
|
||||
expect(
|
||||
readPushNotificationIdentity({
|
||||
notificationId: 'agent:one',
|
||||
notificationEpoch: 'epoch-1',
|
||||
notificationSeq: 1.5
|
||||
})
|
||||
).toBeNull()
|
||||
for (const invalid of [
|
||||
{ notificationId: '' },
|
||||
{ notificationId: 'n'.repeat(2049) },
|
||||
{ notificationEpoch: '' },
|
||||
{ notificationEpoch: 'e'.repeat(129) },
|
||||
{ notificationEpoch: undefined },
|
||||
{ notificationSeq: undefined },
|
||||
{ notificationSeq: -1 },
|
||||
{ notificationSeq: 1.5 },
|
||||
{ notificationSeq: Number.MAX_SAFE_INTEGER + 1 }
|
||||
]) {
|
||||
expect(readPushNotificationIdentity({ ...identity, ...invalid })).toBeNull()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -11,18 +11,25 @@ describe('typing latency sample window', () => {
|
||||
source: 'direct',
|
||||
text: 'a',
|
||||
inputToDispatchMs: value,
|
||||
dispatchToParseMs: value,
|
||||
parseToPaintMs: value,
|
||||
inputToPaintMs: value,
|
||||
outputBytes: value,
|
||||
outputWrites: value
|
||||
dispatchToParseMs: value + 10000,
|
||||
parseToPaintMs: value + 20000,
|
||||
inputToPaintMs: 3 * value + 30000,
|
||||
outputBytes: value + 40000,
|
||||
outputWrites: value + 5000
|
||||
})
|
||||
}
|
||||
|
||||
for (const values of Object.values(samples)) {
|
||||
for (const [values, first, last] of [
|
||||
[samples.inputToDispatchMs, 1, 2000],
|
||||
[samples.dispatchToParseMs, 10_001, 12_000],
|
||||
[samples.parseToPaintMs, 20_001, 22_000],
|
||||
[samples.inputToPaintMs, 30_003, 36_000],
|
||||
[samples.outputBytes, 40_001, 42_000],
|
||||
[samples.outputWrites, 5001, 7000]
|
||||
] as const) {
|
||||
expect(values).toHaveLength(2000)
|
||||
expect(values[0]).toBe(1)
|
||||
expect(values.at(-1)).toBe(2000)
|
||||
expect(values[0]).toBe(first)
|
||||
expect(values.at(-1)).toBe(last)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -32,6 +32,7 @@ describe('platform process evidence', () => {
|
||||
platform('linux')
|
||||
read.mockRejectedValueOnce(Object.assign(new Error('gone'), { code: 'ENOENT' }))
|
||||
expect(await readAgentProcess(4242)).toEqual({ verdict: 'exited' })
|
||||
expect(read).toHaveBeenLastCalledWith('/proc/4242/stat', 'utf8')
|
||||
const fields = ['S', ...Array(18).fill('0'), '100']
|
||||
read
|
||||
.mockResolvedValueOnce(`4242 (agent (name)) ${fields.join(' ')}`)
|
||||
@@ -46,6 +47,7 @@ describe('platform process evidence', () => {
|
||||
zombie: false,
|
||||
stopped: false
|
||||
})
|
||||
expect(read).toHaveBeenLastCalledWith('/proc/sys/kernel/random/boot_id', 'utf8')
|
||||
read
|
||||
.mockResolvedValueOnce(`4242 (agent) ${['T', ...fields.slice(1)].join(' ')}`)
|
||||
.mockResolvedValueOnce('boot-id\n')
|
||||
|
||||
@@ -28,12 +28,6 @@ describe('agent status run capability codec', () => {
|
||||
).toEqual(new Set([AGENT_STATUS_RUNS_RUNTIME_CAPABILITY]))
|
||||
})
|
||||
|
||||
it('finds the run capability in the current host capability inventory', () => {
|
||||
expect(
|
||||
hasAgentStatusRunCapability([...RUNTIME_CAPABILITIES, AGENT_STATUS_RUNS_RUNTIME_CAPABILITY])
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it.each([null, {}, 'agent-status.runs.v1', [null], ['']])(
|
||||
'rejects malformed capability envelope %#',
|
||||
(value) => {
|
||||
|
||||
@@ -32,9 +32,7 @@ describe('buildAgentTuiClearInput', () => {
|
||||
})
|
||||
|
||||
it('clears before the cursor before clearing the suffix after it', () => {
|
||||
expect(buildAgentTuiClearInput(4)).toBe(
|
||||
AGENT_TUI_CLEAR_INPUT_LINE.repeat(7) + AGENT_TUI_CLEAR_INPUT_FORWARD.repeat(7)
|
||||
)
|
||||
expect(buildAgentTuiClearInput(4)).toBe('\x15'.repeat(7) + '\x0b'.repeat(7))
|
||||
})
|
||||
|
||||
it('still clears one line for a zero or negative count', () => {
|
||||
|
||||
@@ -107,7 +107,7 @@ describe('cron day restriction (#15896)', () => {
|
||||
|
||||
// The star test is per comma term, so a list that reaches a star anywhere is unrestricted.
|
||||
it('treats a day list containing a star term as a star', () => {
|
||||
expect(matchingDaysOfMay2026('0 9 */3 * 1,5')).toEqual([1, 4, 22, 25])
|
||||
expect(matchingDaysOfMay2026('0 9 2,*/3 * 1,5')).toEqual([1, 4, 22, 25])
|
||||
})
|
||||
|
||||
it('normalizes Sunday from 0, from 7 and from the name', () => {
|
||||
|
||||
@@ -35,7 +35,7 @@ describe('readExternalAutomationJobsFile', () => {
|
||||
).resolves.toEqual([{ id: 'two' }])
|
||||
})
|
||||
|
||||
it('rejects oversized sparse files before reading them wholesale', async () => {
|
||||
it('rejects oversized sparse files with the explicit memory-limit error', async () => {
|
||||
const path = await tempFile('oversized.json')
|
||||
await writeFile(path, '')
|
||||
await truncate(path, EXTERNAL_AUTOMATION_JOBS_FILE_MAX_BYTES + 64 * 1024 * 1024)
|
||||
|
||||
@@ -78,46 +78,69 @@ export type TwinRepoHostCase = {
|
||||
expected: ExecutionHostId
|
||||
}
|
||||
|
||||
const PAIRS: [string, TwinRepoRow, ExecutionHostId, TwinRepoRow, ExecutionHostId][] = [
|
||||
['local+ssh', LOCAL_ROW, 'local', SSH_ROW, 'ssh:box'],
|
||||
['local+runtime', LOCAL_ROW, 'local', RUNTIME_ROW, 'runtime:env-1'],
|
||||
['ssh+runtime', SSH_ROW, 'ssh:box', RUNTIME_ROW, 'runtime:env-1']
|
||||
const PAIRS: [
|
||||
string,
|
||||
TwinRepoRow,
|
||||
ExecutionHostId,
|
||||
ExecutionHostId,
|
||||
TwinRepoRow,
|
||||
ExecutionHostId,
|
||||
ExecutionHostId,
|
||||
ExecutionHostId
|
||||
][] = [
|
||||
['local+ssh', LOCAL_ROW, 'local', 'local', SSH_ROW, 'ssh:box', 'local', 'local'],
|
||||
[
|
||||
'local+runtime',
|
||||
LOCAL_ROW,
|
||||
'local',
|
||||
'local',
|
||||
RUNTIME_ROW,
|
||||
'runtime:env-1',
|
||||
'runtime:env-1',
|
||||
'local'
|
||||
],
|
||||
[
|
||||
'ssh+runtime',
|
||||
SSH_ROW,
|
||||
'ssh:box',
|
||||
'local',
|
||||
RUNTIME_ROW,
|
||||
'runtime:env-1',
|
||||
'runtime:env-1',
|
||||
'ssh:box'
|
||||
]
|
||||
]
|
||||
|
||||
// The window: a contested id goes to the shared primary, a runtime-owned one to its runtime, and
|
||||
// any other (SSH-only, local-only, unknown) to local, because the twin repo names no single host.
|
||||
function windowFresh(hosts: ExecutionHostId[]): ExecutionHostId {
|
||||
if (hosts.length > 1) {
|
||||
return hosts.includes('local')
|
||||
? 'local'
|
||||
: (hosts.find((h) => h.startsWith('ssh:')) ?? hosts[0]!)
|
||||
const GENERATED_CASES: TwinRepoHostCase[] = PAIRS.flatMap(
|
||||
([pair, rowA, hostA, expectedA, rowB, hostB, expectedB, contested]) => {
|
||||
// Both catalog orders, so a case cannot pass by taking the first row.
|
||||
const orders = [
|
||||
[rowB, rowA],
|
||||
[rowA, rowB]
|
||||
]
|
||||
const holdings: [string, ExecutionHostId[], ExecutionHostId][] = [
|
||||
['contested', [hostA, hostB], contested],
|
||||
[`${hostA} only`, [hostA], expectedA],
|
||||
[`${hostB} only`, [hostB], expectedB],
|
||||
['unknown', [], 'local']
|
||||
]
|
||||
return orders.flatMap((repos, order) =>
|
||||
holdings.flatMap(([holding, hosts, expected]) => {
|
||||
const catalog = hosts.map(listedOn)
|
||||
return [
|
||||
{ name: `${pair} #${order} ${holding}, fresh`, repos, catalog, saved: [], expected },
|
||||
{
|
||||
name: `${pair} #${order} ${holding}, saved`,
|
||||
repos,
|
||||
catalog,
|
||||
saved: [expected],
|
||||
expected
|
||||
}
|
||||
]
|
||||
})
|
||||
)
|
||||
}
|
||||
return hosts[0]?.startsWith('runtime:') ? hosts[0] : 'local'
|
||||
}
|
||||
|
||||
const GENERATED_CASES: TwinRepoHostCase[] = PAIRS.flatMap(([pair, rowA, hostA, rowB, hostB]) => {
|
||||
// Both catalog orders, so a case cannot pass by taking the first row.
|
||||
const orders = [
|
||||
[rowB, rowA],
|
||||
[rowA, rowB]
|
||||
]
|
||||
const holdings: [string, ExecutionHostId[]][] = [
|
||||
['contested', [hostA, hostB]],
|
||||
[`${hostA} only`, [hostA]],
|
||||
[`${hostB} only`, [hostB]],
|
||||
['unknown', []]
|
||||
]
|
||||
return orders.flatMap((repos, order) =>
|
||||
holdings.flatMap(([holding, hosts]) => {
|
||||
const expected = windowFresh(hosts)
|
||||
const catalog = hosts.map(listedOn)
|
||||
return [
|
||||
{ name: `${pair} #${order} ${holding}, fresh`, repos, catalog, saved: [], expected },
|
||||
{ name: `${pair} #${order} ${holding}, saved`, repos, catalog, saved: [expected], expected }
|
||||
]
|
||||
})
|
||||
)
|
||||
})
|
||||
)
|
||||
|
||||
export const TWIN_REPO_HOST_CASES: TwinRepoHostCase[] = [
|
||||
...GENERATED_CASES,
|
||||
|
||||
Reference in New Issue
Block a user