Files
lefarcen 7a4175c86f feat(analytics): report why an installed agent CLI cannot be used (#7285)
* feat(analytics): report why an installed agent CLI cannot be used

A user installed DeepSeek Harness correctly and OpenDesign showed nothing.
We only learned about it because they said so and sent a diagnostics zip:
detection is the one stage that knows an installed CLI is unusable, and it
told nobody. There is no way today to ask how many installs are in that
state, or which CLI, or why.

Report each diagnostic detection produces, once per (agent, reason,
version) per page session. Detection runs on cold start and on every
rescan, so counting passes would turn one broken install into dozens of
events; deduped, the number means "installs affected", which is the
denominator a decision here actually needs. Warnings count too — an
`untested-version` spike across many installs is how we would learn our
supported-version list had fallen behind our own installer, which is
precisely what went unnoticed.

The resolved path is deliberately absent: an agent binary path contains
the OS username. `has_path` carries the only part that matters, since the
picker hides a row that has none.

Half the registry was invisible to this before it started. `TrackingCliProviderId`
covered 14 ids while the daemon ships 28, so DeepSeek Harness and a dozen
others collapsed into `other` — a breakdown that cannot name the failing
CLI answers nothing. Every shipped agent now has an id, and a test fails
when a new runtime def arrives without one.

* test(runtimes): assert the shipped agents, not the machine's

The tracking-id guard walked AGENT_DEFS, which appends whatever local
profiles the developer declared in their own agents.local.json. Those ids
are required not to collide with a shipped one, so they correctly collapse
to `other` — and turned anyone's personal config into a failure reading
`expected [ 'my-custom-agent' ] to deeply equal []`, indistinguishable from
the missing-mapper regression the guard exists to catch.

Export the shipped list under a name that says which question it answers,
and assert on that. The unknown-id case stays: a locally declared agent is
not ours to name, and PostHog should never learn an id out of someone's
config file.

* test(runtimes): pin the local-profile case in the repo, not in my shell

The isolation fix was verified by pointing OD_AGENT_PROFILES_CONFIG at a
throwaway file by hand, which proves it once and protects nothing. Build
the profile inside the test instead, so the machine-dependent failure the
guard used to produce cannot come back unnoticed.

Registry lists are built at module load, so the profile is written before
a fresh dynamic import. The case asserts the profile actually reached
AGENT_DEFS before asserting it stayed out of SHIPPED_AGENT_DEFS — without
that, it would pass just as happily if the profile never loaded at all.
2026-08-24 01:42:23 +00:00

110 lines
3.5 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from 'vitest';
import type { AgentInfo } from '@open-design/contracts';
import {
reportAgentDetectDiagnostics,
resetAgentDetectDiagnosticReporting,
} from '../../src/analytics/agent-detect';
function agent(overrides: Partial<AgentInfo> = {}): AgentInfo {
return {
id: 'deepseek-harness',
name: 'DeepSeek Harness',
bin: 'dsh',
available: false,
...overrides,
};
}
describe('agent detection diagnostics reporting', () => {
beforeEach(() => {
resetAgentDetectDiagnosticReporting();
});
it('names the agent instead of collapsing it into "other"', () => {
const track = vi.fn();
reportAgentDetectDiagnostics(track, [
agent({
path: '/Users/someone/.local/bin/dsh',
version: '0.1.0-rc.8',
diagnostics: [
{ reason: 'untested-version', severity: 'warning', message: 'untested' },
],
}),
]);
expect(track).toHaveBeenCalledTimes(1);
expect(track.mock.calls[0]?.[0]).toBe('agent_detect_diagnostic');
expect(track.mock.calls[0]?.[1]).toMatchObject({
area: 'runtime_detection',
cli_provider_id: 'deepseek_harness',
reason: 'untested-version',
severity: 'warning',
agent_available: false,
agent_version: '0.1.0-rc.8',
has_path: true,
});
});
// An agent binary path contains the OS username, so it must never leave the
// machine. `has_path` carries the only thing the analysis needs: whether the
// row was renderable at all.
it('never sends the resolved path', () => {
const track = vi.fn();
reportAgentDetectDiagnostics(track, [
agent({
path: '/Users/someone/.local/bin/dsh',
diagnostics: [{ reason: 'shim-broken', severity: 'error', message: 'broken' }],
}),
]);
expect(JSON.stringify(track.mock.calls[0]?.[1])).not.toContain('someone');
expect(track.mock.calls[0]?.[1]).toMatchObject({ has_path: true });
});
// Detection runs on cold start and on every rescan. Counting each pass would
// turn one broken install into dozens of events and make the number mean
// "times Settings was opened" instead of "installs affected".
it('reports one install once, however many times detection runs', () => {
const track = vi.fn();
const scanned = [
agent({
version: '0.1.0-rc.6',
diagnostics: [{ reason: 'shim-broken', severity: 'error', message: 'broken' }],
}),
];
reportAgentDetectDiagnostics(track, scanned);
reportAgentDetectDiagnostics(track, scanned);
reportAgentDetectDiagnostics(track, scanned);
expect(track).toHaveBeenCalledTimes(1);
});
// ...but a user who upgrades the CLI and still fails is a different fact.
it('reports again when the version changed', () => {
const track = vi.fn();
const diagnostics: AgentInfo['diagnostics'] = [
{ reason: 'shim-broken', severity: 'error', message: 'broken' },
];
reportAgentDetectDiagnostics(track, [agent({ version: '0.1.0-rc.6', diagnostics })]);
reportAgentDetectDiagnostics(track, [agent({ version: '0.1.0-rc.8', diagnostics })]);
expect(track).toHaveBeenCalledTimes(2);
});
it('ignores healthy agents and info-level notes', () => {
const track = vi.fn();
reportAgentDetectDiagnostics(track, [
agent({ id: 'claude', available: true }),
agent({
id: 'codex',
available: true,
diagnostics: [{ reason: 'auth-unknown', severity: 'info', message: 'fyi' }],
}),
]);
expect(track).not.toHaveBeenCalled();
});
});