claude-code-session-manager 0.83.0 → 0.85.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/dist/assets/{AgentLibrary-B-OM-4gk.js → AgentLibrary-Bkv-HcP1.js} +1 -1
  2. package/dist/assets/{DataModel-BC6ppcBr.js → DataModel-DRH-Ty20.js} +1 -1
  3. package/dist/assets/{History-D3Qw2BWn.js → History-CfRhT1Im.js} +2 -2
  4. package/dist/assets/{Hooks-OxBQ0nIc.js → Hooks-wEmh_U6c.js} +1 -1
  5. package/dist/assets/{HostBilko-BNFC6bNZ.js → HostBilko-D_t7Rbi7.js} +1 -1
  6. package/dist/assets/{Library-DAMyrUtY.js → Library-CpArQ-OJ.js} +1 -1
  7. package/dist/assets/{ListDetail-BwywrpvE.js → ListDetail-pjaKYs84.js} +1 -1
  8. package/dist/assets/{MarkdownEditor-HEwjN1bp.js → MarkdownEditor-Xc141kjj.js} +1 -1
  9. package/dist/assets/{McpServers-D1VZF7Ix.js → McpServers-ftqaV3kn.js} +1 -1
  10. package/dist/assets/{Memory-CgYJ_QqD.js → Memory-ChMWkNd0.js} +6 -6
  11. package/dist/assets/{Panel-dTNWC59Q.js → Panel-D9Kr40Ai.js} +1 -1
  12. package/dist/assets/{Permissions-qI_NKi1b.js → Permissions-DKoNVgzj.js} +1 -1
  13. package/dist/assets/{Plugins-DZ0_CHiF.js → Plugins-BtChISho.js} +2 -2
  14. package/dist/assets/{ProvenanceBadge-B2i_uxCu.js → ProvenanceBadge-DBA5EcYy.js} +1 -1
  15. package/dist/assets/{SaveBar-4tNMCjUd.js → SaveBar-I0_dWNTX.js} +1 -1
  16. package/dist/assets/{Scheduler-CUen18Gc.js → Scheduler-CbES7MC8.js} +7 -7
  17. package/dist/assets/{ScopeSwitcher-Co3tkLTT.js → ScopeSwitcher-5GTEveb2.js} +1 -1
  18. package/dist/assets/Settings-BX3FElXk.js +3 -0
  19. package/dist/assets/{SkillReferenceGraph-BKcHtMPc.js → SkillReferenceGraph-DNBFGrYE.js} +1 -1
  20. package/dist/assets/{Skills-DHb4INtF.js → Skills-DJB6-bBM.js} +2 -2
  21. package/dist/assets/SystemPrompt-BiDDrJUA.js +1 -0
  22. package/dist/assets/{TagLibrary-xLP5dJvh.js → TagLibrary-_Wrevtop.js} +1 -1
  23. package/dist/assets/{TiptapBody-T-ULTjK4.js → TiptapBody-OWWXdLRy.js} +1 -1
  24. package/dist/assets/{Toggle-B-5M2hGe.js → Toggle-B122N0HL.js} +1 -1
  25. package/dist/assets/{index-CYhdtisq.css → index-CDo9xBR9.css} +1 -1
  26. package/dist/assets/{index-CzAxC432.js → index-DhvuQL4C.js} +316 -314
  27. package/dist/assets/{settingsSchema-C_nsemcF.js → settingsSchema-sGoCTd7J.js} +1 -1
  28. package/dist/index.html +2 -2
  29. package/package.json +4 -1
  30. package/scripts/hooks/guard-destructive-git.cjs +514 -0
  31. package/scripts/hooks/guard-inline-implementation.cjs +219 -0
  32. package/scripts/hooks/guard-prd-writes.cjs +200 -0
  33. package/src/main/__tests__/epicMintTelemetryTap.test.cjs +64 -0
  34. package/src/main/__tests__/health-delegation-chain.test.cjs +2 -1
  35. package/src/main/__tests__/health-queue-dispatch.test.cjs +84 -0
  36. package/src/main/__tests__/health-usage-poller.test.cjs +97 -0
  37. package/src/main/__tests__/health-worktree-cap-blocked.test.cjs +65 -0
  38. package/src/main/__tests__/opsErrorLogTelemetryTap.test.cjs +149 -0
  39. package/src/main/__tests__/pollLoop-dispatch-on-failure.test.cjs +120 -0
  40. package/src/main/__tests__/promptSessionTranscript.test.cjs +0 -0
  41. package/src/main/__tests__/queue-starvation-dispatch-driver.test.cjs +143 -0
  42. package/src/main/__tests__/rateLimitPollerStreak.test.cjs +79 -0
  43. package/src/main/__tests__/scheduleJobTransitionsTelemetryTap.test.cjs +72 -0
  44. package/src/main/__tests__/scheduler-inplace-salvage.test.cjs +74 -0
  45. package/src/main/__tests__/scheduler-job-overrun.test.cjs +58 -0
  46. package/src/main/__tests__/scheduler-notify-originating-tab-transcript.test.cjs +1 -0
  47. package/src/main/__tests__/scheduler-periodic-reverify-guard.test.cjs +134 -2
  48. package/src/main/__tests__/scheduler-reap-dead-running-jobs.test.cjs +33 -0
  49. package/src/main/__tests__/scheduler-stuck-failed-escalation.test.cjs +136 -0
  50. package/src/main/__tests__/telemetryClient.test.cjs +242 -1
  51. package/src/main/__tests__/telemetryContract.test.cjs +930 -0
  52. package/src/main/crashDiagnostics.cjs +29 -1
  53. package/src/main/health.cjs +197 -3
  54. package/src/main/index.cjs +65 -6
  55. package/src/main/ipcSchemas.cjs +19 -2
  56. package/src/main/lib/__tests__/crashTelemetry.test.cjs +103 -0
  57. package/src/main/lib/__tests__/delegationReadiness.test.cjs +302 -4
  58. package/src/main/lib/__tests__/fixtures/scheduler-machine.json.corrupt-1789147548 +34 -0
  59. package/src/main/lib/__tests__/gitWorktree.test.cjs +413 -1
  60. package/src/main/lib/__tests__/jobWorktreeBootLive.test.cjs +71 -0
  61. package/src/main/lib/__tests__/queueStoreAtomicWrite.test.cjs +88 -0
  62. package/src/main/lib/__tests__/queueStoreMachineStateRecovery.test.cjs +123 -0
  63. package/src/main/lib/__tests__/reaperHelpers.test.cjs +58 -0
  64. package/src/main/lib/__tests__/telemetryBacklog.test.cjs +626 -0
  65. package/src/main/lib/__tests__/telemetryBoot.test.cjs +123 -0
  66. package/src/main/lib/__tests__/telemetryConsent.test.cjs +136 -0
  67. package/src/main/lib/__tests__/telemetryCounters.test.cjs +57 -0
  68. package/src/main/lib/__tests__/telemetryCountersMetadataColumn.test.cjs +95 -0
  69. package/src/main/lib/crashTelemetry.cjs +37 -0
  70. package/src/main/lib/delegationReadiness.cjs +119 -1
  71. package/src/main/lib/epicMint.cjs +2 -0
  72. package/src/main/lib/gitWorktree.cjs +427 -17
  73. package/src/main/lib/jobWorktree.cjs +2 -1
  74. package/src/main/lib/jobWorktreeBootLive.cjs +51 -0
  75. package/src/main/lib/jobWorktreeTerminalOrphanLive.cjs +68 -0
  76. package/src/main/lib/opsErrorLog.cjs +78 -25
  77. package/src/main/lib/queueStore.cjs +241 -22
  78. package/src/main/lib/reaperHelpers.cjs +23 -1
  79. package/src/main/lib/scheduleJobSchema.cjs +7 -0
  80. package/src/main/lib/scheduleJobTransitions.cjs +12 -0
  81. package/src/main/lib/telemetryBacklog.cjs +601 -0
  82. package/src/main/lib/telemetryBoot.cjs +76 -0
  83. package/src/main/lib/telemetryClient.cjs +150 -5
  84. package/src/main/lib/telemetryConsent.cjs +34 -0
  85. package/src/main/lib/telemetryCounters.cjs +45 -0
  86. package/src/main/promptSessionTranscript.cjs +0 -0
  87. package/src/main/pty.cjs +2 -0
  88. package/src/main/scheduler.cjs +481 -33
  89. package/src/preload/api.d.ts +84 -4
  90. package/src/preload/index.cjs +9 -0
  91. package/dist/assets/Settings-DXVgKLUx.js +0 -3
  92. package/dist/assets/SystemPrompt-BjqFOHSk.js +0 -1
