Skip to content

Commit d0f4c81

Browse files
committed
fix(update): keep the unverified reason on manual installs too
1 parent 832856e commit d0f4c81

3 files changed

Lines changed: 40 additions & 6 deletions

File tree

apps/pythinker-code/src/cli/sub/upgrade.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
} from '#/cli/update/install-state';
1414
import { isTargetInstallable, selectUpdateTarget } from '#/cli/update/select';
1515
import { detectInstallSource } from '#/cli/update/source';
16+
import type { InstallVerification } from '#/cli/update/verify-install';
1617
import {
1718
canAutoInstall,
1819
installCommandFor,
@@ -47,7 +48,7 @@ export interface UpgradeDeps {
4748
source: InstallSource,
4849
version: string,
4950
platform: NodeJS.Platform,
50-
) => Promise<void>;
51+
) => Promise<InstallVerification>;
5152
readonly promptForInstallChoice: (
5253
options: InstallPromptOptions,
5354
) => Promise<InstallPromptChoiceValue>;
@@ -180,7 +181,7 @@ export async function handleUpgrade(
180181
target_version: target.version,
181182
source,
182183
});
183-
await deps.installUpdate(source, target.version, deps.platform);
184+
const verification = await deps.installUpdate(source, target.version, deps.platform);
184185
await deps.writeUpdateInstallState({
185186
...installState,
186187
active: null,
@@ -189,6 +190,7 @@ export async function handleUpgrade(
189190
version: target.version,
190191
installedAt: nowIso(),
191192
notifiedAt: null,
193+
unverified: verification.ok ? verification.unverified : undefined,
192194
},
193195
}).catch(() => {});
194196
trackUpgradeEvent(deps.track, 'upgrade_command_succeeded', {

apps/pythinker-code/src/cli/update/preflight.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -583,7 +583,7 @@ export async function installUpdate(
583583
source: InstallSource,
584584
version: string,
585585
platform: NodeJS.Platform,
586-
): Promise<void> {
586+
): Promise<InstallVerification> {
587587
const { cmd, args, env } = spawnForSource(source, version, platform);
588588
await new Promise<void>((resolve, reject) => {
589589
const child = spawn(cmd, [...args], {
@@ -605,6 +605,9 @@ export async function installUpdate(
605605
// installer gets, instead of printing "Updated …" over an unchanged binary.
606606
const verification = await verifyInstalledVersion(source, version);
607607
if (!verification.ok) throw new Error(verification.reason);
608+
// Returned so the caller can record *why* a success is unproven; see
609+
// verify-install.ts for the fail-open rule.
610+
return verification;
608611
}
609612

610613
/** Keep the tail only: installers can be chatty, and the state file is small. */
@@ -1449,7 +1452,7 @@ export async function runUpdatePreflight(
14491452
if (lock === null) return 'continue';
14501453

14511454
try {
1452-
await installUpdate(source, userVisibleTarget.version, platform);
1455+
const verification = await installUpdate(source, userVisibleTarget.version, platform);
14531456
await writeUpdateInstallState({
14541457
...installState,
14551458
active: null,
@@ -1458,6 +1461,7 @@ export async function runUpdatePreflight(
14581461
version: userVisibleTarget.version,
14591462
installedAt: nowIso(),
14601463
notifiedAt: null,
1464+
unverified: verification.ok ? verification.unverified : undefined,
14611465
},
14621466
}).catch(() => {});
14631467
stdout.write(renderInstallSuccessMessage(userVisibleTarget));

apps/pythinker-code/test/cli/upgrade.test.ts

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { emptyUpdateInstallState } from '#/cli/update/install-state';
55
import type { UpdateInstallLockHandle } from '#/cli/update/install-lock';
66
import type { InstallPromptChoiceValue } from '#/cli/update/prompt';
77
import type { InstallSource, UpdateCache, UpdateInstallState } from '#/cli/update/types';
8+
import type { InstallVerification } from '#/cli/update/verify-install';
89

910
function cacheWith(
1011
version: string | null,
@@ -69,7 +70,11 @@ function createDeps(overrides: {
6970
readonly source?: InstallSource;
7071
readonly isInteractive?: boolean;
7172
readonly promptForInstallChoice?: () => Promise<InstallPromptChoiceValue>;
72-
readonly installUpdate?: (source: InstallSource, version: string, platform: NodeJS.Platform) => Promise<void>;
73+
readonly installUpdate?: (
74+
source: InstallSource,
75+
version: string,
76+
platform: NodeJS.Platform,
77+
) => Promise<InstallVerification>;
7378
readonly readUpdateInstallState?: () => Promise<UpdateInstallState>;
7479
readonly writeUpdateInstallState?: (state: UpdateInstallState) => Promise<void>;
7580
readonly tryAcquireUpdateInstallLock?: () => Promise<UpdateInstallLockHandle | null>;
@@ -80,7 +85,7 @@ function createDeps(overrides: {
8085
source: InstallSource,
8186
version: string,
8287
platform: NodeJS.Platform,
83-
) => Promise<void>>().mockResolvedValue(undefined);
88+
) => Promise<InstallVerification>>().mockResolvedValue({ ok: true });
8489

8590
return {
8691
refreshUpdateCache: vi
@@ -212,6 +217,29 @@ describe('handleUpgrade', () => {
212217
expect(stdout.join('')).toContain('To update manually, run: npm install -g @pythoughts/pythinker-code@0.5.0');
213218
});
214219

220+
it('records why a manual install could not be verified', async () => {
221+
const { writable } = captureOutput();
222+
const writeUpdateInstallState = vi.fn().mockResolvedValue(undefined);
223+
const deps = createDeps({
224+
latest: '0.5.0',
225+
source: 'native',
226+
installUpdate: vi.fn().mockResolvedValue({
227+
ok: true,
228+
unverified: '/usr/local/bin/pythinker could not be run: ETIMEDOUT',
229+
}),
230+
writeUpdateInstallState,
231+
});
232+
233+
await expect(handleUpgrade('0.4.0', { ...deps, ...writable })).resolves.toBe(0);
234+
235+
expect(writeUpdateInstallState).toHaveBeenCalledWith(expect.objectContaining({
236+
lastSuccess: expect.objectContaining({
237+
version: '0.5.0',
238+
unverified: expect.stringContaining('ETIMEDOUT'),
239+
}),
240+
}));
241+
});
242+
215243
it('returns a failing exit code when the foreground install fails', async () => {
216244
const { stderr, writable } = captureOutput();
217245
const deps = createDeps({

0 commit comments

Comments
 (0)