berget 2.2.6 → 2.2.8
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/.github/workflows/publish.yml +2 -2
- package/.github/workflows/test.yml +10 -4
- package/.husky/pre-commit +1 -0
- package/.prettierignore +15 -0
- package/.prettierrc +7 -3
- package/CONTRIBUTING.md +38 -0
- package/README.md +2 -148
- package/dist/index.js +10 -11
- package/dist/package.json +30 -2
- package/dist/src/agents/app.js +28 -0
- package/dist/src/agents/backend.js +25 -0
- package/dist/src/agents/devops.js +34 -0
- package/dist/src/agents/frontend.js +25 -0
- package/dist/src/agents/fullstack.js +25 -0
- package/dist/src/agents/index.js +61 -0
- package/dist/src/agents/quality.js +70 -0
- package/dist/src/agents/security.js +26 -0
- package/dist/src/agents/types.js +2 -0
- package/dist/src/client.js +97 -117
- package/dist/src/commands/api-keys.js +75 -90
- package/dist/src/commands/auth.js +7 -16
- package/dist/src/commands/autocomplete.js +1 -1
- package/dist/src/commands/billing.js +6 -17
- package/dist/src/commands/chat.js +68 -101
- package/dist/src/commands/clusters.js +9 -18
- package/dist/src/commands/code/__tests__/auth-sync.test.js +351 -0
- package/dist/src/commands/code/__tests__/fake-api-key-service.js +13 -0
- package/dist/src/commands/code/__tests__/fake-auth-service.js +47 -0
- package/dist/src/commands/code/__tests__/fake-command-runner.js +21 -34
- package/dist/src/commands/code/__tests__/fake-file-store.js +20 -33
- package/dist/src/commands/code/__tests__/fake-prompter.js +83 -57
- package/dist/src/commands/code/__tests__/setup-flow.test.js +359 -92
- package/dist/src/commands/code/adapters/clack-prompter.js +15 -22
- package/dist/src/commands/code/adapters/fs-file-store.js +26 -40
- package/dist/src/commands/code/adapters/spawn-command-runner.js +27 -37
- package/dist/src/commands/code/auth-sync.js +270 -0
- package/dist/src/commands/code/errors.js +12 -9
- package/dist/src/commands/code/ports/auth-services.js +2 -0
- package/dist/src/commands/code/setup.js +387 -281
- package/dist/src/commands/code.js +205 -332
- package/dist/src/commands/index.js +5 -5
- package/dist/src/commands/models.js +6 -17
- package/dist/src/commands/users.js +5 -16
- package/dist/src/constants/command-structure.js +104 -104
- package/dist/src/services/api-key-service.js +132 -157
- package/dist/src/services/auth-service.js +89 -342
- package/dist/src/services/browser-auth.js +268 -0
- package/dist/src/services/chat-service.js +371 -401
- package/dist/src/services/cluster-service.js +47 -62
- package/dist/src/services/collaborator-service.js +10 -25
- package/dist/src/services/flux-service.js +14 -29
- package/dist/src/services/helm-service.js +10 -25
- package/dist/src/services/kubectl-service.js +16 -33
- package/dist/src/utils/config-checker.js +3 -3
- package/dist/src/utils/config-loader.js +95 -95
- package/dist/src/utils/default-api-key.js +124 -134
- package/dist/src/utils/env-manager.js +55 -66
- package/dist/src/utils/error-handler.js +20 -21
- package/dist/src/utils/logger.js +72 -65
- package/dist/src/utils/markdown-renderer.js +27 -27
- package/dist/src/utils/opencode-validator.js +63 -68
- package/dist/src/utils/token-manager.js +74 -45
- package/dist/tests/commands/chat.test.js +16 -25
- package/dist/tests/commands/code.test.js +95 -104
- package/dist/tests/utils/config-loader.test.js +48 -48
- package/dist/tests/utils/env-manager.test.js +43 -52
- package/dist/tests/utils/opencode-validator.test.js +22 -21
- package/dist/vitest.config.js +1 -1
- package/eslint.config.mjs +67 -0
- package/index.ts +35 -42
- package/package.json +30 -2
- package/src/agents/app.ts +27 -0
- package/src/agents/backend.ts +24 -0
- package/src/agents/devops.ts +33 -0
- package/src/agents/frontend.ts +24 -0
- package/src/agents/fullstack.ts +24 -0
- package/src/agents/index.ts +73 -0
- package/src/agents/quality.ts +69 -0
- package/src/agents/security.ts +26 -0
- package/src/agents/types.ts +17 -0
- package/src/client.ts +118 -152
- package/src/commands/api-keys.ts +241 -333
- package/src/commands/auth.ts +22 -27
- package/src/commands/autocomplete.ts +9 -9
- package/src/commands/billing.ts +20 -24
- package/src/commands/chat.ts +248 -338
- package/src/commands/clusters.ts +27 -26
- package/src/commands/code/__tests__/auth-sync.test.ts +482 -0
- package/src/commands/code/__tests__/fake-api-key-service.ts +13 -0
- package/src/commands/code/__tests__/fake-auth-service.ts +50 -0
- package/src/commands/code/__tests__/fake-command-runner.ts +45 -42
- package/src/commands/code/__tests__/fake-file-store.ts +32 -23
- package/src/commands/code/__tests__/fake-prompter.ts +116 -77
- package/src/commands/code/__tests__/setup-flow.test.ts +624 -268
- package/src/commands/code/adapters/clack-prompter.ts +53 -39
- package/src/commands/code/adapters/fs-file-store.ts +32 -27
- package/src/commands/code/adapters/spawn-command-runner.ts +38 -29
- package/src/commands/code/auth-sync.ts +329 -0
- package/src/commands/code/errors.ts +18 -18
- package/src/commands/code/ports/auth-services.ts +14 -0
- package/src/commands/code/ports/command-runner.ts +8 -4
- package/src/commands/code/ports/file-store.ts +5 -4
- package/src/commands/code/ports/prompter.ts +24 -18
- package/src/commands/code/setup.ts +570 -340
- package/src/commands/code.ts +338 -539
- package/src/commands/index.ts +20 -19
- package/src/commands/models.ts +28 -32
- package/src/commands/users.ts +15 -21
- package/src/constants/command-structure.ts +134 -157
- package/src/services/api-key-service.ts +105 -122
- package/src/services/auth-service.ts +99 -345
- package/src/services/browser-auth.ts +296 -0
- package/src/services/chat-service.ts +265 -299
- package/src/services/cluster-service.ts +42 -45
- package/src/services/collaborator-service.ts +14 -19
- package/src/services/flux-service.ts +23 -25
- package/src/services/helm-service.ts +19 -21
- package/src/services/kubectl-service.ts +17 -19
- package/src/types/api.d.ts +1905 -1907
- package/src/types/json.d.ts +2 -2
- package/src/utils/config-checker.ts +10 -10
- package/src/utils/config-loader.ts +162 -178
- package/src/utils/default-api-key.ts +114 -125
- package/src/utils/env-manager.ts +53 -57
- package/src/utils/error-handler.ts +61 -56
- package/src/utils/logger.ts +79 -73
- package/src/utils/markdown-renderer.ts +31 -31
- package/src/utils/opencode-validator.ts +85 -89
- package/src/utils/token-manager.ts +108 -87
- package/templates/agents/app.md +1 -0
- package/templates/agents/backend.md +1 -0
- package/templates/agents/devops.md +2 -0
- package/templates/agents/frontend.md +1 -0
- package/templates/agents/fullstack.md +1 -0
- package/templates/agents/quality.md +45 -40
- package/templates/agents/security.md +1 -0
- package/tests/commands/chat.test.ts +53 -62
- package/tests/commands/code.test.ts +265 -310
- package/tests/utils/config-loader.test.ts +189 -188
- package/tests/utils/env-manager.test.ts +110 -113
- package/tests/utils/opencode-validator.test.ts +52 -56
- package/tsconfig.json +4 -3
- package/vitest.config.ts +3 -3
- package/AGENTS.md +0 -374
- package/TODO.md +0 -19
|
@@ -1,111 +1,170 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
-
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
-
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
-
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
-
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
-
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
-
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
-
});
|
|
10
|
-
};
|
|
11
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
3
|
const vitest_1 = require("vitest");
|
|
13
|
-
const setup_1 = require("../setup");
|
|
14
4
|
const errors_1 = require("../errors");
|
|
15
|
-
const
|
|
16
|
-
const
|
|
5
|
+
const setup_1 = require("../setup");
|
|
6
|
+
const fake_api_key_service_1 = require("./fake-api-key-service");
|
|
7
|
+
const fake_auth_service_1 = require("./fake-auth-service");
|
|
17
8
|
const fake_command_runner_1 = require("./fake-command-runner");
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
9
|
+
const fake_file_store_1 = require("./fake-file-store");
|
|
10
|
+
const fake_prompter_1 = require("./fake-prompter");
|
|
11
|
+
const makeDeps = (overrides = {}) => {
|
|
12
|
+
return {
|
|
13
|
+
apiKeyService: overrides.apiKeyService ?? new fake_api_key_service_1.FakeApiKeyService('sk_ber_test'),
|
|
14
|
+
authService: overrides.authService ?? new fake_auth_service_1.FakeAuthService(false),
|
|
15
|
+
commands: overrides.commands ??
|
|
16
|
+
new fake_command_runner_1.FakeCommandRunner()
|
|
17
|
+
.handle('opencode --version', 'mocked')
|
|
18
|
+
.handle('pi --version', 'mocked'),
|
|
19
|
+
cwd: '/home/user/project',
|
|
20
|
+
files: overrides.files ?? new fake_file_store_1.FakeFileStore(),
|
|
21
|
+
homeDir: '/home/user',
|
|
22
|
+
prompter: overrides.prompter ?? new fake_prompter_1.FakePrompter([]),
|
|
23
|
+
...Object.fromEntries(Object.entries(overrides).filter(([k]) => k !== 'prompter' &&
|
|
24
|
+
k !== 'files' &&
|
|
25
|
+
k !== 'commands' &&
|
|
26
|
+
k !== 'authService' &&
|
|
27
|
+
k !== 'apiKeyService')),
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
function base64urlEncode(data) {
|
|
31
|
+
return Buffer.from(data).toString('base64url');
|
|
32
|
+
}
|
|
33
|
+
function makeJwt(payload) {
|
|
34
|
+
const header = base64urlEncode(JSON.stringify({ alg: 'none', typ: 'JWT' }));
|
|
35
|
+
const body = base64urlEncode(JSON.stringify(payload));
|
|
36
|
+
return `${header}.${body}.signature`;
|
|
37
|
+
}
|
|
21
38
|
(0, vitest_1.describe)('runSetup', () => {
|
|
22
39
|
(0, vitest_1.describe)('happy path', () => {
|
|
23
|
-
(0, vitest_1.it)('sets up opencode project without existing config', () =>
|
|
40
|
+
(0, vitest_1.it)('sets up opencode project without existing config', async () => {
|
|
24
41
|
const deps = makeDeps({
|
|
25
42
|
prompter: new fake_prompter_1.FakePrompter([
|
|
26
43
|
(0, fake_prompter_1.select)('opencode'),
|
|
27
44
|
(0, fake_prompter_1.select)('project'),
|
|
28
|
-
(0, fake_prompter_1.confirm)(true, 'Create'),
|
|
45
|
+
(0, fake_prompter_1.confirm)(true, 'Create'), // Config write
|
|
46
|
+
(0, fake_prompter_1.multiselect)([]), // No agents selected
|
|
29
47
|
]),
|
|
30
48
|
});
|
|
31
|
-
|
|
49
|
+
await (0, setup_1.runSetup)(deps);
|
|
32
50
|
const files = deps.files;
|
|
33
51
|
const written = files.getWrittenFiles();
|
|
34
52
|
(0, vitest_1.expect)(written.has('/home/user/project/opencode.json')).toBe(true);
|
|
35
53
|
const config = JSON.parse(written.get('/home/user/project/opencode.json'));
|
|
36
|
-
(0, vitest_1.expect)(config.plugin).toContain('@bergetai/opencode-auth
|
|
37
|
-
})
|
|
38
|
-
(0, vitest_1.it)('sets up opencode globally without existing config', () =>
|
|
54
|
+
(0, vitest_1.expect)(config.plugin).toContain('@bergetai/opencode-auth');
|
|
55
|
+
});
|
|
56
|
+
(0, vitest_1.it)('sets up opencode globally without existing config', async () => {
|
|
39
57
|
const deps = makeDeps({
|
|
40
58
|
prompter: new fake_prompter_1.FakePrompter([
|
|
41
59
|
(0, fake_prompter_1.select)('opencode'),
|
|
42
60
|
(0, fake_prompter_1.select)('global'),
|
|
43
|
-
(0, fake_prompter_1.confirm)(true, 'Create'),
|
|
61
|
+
(0, fake_prompter_1.confirm)(true, 'Create'), // Config write
|
|
62
|
+
(0, fake_prompter_1.multiselect)([]), // No agents selected
|
|
44
63
|
]),
|
|
45
64
|
});
|
|
46
|
-
|
|
65
|
+
await (0, setup_1.runSetup)(deps);
|
|
47
66
|
const files = deps.files;
|
|
48
67
|
const written = files.getWrittenFiles();
|
|
49
68
|
(0, vitest_1.expect)(written.has('/home/user/.config/opencode/opencode.json')).toBe(true);
|
|
50
|
-
})
|
|
51
|
-
(0, vitest_1.it)('sets up pi project with fresh install', () =>
|
|
69
|
+
});
|
|
70
|
+
(0, vitest_1.it)('sets up pi project with fresh install', async () => {
|
|
52
71
|
const deps = makeDeps({
|
|
72
|
+
commands: new fake_command_runner_1.FakeCommandRunner()
|
|
73
|
+
.handle('pi --version', 'mocked') // For checkInstalled
|
|
74
|
+
.handle('pi install', ''), // For actual install
|
|
53
75
|
prompter: new fake_prompter_1.FakePrompter([
|
|
54
76
|
(0, fake_prompter_1.select)('pi'),
|
|
55
77
|
(0, fake_prompter_1.select)('project'),
|
|
56
|
-
(0, fake_prompter_1.
|
|
78
|
+
(0, fake_prompter_1.select)('fullstack'), // Agent selection
|
|
79
|
+
(0, fake_prompter_1.confirm)(true, 'Create'),
|
|
57
80
|
]),
|
|
58
|
-
commands: new fake_command_runner_1.FakeCommandRunner()
|
|
59
|
-
.handle('pi --version', 'mocked') // For checkInstalled
|
|
60
|
-
.handle('pi install', ''), // For actual install
|
|
61
81
|
});
|
|
62
|
-
|
|
82
|
+
await (0, setup_1.runSetup)(deps);
|
|
63
83
|
const commands = deps.commands;
|
|
64
84
|
(0, vitest_1.expect)(commands.calls.length).toBeGreaterThan(0);
|
|
65
|
-
const installCall = commands.calls.find(c => c.command === 'pi');
|
|
66
|
-
(0, vitest_1.expect)(installCall
|
|
67
|
-
})
|
|
68
|
-
|
|
69
|
-
(0, vitest_1.describe)('prerequisites', () => {
|
|
70
|
-
(0, vitest_1.it)('throws PrerequisiteError when opencode is not installed', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
85
|
+
const installCall = commands.calls.find((c) => c.command === 'pi');
|
|
86
|
+
(0, vitest_1.expect)(installCall?.args).toContain('npm:@bergetai/pi-provider');
|
|
87
|
+
});
|
|
88
|
+
(0, vitest_1.it)('skips agent selection for pi project', async () => {
|
|
71
89
|
const deps = makeDeps({
|
|
90
|
+
commands: new fake_command_runner_1.FakeCommandRunner()
|
|
91
|
+
.handle('pi --version', 'mocked') // For checkInstalled
|
|
92
|
+
.handle('pi install', ''), // For actual install
|
|
72
93
|
prompter: new fake_prompter_1.FakePrompter([
|
|
73
|
-
(0, fake_prompter_1.select)('
|
|
94
|
+
(0, fake_prompter_1.select)('pi'),
|
|
74
95
|
(0, fake_prompter_1.select)('project'),
|
|
96
|
+
(0, fake_prompter_1.select)('__skip__'), // Skip agent selection
|
|
75
97
|
]),
|
|
98
|
+
});
|
|
99
|
+
await (0, setup_1.runSetup)(deps);
|
|
100
|
+
const files = deps.files;
|
|
101
|
+
const written = files.getWrittenFiles();
|
|
102
|
+
// Should not create any agent files
|
|
103
|
+
for (const path of written.keys()) {
|
|
104
|
+
(0, vitest_1.expect)(path).not.toContain('SYSTEM.md');
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
(0, vitest_1.describe)('prerequisites', () => {
|
|
109
|
+
(0, vitest_1.it)('throws PrerequisiteError when opencode is not installed', async () => {
|
|
110
|
+
const deps = makeDeps({
|
|
76
111
|
commands: new fake_command_runner_1.FakeCommandRunner(),
|
|
112
|
+
prompter: new fake_prompter_1.FakePrompter([(0, fake_prompter_1.select)('opencode'), (0, fake_prompter_1.select)('project')]),
|
|
77
113
|
});
|
|
78
114
|
// Simulate opencode not being installed
|
|
79
|
-
|
|
80
|
-
})
|
|
115
|
+
await (0, vitest_1.expect)((0, setup_1.runSetup)(deps)).rejects.toBeInstanceOf(errors_1.PrerequisiteError);
|
|
116
|
+
});
|
|
81
117
|
});
|
|
82
118
|
(0, vitest_1.describe)('cancellation', () => {
|
|
83
|
-
(0, vitest_1.it)('throws CancelledError when user cancels at tool selection', () =>
|
|
119
|
+
(0, vitest_1.it)('throws CancelledError when user cancels at tool selection', async () => {
|
|
120
|
+
const deps = makeDeps({
|
|
121
|
+
prompter: new fake_prompter_1.FakePrompter([(0, fake_prompter_1.select)(fake_prompter_1.CANCEL)]),
|
|
122
|
+
});
|
|
123
|
+
await (0, vitest_1.expect)((0, setup_1.runSetup)(deps)).rejects.toBeInstanceOf(errors_1.CancelledError);
|
|
124
|
+
});
|
|
125
|
+
(0, vitest_1.it)('throws CancelledError when user cancels at write confirmation', async () => {
|
|
84
126
|
const deps = makeDeps({
|
|
85
127
|
prompter: new fake_prompter_1.FakePrompter([
|
|
86
|
-
(0, fake_prompter_1.select)(
|
|
128
|
+
(0, fake_prompter_1.select)('opencode'),
|
|
129
|
+
(0, fake_prompter_1.select)('project'),
|
|
130
|
+
(0, fake_prompter_1.confirm)(false, 'Create'),
|
|
87
131
|
]),
|
|
88
132
|
});
|
|
89
|
-
|
|
90
|
-
})
|
|
91
|
-
(0, vitest_1.it)('throws CancelledError when user cancels at write confirmation
|
|
133
|
+
await (0, vitest_1.expect)((0, setup_1.runSetup)(deps)).rejects.toBeInstanceOf(errors_1.CancelledError);
|
|
134
|
+
});
|
|
135
|
+
(0, vitest_1.it)('throws CancelledError when user cancels at agent write confirmation (opencode)', async () => {
|
|
92
136
|
const deps = makeDeps({
|
|
93
137
|
prompter: new fake_prompter_1.FakePrompter([
|
|
94
138
|
(0, fake_prompter_1.select)('opencode'),
|
|
95
139
|
(0, fake_prompter_1.select)('project'),
|
|
96
|
-
(0, fake_prompter_1.confirm)(
|
|
140
|
+
(0, fake_prompter_1.confirm)(true, 'Create'),
|
|
141
|
+
(0, fake_prompter_1.multiselect)(['backend', 'frontend']),
|
|
142
|
+
(0, fake_prompter_1.confirm)(false, 'agent'),
|
|
143
|
+
]),
|
|
144
|
+
});
|
|
145
|
+
await (0, vitest_1.expect)((0, setup_1.runSetup)(deps)).rejects.toBeInstanceOf(errors_1.CancelledError);
|
|
146
|
+
});
|
|
147
|
+
(0, vitest_1.it)('throws CancelledError when user cancels at agent write confirmation (pi)', async () => {
|
|
148
|
+
const deps = makeDeps({
|
|
149
|
+
commands: new fake_command_runner_1.FakeCommandRunner().handle('pi --version', 'mocked').handle('pi install', ''),
|
|
150
|
+
prompter: new fake_prompter_1.FakePrompter([
|
|
151
|
+
(0, fake_prompter_1.select)('pi'),
|
|
152
|
+
(0, fake_prompter_1.select)('project'),
|
|
153
|
+
(0, fake_prompter_1.select)('fullstack'),
|
|
154
|
+
(0, fake_prompter_1.confirm)(false, /Create|Overwrite/),
|
|
97
155
|
]),
|
|
98
156
|
});
|
|
99
|
-
|
|
100
|
-
})
|
|
157
|
+
await (0, vitest_1.expect)((0, setup_1.runSetup)(deps)).rejects.toBeInstanceOf(errors_1.CancelledError);
|
|
158
|
+
});
|
|
101
159
|
});
|
|
102
160
|
(0, vitest_1.describe)('file operations', () => {
|
|
103
|
-
(0, vitest_1.it)('preserves existing configuration keys when updating', () =>
|
|
161
|
+
(0, vitest_1.it)('preserves existing configuration keys when updating', async () => {
|
|
104
162
|
const deps = makeDeps({
|
|
105
163
|
prompter: new fake_prompter_1.FakePrompter([
|
|
106
164
|
(0, fake_prompter_1.select)('opencode'),
|
|
107
165
|
(0, fake_prompter_1.select)('project'),
|
|
108
166
|
(0, fake_prompter_1.confirm)(true, 'Write'),
|
|
167
|
+
(0, fake_prompter_1.multiselect)([]),
|
|
109
168
|
]),
|
|
110
169
|
});
|
|
111
170
|
const files = deps.files;
|
|
@@ -113,19 +172,20 @@ const makeDeps = (overrides = {}) => (Object.assign({ prompter: new fake_prompte
|
|
|
113
172
|
customField: 'should-preserve',
|
|
114
173
|
plugin: ['other-plugin'],
|
|
115
174
|
}));
|
|
116
|
-
|
|
175
|
+
await (0, setup_1.runSetup)(deps);
|
|
117
176
|
const written = files.getWrittenFiles();
|
|
118
177
|
const config = JSON.parse(written.get('/home/user/project/opencode.json'));
|
|
119
178
|
(0, vitest_1.expect)(config.customField).toBe('should-preserve');
|
|
120
179
|
(0, vitest_1.expect)(config.plugin).toContain('other-plugin');
|
|
121
|
-
(0, vitest_1.expect)(config.plugin).toContain('@bergetai/opencode-auth
|
|
122
|
-
})
|
|
123
|
-
(0, vitest_1.it)('preserves jsonc comments when updating', () =>
|
|
180
|
+
(0, vitest_1.expect)(config.plugin).toContain('@bergetai/opencode-auth');
|
|
181
|
+
});
|
|
182
|
+
(0, vitest_1.it)('preserves jsonc comments when updating', async () => {
|
|
124
183
|
const deps = makeDeps({
|
|
125
184
|
prompter: new fake_prompter_1.FakePrompter([
|
|
126
185
|
(0, fake_prompter_1.select)('opencode'),
|
|
127
186
|
(0, fake_prompter_1.select)('project'),
|
|
128
187
|
(0, fake_prompter_1.confirm)(true, 'Write'),
|
|
188
|
+
(0, fake_prompter_1.multiselect)([]),
|
|
129
189
|
]),
|
|
130
190
|
});
|
|
131
191
|
const files = deps.files;
|
|
@@ -135,104 +195,311 @@ const makeDeps = (overrides = {}) => (Object.assign({ prompter: new fake_prompte
|
|
|
135
195
|
/* block comment explaining plugin */
|
|
136
196
|
"plugin": ["other-plugin"]
|
|
137
197
|
}`);
|
|
138
|
-
|
|
198
|
+
await (0, setup_1.runSetup)(deps);
|
|
139
199
|
const written = files.getWrittenFiles();
|
|
140
200
|
const content = written.get('/home/user/project/opencode.jsonc');
|
|
141
201
|
(0, vitest_1.expect)(content).toContain('// This is my custom config');
|
|
142
202
|
(0, vitest_1.expect)(content).toContain('/* block comment explaining plugin */');
|
|
143
203
|
(0, vitest_1.expect)(content).toContain('"customField": "should-preserve"');
|
|
144
|
-
(0, vitest_1.expect)(content).toContain('@bergetai/opencode-auth
|
|
145
|
-
})
|
|
146
|
-
(0, vitest_1.it)('shows no changes needed when config is already up to date', () =>
|
|
204
|
+
(0, vitest_1.expect)(content).toContain('@bergetai/opencode-auth');
|
|
205
|
+
});
|
|
206
|
+
(0, vitest_1.it)('shows no changes needed when config is already up to date', async () => {
|
|
147
207
|
const deps = makeDeps({
|
|
148
|
-
prompter: new fake_prompter_1.FakePrompter([
|
|
149
|
-
(0, fake_prompter_1.select)('opencode'),
|
|
150
|
-
(0, fake_prompter_1.select)('project'),
|
|
151
|
-
]),
|
|
208
|
+
prompter: new fake_prompter_1.FakePrompter([(0, fake_prompter_1.select)('opencode'), (0, fake_prompter_1.select)('project'), (0, fake_prompter_1.multiselect)([])]),
|
|
152
209
|
});
|
|
153
210
|
const files = deps.files;
|
|
154
211
|
// Already has the exact same plugin version
|
|
155
212
|
files.seed('/home/user/project/opencode.json', JSON.stringify({
|
|
156
213
|
$schema: 'https://opencode.ai/config.json',
|
|
157
|
-
plugin: ['@bergetai/opencode-auth
|
|
214
|
+
plugin: ['@bergetai/opencode-auth'],
|
|
158
215
|
}, null, 2) + '\n');
|
|
159
|
-
|
|
216
|
+
await (0, setup_1.runSetup)(deps);
|
|
160
217
|
// Check that no write happened — content should be unchanged
|
|
161
218
|
const written = files.getWrittenFiles();
|
|
162
219
|
const content = written.get('/home/user/project/opencode.json');
|
|
163
220
|
const config = JSON.parse(content);
|
|
164
|
-
(0, vitest_1.expect)(config.plugin).toEqual(['@bergetai/opencode-auth
|
|
221
|
+
(0, vitest_1.expect)(config.plugin).toEqual(['@bergetai/opencode-auth']);
|
|
165
222
|
(0, vitest_1.expect)(content).toContain('$schema');
|
|
166
|
-
})
|
|
167
|
-
(0, vitest_1.it)('preserves existing Pi settings when setting defaultProvider', () =>
|
|
223
|
+
});
|
|
224
|
+
(0, vitest_1.it)('preserves existing Pi settings when setting defaultProvider', async () => {
|
|
168
225
|
const deps = makeDeps({
|
|
226
|
+
commands: new fake_command_runner_1.FakeCommandRunner().handle('pi --version', 'mocked').handle('pi install', ''),
|
|
169
227
|
prompter: new fake_prompter_1.FakePrompter([
|
|
170
228
|
(0, fake_prompter_1.select)('pi'),
|
|
171
229
|
(0, fake_prompter_1.select)('project'),
|
|
172
|
-
(0, fake_prompter_1.
|
|
230
|
+
(0, fake_prompter_1.select)('fullstack'),
|
|
231
|
+
(0, fake_prompter_1.confirm)(true, 'Create'),
|
|
173
232
|
]),
|
|
174
|
-
commands: new fake_command_runner_1.FakeCommandRunner()
|
|
175
|
-
.handle('pi --version', 'mocked')
|
|
176
|
-
.handle('pi install', ''),
|
|
177
233
|
});
|
|
178
234
|
const files = deps.files;
|
|
179
235
|
files.seed('/home/user/project/.pi/settings.json', JSON.stringify({
|
|
180
|
-
existingKey: 'should-preserve',
|
|
181
236
|
anotherSetting: true,
|
|
237
|
+
existingKey: 'should-preserve',
|
|
182
238
|
}));
|
|
183
|
-
|
|
239
|
+
await (0, setup_1.runSetup)(deps);
|
|
184
240
|
const written = files.getWrittenFiles();
|
|
185
241
|
const settings = JSON.parse(written.get('/home/user/project/.pi/settings.json'));
|
|
186
242
|
(0, vitest_1.expect)(settings.existingKey).toBe('should-preserve');
|
|
187
243
|
(0, vitest_1.expect)(settings.anotherSetting).toBe(true);
|
|
188
244
|
(0, vitest_1.expect)(settings.defaultProvider).toBe('berget');
|
|
189
|
-
})
|
|
190
|
-
(0, vitest_1.it)('creates parent directories when writing files', () =>
|
|
245
|
+
});
|
|
246
|
+
(0, vitest_1.it)('creates parent directories when writing files', async () => {
|
|
191
247
|
const deps = makeDeps({
|
|
192
248
|
prompter: new fake_prompter_1.FakePrompter([
|
|
193
249
|
(0, fake_prompter_1.select)('opencode'),
|
|
194
250
|
(0, fake_prompter_1.select)('global'),
|
|
195
251
|
(0, fake_prompter_1.confirm)(true, 'Create'),
|
|
252
|
+
(0, fake_prompter_1.multiselect)([]),
|
|
196
253
|
]),
|
|
197
254
|
});
|
|
198
|
-
|
|
255
|
+
await (0, setup_1.runSetup)(deps);
|
|
199
256
|
const files = deps.files;
|
|
200
257
|
const written = files.getWrittenFiles();
|
|
201
258
|
(0, vitest_1.expect)(written.has('/home/user/.config/opencode/opencode.json')).toBe(true);
|
|
202
|
-
})
|
|
259
|
+
});
|
|
203
260
|
});
|
|
204
261
|
(0, vitest_1.describe)('command execution', () => {
|
|
205
|
-
(0, vitest_1.it)('passes arguments as array (no shell injection)', () =>
|
|
262
|
+
(0, vitest_1.it)('passes arguments as array (no shell injection)', async () => {
|
|
206
263
|
const deps = makeDeps({
|
|
264
|
+
commands: new fake_command_runner_1.FakeCommandRunner().handle('pi --version', 'mocked').handle('pi install', ''),
|
|
207
265
|
prompter: new fake_prompter_1.FakePrompter([
|
|
208
266
|
(0, fake_prompter_1.select)('pi'),
|
|
209
267
|
(0, fake_prompter_1.select)('project'),
|
|
210
|
-
(0, fake_prompter_1.
|
|
268
|
+
(0, fake_prompter_1.select)('fullstack'),
|
|
269
|
+
(0, fake_prompter_1.confirm)(true, 'Create'),
|
|
211
270
|
]),
|
|
212
|
-
commands: new fake_command_runner_1.FakeCommandRunner()
|
|
213
|
-
.handle('pi --version', 'mocked')
|
|
214
|
-
.handle('pi install', ''),
|
|
215
271
|
});
|
|
216
|
-
|
|
272
|
+
await (0, setup_1.runSetup)(deps);
|
|
217
273
|
const commands = deps.commands;
|
|
218
|
-
const installCall = commands.calls.find(c => c.command === 'pi');
|
|
219
|
-
(0, vitest_1.expect)(installCall
|
|
220
|
-
(0, vitest_1.expect)(installCall
|
|
221
|
-
})
|
|
274
|
+
const installCall = commands.calls.find((c) => c.command === 'pi');
|
|
275
|
+
(0, vitest_1.expect)(installCall?.args).toContain('npm:@bergetai/pi-provider');
|
|
276
|
+
(0, vitest_1.expect)(installCall?.args).toContain('-l');
|
|
277
|
+
});
|
|
222
278
|
});
|
|
223
279
|
(0, vitest_1.describe)('error handling', () => {
|
|
224
|
-
(0, vitest_1.it)('throws CommandFailedError when pi install fails', () =>
|
|
280
|
+
(0, vitest_1.it)('throws CommandFailedError when pi install fails', async () => {
|
|
225
281
|
const deps = makeDeps({
|
|
282
|
+
commands: new fake_command_runner_1.FakeCommandRunner()
|
|
283
|
+
.handle('pi --version', 'mocked')
|
|
284
|
+
.handle('pi install', new Error('npm error')),
|
|
285
|
+
prompter: new fake_prompter_1.FakePrompter([(0, fake_prompter_1.select)('pi'), (0, fake_prompter_1.select)('project')]),
|
|
286
|
+
});
|
|
287
|
+
await (0, vitest_1.expect)((0, setup_1.runSetup)(deps)).rejects.toBeInstanceOf(errors_1.CommandFailedError);
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
(0, vitest_1.describe)('auth integration', () => {
|
|
291
|
+
(0, vitest_1.it)('already authenticated shows simplified message', async () => {
|
|
292
|
+
const files = new fake_file_store_1.FakeFileStore();
|
|
293
|
+
files.seed('/home/user/.local/share/opencode/auth.json', JSON.stringify({ berget: { type: 'oauth' } }));
|
|
294
|
+
const deps = makeDeps({
|
|
295
|
+
files,
|
|
296
|
+
prompter: new fake_prompter_1.FakePrompter([
|
|
297
|
+
(0, fake_prompter_1.select)('opencode'),
|
|
298
|
+
(0, fake_prompter_1.select)('project'),
|
|
299
|
+
(0, fake_prompter_1.select)('keep'), // New: keep existing auth
|
|
300
|
+
(0, fake_prompter_1.confirm)(true, 'Create'), // Config write
|
|
301
|
+
(0, fake_prompter_1.multiselect)([]),
|
|
302
|
+
]),
|
|
303
|
+
});
|
|
304
|
+
await (0, setup_1.runSetup)(deps);
|
|
305
|
+
const prompter = deps.prompter;
|
|
306
|
+
const notes = prompter.calls.filter((c) => c.method === 'note');
|
|
307
|
+
const lastNote = notes.at(-1);
|
|
308
|
+
(0, vitest_1.expect)(JSON.stringify(lastNote)).toContain('Run: opencode');
|
|
309
|
+
(0, vitest_1.expect)(JSON.stringify(lastNote)).not.toContain('/connect');
|
|
310
|
+
});
|
|
311
|
+
(0, vitest_1.it)('login failure shows manual auth instructions', async () => {
|
|
312
|
+
const deps = makeDeps({
|
|
313
|
+
authService: new fake_auth_service_1.FakeAuthService(false),
|
|
314
|
+
commands: new fake_command_runner_1.FakeCommandRunner().handle('pi --version', 'mocked').handle('pi install', ''),
|
|
315
|
+
files: new fake_file_store_1.FakeFileStore(), // No pre-seeded auth → auth flow runs
|
|
226
316
|
prompter: new fake_prompter_1.FakePrompter([
|
|
227
317
|
(0, fake_prompter_1.select)('pi'),
|
|
228
318
|
(0, fake_prompter_1.select)('project'),
|
|
229
|
-
(0, fake_prompter_1.
|
|
319
|
+
(0, fake_prompter_1.select)('fullstack'),
|
|
320
|
+
(0, fake_prompter_1.confirm)(true, 'Create'),
|
|
230
321
|
]),
|
|
231
|
-
commands: new fake_command_runner_1.FakeCommandRunner()
|
|
232
|
-
.handle('pi --version', 'mocked')
|
|
233
|
-
.handle('pi install', new Error('npm error')),
|
|
234
322
|
});
|
|
235
|
-
|
|
236
|
-
|
|
323
|
+
await (0, setup_1.runSetup)(deps);
|
|
324
|
+
const prompter = deps.prompter;
|
|
325
|
+
const notes = prompter.calls.filter((c) => c.method === 'note');
|
|
326
|
+
const lastNote = notes.at(-1);
|
|
327
|
+
(0, vitest_1.expect)(JSON.stringify(lastNote)).toContain('/login');
|
|
328
|
+
});
|
|
329
|
+
(0, vitest_1.it)('creates api key for pi when no seat', async () => {
|
|
330
|
+
const files = new fake_file_store_1.FakeFileStore();
|
|
331
|
+
const deps = makeDeps({
|
|
332
|
+
authService: new fake_auth_service_1.FakeAuthService(true, false), // succeed, no seat
|
|
333
|
+
commands: new fake_command_runner_1.FakeCommandRunner().handle('pi --version', 'mocked').handle('pi install', ''),
|
|
334
|
+
files,
|
|
335
|
+
prompter: new fake_prompter_1.FakePrompter([
|
|
336
|
+
(0, fake_prompter_1.select)('pi'),
|
|
337
|
+
(0, fake_prompter_1.select)('project'),
|
|
338
|
+
(0, fake_prompter_1.confirm)(true), // API key creation prompt
|
|
339
|
+
(0, fake_prompter_1.select)('fullstack'),
|
|
340
|
+
(0, fake_prompter_1.confirm)(true, 'Create'),
|
|
341
|
+
]),
|
|
342
|
+
});
|
|
343
|
+
await (0, setup_1.runSetup)(deps);
|
|
344
|
+
const written = files.getWrittenFiles();
|
|
345
|
+
(0, vitest_1.expect)(written.has('/home/user/.pi/agent/auth.json')).toBe(true);
|
|
346
|
+
const parsed = JSON.parse(written.get('/home/user/.pi/agent/auth.json'));
|
|
347
|
+
(0, vitest_1.expect)(parsed.berget.type).toBe('api_key');
|
|
348
|
+
});
|
|
349
|
+
(0, vitest_1.it)('uses subscription when berget_code_seat present', async () => {
|
|
350
|
+
const files = new fake_file_store_1.FakeFileStore();
|
|
351
|
+
const farFuture = Math.floor(Date.now() / 1000) + 3600 * 24 * 365; // 1 year from now in seconds
|
|
352
|
+
files.seed('/home/user/.berget/auth.json', JSON.stringify({
|
|
353
|
+
access_token: makeJwt({ exp: farFuture, realm_access: { roles: ['berget_code_seat'] } }),
|
|
354
|
+
expires_at: farFuture * 1000,
|
|
355
|
+
refresh_token: 'ref',
|
|
356
|
+
}));
|
|
357
|
+
const deps = makeDeps({
|
|
358
|
+
files,
|
|
359
|
+
prompter: new fake_prompter_1.FakePrompter([
|
|
360
|
+
(0, fake_prompter_1.select)('opencode'),
|
|
361
|
+
(0, fake_prompter_1.select)('project'),
|
|
362
|
+
(0, fake_prompter_1.select)('subscription'),
|
|
363
|
+
(0, fake_prompter_1.confirm)(true, 'Create'),
|
|
364
|
+
(0, fake_prompter_1.multiselect)([]),
|
|
365
|
+
]),
|
|
366
|
+
});
|
|
367
|
+
await (0, setup_1.runSetup)(deps);
|
|
368
|
+
const written = files.getWrittenFiles();
|
|
369
|
+
const parsed = JSON.parse(written.get('/home/user/.local/share/opencode/auth.json'));
|
|
370
|
+
(0, vitest_1.expect)(parsed.berget.type).toBe('oauth');
|
|
371
|
+
});
|
|
372
|
+
});
|
|
373
|
+
(0, vitest_1.describe)('agent configuration', () => {
|
|
374
|
+
(0, vitest_1.it)('sets up multiple agents for opencode project', async () => {
|
|
375
|
+
const deps = makeDeps({
|
|
376
|
+
prompter: new fake_prompter_1.FakePrompter([
|
|
377
|
+
(0, fake_prompter_1.select)('opencode'),
|
|
378
|
+
(0, fake_prompter_1.select)('project'),
|
|
379
|
+
(0, fake_prompter_1.confirm)(true, 'Create'),
|
|
380
|
+
(0, fake_prompter_1.multiselect)(['backend', 'frontend']),
|
|
381
|
+
(0, fake_prompter_1.confirm)(true, 'agent'),
|
|
382
|
+
]),
|
|
383
|
+
});
|
|
384
|
+
await (0, setup_1.runSetup)(deps);
|
|
385
|
+
const files = deps.files;
|
|
386
|
+
const written = files.getWrittenFiles();
|
|
387
|
+
(0, vitest_1.expect)(written.has('/home/user/project/.opencode/agents/backend.md')).toBe(true);
|
|
388
|
+
(0, vitest_1.expect)(written.has('/home/user/project/.opencode/agents/frontend.md')).toBe(true);
|
|
389
|
+
});
|
|
390
|
+
(0, vitest_1.it)('sets up no agents for opencode when none selected', async () => {
|
|
391
|
+
const deps = makeDeps({
|
|
392
|
+
prompter: new fake_prompter_1.FakePrompter([
|
|
393
|
+
(0, fake_prompter_1.select)('opencode'),
|
|
394
|
+
(0, fake_prompter_1.select)('project'),
|
|
395
|
+
(0, fake_prompter_1.confirm)(true, 'Create'),
|
|
396
|
+
(0, fake_prompter_1.multiselect)([]),
|
|
397
|
+
]),
|
|
398
|
+
});
|
|
399
|
+
await (0, setup_1.runSetup)(deps);
|
|
400
|
+
const files = deps.files;
|
|
401
|
+
const written = files.getWrittenFiles();
|
|
402
|
+
for (const path of written.keys()) {
|
|
403
|
+
(0, vitest_1.expect)(path).not.toMatch(/agents\/\w+\.md$/);
|
|
404
|
+
}
|
|
405
|
+
});
|
|
406
|
+
(0, vitest_1.it)('sets up agent globally for opencode', async () => {
|
|
407
|
+
const deps = makeDeps({
|
|
408
|
+
prompter: new fake_prompter_1.FakePrompter([
|
|
409
|
+
(0, fake_prompter_1.select)('opencode'),
|
|
410
|
+
(0, fake_prompter_1.select)('global'),
|
|
411
|
+
(0, fake_prompter_1.confirm)(true, 'Create'),
|
|
412
|
+
(0, fake_prompter_1.multiselect)(['fullstack']),
|
|
413
|
+
(0, fake_prompter_1.confirm)(true, 'agent'),
|
|
414
|
+
]),
|
|
415
|
+
});
|
|
416
|
+
await (0, setup_1.runSetup)(deps);
|
|
417
|
+
const files = deps.files;
|
|
418
|
+
const written = files.getWrittenFiles();
|
|
419
|
+
(0, vitest_1.expect)(written.has('/home/user/.config/opencode/agents/fullstack.md')).toBe(true);
|
|
420
|
+
});
|
|
421
|
+
(0, vitest_1.it)('sets up agent for pi project', async () => {
|
|
422
|
+
const deps = makeDeps({
|
|
423
|
+
commands: new fake_command_runner_1.FakeCommandRunner().handle('pi --version', 'mocked').handle('pi install', ''),
|
|
424
|
+
prompter: new fake_prompter_1.FakePrompter([
|
|
425
|
+
(0, fake_prompter_1.select)('pi'),
|
|
426
|
+
(0, fake_prompter_1.select)('project'),
|
|
427
|
+
(0, fake_prompter_1.select)('fullstack'),
|
|
428
|
+
(0, fake_prompter_1.confirm)(true, 'Create'),
|
|
429
|
+
]),
|
|
430
|
+
});
|
|
431
|
+
await (0, setup_1.runSetup)(deps);
|
|
432
|
+
const files = deps.files;
|
|
433
|
+
const written = files.getWrittenFiles();
|
|
434
|
+
(0, vitest_1.expect)(written.has('/home/user/project/.pi/SYSTEM.md')).toBe(true);
|
|
435
|
+
});
|
|
436
|
+
(0, vitest_1.it)('sets up agent for pi globally', async () => {
|
|
437
|
+
const deps = makeDeps({
|
|
438
|
+
commands: new fake_command_runner_1.FakeCommandRunner().handle('pi --version', 'mocked').handle('pi install', ''),
|
|
439
|
+
prompter: new fake_prompter_1.FakePrompter([
|
|
440
|
+
(0, fake_prompter_1.select)('pi'),
|
|
441
|
+
(0, fake_prompter_1.select)('global'),
|
|
442
|
+
(0, fake_prompter_1.select)('backend'),
|
|
443
|
+
(0, fake_prompter_1.confirm)(true, 'Create'),
|
|
444
|
+
]),
|
|
445
|
+
});
|
|
446
|
+
await (0, setup_1.runSetup)(deps);
|
|
447
|
+
const files = deps.files;
|
|
448
|
+
const written = files.getWrittenFiles();
|
|
449
|
+
(0, vitest_1.expect)(written.has('/home/user/.pi/agent/SYSTEM.md')).toBe(true);
|
|
450
|
+
});
|
|
451
|
+
(0, vitest_1.it)('skips writing identical opencode agent files', async () => {
|
|
452
|
+
const deps = makeDeps({
|
|
453
|
+
prompter: new fake_prompter_1.FakePrompter([
|
|
454
|
+
(0, fake_prompter_1.select)('opencode'),
|
|
455
|
+
(0, fake_prompter_1.select)('project'),
|
|
456
|
+
(0, fake_prompter_1.confirm)(true, 'Create'),
|
|
457
|
+
(0, fake_prompter_1.multiselect)(['backend', 'frontend']),
|
|
458
|
+
(0, fake_prompter_1.confirm)(true, 'agent'),
|
|
459
|
+
]),
|
|
460
|
+
});
|
|
461
|
+
// First run writes the files
|
|
462
|
+
await (0, setup_1.runSetup)(deps);
|
|
463
|
+
const files = deps.files;
|
|
464
|
+
const firstBackend = files
|
|
465
|
+
.getWrittenFiles()
|
|
466
|
+
.get('/home/user/project/.opencode/agents/backend.md');
|
|
467
|
+
const firstFrontend = files
|
|
468
|
+
.getWrittenFiles()
|
|
469
|
+
.get('/home/user/project/.opencode/agents/frontend.md');
|
|
470
|
+
// Second run with exact same content should not prompt for overwrite
|
|
471
|
+
const deps2 = makeDeps({
|
|
472
|
+
files,
|
|
473
|
+
prompter: new fake_prompter_1.FakePrompter([
|
|
474
|
+
(0, fake_prompter_1.select)('opencode'),
|
|
475
|
+
(0, fake_prompter_1.select)('project'),
|
|
476
|
+
(0, fake_prompter_1.multiselect)(['backend', 'frontend']),
|
|
477
|
+
]),
|
|
478
|
+
});
|
|
479
|
+
await (0, setup_1.runSetup)(deps2);
|
|
480
|
+
// Content should be unchanged
|
|
481
|
+
(0, vitest_1.expect)(files.getWrittenFiles().get('/home/user/project/.opencode/agents/backend.md')).toBe(firstBackend);
|
|
482
|
+
(0, vitest_1.expect)(files.getWrittenFiles().get('/home/user/project/.opencode/agents/frontend.md')).toBe(firstFrontend);
|
|
483
|
+
});
|
|
484
|
+
(0, vitest_1.it)('overwrites pi SYSTEM.md when content differs', async () => {
|
|
485
|
+
const files = new fake_file_store_1.FakeFileStore();
|
|
486
|
+
files.seed('/home/user/project/.pi/SYSTEM.md', 'old agent content');
|
|
487
|
+
const deps = makeDeps({
|
|
488
|
+
commands: new fake_command_runner_1.FakeCommandRunner().handle('pi --version', 'mocked').handle('pi install', ''),
|
|
489
|
+
files,
|
|
490
|
+
prompter: new fake_prompter_1.FakePrompter([
|
|
491
|
+
(0, fake_prompter_1.select)('pi'),
|
|
492
|
+
(0, fake_prompter_1.select)('project'),
|
|
493
|
+
(0, fake_prompter_1.select)('fullstack'),
|
|
494
|
+
(0, fake_prompter_1.confirm)(true, 'Overwrite'),
|
|
495
|
+
]),
|
|
496
|
+
});
|
|
497
|
+
await (0, setup_1.runSetup)(deps);
|
|
498
|
+
const written = files.getWrittenFiles();
|
|
499
|
+
const content = written.get('/home/user/project/.pi/SYSTEM.md');
|
|
500
|
+
(0, vitest_1.expect)(content).not.toBe('old agent content');
|
|
501
|
+
// Pi doesn't use front matter, so check for system prompt content
|
|
502
|
+
(0, vitest_1.expect)(content).toContain('Fullstack Agent');
|
|
503
|
+
});
|
|
237
504
|
});
|
|
238
505
|
});
|