Skip to content

Commit 832856e

Browse files
committed
fix(update): record why a success could not be verified
1 parent dbb7e60 commit 832856e

8 files changed

Lines changed: 48 additions & 11 deletions

File tree

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,10 @@ function resolveDeps(deps: Partial<DoctorDeps> | DoctorDeps | undefined): Resolv
212212
installState.lastSuccess === null
213213
? undefined
214214
: `${installState.lastSuccess.version} (installed ` +
215-
`${installState.lastSuccess.installedAt})`,
215+
`${installState.lastSuccess.installedAt})` +
216+
(installState.lastSuccess.unverified === undefined
217+
? ''
218+
: ` — unverified: ${installState.lastSuccess.unverified}`),
216219
lastFailure:
217220
installState.lastFailure === null
218221
? undefined

apps/pythinker-code/src/cli/update/install-state.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ const UpdateInstallStateSchema: z.ZodType<UpdateInstallState> = z
153153
version: z.string().min(1),
154154
installedAt: z.string().min(1),
155155
notifiedAt: z.string().min(1).nullable(),
156+
unverified: z.string().min(1).optional(),
156157
})
157158
.strict()
158159
.nullable(),

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -927,6 +927,7 @@ async function startBackgroundInstall(
927927
version: target.version,
928928
installedAt: nowIso(),
929929
notifiedAt: null,
930+
unverified: verification.ok ? verification.unverified : undefined,
930931
},
931932
}
932933
: {

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,12 @@ export interface UpdateInstallSuccess {
112112
readonly version: string;
113113
readonly installedAt: string;
114114
readonly notifiedAt: string | null;
115+
/**
116+
* Why this success was recorded without proof that the new version runs.
117+
* Absent when the installed binary was probed and matched. `doctor` prints
118+
* it, so "it says updated but it did not" is answerable in one command.
119+
*/
120+
readonly unverified?: string;
115121
}
116122

117123
export interface UpdateInstallState {

apps/pythinker-code/src/cli/update/verify-install.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -94,10 +94,9 @@ export async function verifyInstalledVersion(
9494
expectedVersion: string,
9595
overrides: Partial<VerifyInstalledVersionDeps> = {},
9696
): Promise<InstallVerification> {
97-
if (source !== 'native') return unverified(`not verified for ${source} installs`);
98-
if (valid(expectedVersion) === null) {
99-
return unverified(`not a version to verify against: ${expectedVersion}`);
100-
}
97+
// Non-native sources are not checkable from here (see the module comment),
98+
// and a note on every npm install would be noise rather than a signal.
99+
if (source !== 'native' || valid(expectedVersion) === null) return { ok: true };
101100

102101
const execPath = overrides.execPath ?? process.execPath;
103102
const probe = overrides.probeExecutableVersion ?? defaultProbeExecutableVersion;

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,8 @@ describe('pythinker doctor', () => {
207207
update: {
208208
latest: '0.13.1',
209209
checkedAt: '2026-08-08T12:00:00.000Z',
210-
lastSuccess: '0.13.1 (installed 2026-08-08T12:01:00.000Z)',
210+
lastSuccess:
211+
'0.13.1 (installed 2026-08-08T12:01:00.000Z) — unverified: probe timed out',
211212
lastFailure: 'install 0.13.1 (attempt 1): still reports 0.12.0',
212213
},
213214
}),
@@ -217,7 +218,9 @@ describe('pythinker doctor', () => {
217218

218219
expect(code).toBe(0);
219220
const output = stdout.join('');
220-
expect(output).toContain(' Last update success: 0.13.1 (installed 2026-08-08T12:01:00.000Z)');
221+
expect(output).toContain(
222+
' Last update success: 0.13.1 (installed 2026-08-08T12:01:00.000Z) — unverified: probe timed out',
223+
);
221224
expect(output).toContain(' Last update failure: install 0.13.1 (attempt 1): still reports 0.12.0');
222225
});
223226

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

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1439,6 +1439,30 @@ describe('runUpdatePreflight', () => {
14391439
}));
14401440
});
14411441

1442+
it('records why a success could not be verified', async () => {
1443+
mocks.readUpdateCache.mockResolvedValue(cacheWith('0.5.0'));
1444+
mocks.readUpdateInstallState.mockResolvedValue(installState());
1445+
mocks.refreshUpdateCache.mockResolvedValue(cacheWith('0.5.0'));
1446+
mocks.detectInstallSource.mockResolvedValue('native');
1447+
mocks.verifyInstalledVersion.mockResolvedValue({
1448+
ok: true,
1449+
unverified: '/usr/local/bin/pythinker could not be run: ETIMEDOUT',
1450+
});
1451+
mockSpawnExit(0);
1452+
const { options } = captureOutput();
1453+
1454+
await expect(runUpdatePreflight('0.4.0', options)).resolves.toBe('continue');
1455+
await flushBackgroundInstall();
1456+
1457+
expect(writeUpdateInstallState).toHaveBeenLastCalledWith(expect.objectContaining({
1458+
lastFailure: null,
1459+
lastSuccess: expect.objectContaining({
1460+
version: '0.5.0',
1461+
unverified: expect.stringContaining('ETIMEDOUT'),
1462+
}),
1463+
}));
1464+
});
1465+
14421466
it('does not verify an install the installer already reported as failed', async () => {
14431467
mocks.readUpdateCache.mockResolvedValue(cacheWith('0.5.0'));
14441468
mocks.readUpdateInstallState.mockResolvedValue(installState());

apps/pythinker-code/test/cli/update/verify-install.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -83,17 +83,17 @@ describe('verifyInstalledVersion native', () => {
8383
).resolves.toEqual({ ok: true, unverified: expect.stringContaining('printed no version') });
8484
});
8585

86-
it('accepts the install unverified when the target is not a version', async () => {
86+
it('checks nothing when the target is not a version', async () => {
8787
await expect(
8888
verifyInstalledVersion('native', 'latest', NEVER_PROBED),
89-
).resolves.toEqual({ ok: true, unverified: expect.stringContaining('latest') });
89+
).resolves.toEqual({ ok: true });
9090
});
9191
});
9292

9393
describe('verifyInstalledVersion other sources', () => {
9494
// A global reinstall rewrites the directory this process was loaded from,
9595
// so nothing readable here proves what the next launch will run.
96-
it('leaves every non-native source unverified without probing', async () => {
96+
it('checks nothing for a source it cannot prove, without probing', async () => {
9797
for (const source of [
9898
'npm-global',
9999
'pnpm-global',
@@ -104,7 +104,7 @@ describe('verifyInstalledVersion other sources', () => {
104104
] as const) {
105105
await expect(
106106
verifyInstalledVersion(source, '0.13.1', NEVER_PROBED),
107-
).resolves.toEqual({ ok: true, unverified: `not verified for ${source} installs` });
107+
).resolves.toEqual({ ok: true });
108108
}
109109
expect(NEVER_PROBED.probeExecutableVersion).not.toHaveBeenCalled();
110110
});

0 commit comments

Comments
 (0)