Skip to content

Commit d37db1c

Browse files
committed
[Telemetry] Implement Phase 4: Integration Tests & Cleanup
1 parent 5be0ae1 commit d37db1c

2 files changed

Lines changed: 143 additions & 40 deletions

File tree

tests/telemetry/clearcut-logger.test.ts

Lines changed: 30 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -10,22 +10,23 @@ import {describe, it, afterEach, beforeEach} from 'node:test';
1010
import sinon from 'sinon';
1111

1212
import {ClearcutLogger} from '../../src/telemetry/clearcut-logger.js';
13-
import {ClearcutSender} from '../../src/telemetry/clearcut-sender.js';
13+
import {WatchdogClient} from '../../src/telemetry/watchdog-client.js';
1414
import type {Persistence} from '../../src/telemetry/persistence.js';
1515
import {FilePersistence} from '../../src/telemetry/persistence.js';
16+
import {IpcMessageType} from '../../src/telemetry/types.js';
1617

1718
describe('ClearcutLogger', () => {
1819
let mockPersistence: sinon.SinonStubbedInstance<Persistence>;
19-
let mockSender: sinon.SinonStubbedInstance<ClearcutSender>;
20+
let sendStub: sinon.SinonStub;
2021

2122
beforeEach(() => {
2223
mockPersistence = sinon.createStubInstance(FilePersistence, {
2324
loadState: Promise.resolve({
2425
lastActive: '',
2526
}),
2627
});
27-
mockSender = sinon.createStubInstance(ClearcutSender);
28-
mockSender.send.resolves();
28+
// Stub the prototype of WatchdogClient so newly created instances use it
29+
sendStub = sinon.stub(WatchdogClient.prototype, 'send');
2930
});
3031

3132
afterEach(() => {
@@ -36,40 +37,38 @@ describe('ClearcutLogger', () => {
3637
it('sends correct payload', async () => {
3738
const logger = new ClearcutLogger({
3839
persistence: mockPersistence,
39-
sender: mockSender,
40+
appVersion: '1.0.0',
4041
});
4142
await logger.logToolInvocation({
4243
toolName: 'test_tool',
4344
success: true,
4445
latencyMs: 123,
4546
});
4647

47-
assert(mockSender.send.calledOnce);
48-
const extension = mockSender.send.firstCall.args[0];
49-
assert.strictEqual(extension.tool_invocation?.tool_name, 'test_tool');
50-
assert.strictEqual(extension.tool_invocation?.success, true);
51-
assert.strictEqual(extension.tool_invocation?.latency_ms, 123);
48+
assert(sendStub.calledOnce);
49+
const msg = sendStub.firstCall.args[0];
50+
assert.strictEqual(msg.type, IpcMessageType.EVENT);
51+
assert.strictEqual(msg.payload.tool_invocation?.tool_name, 'test_tool');
52+
assert.strictEqual(msg.payload.tool_invocation?.success, true);
53+
assert.strictEqual(msg.payload.tool_invocation?.latency_ms, 123);
5254
});
5355
});
5456

5557
describe('logServerStart', () => {
5658
it('logs flag usage', async () => {
5759
const logger = new ClearcutLogger({
5860
persistence: mockPersistence,
59-
sender: mockSender,
61+
appVersion: '1.0.0',
6062
});
6163

6264
await logger.logServerStart({headless: true});
6365

6466
// Should have logged server start
65-
const calls = mockSender.send.getCalls();
66-
const serverStartCall = calls.find(call => {
67-
return !!call.args[0].server_start;
68-
});
69-
70-
assert(serverStartCall);
67+
assert(sendStub.calledOnce);
68+
const msg = sendStub.firstCall.args[0];
69+
assert.strictEqual(msg.type, IpcMessageType.EVENT);
7170
assert.strictEqual(
72-
serverStartCall.args[0].server_start?.flag_usage?.headless,
71+
msg.payload.server_start?.flag_usage?.headless,
7372
true,
7473
);
7574
});
@@ -86,17 +85,16 @@ describe('ClearcutLogger', () => {
8685

8786
const logger = new ClearcutLogger({
8887
persistence: mockPersistence,
89-
sender: mockSender,
88+
appVersion: '1.0.0',
9089
});
9190

9291
await logger.logDailyActiveIfNeeded();
9392

94-
const calls = mockSender.send.getCalls();
95-
const dailyActiveCall = calls.find(call => {
96-
return !!call.args[0].daily_active;
97-
});
98-
99-
assert(dailyActiveCall, 'Should have logged daily active');
93+
assert(sendStub.calledOnce);
94+
const msg = sendStub.firstCall.args[0];
95+
assert.strictEqual(msg.type, IpcMessageType.EVENT);
96+
assert.ok(msg.payload.daily_active);
97+
10098
assert(mockPersistence.saveState.called);
10199
});
102100

@@ -107,17 +105,12 @@ describe('ClearcutLogger', () => {
107105

108106
const logger = new ClearcutLogger({
109107
persistence: mockPersistence,
110-
sender: mockSender,
108+
appVersion: '1.0.0',
111109
});
112110

113111
await logger.logDailyActiveIfNeeded();
114112

115-
const calls = mockSender.send.getCalls();
116-
const dailyActiveCall = calls.find(call => {
117-
return !!call.args[0].daily_active;
118-
});
119-
120-
assert(!dailyActiveCall, 'Should NOT have logged daily active');
113+
assert(sendStub.notCalled);
121114
assert(mockPersistence.saveState.notCalled);
122115
});
123116

@@ -128,19 +121,16 @@ describe('ClearcutLogger', () => {
128121

129122
const logger = new ClearcutLogger({
130123
persistence: mockPersistence,
131-
sender: mockSender,
124+
appVersion: '1.0.0',
132125
});
133126

134127
await logger.logDailyActiveIfNeeded();
135128

136-
const calls = mockSender.send.getCalls();
137-
const dailyActiveCall = calls.find(call => {
138-
return !!call.args[0].daily_active;
139-
});
140-
141-
assert(dailyActiveCall, 'Should have logged daily active');
129+
assert(sendStub.calledOnce);
130+
const msg = sendStub.firstCall.args[0];
131+
assert.strictEqual(msg.type, IpcMessageType.EVENT);
142132
assert.strictEqual(
143-
dailyActiveCall.args[0].daily_active?.days_since_last_active,
133+
msg.payload.daily_active?.days_since_last_active,
144134
-1,
145135
);
146136
assert(mockPersistence.saveState.called);
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
/**
2+
* @license
3+
* Copyright 2026 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
import {describe, it, afterEach, beforeEach} from 'node:test';
8+
import assert from 'node:assert';
9+
import sinon from 'sinon';
10+
import {spawn, type ChildProcess} from 'node:child_process';
11+
import EventEmitter from 'node:events';
12+
import {Writable} from 'node:stream';
13+
14+
import {WatchdogClient} from '../../src/telemetry/watchdog-client.js';
15+
import {OsType, IpcMessageType} from '../../src/telemetry/types.js';
16+
17+
describe('WatchdogClient', () => {
18+
let spawnStub: sinon.SinonStub;
19+
let mockChildProcess: ChildProcess & {stdin: Writable; kill: sinon.SinonStub};
20+
21+
beforeEach(() => {
22+
// Mock child process
23+
const eventEmitter = new EventEmitter();
24+
const stdin = new Writable({
25+
write(_chunk, _encoding, callback) {
26+
callback();
27+
}
28+
});
29+
// Use stub instead of spy to allow throws()
30+
sinon.stub(stdin, 'write');
31+
32+
mockChildProcess = Object.assign(eventEmitter, {
33+
pid: 12345,
34+
stdin,
35+
stdout: null,
36+
stderr: null,
37+
kill: sinon.stub(),
38+
unref: sinon.stub(),
39+
destroyed: false,
40+
} as unknown as ChildProcess) as ChildProcess & {stdin: Writable; kill: sinon.SinonStub};
41+
42+
spawnStub = sinon.stub().returns(mockChildProcess);
43+
});
44+
45+
afterEach(() => {
46+
sinon.restore();
47+
});
48+
49+
it('spawns watchdog process with correct arguments', () => {
50+
new WatchdogClient({
51+
parentPid: 100,
52+
appVersion: '1.2.3',
53+
osType: OsType.OS_TYPE_MACOS,
54+
}, {spawn: spawnStub as unknown as typeof spawn});
55+
56+
assert.strictEqual(spawnStub.calledOnce, true);
57+
const args = spawnStub.firstCall.args;
58+
// args[0] is process.execPath
59+
// args[1] is arguments array
60+
const cmdArgs = args[1];
61+
62+
// Check main arg structure - we expect path to main.js as first arg
63+
assert.match(cmdArgs[0], /watchdog\/main\.js$/);
64+
assert.strictEqual(cmdArgs.includes('--parent-pid=100'), true);
65+
assert.strictEqual(cmdArgs.includes('--app-version=1.2.3'), true);
66+
assert.strictEqual(cmdArgs.includes('--os-type=2'), true);
67+
});
68+
69+
it('passes log-file argument if provided', () => {
70+
new WatchdogClient({
71+
parentPid: 100,
72+
appVersion: '1.0.0',
73+
osType: OsType.OS_TYPE_LINUX,
74+
logFile: '/tmp/test.log',
75+
}, {spawn: spawnStub as unknown as typeof spawn});
76+
77+
const cmdArgs = spawnStub.firstCall.args[1];
78+
assert.strictEqual(cmdArgs.includes('--log-file=/tmp/test.log'), true);
79+
});
80+
81+
it('sends IPC messages via stdin', () => {
82+
const client = new WatchdogClient({
83+
parentPid: 100,
84+
appVersion: '1.0.0',
85+
osType: OsType.OS_TYPE_LINUX,
86+
}, {spawn: spawnStub as unknown as typeof spawn});
87+
88+
const msg = {type: IpcMessageType.EVENT, payload: {}};
89+
client.send(msg);
90+
91+
const writeSpy = mockChildProcess.stdin.write as sinon.SinonSpy;
92+
assert.strictEqual(writeSpy.calledOnce, true);
93+
94+
const writtenData = writeSpy.firstCall.args[0];
95+
assert.strictEqual(writtenData.trim(), JSON.stringify(msg));
96+
});
97+
98+
it('handles write errors gracefully', () => {
99+
const client = new WatchdogClient({
100+
parentPid: 100,
101+
appVersion: '1.0.0',
102+
osType: OsType.OS_TYPE_LINUX,
103+
}, {spawn: spawnStub as unknown as typeof spawn});
104+
105+
const writeStub = mockChildProcess.stdin.write as unknown as sinon.SinonStub;
106+
writeStub.throws(new Error('EPIPE'));
107+
108+
// Should not throw
109+
assert.doesNotThrow(() => {
110+
client.send({type: IpcMessageType.EVENT, payload: {}});
111+
});
112+
});
113+
});

0 commit comments

Comments
 (0)