Skip to content

Commit 3bd8d11

Browse files
committed
refactor: simplify CLI telemetry wrappers and share test harness
- Make fetchBinary/configure thin telemetry wrappers over private impls; thread trace as the trailing arg and group value params into an options object - Inline getDownloadAction and fold cli.ts phase helpers (downloadDecision, tracedPhase) for a smaller instrumentation surface - Categorize configure failures via a typed CredentialFileError instead of error-shape heuristics - Extract remote.setup compatibility check into checkCompatibility - Share a setupCliManager() harness across the cliManager unit and telemetry tests; per-test setup with local mocks, no beforeEach
1 parent 8315267 commit 3bd8d11

7 files changed

Lines changed: 857 additions & 830 deletions

File tree

src/core/cliCredentialManager.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,20 @@ type KeyringFeature = "keyringAuth" | "keyringTokenRead";
2727
const EXEC_TIMEOUT_MS = 60_000;
2828
const EXEC_LOG_INTERVAL_MS = 5_000;
2929

30+
/**
31+
* Thrown when writing the plaintext credential files fails, distinguishing a
32+
* filesystem failure from a keyring/CLI one.
33+
*/
34+
export class CredentialFileError extends Error {
35+
public constructor(cause: unknown) {
36+
super(
37+
cause instanceof Error ? cause.message : "Failed to write credentials",
38+
{ cause },
39+
);
40+
this.name = "CredentialFileError";
41+
}
42+
}
43+
3044
/**
3145
* Resolves a CLI binary path for a given deployment URL, fetching/downloading
3246
* if needed. Returns the path or throws if unavailable.
@@ -71,7 +85,11 @@ export class CliCredentialManager {
7185
"keyringAuth",
7286
);
7387
if (!binPath) {
74-
await this.writeCredentialFiles(url, token);
88+
try {
89+
await this.writeCredentialFiles(url, token);
90+
} catch (error) {
91+
throw new CredentialFileError(error);
92+
}
7593
return;
7694
}
7795

src/core/cliManager.ts

Lines changed: 120 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
CliDownloadsDisabledError,
1515
CliFallbackDeclinedError,
1616
CliTelemetry,
17+
type CliConfigureTrace,
1718
type CliDownloadAction,
1819
type CliDownloadReason,
1920
type CliVersionCheckOutcome,
@@ -55,16 +56,6 @@ type SingleVerifyResult =
5556
| { kind: "bypassed" }
5657
| { kind: "sig_unavailable"; status: number };
5758

58-
function getDownloadAction(
59-
downloadsEnabled: boolean,
60-
hasExistingBinary: boolean,
61-
): CliDownloadAction {
62-
if (downloadsEnabled) {
63-
return "download";
64-
}
65-
return hasExistingBinary ? "fallback" : "blocked";
66-
}
67-
6859
export class CliManager {
6960
private readonly binaryLock: BinaryLock;
7061
private readonly cliTelemetry: CliTelemetry;
@@ -142,73 +133,82 @@ export class CliManager {
142133
* unable to download a working binary, whether because of network issues or
143134
* downloads being disabled.
144135
*/
145-
public async fetchBinary(restClient: Api): Promise<string> {
146-
return this.cliTelemetry.resolve(async (trace) => {
147-
const baseUrl = restClient.getAxiosInstance().defaults.baseURL;
148-
if (!baseUrl) {
149-
trace.setFailure("unknown");
150-
throw new Error("REST client has no base URL configured");
151-
}
152-
const safeHostname = toSafeHost(baseUrl);
153-
const cfg = vscode.workspace.getConfiguration("coder");
154-
// Settings can be undefined when set to their defaults (true in this
155-
// case), so explicitly check against false.
156-
const enableDownloads = cfg.get("enableDownloads") !== false;
157-
this.output.debug(
158-
"Downloads are",
159-
enableDownloads ? "enabled" : "disabled",
160-
);
136+
public fetchBinary(restClient: Api): Promise<string> {
137+
return this.cliTelemetry.resolve((trace) =>
138+
this.resolveBinary(restClient, trace),
139+
);
140+
}
161141

162-
const resolved = await trace.cacheLookup(() =>
163-
this.lookupBinary(safeHostname),
164-
);
165-
const { buildInfo, parsedVersion, existingVersion, downloadReason } =
166-
await trace.versionCheck(() =>
167-
this.checkResolvedBinary(restClient, resolved),
168-
);
142+
private async resolveBinary(
143+
restClient: Api,
144+
trace: CliResolveTrace,
145+
): Promise<string> {
146+
const baseUrl = restClient.getAxiosInstance().defaults.baseURL;
147+
if (!baseUrl) {
148+
trace.setFailure("unknown");
149+
throw new Error("REST client has no base URL configured");
150+
}
151+
const safeHostname = toSafeHost(baseUrl);
152+
const cfg = vscode.workspace.getConfiguration("coder");
153+
// Settings can be undefined when set to their defaults (true in this
154+
// case), so explicitly check against false.
155+
const enableDownloads = cfg.get("enableDownloads") !== false;
156+
this.output.debug(
157+
"Downloads are",
158+
enableDownloads ? "enabled" : "disabled",
159+
);
169160

170-
if (existingVersion === buildInfo.version) {
171-
this.output.debug("Existing binary matches server version");
172-
trace.setOutcome("cache_hit");
173-
return resolved.binPath;
174-
}
161+
const resolved = await trace.cacheLookup(() =>
162+
this.lookupBinary(safeHostname),
163+
);
164+
const { buildInfo, parsedVersion, existingVersion, downloadReason } =
165+
await trace.versionCheck(() =>
166+
this.checkResolvedBinary(restClient, resolved),
167+
);
175168

176-
await trace.recordDownloadDecision({
177-
reason: downloadReason,
178-
action: getDownloadAction(enableDownloads, existingVersion !== null),
179-
});
169+
if (existingVersion === buildInfo.version) {
170+
this.output.debug("Existing binary matches server version");
171+
trace.setOutcome("cache_hit");
172+
return resolved.binPath;
173+
}
180174

181-
if (!enableDownloads) {
182-
if (existingVersion) {
183-
this.output.info(
184-
"Using existing binary despite version mismatch because downloads are disabled",
185-
);
186-
trace.setOutcome("download_disabled_fallback");
187-
return resolved.binPath;
188-
}
189-
this.output.warn(
190-
"Unable to download CLI because downloads are disabled",
191-
);
192-
const error = new CliDownloadsDisabledError();
193-
trace.setFailure("downloads_disabled");
194-
throw error;
195-
}
175+
let action: CliDownloadAction;
176+
if (enableDownloads) {
177+
action = "download";
178+
} else {
179+
action = existingVersion !== null ? "fallback" : "blocked";
180+
}
181+
await trace.downloadDecision(downloadReason, action);
196182

183+
if (!enableDownloads) {
197184
if (existingVersion) {
198185
this.output.info(
199-
"Downloading since existing binary does not match the server version",
186+
"Using existing binary despite version mismatch because downloads are disabled",
200187
);
188+
trace.setOutcome("download_disabled_fallback");
189+
return resolved.binPath;
201190
}
191+
this.output.warn("Unable to download CLI because downloads are disabled");
192+
trace.setFailure("downloads_disabled");
193+
throw new CliDownloadsDisabledError();
194+
}
195+
196+
if (existingVersion) {
197+
this.output.info(
198+
"Downloading since existing binary does not match the server version",
199+
);
200+
}
202201

203-
return this.downloadBinary(
204-
restClient,
205-
trace,
202+
return this.downloadBinary(
203+
restClient,
204+
{
206205
resolved,
207206
parsedVersion,
208-
buildInfo.version,
207+
serverVersion: buildInfo.version,
209208
downloadReason,
210-
);
211-
});
209+
},
210+
trace,
211+
);
212212
}
213213

214214
private async lookupBinary(safeHostname: string): Promise<ResolvedBinary> {
@@ -282,12 +282,15 @@ export class CliManager {
282282

283283
private async downloadBinary(
284284
restClient: Api,
285+
options: {
286+
resolved: ResolvedBinary;
287+
parsedVersion: semver.SemVer;
288+
serverVersion: string;
289+
downloadReason: CliDownloadReason;
290+
},
285291
trace: CliResolveTrace,
286-
resolved: ResolvedBinary,
287-
parsedVersion: semver.SemVer,
288-
serverVersion: string,
289-
downloadReason: CliDownloadReason,
290292
): Promise<string> {
293+
const { resolved, parsedVersion, serverVersion, downloadReason } = options;
291294
// Always download using the platform-specific name.
292295
const downloadBinPath = path.join(
293296
path.dirname(resolved.binPath),
@@ -308,7 +311,7 @@ export class CliManager {
308311
this.output.debug("Acquired download lock");
309312

310313
if (lockResult.waited) {
311-
const waitResult = await trace.lockWaitRecheck(() =>
314+
const waitResult = await trace.lockRecheck(() =>
312315
this.recheckBinaryAfterWait(restClient, downloadBinPath),
313316
);
314317
if (waitResult.matches) {
@@ -324,9 +327,11 @@ export class CliManager {
324327
async (span) => {
325328
const downloadedBinPath = await this.performBinaryDownload(
326329
restClient,
327-
latestVersion,
328-
downloadBinPath,
329-
progressLogPath,
330+
{
331+
parsedVersion: latestVersion,
332+
binPath: downloadBinPath,
333+
progressLogPath,
334+
},
330335
span,
331336
);
332337
return this.renameToFinalPath(resolved, downloadedBinPath);
@@ -564,11 +569,14 @@ export class CliManager {
564569

565570
private async performBinaryDownload(
566571
restClient: Api,
567-
parsedVersion: semver.SemVer,
568-
binPath: string,
569-
progressLogPath: string,
572+
options: {
573+
parsedVersion: semver.SemVer;
574+
binPath: string;
575+
progressLogPath: string;
576+
},
570577
downloadSpan: Span,
571578
): Promise<string> {
579+
const { parsedVersion, binPath, progressLogPath } = options;
572580
const cfg = vscode.workspace.getConfiguration("coder");
573581
const tempFile = tempFilePath(binPath, "temp");
574582

@@ -1010,42 +1018,46 @@ export class CliManager {
10101018
silent,
10111019
credentialSource: token === "" ? "empty_token" : "session_token",
10121020
},
1013-
async (trace) => {
1014-
const configs = vscode.workspace.getConfiguration();
1015-
1016-
if (silent) {
1017-
try {
1018-
await this.cliCredentialManager.storeToken(url, token, configs);
1019-
} catch (error) {
1020-
trace.failed(error);
1021-
this.handleStoreError(error);
1022-
}
1023-
return;
1024-
}
1021+
(trace) => this.storeCredentials({ url, token, silent }, trace),
1022+
);
1023+
}
10251024

1026-
const result = await withCancellableProgress(
1027-
({ signal }) =>
1028-
this.cliCredentialManager.storeToken(url, token, configs, {
1029-
signal,
1030-
}),
1031-
{
1032-
location: vscode.ProgressLocation.Notification,
1033-
title: `Storing credentials for ${url}`,
1034-
cancellable: true,
1035-
},
1036-
);
1037-
if (result.ok) {
1038-
return;
1039-
}
1040-
if (result.cancelled) {
1041-
this.output.info("Credential storage cancelled by user");
1042-
trace.cancelled();
1043-
return;
1044-
}
1045-
trace.failed(result.error);
1046-
this.handleStoreError(result.error);
1025+
private async storeCredentials(
1026+
options: { url: string; token: string; silent: boolean },
1027+
trace: CliConfigureTrace,
1028+
): Promise<void> {
1029+
const { url, token, silent } = options;
1030+
const configs = vscode.workspace.getConfiguration();
1031+
1032+
if (silent) {
1033+
try {
1034+
await this.cliCredentialManager.storeToken(url, token, configs);
1035+
} catch (error) {
1036+
trace.failed(error);
1037+
this.handleStoreError(error);
1038+
}
1039+
return;
1040+
}
1041+
1042+
const result = await withCancellableProgress(
1043+
({ signal }) =>
1044+
this.cliCredentialManager.storeToken(url, token, configs, { signal }),
1045+
{
1046+
location: vscode.ProgressLocation.Notification,
1047+
title: `Storing credentials for ${url}`,
1048+
cancellable: true,
10471049
},
10481050
);
1051+
if (result.ok) {
1052+
return;
1053+
}
1054+
if (result.cancelled) {
1055+
this.output.info("Credential storage cancelled by user");
1056+
trace.cancelled();
1057+
return;
1058+
}
1059+
trace.failed(result.error);
1060+
this.handleStoreError(result.error);
10491061
}
10501062

10511063
/**

0 commit comments

Comments
 (0)