refactor(telemetry): keep frozen-consent precedence coverage-enforced

Split the deferred resolver into its own function so the v8 ignore covers
only the production resolver arm, leaving the security-relevant
deps.consent precedence branch enforced at 100% branch coverage. Assert
the frozen-allow path so both sides of that branch are exercised, and
document the caller obligation on reportCommandTelemetry and
resolveTelemetryConsent.
This commit is contained in:
Chinesezjc
2026-08-11 15:23:19 +08:00
parent ec236b273e
commit adcba3ed96
3 changed files with 34 additions and 3 deletions
+17 -3
View File
@@ -59,15 +59,18 @@ export function freezeTelemetryConsent(env: NodeJS.ProcessEnv = process.env): Co
* sends before returning. Swallows every error so telemetry can never change a
* command's result.
* @param event - the command lifecycle facts.
* @param deps - Consent and delivery hooks; defaults hit the real endpoint.
* @param deps - Consent and delivery hooks; defaults hit the real endpoint. Pass
* `consent` frozen from the launching environment before the command ran:
* without it this resolves `process.env` as it stands now, which a project
* `.env` or project code may already have changed.
*/
export async function reportCommandTelemetry(
event: CommandTelemetryEvent,
deps: CommandTelemetryDeps = {},
): Promise<void> {
try {
/* v8 ignore next -- the production resolver is exercised by its owning tests */
const consent = deps.consent ?? await (deps.resolve?.() ?? resolveTelemetryConsent())
const consent = deps.consent ?? await resolveDeferredConsent(deps)
/* v8 ignore next -- v8 mis-accounts this early return's implicit else; both outcomes are asserted */
if (!consent.allowed) return
const payload = await buildTelemetryPayload({
command: event.command,
@@ -83,3 +86,14 @@ export async function reportCommandTelemetry(
// Telemetry is best-effort; a consent, payload, or delivery fault never reaches the command.
}
}
/**
* Resolve consent for a caller that supplied no frozen decision, reading the
* environment as it stands after the command ran.
* @param deps - the caller's consent hooks.
* @returns the resolved consent decision.
*/
async function resolveDeferredConsent(deps: CommandTelemetryDeps): Promise<ConsentDecision> {
/* v8 ignore next -- the production resolver is exercised by its owning tests */
return await (deps.resolve?.() ?? resolveTelemetryConsent())
}
@@ -647,6 +647,18 @@ describe('command telemetry', () => {
)
expect(sent).toHaveLength(0)
expect(resolved).toBe(0)
// A frozen FULL decision reports without consulting the resolver either.
await reportCommandTelemetry(
{ command: 'start', cwd: dir, durationMs: 5, success: true },
{
consent: { allowed: true, reason: 'FULL' },
resolve: () => { resolved += 1; return { allowed: false, reason: 'DISABLED' } },
reporter,
},
)
expect(sent).toHaveLength(1)
expect(resolved).toBe(0)
})
it('freezes consent from the launching environment and denies unsupported modes', () => {
@@ -24,6 +24,11 @@ export interface ConsentDecision {
/**
* Resolve launcher telemetry consent from the shared telemetry mode.
*
* Callers that wrap a command must read the launching environment before that
* command runs: a project `.env` load or project code can change
* `process.env`, and resolving afterwards would let the project authorize
* reporting of its own configuration.
* @param env - Environment containing `DSH_TELEMETRY_MODE`; defaults to `process.env`.
* @returns Whether launcher telemetry may report and the resolved mode.
*/