Files
orca/src/main/ipc/telemetry.test.ts
T
NeilandOrca e33b2006f4 Remove stale max-lines lint disables from files under the limit (#7548)
110 files carried an eslint/oxlint-disable max-lines directive but are
already under the default max-lines budget (300 .ts / 400 .tsx / 600 .mjs
/ 800 test), so the suppression is dead. Removing it restores real
max-lines coverage on these files with zero behavior change.

Each removed directive had max-lines as its only rule; verified via a
full oxlint run (0 max-lines violations, 0 new errors). Diff is pure
deletions (200 lines, 0 additions) — no code touched.

Co-authored-by: Orca <help@stably.ai>
2026-07-06 02:12:32 -07:00

519 lines
22 KiB
TypeScript

// IPC boundary behavior for the telemetry surface. Strict type narrows must
// drop obviously-malformed calls before they reach the validator (the
// renderer is in the threat model). Pins the consent-mutation rate limit:
// ≤5 consent-related IPC calls per session. Pins the main-side `via`
// derivation: both `OptInVia` branches are reachable from renderer input,
// and the one path that must NOT emit (`acknowledgeBanner`) has its own
// channel and handler rather than being a flag on `setOptIn`.
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type { GlobalSettings } from '../../shared/types'
import type { Store } from '../persistence'
const handlers = new Map<string, (_event: unknown, ...args: unknown[]) => unknown>()
const {
handleMock,
trackMock,
setOptInMock,
persistBannerAcknowledgeMock,
consumeConsentMutationTokenMock,
getCohortAtEmitMock,
getOnboardingCohortAtEmitMock
} = vi.hoisted(() => ({
handleMock: vi.fn(),
trackMock: vi.fn(),
setOptInMock: vi.fn(),
persistBannerAcknowledgeMock: vi.fn(),
consumeConsentMutationTokenMock: vi.fn(),
getCohortAtEmitMock: vi.fn(),
getOnboardingCohortAtEmitMock: vi.fn()
}))
vi.mock('electron', () => ({ ipcMain: { handle: handleMock } }))
vi.mock('../telemetry/client', () => ({
track: trackMock,
setOptIn: setOptInMock,
persistBannerAcknowledgeWithoutEmitting: persistBannerAcknowledgeMock
}))
vi.mock('../telemetry/burst-cap', () => ({
consumeConsentMutationToken: consumeConsentMutationTokenMock
}))
vi.mock('../telemetry/cohort-classifier', () => ({
getCohortAtEmit: getCohortAtEmitMock
}))
vi.mock('../telemetry/onboarding-cohort-classifier', () => ({
getOnboardingCohortAtEmit: getOnboardingCohortAtEmitMock
}))
import { _resetStoreForTests, registerTelemetryHandlers } from './telemetry'
function captureHandlers(): void {
handlers.clear()
for (const call of handleMock.mock.calls) {
const [channel, handler] = call as [
string,
typeof handlers extends Map<string, infer V> ? V : never
]
handlers.set(channel, handler)
}
}
// Build a fake Store with a settable `telemetry` block. Tests reassign
// `settings.telemetry` between handler invocations to seed the two
// derivation states.
type FakeStoreState = { settings: GlobalSettings }
function makeFakeStore(telemetry: GlobalSettings['telemetry']): {
store: Store
state: FakeStoreState
} {
const state: FakeStoreState = { settings: { telemetry } as unknown as GlobalSettings }
const store = {
getSettings: vi.fn(() => state.settings),
updateSettings: vi.fn((updates: Partial<GlobalSettings>) => {
state.settings = { ...state.settings, ...updates } as GlobalSettings
return state.settings
})
} as unknown as Store
return { store, state }
}
function registerWith(telemetry: GlobalSettings['telemetry']): FakeStoreState {
const { store, state } = makeFakeStore(telemetry)
registerTelemetryHandlers(store)
captureHandlers()
return state
}
describe('telemetry IPC handlers', () => {
beforeEach(() => {
handleMock.mockReset()
trackMock.mockReset()
setOptInMock.mockReset()
persistBannerAcknowledgeMock.mockReset()
consumeConsentMutationTokenMock.mockReset()
consumeConsentMutationTokenMock.mockReturnValue(true)
getCohortAtEmitMock.mockReset()
getCohortAtEmitMock.mockReturnValue({ nth_repo_added: 0 })
getOnboardingCohortAtEmitMock.mockReset()
getOnboardingCohortAtEmitMock.mockReturnValue({ cohort: undefined })
_resetStoreForTests()
})
afterEach(() => {
vi.restoreAllMocks()
})
it('registers all four channels', () => {
registerWith({
installId: 'x',
existedBeforeTelemetryRelease: false,
optedIn: true
})
expect(handlers.has('telemetry:track')).toBe(true)
expect(handlers.has('telemetry:setOptIn')).toBe(true)
expect(handlers.has('telemetry:acknowledgeBanner')).toBe(true)
expect(handlers.has('telemetry:getConsentState')).toBe(true)
})
// ── telemetry:track ──────────────────────────────────────────────────
it('forwards a well-typed track call to track() and injects cohort for COHORT_EXTENDED events', () => {
registerWith({ installId: 'x', existedBeforeTelemetryRelease: false, optedIn: true })
getCohortAtEmitMock.mockReturnValue({ nth_repo_added: 2 })
const handler = handlers.get('telemetry:track')!
handler({}, 'app_opened', {})
expect(trackMock).toHaveBeenCalledTimes(1)
expect(trackMock).toHaveBeenCalledWith('app_opened', { nth_repo_added: 2 })
})
// The IPC handler's selectivity is load-bearing: schemas are `.strict()`,
// so injecting `nth_repo_added` on a non-cohort event would silently
// drop the entire event at the validator. Events outside `COHORT_EXTENDED`
// must reach `track()` unmodified.
it('does NOT inject cohort on events outside COHORT_EXTENDED', () => {
registerWith({ installId: 'x', existedBeforeTelemetryRelease: false, optedIn: true })
const handler = handlers.get('telemetry:track')!
handler({}, 'settings_changed', { setting_key: 'editorAutoSave', value_kind: 'bool' })
expect(trackMock).toHaveBeenCalledTimes(1)
expect(trackMock).toHaveBeenCalledWith('settings_changed', {
setting_key: 'editorAutoSave',
value_kind: 'bool'
})
expect(getCohortAtEmitMock).not.toHaveBeenCalled()
})
// The renderer-only Setup-step events fire from React `onClick` and
// depend on the IPC handler injecting cohort — call sites stay
// synchronous and pass only their own props.
it('injects cohort for add_repo_setup_step_action (renderer-only event)', () => {
registerWith({ installId: 'x', existedBeforeTelemetryRelease: false, optedIn: true })
getCohortAtEmitMock.mockReturnValue({ nth_repo_added: 1 })
const handler = handlers.get('telemetry:track')!
handler({}, 'add_repo_setup_step_action', { action: 'skip' })
expect(trackMock).toHaveBeenCalledWith('add_repo_setup_step_action', {
action: 'skip',
nth_repo_added: 1
})
})
it('drops main-owned events from renderer telemetry IPC', () => {
registerWith({ installId: 'x', existedBeforeTelemetryRelease: false, optedIn: true })
const handler = handlers.get('telemetry:track')!
handler({}, 'app_starred_orca', { source: 'settings' })
handler({}, 'star_nag_outcome', {
outcome: 'shown',
source: 'threshold',
mode: 'gh',
threshold: 35,
agents_since_baseline: 35,
agents_since_baseline_bucket: '35-69'
})
handler({}, 'feature_interaction_usage_bucket_reached', {
feature_id: 'tasks',
feature_category: 'task_management',
count_bucket: 'count_1',
bucket_source: 'crossed_now'
})
expect(trackMock).not.toHaveBeenCalled()
expect(getCohortAtEmitMock).not.toHaveBeenCalled()
})
it('injects cohort for setup script prompt events', () => {
registerWith({ installId: 'x', existedBeforeTelemetryRelease: false, optedIn: true })
getCohortAtEmitMock.mockReturnValue({ nth_repo_added: 3 })
const handler = handlers.get('telemetry:track')!
handler({}, 'setup_script_prompt_shown', {
mode: 'import_available',
provider: 'codex',
file_count_bucket: '1',
unsupported_field_count_bucket: '0',
has_shared_hooks: false
})
handler({}, 'setup_script_prompt_action', {
action: 'configure_clicked',
mode: 'configure_needed',
file_count_bucket: '0',
unsupported_field_count_bucket: '0',
has_shared_hooks: true
})
expect(trackMock).toHaveBeenCalledWith('setup_script_prompt_shown', {
mode: 'import_available',
provider: 'codex',
file_count_bucket: '1',
unsupported_field_count_bucket: '0',
has_shared_hooks: false,
nth_repo_added: 3
})
expect(trackMock).toHaveBeenCalledWith('setup_script_prompt_action', {
action: 'configure_clicked',
mode: 'configure_needed',
file_count_bucket: '0',
unsupported_field_count_bucket: '0',
has_shared_hooks: true,
nth_repo_added: 3
})
})
// Fail-soft: a degraded classifier returns `{ nth_repo_added: undefined }`.
// The schemas declare the field optional, so the event still validates.
it('forwards undefined cohort when the classifier returns undefined', () => {
registerWith({ installId: 'x', existedBeforeTelemetryRelease: false, optedIn: true })
getCohortAtEmitMock.mockReturnValue({ nth_repo_added: undefined })
const handler = handlers.get('telemetry:track')!
handler({}, 'app_opened', {})
expect(trackMock).toHaveBeenCalledWith('app_opened', { nth_repo_added: undefined })
})
// Threat-model parity with the cohort override test: a compromised
// renderer must NOT be able to forge `nth_repo_added` either. The same
// spread-order invariant applies — `{ ...baseProps, ...getCohortAtEmit() }`
// — and the same future-refactor regression risk exists. Pinning both
// fields keeps the threat model symmetric.
it('main-derived nth_repo_added overrides renderer-supplied value', () => {
registerWith({ installId: 'x', existedBeforeTelemetryRelease: false, optedIn: true })
getCohortAtEmitMock.mockReturnValue({ nth_repo_added: 2 })
const handler = handlers.get('telemetry:track')!
handler({}, 'app_opened', { nth_repo_added: 99 })
expect(trackMock).toHaveBeenCalledWith('app_opened', { nth_repo_added: 2 })
})
// ── Onboarding cohort injection (mirrors the nth_repo_added pattern) ──
it('injects onboarding cohort on events whose schema declares cohort', () => {
registerWith({ installId: 'x', existedBeforeTelemetryRelease: false, optedIn: true })
getOnboardingCohortAtEmitMock.mockReturnValue({ cohort: 'fresh_install' })
const handler = handlers.get('telemetry:track')!
handler({}, 'onboarding_step_viewed', { step: 1, value_kind: 'agent' })
expect(trackMock).toHaveBeenCalledWith('onboarding_step_viewed', {
step: 1,
value_kind: 'agent',
cohort: 'fresh_install'
})
})
it('does NOT inject onboarding cohort on non-onboarding events', () => {
registerWith({ installId: 'x', existedBeforeTelemetryRelease: false, optedIn: true })
const handler = handlers.get('telemetry:track')!
handler({}, 'settings_changed', { setting_key: 'editorAutoSave', value_kind: 'bool' })
expect(getOnboardingCohortAtEmitMock).not.toHaveBeenCalled()
})
it('forwards undefined onboarding cohort fail-soft', () => {
registerWith({ installId: 'x', existedBeforeTelemetryRelease: true, optedIn: null })
getOnboardingCohortAtEmitMock.mockReturnValue({ cohort: undefined })
const handler = handlers.get('telemetry:track')!
handler({}, 'onboarding_started', {})
expect(trackMock).toHaveBeenCalledWith('onboarding_started', { cohort: undefined })
})
// Threat-model invariant: a compromised renderer must NOT be able to forge
// `cohort` by including it in the props payload. The IPC handler spreads
// the main-derived cohort AFTER the caller-supplied props, so the main
// value wins. This test pins that invariant — flipping the spread order
// would silently let a compromised renderer fake any cohort value.
it('main-derived cohort overrides renderer-supplied cohort', () => {
registerWith({ installId: 'x', existedBeforeTelemetryRelease: false, optedIn: true })
getOnboardingCohortAtEmitMock.mockReturnValue({ cohort: 'fresh_install' })
const handler = handlers.get('telemetry:track')!
// Caller tries to forge cohort='upgrade_backfill'; main must overwrite.
handler({}, 'onboarding_started', { cohort: 'upgrade_backfill' })
expect(trackMock).toHaveBeenCalledWith('onboarding_started', {
cohort: 'fresh_install'
})
})
// Threat-model invariant under degraded classifier: a compromised
// renderer must NOT be able to forge `cohort` even when the classifier
// fails soft to `{ cohort: undefined }`. The IPC handler spreads the
// classifier output AFTER the caller-supplied props, so an explicit
// `undefined` from the classifier still overwrites a forged value. A
// future refactor that switches the spread to a conditional assign
// (`if (c.cohort !== undefined) baseProps.cohort = c.cohort`) would
// silently regress this — pinning it here.
it('main-derived undefined cohort overrides renderer-supplied cohort (degraded classifier)', () => {
registerWith({ installId: 'x', existedBeforeTelemetryRelease: true, optedIn: true })
getOnboardingCohortAtEmitMock.mockReturnValue({ cohort: undefined })
const handler = handlers.get('telemetry:track')!
// Compromised renderer attempts to forge cohort='upgrade_backfill';
// main strips it via the explicit-undefined spread.
handler({}, 'onboarding_started', { cohort: 'upgrade_backfill' })
expect(trackMock).toHaveBeenCalledWith('onboarding_started', {
cohort: undefined
})
})
it('drops track calls with a non-string name', () => {
registerWith({ installId: 'x', existedBeforeTelemetryRelease: false, optedIn: true })
const handler = handlers.get('telemetry:track')!
handler({}, 42, {})
handler({}, null, {})
handler({}, { event: 'app_opened' }, {})
expect(trackMock).not.toHaveBeenCalled()
})
it('drops track calls with non-object props', () => {
registerWith({ installId: 'x', existedBeforeTelemetryRelease: false, optedIn: true })
const handler = handlers.get('telemetry:track')!
handler({}, 'app_opened', 'string-not-object')
handler({}, 'app_opened', 42)
expect(trackMock).not.toHaveBeenCalled()
})
it('treats null/undefined props as an empty object', () => {
registerWith({ installId: 'x', existedBeforeTelemetryRelease: false, optedIn: true })
getCohortAtEmitMock.mockReturnValue({ nth_repo_added: 0 })
const handler = handlers.get('telemetry:track')!
handler({}, 'app_opened', null)
handler({}, 'app_opened', undefined)
expect(trackMock).toHaveBeenCalledTimes(2)
expect(trackMock).toHaveBeenNthCalledWith(1, 'app_opened', { nth_repo_added: 0 })
expect(trackMock).toHaveBeenNthCalledWith(2, 'app_opened', { nth_repo_added: 0 })
})
// ── telemetry:setOptIn — input narrowing ─────────────────────────────
it('drops setOptIn with non-boolean optedIn', () => {
registerWith({ installId: 'x', existedBeforeTelemetryRelease: false, optedIn: true })
const handler = handlers.get('telemetry:setOptIn')!
handler({}, 'true')
handler({}, 1)
handler({}, null)
handler({}, undefined)
expect(setOptInMock).not.toHaveBeenCalled()
// None of these should have consumed a mutation token either.
expect(consumeConsentMutationTokenMock).not.toHaveBeenCalled()
})
it('drops setOptIn past the consent-mutation rate limit', () => {
registerWith({ installId: 'x', existedBeforeTelemetryRelease: false, optedIn: true })
const handler = handlers.get('telemetry:setOptIn')!
consumeConsentMutationTokenMock.mockReturnValue(false)
handler({}, true)
expect(setOptInMock).not.toHaveBeenCalled()
})
// ── telemetry:setOptIn — `via` derivation ────────────────────────────
it("derives via='first_launch_banner' for an existing user with optedIn=null clicking Turn off", () => {
// Existing-user notice is the only path where an existing user (cohort
// marker true) with optedIn=null flips to false. That is the contract
// the notice's "Turn off" button routes through.
registerWith({
installId: 'x',
existedBeforeTelemetryRelease: true,
optedIn: null
})
const handler = handlers.get('telemetry:setOptIn')!
handler({}, false)
expect(setOptInMock).toHaveBeenCalledWith('first_launch_banner', false)
})
it("derives via='settings' (not 'first_launch_banner') for a defensive opt-in call from the pre-notice state", () => {
// Defensive: the notice's opt-in path is the ✕ (silent acknowledge),
// which does NOT route through setOptIn. A compromised renderer
// could try to call telemetrySetOptIn(true) in the pre-notice state
// and synthesize a spurious telemetry_opted_in { via:
// 'first_launch_banner' }. The derivation must refuse that tag for
// the true-incoming case and fall through to 'settings'.
registerWith({
installId: 'x',
existedBeforeTelemetryRelease: true,
optedIn: null
})
const handler = handlers.get('telemetry:setOptIn')!
handler({}, true)
expect(setOptInMock).toHaveBeenCalledWith('settings', true)
})
it("derives via='settings' for a new user toggling off from Settings (no first-launch surface exists)", () => {
// New users (existedBeforeTelemetryRelease=false) are initialized with
// optedIn=true at migration and see no first-launch surface. Any
// opt-out from this cohort routes through Settings → Privacy and
// must tag as `via: 'settings'`.
registerWith({
installId: 'x',
existedBeforeTelemetryRelease: false,
optedIn: true
})
const handler = handlers.get('telemetry:setOptIn')!
handler({}, false)
expect(setOptInMock).toHaveBeenCalledWith('settings', false)
})
it("derives via='settings' for an opt-in toggle flip after a prior opt-out", () => {
// User flipped off in Settings, flipping back on in Settings. Neither
// cohort marker nor notice state triggers a first-launch tag.
registerWith({
installId: 'x',
existedBeforeTelemetryRelease: true,
optedIn: false
})
const handler = handlers.get('telemetry:setOptIn')!
handler({}, true)
expect(setOptInMock).toHaveBeenCalledWith('settings', true)
})
it("derives via='settings' for a new user flipping Settings off→on (not a first-launch interaction)", () => {
registerWith({
installId: 'x',
existedBeforeTelemetryRelease: false,
optedIn: false
})
const handler = handlers.get('telemetry:setOptIn')!
handler({}, true)
expect(setOptInMock).toHaveBeenCalledWith('settings', true)
})
it("derives via='settings' when the telemetry block is missing (defensive)", () => {
// Should never happen post-migration, but if it does the handler must
// fall through to 'settings' rather than throwing or mis-tagging.
registerWith(undefined)
const handler = handlers.get('telemetry:setOptIn')!
handler({}, true)
expect(setOptInMock).toHaveBeenCalledWith('settings', true)
})
// ── telemetry:acknowledgeBanner — silent-persist path ────────────────
it('routes banner ✕ through persistBannerAcknowledgeWithoutEmitting without invoking setOptIn', () => {
// This is the whole point of the separate channel: the silent-persist
// path MUST NOT reach setOptIn, which would derive a `via` and fire
// `telemetry_opted_in`. The client primitive may unlock `app_opened`,
// but the acknowledge channel itself must not emit an opt-in event.
registerWith({
installId: 'x',
existedBeforeTelemetryRelease: true,
optedIn: null
})
const handler = handlers.get('telemetry:acknowledgeBanner')!
handler({})
expect(persistBannerAcknowledgeMock).toHaveBeenCalledTimes(1)
expect(setOptInMock).not.toHaveBeenCalled()
expect(trackMock).not.toHaveBeenCalled()
})
it('acknowledgeBanner consumes a consent-mutation token and drops past the cap', () => {
registerWith({
installId: 'x',
existedBeforeTelemetryRelease: true,
optedIn: null
})
const handler = handlers.get('telemetry:acknowledgeBanner')!
consumeConsentMutationTokenMock.mockReturnValue(false)
handler({})
expect(persistBannerAcknowledgeMock).not.toHaveBeenCalled()
})
// ── telemetry:acknowledgeBanner — state-precondition guard ───────────
// These tests pin the guard, which rejects any cohort/optedIn
// combination other than (existed=true, optedIn=null). The guard is
// the defense against a compromised renderer silently flipping
// optedIn=true for a user who already resolved consent — a future
// refactor that weakens it must fail here. The guard also runs BEFORE
// consumeConsentMutationToken, so a rejected call must not burn a
// token either.
it('acknowledgeBanner rejects an existing user who already opted in', () => {
registerWith({
installId: 'x',
existedBeforeTelemetryRelease: true,
optedIn: true
})
const handler = handlers.get('telemetry:acknowledgeBanner')!
handler({})
expect(persistBannerAcknowledgeMock).not.toHaveBeenCalled()
expect(consumeConsentMutationTokenMock).not.toHaveBeenCalled()
})
it('acknowledgeBanner rejects an existing user who already opted out', () => {
registerWith({
installId: 'x',
existedBeforeTelemetryRelease: true,
optedIn: false
})
const handler = handlers.get('telemetry:acknowledgeBanner')!
handler({})
expect(persistBannerAcknowledgeMock).not.toHaveBeenCalled()
expect(consumeConsentMutationTokenMock).not.toHaveBeenCalled()
})
it('acknowledgeBanner rejects the new-user cohort regardless of optedIn', () => {
registerWith({
installId: 'x',
existedBeforeTelemetryRelease: false,
optedIn: true
})
const handler = handlers.get('telemetry:acknowledgeBanner')!
handler({})
expect(persistBannerAcknowledgeMock).not.toHaveBeenCalled()
expect(consumeConsentMutationTokenMock).not.toHaveBeenCalled()
})
it('acknowledgeBanner rejects a missing telemetry block', () => {
registerWith(undefined)
const handler = handlers.get('telemetry:acknowledgeBanner')!
handler({})
expect(persistBannerAcknowledgeMock).not.toHaveBeenCalled()
expect(consumeConsentMutationTokenMock).not.toHaveBeenCalled()
})
})