fix(statusline): GET /api/settings never writes, and a save sends the collection switch only on a flip

Two follow-ups to #361's sticky telemetry switch.

GET /api/settings reconciled an absent showPlanUsageLimits by persisting
true, but readJsonConfig() answers {} for ANY read failure (a parse
error, EACCES, EMFILE, a read landing inside PUT's non-atomic write), not
only ENOENT, and every page load calls this route, so one unlucky read
replaced the whole settings file with a one-key file. The route is a
plain read again and the default moved into the reader:
readPlanUsageTelemetryEnabled() treats an absent key as ON, the same way
readWorkspaceHooksEnabled() does, which is what the desktop chip already
shows for an install that never touched the setting.

saveAppSettings() sent showPlanUsageLimits on every save. The chip
defaults OFF on handhelds, so a phone saving its font size persisted
false and switched collection off for every desktop, whose chip then
went stale with no error anywhere. The key is now stripped like the
other per-device display keys and re-added only when the save FLIPS the
chip relative to what the device had (planUsageCollectionFlip), so an
explicit toggle on any device still writes it in either direction.

Tests pin both: the GET route with a mocked filesystem (absent, missing,
EACCES, garbage, explicit), the reader default, and the flip helper plus
its wiring in saveAppSettings.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer
2026-09-14 15:59:00 +02:00
parent b2b2c767ea
commit 707ea345eb
11 changed files with 214 additions and 121 deletions
+12 -3
View File
@@ -1345,13 +1345,22 @@ describe('readPlanUsageTelemetryEnabled', () => {
expect(await readPlanUsageTelemetryEnabled()).toBe(false);
});
it('defaults to false when the setting is absent or the file is missing', async () => {
it('defaults to true when the setting is absent or the file is missing (mirrors readWorkspaceHooksEnabled)', async () => {
// The desktop chip shows as ON for an install that never touched the
// setting, so collection must agree with it. Resolving the default HERE
// is what keeps GET /api/settings a plain read (see its route test).
rmSync(SETTINGS_PATH, { force: true });
expect(await readPlanUsageTelemetryEnabled()).toBe(false);
expect(await readPlanUsageTelemetryEnabled()).toBe(true);
mkdirSync(join(SETTINGS_PATH, '..'), { recursive: true });
writeFileSync(SETTINGS_PATH, JSON.stringify({ someOtherSetting: true }));
expect(await readPlanUsageTelemetryEnabled()).toBe(false);
expect(await readPlanUsageTelemetryEnabled()).toBe(true);
});
it('only an explicit false turns collection off; junk values read as on', async () => {
mkdirSync(join(SETTINGS_PATH, '..'), { recursive: true });
writeFileSync(SETTINGS_PATH, JSON.stringify({ showPlanUsageLimits: 'no' }));
expect(await readPlanUsageTelemetryEnabled()).toBe(true);
});
it('never caches — a change on disk is visible on the very next call', async () => {