Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@
"prepublishOnly": "npm run build",
"version:bump": "npx tsx scripts/bump-version.ts",
"test": "vitest run",
"test:watch": "vitest"
"test:watch": "vitest",
"test:integration": "vitest run --config vitest.integration.config.ts"
},
"keywords": [
"claude",
Expand Down
57 changes: 57 additions & 0 deletions tests/integration/ambient-activation.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { describe, it, expect } from 'vitest';
import {
isClaudeAvailable,
runClaude,
hasClassification,
isQuietResponse,
extractIntent,
extractDepth,
} from './helpers.js';

/**
* Integration tests for ambient mode skill activation.
*
* These tests require:
* - `claude` CLI installed and authenticated
* - Ambient mode enabled (`devflow ambient --enable`)
* - DevFlow skills installed (`devflow init`)
*
* Run manually: npm run test:integration
* Not part of `npm test` — each test is an API call.
*/
describe.skipIf(!isClaudeAvailable())('ambient classification', () => {
// QUICK tier — no skills loaded, no classification output
it('classifies "thanks" as QUICK (silent)', () => {
const output = runClaude('thanks');
expect(isQuietResponse(output)).toBe(true);
});

it('classifies "commit this" as QUICK (git op)', () => {
const output = runClaude('commit the current changes');
// Git operations should not trigger STANDARD classification
expect(isQuietResponse(output) || extractDepth(output) === 'QUICK').toBe(true);
});

// STANDARD tier — skills referenced in output
it('classifies "add a login form" as BUILD/STANDARD', () => {
const output = runClaude('add a login form with email and password fields');
if (hasClassification(output)) {
expect(extractIntent(output)).toBe('BUILD');
expect(extractDepth(output)).toBe('STANDARD');
}
// Even without explicit classification, BUILD prompts should reference TDD
expect(
output.toLowerCase().includes('test') ||
output.toLowerCase().includes('tdd') ||
hasClassification(output)
).toBe(true);
});

it('classifies "fix the auth error" as DEBUG/STANDARD', () => {
const output = runClaude('fix the authentication error in the login handler');
if (hasClassification(output)) {
expect(extractIntent(output)).toBe('DEBUG');
expect(['STANDARD', 'ESCALATE']).toContain(extractDepth(output));
}
});
});
64 changes: 64 additions & 0 deletions tests/integration/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { execSync } from 'child_process';

/**
* Check if the `claude` CLI is available on this machine.
*/
export function isClaudeAvailable(): boolean {
try {
execSync('claude --version', { stdio: 'pipe', timeout: 5000 });
return true;
} catch {
return false;
}
}

/**
* Run a prompt through claude CLI in non-interactive mode.
* Returns the text output.
*/
export function runClaude(prompt: string, options?: { timeout?: number }): string {
const timeout = options?.timeout ?? 30000;

const result = execSync(
`claude -p --output-format text --model haiku "${prompt.replace(/"/g, '\\"')}"`,
{
stdio: 'pipe',
timeout,
encoding: 'utf-8',
},
);

return result.trim();
}

/**
* Assert that output contains a classification marker (case-insensitive).
* Classification markers look like: "Ambient: BUILD/STANDARD"
*/
export function hasClassification(output: string): boolean {
return /ambient:\s*(BUILD|DEBUG|REVIEW|PLAN|EXPLORE|CHAT)\s*\/\s*(QUICK|STANDARD|ESCALATE)/i.test(output);
}

/**
* Assert that output does NOT contain a classification marker.
* QUICK responses should be silent — no classification output.
*/
export function isQuietResponse(output: string): boolean {
return !hasClassification(output);
}

/**
* Extract the intent from a classification marker.
*/
export function extractIntent(output: string): string | null {
const match = output.match(/ambient:\s*(BUILD|DEBUG|REVIEW|PLAN|EXPLORE|CHAT)/i);
return match ? match[1].toUpperCase() : null;
}

/**
* Extract the depth from a classification marker.
*/
export function extractDepth(output: string): string | null {
const match = output.match(/ambient:\s*\w+\s*\/\s*(QUICK|STANDARD|ESCALATE)/i);
return match ? match[1].toUpperCase() : null;
}
1 change: 1 addition & 0 deletions vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export default defineConfig({
test: {
root: '.',
include: ['tests/**/*.test.ts'],
exclude: ['tests/integration/**'],
globals: false,
environment: 'node',
restoreMocks: true,
Expand Down
18 changes: 18 additions & 0 deletions vitest.integration.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { defineConfig } from 'vitest/config';

export default defineConfig({
test: {
root: '.',
include: ['tests/integration/**/*.test.ts'],
globals: false,
environment: 'node',
restoreMocks: true,
testTimeout: 60000,
retry: 2,
},
resolve: {
alias: {
'#cli': new URL('./src/cli/', import.meta.url).pathname,
},
},
});