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.
- package/dist/assets/{AgentLibrary-B-OM-4gk.js → AgentLibrary-Bkv-HcP1.js} +1 -1
- package/dist/assets/{DataModel-BC6ppcBr.js → DataModel-DRH-Ty20.js} +1 -1
- package/dist/assets/{History-D3Qw2BWn.js → History-CfRhT1Im.js} +2 -2
- package/dist/assets/{Hooks-OxBQ0nIc.js → Hooks-wEmh_U6c.js} +1 -1
- package/dist/assets/{HostBilko-BNFC6bNZ.js → HostBilko-D_t7Rbi7.js} +1 -1
- package/dist/assets/{Library-DAMyrUtY.js → Library-CpArQ-OJ.js} +1 -1
- package/dist/assets/{ListDetail-BwywrpvE.js → ListDetail-pjaKYs84.js} +1 -1
- package/dist/assets/{MarkdownEditor-HEwjN1bp.js → MarkdownEditor-Xc141kjj.js} +1 -1
- package/dist/assets/{McpServers-D1VZF7Ix.js → McpServers-ftqaV3kn.js} +1 -1
- package/dist/assets/{Memory-CgYJ_QqD.js → Memory-ChMWkNd0.js} +6 -6
- package/dist/assets/{Panel-dTNWC59Q.js → Panel-D9Kr40Ai.js} +1 -1
- package/dist/assets/{Permissions-qI_NKi1b.js → Permissions-DKoNVgzj.js} +1 -1
- package/dist/assets/{Plugins-DZ0_CHiF.js → Plugins-BtChISho.js} +2 -2
- package/dist/assets/{ProvenanceBadge-B2i_uxCu.js → ProvenanceBadge-DBA5EcYy.js} +1 -1
- package/dist/assets/{SaveBar-4tNMCjUd.js → SaveBar-I0_dWNTX.js} +1 -1
- package/dist/assets/{Scheduler-CUen18Gc.js → Scheduler-CbES7MC8.js} +7 -7
- package/dist/assets/{ScopeSwitcher-Co3tkLTT.js → ScopeSwitcher-5GTEveb2.js} +1 -1
- package/dist/assets/Settings-BX3FElXk.js +3 -0
- package/dist/assets/{SkillReferenceGraph-BKcHtMPc.js → SkillReferenceGraph-DNBFGrYE.js} +1 -1
- package/dist/assets/{Skills-DHb4INtF.js → Skills-DJB6-bBM.js} +2 -2
- package/dist/assets/SystemPrompt-BiDDrJUA.js +1 -0
- package/dist/assets/{TagLibrary-xLP5dJvh.js → TagLibrary-_Wrevtop.js} +1 -1
- package/dist/assets/{TiptapBody-T-ULTjK4.js → TiptapBody-OWWXdLRy.js} +1 -1
- package/dist/assets/{Toggle-B-5M2hGe.js → Toggle-B122N0HL.js} +1 -1
- package/dist/assets/{index-CYhdtisq.css → index-CDo9xBR9.css} +1 -1
- package/dist/assets/{index-CzAxC432.js → index-DhvuQL4C.js} +316 -314
- package/dist/assets/{settingsSchema-C_nsemcF.js → settingsSchema-sGoCTd7J.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +4 -1
- package/scripts/hooks/guard-destructive-git.cjs +514 -0
- package/scripts/hooks/guard-inline-implementation.cjs +219 -0
- package/scripts/hooks/guard-prd-writes.cjs +200 -0
- package/src/main/__tests__/epicMintTelemetryTap.test.cjs +64 -0
- package/src/main/__tests__/health-delegation-chain.test.cjs +2 -1
- package/src/main/__tests__/health-queue-dispatch.test.cjs +84 -0
- package/src/main/__tests__/health-usage-poller.test.cjs +97 -0
- package/src/main/__tests__/health-worktree-cap-blocked.test.cjs +65 -0
- package/src/main/__tests__/opsErrorLogTelemetryTap.test.cjs +149 -0
- package/src/main/__tests__/pollLoop-dispatch-on-failure.test.cjs +120 -0
- package/src/main/__tests__/promptSessionTranscript.test.cjs +0 -0
- package/src/main/__tests__/queue-starvation-dispatch-driver.test.cjs +143 -0
- package/src/main/__tests__/rateLimitPollerStreak.test.cjs +79 -0
- package/src/main/__tests__/scheduleJobTransitionsTelemetryTap.test.cjs +72 -0
- package/src/main/__tests__/scheduler-inplace-salvage.test.cjs +74 -0
- package/src/main/__tests__/scheduler-job-overrun.test.cjs +58 -0
- package/src/main/__tests__/scheduler-notify-originating-tab-transcript.test.cjs +1 -0
- package/src/main/__tests__/scheduler-periodic-reverify-guard.test.cjs +134 -2
- package/src/main/__tests__/scheduler-reap-dead-running-jobs.test.cjs +33 -0
- package/src/main/__tests__/scheduler-stuck-failed-escalation.test.cjs +136 -0
- package/src/main/__tests__/telemetryClient.test.cjs +242 -1
- package/src/main/__tests__/telemetryContract.test.cjs +930 -0
- package/src/main/crashDiagnostics.cjs +29 -1
- package/src/main/health.cjs +197 -3
- package/src/main/index.cjs +65 -6
- package/src/main/ipcSchemas.cjs +19 -2
- package/src/main/lib/__tests__/crashTelemetry.test.cjs +103 -0
- package/src/main/lib/__tests__/delegationReadiness.test.cjs +302 -4
- package/src/main/lib/__tests__/fixtures/scheduler-machine.json.corrupt-1789147548 +34 -0
- package/src/main/lib/__tests__/gitWorktree.test.cjs +413 -1
- package/src/main/lib/__tests__/jobWorktreeBootLive.test.cjs +71 -0
- package/src/main/lib/__tests__/queueStoreAtomicWrite.test.cjs +88 -0
- package/src/main/lib/__tests__/queueStoreMachineStateRecovery.test.cjs +123 -0
- package/src/main/lib/__tests__/reaperHelpers.test.cjs +58 -0
- package/src/main/lib/__tests__/telemetryBacklog.test.cjs +626 -0
- package/src/main/lib/__tests__/telemetryBoot.test.cjs +123 -0
- package/src/main/lib/__tests__/telemetryConsent.test.cjs +136 -0
- package/src/main/lib/__tests__/telemetryCounters.test.cjs +57 -0
- package/src/main/lib/__tests__/telemetryCountersMetadataColumn.test.cjs +95 -0
- package/src/main/lib/crashTelemetry.cjs +37 -0
- package/src/main/lib/delegationReadiness.cjs +119 -1
- package/src/main/lib/epicMint.cjs +2 -0
- package/src/main/lib/gitWorktree.cjs +427 -17
- package/src/main/lib/jobWorktree.cjs +2 -1
- package/src/main/lib/jobWorktreeBootLive.cjs +51 -0
- package/src/main/lib/jobWorktreeTerminalOrphanLive.cjs +68 -0
- package/src/main/lib/opsErrorLog.cjs +78 -25
- package/src/main/lib/queueStore.cjs +241 -22
- package/src/main/lib/reaperHelpers.cjs +23 -1
- package/src/main/lib/scheduleJobSchema.cjs +7 -0
- package/src/main/lib/scheduleJobTransitions.cjs +12 -0
- package/src/main/lib/telemetryBacklog.cjs +601 -0
- package/src/main/lib/telemetryBoot.cjs +76 -0
- package/src/main/lib/telemetryClient.cjs +150 -5
- package/src/main/lib/telemetryConsent.cjs +34 -0
- package/src/main/lib/telemetryCounters.cjs +45 -0
- package/src/main/promptSessionTranscript.cjs +0 -0
- package/src/main/pty.cjs +2 -0
- package/src/main/scheduler.cjs +481 -33
- package/src/preload/api.d.ts +84 -4
- package/src/preload/index.cjs +9 -0
- package/dist/assets/Settings-DXVgKLUx.js +0 -3
- 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
|
+
});
|