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:
@@ -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.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user