@@ -17,12 +17,14 @@ let originalHome;
17
17
  let originalSmTelemetry;
18
18
  let originalSmTelemetryEndpoint;
19
19
  let originalSmBeaconKey;
20
+ let originalSmTelemetrySpool;
20
21
 
21
22
  beforeEach(() => {
22
23
  originalHome = process.env.HOME;
23
24
  originalSmTelemetry = process.env.SM_TELEMETRY;
24
25
  originalSmTelemetryEndpoint = process.env.SM_TELEMETRY_ENDPOINT;
25
26
  originalSmBeaconKey = process.env.SM_BEACON_KEY;
27
+ originalSmTelemetrySpool = process.env.SM_TELEMETRY_SPOOL;
26
28
  });
27
29
 
28
30
  afterEach(async () => {
@@ -30,6 +32,7 @@ afterEach(async () => {
30
32
  if (originalSmTelemetry === undefined) delete process.env.SM_TELEMETRY; else process.env.SM_TELEMETRY = originalSmTelemetry;
31
33
  if (originalSmTelemetryEndpoint === undefined) delete process.env.SM_TELEMETRY_ENDPOINT; else process.env.SM_TELEMETRY_ENDPOINT = originalSmTelemetryEndpoint;
32
34
  if (originalSmBeaconKey === undefined) delete process.env.SM_BEACON_KEY; else process.env.SM_BEACON_KEY = originalSmBeaconKey;
35
+ if (originalSmTelemetrySpool === undefined) delete process.env.SM_TELEMETRY_SPOOL; else process.env.SM_TELEMETRY_SPOOL = originalSmTelemetrySpool;
33
36
  vi.useRealTimers();
34
37
  vi.unstubAllGlobals();
35
38
  while (tmpDirs.length) {
@@ -51,6 +54,13 @@ function fakeProfile(overrides = {}) {
51
54
  arch: 'x64',
52
55
  machineDigest: 'deadbeefcafe',
53
56
  installChannel: 'dev',
57
+ osRelease: '6.1.0',
58
+ cpuCount: 8,
59
+ totalMemMb: 16384,
60
+ nodeVersion: '20.11.0',
61
+ electronVersion: '33.0.0',
62
+ locale: 'en-US',
63
+ timezoneOffsetMinutes: 420,
54
64
  ...overrides,
55
65
  };
56
66
  }
@@ -65,6 +75,11 @@ function fakeProfile(overrides = {}) {
65
75
  */
66
76
  function freshClient(home, profileOverrides) {
67
77
  process.env.HOME = home;
78
+ // Explicit opt-in: this suite deliberately exercises telemetryClient's real
79
+ // queue/sent-file I/O, so it must override the test-environment no-op guard
80
+ // by pointing the spool at this test's own isolated tmp dir (never a real
81
+ // user's ~/.config/session-manager).
82
+ process.env.SM_TELEMETRY_SPOOL = path.join(home, '.config', 'session-manager');
68
83
  for (const p of ['../lib/telemetryClient.cjs', '../config.cjs', '../lib/telemetrySettings.cjs', '../lib/machineProfile.cjs']) {
69
84
  const resolved = require.resolve(p);
70
85
  delete require.cache[resolved];
@@ -110,7 +125,7 @@ async function readQueueLines(client) {
110
125
  test('exports the full contract', async () => {
111
126
  const home = await mkHome();
112
127
  const client = freshClient(home);
113
- for (const fn of ['track', 'logLine', 'reportError', 'flush', 'shutdown', 'status', 'recentRecords', 'isPending']) {
128
+ for (const fn of ['track', 'logLine', 'reportError', 'reportInstall', 'flush', 'shutdown', 'status', 'recentRecords', 'isPending']) {
114
129
  expect(typeof client[fn]).toBe('function');
115
130
  }
116
131
  client.shutdown();
@@ -130,6 +145,7 @@ test('ingress functions never throw on hostile inputs', async () => {
130
145
  await expect(client.track(bad, bad)).resolves.toBeDefined();
131
146
  await expect(client.logLine(bad)).resolves.toBeDefined();
132
147
  await expect(client.reportError(bad)).resolves.toBeDefined();
148
+ await expect(client.reportInstall(bad)).resolves.toBeDefined();
133
149
  }
134
150
  client.shutdown();
135
151
  });
@@ -146,6 +162,7 @@ test('when telemetry is disabled, ingress drops at the point of entry', async ()
146
162
  await client.track('evt', { a: 1 });
147
163
  await client.logLine({ level: 'info', msg: 'hi' });
148
164
  await client.reportError({ name: 'E', msg: 'boom', stack: 'at x' });
165
+ await client.reportInstall(fakeProfile());
149
166
 
150
167
  expect(client.status().pendingCount).toBe(0);
151
168
  expect(await readQueueLines(client)).toEqual([]);
@@ -504,6 +521,7 @@ test('wire field names match server/routes/telemetry.ts exactly', async () => {
504
521
  await client.track('evt-name', { a: 1 });
505
522
  await client.logLine({ level: 'warn', msg: 'm', fields: { b: 2 } });
506
523
  await client.reportError({ name: 'E', msg: 'm', stack: 'Error: m\n at f (a.js:1:1)', context: { c: 3 } });
524
+ await client.reportInstall(fakeProfile());
507
525
 
508
526
  const fetchFn = fetchStub(async () => okResponse());
509
527
  client._setFetchImpl(fetchFn);
@@ -512,10 +530,16 @@ test('wire field names match server/routes/telemetry.ts exactly', async () => {
512
530
  const eventCall = fetchFn.calls.find((c) => c.url.endsWith('/event'));
513
531
  const logCall = fetchFn.calls.find((c) => c.url.endsWith('/log'));
514
532
  const errorCall = fetchFn.calls.find((c) => c.url.endsWith('/error'));
533
+ const installCall = fetchFn.calls.find((c) => c.url.endsWith('/install'));
515
534
 
516
535
  expect(Object.keys(eventCall.body.batch[0]).sort()).toEqual(['app', 'name', 'props', 'session_id', 'visitor_id'].sort());
517
536
  expect(Object.keys(logCall.body.batch[0]).sort()).toEqual(['app', 'version', 'level', 'msg', 'visitor_id', 'session_id', 'fields', 'ts'].sort());
518
537
  expect(Object.keys(errorCall.body.batch[0]).sort()).toEqual(['app', 'version', 'name', 'msg', 'stack', 'url', 'ua', 'visitor_id', 'session_id', 'context', 'ts'].sort());
538
+ expect(Object.keys(installCall.body).sort()).toEqual([
539
+ 'app', 'app_version', 'arch', 'cpu_count', 'electron_version',
540
+ 'install_channel', 'install_id', 'locale', 'node_version',
541
+ 'os_release', 'platform', 'timezone', 'total_mem_mb',
542
+ ].sort());
519
543
  client.shutdown();
520
544
  });
521
545
 
@@ -742,3 +766,220 @@ test('the default path uses global fetch when no injection is made', async () =>
742
766
  expect(globalFetch).toHaveBeenCalledTimes(1);
743
767
  client.shutdown();
744
768
  });
769
+
770
+ // ─── status(): lastError / lastFlushAt / lastFlushReason (PRD 1142) ──────
771
+
772
+ test('status() exposes the last error and clears it on a subsequent successful flush', async () => {
773
+ const home = await mkHome();
774
+ const client = freshClient(home);
775
+ vi.useFakeTimers();
776
+ await client.track('evt', { a: 1 });
777
+ client._setFetchImpl(fetchStub(async () => statusResponse(500)));
778
+ await client.flush('manual');
779
+ let st = client.status();
780
+ expect(st.lastError).toEqual(expect.objectContaining({ status: 500, message: 'HTTP 500' }));
781
+ expect(st.lastFlushAt).toBeGreaterThan(0);
782
+ expect(st.lastFlushReason).toBe('manual');
783
+
784
+ // Past the backoff window this failure armed, so the next flush actually attempts delivery.
785
+ vi.advanceTimersByTime(6 * 60 * 1000 + 1000);
786
+ client._setFetchImpl(fetchStub(async () => okResponse()));
787
+ await client.flush('boot');
788
+ st = client.status();
789
+ expect(st.lastError).toBeNull();
790
+ expect(st.lastFlushReason).toBe('boot');
791
+ client.shutdown();
792
+ });
793
+
794
+ test('status() reports a network error (fetch throw / status 0) with a plain-language message', async () => {
795
+ const home = await mkHome();
796
+ const client = freshClient(home);
797
+ await client.track('evt', { a: 1 });
798
+ client._setFetchImpl(vi.fn(async () => { throw new Error('ECONNREFUSED'); }));
799
+ await client.flush('manual');
800
+ const st = client.status();
801
+ expect(st.lastError).toEqual(expect.objectContaining({ status: 0, message: 'network error' }));
802
+ client.shutdown();
803
+ });
804
+
805
+ // ─── test-environment guard (poisoned-spool prevention) ─────────────────
806
+
807
+ test('appendRecord() is a no-op under a test runner unless SM_TELEMETRY_SPOOL is explicitly set', async () => {
808
+ const home = await mkHome();
809
+ process.env.HOME = home;
810
+ delete process.env.SM_TELEMETRY_SPOOL;
811
+ for (const p of ['../lib/telemetryClient.cjs', '../config.cjs', '../lib/telemetrySettings.cjs', '../lib/machineProfile.cjs']) {
812
+ delete require.cache[require.resolve(p)];
813
+ }
814
+ const client = require('../lib/telemetryClient.cjs');
815
+ client._setMachineProfileBuilder(async () => fakeProfile());
816
+
817
+ expect(process.env.VITEST).toBeTruthy();
818
+ const trackResult = await client.track('evt', { a: 1 });
819
+ const logResult = await client.logLine({ level: 'info', msg: 'hi' });
820
+ const errorResult = await client.reportError({ name: 'E', msg: 'boom', stack: 'at x' });
821
+
822
+ expect(trackResult).toEqual({ accepted: false, reason: 'test-environment', recordId: expect.any(String) });
823
+ expect(logResult.accepted).toBe(false);
824
+ expect(errorResult.accepted).toBe(false);
825
+ expect(client.status().pendingCount).toBe(0);
826
+ await expect(fsp.access(client.queuePath())).rejects.toThrow();
827
+ client.shutdown();
828
+ });
829
+
830
+ // ─── clearQueue (opt-out side effect, PRD 1142) ──────────────────────────
831
+
832
+ test('clearQueue() empties the in-memory queue and truncates the on-disk queue file', async () => {
833
+ const home = await mkHome();
834
+ const client = freshClient(home);
835
+ await client.track('evt', { a: 1 });
836
+ await client.track('evt', { a: 2 });
837
+ expect(client.status().pendingCount).toBe(2);
838
+
839
+ await client.clearQueue();
840
+ expect(client.status().pendingCount).toBe(0);
841
+ const raw = await fsp.readFile(client.queuePath(), 'utf8');
842
+ expect(raw.trim()).toBe('');
843
+ client.shutdown();
844
+ });
845
+
846
+ // ─── install channel (PRD: bilko.run app_installs) ──────────────────────
847
+
848
+ test('reportInstall() maps buildMachineProfile()\'s camelCase onto the exact snake_case key set app_installs expects', async () => {
849
+ const home = await mkHome();
850
+ const client = freshClient(home);
851
+ const profile = fakeProfile();
852
+ await client.reportInstall(profile);
853
+ const [rec] = await readQueueLines(client);
854
+
855
+ expect(rec.channel).toBe('install');
856
+ expect(Object.keys(rec.wire).sort()).toEqual([
857
+ 'app', 'app_version', 'arch', 'cpu_count', 'electron_version',
858
+ 'install_channel', 'install_id', 'locale', 'node_version',
859
+ 'os_release', 'platform', 'timezone', 'total_mem_mb',
860
+ ].sort());
861
+ expect(rec.wire.app).toBe('session-manager');
862
+ expect(rec.wire.app_version).toBe(profile.appVersion);
863
+ expect(rec.wire.platform).toBe(profile.platform);
864
+ expect(rec.wire.os_release).toBe(profile.osRelease);
865
+ expect(rec.wire.arch).toBe(profile.arch);
866
+ expect(rec.wire.cpu_count).toBe(profile.cpuCount);
867
+ expect(rec.wire.total_mem_mb).toBe(profile.totalMemMb);
868
+ expect(rec.wire.node_version).toBe(profile.nodeVersion);
869
+ expect(rec.wire.electron_version).toBe(profile.electronVersion);
870
+ expect(rec.wire.install_channel).toBe(profile.installChannel);
871
+ expect(rec.wire.locale).toBe(profile.locale);
872
+ expect(rec.wire.timezone).toBe(String(profile.timezoneOffsetMinutes));
873
+ expect(typeof rec.wire.install_id).toBe('string');
874
+ expect(rec.wire.install_id).toBeTruthy();
875
+ client.shutdown();
876
+ });
877
+
878
+ test('the install channel POSTs a flat body, never a {batch:[...]} envelope', async () => {
879
+ const home = await mkHome();
880
+ const client = freshClient(home);
881
+ await client.reportInstall(fakeProfile());
882
+ const fetchFn = fetchStub(async () => okResponse());
883
+ client._setFetchImpl(fetchFn);
884
+ const result = await client.flush('manual');
885
+
886
+ expect(result.sent.length).toBe(1);
887
+ expect(fetchFn).toHaveBeenCalledTimes(1);
888
+ expect(fetchFn.calls[0].url.endsWith('/api/telemetry/install')).toBe(true);
889
+ expect(fetchFn.calls[0].body.batch).toBeUndefined();
890
+ expect(fetchFn.calls[0].body.install_id).toBeTruthy();
891
+ client.shutdown();
892
+ });
893
+
894
+ test('a 400 on the install channel disables the client like any other malformed-contract 4xx', async () => {
895
+ const home = await mkHome();
896
+ const client = freshClient(home);
897
+ await client.reportInstall(fakeProfile());
898
+ client._setFetchImpl(fetchStub(async () => statusResponse(400)));
899
+ await client.flush('manual');
900
+ const st = client.status();
901
+ expect(st.disabledForProcess).toBe(true);
902
+ expect(st.pendingCount).toBe(1);
903
+ client.shutdown();
904
+ });
905
+
906
+ test('a 429 on the install channel backs off rather than disabling, same as event/log/error', async () => {
907
+ const home = await mkHome();
908
+ const client = freshClient(home);
909
+ await client.reportInstall(fakeProfile());
910
+ client._setFetchImpl(fetchStub(async () => statusResponse(429)));
911
+ await client.flush('manual');
912
+ const st = client.status();
913
+ expect(st.disabledForProcess).toBe(false);
914
+ expect(st.backoffUntil).toBeGreaterThan(Date.now() - 1);
915
+ expect(st.pendingCount).toBe(1);
916
+ client.shutdown();
917
+ });
918
+
919
+ test('a stub server bound to 127.0.0.1 receives exactly one well-formed install POST during a simulated boot', async () => {
920
+ const http = require('node:http');
921
+ const home = await mkHome();
922
+ let received = null;
923
+ let requestCount = 0;
924
+ const server = http.createServer((req, res) => {
925
+ if (req.url === '/api/telemetry/install') {
926
+ requestCount += 1;
927
+ let raw = '';
928
+ req.on('data', (chunk) => { raw += chunk; });
929
+ req.on('end', () => {
930
+ received = JSON.parse(raw);
931
+ res.writeHead(200, { 'Content-Type': 'application/json' });
932
+ res.end(JSON.stringify({ ok: true }));
933
+ });
934
+ } else {
935
+ res.writeHead(404);
936
+ res.end();
937
+ }
938
+ });
939
+ await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
940
+ const { port } = server.address();
941
+ process.env.SM_TELEMETRY_ENDPOINT = `http://127.0.0.1:${port}`;
942
+
943
+ try {
944
+ const client = freshClient(home);
945
+ // No _setFetchImpl — exercises the real global fetch path against the local stub.
946
+ const profile = fakeProfile();
947
+ await client.reportInstall(profile);
948
+ await client.flush('boot');
949
+
950
+ expect(requestCount).toBe(1);
951
+ expect(received).toEqual({
952
+ install_id: expect.any(String),
953
+ app: 'session-manager',
954
+ app_version: profile.appVersion,
955
+ platform: profile.platform,
956
+ os_release: profile.osRelease,
957
+ arch: profile.arch,
958
+ cpu_count: profile.cpuCount,
959
+ total_mem_mb: profile.totalMemMb,
960
+ node_version: profile.nodeVersion,
961
+ electron_version: profile.electronVersion,
962
+ install_channel: profile.installChannel,
963
+ locale: profile.locale,
964
+ timezone: String(profile.timezoneOffsetMinutes),
965
+ });
966
+ client.shutdown();
967
+ } finally {
968
+ await new Promise((resolve) => server.close(resolve));
969
+ }
970
+ }, 15000);
971
+
972
+ test('clearQueue() never touches telemetry-sent.json', async () => {
973
+ const home = await mkHome();
974
+ const client = freshClient(home);
975
+ client._setFetchImpl(fetchStub(async () => okResponse()));
976
+ await client.track('evt', { a: 1 });
977
+ await client.flush('manual');
978
+ const sentBefore = await fsp.readFile(client.sentPath(), 'utf8');
979
+
980
+ await client.track('evt', { a: 2 });
981
+ await client.clearQueue();
982
+ const sentAfter = await fsp.readFile(client.sentPath(), 'utf8');
983
+ expect(sentAfter).toBe(sentBefore);
984
+ client.shutdown();
985
+ });