@revenium/claude-code-metering 0.1.4 → 0.1.5
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/.env.example +15 -0
- package/.eslintrc.js +24 -0
- package/.github/workflows/branch-bypass-alert.yml +68 -0
- package/CODE_OF_CONDUCT.md +57 -0
- package/CONTRIBUTING.md +73 -0
- package/README.md +57 -3
- package/SECURITY.md +46 -0
- package/dist/cli/commands/setup.js +3 -1
- package/dist/cli/commands/setup.js.map +1 -1
- package/dist/core/api/client.d.ts.map +1 -1
- package/dist/core/api/client.js +4 -1
- package/dist/core/api/client.js.map +1 -1
- package/dist/core/tool-context.d.ts +6 -0
- package/dist/core/tool-context.d.ts.map +1 -0
- package/dist/core/tool-context.js +21 -0
- package/dist/core/tool-context.js.map +1 -0
- package/dist/core/tool-tracker.d.ts +4 -0
- package/dist/core/tool-tracker.d.ts.map +1 -0
- package/dist/core/tool-tracker.js +156 -0
- package/dist/core/tool-tracker.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +15 -0
- package/dist/types/index.js.map +1 -1
- package/dist/types/tool-metering.d.ts +36 -0
- package/dist/types/tool-metering.d.ts.map +1 -0
- package/dist/types/tool-metering.js +3 -0
- package/dist/types/tool-metering.js.map +1 -0
- package/docs/research/settings-json-telemetry-findings.md +171 -0
- package/examples/README.md +114 -0
- package/examples/validation/validate-installation.sh +212 -0
- package/package.json +1 -7
- package/public-allowlist-node.txt +7 -0
- package/src/cli/commands/backfill.ts +865 -0
- package/src/cli/commands/setup.ts +254 -0
- package/src/cli/commands/status.ts +108 -0
- package/src/cli/commands/test.ts +91 -0
- package/src/cli/index.ts +103 -0
- package/src/core/api/client.ts +194 -0
- package/src/core/config/loader.ts +217 -0
- package/src/core/config/validator.ts +142 -0
- package/src/core/config/writer.ts +212 -0
- package/src/core/shell/detector.ts +92 -0
- package/src/core/shell/profile-updater.ts +131 -0
- package/src/core/tool-context.ts +23 -0
- package/src/core/tool-tracker.ts +204 -0
- package/src/index.ts +12 -0
- package/src/types/index.ts +110 -0
- package/src/types/tool-metering.ts +38 -0
- package/src/utils/constants.ts +80 -0
- package/src/utils/hashing.ts +35 -0
- package/src/utils/masking.ts +32 -0
- package/tests/integration/cli-commands.test.ts +158 -0
- package/tests/unit/backfill-command.test.ts +366 -0
- package/tests/unit/backfill-helpers.test.ts +397 -0
- package/tests/unit/backfill-parse.test.ts +276 -0
- package/tests/unit/backfill-stream.test.ts +147 -0
- package/tests/unit/backfill.test.ts +344 -0
- package/tests/unit/cli-index.test.ts +193 -0
- package/tests/unit/client.test.ts +195 -0
- package/tests/unit/detector.test.ts +247 -0
- package/tests/unit/hashing.test.ts +121 -0
- package/tests/unit/loader.test.ts +272 -0
- package/tests/unit/masking.test.ts +46 -0
- package/tests/unit/profile-updater.test.ts +146 -0
- package/tests/unit/setup.test.ts +557 -0
- package/tests/unit/status.test.ts +149 -0
- package/tests/unit/test.test.ts +165 -0
- package/tests/unit/validator.test.ts +211 -0
- package/tests/unit/writer.test.ts +176 -0
- package/tsconfig.json +20 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import type { SubscriptionTier } from "../utils/constants.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Configuration stored in ~/.claude/revenium.env
|
|
5
|
+
*/
|
|
6
|
+
export interface ReveniumConfig {
|
|
7
|
+
apiKey: string;
|
|
8
|
+
endpoint: string;
|
|
9
|
+
email?: string;
|
|
10
|
+
subscriptionTier?: SubscriptionTier;
|
|
11
|
+
/** Optional override for the cost multiplier (overrides tier default) */
|
|
12
|
+
costMultiplierOverride?: number;
|
|
13
|
+
/** Optional organization name for attributing costs to a specific customer/company */
|
|
14
|
+
organizationName?: string;
|
|
15
|
+
/**
|
|
16
|
+
* @deprecated Use organizationName instead. This field will be removed in a future version.
|
|
17
|
+
* Organization or company identifier
|
|
18
|
+
*/
|
|
19
|
+
organizationId?: string;
|
|
20
|
+
/** Optional product name for attributing costs to a specific product/project */
|
|
21
|
+
productName?: string;
|
|
22
|
+
/**
|
|
23
|
+
* @deprecated Use productName instead. This field will be removed in a future version.
|
|
24
|
+
* Product or application identifier
|
|
25
|
+
*/
|
|
26
|
+
productId?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Result of configuration validation
|
|
31
|
+
*/
|
|
32
|
+
export interface ValidationResult {
|
|
33
|
+
valid: boolean;
|
|
34
|
+
errors: string[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Result of endpoint health check
|
|
39
|
+
*/
|
|
40
|
+
export interface HealthCheckResult {
|
|
41
|
+
healthy: boolean;
|
|
42
|
+
statusCode?: number;
|
|
43
|
+
message: string;
|
|
44
|
+
latencyMs?: number;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Shell types supported for profile updates
|
|
49
|
+
*/
|
|
50
|
+
export type ShellType = "bash" | "zsh" | "fish" | "unknown";
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Result of shell profile update
|
|
54
|
+
*/
|
|
55
|
+
export interface ShellUpdateResult {
|
|
56
|
+
success: boolean;
|
|
57
|
+
shellType: ShellType;
|
|
58
|
+
profilePath?: string;
|
|
59
|
+
message: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* OTLP log payload structure (matching backend expectations)
|
|
64
|
+
*/
|
|
65
|
+
export interface OTLPLogsPayload {
|
|
66
|
+
resourceLogs: Array<{
|
|
67
|
+
resource?: {
|
|
68
|
+
attributes?: Array<{
|
|
69
|
+
key: string;
|
|
70
|
+
value: OTLPValue;
|
|
71
|
+
}>;
|
|
72
|
+
};
|
|
73
|
+
scopeLogs: Array<{
|
|
74
|
+
scope?: {
|
|
75
|
+
name: string;
|
|
76
|
+
version: string;
|
|
77
|
+
};
|
|
78
|
+
logRecords: Array<{
|
|
79
|
+
timeUnixNano?: string;
|
|
80
|
+
body: OTLPValue;
|
|
81
|
+
attributes: Array<{
|
|
82
|
+
key: string;
|
|
83
|
+
value: OTLPValue;
|
|
84
|
+
}>;
|
|
85
|
+
}>;
|
|
86
|
+
}>;
|
|
87
|
+
}>;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* OTLP any value type
|
|
92
|
+
*/
|
|
93
|
+
export interface OTLPValue {
|
|
94
|
+
stringValue?: string;
|
|
95
|
+
intValue?: number;
|
|
96
|
+
doubleValue?: number;
|
|
97
|
+
boolValue?: boolean;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Response from OTLP endpoint
|
|
102
|
+
*/
|
|
103
|
+
export interface OTLPResponse {
|
|
104
|
+
id: string;
|
|
105
|
+
resourceType: string;
|
|
106
|
+
processedEvents: number;
|
|
107
|
+
created: string;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export * from "./tool-metering.js";
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export interface ToolContext {
|
|
2
|
+
sessionId?: string;
|
|
3
|
+
userId?: string;
|
|
4
|
+
organizationName?: string;
|
|
5
|
+
productName?: string;
|
|
6
|
+
apiKey?: string;
|
|
7
|
+
endpoint?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface ToolMetadata {
|
|
11
|
+
description?: string;
|
|
12
|
+
category?: string;
|
|
13
|
+
version?: string;
|
|
14
|
+
tags?: string[];
|
|
15
|
+
outputFields?: string[];
|
|
16
|
+
usageMetadata?: Record<string, unknown>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface ToolEventPayload {
|
|
20
|
+
toolId: string;
|
|
21
|
+
sessionId: string;
|
|
22
|
+
startTime: number;
|
|
23
|
+
endTime: number;
|
|
24
|
+
durationMs: number;
|
|
25
|
+
success: boolean;
|
|
26
|
+
errorMessage?: string;
|
|
27
|
+
metadata?: ToolMetadata;
|
|
28
|
+
userId?: string;
|
|
29
|
+
organizationName?: string;
|
|
30
|
+
productName?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface ToolCallReport {
|
|
34
|
+
success: boolean;
|
|
35
|
+
durationMs: number;
|
|
36
|
+
errorMessage?: string;
|
|
37
|
+
metadata?: ToolMetadata;
|
|
38
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Constants for the Revenium Claude Code Metering CLI
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/** Default Revenium API base URL */
|
|
6
|
+
export const DEFAULT_REVENIUM_URL = 'https://api.revenium.ai';
|
|
7
|
+
|
|
8
|
+
/** Path appended to base URL for OTLP endpoint */
|
|
9
|
+
export const OTLP_PATH = '/meter/v2/otlp';
|
|
10
|
+
|
|
11
|
+
/** API key prefix required for valid Revenium API keys */
|
|
12
|
+
export const API_KEY_PREFIX = 'hak_';
|
|
13
|
+
|
|
14
|
+
/** Directory for Claude Code configuration */
|
|
15
|
+
export const CLAUDE_CONFIG_DIR = '.claude';
|
|
16
|
+
|
|
17
|
+
/** Filename for Revenium environment configuration */
|
|
18
|
+
export const REVENIUM_ENV_FILE = 'revenium.env';
|
|
19
|
+
|
|
20
|
+
/** File permissions for config file (owner read/write only) */
|
|
21
|
+
export const CONFIG_FILE_MODE = 0o600;
|
|
22
|
+
|
|
23
|
+
/** Available subscription tiers with their cost multipliers
|
|
24
|
+
*
|
|
25
|
+
* Cost multipliers represent the effective discount vs API pricing.
|
|
26
|
+
* Values are estimates based on fully consuming monthly token allotments.
|
|
27
|
+
*
|
|
28
|
+
* Calculation basis: Max 20x tier ($200 for 20X tokens = $2,500 API equivalent)
|
|
29
|
+
* establishes $125 per X tokens at API rates. Other tiers calculated proportionally.
|
|
30
|
+
*
|
|
31
|
+
* For detailed explanation and manual override instructions, see README.md
|
|
32
|
+
*/
|
|
33
|
+
export const SUBSCRIPTION_TIER_CONFIG = {
|
|
34
|
+
pro: {
|
|
35
|
+
name: 'Pro (~$20 USD/month or local equivalent)',
|
|
36
|
+
multiplier: 0.16, // $20 / $125 API equivalent = 16%
|
|
37
|
+
},
|
|
38
|
+
max_5x: {
|
|
39
|
+
name: 'Max 5x (~$100 USD/month or local equivalent)',
|
|
40
|
+
multiplier: 0.16, // $100 / $625 API equivalent = 16%
|
|
41
|
+
},
|
|
42
|
+
max_20x: {
|
|
43
|
+
name: 'Max 20x (~$200 USD/month or local equivalent)',
|
|
44
|
+
multiplier: 0.08, // $200 / $2,500 API equivalent = 8% (baseline from real data)
|
|
45
|
+
},
|
|
46
|
+
team_premium: {
|
|
47
|
+
name: 'Team Premium (~$125 USD/seat or local equivalent)',
|
|
48
|
+
multiplier: 0.20, // $125 / $625 API equivalent = 20%
|
|
49
|
+
},
|
|
50
|
+
enterprise: {
|
|
51
|
+
name: 'Enterprise (custom)',
|
|
52
|
+
multiplier: 0.05, // Custom pricing with best discounts
|
|
53
|
+
},
|
|
54
|
+
api: {
|
|
55
|
+
name: 'API (no subscription)',
|
|
56
|
+
multiplier: 1.0, // Full API pricing
|
|
57
|
+
},
|
|
58
|
+
} as const;
|
|
59
|
+
|
|
60
|
+
export const SUBSCRIPTION_TIERS = Object.keys(SUBSCRIPTION_TIER_CONFIG) as ReadonlyArray<keyof typeof SUBSCRIPTION_TIER_CONFIG>;
|
|
61
|
+
|
|
62
|
+
export type SubscriptionTier = keyof typeof SUBSCRIPTION_TIER_CONFIG;
|
|
63
|
+
|
|
64
|
+
/** Get the cost multiplier for a given tier */
|
|
65
|
+
export function getCostMultiplier(tier: SubscriptionTier): number {
|
|
66
|
+
return SUBSCRIPTION_TIER_CONFIG[tier].multiplier;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Environment variable names */
|
|
70
|
+
export const ENV_VARS = {
|
|
71
|
+
TELEMETRY_ENABLED: 'CLAUDE_CODE_ENABLE_TELEMETRY',
|
|
72
|
+
OTLP_ENDPOINT: 'OTEL_EXPORTER_OTLP_ENDPOINT',
|
|
73
|
+
OTLP_HEADERS: 'OTEL_EXPORTER_OTLP_HEADERS',
|
|
74
|
+
OTLP_PROTOCOL: 'OTEL_EXPORTER_OTLP_PROTOCOL',
|
|
75
|
+
SUBSCRIBER_EMAIL: 'REVENIUM_SUBSCRIBER_EMAIL',
|
|
76
|
+
SUBSCRIPTION: 'CLAUDE_CODE_SUBSCRIPTION',
|
|
77
|
+
COST_MULTIPLIER: 'CLAUDE_CODE_COST_MULTIPLIER',
|
|
78
|
+
ORGANIZATION_ID: 'REVENIUM_ORGANIZATION_ID',
|
|
79
|
+
PRODUCT_ID: 'REVENIUM_PRODUCT_ID',
|
|
80
|
+
} as const;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
export interface TransactionIdComponents {
|
|
4
|
+
sessionId: string;
|
|
5
|
+
timestamp: string;
|
|
6
|
+
model: string;
|
|
7
|
+
inputTokens: number;
|
|
8
|
+
outputTokens: number;
|
|
9
|
+
cacheReadTokens: number;
|
|
10
|
+
cacheCreationTokens: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Generates a deterministic transaction ID from record components.
|
|
15
|
+
* Uses SHA-256 hash truncated to 32 hex characters.
|
|
16
|
+
*
|
|
17
|
+
* IMPORTANT: This formula must match ClaudeCodeMapper.kt in the backend.
|
|
18
|
+
* Format: sessionId|timestamp|model|input|output|cacheRead|cacheCreation
|
|
19
|
+
*/
|
|
20
|
+
export function generateTransactionId(components: TransactionIdComponents): string {
|
|
21
|
+
const input = [
|
|
22
|
+
components.sessionId,
|
|
23
|
+
components.timestamp,
|
|
24
|
+
components.model,
|
|
25
|
+
components.inputTokens,
|
|
26
|
+
components.outputTokens,
|
|
27
|
+
components.cacheReadTokens,
|
|
28
|
+
components.cacheCreationTokens,
|
|
29
|
+
].join('|');
|
|
30
|
+
|
|
31
|
+
return createHash('sha256')
|
|
32
|
+
.update(input)
|
|
33
|
+
.digest('hex')
|
|
34
|
+
.substring(0, 32);
|
|
35
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility functions for masking sensitive data in terminal output
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Masks an API key, showing only the prefix and last 4 characters.
|
|
7
|
+
* Example: "hak_tenant_abc123xyz" -> "hak_***xyz"
|
|
8
|
+
*/
|
|
9
|
+
export function maskApiKey(apiKey: string): string {
|
|
10
|
+
if (!apiKey || apiKey.length < 8) {
|
|
11
|
+
return '***';
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const prefix = apiKey.substring(0, 4); // "hak_"
|
|
15
|
+
const lastFour = apiKey.substring(apiKey.length - 4);
|
|
16
|
+
return `${prefix}***${lastFour}`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Masks an email address, showing only the first character and domain.
|
|
21
|
+
* Example: "dev@company.com" -> "d***@company.com"
|
|
22
|
+
*/
|
|
23
|
+
export function maskEmail(email: string): string {
|
|
24
|
+
const atIndex = email.indexOf('@');
|
|
25
|
+
if (atIndex <= 0) {
|
|
26
|
+
return '***';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const firstChar = email.charAt(0);
|
|
30
|
+
const domain = email.substring(atIndex);
|
|
31
|
+
return `${firstChar}***${domain}`;
|
|
32
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { setupCommand } from "../../src/cli/commands/setup.js";
|
|
3
|
+
import { backfillCommand } from "../../src/cli/commands/backfill.js";
|
|
4
|
+
import * as loader from "../../src/core/config/loader.js";
|
|
5
|
+
import * as writer from "../../src/core/config/writer.js";
|
|
6
|
+
import * as client from "../../src/core/api/client.js";
|
|
7
|
+
import * as profileUpdater from "../../src/core/shell/profile-updater.js";
|
|
8
|
+
import * as detector from "../../src/core/shell/detector.js";
|
|
9
|
+
import inquirer from "inquirer";
|
|
10
|
+
import { readdir } from "node:fs/promises";
|
|
11
|
+
import { existsSync } from "node:fs";
|
|
12
|
+
|
|
13
|
+
vi.mock("inquirer");
|
|
14
|
+
vi.mock("../../src/core/config/loader.js");
|
|
15
|
+
vi.mock("../../src/core/config/writer.js");
|
|
16
|
+
vi.mock("../../src/core/api/client.js");
|
|
17
|
+
vi.mock("../../src/core/shell/profile-updater.js");
|
|
18
|
+
vi.mock("../../src/core/shell/detector.js");
|
|
19
|
+
vi.mock("node:fs/promises");
|
|
20
|
+
vi.mock("node:fs");
|
|
21
|
+
vi.mock("ora", () => ({
|
|
22
|
+
default: vi.fn(() => ({
|
|
23
|
+
start: vi.fn().mockReturnThis(),
|
|
24
|
+
succeed: vi.fn().mockReturnThis(),
|
|
25
|
+
fail: vi.fn().mockReturnThis(),
|
|
26
|
+
warn: vi.fn().mockReturnThis(),
|
|
27
|
+
text: "",
|
|
28
|
+
})),
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
describe("CLI Commands Integration", () => {
|
|
32
|
+
beforeEach(() => {
|
|
33
|
+
vi.clearAllMocks();
|
|
34
|
+
vi.spyOn(console, "log").mockImplementation(() => {});
|
|
35
|
+
vi.spyOn(console, "error").mockImplementation(() => {});
|
|
36
|
+
vi.spyOn(process, "exit").mockImplementation((() => {}) as any);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
afterEach(() => {
|
|
40
|
+
vi.restoreAllMocks();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe("setup command end-to-end", () => {
|
|
44
|
+
it("should complete full setup flow successfully", async () => {
|
|
45
|
+
vi.mocked(inquirer.prompt).mockResolvedValue({
|
|
46
|
+
apiKey: "hak_test123",
|
|
47
|
+
endpoint: "https://api.revenium.ai",
|
|
48
|
+
email: "test@example.com",
|
|
49
|
+
tier: "pro",
|
|
50
|
+
} as any);
|
|
51
|
+
|
|
52
|
+
vi.mocked(client.checkEndpointHealth).mockResolvedValue({
|
|
53
|
+
healthy: true,
|
|
54
|
+
statusCode: 200,
|
|
55
|
+
message: "OK",
|
|
56
|
+
latencyMs: 50,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
vi.mocked(writer.writeConfig).mockResolvedValue(
|
|
60
|
+
"/home/user/.claude/revenium.env",
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
vi.mocked(profileUpdater.updateShellProfile).mockResolvedValue({
|
|
64
|
+
success: true,
|
|
65
|
+
shellType: "bash",
|
|
66
|
+
profilePath: "/home/user/.bashrc",
|
|
67
|
+
message: "Added configuration to /home/user/.bashrc",
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
await setupCommand({});
|
|
71
|
+
|
|
72
|
+
expect(inquirer.prompt).toHaveBeenCalled();
|
|
73
|
+
expect(client.checkEndpointHealth).toHaveBeenCalledWith(
|
|
74
|
+
"https://api.revenium.ai",
|
|
75
|
+
"hak_test123",
|
|
76
|
+
);
|
|
77
|
+
expect(writer.writeConfig).toHaveBeenCalledWith(
|
|
78
|
+
expect.objectContaining({
|
|
79
|
+
apiKey: "hak_test123",
|
|
80
|
+
endpoint: "https://api.revenium.ai",
|
|
81
|
+
email: "test@example.com",
|
|
82
|
+
subscriptionTier: "pro",
|
|
83
|
+
}),
|
|
84
|
+
);
|
|
85
|
+
expect(profileUpdater.updateShellProfile).toHaveBeenCalled();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("should handle health check failure gracefully", async () => {
|
|
89
|
+
vi.mocked(inquirer.prompt).mockResolvedValue({
|
|
90
|
+
apiKey: "hak_invalid",
|
|
91
|
+
endpoint: "https://api.revenium.ai",
|
|
92
|
+
} as any);
|
|
93
|
+
|
|
94
|
+
vi.mocked(client.checkEndpointHealth).mockResolvedValue({
|
|
95
|
+
healthy: false,
|
|
96
|
+
statusCode: 401,
|
|
97
|
+
message: "Unauthorized",
|
|
98
|
+
latencyMs: 100,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
await setupCommand({});
|
|
102
|
+
|
|
103
|
+
expect(process.exit).toHaveBeenCalledWith(1);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("should skip shell update when option is set", async () => {
|
|
107
|
+
vi.mocked(inquirer.prompt).mockResolvedValue({
|
|
108
|
+
apiKey: "hak_test123",
|
|
109
|
+
endpoint: "https://api.revenium.ai",
|
|
110
|
+
} as any);
|
|
111
|
+
|
|
112
|
+
vi.mocked(client.checkEndpointHealth).mockResolvedValue({
|
|
113
|
+
healthy: true,
|
|
114
|
+
statusCode: 200,
|
|
115
|
+
message: "OK",
|
|
116
|
+
latencyMs: 50,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
vi.mocked(writer.writeConfig).mockResolvedValue(
|
|
120
|
+
"/home/user/.claude/revenium.env",
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
await setupCommand({ skipShellUpdate: true });
|
|
124
|
+
|
|
125
|
+
expect(profileUpdater.updateShellProfile).not.toHaveBeenCalled();
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
describe("backfill command end-to-end", () => {
|
|
130
|
+
it("should exit when config is not found", async () => {
|
|
131
|
+
vi.mocked(loader.loadConfig).mockResolvedValue(null);
|
|
132
|
+
vi.mocked(process.exit).mockImplementation((() => {
|
|
133
|
+
throw new Error("process.exit called");
|
|
134
|
+
}) as any);
|
|
135
|
+
|
|
136
|
+
await expect(backfillCommand({})).rejects.toThrow("process.exit called");
|
|
137
|
+
|
|
138
|
+
expect(console.log).toHaveBeenCalledWith(
|
|
139
|
+
expect.stringContaining("Configuration not found"),
|
|
140
|
+
);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("should handle dry-run mode", async () => {
|
|
144
|
+
vi.mocked(loader.loadConfig).mockResolvedValue({
|
|
145
|
+
apiKey: "hak_test123",
|
|
146
|
+
endpoint: "https://api.revenium.ai",
|
|
147
|
+
subscriptionTier: "pro",
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
vi.mocked(readdir).mockResolvedValue([] as any);
|
|
151
|
+
vi.mocked(existsSync).mockReturnValue(false);
|
|
152
|
+
|
|
153
|
+
await backfillCommand({ dryRun: true });
|
|
154
|
+
|
|
155
|
+
expect(client.sendOtlpLogs).not.toHaveBeenCalled();
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
});
|