@productbrain/cli 0.1.0-beta.29 → 0.1.0-beta.32
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/README.md +62 -178
- package/dist/__tests__/constants.test.d.ts +2 -0
- package/dist/__tests__/constants.test.d.ts.map +1 -0
- package/dist/__tests__/constants.test.js +94 -0
- package/dist/__tests__/constants.test.js.map +1 -0
- package/dist/__tests__/errors.test.d.ts +2 -0
- package/dist/__tests__/errors.test.d.ts.map +1 -0
- package/dist/__tests__/errors.test.js +117 -0
- package/dist/__tests__/errors.test.js.map +1 -0
- package/dist/__tests__/glossary.test.d.ts +2 -0
- package/dist/__tests__/glossary.test.d.ts.map +1 -0
- package/dist/__tests__/glossary.test.js +32 -0
- package/dist/__tests__/glossary.test.js.map +1 -0
- package/dist/__tests__/login.test.d.ts +2 -0
- package/dist/__tests__/login.test.d.ts.map +1 -0
- package/dist/__tests__/login.test.js +168 -0
- package/dist/__tests__/login.test.js.map +1 -0
- package/dist/__tests__/profiles.test.d.ts +2 -0
- package/dist/__tests__/profiles.test.d.ts.map +1 -0
- package/dist/__tests__/profiles.test.js +168 -0
- package/dist/__tests__/profiles.test.js.map +1 -0
- package/dist/__tests__/setup.test.d.ts +2 -0
- package/dist/__tests__/setup.test.d.ts.map +1 -0
- package/dist/__tests__/setup.test.js +170 -0
- package/dist/__tests__/setup.test.js.map +1 -0
- package/dist/commands/capture.d.ts.map +1 -1
- package/dist/commands/capture.js +23 -2
- package/dist/commands/capture.js.map +1 -1
- package/dist/commands/doctor.d.ts +18 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +211 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/doctor.test.d.ts +7 -0
- package/dist/commands/doctor.test.d.ts.map +1 -0
- package/dist/commands/doctor.test.js +265 -0
- package/dist/commands/doctor.test.js.map +1 -0
- package/dist/commands/login.d.ts +4 -0
- package/dist/commands/login.d.ts.map +1 -1
- package/dist/commands/login.js +53 -27
- package/dist/commands/login.js.map +1 -1
- package/dist/commands/profile.d.ts +24 -0
- package/dist/commands/profile.d.ts.map +1 -0
- package/dist/commands/profile.js +82 -0
- package/dist/commands/profile.js.map +1 -0
- package/dist/commands/promote.d.ts.map +1 -1
- package/dist/commands/promote.js +3 -2
- package/dist/commands/promote.js.map +1 -1
- package/dist/commands/setup.d.ts +16 -0
- package/dist/commands/setup.d.ts.map +1 -0
- package/dist/commands/setup.js +213 -0
- package/dist/commands/setup.js.map +1 -0
- package/dist/formatters/promote.d.ts +1 -0
- package/dist/formatters/promote.d.ts.map +1 -1
- package/dist/formatters/promote.js +1 -0
- package/dist/formatters/promote.js.map +1 -1
- package/dist/index.js +251 -284
- package/dist/index.js.map +1 -1
- package/dist/lib/activation.d.ts +28 -0
- package/dist/lib/activation.d.ts.map +1 -0
- package/dist/lib/activation.js +57 -0
- package/dist/lib/activation.js.map +1 -0
- package/dist/lib/activation.test.d.ts +6 -0
- package/dist/lib/activation.test.d.ts.map +1 -0
- package/dist/lib/activation.test.js +121 -0
- package/dist/lib/activation.test.js.map +1 -0
- package/dist/lib/client.d.ts +19 -2
- package/dist/lib/client.d.ts.map +1 -1
- package/dist/lib/client.js +71 -11
- package/dist/lib/client.js.map +1 -1
- package/dist/lib/config.d.ts +9 -3
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +54 -15
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/constants.d.ts +21 -0
- package/dist/lib/constants.d.ts.map +1 -0
- package/dist/lib/constants.js +39 -0
- package/dist/lib/constants.js.map +1 -0
- package/dist/lib/errors.d.ts +57 -0
- package/dist/lib/errors.d.ts.map +1 -0
- package/dist/lib/errors.js +65 -0
- package/dist/lib/errors.js.map +1 -0
- package/dist/lib/glossary.d.ts +19 -0
- package/dist/lib/glossary.d.ts.map +1 -0
- package/dist/lib/glossary.js +53 -0
- package/dist/lib/glossary.js.map +1 -0
- package/dist/lib/profiles.d.ts +34 -0
- package/dist/lib/profiles.d.ts.map +1 -0
- package/dist/lib/profiles.js +173 -0
- package/dist/lib/profiles.js.map +1 -0
- package/dist/lib/runner.d.ts +2 -0
- package/dist/lib/runner.d.ts.map +1 -1
- package/dist/lib/runner.js +33 -4
- package/dist/lib/runner.js.map +1 -1
- package/dist/lib/style.d.ts +65 -0
- package/dist/lib/style.d.ts.map +1 -0
- package/dist/lib/style.js +108 -0
- package/dist/lib/style.js.map +1 -0
- package/dist/lib/style.test.d.ts +7 -0
- package/dist/lib/style.test.d.ts.map +1 -0
- package/dist/lib/style.test.js +195 -0
- package/dist/lib/style.test.js.map +1 -0
- package/dist/lib/telemetry.d.ts +15 -0
- package/dist/lib/telemetry.d.ts.map +1 -0
- package/dist/lib/telemetry.js +29 -0
- package/dist/lib/telemetry.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
// ---------------------------------------------------------------------------
|
|
3
|
+
// Mocks
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
const validateKeyMock = vi.fn();
|
|
6
|
+
vi.mock('../lib/client.js', () => ({
|
|
7
|
+
validateKey: (...args) => validateKeyMock(...args),
|
|
8
|
+
}));
|
|
9
|
+
vi.mock('../lib/config.js', () => ({
|
|
10
|
+
HOME_CONFIG_DIR: '/tmp/test-pb-config',
|
|
11
|
+
HOME_ENV_PATH: '/tmp/test-pb-config/.env',
|
|
12
|
+
}));
|
|
13
|
+
const mkdirSyncMock = vi.fn();
|
|
14
|
+
const writeFileSyncMock = vi.fn();
|
|
15
|
+
vi.mock('fs', () => ({
|
|
16
|
+
mkdirSync: (...args) => mkdirSyncMock(...args),
|
|
17
|
+
writeFileSync: (...args) => writeFileSyncMock(...args),
|
|
18
|
+
existsSync: vi.fn(() => false),
|
|
19
|
+
readFileSync: vi.fn(() => ''),
|
|
20
|
+
}));
|
|
21
|
+
// Mock readline — simulate user pasting a key
|
|
22
|
+
let readlineAnswer = '';
|
|
23
|
+
vi.mock('readline', () => ({
|
|
24
|
+
createInterface: () => ({
|
|
25
|
+
question: (_prompt, cb) => cb(readlineAnswer),
|
|
26
|
+
close: vi.fn(),
|
|
27
|
+
}),
|
|
28
|
+
}));
|
|
29
|
+
// Capture console/process output
|
|
30
|
+
const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
|
|
31
|
+
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => { });
|
|
32
|
+
const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => { });
|
|
33
|
+
const stdoutWriteSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
34
|
+
// Track process.exit calls without throwing (so try/catch in login.ts doesn't catch it)
|
|
35
|
+
let exitCode;
|
|
36
|
+
vi.spyOn(process, 'exit').mockImplementation((code) => {
|
|
37
|
+
exitCode = code;
|
|
38
|
+
// Return undefined as never — the function won't actually continue
|
|
39
|
+
// but the test can check exitCode after the call
|
|
40
|
+
return undefined;
|
|
41
|
+
});
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
// Tests
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
describe('pb login — key validation', () => {
|
|
46
|
+
beforeEach(() => {
|
|
47
|
+
vi.clearAllMocks();
|
|
48
|
+
readlineAnswer = '';
|
|
49
|
+
exitCode = undefined;
|
|
50
|
+
delete process.env.CONVEX_SITE_URL;
|
|
51
|
+
});
|
|
52
|
+
it('saves key on successful validation', async () => {
|
|
53
|
+
readlineAnswer = 'pb_sk_valid_key_123';
|
|
54
|
+
validateKeyMock.mockResolvedValue({ valid: true, workspaceId: 'ws-123' });
|
|
55
|
+
const { runLogin } = await import('../commands/login.js');
|
|
56
|
+
await runLogin();
|
|
57
|
+
expect(validateKeyMock).toHaveBeenCalledWith('pb_sk_valid_key_123', 'https://gateway.productbrain.io');
|
|
58
|
+
expect(writeFileSyncMock).toHaveBeenCalledTimes(1);
|
|
59
|
+
expect(writeFileSyncMock.mock.calls[0][1]).toContain('PRODUCTBRAIN_API_KEY=pb_sk_valid_key_123');
|
|
60
|
+
expect(stdoutWriteSpy).toHaveBeenCalledWith(' connected.\n\n');
|
|
61
|
+
expect(exitCode).toBeUndefined();
|
|
62
|
+
});
|
|
63
|
+
it('rejects invalid key (401) without saving', async () => {
|
|
64
|
+
readlineAnswer = 'pb_sk_bad_key';
|
|
65
|
+
validateKeyMock.mockResolvedValue({ valid: false, error: 'Invalid API key.' });
|
|
66
|
+
const { runLogin } = await import('../commands/login.js');
|
|
67
|
+
await runLogin();
|
|
68
|
+
expect(writeFileSyncMock).not.toHaveBeenCalled();
|
|
69
|
+
expect(exitCode).toBe(1);
|
|
70
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining('Invalid key'));
|
|
71
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining('work.productbrain.io'));
|
|
72
|
+
});
|
|
73
|
+
it('saves key on network timeout with warning', async () => {
|
|
74
|
+
readlineAnswer = 'pb_sk_offline_key';
|
|
75
|
+
validateKeyMock.mockRejectedValue(new Error('fetch failed'));
|
|
76
|
+
const { runLogin } = await import('../commands/login.js');
|
|
77
|
+
await runLogin();
|
|
78
|
+
expect(writeFileSyncMock).toHaveBeenCalledTimes(1);
|
|
79
|
+
expect(writeFileSyncMock.mock.calls[0][1]).toContain('PRODUCTBRAIN_API_KEY=pb_sk_offline_key');
|
|
80
|
+
expect(consoleWarnSpy).toHaveBeenCalledWith(expect.stringContaining('Could not reach server'));
|
|
81
|
+
expect(exitCode).toBeUndefined();
|
|
82
|
+
});
|
|
83
|
+
it('exits on empty key', async () => {
|
|
84
|
+
readlineAnswer = '';
|
|
85
|
+
const { runLogin } = await import('../commands/login.js');
|
|
86
|
+
await runLogin();
|
|
87
|
+
expect(exitCode).toBe(1);
|
|
88
|
+
expect(validateKeyMock).not.toHaveBeenCalled();
|
|
89
|
+
expect(writeFileSyncMock).not.toHaveBeenCalled();
|
|
90
|
+
});
|
|
91
|
+
it('exits on key without pb_sk_ prefix', async () => {
|
|
92
|
+
readlineAnswer = 'sk_not_a_pb_key';
|
|
93
|
+
const { runLogin } = await import('../commands/login.js');
|
|
94
|
+
await runLogin();
|
|
95
|
+
expect(exitCode).toBe(1);
|
|
96
|
+
expect(validateKeyMock).not.toHaveBeenCalled();
|
|
97
|
+
expect(writeFileSyncMock).not.toHaveBeenCalled();
|
|
98
|
+
});
|
|
99
|
+
it('shows signup URL in error message', async () => {
|
|
100
|
+
readlineAnswer = 'pb_sk_bad';
|
|
101
|
+
validateKeyMock.mockResolvedValue({ valid: false, error: 'Invalid API key.' });
|
|
102
|
+
const { runLogin } = await import('../commands/login.js');
|
|
103
|
+
await runLogin();
|
|
104
|
+
// Signup URL should appear in the error output
|
|
105
|
+
const allErrorCalls = consoleErrorSpy.mock.calls.flat().join(' ');
|
|
106
|
+
expect(allErrorCalls).toContain('work.productbrain.io');
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
describe('validateKey()', () => {
|
|
110
|
+
const originalFetch = globalThis.fetch;
|
|
111
|
+
beforeEach(() => {
|
|
112
|
+
vi.clearAllMocks();
|
|
113
|
+
});
|
|
114
|
+
afterEach(() => {
|
|
115
|
+
globalThis.fetch = originalFetch;
|
|
116
|
+
});
|
|
117
|
+
it('returns valid on 200 with workspace data', async () => {
|
|
118
|
+
globalThis.fetch = vi.fn().mockResolvedValue({
|
|
119
|
+
ok: true,
|
|
120
|
+
status: 200,
|
|
121
|
+
json: () => Promise.resolve({ data: { _id: 'ws-abc' } }),
|
|
122
|
+
});
|
|
123
|
+
const mod = await vi.importActual('../lib/client.js');
|
|
124
|
+
const result = await mod.validateKey('pb_sk_test', 'https://test.convex.site');
|
|
125
|
+
expect(result).toEqual({ valid: true, workspaceId: 'ws-abc' });
|
|
126
|
+
});
|
|
127
|
+
it('returns invalid on 401', async () => {
|
|
128
|
+
globalThis.fetch = vi.fn().mockResolvedValue({
|
|
129
|
+
ok: false,
|
|
130
|
+
status: 401,
|
|
131
|
+
json: () => Promise.resolve({ error: 'Unauthorized' }),
|
|
132
|
+
});
|
|
133
|
+
const mod = await vi.importActual('../lib/client.js');
|
|
134
|
+
const result = await mod.validateKey('pb_sk_bad', 'https://test.convex.site');
|
|
135
|
+
expect(result).toEqual({ valid: false, error: 'Invalid API key.' });
|
|
136
|
+
});
|
|
137
|
+
it('returns invalid on 403', async () => {
|
|
138
|
+
globalThis.fetch = vi.fn().mockResolvedValue({
|
|
139
|
+
ok: false,
|
|
140
|
+
status: 403,
|
|
141
|
+
json: () => Promise.resolve({ error: 'Forbidden' }),
|
|
142
|
+
});
|
|
143
|
+
const mod = await vi.importActual('../lib/client.js');
|
|
144
|
+
const result = await mod.validateKey('pb_sk_bad', 'https://test.convex.site');
|
|
145
|
+
expect(result).toEqual({ valid: false, error: 'Invalid API key.' });
|
|
146
|
+
});
|
|
147
|
+
it('returns invalid on server error with message', async () => {
|
|
148
|
+
globalThis.fetch = vi.fn().mockResolvedValue({
|
|
149
|
+
ok: false,
|
|
150
|
+
status: 500,
|
|
151
|
+
json: () => Promise.resolve({ error: 'Internal server error' }),
|
|
152
|
+
});
|
|
153
|
+
const mod = await vi.importActual('../lib/client.js');
|
|
154
|
+
const result = await mod.validateKey('pb_sk_test', 'https://test.convex.site');
|
|
155
|
+
expect(result).toEqual({ valid: false, error: 'Internal server error' });
|
|
156
|
+
});
|
|
157
|
+
it('throws on network error (fetch failed)', async () => {
|
|
158
|
+
globalThis.fetch = vi.fn().mockRejectedValue(new Error('fetch failed'));
|
|
159
|
+
const mod = await vi.importActual('../lib/client.js');
|
|
160
|
+
await expect(mod.validateKey('pb_sk_test', 'https://test.convex.site')).rejects.toThrow('fetch failed');
|
|
161
|
+
});
|
|
162
|
+
it('throws on timeout (abort)', async () => {
|
|
163
|
+
globalThis.fetch = vi.fn().mockImplementation(() => new Promise((_, reject) => setTimeout(() => reject(new Error('AbortError')), 100)));
|
|
164
|
+
const mod = await vi.importActual('../lib/client.js');
|
|
165
|
+
await expect(mod.validateKey('pb_sk_test', 'https://test.convex.site', 50)).rejects.toThrow();
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
//# sourceMappingURL=login.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"login.test.js","sourceRoot":"","sources":["../../src/__tests__/login.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"profiles.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/profiles.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Profile system tests — WP-302 Slice 2.
|
|
3
|
+
* Tests: CRUD round-trip, auto-migration, resolution order,
|
|
4
|
+
* cannot-delete-active, session close on switch.
|
|
5
|
+
*/
|
|
6
|
+
import { beforeEach, afterEach, describe, expect, it, vi } from 'vitest';
|
|
7
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, rmSync } from 'fs';
|
|
8
|
+
import { resolve } from 'path';
|
|
9
|
+
// vi.hoisted runs before mock hoisting — safe to compute test paths here
|
|
10
|
+
const { TEST_HOME, TEST_CONFIG_DIR, TEST_PROFILES_DIR, TEST_ACTIVE_PATH, TEST_LEGACY_PATH } = vi.hoisted(() => {
|
|
11
|
+
const os = require('os');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
const home = path.resolve(os.tmpdir(), `pb-test-profiles-${process.pid}`);
|
|
14
|
+
return {
|
|
15
|
+
TEST_HOME: home,
|
|
16
|
+
TEST_CONFIG_DIR: path.resolve(home, '.config', 'productbrain'),
|
|
17
|
+
TEST_PROFILES_DIR: path.resolve(home, '.config', 'productbrain', 'profiles'),
|
|
18
|
+
TEST_ACTIVE_PATH: path.resolve(home, '.config', 'productbrain', 'active-profile'),
|
|
19
|
+
TEST_LEGACY_PATH: path.resolve(home, '.config', 'productbrain', '.env'),
|
|
20
|
+
};
|
|
21
|
+
});
|
|
22
|
+
vi.mock('os', async (importOriginal) => {
|
|
23
|
+
const actual = await importOriginal();
|
|
24
|
+
return { ...actual, homedir: () => TEST_HOME };
|
|
25
|
+
});
|
|
26
|
+
// Mock session to track close-on-switch
|
|
27
|
+
const mockClearSession = vi.fn();
|
|
28
|
+
let mockSessionState = null;
|
|
29
|
+
vi.mock('../lib/session.js', () => ({
|
|
30
|
+
readSession: () => mockSessionState,
|
|
31
|
+
writeSession: vi.fn(),
|
|
32
|
+
clearSession: mockClearSession,
|
|
33
|
+
addCapturedEntry: vi.fn(),
|
|
34
|
+
}));
|
|
35
|
+
// Import after mocks
|
|
36
|
+
import { listProfiles, getActiveProfile, createProfile, useProfile, deleteProfile, resolveProfileConfig, } from '../lib/profiles.js';
|
|
37
|
+
describe('profiles', () => {
|
|
38
|
+
beforeEach(() => {
|
|
39
|
+
// Clean slate
|
|
40
|
+
if (existsSync(TEST_CONFIG_DIR)) {
|
|
41
|
+
rmSync(TEST_CONFIG_DIR, { recursive: true, force: true });
|
|
42
|
+
}
|
|
43
|
+
mkdirSync(TEST_CONFIG_DIR, { recursive: true });
|
|
44
|
+
delete process.env.PB_PROFILE;
|
|
45
|
+
mockSessionState = null;
|
|
46
|
+
vi.clearAllMocks();
|
|
47
|
+
});
|
|
48
|
+
afterEach(() => {
|
|
49
|
+
if (existsSync(TEST_HOME)) {
|
|
50
|
+
rmSync(TEST_HOME, { recursive: true, force: true });
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
describe('CRUD round-trip', () => {
|
|
54
|
+
it('creates, lists, and deletes a profile', () => {
|
|
55
|
+
createProfile('staging', 'pb_sk_test_staging');
|
|
56
|
+
const profiles = listProfiles();
|
|
57
|
+
expect(profiles).toContain('staging');
|
|
58
|
+
// First profile becomes active
|
|
59
|
+
expect(getActiveProfile()).toBe('staging');
|
|
60
|
+
// Create a second profile so we can delete the first
|
|
61
|
+
createProfile('dev', 'pb_sk_test_dev');
|
|
62
|
+
useProfile('dev');
|
|
63
|
+
deleteProfile('staging');
|
|
64
|
+
expect(listProfiles()).not.toContain('staging');
|
|
65
|
+
});
|
|
66
|
+
it('rejects duplicate profile names', () => {
|
|
67
|
+
createProfile('myprofile', 'pb_sk_test');
|
|
68
|
+
expect(() => createProfile('myprofile', 'pb_sk_test2')).toThrow(/already exists/);
|
|
69
|
+
});
|
|
70
|
+
it('rejects invalid profile names', () => {
|
|
71
|
+
expect(() => createProfile('My Profile!', 'pb_sk_test')).toThrow(/Invalid profile name/);
|
|
72
|
+
expect(() => createProfile('', 'pb_sk_test')).toThrow(/Invalid profile name/);
|
|
73
|
+
expect(() => createProfile('-bad', 'pb_sk_test')).toThrow(/Invalid profile name/);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
describe('auto-migration from legacy .env', () => {
|
|
77
|
+
it('migrates legacy .env to default profile on first resolveProfileConfig()', () => {
|
|
78
|
+
// Set up legacy .env (no profiles dir yet)
|
|
79
|
+
writeFileSync(TEST_LEGACY_PATH, 'PRODUCTBRAIN_API_KEY=pb_sk_legacy_key\nCONVEX_SITE_URL=https://test.convex.site\n');
|
|
80
|
+
const config = resolveProfileConfig();
|
|
81
|
+
expect(config).not.toBeNull();
|
|
82
|
+
expect(config.apiKey).toBe('pb_sk_legacy_key');
|
|
83
|
+
// Should have created the default profile
|
|
84
|
+
expect(existsSync(resolve(TEST_PROFILES_DIR, 'default.env'))).toBe(true);
|
|
85
|
+
expect(readFileSync(TEST_ACTIVE_PATH, 'utf8').trim()).toBe('default');
|
|
86
|
+
});
|
|
87
|
+
it('does not re-migrate if profiles dir already exists', () => {
|
|
88
|
+
mkdirSync(TEST_PROFILES_DIR, { recursive: true });
|
|
89
|
+
writeFileSync(TEST_LEGACY_PATH, 'PRODUCTBRAIN_API_KEY=pb_sk_legacy_key\n');
|
|
90
|
+
// Profiles dir exists, so no migration
|
|
91
|
+
const config = resolveProfileConfig();
|
|
92
|
+
// Falls through to legacy .env directly
|
|
93
|
+
expect(config).not.toBeNull();
|
|
94
|
+
expect(config.apiKey).toBe('pb_sk_legacy_key');
|
|
95
|
+
// Should NOT have created a default profile
|
|
96
|
+
expect(existsSync(resolve(TEST_PROFILES_DIR, 'default.env'))).toBe(false);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
describe('resolution order', () => {
|
|
100
|
+
it('PB_PROFILE env var wins over active-profile file', () => {
|
|
101
|
+
createProfile('prod', 'pb_sk_prod_key');
|
|
102
|
+
createProfile('staging', 'pb_sk_staging_key');
|
|
103
|
+
useProfile('prod');
|
|
104
|
+
process.env.PB_PROFILE = 'staging';
|
|
105
|
+
const config = resolveProfileConfig();
|
|
106
|
+
expect(config.apiKey).toBe('pb_sk_staging_key');
|
|
107
|
+
});
|
|
108
|
+
it('active-profile file wins over default', () => {
|
|
109
|
+
createProfile('default', 'pb_sk_default_key');
|
|
110
|
+
createProfile('custom', 'pb_sk_custom_key');
|
|
111
|
+
useProfile('custom');
|
|
112
|
+
const config = resolveProfileConfig();
|
|
113
|
+
expect(config.apiKey).toBe('pb_sk_custom_key');
|
|
114
|
+
});
|
|
115
|
+
it('falls back to default profile when no active-profile file', () => {
|
|
116
|
+
mkdirSync(TEST_PROFILES_DIR, { recursive: true });
|
|
117
|
+
writeFileSync(resolve(TEST_PROFILES_DIR, 'default.env'), 'PRODUCTBRAIN_API_KEY=pb_sk_fallback\n', { mode: 0o600 });
|
|
118
|
+
const config = resolveProfileConfig();
|
|
119
|
+
expect(config.apiKey).toBe('pb_sk_fallback');
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
describe('cannot delete active profile', () => {
|
|
123
|
+
it('throws PROFILE_IS_ACTIVE error', () => {
|
|
124
|
+
createProfile('active-one', 'pb_sk_test');
|
|
125
|
+
createProfile('other', 'pb_sk_test2');
|
|
126
|
+
useProfile('active-one');
|
|
127
|
+
expect(() => deleteProfile('active-one')).toThrow(/Cannot delete the active profile/);
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
describe('cannot delete last profile', () => {
|
|
131
|
+
it('throws PROFILE_IS_LAST error', () => {
|
|
132
|
+
createProfile('only-one', 'pb_sk_test');
|
|
133
|
+
createProfile('other', 'pb_sk_test2');
|
|
134
|
+
useProfile('other');
|
|
135
|
+
deleteProfile('only-one');
|
|
136
|
+
// Now 'other' is the last profile — PROFILE_IS_LAST fires before PROFILE_IS_ACTIVE
|
|
137
|
+
expect(() => deleteProfile('other')).toThrow(/Cannot delete the last profile/);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
describe('profile use', () => {
|
|
141
|
+
it('switches active profile', () => {
|
|
142
|
+
createProfile('first', 'pb_sk_first');
|
|
143
|
+
createProfile('second', 'pb_sk_second');
|
|
144
|
+
useProfile('second');
|
|
145
|
+
expect(getActiveProfile()).toBe('second');
|
|
146
|
+
useProfile('first');
|
|
147
|
+
expect(getActiveProfile()).toBe('first');
|
|
148
|
+
});
|
|
149
|
+
it('errors on non-existent profile', () => {
|
|
150
|
+
expect(() => useProfile('nonexistent')).toThrow(/does not exist/);
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
describe('profile file format', () => {
|
|
154
|
+
it('stores apiKey and optional siteUrl', () => {
|
|
155
|
+
createProfile('with-url', 'pb_sk_test', 'https://custom.convex.site');
|
|
156
|
+
const content = readFileSync(resolve(TEST_PROFILES_DIR, 'with-url.env'), 'utf8');
|
|
157
|
+
expect(content).toContain('PRODUCTBRAIN_API_KEY=pb_sk_test');
|
|
158
|
+
expect(content).toContain('CONVEX_SITE_URL=https://custom.convex.site');
|
|
159
|
+
});
|
|
160
|
+
it('omits siteUrl when not provided', () => {
|
|
161
|
+
createProfile('no-url', 'pb_sk_test');
|
|
162
|
+
const content = readFileSync(resolve(TEST_PROFILES_DIR, 'no-url.env'), 'utf8');
|
|
163
|
+
expect(content).toContain('PRODUCTBRAIN_API_KEY=pb_sk_test');
|
|
164
|
+
expect(content).not.toContain('CONVEX_SITE_URL');
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
//# sourceMappingURL=profiles.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"profiles.test.js","sourceRoot":"","sources":["../../src/__tests__/profiles.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"setup.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/setup.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pb setup — guided onboarding tests.
|
|
3
|
+
* WP-301 Slice 3. Verifies:
|
|
4
|
+
* - setup with valid existing config skips login
|
|
5
|
+
* - setup tracks telemetry events in order
|
|
6
|
+
* - setup with no config triggers login prompt
|
|
7
|
+
* - first capture flow starts session and creates entry
|
|
8
|
+
*/
|
|
9
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
10
|
+
// Track telemetry calls in order
|
|
11
|
+
const telemetryEvents = [];
|
|
12
|
+
vi.mock('../lib/telemetry.js', () => ({
|
|
13
|
+
trackEvent: (event) => {
|
|
14
|
+
telemetryEvents.push(event);
|
|
15
|
+
},
|
|
16
|
+
}));
|
|
17
|
+
const mcpCallMock = vi.fn();
|
|
18
|
+
const mcpCallWithSessionMock = vi.fn();
|
|
19
|
+
vi.mock('../lib/client.js', () => ({
|
|
20
|
+
mcpCall: (...args) => mcpCallMock(...args),
|
|
21
|
+
mcpCallWithSession: (...args) => mcpCallWithSessionMock(...args),
|
|
22
|
+
}));
|
|
23
|
+
let mockConfig = {
|
|
24
|
+
apiKey: 'pb_sk_test_key_1234',
|
|
25
|
+
siteUrl: 'https://test.convex.site',
|
|
26
|
+
};
|
|
27
|
+
vi.mock('../lib/config.js', () => ({
|
|
28
|
+
getConfig: () => {
|
|
29
|
+
if (!mockConfig)
|
|
30
|
+
throw new Error('No API key.');
|
|
31
|
+
return mockConfig;
|
|
32
|
+
},
|
|
33
|
+
getConfigOrGuide: vi.fn(() => mockConfig
|
|
34
|
+
? Promise.resolve(mockConfig)
|
|
35
|
+
: Promise.reject(new Error('No API key.'))),
|
|
36
|
+
HOME_CONFIG_DIR: '/tmp/test-config',
|
|
37
|
+
HOME_ENV_PATH: '/tmp/test-config/.env',
|
|
38
|
+
}));
|
|
39
|
+
let mockSession = null;
|
|
40
|
+
vi.mock('../lib/session.js', () => ({
|
|
41
|
+
readSession: () => mockSession,
|
|
42
|
+
writeSession: vi.fn(),
|
|
43
|
+
clearSession: vi.fn(),
|
|
44
|
+
addCapturedEntry: vi.fn(),
|
|
45
|
+
}));
|
|
46
|
+
const runLoginMock = vi.fn();
|
|
47
|
+
vi.mock('../commands/login.js', () => ({
|
|
48
|
+
runLogin: () => runLoginMock(),
|
|
49
|
+
}));
|
|
50
|
+
// Mock readline — supply answers in sequence
|
|
51
|
+
let readlineAnswers = [];
|
|
52
|
+
let answerIndex = 0;
|
|
53
|
+
vi.mock('readline', () => ({
|
|
54
|
+
createInterface: () => ({
|
|
55
|
+
question: (_prompt, cb) => {
|
|
56
|
+
const answer = readlineAnswers[answerIndex] ?? '';
|
|
57
|
+
answerIndex++;
|
|
58
|
+
cb(answer);
|
|
59
|
+
},
|
|
60
|
+
close: vi.fn(),
|
|
61
|
+
}),
|
|
62
|
+
}));
|
|
63
|
+
import { runSetup } from '../commands/setup.js';
|
|
64
|
+
describe('runSetup', () => {
|
|
65
|
+
beforeEach(() => {
|
|
66
|
+
vi.clearAllMocks();
|
|
67
|
+
telemetryEvents.length = 0;
|
|
68
|
+
readlineAnswers = [];
|
|
69
|
+
answerIndex = 0;
|
|
70
|
+
mockConfig = { apiKey: 'pb_sk_test_key_1234', siteUrl: 'https://test.convex.site' };
|
|
71
|
+
mockSession = null;
|
|
72
|
+
});
|
|
73
|
+
it('skips login when valid config exists and user declines capture', async () => {
|
|
74
|
+
// User answers: "n" to "Skip to first capture?"
|
|
75
|
+
readlineAnswers = ['n'];
|
|
76
|
+
mcpCallMock.mockResolvedValueOnce({ _id: 'ws-1', keyId: 'key-1', name: 'Test Workspace' }); // resolveWorkspace
|
|
77
|
+
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
|
|
78
|
+
await runSetup();
|
|
79
|
+
// Should NOT call runLogin
|
|
80
|
+
expect(runLoginMock).not.toHaveBeenCalled();
|
|
81
|
+
// Should show masked key
|
|
82
|
+
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('pb_sk_'));
|
|
83
|
+
// Should show workspace name
|
|
84
|
+
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('Test Workspace'));
|
|
85
|
+
logSpy.mockRestore();
|
|
86
|
+
});
|
|
87
|
+
it('tracks telemetry events in correct order when user completes full flow with existing config', async () => {
|
|
88
|
+
// User answers: "y" to skip to capture, "y" to capture, then capture text
|
|
89
|
+
readlineAnswers = ['y', 'y', 'My first insight'];
|
|
90
|
+
mcpCallMock
|
|
91
|
+
.mockResolvedValueOnce({ _id: 'ws-1', keyId: 'key-1', name: 'Test Workspace' }) // resolveWorkspace (step 1)
|
|
92
|
+
.mockResolvedValueOnce({ _id: 'ws-1', keyId: 'key-1', name: 'Test Workspace' }) // resolveWorkspace (capture)
|
|
93
|
+
.mockResolvedValueOnce({ sessionId: 'sess-1', workspaceName: 'Test Workspace', initiatedBy: 'cli', toolsScope: 'readwrite' }); // startSession
|
|
94
|
+
mcpCallWithSessionMock
|
|
95
|
+
.mockResolvedValueOnce({ docId: 'doc-1', entryId: 'INS-1' }); // createEntry
|
|
96
|
+
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
|
|
97
|
+
await runSetup();
|
|
98
|
+
expect(telemetryEvents).toEqual([
|
|
99
|
+
'setup_started',
|
|
100
|
+
'workspace_bound',
|
|
101
|
+
'first_capture_prompted',
|
|
102
|
+
'setup_completed',
|
|
103
|
+
]);
|
|
104
|
+
logSpy.mockRestore();
|
|
105
|
+
});
|
|
106
|
+
it('triggers login prompt when no valid config exists', async () => {
|
|
107
|
+
mockConfig = null;
|
|
108
|
+
// User answers: "n" to "Do you have an account?"
|
|
109
|
+
readlineAnswers = ['n'];
|
|
110
|
+
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
|
|
111
|
+
await runSetup();
|
|
112
|
+
// Should show signup guidance
|
|
113
|
+
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('productbrain.io'));
|
|
114
|
+
// Should NOT call runLogin since user said no account
|
|
115
|
+
expect(runLoginMock).not.toHaveBeenCalled();
|
|
116
|
+
logSpy.mockRestore();
|
|
117
|
+
});
|
|
118
|
+
it('calls runLogin when user has account but no config', async () => {
|
|
119
|
+
mockConfig = null;
|
|
120
|
+
// User answers: "y" to "have account?", then login runs,
|
|
121
|
+
// then "n" to capture prompt (via post-login rl)
|
|
122
|
+
readlineAnswers = ['y', 'n'];
|
|
123
|
+
// After login, config becomes valid
|
|
124
|
+
runLoginMock.mockImplementation(() => {
|
|
125
|
+
mockConfig = { apiKey: 'pb_sk_new_key', siteUrl: 'https://test.convex.site' };
|
|
126
|
+
});
|
|
127
|
+
mcpCallMock.mockResolvedValueOnce({ _id: 'ws-1', keyId: 'key-1', name: 'Test Workspace' }); // resolveWorkspace
|
|
128
|
+
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
|
|
129
|
+
await runSetup();
|
|
130
|
+
expect(runLoginMock).toHaveBeenCalledOnce();
|
|
131
|
+
expect(telemetryEvents).toContain('setup_started');
|
|
132
|
+
expect(telemetryEvents).toContain('key_validated');
|
|
133
|
+
logSpy.mockRestore();
|
|
134
|
+
});
|
|
135
|
+
it('handles first capture with session creation', async () => {
|
|
136
|
+
// Already configured, user wants to capture
|
|
137
|
+
readlineAnswers = ['y', 'y', 'DEC: Use trunk-based dev'];
|
|
138
|
+
mcpCallMock
|
|
139
|
+
.mockResolvedValueOnce({ _id: 'ws-1', keyId: 'key-1', name: 'Test Workspace' }) // resolveWorkspace (verify)
|
|
140
|
+
.mockResolvedValueOnce({ _id: 'ws-1', keyId: 'key-1', name: 'Test Workspace' }) // resolveWorkspace (capture)
|
|
141
|
+
.mockResolvedValueOnce({ sessionId: 'sess-setup', workspaceName: 'Test Workspace', initiatedBy: 'cli', toolsScope: 'readwrite' }); // startSession
|
|
142
|
+
mcpCallWithSessionMock
|
|
143
|
+
.mockResolvedValueOnce({ docId: 'doc-1', entryId: 'DEC-100' }); // createEntry
|
|
144
|
+
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
|
|
145
|
+
await runSetup();
|
|
146
|
+
// Should have called createEntry with the capture text
|
|
147
|
+
expect(mcpCallWithSessionMock).toHaveBeenCalledWith('chain.createEntry', expect.objectContaining({
|
|
148
|
+
name: 'DEC: Use trunk-based dev',
|
|
149
|
+
collectionSlug: 'insights',
|
|
150
|
+
}));
|
|
151
|
+
// Should show the entry ID
|
|
152
|
+
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('DEC-100'));
|
|
153
|
+
logSpy.mockRestore();
|
|
154
|
+
});
|
|
155
|
+
it('skips capture gracefully when user enters empty text', async () => {
|
|
156
|
+
readlineAnswers = ['y', 'y', ''];
|
|
157
|
+
mcpCallMock
|
|
158
|
+
.mockResolvedValueOnce({ _id: 'ws-1', keyId: 'key-1', name: 'Test Workspace' }) // resolveWorkspace (verify)
|
|
159
|
+
.mockResolvedValueOnce({ _id: 'ws-1', keyId: 'key-1', name: 'Test Workspace' }) // resolveWorkspace (capture)
|
|
160
|
+
.mockResolvedValueOnce({ sessionId: 'sess-setup', workspaceName: 'Test Workspace', initiatedBy: 'cli', toolsScope: 'readwrite' }); // startSession
|
|
161
|
+
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
|
|
162
|
+
await runSetup();
|
|
163
|
+
// Should NOT call createEntry
|
|
164
|
+
expect(mcpCallWithSessionMock).not.toHaveBeenCalled();
|
|
165
|
+
// Should still complete setup
|
|
166
|
+
expect(telemetryEvents).toContain('setup_completed');
|
|
167
|
+
logSpy.mockRestore();
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
//# sourceMappingURL=setup.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"setup.test.js","sourceRoot":"","sources":["../../src/__tests__/setup.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"capture.d.ts","sourceRoot":"","sources":["../../src/commands/capture.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;
|
|
1
|
+
{"version":3,"file":"capture.d.ts","sourceRoot":"","sources":["../../src/commands/capture.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAkDH,UAAU,cAAc;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,uEAAuE;IACvE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,oDAAoD;IACpD,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,wFAAwF;IACxF,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AA4ED,wBAAsB,UAAU,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAqJvE"}
|
package/dist/commands/capture.js
CHANGED
|
@@ -11,6 +11,8 @@ import { mcpCallWithSession } from '../lib/client.js';
|
|
|
11
11
|
import { readSession, addCapturedEntry } from '../lib/session.js';
|
|
12
12
|
import { isJsonMode } from '../lib/runner.js';
|
|
13
13
|
import { formatCaptureReceipt } from '../formatters/capture.js';
|
|
14
|
+
import { checkActivation } from '../lib/activation.js';
|
|
15
|
+
import { progress } from '../lib/style.js';
|
|
14
16
|
/**
|
|
15
17
|
* Infer an ISO date (YYYY-MM-DD) from free-text values.
|
|
16
18
|
*
|
|
@@ -113,7 +115,7 @@ export async function runCapture(options) {
|
|
|
113
115
|
}
|
|
114
116
|
else {
|
|
115
117
|
if (!json)
|
|
116
|
-
|
|
118
|
+
progress('Classifying...');
|
|
117
119
|
classification = await mcpCallWithSession('chain.resolveCollection', {
|
|
118
120
|
entryName,
|
|
119
121
|
entryDescription,
|
|
@@ -126,7 +128,7 @@ export async function runCapture(options) {
|
|
|
126
128
|
}
|
|
127
129
|
// Phase 2: Create entry as draft
|
|
128
130
|
if (!json)
|
|
129
|
-
|
|
131
|
+
progress(`Capturing to ${collectionSlug}...`);
|
|
130
132
|
const data = { description: entryDescription };
|
|
131
133
|
// Only set date when we can infer a source date from the capture itself.
|
|
132
134
|
const datedCollections = ['tensions', 'decisions', 'insights', 'standards', 'business-rules'];
|
|
@@ -147,6 +149,25 @@ export async function runCapture(options) {
|
|
|
147
149
|
});
|
|
148
150
|
// Track in session state
|
|
149
151
|
addCapturedEntry(result.entryId);
|
|
152
|
+
// WP-301 S4: Activation ceremony — show contextual retrieval after first capture
|
|
153
|
+
if (!json) {
|
|
154
|
+
try {
|
|
155
|
+
const activation = await checkActivation(result.entryId);
|
|
156
|
+
if (activation?.isFirst && activation.relatedEntries.length > 0) {
|
|
157
|
+
process.stdout.write('\n Your first capture is on the Chain. Here\'s how it connects:\n');
|
|
158
|
+
for (const rel of activation.relatedEntries.slice(0, 3)) {
|
|
159
|
+
process.stdout.write(` \u2192 ${rel.entryId}: ${rel.name} (${rel.relation})\n`);
|
|
160
|
+
}
|
|
161
|
+
process.stdout.write('\n');
|
|
162
|
+
}
|
|
163
|
+
else if (activation?.isFirst) {
|
|
164
|
+
process.stdout.write('\n Your first capture is on the Chain! Run `pb orient -b` to see your workspace.\n\n');
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
// Activation is additive — never blocks capture
|
|
169
|
+
}
|
|
170
|
+
}
|
|
150
171
|
// BET-272 S3: Advisory quality hints — from createEntryWithClassification result (BR-144)
|
|
151
172
|
const qualityHints = result.qualityHints ?? [];
|
|
152
173
|
// Phase 3: Link to existing entry if --link provided (TEN-705)
|