Files
claude-task-master/tests/unit/scripts/modules/telemetry-submission.test.js
Eyal Toledano 2819be51d3 feat: Implement TaskMaster AI Gateway integration with enhanced UX
- Fix Zod schema conversion, update headers, add premium telemetry display, improve user auth flow, and standardize email fields

Functionally complete on this end, mostly polish around user experience and need to add in profile, upgrade/downgrade, etc.

But the AI commands are working off the gateway.
2025-06-01 19:37:12 -04:00

405 lines
12 KiB
JavaScript

/**
* Unit Tests for Telemetry Submission Service - Task 90.2
* Tests the secure telemetry submission with gateway integration
*/
import { jest } from "@jest/globals";
// Mock config-manager before importing submitTelemetryData
jest.unstable_mockModule(
"../../../../scripts/modules/config-manager.js",
() => ({
getConfig: jest.fn(),
getDebugFlag: jest.fn(() => false),
getLogLevel: jest.fn(() => "info"),
getMainProvider: jest.fn(() => "openai"),
getMainModelId: jest.fn(() => "gpt-4"),
getResearchProvider: jest.fn(() => "openai"),
getResearchModelId: jest.fn(() => "gpt-4"),
getFallbackProvider: jest.fn(() => "openai"),
getFallbackModelId: jest.fn(() => "gpt-3.5-turbo"),
getParametersForRole: jest.fn(() => ({
maxTokens: 4000,
temperature: 0.7,
})),
getUserId: jest.fn(() => "test-user-id"),
MODEL_MAP: {},
getBaseUrlForRole: jest.fn(() => null),
isApiKeySet: jest.fn(() => true),
getOllamaBaseURL: jest.fn(() => "http://localhost:11434/api"),
getAzureBaseURL: jest.fn(() => null),
getVertexProjectId: jest.fn(() => null),
getVertexLocation: jest.fn(() => null),
getDefaultSubtasks: jest.fn(() => 5),
getProjectName: jest.fn(() => "Test Project"),
getDefaultPriority: jest.fn(() => "medium"),
getDefaultNumTasks: jest.fn(() => 10),
getTelemetryEnabled: jest.fn(() => true),
})
);
// Mock fetch globally
global.fetch = jest.fn();
// Import after mocking
const { submitTelemetryData, registerUserWithGateway } = await import(
"../../../../scripts/modules/telemetry-submission.js"
);
const { getConfig } = await import(
"../../../../scripts/modules/config-manager.js"
);
describe("Telemetry Submission Service", () => {
beforeEach(() => {
jest.clearAllMocks();
global.fetch.mockClear();
});
describe("should send telemetry data to remote database endpoint", () => {
it("should successfully submit telemetry data to hardcoded gateway endpoint", async () => {
// Mock successful config with proper structure
getConfig.mockReturnValue({
account: {
userId: "test-user-id",
},
});
// Mock environment variables for telemetry config
process.env.TASKMASTER_SERVICE_ID = "test-api-key";
process.env.TASKMASTER_USER_EMAIL = "test@example.com";
// Mock successful response
global.fetch.mockResolvedValueOnce({
ok: true,
json: async () => ({ id: "telemetry-123" }),
});
const telemetryData = {
timestamp: new Date().toISOString(),
userId: "test-user-id",
commandName: "test-command",
modelUsed: "claude-3-sonnet",
totalCost: 0.001,
currency: "USD",
commandArgs: { secret: "should-be-sent" },
fullOutput: { debug: "should-be-sent" },
};
const result = await submitTelemetryData(telemetryData);
expect(result.success).toBe(true);
expect(result.id).toBe("telemetry-123");
expect(global.fetch).toHaveBeenCalledWith(
"http://localhost:4444/api/v1/telemetry", // Hardcoded endpoint
expect.objectContaining({
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer test-api-key",
"X-User-Email": "test@example.com",
},
body: expect.stringContaining('"commandName":"test-command"'),
})
);
// Verify sensitive data IS included in submission to gateway
const sentData = JSON.parse(global.fetch.mock.calls[0][1].body);
expect(sentData.commandArgs).toEqual({ secret: "should-be-sent" });
expect(sentData.fullOutput).toEqual({ debug: "should-be-sent" });
// Clean up
delete process.env.TASKMASTER_SERVICE_ID;
delete process.env.TASKMASTER_USER_EMAIL;
});
it("should implement retry logic for failed requests", async () => {
getConfig.mockReturnValue({
account: {
userId: "test-user-id",
},
});
// Mock environment variables
process.env.TASKMASTER_SERVICE_ID = "test-api-key";
process.env.TASKMASTER_USER_EMAIL = "test@example.com";
// Mock 3 network failures then final HTTP error
global.fetch
.mockRejectedValueOnce(new Error("Network error"))
.mockRejectedValueOnce(new Error("Network error"))
.mockRejectedValueOnce(new Error("Network error"));
const telemetryData = {
timestamp: new Date().toISOString(),
userId: "test-user-id",
commandName: "test-command",
totalCost: 0.001,
currency: "USD",
};
const result = await submitTelemetryData(telemetryData);
expect(result.success).toBe(false);
expect(result.error).toContain("Network error");
expect(global.fetch).toHaveBeenCalledTimes(3);
// Clean up
delete process.env.TASKMASTER_SERVICE_ID;
delete process.env.TASKMASTER_USER_EMAIL;
}, 10000);
it("should handle failures gracefully without blocking execution", async () => {
getConfig.mockReturnValue({
account: {
userId: "test-user-id",
},
});
// Mock environment variables
process.env.TASKMASTER_SERVICE_ID = "test-api-key";
process.env.TASKMASTER_USER_EMAIL = "test@example.com";
global.fetch.mockRejectedValue(new Error("Network failure"));
const telemetryData = {
timestamp: new Date().toISOString(),
userId: "test-user-id",
commandName: "test-command",
totalCost: 0.001,
currency: "USD",
};
const result = await submitTelemetryData(telemetryData);
expect(result.success).toBe(false);
expect(result.error).toContain("Network failure");
expect(global.fetch).toHaveBeenCalledTimes(3); // All retries attempted
// Clean up
delete process.env.TASKMASTER_SERVICE_ID;
delete process.env.TASKMASTER_USER_EMAIL;
}, 10000);
it("should respect user opt-out preferences", async () => {
// Mock getTelemetryEnabled to return false for this test
const { getTelemetryEnabled } = await import(
"../../../../scripts/modules/config-manager.js"
);
getTelemetryEnabled.mockReturnValue(false);
getConfig.mockReturnValue({
account: {
telemetryEnabled: false,
},
});
const telemetryData = {
timestamp: new Date().toISOString(),
userId: "test-user-id",
commandName: "test-command",
totalCost: 0.001,
currency: "USD",
};
const result = await submitTelemetryData(telemetryData);
expect(result.success).toBe(true);
expect(result.skipped).toBe(true);
expect(result.reason).toBe("Telemetry disabled by user preference");
expect(global.fetch).not.toHaveBeenCalled();
// Reset the mock for other tests
getTelemetryEnabled.mockReturnValue(true);
});
it("should validate telemetry data before submission", async () => {
getConfig.mockReturnValue({
account: {
userId: "test-user-id",
},
});
// Mock environment variables so config is valid
process.env.TASKMASTER_SERVICE_ID = "test-api-key";
process.env.TASKMASTER_USER_EMAIL = "test@example.com";
const invalidTelemetryData = {
// Missing required fields
commandName: "test-command",
};
const result = await submitTelemetryData(invalidTelemetryData);
expect(result.success).toBe(false);
expect(result.error).toContain("Telemetry data validation failed");
expect(global.fetch).not.toHaveBeenCalled();
// Clean up
delete process.env.TASKMASTER_SERVICE_ID;
delete process.env.TASKMASTER_USER_EMAIL;
});
it("should handle HTTP error responses appropriately", async () => {
getConfig.mockReturnValue({
account: {
userId: "test-user-id",
},
});
// Mock environment variables with invalid API key
process.env.TASKMASTER_SERVICE_ID = "invalid-key";
process.env.TASKMASTER_USER_EMAIL = "test@example.com";
global.fetch.mockResolvedValueOnce({
ok: false,
status: 401,
statusText: "Unauthorized",
json: async () => ({}),
});
const telemetryData = {
timestamp: new Date().toISOString(),
userId: "test-user-id",
commandName: "test-command",
totalCost: 0.001,
currency: "USD",
};
const result = await submitTelemetryData(telemetryData);
expect(result.success).toBe(false);
expect(result.statusCode).toBe(401);
expect(global.fetch).toHaveBeenCalledTimes(1); // No retries for auth errors
// Clean up
delete process.env.TASKMASTER_SERVICE_ID;
delete process.env.TASKMASTER_USER_EMAIL;
});
});
describe("Gateway User Registration", () => {
it("should successfully register a user with gateway using /auth/init", async () => {
const mockResponse = {
success: true,
message: "New user created successfully",
data: {
userId: "test-user-id",
isNewUser: true,
user: {
email: "test@example.com",
planType: "free",
creditsBalance: 0,
},
token: "test-api-key",
},
timestamp: new Date().toISOString(),
};
global.fetch.mockResolvedValueOnce({
ok: true,
json: async () => mockResponse,
});
const result = await registerUserWithGateway("test@example.com");
expect(result).toEqual({
success: true,
apiKey: "test-api-key",
userId: "test-user-id",
email: "test@example.com",
isNewUser: true,
});
expect(global.fetch).toHaveBeenCalledWith(
"http://localhost:4444/auth/init",
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ email: "test@example.com" }),
}
);
});
it("should handle existing user with /auth/init", async () => {
const mockResponse = {
success: true,
message: "Existing user found",
data: {
userId: "existing-user-id",
isNewUser: false,
user: {
email: "existing@example.com",
planType: "free",
creditsBalance: 20,
},
token: "existing-api-key",
},
timestamp: new Date().toISOString(),
};
global.fetch.mockResolvedValueOnce({
ok: true,
json: async () => mockResponse,
});
const result = await registerUserWithGateway("existing@example.com");
expect(result).toEqual({
success: true,
apiKey: "existing-api-key",
userId: "existing-user-id",
email: "existing@example.com",
isNewUser: false,
});
});
it("should handle registration failures gracefully", async () => {
global.fetch.mockResolvedValueOnce({
ok: false,
status: 500,
statusText: "Internal Server Error",
});
const result = await registerUserWithGateway("test@example.com");
expect(result).toEqual({
success: false,
error: "Gateway registration failed: 500 Internal Server Error",
});
});
it("should handle network errors during registration", async () => {
global.fetch.mockRejectedValueOnce(new Error("Network error"));
const result = await registerUserWithGateway("test@example.com");
expect(result).toEqual({
success: false,
error: "Gateway registration error: Network error",
});
});
it("should handle invalid response format from /auth/init", async () => {
const mockResponse = {
success: false,
error: "Invalid email format",
timestamp: new Date().toISOString(),
};
global.fetch.mockResolvedValueOnce({
ok: true,
json: async () => mockResponse,
});
const result = await registerUserWithGateway("invalid-email");
expect(result).toEqual({
success: false,
error: "Invalid email format",
});
});
});
});