Skip to content
Closed
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
2 changes: 1 addition & 1 deletion backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
"prepack": "npm run build",
"typecheck": "tsc --noEmit",
"test": "jest",
"test:core": "jest --runInBand --forceExit src/agent/communication/__tests__/agentMessageBus.test.ts src/agent/core/executors/__tests__/strategyExecutor.test.ts src/agent/core/executors/__tests__/hypothesisExecutor.test.ts src/agent/context/__tests__/enhancedSessionContext.test.ts src/tests/adbTools.test.ts src/services/__tests__/sessionLogger.test.ts src/services/__tests__/traceAnalysisSkillConfig.test.ts src/agent/agents/domain/__tests__/registry.test.ts src/agentv3/__tests__/sqlIncludeInjector.test.ts src/agentv3/__tests__/claudeRuntimeRuntimeSnapshots.test.ts src/middleware/__tests__/auth.test.ts src/services/__tests__/rbac.test.ts src/routes/__tests__/agentRoutesRbac.test.ts src/routes/__tests__/ownerGuardRoutes.test.ts src/routes/__tests__/requestContextRouteCoverage.test.ts src/middleware/__tests__/legacyApiCompatibility.test.ts src/services/__tests__/enterpriseDb.test.ts src/services/__tests__/enterpriseSchema.test.ts src/services/__tests__/enterpriseRepository.test.ts src/services/__tests__/runtimeSnapshotStore.test.ts src/services/providerManager/__tests__/enterpriseProviderStore.test.ts src/routes/__tests__/enterpriseTraceMetadataRoutes.test.ts src/routes/__tests__/enterpriseReportRoutes.test.ts",
"test:core": "jest --runInBand --forceExit src/agent/communication/__tests__/agentMessageBus.test.ts src/agent/core/executors/__tests__/strategyExecutor.test.ts src/agent/core/executors/__tests__/hypothesisExecutor.test.ts src/agent/context/__tests__/enhancedSessionContext.test.ts src/tests/adbTools.test.ts src/services/__tests__/sessionLogger.test.ts src/services/__tests__/traceAnalysisSkillConfig.test.ts src/agent/agents/domain/__tests__/registry.test.ts src/agentv3/__tests__/sqlIncludeInjector.test.ts src/agentv3/__tests__/analysisPatternMemory.test.ts src/agentv3/__tests__/claudeRuntimeRuntimeSnapshots.test.ts src/middleware/__tests__/auth.test.ts src/services/__tests__/rbac.test.ts src/routes/__tests__/agentRoutesRbac.test.ts src/routes/__tests__/ownerGuardRoutes.test.ts src/routes/__tests__/requestContextRouteCoverage.test.ts src/middleware/__tests__/legacyApiCompatibility.test.ts src/services/__tests__/enterpriseDb.test.ts src/services/__tests__/enterpriseSchema.test.ts src/services/__tests__/enterpriseRepository.test.ts src/services/__tests__/enterpriseKnowledgeScope.test.ts src/services/__tests__/runtimeSnapshotStore.test.ts src/services/providerManager/__tests__/enterpriseProviderStore.test.ts src/routes/__tests__/enterpriseTraceMetadataRoutes.test.ts src/routes/__tests__/enterpriseReportRoutes.test.ts",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
"test:unit": "jest --testPathPatterns=src/tests",
Expand Down
14 changes: 13 additions & 1 deletion backend/src/agentOpenAI/openAiRuntime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ import { DEFAULT_OUTPUT_LANGUAGE, localize, type OutputLanguage } from '../agent
import { loadOpenAIConfig, type OpenAIAgentConfig } from './openAiConfig';
import { createOpenAIToolsFromMcpDefinitions } from './openAiToolAdapter';
import type { ProviderScope } from '../services/providerManager';
import type { KnowledgeScope } from '../services/scopedKnowledgeStore';

interface OpenAISessionEntry {
history?: AgentInputItem[];
Expand Down Expand Up @@ -132,6 +133,16 @@ function providerScopeFromOptions(options: AnalysisOptions): ProviderScope | und
};
}

function knowledgeScopeFromOptions(options: AnalysisOptions): KnowledgeScope | undefined {
if (!options.tenantId || !options.workspaceId) return undefined;
return {
tenantId: options.tenantId,
workspaceId: options.workspaceId,
userId: options.userId,
sourceRunId: options.runId,
};
}

