@productbrain/cli 0.1.0-beta.71 → 0.1.0-beta.75
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/dist/__tests__/audit.test.js +44 -44
- package/dist/__tests__/capture.test.js +37 -37
- package/dist/__tests__/constellation.test.js +14 -14
- package/dist/__tests__/context-strategy.test.js +8 -8
- package/dist/__tests__/fields.test.js +20 -20
- package/dist/__tests__/ingest.test.js +28 -28
- package/dist/__tests__/orient.test.js +8 -8
- package/dist/__tests__/promote.test.js +15 -15
- package/dist/__tests__/proposals.test.js +18 -18
- package/dist/__tests__/relate.test.js +14 -14
- package/dist/__tests__/session-touch.test.js +11 -11
- package/dist/__tests__/session.test.js +2 -2
- package/dist/__tests__/setup.test.js +16 -30
- package/dist/__tests__/setup.test.js.map +1 -1
- package/dist/__tests__/state.test.d.ts +6 -0
- package/dist/__tests__/state.test.d.ts.map +1 -0
- package/dist/__tests__/state.test.js +97 -0
- package/dist/__tests__/state.test.js.map +1 -0
- package/dist/__tests__/update.test.js +21 -21
- package/dist/__tests__/workspace.test.js +20 -20
- package/dist/commands/accept.js +4 -4
- package/dist/commands/admin/cockpit.d.ts +4 -2
- package/dist/commands/admin/cockpit.d.ts.map +1 -1
- package/dist/commands/admin/cockpit.js +219 -10
- package/dist/commands/admin/cockpit.js.map +1 -1
- package/dist/commands/admin/index.d.ts.map +1 -1
- package/dist/commands/admin/index.js +2 -0
- package/dist/commands/admin/index.js.map +1 -1
- package/dist/commands/admin/inspect.d.ts +9 -0
- package/dist/commands/admin/inspect.d.ts.map +1 -1
- package/dist/commands/admin/inspect.js +19 -0
- package/dist/commands/admin/inspect.js.map +1 -1
- package/dist/commands/admin/inspect.test.js +20 -1
- package/dist/commands/admin/inspect.test.js.map +1 -1
- package/dist/commands/admin/manage.d.ts +8 -0
- package/dist/commands/admin/manage.d.ts.map +1 -0
- package/dist/commands/admin/manage.js +76 -0
- package/dist/commands/admin/manage.js.map +1 -0
- package/dist/commands/audit.js +4 -4
- package/dist/commands/brief.js +4 -4
- package/dist/commands/capture.js +6 -6
- package/dist/commands/chain-walk.js +2 -2
- package/dist/commands/changes.js +2 -2
- package/dist/commands/codex-prep.js +2 -2
- package/dist/commands/collections.js +5 -5
- package/dist/commands/connect-screens.d.ts +21 -0
- package/dist/commands/connect-screens.d.ts.map +1 -0
- package/dist/commands/connect-screens.js +79 -0
- package/dist/commands/connect-screens.js.map +1 -0
- package/dist/commands/constellation.js +2 -2
- package/dist/commands/context.js +2 -2
- package/dist/commands/cross-cut.js +2 -2
- package/dist/commands/doctor.js +3 -3
- package/dist/commands/doctor.test.js +1 -1
- package/dist/commands/fields.js +2 -2
- package/dist/commands/get.js +3 -3
- package/dist/commands/handshake.js +5 -5
- package/dist/commands/ingest.js +6 -6
- package/dist/commands/init.d.ts +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +1 -9
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/orient.js +2 -2
- package/dist/commands/promote.js +4 -4
- package/dist/commands/proposals.js +2 -2
- package/dist/commands/reject.js +2 -2
- package/dist/commands/relate.js +3 -3
- package/dist/commands/search.js +2 -2
- package/dist/commands/session.js +7 -7
- package/dist/commands/setup.d.ts +1 -1
- package/dist/commands/setup.d.ts.map +1 -1
- package/dist/commands/setup.js +10 -31
- package/dist/commands/setup.js.map +1 -1
- package/dist/commands/update.js +3 -3
- package/dist/commands/usage.d.ts +1 -1
- package/dist/commands/usage.js +4 -4
- package/dist/commands/verify.js +2 -2
- package/dist/commands/welcome.d.ts +21 -0
- package/dist/commands/welcome.d.ts.map +1 -0
- package/dist/commands/welcome.js +50 -0
- package/dist/commands/welcome.js.map +1 -0
- package/dist/commands/workspace.js +4 -4
- package/dist/generators/chain-rules.d.ts +3 -3
- package/dist/generators/chain-rules.js +3 -3
- package/dist/generators/chain-rules.test.js +2 -2
- package/dist/generators/portable-knowledge.js +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/activation.js +2 -2
- package/dist/lib/activation.test.js +3 -3
- package/dist/lib/client.d.ts +4 -4
- package/dist/lib/client.d.ts.map +1 -1
- package/dist/lib/client.js +8 -9
- package/dist/lib/client.js.map +1 -1
- package/dist/lib/onboarding-path-b.js +8 -8
- package/dist/lib/onboarding-shared.js +2 -2
- package/dist/lib/onboarding.js +4 -4
- package/dist/lib/state.d.ts +51 -0
- package/dist/lib/state.d.ts.map +1 -0
- package/dist/lib/state.js +90 -0
- package/dist/lib/state.js.map +1 -0
- package/dist/lib/workspace-probe.js +2 -2
- package/package.json +1 -1
|
@@ -3,16 +3,16 @@
|
|
|
3
3
|
* BET-181 Slice 3: compound view (entry + grouped relations).
|
|
4
4
|
*
|
|
5
5
|
* Verifies:
|
|
6
|
-
* 1. Mock
|
|
6
|
+
* 1. Mock kernelCall returns known constellation structure; command outputs entry + grouped relations
|
|
7
7
|
* 2. Edge: entry with no relations returns entry-only, empty relations object
|
|
8
8
|
* 3. Relation types in output derive from the server response, not hardcoded
|
|
9
9
|
*/
|
|
10
10
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
11
11
|
import { runConstellation } from '../commands/constellation.js';
|
|
12
12
|
import { setOutputMode } from '../lib/runner.js';
|
|
13
|
-
const
|
|
13
|
+
const kernelCallMock = vi.fn();
|
|
14
14
|
vi.mock('../lib/client.js', () => ({
|
|
15
|
-
|
|
15
|
+
kernelCall: (...args) => kernelCallMock(...args),
|
|
16
16
|
}));
|
|
17
17
|
vi.mock('../lib/config.js', () => ({
|
|
18
18
|
getConfigOrGuide: vi.fn((_fn) => Promise.resolve({ apiKey: 'pb_sk_test', siteUrl: 'https://test.convex.site' })),
|
|
@@ -95,12 +95,12 @@ afterEach(() => {
|
|
|
95
95
|
});
|
|
96
96
|
describe('runConstellation — JSON mode (non-TTY)', () => {
|
|
97
97
|
it('calls chain.getConstellation with the correct entryId', withTty(false, async () => {
|
|
98
|
-
|
|
98
|
+
kernelCallMock.mockResolvedValue(CONSTELLATION_FIXTURE);
|
|
99
99
|
await runConstellation({ entryId: 'BET-181' });
|
|
100
|
-
expect(
|
|
100
|
+
expect(kernelCallMock).toHaveBeenCalledWith('chain.getConstellation', { entryId: 'BET-181' });
|
|
101
101
|
}));
|
|
102
102
|
it('outputs entry and grouped relations as JSON in non-TTY mode', withTty(false, async () => {
|
|
103
|
-
|
|
103
|
+
kernelCallMock.mockResolvedValue(CONSTELLATION_FIXTURE);
|
|
104
104
|
await runConstellation({ entryId: 'BET-181' });
|
|
105
105
|
const parsed = JSON.parse(stdoutOutput);
|
|
106
106
|
expect(parsed.entry.entryId).toBe('BET-181');
|
|
@@ -114,7 +114,7 @@ describe('runConstellation — JSON mode (non-TTY)', () => {
|
|
|
114
114
|
...CONSTELLATION_FIXTURE,
|
|
115
115
|
entry: { ...CONSTELLATION_FIXTURE.entry, _id: 'sys-id', _creationTime: 999999 },
|
|
116
116
|
};
|
|
117
|
-
|
|
117
|
+
kernelCallMock.mockResolvedValue(fixtureWithInternals);
|
|
118
118
|
await runConstellation({ entryId: 'BET-181' });
|
|
119
119
|
const parsed = JSON.parse(stdoutOutput);
|
|
120
120
|
expect(parsed.entry).not.toHaveProperty('_id');
|
|
@@ -123,13 +123,13 @@ describe('runConstellation — JSON mode (non-TTY)', () => {
|
|
|
123
123
|
});
|
|
124
124
|
describe('runConstellation — pretty mode (TTY)', () => {
|
|
125
125
|
it('outputs human-readable grouped view in TTY mode', withTty(true, async () => {
|
|
126
|
-
|
|
126
|
+
kernelCallMock.mockResolvedValue(CONSTELLATION_FIXTURE);
|
|
127
127
|
await runConstellation({ entryId: 'BET-181' });
|
|
128
128
|
expect(stdoutOutput).toContain('BET-181');
|
|
129
129
|
expect(stdoutOutput).toContain('CLI Agent-Native Foundation');
|
|
130
130
|
}));
|
|
131
131
|
it('shows relation types grouped in TTY output', withTty(true, async () => {
|
|
132
|
-
|
|
132
|
+
kernelCallMock.mockResolvedValue(CONSTELLATION_FIXTURE);
|
|
133
133
|
await runConstellation({ entryId: 'BET-181' });
|
|
134
134
|
expect(stdoutOutput).toContain('part_of');
|
|
135
135
|
expect(stdoutOutput).toContain('informed_by');
|
|
@@ -137,7 +137,7 @@ describe('runConstellation — pretty mode (TTY)', () => {
|
|
|
137
137
|
expect(stdoutOutput).toContain('FEAT-100');
|
|
138
138
|
}));
|
|
139
139
|
it('shows direction arrows in TTY output', withTty(true, async () => {
|
|
140
|
-
|
|
140
|
+
kernelCallMock.mockResolvedValue(CONSTELLATION_FIXTURE);
|
|
141
141
|
await runConstellation({ entryId: 'BET-181' });
|
|
142
142
|
// outgoing → for part_of, incoming ← for informed_by
|
|
143
143
|
expect(stdoutOutput).toContain('→');
|
|
@@ -160,7 +160,7 @@ describe('runConstellation — edge case: entry with no relations', () => {
|
|
|
160
160
|
relationTypes: [],
|
|
161
161
|
},
|
|
162
162
|
};
|
|
163
|
-
|
|
163
|
+
kernelCallMock.mockResolvedValue(noRelationsFixture);
|
|
164
164
|
await runConstellation({ entryId: 'BET-999' });
|
|
165
165
|
const parsed = JSON.parse(stdoutOutput);
|
|
166
166
|
expect(parsed.entry.entryId).toBe('BET-999');
|
|
@@ -183,7 +183,7 @@ describe('runConstellation — edge case: entry with no relations', () => {
|
|
|
183
183
|
relationTypes: [],
|
|
184
184
|
},
|
|
185
185
|
};
|
|
186
|
-
|
|
186
|
+
kernelCallMock.mockResolvedValue(noRelationsFixture);
|
|
187
187
|
await runConstellation({ entryId: 'BET-999' });
|
|
188
188
|
expect(stdoutOutput).toContain('No relations');
|
|
189
189
|
}));
|
|
@@ -233,7 +233,7 @@ describe('runConstellation — relation types derive from server response', () =
|
|
|
233
233
|
relationTypes: ['related_to', 'governs', 'blocks'],
|
|
234
234
|
},
|
|
235
235
|
};
|
|
236
|
-
|
|
236
|
+
kernelCallMock.mockResolvedValue(customRelationFixture);
|
|
237
237
|
await runConstellation({ entryId: 'DEC-999' });
|
|
238
238
|
const parsed = JSON.parse(stdoutOutput);
|
|
239
239
|
// Verify the CLI passes through whatever the server returns
|
|
@@ -247,7 +247,7 @@ describe('runConstellation — relation types derive from server response', () =
|
|
|
247
247
|
});
|
|
248
248
|
describe('runConstellation — error handling', () => {
|
|
249
249
|
it('throws CLIError when entry is not found', withTty(false, async () => {
|
|
250
|
-
|
|
250
|
+
kernelCallMock.mockRejectedValue(new Error('Entry "NOTEXIST-9999" not found.'));
|
|
251
251
|
await expect(runConstellation({ entryId: 'NOTEXIST-9999' })).rejects.toThrow('NOTEXIST-9999');
|
|
252
252
|
}));
|
|
253
253
|
});
|
|
@@ -9,9 +9,9 @@
|
|
|
9
9
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
10
10
|
import { runContext } from '../commands/context.js';
|
|
11
11
|
import { setOutputMode } from '../lib/runner.js';
|
|
12
|
-
const
|
|
12
|
+
const kernelCallMock = vi.fn();
|
|
13
13
|
vi.mock('../lib/client.js', () => ({
|
|
14
|
-
|
|
14
|
+
kernelCall: (...args) => kernelCallMock(...args),
|
|
15
15
|
}));
|
|
16
16
|
vi.mock('../lib/config.js', () => ({
|
|
17
17
|
getConfigOrGuide: vi.fn(() => Promise.resolve({ apiKey: 'pb_sk_test', siteUrl: 'https://test.convex.site' })),
|
|
@@ -46,7 +46,7 @@ beforeEach(() => {
|
|
|
46
46
|
stderrOutput += String(chunk);
|
|
47
47
|
return true;
|
|
48
48
|
});
|
|
49
|
-
|
|
49
|
+
kernelCallMock.mockReset();
|
|
50
50
|
setOutputMode('pretty');
|
|
51
51
|
});
|
|
52
52
|
afterEach(() => {
|
|
@@ -56,21 +56,21 @@ afterEach(() => {
|
|
|
56
56
|
});
|
|
57
57
|
describe('runContext — entry-based gather (BET-205 S2 regression)', () => {
|
|
58
58
|
it('calls chain.gatherContext with entryId and maxHops only', async () => {
|
|
59
|
-
|
|
59
|
+
kernelCallMock.mockResolvedValue(GATHER_FIXTURE);
|
|
60
60
|
await runContext({ entryId: 'BET-205' });
|
|
61
|
-
expect(
|
|
61
|
+
expect(kernelCallMock).toHaveBeenCalledWith('chain.gatherContext', {
|
|
62
62
|
entryId: 'BET-205',
|
|
63
63
|
maxHops: 2,
|
|
64
64
|
});
|
|
65
65
|
});
|
|
66
66
|
it('does not pass strategy to chain.gatherContext', async () => {
|
|
67
|
-
|
|
67
|
+
kernelCallMock.mockResolvedValue(GATHER_FIXTURE);
|
|
68
68
|
await runContext({ entryId: 'BET-205' });
|
|
69
|
-
const callArgs =
|
|
69
|
+
const callArgs = kernelCallMock.mock.calls[0][1];
|
|
70
70
|
expect(callArgs).not.toHaveProperty('strategy');
|
|
71
71
|
});
|
|
72
72
|
it('renders context output correctly', async () => {
|
|
73
|
-
|
|
73
|
+
kernelCallMock.mockResolvedValue(GATHER_FIXTURE);
|
|
74
74
|
await runContext({ entryId: 'BET-205' });
|
|
75
75
|
expect(stdoutOutput).toContain('BET-205');
|
|
76
76
|
expect(stdoutOutput).toContain('Vector Seed Spike');
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* BET-181 Slice 2: Verifies field discovery via chain.getCollectionFields.
|
|
4
4
|
*
|
|
5
5
|
* Test contract:
|
|
6
|
-
* 1. Mock
|
|
6
|
+
* 1. Mock kernelCall returns known field array → command outputs all field properties
|
|
7
7
|
* 2. Edge: collection with no fields → returns empty array, not error
|
|
8
8
|
* 3. Error: invalid collection slug → actionable error message
|
|
9
9
|
*/
|
|
@@ -11,9 +11,9 @@ import { beforeEach, afterEach, describe, expect, it, vi } from 'vitest';
|
|
|
11
11
|
import { runFields } from '../commands/fields.js';
|
|
12
12
|
import { setOutputMode } from '../lib/runner.js';
|
|
13
13
|
import { CLIError } from '../lib/errors.js';
|
|
14
|
-
const
|
|
14
|
+
const kernelCallMock = vi.fn();
|
|
15
15
|
vi.mock('../lib/client.js', () => ({
|
|
16
|
-
|
|
16
|
+
kernelCall: (...args) => kernelCallMock(...args),
|
|
17
17
|
}));
|
|
18
18
|
vi.mock('../lib/config.js', () => ({
|
|
19
19
|
getConfigOrGuide: vi.fn(() => Promise.resolve({ apiKey: 'pb_sk_test', siteUrl: 'https://test.convex.site' })),
|
|
@@ -35,7 +35,7 @@ beforeEach(() => {
|
|
|
35
35
|
return true;
|
|
36
36
|
});
|
|
37
37
|
exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => { throw new Error('process.exit'); });
|
|
38
|
-
|
|
38
|
+
kernelCallMock.mockReset();
|
|
39
39
|
// Force TTY mode so we get human-readable output in tests
|
|
40
40
|
setOutputMode('pretty');
|
|
41
41
|
});
|
|
@@ -76,7 +76,7 @@ const SAMPLE_FIELDS = [
|
|
|
76
76
|
];
|
|
77
77
|
describe('runFields — TTY output with known field array', () => {
|
|
78
78
|
it('outputs collection name and slug header', async () => {
|
|
79
|
-
|
|
79
|
+
kernelCallMock.mockResolvedValueOnce({
|
|
80
80
|
slug: 'work-packages',
|
|
81
81
|
name: 'Work Packages',
|
|
82
82
|
fields: SAMPLE_FIELDS,
|
|
@@ -85,7 +85,7 @@ describe('runFields — TTY output with known field array', () => {
|
|
|
85
85
|
expect(stdoutOutput).toContain('Collection: Work Packages (work-packages)');
|
|
86
86
|
});
|
|
87
87
|
it('outputs all field keys', async () => {
|
|
88
|
-
|
|
88
|
+
kernelCallMock.mockResolvedValueOnce({
|
|
89
89
|
slug: 'work-packages',
|
|
90
90
|
name: 'Work Packages',
|
|
91
91
|
fields: SAMPLE_FIELDS,
|
|
@@ -96,7 +96,7 @@ describe('runFields — TTY output with known field array', () => {
|
|
|
96
96
|
expect(stdoutOutput).toContain('elements');
|
|
97
97
|
});
|
|
98
98
|
it('outputs field type for each field', async () => {
|
|
99
|
-
|
|
99
|
+
kernelCallMock.mockResolvedValueOnce({
|
|
100
100
|
slug: 'work-packages',
|
|
101
101
|
name: 'Work Packages',
|
|
102
102
|
fields: SAMPLE_FIELDS,
|
|
@@ -107,7 +107,7 @@ describe('runFields — TTY output with known field array', () => {
|
|
|
107
107
|
expect(stdoutOutput).toContain('rich-text');
|
|
108
108
|
});
|
|
109
109
|
it('outputs options for select fields', async () => {
|
|
110
|
-
|
|
110
|
+
kernelCallMock.mockResolvedValueOnce({
|
|
111
111
|
slug: 'work-packages',
|
|
112
112
|
name: 'Work Packages',
|
|
113
113
|
fields: SAMPLE_FIELDS,
|
|
@@ -116,7 +116,7 @@ describe('runFields — TTY output with known field array', () => {
|
|
|
116
116
|
expect(stdoutOutput).toContain('small, medium, large');
|
|
117
117
|
});
|
|
118
118
|
it('outputs helpText when present', async () => {
|
|
119
|
-
|
|
119
|
+
kernelCallMock.mockResolvedValueOnce({
|
|
120
120
|
slug: 'work-packages',
|
|
121
121
|
name: 'Work Packages',
|
|
122
122
|
fields: SAMPLE_FIELDS,
|
|
@@ -126,7 +126,7 @@ describe('runFields — TTY output with known field array', () => {
|
|
|
126
126
|
expect(stdoutOutput).toContain('What problem are we solving?');
|
|
127
127
|
});
|
|
128
128
|
it('outputs displayHint when present', async () => {
|
|
129
|
-
|
|
129
|
+
kernelCallMock.mockResolvedValueOnce({
|
|
130
130
|
slug: 'work-packages',
|
|
131
131
|
name: 'Work Packages',
|
|
132
132
|
fields: SAMPLE_FIELDS,
|
|
@@ -135,7 +135,7 @@ describe('runFields — TTY output with known field array', () => {
|
|
|
135
135
|
expect(stdoutOutput).toContain('badge');
|
|
136
136
|
});
|
|
137
137
|
it('outputs zone when present', async () => {
|
|
138
|
-
|
|
138
|
+
kernelCallMock.mockResolvedValueOnce({
|
|
139
139
|
slug: 'work-packages',
|
|
140
140
|
name: 'Work Packages',
|
|
141
141
|
fields: SAMPLE_FIELDS,
|
|
@@ -145,7 +145,7 @@ describe('runFields — TTY output with known field array', () => {
|
|
|
145
145
|
expect(stdoutOutput).toContain('body');
|
|
146
146
|
});
|
|
147
147
|
it('outputs semanticRole when present', async () => {
|
|
148
|
-
|
|
148
|
+
kernelCallMock.mockResolvedValueOnce({
|
|
149
149
|
slug: 'work-packages',
|
|
150
150
|
name: 'Work Packages',
|
|
151
151
|
fields: SAMPLE_FIELDS,
|
|
@@ -157,7 +157,7 @@ describe('runFields — TTY output with known field array', () => {
|
|
|
157
157
|
expect(stdoutOutput).toContain('role: elements');
|
|
158
158
|
});
|
|
159
159
|
it('marks required fields', async () => {
|
|
160
|
-
|
|
160
|
+
kernelCallMock.mockResolvedValueOnce({
|
|
161
161
|
slug: 'work-packages',
|
|
162
162
|
name: 'Work Packages',
|
|
163
163
|
fields: SAMPLE_FIELDS,
|
|
@@ -167,19 +167,19 @@ describe('runFields — TTY output with known field array', () => {
|
|
|
167
167
|
expect(stdoutOutput).toContain('appetite *');
|
|
168
168
|
});
|
|
169
169
|
it('calls chain.getCollectionFields with the correct slug', async () => {
|
|
170
|
-
|
|
170
|
+
kernelCallMock.mockResolvedValueOnce({
|
|
171
171
|
slug: 'decisions',
|
|
172
172
|
name: 'Decisions',
|
|
173
173
|
fields: [],
|
|
174
174
|
});
|
|
175
175
|
await runFields({ collectionSlug: 'decisions' });
|
|
176
|
-
expect(
|
|
176
|
+
expect(kernelCallMock).toHaveBeenCalledWith('chain.getCollectionFields', { slug: 'decisions' });
|
|
177
177
|
});
|
|
178
178
|
});
|
|
179
179
|
describe('runFields — JSON mode', () => {
|
|
180
180
|
it('outputs JSON with full field array when piped', async () => {
|
|
181
181
|
setOutputMode('json');
|
|
182
|
-
|
|
182
|
+
kernelCallMock.mockResolvedValueOnce({
|
|
183
183
|
slug: 'work-packages',
|
|
184
184
|
name: 'Work Packages',
|
|
185
185
|
fields: SAMPLE_FIELDS,
|
|
@@ -202,7 +202,7 @@ describe('runFields — JSON mode', () => {
|
|
|
202
202
|
});
|
|
203
203
|
describe('runFields — edge: empty fields array', () => {
|
|
204
204
|
it('returns "No fields defined" message, does not error', async () => {
|
|
205
|
-
|
|
205
|
+
kernelCallMock.mockResolvedValueOnce({
|
|
206
206
|
slug: 'notes',
|
|
207
207
|
name: 'Notes',
|
|
208
208
|
fields: [],
|
|
@@ -215,21 +215,21 @@ describe('runFields — edge: empty fields array', () => {
|
|
|
215
215
|
});
|
|
216
216
|
describe('runFields — error: invalid collection slug', () => {
|
|
217
217
|
it('throws CLIError with actionable message for invalid slug', async () => {
|
|
218
|
-
|
|
218
|
+
kernelCallMock.mockResolvedValueOnce(null);
|
|
219
219
|
const err = await runFields({ collectionSlug: 'nonexistent-slug' }).catch((e) => e);
|
|
220
220
|
expect(err).toBeInstanceOf(CLIError);
|
|
221
221
|
expect(err.message).toContain('nonexistent-slug');
|
|
222
222
|
expect(err.message).toContain('not found');
|
|
223
223
|
});
|
|
224
224
|
it('includes suggestion to use a valid slug in guidance', async () => {
|
|
225
|
-
|
|
225
|
+
kernelCallMock.mockResolvedValueOnce(null);
|
|
226
226
|
const err = await runFields({ collectionSlug: 'bad-slug' }).catch((e) => e);
|
|
227
227
|
expect(err).toBeInstanceOf(CLIError);
|
|
228
228
|
expect(err.guidance).toContain('valid collection slug');
|
|
229
229
|
});
|
|
230
230
|
it('throws CLIError in JSON mode for invalid slug', async () => {
|
|
231
231
|
setOutputMode('json');
|
|
232
|
-
|
|
232
|
+
kernelCallMock.mockResolvedValueOnce(null);
|
|
233
233
|
const err = await runFields({ collectionSlug: 'bad-slug' }).catch((e) => e);
|
|
234
234
|
expect(err).toBeInstanceOf(CLIError);
|
|
235
235
|
expect(err.message).toContain('bad-slug');
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* CLI ingest command — verify
|
|
2
|
+
* CLI ingest command — verify kernelCall sequence and args.
|
|
3
3
|
* BET-81 Slice 1: chain.ingestDocument called with correct args, batch size.
|
|
4
4
|
* BET-221 S7 (FEAT-697): --dry-run, --resume, --concurrency flags.
|
|
5
5
|
*/
|
|
@@ -8,9 +8,9 @@ import { mkdtempSync, writeFileSync, rmSync } from 'fs';
|
|
|
8
8
|
import { join } from 'path';
|
|
9
9
|
import { tmpdir } from 'os';
|
|
10
10
|
import { runIngest } from '../commands/ingest.js';
|
|
11
|
-
const
|
|
11
|
+
const kernelCallMock = vi.fn();
|
|
12
12
|
vi.mock('../lib/client.js', () => ({
|
|
13
|
-
|
|
13
|
+
kernelCall: (...args) => kernelCallMock(...args),
|
|
14
14
|
}));
|
|
15
15
|
vi.mock('../lib/config.js', () => ({
|
|
16
16
|
getConfigOrGuide: vi.fn((retry) => Promise.resolve({ apiKey: 'pb_sk_test', siteUrl: 'https://test.convex.site' })),
|
|
@@ -20,7 +20,7 @@ describe('runIngest', () => {
|
|
|
20
20
|
beforeEach(() => {
|
|
21
21
|
vi.clearAllMocks();
|
|
22
22
|
tmpDir = mkdtempSync(join(tmpdir(), 'pb-ingest-'));
|
|
23
|
-
|
|
23
|
+
kernelCallMock
|
|
24
24
|
.mockResolvedValueOnce({ _id: 'ws-1', keyId: 'key-1' }) // resolveWorkspace
|
|
25
25
|
.mockResolvedValueOnce({ sessionId: 'sess-1', toolsScope: 'readwrite', workspaceName: 'Test' }) // startSession
|
|
26
26
|
.mockResolvedValue({ entriesCreated: ['e1', 'e2'], skipped: 0 }); // ingestDocument, closeSession
|
|
@@ -32,15 +32,15 @@ describe('runIngest', () => {
|
|
|
32
32
|
const f = join(tmpDir, 'doc.md');
|
|
33
33
|
writeFileSync(f, '# Test\n\nContent here.');
|
|
34
34
|
await runIngest({ pattern: f });
|
|
35
|
-
expect(
|
|
36
|
-
expect(
|
|
35
|
+
expect(kernelCallMock).toHaveBeenNthCalledWith(1, 'resolveWorkspace', {});
|
|
36
|
+
expect(kernelCallMock).toHaveBeenNthCalledWith(2, 'agent.startSession', {
|
|
37
37
|
workspaceId: 'ws-1',
|
|
38
38
|
apiKeyId: 'key-1',
|
|
39
39
|
clientKind: 'cli',
|
|
40
40
|
});
|
|
41
41
|
});
|
|
42
42
|
it('calls chain.ingestDocument with content, sourceRef, ingestionRunId, sessionId', async () => {
|
|
43
|
-
|
|
43
|
+
kernelCallMock
|
|
44
44
|
.mockResolvedValueOnce({ _id: 'ws-1', keyId: 'key-1' })
|
|
45
45
|
.mockResolvedValueOnce({ sessionId: 'sess-1', toolsScope: 'readwrite', workspaceName: 'Test' })
|
|
46
46
|
.mockResolvedValue({ entriesCreated: ['e1', 'e2'], skipped: 0 });
|
|
@@ -48,7 +48,7 @@ describe('runIngest', () => {
|
|
|
48
48
|
const content = '# Decision\n\nWe use TypeScript.';
|
|
49
49
|
writeFileSync(f, content);
|
|
50
50
|
await runIngest({ pattern: f });
|
|
51
|
-
const ingestCall =
|
|
51
|
+
const ingestCall = kernelCallMock.mock.calls.find((c) => c[0] === 'chain.ingestDocument');
|
|
52
52
|
expect(ingestCall).toBeDefined();
|
|
53
53
|
const [, args] = ingestCall;
|
|
54
54
|
expect(args.content).toBe(content);
|
|
@@ -57,19 +57,19 @@ describe('runIngest', () => {
|
|
|
57
57
|
expect(args.sessionId).toBe('sess-1');
|
|
58
58
|
});
|
|
59
59
|
it('calls agent.closeSession after ingest', async () => {
|
|
60
|
-
|
|
60
|
+
kernelCallMock
|
|
61
61
|
.mockResolvedValueOnce({ _id: 'ws-1', keyId: 'key-1' })
|
|
62
62
|
.mockResolvedValueOnce({ sessionId: 'sess-1', toolsScope: 'readwrite', workspaceName: 'Test' })
|
|
63
63
|
.mockResolvedValue({ entriesCreated: ['e1', 'e2'], skipped: 0 });
|
|
64
64
|
const f = join(tmpDir, 'doc.md');
|
|
65
65
|
writeFileSync(f, '# Test');
|
|
66
66
|
await runIngest({ pattern: f });
|
|
67
|
-
const closeCall =
|
|
67
|
+
const closeCall = kernelCallMock.mock.calls.find((c) => c[0] === 'agent.closeSession');
|
|
68
68
|
expect(closeCall).toBeDefined();
|
|
69
69
|
expect(closeCall).toEqual(['agent.closeSession', { sessionId: 'sess-1', status: 'closed' }]);
|
|
70
70
|
});
|
|
71
71
|
it('processes one file per chain.ingestDocument call (no batching of content)', async () => {
|
|
72
|
-
|
|
72
|
+
kernelCallMock
|
|
73
73
|
.mockResolvedValueOnce({ _id: 'ws-1', keyId: 'key-1' })
|
|
74
74
|
.mockResolvedValueOnce({ sessionId: 'sess-1', toolsScope: 'readwrite', workspaceName: 'Test' })
|
|
75
75
|
.mockResolvedValue({ entriesCreated: ['e1', 'e2'], skipped: 0 });
|
|
@@ -78,7 +78,7 @@ describe('runIngest', () => {
|
|
|
78
78
|
writeFileSync(f1, '# A');
|
|
79
79
|
writeFileSync(f2, '# B');
|
|
80
80
|
await runIngest({ pattern: join(tmpDir, '*.md') });
|
|
81
|
-
const ingestCalls =
|
|
81
|
+
const ingestCalls = kernelCallMock.mock.calls.filter((c) => c[0] === 'chain.ingestDocument');
|
|
82
82
|
expect(ingestCalls).toHaveLength(2);
|
|
83
83
|
const contents = ingestCalls.map((c) => c[1].content);
|
|
84
84
|
expect(contents).toContain('# A');
|
|
@@ -86,27 +86,27 @@ describe('runIngest', () => {
|
|
|
86
86
|
});
|
|
87
87
|
// ─── BET-221 S7: --dry-run ────────────────────────────────────────────────
|
|
88
88
|
it('--dry-run passes dryRun: true to chain.ingestDocument', async () => {
|
|
89
|
-
|
|
89
|
+
kernelCallMock
|
|
90
90
|
.mockResolvedValueOnce({ _id: 'ws-1', keyId: 'key-1' })
|
|
91
91
|
.mockResolvedValueOnce({ sessionId: 'sess-1', toolsScope: 'readwrite', workspaceName: 'Test' })
|
|
92
92
|
.mockResolvedValue({ entriesCreated: ['e1'], skipped: 0 });
|
|
93
93
|
const f = join(tmpDir, 'dry.md');
|
|
94
94
|
writeFileSync(f, '# Dry Run Test');
|
|
95
95
|
await runIngest({ pattern: f, dryRun: true });
|
|
96
|
-
const ingestCall =
|
|
96
|
+
const ingestCall = kernelCallMock.mock.calls.find((c) => c[0] === 'chain.ingestDocument');
|
|
97
97
|
expect(ingestCall).toBeDefined();
|
|
98
98
|
const [, args] = ingestCall;
|
|
99
99
|
expect(args.dryRun).toBe(true);
|
|
100
100
|
});
|
|
101
101
|
it('--dry-run still calls agent.closeSession', async () => {
|
|
102
|
-
|
|
102
|
+
kernelCallMock
|
|
103
103
|
.mockResolvedValueOnce({ _id: 'ws-1', keyId: 'key-1' })
|
|
104
104
|
.mockResolvedValueOnce({ sessionId: 'sess-1', toolsScope: 'readwrite', workspaceName: 'Test' })
|
|
105
105
|
.mockResolvedValue({ entriesCreated: ['e1'], skipped: 0 });
|
|
106
106
|
const f = join(tmpDir, 'dry2.md');
|
|
107
107
|
writeFileSync(f, '# Dry Run 2');
|
|
108
108
|
await runIngest({ pattern: f, dryRun: true });
|
|
109
|
-
const closeCall =
|
|
109
|
+
const closeCall = kernelCallMock.mock.calls.find((c) => c[0] === 'agent.closeSession');
|
|
110
110
|
expect(closeCall).toBeDefined();
|
|
111
111
|
});
|
|
112
112
|
// ─── BET-221 S7: --resume ─────────────────────────────────────────────────
|
|
@@ -115,14 +115,14 @@ describe('runIngest', () => {
|
|
|
115
115
|
const f2 = join(tmpDir, 'new.md');
|
|
116
116
|
writeFileSync(f1, '# Already committed');
|
|
117
117
|
writeFileSync(f2, '# New file');
|
|
118
|
-
|
|
119
|
-
|
|
118
|
+
kernelCallMock.mockReset();
|
|
119
|
+
kernelCallMock
|
|
120
120
|
.mockResolvedValueOnce({ _id: 'ws-1', keyId: 'key-1' }) // resolveWorkspace
|
|
121
121
|
.mockResolvedValueOnce([f1]) // staging.getCommittedSourceRefs
|
|
122
122
|
.mockResolvedValueOnce({ sessionId: 'sess-1', toolsScope: 'readwrite', workspaceName: 'Test' }) // startSession
|
|
123
123
|
.mockResolvedValue({ entriesCreated: ['e1'], skipped: 0 }); // ingestDocument, closeSession
|
|
124
124
|
await runIngest({ pattern: join(tmpDir, '*.md'), resume: true });
|
|
125
|
-
const ingestCalls =
|
|
125
|
+
const ingestCalls = kernelCallMock.mock.calls.filter((c) => c[0] === 'chain.ingestDocument');
|
|
126
126
|
expect(ingestCalls).toHaveLength(1);
|
|
127
127
|
const [, args] = ingestCalls[0];
|
|
128
128
|
expect(args.sourceRef).toBe(f2);
|
|
@@ -130,13 +130,13 @@ describe('runIngest', () => {
|
|
|
130
130
|
it('--resume exits early when all files already committed', async () => {
|
|
131
131
|
const f = join(tmpDir, 'done.md');
|
|
132
132
|
writeFileSync(f, '# Done');
|
|
133
|
-
|
|
134
|
-
|
|
133
|
+
kernelCallMock.mockReset();
|
|
134
|
+
kernelCallMock
|
|
135
135
|
.mockResolvedValueOnce({ _id: 'ws-1', keyId: 'key-1' }) // resolveWorkspace
|
|
136
136
|
.mockResolvedValueOnce([f]); // staging.getCommittedSourceRefs
|
|
137
137
|
await runIngest({ pattern: f, resume: true });
|
|
138
138
|
// Should NOT call startSession or ingestDocument — all skipped
|
|
139
|
-
const sessionCalls =
|
|
139
|
+
const sessionCalls = kernelCallMock.mock.calls.filter((c) => c[0] === 'agent.startSession');
|
|
140
140
|
expect(sessionCalls).toHaveLength(0);
|
|
141
141
|
});
|
|
142
142
|
// ─── BET-221 S7: --concurrency ────────────────────────────────────────────
|
|
@@ -147,12 +147,12 @@ describe('runIngest', () => {
|
|
|
147
147
|
writeFileSync(f1, '# P1');
|
|
148
148
|
writeFileSync(f2, '# P2');
|
|
149
149
|
writeFileSync(f3, '# P3');
|
|
150
|
-
|
|
150
|
+
kernelCallMock
|
|
151
151
|
.mockResolvedValueOnce({ _id: 'ws-1', keyId: 'key-1' })
|
|
152
152
|
.mockResolvedValueOnce({ sessionId: 'sess-1', toolsScope: 'readwrite', workspaceName: 'Test' })
|
|
153
153
|
.mockResolvedValue({ entriesCreated: ['e1'], skipped: 0 });
|
|
154
154
|
await runIngest({ pattern: join(tmpDir, '*.md'), concurrency: 3 });
|
|
155
|
-
const ingestCalls =
|
|
155
|
+
const ingestCalls = kernelCallMock.mock.calls.filter((c) => c[0] === 'chain.ingestDocument');
|
|
156
156
|
expect(ingestCalls).toHaveLength(3);
|
|
157
157
|
});
|
|
158
158
|
it('default concurrency is 1 (sequential)', async () => {
|
|
@@ -160,25 +160,25 @@ describe('runIngest', () => {
|
|
|
160
160
|
const f2 = join(tmpDir, 'seq2.md');
|
|
161
161
|
writeFileSync(f1, '# Seq1');
|
|
162
162
|
writeFileSync(f2, '# Seq2');
|
|
163
|
-
|
|
163
|
+
kernelCallMock
|
|
164
164
|
.mockResolvedValueOnce({ _id: 'ws-1', keyId: 'key-1' })
|
|
165
165
|
.mockResolvedValueOnce({ sessionId: 'sess-1', toolsScope: 'readwrite', workspaceName: 'Test' })
|
|
166
166
|
.mockResolvedValue({ entriesCreated: ['e1'], skipped: 0 });
|
|
167
167
|
await runIngest({ pattern: join(tmpDir, '*.md') });
|
|
168
168
|
// Both files processed — sequential, no concurrency option
|
|
169
|
-
const ingestCalls =
|
|
169
|
+
const ingestCalls = kernelCallMock.mock.calls.filter((c) => c[0] === 'chain.ingestDocument');
|
|
170
170
|
expect(ingestCalls).toHaveLength(2);
|
|
171
171
|
});
|
|
172
172
|
it('--concurrency is clamped to max 5', async () => {
|
|
173
173
|
const f = join(tmpDir, 'clamp.md');
|
|
174
174
|
writeFileSync(f, '# Clamp');
|
|
175
|
-
|
|
175
|
+
kernelCallMock
|
|
176
176
|
.mockResolvedValueOnce({ _id: 'ws-1', keyId: 'key-1' })
|
|
177
177
|
.mockResolvedValueOnce({ sessionId: 'sess-1', toolsScope: 'readwrite', workspaceName: 'Test' })
|
|
178
178
|
.mockResolvedValue({ entriesCreated: ['e1'], skipped: 0 });
|
|
179
179
|
// concurrency 10 should be clamped to 5 internally (no error)
|
|
180
180
|
await runIngest({ pattern: f, concurrency: 10 });
|
|
181
|
-
const ingestCalls =
|
|
181
|
+
const ingestCalls = kernelCallMock.mock.calls.filter((c) => c[0] === 'chain.ingestDocument');
|
|
182
182
|
expect(ingestCalls).toHaveLength(1);
|
|
183
183
|
});
|
|
184
184
|
});
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
2
|
import { runOrient } from '../commands/orient.js';
|
|
3
|
-
const
|
|
3
|
+
const kernelCallMock = vi.fn();
|
|
4
4
|
vi.mock('../lib/client.js', () => ({
|
|
5
|
-
|
|
5
|
+
kernelCall: (...args) => kernelCallMock(...args),
|
|
6
6
|
}));
|
|
7
7
|
vi.mock('../lib/config.js', () => ({
|
|
8
8
|
getConfigOrGuide: vi.fn(() => Promise.resolve({ apiKey: 'pb_sk_test', siteUrl: 'https://test.convex.site' })),
|
|
@@ -85,24 +85,24 @@ const orientViewTaskOnly = {
|
|
|
85
85
|
describe('runOrient', () => {
|
|
86
86
|
beforeEach(() => {
|
|
87
87
|
vi.clearAllMocks();
|
|
88
|
-
|
|
88
|
+
kernelCallMock.mockResolvedValue(orientViewWithoutTask);
|
|
89
89
|
});
|
|
90
90
|
it('calls chain.getOrientView without task by default', async () => {
|
|
91
91
|
const writeSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
92
92
|
await runOrient();
|
|
93
93
|
const output = writeSpy.mock.calls.map((call) => String(call[0])).join('');
|
|
94
94
|
writeSpy.mockRestore();
|
|
95
|
-
expect(
|
|
95
|
+
expect(kernelCallMock).toHaveBeenCalledWith('chain.getOrientView', {});
|
|
96
96
|
expect(output).toContain('Task grounding required');
|
|
97
97
|
expect(output).toContain('pb orient --task');
|
|
98
98
|
});
|
|
99
99
|
it('passes task through to chain.getOrientView and renders task context', async () => {
|
|
100
100
|
const writeSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
101
|
-
|
|
101
|
+
kernelCallMock.mockResolvedValueOnce(orientView);
|
|
102
102
|
await runOrient({ task: 'harden auth startup flow' });
|
|
103
103
|
const output = writeSpy.mock.calls.map((call) => String(call[0])).join('');
|
|
104
104
|
writeSpy.mockRestore();
|
|
105
|
-
expect(
|
|
105
|
+
expect(kernelCallMock).toHaveBeenCalledWith('chain.getOrientView', {
|
|
106
106
|
task: 'harden auth startup flow',
|
|
107
107
|
});
|
|
108
108
|
expect(output).toContain('## Task grounding');
|
|
@@ -113,7 +113,7 @@ describe('runOrient', () => {
|
|
|
113
113
|
});
|
|
114
114
|
it('renders task context even when governance buckets are empty', async () => {
|
|
115
115
|
const writeSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
116
|
-
|
|
116
|
+
kernelCallMock.mockResolvedValueOnce(orientViewTaskOnly);
|
|
117
117
|
await runOrient({ task: 'trace supporting context only' });
|
|
118
118
|
const output = writeSpy.mock.calls.map((call) => String(call[0])).join('');
|
|
119
119
|
writeSpy.mockRestore();
|
|
@@ -131,7 +131,7 @@ describe('runOrient', () => {
|
|
|
131
131
|
});
|
|
132
132
|
it('brief mode with task context renders startup grounding line', async () => {
|
|
133
133
|
const writeSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
134
|
-
|
|
134
|
+
kernelCallMock.mockResolvedValueOnce(orientView);
|
|
135
135
|
await runOrient({ brief: true, task: 'harden auth startup flow' });
|
|
136
136
|
const output = writeSpy.mock.calls.map((call) => String(call[0])).join('');
|
|
137
137
|
writeSpy.mockRestore();
|