@productbrain/cli 0.1.0-beta.71 → 0.1.0-beta.72
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 +7 -7
- 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 +213 -6
- 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/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.js +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.js +5 -5
- 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/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.js +8 -8
- 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/workspace-probe.js +2 -2
- package/package.json +1 -1
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* pb promote — verify
|
|
2
|
+
* pb promote — verify kernelCallWithSession sequence and args.
|
|
3
3
|
* Covers: happy path (direct commit), governance proposal, no session, empty entryId.
|
|
4
4
|
*/
|
|
5
5
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
6
6
|
import { runPromote } from '../commands/promote.js';
|
|
7
|
-
const
|
|
7
|
+
const kernelCallWithSessionMock = vi.fn();
|
|
8
8
|
vi.mock('../lib/client.js', () => ({
|
|
9
|
-
|
|
9
|
+
kernelCallWithSession: (...args) => kernelCallWithSessionMock(...args),
|
|
10
10
|
}));
|
|
11
11
|
vi.mock('../lib/config.js', () => ({
|
|
12
12
|
getConfigOrGuide: vi.fn(() => Promise.resolve({ apiKey: 'pb_sk_test', siteUrl: 'https://test.convex.site' })),
|
|
@@ -29,7 +29,7 @@ describe('runPromote', () => {
|
|
|
29
29
|
mockSession = { sessionId: 'sess-test' };
|
|
30
30
|
});
|
|
31
31
|
it('calls chain.commitEntry with correct args (happy path)', async () => {
|
|
32
|
-
|
|
32
|
+
kernelCallWithSessionMock
|
|
33
33
|
.mockResolvedValueOnce({ name: 'Test entry', collectionSlug: 'tensions', data: { description: 'Valid description' } })
|
|
34
34
|
.mockResolvedValueOnce([])
|
|
35
35
|
.mockResolvedValueOnce({
|
|
@@ -41,14 +41,14 @@ describe('runPromote', () => {
|
|
|
41
41
|
const writeSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
42
42
|
await runPromote({ entryId: 'TEN-951' });
|
|
43
43
|
writeSpy.mockRestore();
|
|
44
|
-
expect(
|
|
44
|
+
expect(kernelCallWithSessionMock).toHaveBeenCalledWith('chain.commitEntry', {
|
|
45
45
|
entryId: 'TEN-951',
|
|
46
46
|
author: 'agent:sess-test',
|
|
47
47
|
sessionId: 'sess-test',
|
|
48
48
|
});
|
|
49
49
|
});
|
|
50
50
|
it('passes commitMessage when --message is provided', async () => {
|
|
51
|
-
|
|
51
|
+
kernelCallWithSessionMock
|
|
52
52
|
.mockResolvedValueOnce({ name: 'Test entry', collectionSlug: 'tensions', data: { description: 'Valid description' } })
|
|
53
53
|
.mockResolvedValueOnce([])
|
|
54
54
|
.mockResolvedValueOnce({
|
|
@@ -60,7 +60,7 @@ describe('runPromote', () => {
|
|
|
60
60
|
const writeSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
61
61
|
await runPromote({ entryId: 'TEN-951', message: 'Validated and ready' });
|
|
62
62
|
writeSpy.mockRestore();
|
|
63
|
-
expect(
|
|
63
|
+
expect(kernelCallWithSessionMock).toHaveBeenCalledWith('chain.commitEntry', {
|
|
64
64
|
entryId: 'TEN-951',
|
|
65
65
|
author: 'agent:sess-test',
|
|
66
66
|
sessionId: 'sess-test',
|
|
@@ -68,7 +68,7 @@ describe('runPromote', () => {
|
|
|
68
68
|
});
|
|
69
69
|
});
|
|
70
70
|
it('displays version result for direct commit', async () => {
|
|
71
|
-
|
|
71
|
+
kernelCallWithSessionMock
|
|
72
72
|
.mockResolvedValueOnce({ name: 'Wave 2 smoke test', collectionSlug: 'tensions', data: { description: 'Valid description' } })
|
|
73
73
|
.mockResolvedValueOnce([])
|
|
74
74
|
.mockResolvedValueOnce({
|
|
@@ -88,7 +88,7 @@ describe('runPromote', () => {
|
|
|
88
88
|
expect(output).toContain('v1');
|
|
89
89
|
});
|
|
90
90
|
it('displays proposal result for governance mode', async () => {
|
|
91
|
-
|
|
91
|
+
kernelCallWithSessionMock
|
|
92
92
|
.mockResolvedValueOnce({ name: 'Some governed decision', collectionSlug: 'decisions', data: { rationale: 'Valid description' } })
|
|
93
93
|
.mockResolvedValueOnce([])
|
|
94
94
|
.mockResolvedValueOnce({
|
|
@@ -107,7 +107,7 @@ describe('runPromote', () => {
|
|
|
107
107
|
expect(output).toContain('governance mode');
|
|
108
108
|
});
|
|
109
109
|
it('displays quality warnings when present', async () => {
|
|
110
|
-
|
|
110
|
+
kernelCallWithSessionMock
|
|
111
111
|
.mockResolvedValueOnce({ name: 'Test Bet', collectionSlug: 'bets', data: { description: 'Valid description' } })
|
|
112
112
|
.mockResolvedValueOnce([])
|
|
113
113
|
.mockResolvedValueOnce({
|
|
@@ -132,18 +132,18 @@ describe('runPromote', () => {
|
|
|
132
132
|
it('throws CLIError with no active session', async () => {
|
|
133
133
|
mockSession = null;
|
|
134
134
|
await expect(runPromote({ entryId: 'TEN-951' })).rejects.toThrow('No active session');
|
|
135
|
-
expect(
|
|
135
|
+
expect(kernelCallWithSessionMock).not.toHaveBeenCalled();
|
|
136
136
|
});
|
|
137
137
|
it('throws CLIError with empty entryId', async () => {
|
|
138
138
|
await expect(runPromote({ entryId: ' ' })).rejects.toThrow('Entry ID is required');
|
|
139
|
-
expect(
|
|
139
|
+
expect(kernelCallWithSessionMock).not.toHaveBeenCalled();
|
|
140
140
|
});
|
|
141
141
|
it('handles server errors thrown during commit', async () => {
|
|
142
|
-
|
|
142
|
+
kernelCallWithSessionMock.mockRejectedValue(new Error('Entry not found.'));
|
|
143
143
|
await expect(runPromote({ entryId: 'TEN-999' })).rejects.toThrow('Entry not found.');
|
|
144
144
|
});
|
|
145
145
|
it('blocks promote when semantic contradiction preflight finds a contradiction', async () => {
|
|
146
|
-
|
|
146
|
+
kernelCallWithSessionMock
|
|
147
147
|
.mockResolvedValueOnce({ name: 'Conflicting entry', collectionSlug: 'decisions', data: { rationale: 'Valid description' } })
|
|
148
148
|
.mockResolvedValueOnce([
|
|
149
149
|
{
|
|
@@ -155,7 +155,7 @@ describe('runPromote', () => {
|
|
|
155
155
|
},
|
|
156
156
|
]);
|
|
157
157
|
await expect(runPromote({ entryId: 'DEC-100' })).rejects.toThrow('Commit blocked by contradiction with DEC-42: Directly contradicts the approved baseline.');
|
|
158
|
-
expect(
|
|
158
|
+
expect(kernelCallWithSessionMock).not.toHaveBeenCalledWith('chain.commitEntry', expect.anything());
|
|
159
159
|
});
|
|
160
160
|
});
|
|
161
161
|
//# sourceMappingURL=promote.test.js.map
|
|
@@ -13,9 +13,9 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
13
13
|
import { runProposals } from '../commands/proposals.js';
|
|
14
14
|
import { runAccept } from '../commands/accept.js';
|
|
15
15
|
import { runReject } from '../commands/reject.js';
|
|
16
|
-
const
|
|
16
|
+
const kernelCallMock = vi.fn();
|
|
17
17
|
vi.mock('../lib/client.js', () => ({
|
|
18
|
-
|
|
18
|
+
kernelCall: (...args) => kernelCallMock(...args),
|
|
19
19
|
}));
|
|
20
20
|
vi.mock('../lib/config.js', () => ({
|
|
21
21
|
getConfigOrGuide: vi.fn(() => Promise.resolve({ apiKey: 'pb_sk_test', siteUrl: 'https://test.convex.site' })),
|
|
@@ -48,16 +48,16 @@ describe('runProposals', () => {
|
|
|
48
48
|
vi.clearAllMocks();
|
|
49
49
|
});
|
|
50
50
|
it('formats empty proposal list', async () => {
|
|
51
|
-
|
|
51
|
+
kernelCallMock.mockResolvedValue([]);
|
|
52
52
|
const writeSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
53
53
|
await runProposals();
|
|
54
54
|
const output = writeSpy.mock.calls.map((c) => String(c[0])).join('');
|
|
55
55
|
writeSpy.mockRestore();
|
|
56
56
|
expect(output).toContain('No open consent proposals');
|
|
57
|
-
expect(
|
|
57
|
+
expect(kernelCallMock).toHaveBeenCalledWith('governance.listProposals', {});
|
|
58
58
|
});
|
|
59
59
|
it('formats non-empty proposal list with expiry', async () => {
|
|
60
|
-
|
|
60
|
+
kernelCallMock.mockResolvedValue([sampleProposal]);
|
|
61
61
|
const writeSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
62
62
|
await runProposals();
|
|
63
63
|
const output = writeSpy.mock.calls.map((c) => String(c[0])).join('');
|
|
@@ -74,7 +74,7 @@ describe('runAccept', () => {
|
|
|
74
74
|
vi.clearAllMocks();
|
|
75
75
|
});
|
|
76
76
|
it('calls governance.respondToProposal with approve verdict', async () => {
|
|
77
|
-
|
|
77
|
+
kernelCallMock.mockResolvedValue({
|
|
78
78
|
status: 'approved',
|
|
79
79
|
proposalId: 'prop-abc123',
|
|
80
80
|
message: 'Proposal approved. Governs relation created.',
|
|
@@ -83,7 +83,7 @@ describe('runAccept', () => {
|
|
|
83
83
|
await runAccept({ proposalId: 'prop-abc123' });
|
|
84
84
|
const output = writeSpy.mock.calls.map((c) => String(c[0])).join('');
|
|
85
85
|
writeSpy.mockRestore();
|
|
86
|
-
expect(
|
|
86
|
+
expect(kernelCallMock).toHaveBeenCalledWith('governance.respondToProposal', {
|
|
87
87
|
proposalId: 'prop-abc123',
|
|
88
88
|
verdict: 'approve',
|
|
89
89
|
});
|
|
@@ -98,9 +98,9 @@ describe('runAccept', () => {
|
|
|
98
98
|
};
|
|
99
99
|
const freshProposal = { ...sampleProposal, _id: 'prop-fresh' };
|
|
100
100
|
// First call: listProposals
|
|
101
|
-
|
|
101
|
+
kernelCallMock.mockResolvedValueOnce([expiredProposal, freshProposal]);
|
|
102
102
|
// Second call: respondToProposal for fresh proposal
|
|
103
|
-
|
|
103
|
+
kernelCallMock.mockResolvedValueOnce({
|
|
104
104
|
status: 'approved',
|
|
105
105
|
proposalId: 'prop-fresh',
|
|
106
106
|
message: 'Proposal approved. Governs relation created.',
|
|
@@ -110,9 +110,9 @@ describe('runAccept', () => {
|
|
|
110
110
|
const output = writeSpy.mock.calls.map((c) => String(c[0])).join('');
|
|
111
111
|
writeSpy.mockRestore();
|
|
112
112
|
// Should have called listProposals, then respondToProposal for fresh only
|
|
113
|
-
expect(
|
|
114
|
-
expect(
|
|
115
|
-
expect(
|
|
113
|
+
expect(kernelCallMock).toHaveBeenCalledTimes(2);
|
|
114
|
+
expect(kernelCallMock).toHaveBeenNthCalledWith(1, 'governance.listProposals', {});
|
|
115
|
+
expect(kernelCallMock).toHaveBeenNthCalledWith(2, 'governance.respondToProposal', {
|
|
116
116
|
proposalId: 'prop-fresh',
|
|
117
117
|
verdict: 'approve',
|
|
118
118
|
});
|
|
@@ -122,7 +122,7 @@ describe('runAccept', () => {
|
|
|
122
122
|
expect(output).toContain('Expired');
|
|
123
123
|
});
|
|
124
124
|
it('--auto with no proposals reports nothing to process', async () => {
|
|
125
|
-
|
|
125
|
+
kernelCallMock.mockResolvedValueOnce([]);
|
|
126
126
|
const writeSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
127
127
|
await runAccept({ auto: true });
|
|
128
128
|
const output = writeSpy.mock.calls.map((c) => String(c[0])).join('');
|
|
@@ -131,7 +131,7 @@ describe('runAccept', () => {
|
|
|
131
131
|
});
|
|
132
132
|
it('throws CLIError when no proposal-id and no --auto', async () => {
|
|
133
133
|
await expect(runAccept({})).rejects.toThrow('Proposal ID is required');
|
|
134
|
-
expect(
|
|
134
|
+
expect(kernelCallMock).not.toHaveBeenCalled();
|
|
135
135
|
});
|
|
136
136
|
});
|
|
137
137
|
describe('runReject', () => {
|
|
@@ -139,7 +139,7 @@ describe('runReject', () => {
|
|
|
139
139
|
vi.clearAllMocks();
|
|
140
140
|
});
|
|
141
141
|
it('calls governance.respondToProposal with reject verdict and reason', async () => {
|
|
142
|
-
|
|
142
|
+
kernelCallMock.mockResolvedValue({
|
|
143
143
|
status: 'objected',
|
|
144
144
|
proposalId: 'prop-abc123',
|
|
145
145
|
message: 'Proposal rejected: Creates circular governance',
|
|
@@ -148,7 +148,7 @@ describe('runReject', () => {
|
|
|
148
148
|
await runReject({ proposalId: 'prop-abc123', reason: 'Creates circular governance' });
|
|
149
149
|
const output = writeSpy.mock.calls.map((c) => String(c[0])).join('');
|
|
150
150
|
writeSpy.mockRestore();
|
|
151
|
-
expect(
|
|
151
|
+
expect(kernelCallMock).toHaveBeenCalledWith('governance.respondToProposal', {
|
|
152
152
|
proposalId: 'prop-abc123',
|
|
153
153
|
verdict: 'reject',
|
|
154
154
|
reason: 'Creates circular governance',
|
|
@@ -157,11 +157,11 @@ describe('runReject', () => {
|
|
|
157
157
|
});
|
|
158
158
|
it('throws CLIError when reason is empty', async () => {
|
|
159
159
|
await expect(runReject({ proposalId: 'prop-abc123', reason: '' })).rejects.toThrow('reason is required');
|
|
160
|
-
expect(
|
|
160
|
+
expect(kernelCallMock).not.toHaveBeenCalled();
|
|
161
161
|
});
|
|
162
162
|
it('throws CLIError when proposal-id is empty', async () => {
|
|
163
163
|
await expect(runReject({ proposalId: '', reason: 'Some reason' })).rejects.toThrow('Proposal ID is required');
|
|
164
|
-
expect(
|
|
164
|
+
expect(kernelCallMock).not.toHaveBeenCalled();
|
|
165
165
|
});
|
|
166
166
|
});
|
|
167
167
|
//# sourceMappingURL=proposals.test.js.map
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* pb relate / pb unrelate — verify
|
|
2
|
+
* pb relate / pb unrelate — verify kernelCallWithSession sequence and args.
|
|
3
3
|
* TEN-341 test contract: happy paths, no session.
|
|
4
4
|
*/
|
|
5
5
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
6
6
|
import { runRelate, runUnrelate } from '../commands/relate.js';
|
|
7
|
-
const
|
|
7
|
+
const kernelCallWithSessionMock = vi.fn();
|
|
8
8
|
vi.mock('../lib/client.js', () => ({
|
|
9
|
-
|
|
9
|
+
kernelCallWithSession: (...args) => kernelCallWithSessionMock(...args),
|
|
10
10
|
}));
|
|
11
11
|
vi.mock('../lib/config.js', () => ({
|
|
12
12
|
getConfigOrGuide: vi.fn(() => Promise.resolve({ apiKey: 'pb_sk_test', siteUrl: 'https://test.convex.site' })),
|
|
@@ -19,11 +19,11 @@ describe('runRelate', () => {
|
|
|
19
19
|
beforeEach(() => {
|
|
20
20
|
vi.clearAllMocks();
|
|
21
21
|
mockSession = { sessionId: 'sess-test' };
|
|
22
|
-
|
|
22
|
+
kernelCallWithSessionMock.mockResolvedValue({});
|
|
23
23
|
});
|
|
24
24
|
it('calls chain.createEntryRelation with correct args', async () => {
|
|
25
25
|
await runRelate({ fromId: 'BET-151', type: 'informed_by', toId: 'DEC-264' });
|
|
26
|
-
expect(
|
|
26
|
+
expect(kernelCallWithSessionMock).toHaveBeenCalledWith('chain.createEntryRelation', {
|
|
27
27
|
fromEntryId: 'BET-151',
|
|
28
28
|
toEntryId: 'DEC-264',
|
|
29
29
|
type: 'informed_by',
|
|
@@ -31,7 +31,7 @@ describe('runRelate', () => {
|
|
|
31
31
|
});
|
|
32
32
|
it('passes ifMissing=true when --if-missing flag is set', async () => {
|
|
33
33
|
await runRelate({ fromId: 'BET-151', type: 'part_of', toId: 'FEAT-77', ifMissing: true });
|
|
34
|
-
expect(
|
|
34
|
+
expect(kernelCallWithSessionMock).toHaveBeenCalledWith('chain.createEntryRelation', {
|
|
35
35
|
fromEntryId: 'BET-151',
|
|
36
36
|
toEntryId: 'FEAT-77',
|
|
37
37
|
type: 'part_of',
|
|
@@ -40,14 +40,14 @@ describe('runRelate', () => {
|
|
|
40
40
|
});
|
|
41
41
|
it('does not pass ifMissing when flag is not set', async () => {
|
|
42
42
|
await runRelate({ fromId: 'BET-151', type: 'part_of', toId: 'FEAT-77' });
|
|
43
|
-
expect(
|
|
43
|
+
expect(kernelCallWithSessionMock).toHaveBeenCalledWith('chain.createEntryRelation', {
|
|
44
44
|
fromEntryId: 'BET-151',
|
|
45
45
|
toEntryId: 'FEAT-77',
|
|
46
46
|
type: 'part_of',
|
|
47
47
|
});
|
|
48
48
|
});
|
|
49
49
|
it('handles alreadyExists=true response gracefully and reflects it in output', async () => {
|
|
50
|
-
|
|
50
|
+
kernelCallWithSessionMock.mockResolvedValue({ alreadyExists: true });
|
|
51
51
|
const writeSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
52
52
|
await runRelate({ fromId: 'BET-151', type: 'part_of', toId: 'FEAT-77', ifMissing: true });
|
|
53
53
|
const output = writeSpy.mock.calls.map(c => String(c[0])).join('');
|
|
@@ -56,7 +56,7 @@ describe('runRelate', () => {
|
|
|
56
56
|
expect(output).toContain('alreadyExists');
|
|
57
57
|
});
|
|
58
58
|
it('surfaces proposal_created when backend returns a proposal instead of a relation', async () => {
|
|
59
|
-
|
|
59
|
+
kernelCallWithSessionMock.mockResolvedValue({
|
|
60
60
|
status: 'proposal_created',
|
|
61
61
|
proposalId: 'prop-123',
|
|
62
62
|
fromEntryId: 'STD-2',
|
|
@@ -73,22 +73,22 @@ describe('runRelate', () => {
|
|
|
73
73
|
it('throws CLIError with no active session', async () => {
|
|
74
74
|
mockSession = null;
|
|
75
75
|
await expect(runRelate({ fromId: 'A', type: 'x', toId: 'B' })).rejects.toThrow('No active session');
|
|
76
|
-
expect(
|
|
76
|
+
expect(kernelCallWithSessionMock).not.toHaveBeenCalled();
|
|
77
77
|
});
|
|
78
78
|
it('throws CLIError with empty args', async () => {
|
|
79
79
|
await expect(runRelate({ fromId: '', type: 'x', toId: 'B' })).rejects.toThrow('All arguments required');
|
|
80
|
-
expect(
|
|
80
|
+
expect(kernelCallWithSessionMock).not.toHaveBeenCalled();
|
|
81
81
|
});
|
|
82
82
|
});
|
|
83
83
|
describe('runUnrelate', () => {
|
|
84
84
|
beforeEach(() => {
|
|
85
85
|
vi.clearAllMocks();
|
|
86
86
|
mockSession = { sessionId: 'sess-test' };
|
|
87
|
-
|
|
87
|
+
kernelCallWithSessionMock.mockResolvedValue({});
|
|
88
88
|
});
|
|
89
89
|
it('calls chain.removeEntryRelation with correct args', async () => {
|
|
90
90
|
await runUnrelate({ fromId: 'BET-151', type: 'part_of', toId: 'FEAT-77' });
|
|
91
|
-
expect(
|
|
91
|
+
expect(kernelCallWithSessionMock).toHaveBeenCalledWith('chain.removeEntryRelation', {
|
|
92
92
|
fromEntryId: 'BET-151',
|
|
93
93
|
toEntryId: 'FEAT-77',
|
|
94
94
|
type: 'part_of',
|
|
@@ -97,7 +97,7 @@ describe('runUnrelate', () => {
|
|
|
97
97
|
it('throws CLIError with no active session', async () => {
|
|
98
98
|
mockSession = null;
|
|
99
99
|
await expect(runUnrelate({ fromId: 'A', type: 'x', toId: 'B' })).rejects.toThrow('No active session');
|
|
100
|
-
expect(
|
|
100
|
+
expect(kernelCallWithSessionMock).not.toHaveBeenCalled();
|
|
101
101
|
});
|
|
102
102
|
});
|
|
103
103
|
//# sourceMappingURL=relate.test.js.map
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* session-touch — verify
|
|
2
|
+
* session-touch — verify kernelCallWithSession auto-renews the session TTL.
|
|
3
3
|
* BET-181 Slice 1: Session Auto-Renew on Write.
|
|
4
4
|
*
|
|
5
5
|
* Done-when criteria tested:
|
|
6
|
-
* 1. After a successful
|
|
7
|
-
* 2. Touch happens in exactly ONE place (
|
|
6
|
+
* 1. After a successful kernelCallWithSession, agent.touchSession is called with the current session ID
|
|
7
|
+
* 2. Touch happens in exactly ONE place (kernelCallWithSession) — not per-command
|
|
8
8
|
* 3. Touch is fire-and-forget (does not block the write response)
|
|
9
9
|
* 4. When no session is active (session.json absent), no touch is attempted
|
|
10
|
-
* 5. When
|
|
10
|
+
* 5. When kernelCallWithSession fails (write error), touch is NOT called
|
|
11
11
|
*/
|
|
12
12
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
13
13
|
// We need to control what readSession() returns per test.
|
|
@@ -19,7 +19,7 @@ vi.mock('../lib/config.js', () => ({
|
|
|
19
19
|
getConfig: vi.fn(() => ({ apiKey: 'pb_sk_test', siteUrl: 'https://test.convex.site' })),
|
|
20
20
|
}));
|
|
21
21
|
// Import the module under test AFTER setting up mocks.
|
|
22
|
-
import {
|
|
22
|
+
import { kernelCallWithSession } from '../lib/client.js';
|
|
23
23
|
// Helper: build a fetch mock that returns different responses based on `fn`.
|
|
24
24
|
function makeFetch({ writeOk = true, writeData = { ok: true }, } = {}) {
|
|
25
25
|
return vi.fn(async (_input, init) => {
|
|
@@ -43,7 +43,7 @@ async function flushMicrotasks() {
|
|
|
43
43
|
await Promise.resolve();
|
|
44
44
|
await Promise.resolve();
|
|
45
45
|
}
|
|
46
|
-
describe('
|
|
46
|
+
describe('kernelCallWithSession — session touch on write', () => {
|
|
47
47
|
beforeEach(() => {
|
|
48
48
|
vi.clearAllMocks();
|
|
49
49
|
mockSession = {
|
|
@@ -59,7 +59,7 @@ describe('convexCallWithSession — session touch on write', () => {
|
|
|
59
59
|
});
|
|
60
60
|
it('calls agent.touchSession with the current session ID after a successful write', async () => {
|
|
61
61
|
global.fetch = makeFetch({ writeOk: true, writeData: { entryId: 'BET-181' } });
|
|
62
|
-
await
|
|
62
|
+
await kernelCallWithSession('chain.updateEntry', { entryId: 'BET-181', name: 'New name' });
|
|
63
63
|
await flushMicrotasks();
|
|
64
64
|
const calls = global.fetch.mock.calls;
|
|
65
65
|
// Expect exactly 2 fetch calls: the write + the touch
|
|
@@ -75,7 +75,7 @@ describe('convexCallWithSession — session touch on write', () => {
|
|
|
75
75
|
it('does NOT call agent.touchSession when no session is active', async () => {
|
|
76
76
|
mockSession = null;
|
|
77
77
|
global.fetch = makeFetch({ writeOk: true });
|
|
78
|
-
await
|
|
78
|
+
await kernelCallWithSession('chain.updateEntry', { entryId: 'BET-100' });
|
|
79
79
|
await flushMicrotasks();
|
|
80
80
|
const calls = global.fetch.mock.calls;
|
|
81
81
|
// Only the write call — no touch
|
|
@@ -85,7 +85,7 @@ describe('convexCallWithSession — session touch on write', () => {
|
|
|
85
85
|
});
|
|
86
86
|
it('does NOT call agent.touchSession when the write fails', async () => {
|
|
87
87
|
global.fetch = makeFetch({ writeOk: false });
|
|
88
|
-
await expect(
|
|
88
|
+
await expect(kernelCallWithSession('chain.updateEntry', { entryId: 'BET-100' })).rejects.toThrow('Server error');
|
|
89
89
|
await flushMicrotasks();
|
|
90
90
|
const calls = global.fetch.mock.calls;
|
|
91
91
|
// Only the write call — no touch because write threw
|
|
@@ -106,7 +106,7 @@ describe('convexCallWithSession — session touch on write', () => {
|
|
|
106
106
|
};
|
|
107
107
|
});
|
|
108
108
|
// Should resolve successfully despite touch throwing
|
|
109
|
-
const result = await
|
|
109
|
+
const result = await kernelCallWithSession('chain.updateEntry', { entryId: 'BET-181' });
|
|
110
110
|
await flushMicrotasks();
|
|
111
111
|
expect(result).toEqual({ entryId: 'BET-181' });
|
|
112
112
|
});
|
|
@@ -123,7 +123,7 @@ describe('convexCallWithSession — session touch on write', () => {
|
|
|
123
123
|
order.push('write-complete');
|
|
124
124
|
return { ok: true, json: async () => ({ data: { entryId: 'BET-181' } }) };
|
|
125
125
|
});
|
|
126
|
-
await
|
|
126
|
+
await kernelCallWithSession('chain.updateEntry', { entryId: 'BET-181' });
|
|
127
127
|
// Write has resolved — touch has NOT yet completed (it's still pending)
|
|
128
128
|
expect(order).toEqual(['write-complete']);
|
|
129
129
|
// After waiting for the touch to finish
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
6
6
|
import { runSessionId } from '../commands/session.js';
|
|
7
7
|
vi.mock('../lib/client.js', () => ({
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
kernelCall: vi.fn(),
|
|
9
|
+
kernelCallWithSession: vi.fn(),
|
|
10
10
|
}));
|
|
11
11
|
vi.mock('../lib/config.js', () => ({
|
|
12
12
|
getConfigOrGuide: vi.fn(() => Promise.resolve({ apiKey: 'pb_sk_test', siteUrl: 'https://test.convex.site' })),
|
|
@@ -14,11 +14,11 @@ vi.mock('../lib/telemetry.js', () => ({
|
|
|
14
14
|
},
|
|
15
15
|
initTelemetry: vi.fn(),
|
|
16
16
|
}));
|
|
17
|
-
const
|
|
18
|
-
const
|
|
17
|
+
const kernelCallMock = vi.fn();
|
|
18
|
+
const kernelCallWithSessionMock = vi.fn();
|
|
19
19
|
vi.mock('../lib/client.js', () => ({
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
kernelCall: (...args) => kernelCallMock(...args),
|
|
21
|
+
kernelCallWithSession: (...args) => kernelCallWithSessionMock(...args),
|
|
22
22
|
}));
|
|
23
23
|
let mockConfig = {
|
|
24
24
|
apiKey: 'pb_sk_test_key_1234',
|
|
@@ -85,7 +85,7 @@ describe('runSetup', () => {
|
|
|
85
85
|
mockSession = null;
|
|
86
86
|
});
|
|
87
87
|
it('skips login when valid config exists and runs onboarding', async () => {
|
|
88
|
-
|
|
88
|
+
kernelCallMock.mockResolvedValueOnce({ _id: 'ws-1', keyId: 'key-1', name: 'Test Workspace' }); // resolveWorkspace
|
|
89
89
|
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
|
|
90
90
|
await runSetup();
|
|
91
91
|
// Should NOT call runLogin
|
|
@@ -118,7 +118,7 @@ describe('runSetup', () => {
|
|
|
118
118
|
runLoginMock.mockImplementation(() => {
|
|
119
119
|
mockConfig = { apiKey: 'pb_sk_new_key', siteUrl: 'https://test.convex.site' };
|
|
120
120
|
});
|
|
121
|
-
|
|
121
|
+
kernelCallMock.mockResolvedValueOnce({ _id: 'ws-1', keyId: 'key-1', name: 'Test Workspace' }); // resolveWorkspace
|
|
122
122
|
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
|
|
123
123
|
await runSetup();
|
|
124
124
|
expect(runLoginMock).toHaveBeenCalledOnce();
|
|
@@ -128,7 +128,7 @@ describe('runSetup', () => {
|
|
|
128
128
|
logSpy.mockRestore();
|
|
129
129
|
});
|
|
130
130
|
it('handles workspace verification failure gracefully', async () => {
|
|
131
|
-
|
|
131
|
+
kernelCallMock.mockRejectedValueOnce(new Error('Network error')); // resolveWorkspace fails
|
|
132
132
|
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
|
|
133
133
|
await runSetup();
|
|
134
134
|
// Should warn about connection
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* pb update — verify
|
|
2
|
+
* pb update — verify kernelCallWithSession sequence and args.
|
|
3
3
|
* TEN-341 test contract: happy path, no session, empty entryId.
|
|
4
4
|
*/
|
|
5
5
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
6
6
|
import { runUpdate } from '../commands/update.js';
|
|
7
7
|
import { CLIError } from '../lib/errors.js';
|
|
8
|
-
const
|
|
9
|
-
const
|
|
8
|
+
const kernelCallWithSessionMock = vi.fn();
|
|
9
|
+
const kernelCallMock = vi.fn();
|
|
10
10
|
vi.mock('../lib/client.js', () => ({
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
kernelCallWithSession: (...args) => kernelCallWithSessionMock(...args),
|
|
12
|
+
kernelCall: (...args) => kernelCallMock(...args),
|
|
13
13
|
// McpError exported so update.ts can import the type (not used at runtime in tests).
|
|
14
14
|
McpError: class McpError extends Error {
|
|
15
15
|
details;
|
|
@@ -50,7 +50,7 @@ describe('runUpdate', () => {
|
|
|
50
50
|
vi.clearAllMocks();
|
|
51
51
|
mockSession = { sessionId: 'sess-test' };
|
|
52
52
|
// BET-192: updateEntry returns warnings + normalization breakdown (symmetric with createEntry)
|
|
53
|
-
|
|
53
|
+
kernelCallWithSessionMock.mockResolvedValue({
|
|
54
54
|
id: 'j571abc123def456',
|
|
55
55
|
warnings: [],
|
|
56
56
|
normalization: { remapped: {}, rejected: [], accepted: [] },
|
|
@@ -58,7 +58,7 @@ describe('runUpdate', () => {
|
|
|
58
58
|
validationWarnings: [],
|
|
59
59
|
});
|
|
60
60
|
// Post-update refresh: default to returning a minimal entry.
|
|
61
|
-
|
|
61
|
+
kernelCallMock.mockResolvedValue({
|
|
62
62
|
entryId: 'BET-151',
|
|
63
63
|
name: 'Test Entry',
|
|
64
64
|
status: 'active',
|
|
@@ -72,7 +72,7 @@ describe('runUpdate', () => {
|
|
|
72
72
|
note: 'Updated description',
|
|
73
73
|
});
|
|
74
74
|
writeSpy.mockRestore();
|
|
75
|
-
expect(
|
|
75
|
+
expect(kernelCallWithSessionMock).toHaveBeenCalledWith('chain.updateEntry', {
|
|
76
76
|
entryId: 'BET-151',
|
|
77
77
|
changedBy: 'agent:sess-test',
|
|
78
78
|
data: { description: 'new description' },
|
|
@@ -88,7 +88,7 @@ describe('runUpdate', () => {
|
|
|
88
88
|
note: 'Resolved',
|
|
89
89
|
});
|
|
90
90
|
writeSpy.mockRestore();
|
|
91
|
-
expect(
|
|
91
|
+
expect(kernelCallWithSessionMock).toHaveBeenCalledWith('chain.updateEntry', {
|
|
92
92
|
entryId: 'TEN-703',
|
|
93
93
|
changedBy: 'agent:sess-test',
|
|
94
94
|
name: 'New name',
|
|
@@ -103,7 +103,7 @@ describe('runUpdate', () => {
|
|
|
103
103
|
workflowStatus: 'shipped',
|
|
104
104
|
});
|
|
105
105
|
writeSpy.mockRestore();
|
|
106
|
-
expect(
|
|
106
|
+
expect(kernelCallWithSessionMock).toHaveBeenCalledWith('chain.updateEntry', {
|
|
107
107
|
entryId: 'BET-271',
|
|
108
108
|
changedBy: 'agent:sess-test',
|
|
109
109
|
workflowStatus: 'shipped',
|
|
@@ -116,7 +116,7 @@ describe('runUpdate', () => {
|
|
|
116
116
|
field: ['rationale=a=b=c'],
|
|
117
117
|
});
|
|
118
118
|
writeSpy.mockRestore();
|
|
119
|
-
expect(
|
|
119
|
+
expect(kernelCallWithSessionMock).toHaveBeenCalledWith('chain.updateEntry', {
|
|
120
120
|
entryId: 'DEC-264',
|
|
121
121
|
changedBy: 'agent:sess-test',
|
|
122
122
|
data: { rationale: 'a=b=c' },
|
|
@@ -126,21 +126,21 @@ describe('runUpdate', () => {
|
|
|
126
126
|
mockSession = null;
|
|
127
127
|
await expect(runUpdate({ entryId: 'BET-151', field: ['x=1'] })).rejects.toThrow(CLIError);
|
|
128
128
|
await expect(runUpdate({ entryId: 'BET-151', field: ['x=1'] })).rejects.toThrow('No active session');
|
|
129
|
-
expect(
|
|
129
|
+
expect(kernelCallWithSessionMock).not.toHaveBeenCalled();
|
|
130
130
|
});
|
|
131
131
|
it('throws CLIError with empty entryId', async () => {
|
|
132
132
|
await expect(runUpdate({ entryId: ' ', field: ['x=1'] })).rejects.toThrow(CLIError);
|
|
133
133
|
await expect(runUpdate({ entryId: ' ', field: ['x=1'] })).rejects.toThrow('Entry ID is required');
|
|
134
|
-
expect(
|
|
134
|
+
expect(kernelCallWithSessionMock).not.toHaveBeenCalled();
|
|
135
135
|
});
|
|
136
136
|
it('throws CLIError when nothing to update', async () => {
|
|
137
137
|
await expect(runUpdate({ entryId: 'BET-151' })).rejects.toThrow(CLIError);
|
|
138
138
|
await expect(runUpdate({ entryId: 'BET-151' })).rejects.toThrow('Nothing to update');
|
|
139
|
-
expect(
|
|
139
|
+
expect(kernelCallWithSessionMock).not.toHaveBeenCalled();
|
|
140
140
|
});
|
|
141
141
|
// ── Structured validation error tests ────────────────────────────────────
|
|
142
142
|
it('throws CLIError with structured select field error message', async () => {
|
|
143
|
-
|
|
143
|
+
kernelCallWithSessionMock.mockRejectedValue(makeMcpError("Entry data validation failed: Field \"appetite\" must be one of: small, medium, large. Got: 'Small Batch'. Fix the values and try again.", 'VALIDATION_FAILED', [{
|
|
144
144
|
fieldKey: 'appetite',
|
|
145
145
|
fieldLabel: 'appetite',
|
|
146
146
|
invalidValue: 'Small Batch',
|
|
@@ -154,14 +154,14 @@ describe('runUpdate', () => {
|
|
|
154
154
|
expect(err.message).toContain("Got: 'Small Batch'");
|
|
155
155
|
});
|
|
156
156
|
it('throws CLIError with plain error message when no structured details', async () => {
|
|
157
|
-
|
|
157
|
+
kernelCallWithSessionMock.mockRejectedValue(makeMcpError('Entry not found.', 'NOT_FOUND'));
|
|
158
158
|
const err = await runUpdate({ entryId: 'BET-999', field: ['x=1'] }).catch((e) => e);
|
|
159
159
|
expect(err).toBeInstanceOf(CLIError);
|
|
160
160
|
expect(err.message).toContain('Entry not found.');
|
|
161
161
|
});
|
|
162
162
|
// ── Slice 3: post-update refresh ─────────────────────────────────────────
|
|
163
163
|
it('calls chain.getEntry after successful update and includes entry in output', async () => {
|
|
164
|
-
|
|
164
|
+
kernelCallMock.mockResolvedValue({
|
|
165
165
|
entryId: 'BET-151',
|
|
166
166
|
name: 'My Bet',
|
|
167
167
|
status: 'active',
|
|
@@ -170,13 +170,13 @@ describe('runUpdate', () => {
|
|
|
170
170
|
await runUpdate({ entryId: 'BET-151', field: ['description=new'] });
|
|
171
171
|
const output = writeSpy.mock.calls.map(c => String(c[0])).join('');
|
|
172
172
|
writeSpy.mockRestore();
|
|
173
|
-
expect(
|
|
173
|
+
expect(kernelCallMock).toHaveBeenCalledWith('chain.getEntry', { entryId: 'BET-151' });
|
|
174
174
|
// Pretty output: receipt then entry block
|
|
175
175
|
expect(output).toContain('Updated BET-151');
|
|
176
176
|
expect(output).toContain('My Bet');
|
|
177
177
|
});
|
|
178
178
|
it('still shows update receipt when post-update getEntry fails (graceful degradation)', async () => {
|
|
179
|
-
|
|
179
|
+
kernelCallMock.mockRejectedValue(new Error('network error'));
|
|
180
180
|
const writeSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
181
181
|
await runUpdate({ entryId: 'BET-151', field: ['description=new'] });
|
|
182
182
|
const output = writeSpy.mock.calls.map(c => String(c[0])).join('');
|
|
@@ -187,7 +187,7 @@ describe('runUpdate', () => {
|
|
|
187
187
|
});
|
|
188
188
|
// ── BET-192 / FEAT-575: Normalization warning tests ──────────────────────
|
|
189
189
|
it('throws CLIError when fields are rejected (BET-192, BET-271)', async () => {
|
|
190
|
-
|
|
190
|
+
kernelCallWithSessionMock.mockResolvedValue({
|
|
191
191
|
id: 'j571abc123def456',
|
|
192
192
|
warnings: ['Unknown field "foo" dropped. Available fields: description, doneWhen'],
|
|
193
193
|
normalization: { remapped: {}, rejected: ['foo'], accepted: ['description'] },
|
|
@@ -207,7 +207,7 @@ describe('runUpdate', () => {
|
|
|
207
207
|
writeSpy.mockRestore();
|
|
208
208
|
});
|
|
209
209
|
it('displays no warnings when all fields are valid (BET-192)', async () => {
|
|
210
|
-
|
|
210
|
+
kernelCallWithSessionMock.mockResolvedValue({
|
|
211
211
|
id: 'j571abc123def456',
|
|
212
212
|
warnings: [],
|
|
213
213
|
normalization: { remapped: {}, rejected: [], accepted: ['description'] },
|