Skip to content

Commit 742dc8d

Browse files
authored
Support usage tracking message state (#405)
* wip * Support usageTrackingMessageLastShowVersion in state.ts
1 parent 5fec178 commit 742dc8d

File tree

4 files changed

+75
-20
lines changed

4 files changed

+75
-20
lines changed

config/__tests__/state.test.ts

Lines changed: 63 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { vi, describe, it, expect, beforeEach, afterEach, Mock } from 'vitest';
22
import path from 'path';
3-
import { STATE_FILE_PATH } from '../../constants/config.js';
3+
import { STATE_FILE_PATH, STATE_FLAGS } from '../../constants/config.js';
44

55
vi.mock('../../utils/lang', () => ({
66
i18n: vi.fn((key: string) => key),
@@ -129,15 +129,15 @@ describe('config/state', () => {
129129
expect(result).toBe(0);
130130
});
131131

132-
it('returns default value when state file has invalid structure (wrong type)', () => {
132+
it('accepts value even if type does not match default', () => {
133133
existsSyncSpy.mockReturnValue(true);
134134
readFileSyncSpy.mockReturnValue(
135135
JSON.stringify({ mcpTotalToolCalls: 'not-a-number' })
136136
);
137137

138138
const result = getStateValue('mcpTotalToolCalls');
139139

140-
expect(result).toBe(0);
140+
expect(result).toBe('not-a-number');
141141
});
142142

143143
it('ignores extra keys in state file', () => {
@@ -174,6 +174,43 @@ describe('config/state', () => {
174174
expect(result).toBe(0);
175175
});
176176

177+
it('returns usageTrackingMessageLastShowVersion from state file', () => {
178+
const mockState = {
179+
mcpTotalToolCalls: 0,
180+
usageTrackingMessageLastShowVersion: '5.3.2',
181+
};
182+
existsSyncSpy.mockReturnValue(true);
183+
readFileSyncSpy.mockReturnValue(JSON.stringify(mockState));
184+
185+
const result = getStateValue(
186+
STATE_FLAGS.USAGE_TRACKING_MESSAGE_LAST_SHOW_VERSION
187+
);
188+
189+
expect(result).toBe('5.3.2');
190+
});
191+
192+
it('returns undefined for usageTrackingMessageLastShowVersion when not in state file', () => {
193+
const mockState = { mcpTotalToolCalls: 5 };
194+
existsSyncSpy.mockReturnValue(true);
195+
readFileSyncSpy.mockReturnValue(JSON.stringify(mockState));
196+
197+
const result = getStateValue(
198+
STATE_FLAGS.USAGE_TRACKING_MESSAGE_LAST_SHOW_VERSION
199+
);
200+
201+
expect(result).toBeUndefined();
202+
});
203+
204+
it('returns undefined for usageTrackingMessageLastShowVersion when file does not exist', () => {
205+
existsSyncSpy.mockReturnValue(false);
206+
207+
const result = getStateValue(
208+
STATE_FLAGS.USAGE_TRACKING_MESSAGE_LAST_SHOW_VERSION
209+
);
210+
211+
expect(result).toBeUndefined();
212+
});
213+
177214
it('returns default state when JSON parses to an array', () => {
178215
existsSyncSpy.mockReturnValue(true);
179216
readFileSyncSpy.mockReturnValue(JSON.stringify([1, 2, 3]));
@@ -276,18 +313,36 @@ describe('config/state', () => {
276313
});
277314

278315
it('preserves other state values when updating one value', () => {
279-
const existingState = { mcpTotalToolCalls: 100 };
316+
const existingState = {
317+
mcpTotalToolCalls: 100,
318+
usageTrackingMessageLastShowVersion: '5.3.0',
319+
};
280320
existsSyncSpy.mockReturnValue(true);
281321
readFileSyncSpy.mockReturnValue(JSON.stringify(existingState));
282322
writeFileSyncSpy.mockImplementation(() => undefined);
283323

284324
setStateValue('mcpTotalToolCalls', 150);
285325

286-
expect(writeFileSyncSpy).toHaveBeenCalledWith(
287-
STATE_FILE_PATH,
288-
JSON.stringify({ mcpTotalToolCalls: 150 }, null, 2),
289-
'utf-8'
326+
const written = JSON.parse(writeFileSyncSpy.mock.calls[0][1]);
327+
expect(written.mcpTotalToolCalls).toBe(150);
328+
expect(written.usageTrackingMessageLastShowVersion).toBe('5.3.0');
329+
});
330+
331+
it('sets usageTrackingMessageLastShowVersion', () => {
332+
existsSyncSpy.mockReturnValue(true);
333+
readFileSyncSpy.mockReturnValue(
334+
JSON.stringify({ mcpTotalToolCalls: 10 })
290335
);
336+
writeFileSyncSpy.mockImplementation(() => undefined);
337+
338+
setStateValue(
339+
STATE_FLAGS.USAGE_TRACKING_MESSAGE_LAST_SHOW_VERSION,
340+
'5.3.2'
341+
);
342+
343+
const written = JSON.parse(writeFileSyncSpy.mock.calls[0][1]);
344+
expect(written.mcpTotalToolCalls).toBe(10);
345+
expect(written.usageTrackingMessageLastShowVersion).toBe('5.3.2');
291346
});
292347

293348
it('throws error when write fails with non-Error', () => {

config/state.ts

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ const i18nKey = 'config.state';
1010

1111
const DEFAULT_STATE: HubSpotState = {
1212
[STATE_FLAGS.MCP_TOTAL_TOOL_CALLS]: 0,
13+
[STATE_FLAGS.USAGE_TRACKING_MESSAGE_LAST_SHOW_VERSION]: undefined,
1314
};
1415

1516
function ensureCLIDirectory(): void {
@@ -27,23 +28,18 @@ function ensureCLIDirectory(): void {
2728
}
2829
}
2930

30-
function sanitizeAndMerge(parsed: unknown): HubSpotState {
31+
function parseState(parsed: unknown): HubSpotState {
3132
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
3233
return structuredClone(DEFAULT_STATE);
3334
}
3435

35-
const state = parsed as HubSpotState;
36+
const record = parsed as Record<string, unknown>;
3637
const result: HubSpotState = structuredClone(DEFAULT_STATE);
3738

38-
for (const key in DEFAULT_STATE) {
39-
const typedKey = key as keyof HubSpotState;
40-
if (
41-
key in state &&
42-
typeof state[typedKey] === typeof DEFAULT_STATE[typedKey]
43-
) {
44-
result[typedKey] = state[typedKey];
39+
for (const key of Object.keys(DEFAULT_STATE)) {
40+
if (key in record && record[key] !== undefined) {
41+
Object.assign(result, { [key]: record[key] });
4542
}
46-
// keys not in parsed file remain as DEFAULT values
4743
}
4844

4945
return result;
@@ -63,7 +59,7 @@ function getCurrentState(): HubSpotState {
6359
}
6460

6561
const parsed = JSON.parse(data);
66-
return sanitizeAndMerge(parsed);
62+
return parseState(parsed);
6763
} catch (error) {
6864
logger.debug(
6965
i18n(`${i18nKey}.getCurrentState.errors.errorReading`, {

constants/config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,8 @@ export const HUBSPOT_ACCOUNT_TYPE_STRINGS = {
7979

8080
export const STATE_FLAGS = {
8181
MCP_TOTAL_TOOL_CALLS: 'mcpTotalToolCalls',
82+
USAGE_TRACKING_MESSAGE_LAST_SHOW_VERSION:
83+
'usageTrackingMessageLastShowVersion',
8284
} as const;
8385

8486
export const CONFIG_FLAGS = {

types/Config.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {
22
CONFIG_FLAGS,
33
HUBSPOT_CONFIG_ERROR_TYPES,
44
HUBSPOT_CONFIG_OPERATIONS,
5+
STATE_FLAGS,
56
} from '../constants/config.js';
67
import {
78
DeprecatedHubSpotConfigAccountFields,
@@ -41,7 +42,8 @@ export type GitInclusionResult = {
4142
export type ConfigFlag = ValueOf<typeof CONFIG_FLAGS>;
4243

4344
export type HubSpotState = {
44-
mcpTotalToolCalls: number;
45+
[STATE_FLAGS.MCP_TOTAL_TOOL_CALLS]: number;
46+
[STATE_FLAGS.USAGE_TRACKING_MESSAGE_LAST_SHOW_VERSION]?: string;
4547
};
4648

4749
export type HubSpotConfigErrorType = ValueOf<typeof HUBSPOT_CONFIG_ERROR_TYPES>;

0 commit comments

Comments
 (0)