Skip to content

Commit 62ffdfd

Browse files
committed
test(update): prove the footer poll runs and pin the installer's real bytes
Two seams were verified only by inference. Nothing in the suite drove startUpdateStatusPolling through pollUpdateStatus to dispatchFooter, so the status-row chip could have been dead code with every test still passing — a startup test now drives the real poller against real state files and asserts the rendered row. And the line reader had only ever parsed hand-written fixtures, so it now pins the exact bytes a real install.sh run emitted, which also records the throttle's real behaviour: three lines in one chunk write the first update and the terminal one, not the middle.
1 parent c67f64e commit 62ffdfd

2 files changed

Lines changed: 129 additions & 1 deletion

File tree

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

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2058,6 +2058,47 @@ describe('runUpdatePreflight', () => {
20582058
}));
20592059
});
20602060

2061+
/**
2062+
* The exact bytes a real `install.sh` run emitted while downloading the
2063+
* 0.9.2 release, captured from its stderr. Pinning them here means the
2064+
* emitter and this parser cannot drift apart silently.
2065+
*/
2066+
it('parses the bytes a real installer run actually emitted', async () => {
2067+
mocks.readUpdateCache.mockResolvedValue(cacheWith('0.5.0'));
2068+
mocks.readUpdateInstallState.mockResolvedValue(installState());
2069+
mocks.refreshUpdateCache.mockResolvedValue(cacheWith('0.5.0'));
2070+
mocks.detectInstallSource.mockResolvedValue('npm-global');
2071+
mockSpawnExitWithStderr(
2072+
0,
2073+
'progress: state=downloading percent=0 transferred=0 total=55795679\n'
2074+
+ 'progress: state=downloading percent=49 transferred=27103232 total=55795679\n'
2075+
+ 'progress: state=done transferred=55795679\n',
2076+
);
2077+
const { options } = captureOutput();
2078+
2079+
await expect(runUpdatePreflight('0.4.0', options)).resolves.toBe('continue');
2080+
await flushBackgroundInstall();
2081+
2082+
// All three lines arrive in one chunk, so the 2-second write throttle
2083+
// keeps the first downloading update and drops the second; the terminal
2084+
// state always bypasses the throttle.
2085+
expect(writeUpdateInstallState).toHaveBeenCalledWith(expect.objectContaining({
2086+
active: expect.objectContaining({
2087+
progress: expect.objectContaining({
2088+
state: 'downloading',
2089+
percent: 0,
2090+
transferred: 0,
2091+
total: 55_795_679,
2092+
}),
2093+
}),
2094+
}));
2095+
expect(writeUpdateInstallState).toHaveBeenCalledWith(expect.objectContaining({
2096+
active: expect.objectContaining({
2097+
progress: expect.objectContaining({ state: 'done', transferred: 55_795_679 }),
2098+
}),
2099+
}));
2100+
});
2101+
20612102
it('keeps progress lines out of the failure tail and ordinary stderr lines in it', async () => {
20622103
mocks.readUpdateCache.mockResolvedValue(cacheWith('0.5.0'));
20632104
mocks.readUpdateInstallState.mockResolvedValue(installState());

apps/pythinker-code/test/tui/pythinker-tui-startup.test.ts

Lines changed: 88 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { mkdtempSync, rmSync } from 'node:fs';
1+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
22
import { tmpdir } from 'node:os';
33
import { join } from 'node:path';
44

@@ -78,6 +78,11 @@ interface RuntimeStateDriver extends StartupDriver {
7878
closeSession(reason: string): Promise<void>;
7979
}
8080

81+
interface UpdatePollDriver extends StartupDriver {
82+
startUpdateStatusPolling(): void;
83+
stopUpdateStatusPolling(): void;
84+
}
85+
8186
interface ThemeTrackingDriver extends StartupDriver {
8287
refreshTerminalThemeTracking(): void;
8388
}
@@ -2008,3 +2013,85 @@ describe('startup feature parity baseline', () => {
20082013
).toBe(true);
20092014
});
20102015
});
2016+
2017+
describe('footer update status poll', () => {
2018+
/**
2019+
* The poll is the only thing that puts an update into the footer, and it is
2020+
* wired from `finishStartup` — so nothing else in this suite would notice if
2021+
* it stopped dispatching. Drive it against real state files.
2022+
*/
2023+
it('dispatches availability and then live progress into the status row', async () => {
2024+
const home = mkdtempSync(join(tmpdir(), 'pk-footer-update-'));
2025+
const previousHome = process.env['PYTHINKER_CODE_HOME'];
2026+
process.env['PYTHINKER_CODE_HOME'] = home;
2027+
const updates = join(home, 'updates');
2028+
mkdirSync(updates, { recursive: true });
2029+
const manifest = {
2030+
version: '9.9.9',
2031+
publishedAt: '2026-08-07T00:00:00.000Z',
2032+
rollout: [],
2033+
};
2034+
writeFileSync(
2035+
join(updates, 'latest.json'),
2036+
JSON.stringify({
2037+
source: 'cdn',
2038+
checkedAt: '2026-08-07T00:00:00.000Z',
2039+
latest: '9.9.9',
2040+
manifest,
2041+
}),
2042+
);
2043+
2044+
const presentation = new RecordingPresentation();
2045+
const driver = new PythinkerTUI(
2046+
makeHarness() as never,
2047+
makeStartupInput(),
2048+
presentation,
2049+
) as unknown as UpdatePollDriver;
2050+
2051+
try {
2052+
driver.startUpdateStatusPolling();
2053+
await vi.waitFor(
2054+
() => {
2055+
expect(footerStatusItems(presentation.footerModels.at(-1))).toContain('↑ v9.9.9');
2056+
},
2057+
{ timeout: 10_000, interval: 50 },
2058+
);
2059+
2060+
writeFileSync(
2061+
join(updates, 'install.json'),
2062+
JSON.stringify({
2063+
active: {
2064+
version: '9.9.9',
2065+
source: 'native',
2066+
startedAt: new Date().toISOString(),
2067+
pid: process.pid,
2068+
progress: {
2069+
state: 'downloading',
2070+
percent: 42,
2071+
transferred: 5_320_000,
2072+
total: 12_600_000,
2073+
updatedAt: new Date().toISOString(),
2074+
},
2075+
},
2076+
pending: null,
2077+
lastFailure: null,
2078+
lastSuccess: null,
2079+
}),
2080+
);
2081+
await vi.waitFor(
2082+
() => {
2083+
expect(footerStatusItems(presentation.footerModels.at(-1))).toContain(
2084+
'↓ v9.9.9 ▰▰▰▱▱▱▱▱ 42%',
2085+
);
2086+
},
2087+
{ timeout: 10_000, interval: 50 },
2088+
);
2089+
} finally {
2090+
driver.stopUpdateStatusPolling();
2091+
driver.state.footer.dispose();
2092+
if (previousHome === undefined) delete process.env['PYTHINKER_CODE_HOME'];
2093+
else process.env['PYTHINKER_CODE_HOME'] = previousHome;
2094+
rmSync(home, { recursive: true, force: true });
2095+
}
2096+
}, 30_000);
2097+
});

0 commit comments

Comments
 (0)