function summarizeToolOutput(value: unknown): string {
const text = typeof value === 'string' ? value : JSON.stringify(value);
if (!text) return '';
Expand Down Expand Up @@ -737,7 +748,7 @@ export class OpenAIRuntime extends EventEmitter implements IOrchestrator {

let sqlErrors = this.sessionSqlErrors.get(sessionId);
if (!sqlErrors) {
sqlErrors = loadLearnedSqlFixPairs(5);
sqlErrors = loadLearnedSqlFixPairs(5, knowledgeScopeFromOptions(options));
this.sessionSqlErrors.set(sessionId, sqlErrors);
}

Expand Down Expand Up @@ -770,6 +781,7 @@ export class OpenAIRuntime extends EventEmitter implements IOrchestrator {
lightweight,
skillNotesBudget,
outputLanguage: config.outputLanguage,
knowledgeScope: knowledgeScopeFromOptions(options),
});

const tools = createOpenAIToolsFromMcpDefinitions(toolDefinitions);
Expand Down
82 changes: 80 additions & 2 deletions backend/src/agentv3/__tests__/analysisPatternMemory.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,14 @@
* File I/O is mocked — no actual disk writes.
*/

import { jest, describe, it, expect, beforeEach } from '@jest/globals';
import { jest, describe, it, expect, beforeEach, afterEach } from '@jest/globals';

// ── Mock fs before importing module ──────────────────────────────────────

let mockPatterns: any[] = [];
let mockNegativePatterns: any[] = [];
let mockQuickPatterns: any[] = [];
const originalEnterprise = process.env.SMARTPERFETTO_ENTERPRISE;

// Temporary storage for atomic write simulation (writeFile to .tmp, then rename)
let tmpWriteBuffer: Map<string, string> = new Map();
Expand Down Expand Up @@ -106,6 +107,14 @@ beforeEach(() => {
setSupersedeStoreForTesting(null);
});

afterEach(() => {
if (originalEnterprise === undefined) {
delete process.env.SMARTPERFETTO_ENTERPRISE;
} else {
process.env.SMARTPERFETTO_ENTERPRISE = originalEnterprise;
}
});

// ── Feature Extraction ───────────────────────────────────────────────────

describe('extractTraceFeatures', () => {
Expand Down Expand Up @@ -529,6 +538,75 @@ describe('saveAnalysisPattern with extras', () => {
});
});

describe('enterprise scope isolation', () => {
const scopeA = {tenantId: 'tenant-a', workspaceId: 'workspace-a', userId: 'user-a'};
const scopeB = {tenantId: 'tenant-b', workspaceId: 'workspace-b', userId: 'user-b'};
const features = ['arch:STANDARD', 'scene:scrolling'];

beforeEach(() => {
process.env.SMARTPERFETTO_ENTERPRISE = 'true';
});

it('saves source scope provenance and filters positive matches before similarity ranking', async () => {
await saveAnalysisPattern(
features,
['tenant-a insight'],
'scrolling',
'STANDARD',
0.9,
{knowledgeScope: scopeA},
);
await saveAnalysisPattern(
features,
['tenant-b insight'],
'scrolling',
'STANDARD',
0.9,
{knowledgeScope: scopeB},
);

expect(mockPatterns).toHaveLength(2);
expect(mockPatterns[0].provenance.sourceTenantId).toBe('tenant-a');
expect(mockPatterns[1].provenance.sourceTenantId).toBe('tenant-b');
expect(matchPatterns(features, scopeA).map(p => p.keyInsights[0])).toEqual([
'tenant-a insight',
]);
expect(matchPatterns(features, scopeB).map(p => p.keyInsights[0])).toEqual([
'tenant-b insight',
]);
});

it('filters negative and quick-path matches by the same enterprise scope', async () => {
await saveNegativePattern(
features,
[{type: 'sql_error', approach: 'bad sql', reason: 'tenant-a'}],
'scrolling',
'STANDARD',
{knowledgeScope: scopeA},
);
await saveNegativePattern(
features,
[{type: 'sql_error', approach: 'bad sql', reason: 'tenant-b'}],
'scrolling',
'STANDARD',
{knowledgeScope: scopeB},
);
await saveQuickPathPattern(
features,
['tenant-a quick'],
'scrolling',
'STANDARD',
{knowledgeScope: scopeA},
);

expect(matchNegativePatterns(features, scopeA)[0].failedApproaches[0].reason)
.toBe('tenant-a');
expect(matchNegativePatterns(features, scopeB)[0].failedApproaches[0].reason)
.toBe('tenant-b');
expect(matchQuickPatternsAsBackup(features, scopeB)).toHaveLength(0);
});
});

describe('matchPatterns status weighting', () => {
it('drops entries with status=rejected', async () => {
mockPatterns = [{
Expand Down Expand Up @@ -766,4 +844,4 @@ describe('sweepAutoConfirm', () => {
expect(mockPatterns.find(p => p.id === 'conf').status).toBe('confirmed');
expect(mockPatterns.find(p => p.id === 'rej').status).toBe('rejected');
});
});
});
Loading