mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-04 21:23:07 +00:00
feat: add test validation command and improve environment variable handling
- Introduced a new command for validating tests, providing detailed instructions for running tests and fixing failures based on code changes. - Updated the environment variable handling in the Claude provider to only allow explicitly defined variables, enhancing security and preventing leakage of sensitive information. - Improved feature loading to handle errors more gracefully and load features concurrently, optimizing performance. - Centralized port configuration for the Automaker application to prevent accidental termination of critical services.
This commit is contained in:
36
.claude/commands/validate-tests.md
Normal file
36
.claude/commands/validate-tests.md
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
# Project Test and Fix Command
|
||||||
|
|
||||||
|
Run all tests and intelligently fix any failures based on what changed.
|
||||||
|
|
||||||
|
## Instructions
|
||||||
|
|
||||||
|
1. **Run all tests**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run test:all
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **If all tests pass**, report success and stop.
|
||||||
|
|
||||||
|
3. **If any tests fail**, analyze the failures:
|
||||||
|
- Note which tests failed and their error messages
|
||||||
|
- Run `git diff main` to see what code has changed
|
||||||
|
|
||||||
|
4. **Determine the nature of the change**:
|
||||||
|
- **If the logic change is intentional** (new feature, refactor, behavior change):
|
||||||
|
- Update the failing tests to match the new expected behavior
|
||||||
|
- The tests should reflect what the code NOW does correctly
|
||||||
|
|
||||||
|
- **If the logic change appears to be a bug** (regression, unintended side effect):
|
||||||
|
- Fix the source code to restore the expected behavior
|
||||||
|
- Do NOT modify the tests - they are catching a real bug
|
||||||
|
|
||||||
|
5. **How to decide if it's a bug vs intentional change**:
|
||||||
|
- Look at the git diff and commit messages
|
||||||
|
- If the change was deliberate and the test expectations are now outdated → update tests
|
||||||
|
- If the change broke existing functionality that should still work → fix the code
|
||||||
|
- When in doubt, ask the user
|
||||||
|
|
||||||
|
6. **After making fixes**, re-run the tests to verify everything passes.
|
||||||
|
|
||||||
|
7. **Report summary** of what was fixed (tests updated vs code fixed).
|
||||||
@@ -15,22 +15,30 @@ import type {
|
|||||||
ModelDefinition,
|
ModelDefinition,
|
||||||
} from './types.js';
|
} from './types.js';
|
||||||
|
|
||||||
// Automaker-specific environment variables that should not pollute agent processes
|
// Explicit allowlist of environment variables to pass to the SDK.
|
||||||
// These are internal to Automaker and would interfere with user projects
|
// Only these vars are passed - nothing else from process.env leaks through.
|
||||||
// (e.g., PORT=3008 would cause Next.js/Vite to use the wrong port)
|
const ALLOWED_ENV_VARS = [
|
||||||
const AUTOMAKER_ENV_VARS = ['PORT', 'DATA_DIR', 'AUTOMAKER_API_KEY', 'NODE_PATH'];
|
'ANTHROPIC_API_KEY',
|
||||||
|
'PATH',
|
||||||
|
'HOME',
|
||||||
|
'SHELL',
|
||||||
|
'TERM',
|
||||||
|
'USER',
|
||||||
|
'LANG',
|
||||||
|
'LC_ALL',
|
||||||
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build a clean environment for the SDK, excluding Automaker-specific variables
|
* Build environment for the SDK with only explicitly allowed variables
|
||||||
*/
|
*/
|
||||||
function buildCleanEnv(): Record<string, string | undefined> {
|
function buildEnv(): Record<string, string | undefined> {
|
||||||
const cleanEnv: Record<string, string | undefined> = {};
|
const env: Record<string, string | undefined> = {};
|
||||||
for (const [key, value] of Object.entries(process.env)) {
|
for (const key of ALLOWED_ENV_VARS) {
|
||||||
if (!AUTOMAKER_ENV_VARS.includes(key)) {
|
if (process.env[key]) {
|
||||||
cleanEnv[key] = value;
|
env[key] = process.env[key];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return cleanEnv;
|
return env;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ClaudeProvider extends BaseProvider {
|
export class ClaudeProvider extends BaseProvider {
|
||||||
@@ -75,9 +83,8 @@ export class ClaudeProvider extends BaseProvider {
|
|||||||
systemPrompt,
|
systemPrompt,
|
||||||
maxTurns,
|
maxTurns,
|
||||||
cwd,
|
cwd,
|
||||||
// Pass clean environment to SDK, excluding Automaker-specific variables
|
// Pass only explicitly allowed environment variables to SDK
|
||||||
// This prevents PORT, DATA_DIR, etc. from polluting agent-spawned processes
|
env: buildEnv(),
|
||||||
env: buildCleanEnv(),
|
|
||||||
// Only restrict tools if explicitly set OR (no MCP / unrestricted disabled)
|
// Only restrict tools if explicitly set OR (no MCP / unrestricted disabled)
|
||||||
...(allowedTools && shouldRestrictTools && { allowedTools }),
|
...(allowedTools && shouldRestrictTools && { allowedTools }),
|
||||||
...(!allowedTools && shouldRestrictTools && { allowedTools: defaultTools }),
|
...(!allowedTools && shouldRestrictTools && { allowedTools: defaultTools }),
|
||||||
|
|||||||
@@ -185,9 +185,8 @@ export class FeatureLoader {
|
|||||||
})) as any[];
|
})) as any[];
|
||||||
const featureDirs = entries.filter((entry) => entry.isDirectory());
|
const featureDirs = entries.filter((entry) => entry.isDirectory());
|
||||||
|
|
||||||
// Load each feature
|
// Load all features concurrently (secureFs has built-in concurrency limiting)
|
||||||
const features: Feature[] = [];
|
const featurePromises = featureDirs.map(async (dir) => {
|
||||||
for (const dir of featureDirs) {
|
|
||||||
const featureId = dir.name;
|
const featureId = dir.name;
|
||||||
const featureJsonPath = this.getFeatureJsonPath(projectPath, featureId);
|
const featureJsonPath = this.getFeatureJsonPath(projectPath, featureId);
|
||||||
|
|
||||||
@@ -199,13 +198,13 @@ export class FeatureLoader {
|
|||||||
logger.warn(
|
logger.warn(
|
||||||
`[FeatureLoader] Feature ${featureId} missing required 'id' field, skipping`
|
`[FeatureLoader] Feature ${featureId} missing required 'id' field, skipping`
|
||||||
);
|
);
|
||||||
continue;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
features.push(feature);
|
return feature as Feature;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
|
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
|
||||||
continue;
|
return null;
|
||||||
} else if (error instanceof SyntaxError) {
|
} else if (error instanceof SyntaxError) {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
`[FeatureLoader] Failed to parse feature.json for ${featureId}: ${error.message}`
|
`[FeatureLoader] Failed to parse feature.json for ${featureId}: ${error.message}`
|
||||||
@@ -216,8 +215,12 @@ export class FeatureLoader {
|
|||||||
(error as Error).message
|
(error as Error).message
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
|
const results = await Promise.all(featurePromises);
|
||||||
|
const features = results.filter((f): f is Feature => f !== null);
|
||||||
|
|
||||||
// Sort by creation order (feature IDs contain timestamp)
|
// Sort by creation order (feature IDs contain timestamp)
|
||||||
features.sort((a, b) => {
|
features.sort((a, b) => {
|
||||||
|
|||||||
@@ -349,18 +349,18 @@ async function startServer(): Promise<void> {
|
|||||||
// Validate that the found Node executable actually exists
|
// Validate that the found Node executable actually exists
|
||||||
// systemPathExists is used because node-finder returns system paths
|
// systemPathExists is used because node-finder returns system paths
|
||||||
if (command !== 'node') {
|
if (command !== 'node') {
|
||||||
|
let exists: boolean;
|
||||||
try {
|
try {
|
||||||
if (!systemPathExists(command)) {
|
exists = systemPathExists(command);
|
||||||
throw new Error(
|
|
||||||
`Node.js executable not found at: ${command} (source: ${nodeResult.source})`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const originalError = error instanceof Error ? error.message : String(error);
|
const originalError = error instanceof Error ? error.message : String(error);
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Failed to verify Node.js executable at: ${command} (source: ${nodeResult.source}). Reason: ${originalError}`
|
`Failed to verify Node.js executable at: ${command} (source: ${nodeResult.source}). Reason: ${originalError}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (!exists) {
|
||||||
|
throw new Error(`Node.js executable not found at: ${command} (source: ${nodeResult.source})`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let args: string[];
|
let args: string[];
|
||||||
|
|||||||
15
libs/platform/src/config/ports.ts
Normal file
15
libs/platform/src/config/ports.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
/**
|
||||||
|
* Centralized port configuration for AutoMaker
|
||||||
|
*
|
||||||
|
* These ports are reserved for the Automaker application and should never be
|
||||||
|
* killed or terminated by AI agents during feature implementation.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** Port for the static/UI server (Vite dev server) */
|
||||||
|
export const STATIC_PORT = 3007;
|
||||||
|
|
||||||
|
/** Port for the backend API server (Express + WebSocket) */
|
||||||
|
export const SERVER_PORT = 3008;
|
||||||
|
|
||||||
|
/** Array of all reserved Automaker ports */
|
||||||
|
export const RESERVED_PORTS = [STATIC_PORT, SERVER_PORT] as const;
|
||||||
@@ -115,3 +115,6 @@ export {
|
|||||||
electronAppStat,
|
electronAppStat,
|
||||||
electronAppReadFile,
|
electronAppReadFile,
|
||||||
} from './system-paths.js';
|
} from './system-paths.js';
|
||||||
|
|
||||||
|
// Port configuration
|
||||||
|
export { STATIC_PORT, SERVER_PORT, RESERVED_PORTS } from './config/ports.js';
|
||||||
|
|||||||
@@ -574,11 +574,11 @@ export function removeEnvKeySync(envPath: string, key: string): void {
|
|||||||
*/
|
*/
|
||||||
function updateEnvContent(content: string, key: string, value: string): string {
|
function updateEnvContent(content: string, key: string, value: string): string {
|
||||||
const lines = content.split('\n');
|
const lines = content.split('\n');
|
||||||
const keyRegex = new RegExp(`^${escapeRegex(key)}=`);
|
const keyPrefix = `${key}=`;
|
||||||
let found = false;
|
let found = false;
|
||||||
|
|
||||||
const newLines = lines.map((line) => {
|
const newLines = lines.map((line) => {
|
||||||
if (keyRegex.test(line.trim())) {
|
if (line.trim().startsWith(keyPrefix)) {
|
||||||
found = true;
|
found = true;
|
||||||
return `${key}=${value}`;
|
return `${key}=${value}`;
|
||||||
}
|
}
|
||||||
@@ -612,8 +612,8 @@ function updateEnvContent(content: string, key: string, value: string): string {
|
|||||||
*/
|
*/
|
||||||
function removeEnvKeyFromContent(content: string, key: string): string {
|
function removeEnvKeyFromContent(content: string, key: string): string {
|
||||||
const lines = content.split('\n');
|
const lines = content.split('\n');
|
||||||
const keyRegex = new RegExp(`^${escapeRegex(key)}=`);
|
const keyPrefix = `${key}=`;
|
||||||
const newLines = lines.filter((line) => !keyRegex.test(line.trim()));
|
const newLines = lines.filter((line) => !line.trim().startsWith(keyPrefix));
|
||||||
|
|
||||||
// Remove trailing empty lines
|
// Remove trailing empty lines
|
||||||
while (newLines.length > 0 && newLines[newLines.length - 1].trim() === '') {
|
while (newLines.length > 0 && newLines[newLines.length - 1].trim() === '') {
|
||||||
@@ -627,10 +627,3 @@ function removeEnvKeyFromContent(content: string, key: string): string {
|
|||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Escape special regex characters in a string
|
|
||||||
*/
|
|
||||||
function escapeRegex(str: string): string {
|
|
||||||
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
"node": ">=22.0.0 <23.0.0"
|
"node": ">=22.0.0 <23.0.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@automaker/platform": "1.0.0",
|
||||||
"@automaker/types": "1.0.0"
|
"@automaker/types": "1.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import type {
|
|||||||
ResolvedBacklogPlanPrompts,
|
ResolvedBacklogPlanPrompts,
|
||||||
ResolvedEnhancementPrompts,
|
ResolvedEnhancementPrompts,
|
||||||
} from '@automaker/types';
|
} from '@automaker/types';
|
||||||
|
import { STATIC_PORT, SERVER_PORT } from '@automaker/platform';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ========================================================================
|
* ========================================================================
|
||||||
@@ -210,7 +211,7 @@ This feature depends on: {{dependencies}}
|
|||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
**CRITICAL - Port Protection:**
|
**CRITICAL - Port Protection:**
|
||||||
NEVER kill or terminate processes running on ports 3007 or 3008. These are reserved for the Automaker application. Killing these ports will crash Automaker and terminate this session.
|
NEVER kill or terminate processes running on ports ${STATIC_PORT} or ${SERVER_PORT}. These are reserved for the Automaker application. Killing these ports will crash Automaker and terminate this session.
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const DEFAULT_AUTO_MODE_FOLLOW_UP_PROMPT_TEMPLATE = `## Follow-up on Feature Implementation
|
export const DEFAULT_AUTO_MODE_FOLLOW_UP_PROMPT_TEMPLATE = `## Follow-up on Feature Implementation
|
||||||
@@ -303,7 +304,7 @@ You have access to several tools:
|
|||||||
5. Guide users toward good software design principles
|
5. Guide users toward good software design principles
|
||||||
|
|
||||||
**CRITICAL - Port Protection:**
|
**CRITICAL - Port Protection:**
|
||||||
NEVER kill or terminate processes running on ports 3007 or 3008. These are reserved for the Automaker application itself. Killing these ports will crash Automaker and terminate your session.
|
NEVER kill or terminate processes running on ports ${STATIC_PORT} or ${SERVER_PORT}. These are reserved for the Automaker application itself. Killing these ports will crash Automaker and terminate your session.
|
||||||
|
|
||||||
Remember: You're a collaborative partner in the development process. Be helpful, clear, and thorough.`;
|
Remember: You're a collaborative partner in the development process. Be helpful, clear, and thorough.`;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user