testronaut 1.1.1 β 1.2.0
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/CONTRIBUTING.md +194 -0
- package/bin/cli.js +1 -1
- package/bin/init.js +141 -57
- package/bin/initHelpers.js +140 -0
- package/core/agent.js +1 -1
- package/core/turnIntent.js +51 -20
- package/core/turnLoop.js +172 -105
- package/llm/gemini/geminiProvider.js +188 -0
- package/llm/llmFactory.js +50 -0
- package/llm/modelResolver.js +65 -0
- package/llm/openAI/openaiProvider.js +54 -0
- package/package.json +9 -3
- package/tests/binTests/init.test.js +189 -0
- package/tests/binTests/initHelpersTests/defaultConfig.test.js +12 -0
- package/tests/binTests/initHelpersTests/makeEnvTemplate.test.js +14 -0
- package/tests/binTests/initHelpersTests/migrateLegacyConfig.test.js +19 -0
- package/tests/binTests/initHelpersTests/models.test.js +21 -0
- package/tests/coreTests/turnLoop.test.js +177 -0
- package/tests/llmTests/geminiProvider.test.js +191 -0
- package/tests/llmTests/llmFactory.test.js +78 -0
- package/tests/llmTests/modelResolver.test.js +119 -0
- package/tests/llmTests/openAIProvider.test.js +117 -0
- package/tests/toolsTests/tokenControl.test.js +150 -0
- package/tools/generateHtmlReport.js +83 -0
- package/tools/tokenControl.js +107 -17
- package/vitest.config.js +8 -0
- package/openAI/modelResolver.js +0 -33
package/CONTRIBUTING.md
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# Contributing to π§βπ Testronaut
|
|
2
|
+
|
|
3
|
+
Thanks for your interest in contributing to π§βπ **Testronaut**! π
|
|
4
|
+
This document outlines how to set up your environment, run tests, and write code and documentation that align with the projectβs standards.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## π§ Project Overview
|
|
9
|
+
|
|
10
|
+
π§βπ **Testronaut** is an open-source CLI for autonomous end-to-end testing.
|
|
11
|
+
It provides mission-driven testing flows, LLM provider integrations (OpenAI, Gemini, etc.), and a growing ecosystem of helpers and automation tools.
|
|
12
|
+
|
|
13
|
+
This guide applies to all contributions β whether you're fixing a bug, improving documentation, or adding new features.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## π§© Local Development Setup
|
|
18
|
+
|
|
19
|
+
1. **Clone the repository**
|
|
20
|
+
```bash
|
|
21
|
+
git clone https://github.com/<your-org-or-user>/testronaut.git
|
|
22
|
+
cd testronaut
|
|
23
|
+
```
|
|
24
|
+
Install dependencies
|
|
25
|
+
```bash
|
|
26
|
+
npm install
|
|
27
|
+
```
|
|
28
|
+
Run the CLI locally
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
node ./bin/testronaut.js init
|
|
32
|
+
```
|
|
33
|
+
Run tests
|
|
34
|
+
```bash
|
|
35
|
+
npm test
|
|
36
|
+
# or watch mode
|
|
37
|
+
npm run test:watch
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
## π§ͺ Testing Guidelines
|
|
42
|
+
We use [Vitest](https://vitest.dev/) for unit testing.
|
|
43
|
+
|
|
44
|
+
### Test Structure
|
|
45
|
+
- Unit tests β tests/initHelpersTests/ and similar subfolders
|
|
46
|
+
- Pure, no side effects
|
|
47
|
+
- Target helpers and utilities directly
|
|
48
|
+
- Fast and deterministic
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
### Commands
|
|
53
|
+
```bash
|
|
54
|
+
# Run all tests
|
|
55
|
+
npm test
|
|
56
|
+
|
|
57
|
+
# Watch mode
|
|
58
|
+
npm run test:watch
|
|
59
|
+
|
|
60
|
+
# Coverage report
|
|
61
|
+
npx vitest run --coverage
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Principles
|
|
65
|
+
- Each helper or core module should have at least one test file.
|
|
66
|
+
- Tests should verify behavior, not implementation details.
|
|
67
|
+
- Prefer fixtures over mocks when possible.
|
|
68
|
+
|
|
69
|
+
## π¬ Comment & Documentation Standards
|
|
70
|
+
Testronaut uses structured, meaningful comments to make the codebase self-documenting.
|
|
71
|
+
|
|
72
|
+
### 1. File Headers
|
|
73
|
+
Every source file should start with a doc header that summarizes:
|
|
74
|
+
|
|
75
|
+
- Purpose: What the file is responsible for
|
|
76
|
+
- Responsibilities: Major actions or logic areas
|
|
77
|
+
- Side effects: I/O or network operations
|
|
78
|
+
- Related tests: Folder or suite where its tests live
|
|
79
|
+
- Used by: Other modules that import it
|
|
80
|
+
|
|
81
|
+
Example:
|
|
82
|
+
```js
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* ExampleModule.js
|
|
86
|
+
* ----------------
|
|
87
|
+
* Purpose:
|
|
88
|
+
* Handles mission parsing and validation for CLI runs.
|
|
89
|
+
*
|
|
90
|
+
* Design goals:
|
|
91
|
+
* - Pure functions where possible
|
|
92
|
+
* - Unit-tested via Vitest
|
|
93
|
+
*
|
|
94
|
+
* Related tests:
|
|
95
|
+
* Located in `tests/exampleModuleTests/`
|
|
96
|
+
*
|
|
97
|
+
* Used by:
|
|
98
|
+
* ./cli/runMission.js
|
|
99
|
+
*/
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### 2. Inline Comments
|
|
103
|
+
- Focus on why rather than what the code is doing.
|
|
104
|
+
- Use NOTE: or TODO: prefixes to highlight rationale or future work.
|
|
105
|
+
- Avoid restating code syntax (β// read fileβ).
|
|
106
|
+
|
|
107
|
+
Example:
|
|
108
|
+
```js
|
|
109
|
+
// NOTE: We skip writing .env if it already exists to avoid overwriting secrets.
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### 3. Section Headers
|
|
113
|
+
Use clear dividers for major logical steps in longer scripts (e.g., CLI commands):
|
|
114
|
+
```js
|
|
115
|
+
// βββββββββββββββββββββββββββββ
|
|
116
|
+
// STEP 3: Merge defaults safely
|
|
117
|
+
// βββββββββββββββββββββββββββββ
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### 4. Function Documentation (JSDoc)
|
|
121
|
+
Add JSDoc blocks for all exported functions and for internal ones with nontrivial logic:
|
|
122
|
+
```js
|
|
123
|
+
/**
|
|
124
|
+
* Generates default configuration for new projects.
|
|
125
|
+
*
|
|
126
|
+
* @param {string} rootBasename - The current directory name.
|
|
127
|
+
* @returns {{initialized: boolean, outputDir: string}}
|
|
128
|
+
*/
|
|
129
|
+
export function defaultConfig(rootBasename) { ... }
|
|
130
|
+
```
|
|
131
|
+
These comments improve IDE autocompletion and make code self-descriptive.
|
|
132
|
+
|
|
133
|
+
### 5. Test References
|
|
134
|
+
If a file has full coverage:
|
|
135
|
+
- Add one Related tests: line at the top (e.g., tests/initHelpersTests/).
|
|
136
|
+
- Donβt link to individual test files unless the logic is unusually complex.
|
|
137
|
+
|
|
138
|
+
## π§± Code Style
|
|
139
|
+
- Language: Modern JavaScript (ES Modules only)
|
|
140
|
+
- Linting: Use ESLint + Prettier (follow repo config)
|
|
141
|
+
- Imports: Order as β Node built-ins β Third-party β Local
|
|
142
|
+
- Async: Prefer async/await over callbacks
|
|
143
|
+
- File Names: Use camelCase for utilities, PascalCase for React components (if applicable)
|
|
144
|
+
|
|
145
|
+
## π§° Folder Conventions
|
|
146
|
+
| Folder | Purpose |
|
|
147
|
+
|---|---|
|
|
148
|
+
/bin/ | CLI logic, utilities, helpers
|
|
149
|
+
/core/ | central functionality, turn loop, and agent management
|
|
150
|
+
/tests/ | All Vitest unit tests
|
|
151
|
+
/runner/ | Mission runner and suite management
|
|
152
|
+
/openAI/ | OpenAI specific helper functions
|
|
153
|
+
|
|
154
|
+
## βοΈ Submitting Changes
|
|
155
|
+
1. Create a new branch
|
|
156
|
+
```bash
|
|
157
|
+
git checkout -b feat/new-provider
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
2. Make your changes
|
|
161
|
+
- Keep commits focused and descriptive
|
|
162
|
+
- Follow documentation and testing standards
|
|
163
|
+
|
|
164
|
+
3. Run
|
|
165
|
+
```bash
|
|
166
|
+
npm test
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
4. Open a Pull Request
|
|
170
|
+
- Reference related issues if applicable
|
|
171
|
+
- Summarize what was added, fixed, or refactored
|
|
172
|
+
|
|
173
|
+
## π§βπ» Example of a βGoodβ PR Description
|
|
174
|
+
> ### Summary
|
|
175
|
+
> Adds Gemini provider support to the CLI init flow.
|
|
176
|
+
> Includes unit tests for new helper functions in initHelpers.js.
|
|
177
|
+
>
|
|
178
|
+
> ### Details
|
|
179
|
+
> - Added new prompt for selecting Gemini model
|
|
180
|
+
> - Updated .env scaffold to include GEMINI_API_KEY
|
|
181
|
+
> - Added tests in tests/initHelpersTests/makeEnvTemplate.test.js
|
|
182
|
+
>
|
|
183
|
+
> ### Checklist
|
|
184
|
+
> - Unit tests pass (npm test)
|
|
185
|
+
> - Comments and headers added
|
|
186
|
+
> - No regressions in CLI behavior
|
|
187
|
+
|
|
188
|
+
## π€ Code of Conduct
|
|
189
|
+
We expect all contributors to follow the Code of Conduct.
|
|
190
|
+
Be respectful, collaborative, and kind β weβre all here to build something great together.
|
|
191
|
+
|
|
192
|
+
## π§‘ Thank You
|
|
193
|
+
Your contributions make π§βπ Testronaut better for everyone β from developers automating their first mission to teams running autonomous test fleets.
|
|
194
|
+
We deeply appreciate your time, effort, and ideas.
|
package/bin/cli.js
CHANGED
package/bin/init.js
CHANGED
|
@@ -1,118 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Testronaut CLI Initialization
|
|
3
|
+
* -----------------------------
|
|
4
|
+
* Purpose:
|
|
5
|
+
* Bootstraps a new Testronaut project by creating required folders,
|
|
6
|
+
* writing a provider/model-aware config, and scaffolding a .env file.
|
|
7
|
+
*
|
|
8
|
+
* Responsibilities:
|
|
9
|
+
* 1) Ask the user for an LLM provider (OpenAI or Gemini).
|
|
10
|
+
* 2) Ask for a provider-specific model (keeps prior choice when re-run).
|
|
11
|
+
* 3) Write `testronaut-config.json` and an initial `.env` placeholder.
|
|
12
|
+
* 4) Ensure folder structure and create a welcome mission.
|
|
13
|
+
*
|
|
14
|
+
* Side effects:
|
|
15
|
+
* - Writes files to disk: `testronaut-config.json`, `.env`,
|
|
16
|
+
* `missions/`, `missions/mission_reports/`.
|
|
17
|
+
* - Calls `createWelcomeMission()` on first init.
|
|
18
|
+
*
|
|
19
|
+
* Developer notes:
|
|
20
|
+
* - Extend provider support by adding a new provider choice here and
|
|
21
|
+
* updating `initHelpers.js` (model list + template).
|
|
22
|
+
* - Keep this file orchestration-only; pure logic lives in helpers
|
|
23
|
+
* (which are unit-tested).
|
|
24
|
+
*/
|
|
25
|
+
|
|
1
26
|
import fs from 'fs';
|
|
2
27
|
import path from 'path';
|
|
3
28
|
import prompts from 'prompts';
|
|
4
29
|
import { createWelcomeMission } from './createWelcomeMission.js';
|
|
5
30
|
|
|
31
|
+
import {
|
|
32
|
+
migrateLegacyConfig,
|
|
33
|
+
defaultConfig,
|
|
34
|
+
makeEnvTemplate,
|
|
35
|
+
openAIModels,
|
|
36
|
+
geminiModels,
|
|
37
|
+
pickInitialIndex
|
|
38
|
+
} from './initHelpers.js';
|
|
39
|
+
|
|
6
40
|
export async function initializeTestronautProject() {
|
|
41
|
+
// βββββββββββββββββββββββββββββββββββββββββββββ
|
|
42
|
+
// STEP 0: Resolve important paths
|
|
43
|
+
// βββββββββββββββββββββββββββββββββββββββββββββ
|
|
7
44
|
const root = process.cwd();
|
|
8
45
|
const missionsDir = path.join(root, 'missions');
|
|
9
46
|
const reportsDir = path.join(missionsDir, 'mission_reports');
|
|
10
47
|
const configPath = path.join(root, 'testronaut-config.json');
|
|
11
48
|
const envPath = path.join(root, '.env');
|
|
12
49
|
|
|
50
|
+
// βββββββββββββββββββββββββββββββββββββββββββββ
|
|
51
|
+
// STEP 1: Load existing config (if any) and normalize
|
|
52
|
+
// βββββββββββββββββββββββββββββββββββββββββββββ
|
|
53
|
+
/** @type {any} */
|
|
13
54
|
let config = {};
|
|
14
55
|
if (fs.existsSync(configPath)) {
|
|
15
56
|
config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
if (!config.provider && config.model === 'openai') {
|
|
19
|
-
config.provider = 'openai';
|
|
20
|
-
// don't overwrite model yet; we'll re-ask below only if not initialized
|
|
21
|
-
}
|
|
57
|
+
// Normalize legacy structures (e.g., provider implied by model).
|
|
58
|
+
config = migrateLegacyConfig(config);
|
|
22
59
|
}
|
|
23
60
|
|
|
24
|
-
//
|
|
61
|
+
// βββββββββββββββββββββββββββββββββββββββββββββ
|
|
62
|
+
// STEP 2: Interactive setup (first run only)
|
|
63
|
+
// βββββββββββββββββββββββββββββββββββββββββββββ
|
|
25
64
|
if (!config.initialized) {
|
|
26
|
-
//
|
|
65
|
+
// 2a) Provider selection
|
|
27
66
|
const { llmProvider } = await prompts({
|
|
28
67
|
type: 'select',
|
|
29
68
|
name: 'llmProvider',
|
|
30
69
|
message: 'Which LLM provider would you like to use?',
|
|
31
70
|
choices: [
|
|
32
71
|
{ title: 'OpenAI (GPT family)', value: 'openai' },
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
config.provider = llmProvider; // store provider explicitly
|
|
38
|
-
|
|
39
|
-
// 2) If OpenAI, ask for model
|
|
40
|
-
const { openaiModel } = await prompts({
|
|
41
|
-
type: 'select',
|
|
42
|
-
name: 'openaiModel',
|
|
43
|
-
message: 'Select an OpenAI model for agentic workflows (function/tool calling):',
|
|
44
|
-
choices: [
|
|
45
|
-
{ title: 'GPT-4.1 (general purpose)', value: 'gpt-4.1' },
|
|
46
|
-
{ title: 'GPT-4.1 mini (faster, cheaper)', value: 'gpt-4.1-mini' },
|
|
47
|
-
{ title: 'GPT-4o (multimodal, tool use)', value: 'gpt-4o' },
|
|
48
|
-
{ title: 'GPT-4o mini (speed/cost optimized)', value: 'gpt-4o-mini' },
|
|
49
|
-
{ title: 'o3 (reasoning w/ native tool use)', value: 'o3' },
|
|
50
|
-
{ title: 'o4-mini (reasoning, cost-effective)', value: 'o4-mini' },
|
|
51
|
-
|
|
52
|
-
// π GPT-5 family
|
|
53
|
-
{ title: 'GPT-5 (latest reasoning model)', value: 'gpt-5' },
|
|
54
|
-
{ title: 'GPT-5 mini (faster)', value: 'gpt-5-mini' },
|
|
55
|
-
{ title: 'GPT-5 nano (lightweight)', value: 'gpt-5-nano' },
|
|
72
|
+
{ title: 'Google Gemini', value: 'gemini' },
|
|
73
|
+
// Future: add Anthropic, Mistral, etc.
|
|
56
74
|
],
|
|
57
|
-
|
|
75
|
+
// If user re-runs init before writing config, preselect known value.
|
|
76
|
+
initial: ['openai', 'gemini'].indexOf(config.provider ?? 'openai')
|
|
58
77
|
});
|
|
59
78
|
|
|
60
|
-
config.
|
|
79
|
+
// Persist provider explicitly to config.
|
|
80
|
+
config.provider = llmProvider;
|
|
61
81
|
|
|
62
|
-
//
|
|
63
|
-
if (
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
82
|
+
// 2b) Provider-specific model selection
|
|
83
|
+
if (llmProvider === 'openai') {
|
|
84
|
+
const models = openAIModels();
|
|
85
|
+
const { openaiModel } = await prompts({
|
|
86
|
+
type: 'select',
|
|
87
|
+
name: 'openaiModel',
|
|
88
|
+
message: 'Select an OpenAI model for agentic workflows (function/tool calling):',
|
|
89
|
+
choices: [
|
|
90
|
+
{ title: 'GPT-4.1 (general purpose)', value: 'gpt-4.1' },
|
|
91
|
+
{ title: 'GPT-4.1 mini (faster, cheaper)', value: 'gpt-4.1-mini' },
|
|
92
|
+
{ title: 'GPT-4o (multimodal, tool use)', value: 'gpt-4o' },
|
|
93
|
+
{ title: 'GPT-4o mini (speed/cost optimized)', value: 'gpt-4o-mini' },
|
|
94
|
+
{ title: 'o3 (reasoning w/ native tool use)', value: 'o3' },
|
|
95
|
+
{ title: 'o4-mini (reasoning, cost-effective)', value: 'o4-mini' },
|
|
96
|
+
// GPT-5 family (availability varies)
|
|
97
|
+
{ title: 'GPT-5 (latest reasoning model)', value: 'gpt-5' },
|
|
98
|
+
{ title: 'GPT-5 mini (faster)', value: 'gpt-5-mini' },
|
|
99
|
+
{ title: 'GPT-5 nano (lightweight)', value: 'gpt-5-nano' },
|
|
100
|
+
],
|
|
101
|
+
// Keep prior selection highlighted if present; fallback to 4.1-mini.
|
|
102
|
+
initial: pickInitialIndex(models, config.model, 'gpt-4.1-mini')
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
config.model = openaiModel;
|
|
106
|
+
|
|
107
|
+
// Helpful heads-up: GPT-5 access varies by account/region.
|
|
108
|
+
if (openaiModel?.startsWith('gpt-5')) {
|
|
109
|
+
console.log(`
|
|
110
|
+
β οΈ Warning: GPT-5 support may not be available for all users.
|
|
111
|
+
- Access depends on your OpenAI account/region
|
|
112
|
+
- Some capabilities may be gated or rate-limited
|
|
113
|
+
`);
|
|
114
|
+
}
|
|
115
|
+
} else if (llmProvider === 'gemini') {
|
|
116
|
+
const models = geminiModels();
|
|
117
|
+
const { geminiModel } = await prompts({
|
|
118
|
+
type: 'select',
|
|
119
|
+
name: 'geminiModel',
|
|
120
|
+
message: 'Select a Google Gemini model:',
|
|
121
|
+
choices: [
|
|
122
|
+
{ title: 'Gemini 2.5 Pro (general, high quality)', value: 'gemini-2.5-pro' },
|
|
123
|
+
{ title: 'Gemini 2.5 Flash (fast, cost-efficient)', value: 'gemini-2.5-flash' },
|
|
124
|
+
{ title: 'Gemini 2.5 Flash-8B (lightweight)', value: 'gemini-2.5-flash-8b' },
|
|
125
|
+
],
|
|
126
|
+
// Keep prior selection if present; default to fast/cost-effective Flash.
|
|
127
|
+
initial: pickInitialIndex(models, config.model, 'gemini-2.5-flash')
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
config.model = geminiModel;
|
|
69
131
|
}
|
|
70
132
|
|
|
71
|
-
//
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
config.
|
|
76
|
-
|
|
133
|
+
// βββββββββββββββββββββββββββββββββββββββββββββ
|
|
134
|
+
// STEP 3: Merge defaults without clobbering user-provided fields
|
|
135
|
+
// βββββββββββββββββββββββββββββββββββββββββββββ
|
|
136
|
+
const defaults = defaultConfig(config.projectName || path.basename(root));
|
|
137
|
+
config.initialized = true; // mark as initialized so subsequent runs skip prompts
|
|
138
|
+
config.outputDir = config.outputDir || defaults.outputDir;
|
|
139
|
+
config.projectName = config.projectName || defaults.projectName;
|
|
140
|
+
config.maxTurns = config.maxTurns ?? defaults.maxTurns;
|
|
141
|
+
|
|
142
|
+
// βββββββββββββββββββββββββββββββββββββββββββββ
|
|
143
|
+
// STEP 4: Persist config and scaffold .env (first run only)
|
|
144
|
+
// βββββββββββββββββββββββββββββββββββββββββββββ
|
|
77
145
|
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
78
146
|
console.log('π Created testronaut-config.json');
|
|
79
147
|
|
|
80
|
-
if
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
OPENAI_API_KEY=sk-...
|
|
85
|
-
`;
|
|
148
|
+
// Only create .env if it doesn't exist to avoid overwriting secrets.
|
|
149
|
+
if (!fs.existsSync(envPath)) {
|
|
150
|
+
const exampleEnv = makeEnvTemplate(config.provider);
|
|
151
|
+
if (exampleEnv) {
|
|
86
152
|
fs.writeFileSync(envPath, exampleEnv);
|
|
87
153
|
console.log('π Created .env file');
|
|
88
154
|
}
|
|
155
|
+
}
|
|
89
156
|
|
|
157
|
+
// Provider-specific post-setup messages: quick path to first run.
|
|
158
|
+
if (config.provider === 'openai') {
|
|
90
159
|
console.log(`
|
|
91
160
|
π§ Setup Complete!
|
|
92
161
|
|
|
93
|
-
π
|
|
162
|
+
π Ensure your .env file contains your OpenAI key:
|
|
94
163
|
|
|
95
164
|
OPENAI_API_KEY=sk-...
|
|
96
165
|
|
|
97
|
-
|
|
166
|
+
β
Ready to write missions in \`missions/\`. Try:
|
|
167
|
+
|
|
168
|
+
testronaut welcome.mission.js
|
|
169
|
+
`);
|
|
170
|
+
} else if (config.provider === 'gemini') {
|
|
171
|
+
console.log(`
|
|
172
|
+
π§ Setup Complete!
|
|
173
|
+
|
|
174
|
+
π Ensure your .env file contains your Gemini key:
|
|
98
175
|
|
|
99
|
-
|
|
176
|
+
GEMINI_API_KEY=AIza...
|
|
177
|
+
|
|
178
|
+
π If you plan to use tools/function-calling or images, make sure your account has access to those features.
|
|
179
|
+
|
|
180
|
+
β
Ready to write missions in \`missions/\`. Try:
|
|
100
181
|
|
|
101
182
|
testronaut welcome.mission.js
|
|
102
183
|
`);
|
|
103
184
|
}
|
|
104
185
|
|
|
186
|
+
// Create a small starter mission for a smooth first run.
|
|
105
187
|
createWelcomeMission();
|
|
106
188
|
} else {
|
|
189
|
+
// Already initialized: keep init idempotent and non-destructive.
|
|
107
190
|
console.log('π Already initialized. Skipping setup.');
|
|
108
191
|
}
|
|
109
192
|
|
|
110
|
-
//
|
|
193
|
+
// βββββββββββββββββββββββββββββββββββββββββββββ
|
|
194
|
+
// STEP 5: Ensure folder structure exists (safe to call every run)
|
|
195
|
+
// βββββββββββββββββββββββββββββββββββββββββββββ
|
|
111
196
|
if (!fs.existsSync(missionsDir)) {
|
|
112
197
|
fs.mkdirSync(missionsDir);
|
|
113
198
|
console.log('π Created missions/');
|
|
114
199
|
}
|
|
115
|
-
|
|
116
200
|
if (!fs.existsSync(reportsDir)) {
|
|
117
201
|
fs.mkdirSync(reportsDir);
|
|
118
202
|
console.log('π Created missions/mission_reports/');
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* initHelpers.js
|
|
3
|
+
* ---------------
|
|
4
|
+
* Purpose:
|
|
5
|
+
* Collection of pure, unit-testable helper functions used by the Testronaut
|
|
6
|
+
* CLI initialization process. These handle config normalization, defaults,
|
|
7
|
+
* provider-specific templates, and list-based selections.
|
|
8
|
+
*
|
|
9
|
+
* Design goals:
|
|
10
|
+
* - No side effects (no file system or prompts)
|
|
11
|
+
* - All logic easily testable via Vitest
|
|
12
|
+
*
|
|
13
|
+
* Related tests:
|
|
14
|
+
* Located in `tests/binTests/`
|
|
15
|
+
* Each helper has corresponding unit tests (e.g., `migrateLegacyConfig.test.js`,
|
|
16
|
+
* `makeEnvTemplate.test.js`, `pickInitialIndex.test.js`) that validate expected
|
|
17
|
+
* behavior and regression safety.
|
|
18
|
+
*
|
|
19
|
+
* Used by:
|
|
20
|
+
* ./init.js
|
|
21
|
+
*
|
|
22
|
+
* Example usage:
|
|
23
|
+
* const defaults = defaultConfig(path.basename(process.cwd()));
|
|
24
|
+
* const envTemplate = makeEnvTemplate('gemini');
|
|
25
|
+
* const models = openAIModels();
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Ensures backward compatibility for older configs where
|
|
30
|
+
* provider information may have been implied by the model field.
|
|
31
|
+
*
|
|
32
|
+
* @param {object} cfg - Parsed testronaut-config.json object
|
|
33
|
+
* @returns {object} - Updated config with normalized provider field
|
|
34
|
+
*/
|
|
35
|
+
export function migrateLegacyConfig(cfg) {
|
|
36
|
+
const out = { ...cfg };
|
|
37
|
+
if (!out.provider && out.model === 'openai') {
|
|
38
|
+
out.provider = 'openai';
|
|
39
|
+
}
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Generates default configuration values for a new project.
|
|
45
|
+
*
|
|
46
|
+
* @param {string} rootBasename - Name of the current working directory
|
|
47
|
+
* @returns {{initialized: boolean, outputDir: string, projectName: string, maxTurns: number}}
|
|
48
|
+
*/
|
|
49
|
+
export function defaultConfig(rootBasename) {
|
|
50
|
+
return {
|
|
51
|
+
initialized: true,
|
|
52
|
+
outputDir: 'missions/mission_reports',
|
|
53
|
+
projectName: rootBasename,
|
|
54
|
+
maxTurns: 20,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Returns a provider-specific .env template string.
|
|
60
|
+
* This scaffolds a safe placeholder API key for the user to fill in manually.
|
|
61
|
+
*
|
|
62
|
+
* @param {'openai'|'gemini'} provider - The chosen LLM provider
|
|
63
|
+
* @returns {string} A multiline template for the .env file
|
|
64
|
+
*/
|
|
65
|
+
export function makeEnvTemplate(provider) {
|
|
66
|
+
if (provider === 'openai') {
|
|
67
|
+
return `# Add your OpenAI API key below
|
|
68
|
+
OPENAI_API_KEY=sk-...
|
|
69
|
+
`;
|
|
70
|
+
}
|
|
71
|
+
if (provider === 'gemini') {
|
|
72
|
+
return `# Add your Google Gemini API key below
|
|
73
|
+
GEMINI_API_KEY=AIza...
|
|
74
|
+
`;
|
|
75
|
+
}
|
|
76
|
+
return '';
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* List of supported OpenAI model identifiers.
|
|
81
|
+
*
|
|
82
|
+
* @returns {string[]} Array of OpenAI model names
|
|
83
|
+
*/
|
|
84
|
+
export function openAIModels() {
|
|
85
|
+
return [
|
|
86
|
+
'gpt-4.1',
|
|
87
|
+
'gpt-4.1-mini',
|
|
88
|
+
'gpt-4o',
|
|
89
|
+
'gpt-4o-mini',
|
|
90
|
+
'o3',
|
|
91
|
+
'o4-mini',
|
|
92
|
+
'gpt-5',
|
|
93
|
+
'gpt-5-mini',
|
|
94
|
+
'gpt-5-nano',
|
|
95
|
+
];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* List of supported Google Gemini model identifiers.
|
|
100
|
+
*
|
|
101
|
+
* @returns {string[]} Array of Gemini model names
|
|
102
|
+
*/
|
|
103
|
+
export function geminiModels() {
|
|
104
|
+
return [
|
|
105
|
+
'gemini-2.5-pro', // best reasoning, tool use
|
|
106
|
+
'gemini-2.5-flash', // fast, cost-effective
|
|
107
|
+
'gemini-2.5-flash-lite', // lightweight / high throughput
|
|
108
|
+
];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Determines whether a given model name belongs to a known provider.
|
|
113
|
+
*
|
|
114
|
+
* @param {'openai'|'gemini'} provider - The LLM provider
|
|
115
|
+
* @param {string} model - The model name to check
|
|
116
|
+
* @returns {boolean} True if the model is recognized for that provider
|
|
117
|
+
*/
|
|
118
|
+
export function isKnownModel(provider, model) {
|
|
119
|
+
return provider === 'openai'
|
|
120
|
+
? openAIModels().includes(model)
|
|
121
|
+
: geminiModels().includes(model);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Finds the initial selection index for a list-based UI (e.g. prompts.js select).
|
|
126
|
+
* Prefers the current value if found, otherwise falls back to a known default,
|
|
127
|
+
* and finally to the first item if neither is present.
|
|
128
|
+
*
|
|
129
|
+
* @template T
|
|
130
|
+
* @param {T[]} list - Array of available options
|
|
131
|
+
* @param {T} current - Currently selected value (if any)
|
|
132
|
+
* @param {T} fallback - Fallback value if current is missing
|
|
133
|
+
* @returns {number} The index to use as `initial` in a selection prompt
|
|
134
|
+
*/
|
|
135
|
+
export function pickInitialIndex(list, current, fallback) {
|
|
136
|
+
const idx = list.indexOf(current);
|
|
137
|
+
if (idx >= 0) return idx;
|
|
138
|
+
const fb = list.indexOf(fallback);
|
|
139
|
+
return fb >= 0 ? fb : 0;
|
|
140
|
+
}
|
package/core/agent.js
CHANGED
|
@@ -49,7 +49,7 @@ export async function runAgent(goals, missionName, maxTurns = 20, ) {
|
|
|
49
49
|
missionName,
|
|
50
50
|
submissionType: goal.submissionType || goal.label,
|
|
51
51
|
submissionName: goal.submissionName || null,
|
|
52
|
-
status: result
|
|
52
|
+
status: result?.success ? 'passed' : 'failed',
|
|
53
53
|
steps: JSON.parse(JSON.stringify(steps)), // π deep-clone to avoid refs
|
|
54
54
|
startTime: Date.now() - 1, // set real values if you have them
|
|
55
55
|
endTime: Date.now(),
|