@yasserkhanorg/e2e-agents 1.8.5 → 1.10.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/README.md +95 -8
- package/dist/adapters/cypress.d.ts +10 -0
- package/dist/adapters/cypress.d.ts.map +1 -0
- package/dist/adapters/cypress.js +86 -0
- package/dist/adapters/framework_adapter.d.ts +41 -0
- package/dist/adapters/framework_adapter.d.ts.map +1 -0
- package/dist/adapters/framework_adapter.js +152 -0
- package/dist/adapters/playwright.d.ts +10 -0
- package/dist/adapters/playwright.d.ts.map +1 -0
- package/dist/adapters/playwright.js +86 -0
- package/dist/adapters/pytest.d.ts +10 -0
- package/dist/adapters/pytest.d.ts.map +1 -0
- package/dist/adapters/pytest.js +96 -0
- package/dist/adapters/supertest.d.ts +12 -0
- package/dist/adapters/supertest.d.ts.map +1 -0
- package/dist/adapters/supertest.js +85 -0
- package/dist/agent/config.d.ts +1 -1
- package/dist/agent/config.d.ts.map +1 -1
- package/dist/agent/git.d.ts +1 -0
- package/dist/agent/git.d.ts.map +1 -1
- package/dist/agent/git.js +3 -0
- package/dist/agentic/fix_loop.d.ts.map +1 -1
- package/dist/agentic/fix_loop.js +5 -4
- package/dist/agentic/runner.d.ts +2 -0
- package/dist/agentic/runner.d.ts.map +1 -1
- package/dist/agentic/runner.js +15 -12
- package/dist/agents/cross-impact.d.ts.map +1 -1
- package/dist/agents/cross-impact.js +6 -1
- package/dist/agents/executor.d.ts.map +1 -1
- package/dist/agents/executor.js +6 -1
- package/dist/agents/strategist.d.ts.map +1 -1
- package/dist/agents/strategist.js +6 -1
- package/dist/agents/test-designer.d.ts.map +1 -1
- package/dist/agents/test-designer.js +6 -1
- package/dist/anthropic_provider.d.ts.map +1 -1
- package/dist/anthropic_provider.js +1 -0
- package/dist/base_provider.d.ts +56 -0
- package/dist/base_provider.d.ts.map +1 -1
- package/dist/base_provider.js +123 -1
- package/dist/budget_ledger.d.ts +28 -0
- package/dist/budget_ledger.d.ts.map +1 -0
- package/dist/budget_ledger.js +62 -0
- package/dist/cache/cached_provider.d.ts +45 -0
- package/dist/cache/cached_provider.d.ts.map +1 -0
- package/dist/cache/cached_provider.js +88 -0
- package/dist/cache/response_cache.d.ts +79 -0
- package/dist/cache/response_cache.d.ts.map +1 -0
- package/dist/cache/response_cache.js +177 -0
- package/dist/cli/commands/bootstrap.d.ts +3 -0
- package/dist/cli/commands/bootstrap.d.ts.map +1 -0
- package/dist/cli/commands/bootstrap.js +109 -0
- package/dist/cli/commands/cost_report.d.ts +3 -0
- package/dist/cli/commands/cost_report.d.ts.map +1 -0
- package/dist/cli/commands/cost_report.js +115 -0
- package/dist/cli/commands/crew.d.ts.map +1 -1
- package/dist/cli/commands/crew.js +118 -1
- package/dist/cli/commands/gate.d.ts +3 -0
- package/dist/cli/commands/gate.d.ts.map +1 -0
- package/dist/cli/commands/gate.js +86 -0
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +7 -62
- package/dist/cli/commands/train.d.ts.map +1 -1
- package/dist/cli/commands/train.js +16 -21
- package/dist/cli/defaults.d.ts +35 -0
- package/dist/cli/defaults.d.ts.map +1 -0
- package/dist/cli/defaults.js +125 -0
- package/dist/cli/errors.d.ts +27 -0
- package/dist/cli/errors.d.ts.map +1 -0
- package/dist/cli/errors.js +57 -0
- package/dist/cli/parse_args.d.ts.map +1 -1
- package/dist/cli/parse_args.js +24 -2
- package/dist/cli/types.d.ts +7 -1
- package/dist/cli/types.d.ts.map +1 -1
- package/dist/cli.js +47 -2
- package/dist/crew/context.d.ts +15 -0
- package/dist/crew/context.d.ts.map +1 -1
- package/dist/crew/orchestrator.d.ts +14 -0
- package/dist/crew/orchestrator.d.ts.map +1 -1
- package/dist/crew/orchestrator.js +162 -4
- package/dist/crew/protocol.d.ts +13 -0
- package/dist/crew/protocol.d.ts.map +1 -1
- package/dist/crew/provider.d.ts +15 -1
- package/dist/crew/provider.d.ts.map +1 -1
- package/dist/crew/provider.js +24 -4
- package/dist/custom_provider.d.ts.map +1 -1
- package/dist/custom_provider.js +1 -0
- package/dist/engine/diff_loader.d.ts.map +1 -1
- package/dist/engine/diff_loader.js +3 -14
- package/dist/engine/impact_engine.d.ts.map +1 -1
- package/dist/engine/impact_engine.js +9 -23
- package/dist/esm/adapters/cypress.js +49 -0
- package/dist/esm/adapters/framework_adapter.js +114 -0
- package/dist/esm/adapters/playwright.js +49 -0
- package/dist/esm/adapters/pytest.js +59 -0
- package/dist/esm/adapters/supertest.js +48 -0
- package/dist/esm/agent/git.js +3 -1
- package/dist/esm/agentic/fix_loop.js +5 -4
- package/dist/esm/agentic/runner.js +15 -12
- package/dist/esm/agents/cross-impact.js +6 -1
- package/dist/esm/agents/executor.js +6 -1
- package/dist/esm/agents/strategist.js +6 -1
- package/dist/esm/agents/test-designer.js +6 -1
- package/dist/esm/anthropic_provider.js +1 -0
- package/dist/esm/base_provider.js +121 -0
- package/dist/esm/budget_ledger.js +58 -0
- package/dist/esm/cache/cached_provider.js +82 -0
- package/dist/esm/cache/response_cache.js +140 -0
- package/dist/esm/cli/commands/bootstrap.js +106 -0
- package/dist/esm/cli/commands/cost_report.js +112 -0
- package/dist/esm/cli/commands/crew.js +118 -1
- package/dist/esm/cli/commands/gate.js +83 -0
- package/dist/esm/cli/commands/init.js +3 -58
- package/dist/esm/cli/commands/train.js +16 -21
- package/dist/esm/cli/defaults.js +118 -0
- package/dist/esm/cli/errors.js +52 -0
- package/dist/esm/cli/parse_args.js +24 -2
- package/dist/esm/cli.js +47 -2
- package/dist/esm/crew/orchestrator.js +162 -4
- package/dist/esm/crew/provider.js +24 -4
- package/dist/esm/custom_provider.js +1 -0
- package/dist/esm/engine/diff_loader.js +1 -12
- package/dist/esm/engine/impact_engine.js +9 -23
- package/dist/esm/index.js +21 -0
- package/dist/esm/knowledge/api_surface.js +265 -34
- package/dist/esm/knowledge/cluster_utils.js +60 -0
- package/dist/esm/knowledge/failure_history.js +121 -0
- package/dist/esm/knowledge/kg_bridge.js +381 -0
- package/dist/esm/knowledge/kg_types.js +3 -0
- package/dist/esm/knowledge/route_families.js +119 -0
- package/dist/esm/mcp-server.js +2 -4
- package/dist/esm/metrics/prometheus.js +149 -0
- package/dist/esm/model_router.js +59 -0
- package/dist/esm/ollama_provider.js +1 -0
- package/dist/esm/openai_provider.js +1 -0
- package/dist/esm/pipeline/orchestrator.js +6 -12
- package/dist/esm/pipeline/stage0_preprocess.js +12 -19
- package/dist/esm/pipeline/stage1_impact.js +19 -3
- package/dist/esm/pipeline/stage2_coverage.js +29 -7
- package/dist/esm/pipeline/stage3_generation.js +21 -1
- package/dist/esm/progress.js +112 -0
- package/dist/esm/prompts/coverage.js +17 -24
- package/dist/esm/prompts/cross-impact.js +3 -21
- package/dist/esm/prompts/generation.js +201 -45
- package/dist/esm/prompts/generation_profile.js +147 -0
- package/dist/esm/prompts/heal.js +33 -15
- package/dist/esm/prompts/impact.js +3 -22
- package/dist/esm/prompts/json_extract.js +36 -0
- package/dist/esm/prompts/strategist.js +2 -20
- package/dist/esm/prompts/test-designer.js +6 -21
- package/dist/esm/provider_factory.js +6 -4
- package/dist/esm/reporters/junit.js +86 -0
- package/dist/esm/reporters/reporter.js +3 -0
- package/dist/esm/reporters/sarif.js +131 -0
- package/dist/esm/resilience/circuit_breaker.js +78 -0
- package/dist/esm/resilience/retry.js +56 -0
- package/dist/esm/sanitize.js +66 -0
- package/dist/esm/training/kg_scanner.js +115 -0
- package/dist/esm/training/scanner.js +27 -34
- package/dist/esm/validation/guardrails.js +5 -0
- package/dist/esm/version.js +33 -0
- package/dist/index.d.ts +21 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +45 -1
- package/dist/knowledge/api_surface.d.ts +12 -0
- package/dist/knowledge/api_surface.d.ts.map +1 -1
- package/dist/knowledge/api_surface.js +268 -34
- package/dist/knowledge/cluster_utils.d.ts +28 -0
- package/dist/knowledge/cluster_utils.d.ts.map +1 -0
- package/dist/knowledge/cluster_utils.js +67 -0
- package/dist/knowledge/failure_history.d.ts +39 -0
- package/dist/knowledge/failure_history.d.ts.map +1 -0
- package/dist/knowledge/failure_history.js +128 -0
- package/dist/knowledge/kg_bridge.d.ts +31 -0
- package/dist/knowledge/kg_bridge.d.ts.map +1 -0
- package/dist/knowledge/kg_bridge.js +388 -0
- package/dist/knowledge/kg_types.d.ts +75 -0
- package/dist/knowledge/kg_types.d.ts.map +1 -0
- package/dist/knowledge/kg_types.js +4 -0
- package/dist/knowledge/route_families.d.ts +29 -0
- package/dist/knowledge/route_families.d.ts.map +1 -1
- package/dist/knowledge/route_families.js +122 -0
- package/dist/mcp-server.d.ts.map +1 -1
- package/dist/mcp-server.js +2 -4
- package/dist/metrics/prometheus.d.ts +37 -0
- package/dist/metrics/prometheus.d.ts.map +1 -0
- package/dist/metrics/prometheus.js +153 -0
- package/dist/model_router.d.ts +28 -0
- package/dist/model_router.d.ts.map +1 -0
- package/dist/model_router.js +63 -0
- package/dist/ollama_provider.d.ts.map +1 -1
- package/dist/ollama_provider.js +1 -0
- package/dist/openai_provider.d.ts.map +1 -1
- package/dist/openai_provider.js +1 -0
- package/dist/pipeline/orchestrator.d.ts +2 -0
- package/dist/pipeline/orchestrator.d.ts.map +1 -1
- package/dist/pipeline/orchestrator.js +6 -12
- package/dist/pipeline/stage0_preprocess.d.ts.map +1 -1
- package/dist/pipeline/stage0_preprocess.js +11 -18
- package/dist/pipeline/stage1_impact.d.ts +1 -1
- package/dist/pipeline/stage1_impact.d.ts.map +1 -1
- package/dist/pipeline/stage1_impact.js +18 -2
- package/dist/pipeline/stage2_coverage.d.ts +2 -0
- package/dist/pipeline/stage2_coverage.d.ts.map +1 -1
- package/dist/pipeline/stage2_coverage.js +29 -7
- package/dist/pipeline/stage3_generation.d.ts +2 -0
- package/dist/pipeline/stage3_generation.d.ts.map +1 -1
- package/dist/pipeline/stage3_generation.js +21 -1
- package/dist/pipeline/stage4_heal.d.ts +2 -0
- package/dist/pipeline/stage4_heal.d.ts.map +1 -1
- package/dist/progress.d.ts +22 -0
- package/dist/progress.d.ts.map +1 -0
- package/dist/progress.js +116 -0
- package/dist/prompts/coverage.d.ts +2 -0
- package/dist/prompts/coverage.d.ts.map +1 -1
- package/dist/prompts/coverage.js +17 -24
- package/dist/prompts/cross-impact.d.ts +1 -0
- package/dist/prompts/cross-impact.d.ts.map +1 -1
- package/dist/prompts/cross-impact.js +3 -21
- package/dist/prompts/generation.d.ts +4 -2
- package/dist/prompts/generation.d.ts.map +1 -1
- package/dist/prompts/generation.js +201 -45
- package/dist/prompts/generation_profile.d.ts +29 -0
- package/dist/prompts/generation_profile.d.ts.map +1 -0
- package/dist/prompts/generation_profile.js +151 -0
- package/dist/prompts/heal.d.ts +3 -1
- package/dist/prompts/heal.d.ts.map +1 -1
- package/dist/prompts/heal.js +33 -15
- package/dist/prompts/impact.d.ts +1 -0
- package/dist/prompts/impact.d.ts.map +1 -1
- package/dist/prompts/impact.js +3 -22
- package/dist/prompts/json_extract.d.ts +14 -0
- package/dist/prompts/json_extract.d.ts.map +1 -0
- package/dist/prompts/json_extract.js +39 -0
- package/dist/prompts/strategist.d.ts.map +1 -1
- package/dist/prompts/strategist.js +2 -20
- package/dist/prompts/test-designer.d.ts +2 -0
- package/dist/prompts/test-designer.d.ts.map +1 -1
- package/dist/prompts/test-designer.js +6 -21
- package/dist/provider_factory.d.ts.map +1 -1
- package/dist/provider_factory.js +6 -4
- package/dist/reporters/junit.d.ts +6 -0
- package/dist/reporters/junit.d.ts.map +1 -0
- package/dist/reporters/junit.js +89 -0
- package/dist/reporters/reporter.d.ts +42 -0
- package/dist/reporters/reporter.d.ts.map +1 -0
- package/dist/reporters/reporter.js +4 -0
- package/dist/reporters/sarif.d.ts +7 -0
- package/dist/reporters/sarif.d.ts.map +1 -0
- package/dist/reporters/sarif.js +134 -0
- package/dist/resilience/circuit_breaker.d.ts +36 -0
- package/dist/resilience/circuit_breaker.d.ts.map +1 -0
- package/dist/resilience/circuit_breaker.js +82 -0
- package/dist/resilience/retry.d.ts +11 -0
- package/dist/resilience/retry.d.ts.map +1 -0
- package/dist/resilience/retry.js +59 -0
- package/dist/sanitize.d.ts +15 -0
- package/dist/sanitize.d.ts.map +1 -0
- package/dist/sanitize.js +71 -0
- package/dist/training/kg_scanner.d.ts +13 -0
- package/dist/training/kg_scanner.d.ts.map +1 -0
- package/dist/training/kg_scanner.js +118 -0
- package/dist/training/scanner.d.ts +7 -2
- package/dist/training/scanner.d.ts.map +1 -1
- package/dist/training/scanner.js +27 -34
- package/dist/validation/guardrails.d.ts +2 -0
- package/dist/validation/guardrails.d.ts.map +1 -1
- package/dist/validation/guardrails.js +5 -0
- package/dist/validation/output_schema.d.ts +3 -0
- package/dist/validation/output_schema.d.ts.map +1 -1
- package/dist/version.d.ts +6 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +36 -0
- package/package.json +7 -2
- package/schemas/route-families.schema.json +31 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
2
2
|
// See LICENSE.txt for license information.
|
|
3
|
+
import { extractJsonFromResponse } from './json_extract.js';
|
|
3
4
|
import { formatSpecsForPrompt } from '../knowledge/spec_index.js';
|
|
4
5
|
import { formatApiSurfaceForPrompt } from '../knowledge/api_surface.js';
|
|
5
6
|
export function buildImpactPrompt(ctx) {
|
|
@@ -23,7 +24,7 @@ export function buildImpactPrompt(ctx) {
|
|
|
23
24
|
})
|
|
24
25
|
.join('\n\n');
|
|
25
26
|
return [
|
|
26
|
-
|
|
27
|
+
`You are analyzing code changes in ${ctx.projectName || 'Mattermost'} to identify impacted user-facing flows.`,
|
|
27
28
|
'',
|
|
28
29
|
`ROUTE FAMILY: ${ctx.family.id}`,
|
|
29
30
|
`ROUTES: ${familyRoutes}`,
|
|
@@ -58,25 +59,5 @@ export function buildImpactPrompt(ctx) {
|
|
|
58
59
|
].filter(Boolean).join('\n');
|
|
59
60
|
}
|
|
60
61
|
export function parseImpactResponse(text) {
|
|
61
|
-
|
|
62
|
-
const fenced = text.match(/```(?:json)?\s*([\s\S]*?)```/i);
|
|
63
|
-
const candidates = fenced ? [fenced[1], text] : [text];
|
|
64
|
-
for (const candidate of candidates) {
|
|
65
|
-
const start = candidate.indexOf('{');
|
|
66
|
-
const end = candidate.lastIndexOf('}');
|
|
67
|
-
if (start < 0 || end <= start) {
|
|
68
|
-
continue;
|
|
69
|
-
}
|
|
70
|
-
const raw = candidate.slice(start, end + 1);
|
|
71
|
-
try {
|
|
72
|
-
const parsed = JSON.parse(raw);
|
|
73
|
-
if (parsed && Array.isArray(parsed.flows)) {
|
|
74
|
-
return parsed;
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
catch {
|
|
78
|
-
continue;
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
return null;
|
|
62
|
+
return extractJsonFromResponse(text, (obj) => obj != null && typeof obj === 'object' && Array.isArray(obj.flows));
|
|
82
63
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
2
|
+
// See LICENSE.txt for license information.
|
|
3
|
+
/**
|
|
4
|
+
* Shared JSON extraction from LLM text responses.
|
|
5
|
+
* Handles fenced code blocks, bare JSON, and partial text.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Extract and parse JSON from LLM response text.
|
|
9
|
+
* Tries fenced code blocks first, then raw text.
|
|
10
|
+
* Returns null if no valid JSON found.
|
|
11
|
+
*
|
|
12
|
+
* @param text - Raw LLM response text
|
|
13
|
+
* @param validate - Predicate to check if parsed object has the expected shape
|
|
14
|
+
*/
|
|
15
|
+
export function extractJsonFromResponse(text, validate) {
|
|
16
|
+
const fenced = text.match(/```(?:json)?\s*([\s\S]*?)```/i);
|
|
17
|
+
const candidates = fenced ? [fenced[1], text] : [text];
|
|
18
|
+
for (const candidate of candidates) {
|
|
19
|
+
const start = candidate.indexOf('{');
|
|
20
|
+
const end = candidate.lastIndexOf('}');
|
|
21
|
+
if (start < 0 || end <= start) {
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
const raw = candidate.slice(start, end + 1);
|
|
25
|
+
try {
|
|
26
|
+
const parsed = JSON.parse(raw);
|
|
27
|
+
if (validate(parsed)) {
|
|
28
|
+
return parsed;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
2
2
|
// See LICENSE.txt for license information.
|
|
3
3
|
import { sanitizeForPrompt } from '../crew/sanitize.js';
|
|
4
|
+
import { extractJsonFromResponse } from './json_extract.js';
|
|
4
5
|
export function buildStrategistPrompt(ctx) {
|
|
5
6
|
const flowsBlock = ctx.impactedFlows
|
|
6
7
|
.map((f) => {
|
|
@@ -56,24 +57,5 @@ export function buildStrategistPrompt(ctx) {
|
|
|
56
57
|
].join('\n');
|
|
57
58
|
}
|
|
58
59
|
export function parseStrategistResponse(text) {
|
|
59
|
-
|
|
60
|
-
const candidates = fenced ? [fenced[1], text] : [text];
|
|
61
|
-
for (const candidate of candidates) {
|
|
62
|
-
const start = candidate.indexOf('{');
|
|
63
|
-
const end = candidate.lastIndexOf('}');
|
|
64
|
-
if (start < 0 || end <= start) {
|
|
65
|
-
continue;
|
|
66
|
-
}
|
|
67
|
-
const raw = candidate.slice(start, end + 1);
|
|
68
|
-
try {
|
|
69
|
-
const parsed = JSON.parse(raw);
|
|
70
|
-
if (parsed && Array.isArray(parsed.strategy)) {
|
|
71
|
-
return parsed;
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
catch {
|
|
75
|
-
continue;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
return null;
|
|
60
|
+
return extractJsonFromResponse(text, (obj) => obj != null && typeof obj === 'object' && Array.isArray(obj.strategy));
|
|
79
61
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
2
2
|
// See LICENSE.txt for license information.
|
|
3
|
+
import { extractJsonFromResponse } from './json_extract.js';
|
|
3
4
|
import { formatApiSurfaceForPrompt } from '../knowledge/api_surface.js';
|
|
4
5
|
import { sanitizeForPrompt } from '../crew/sanitize.js';
|
|
5
6
|
export function buildTestDesignerPrompt(ctx) {
|
|
@@ -24,7 +25,7 @@ export function buildTestDesignerPrompt(ctx) {
|
|
|
24
25
|
: 'None detected.';
|
|
25
26
|
const categories = ctx.strategy.testCategories.join(', ');
|
|
26
27
|
return [
|
|
27
|
-
|
|
28
|
+
`You are a senior QA engineer designing comprehensive test cases for a ${ctx.profile?.projectName || 'Mattermost'} user flow.`,
|
|
28
29
|
'',
|
|
29
30
|
`FLOW: ${ctx.flow.flowName}`,
|
|
30
31
|
`Flow ID: ${ctx.flow.flowId}`,
|
|
@@ -84,24 +85,8 @@ export function buildTestDesignerPrompt(ctx) {
|
|
|
84
85
|
].join('\n');
|
|
85
86
|
}
|
|
86
87
|
export function parseTestDesignerResponse(text) {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
const end = candidate.lastIndexOf('}');
|
|
92
|
-
if (start < 0 || end <= start) {
|
|
93
|
-
continue;
|
|
94
|
-
}
|
|
95
|
-
const raw = candidate.slice(start, end + 1);
|
|
96
|
-
try {
|
|
97
|
-
const parsed = JSON.parse(raw);
|
|
98
|
-
if (parsed?.testDesign?.testCases && Array.isArray(parsed.testDesign.testCases)) {
|
|
99
|
-
return parsed;
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
catch {
|
|
103
|
-
continue;
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
return null;
|
|
88
|
+
return extractJsonFromResponse(text, (obj) => {
|
|
89
|
+
const r = obj;
|
|
90
|
+
return r?.testDesign?.testCases != null && Array.isArray(r.testDesign.testCases);
|
|
91
|
+
});
|
|
107
92
|
}
|
|
@@ -263,15 +263,17 @@ class HybridProvider {
|
|
|
263
263
|
const primaryStats = this.primary.getUsageStats();
|
|
264
264
|
const fallbackStats = this.fallback.getUsageStats();
|
|
265
265
|
// Combine stats
|
|
266
|
+
const totalRequests = primaryStats.requestCount + fallbackStats.requestCount;
|
|
266
267
|
return {
|
|
267
|
-
requestCount:
|
|
268
|
+
requestCount: totalRequests,
|
|
268
269
|
totalInputTokens: primaryStats.totalInputTokens + fallbackStats.totalInputTokens,
|
|
269
270
|
totalOutputTokens: primaryStats.totalOutputTokens + fallbackStats.totalOutputTokens,
|
|
270
271
|
totalTokens: primaryStats.totalTokens + fallbackStats.totalTokens,
|
|
271
272
|
totalCost: primaryStats.totalCost + fallbackStats.totalCost,
|
|
272
|
-
averageResponseTimeMs:
|
|
273
|
-
|
|
274
|
-
|
|
273
|
+
averageResponseTimeMs: totalRequests > 0
|
|
274
|
+
? (primaryStats.averageResponseTimeMs * primaryStats.requestCount +
|
|
275
|
+
fallbackStats.averageResponseTimeMs * fallbackStats.requestCount) / totalRequests
|
|
276
|
+
: 0,
|
|
275
277
|
failedRequests: primaryStats.failedRequests + fallbackStats.failedRequests,
|
|
276
278
|
startTime: new Date(Math.min(primaryStats.startTime.getTime(), fallbackStats.startTime.getTime())),
|
|
277
279
|
lastUpdated: new Date(Math.max(primaryStats.lastUpdated.getTime(), fallbackStats.lastUpdated.getTime())),
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
2
|
+
// See LICENSE.txt for license information.
|
|
3
|
+
function escapeXml(str) {
|
|
4
|
+
return str
|
|
5
|
+
.replace(/&/g, '&')
|
|
6
|
+
.replace(/</g, '<')
|
|
7
|
+
.replace(/>/g, '>')
|
|
8
|
+
.replace(/"/g, '"')
|
|
9
|
+
.replace(/'/g, ''');
|
|
10
|
+
}
|
|
11
|
+
function buildTestCase(tc, flowName) {
|
|
12
|
+
const className = escapeXml(flowName.replace(/\s+/g, '.'));
|
|
13
|
+
const testName = escapeXml(tc.name);
|
|
14
|
+
return ` <testcase classname="${className}" name="${testName}" status="${escapeXml(tc.priority)}">\n` +
|
|
15
|
+
` <properties>\n` +
|
|
16
|
+
` <property name="type" value="${escapeXml(tc.type)}" />\n` +
|
|
17
|
+
` <property name="priority" value="${escapeXml(tc.priority)}" />\n` +
|
|
18
|
+
` </properties>\n` +
|
|
19
|
+
` </testcase>`;
|
|
20
|
+
}
|
|
21
|
+
function buildFailureCase(finding) {
|
|
22
|
+
const name = escapeXml(finding.title);
|
|
23
|
+
return ` <testcase classname="findings" name="${name}">\n` +
|
|
24
|
+
` <failure message="${escapeXml(finding.title)}" type="${escapeXml(finding.severity)}">${escapeXml(finding.description)}</failure>\n` +
|
|
25
|
+
` </testcase>`;
|
|
26
|
+
}
|
|
27
|
+
export const junitReporter = {
|
|
28
|
+
name: 'junit',
|
|
29
|
+
extension: '.xml',
|
|
30
|
+
format(results) {
|
|
31
|
+
const suites = [];
|
|
32
|
+
// Build a lookup from flowName -> test cases
|
|
33
|
+
const designsByFlow = new Map();
|
|
34
|
+
for (const design of results.testDesigns) {
|
|
35
|
+
designsByFlow.set(design.flowName, design.testCases);
|
|
36
|
+
}
|
|
37
|
+
// High-severity findings as failure cases
|
|
38
|
+
const highFindings = results.findings.filter((f) => f.severity === 'high');
|
|
39
|
+
// Each strategy entry becomes a test suite
|
|
40
|
+
for (const entry of results.strategyEntries) {
|
|
41
|
+
const testCases = designsByFlow.get(entry.flowName) ?? [];
|
|
42
|
+
const failures = highFindings.filter((f) => f.title.includes(entry.flowName));
|
|
43
|
+
const totalTests = testCases.length + failures.length;
|
|
44
|
+
const casesXml = testCases.map((tc) => buildTestCase(tc, entry.flowName)).join('\n');
|
|
45
|
+
const failuresXml = failures.map((f) => buildFailureCase(f)).join('\n');
|
|
46
|
+
const allCases = [casesXml, failuresXml].filter(Boolean).join('\n');
|
|
47
|
+
// Warnings as system-out
|
|
48
|
+
const warningsText = results.warnings.length > 0
|
|
49
|
+
? ` <system-out>${escapeXml(results.warnings.join('\n'))}</system-out>`
|
|
50
|
+
: '';
|
|
51
|
+
suites.push(` <testsuite name="${escapeXml(entry.flowName)}" tests="${totalTests}" failures="${failures.length}" ` +
|
|
52
|
+
`id="${escapeXml(entry.flowId)}">\n` +
|
|
53
|
+
` <properties>\n` +
|
|
54
|
+
` <property name="priority" value="${escapeXml(entry.priority)}" />\n` +
|
|
55
|
+
` <property name="approach" value="${escapeXml(entry.approach)}" />\n` +
|
|
56
|
+
` <property name="rationale" value="${escapeXml(entry.rationale)}" />\n` +
|
|
57
|
+
` </properties>\n` +
|
|
58
|
+
(allCases ? allCases + '\n' : '') +
|
|
59
|
+
(warningsText ? warningsText + '\n' : '') +
|
|
60
|
+
` </testsuite>`);
|
|
61
|
+
}
|
|
62
|
+
// Remaining high findings not tied to a strategy entry
|
|
63
|
+
const coveredFlowNames = new Set(results.strategyEntries.map((e) => e.flowName));
|
|
64
|
+
const uncoveredFindings = highFindings.filter((f) => !Array.from(coveredFlowNames).some((name) => f.title.includes(name)));
|
|
65
|
+
if (uncoveredFindings.length > 0) {
|
|
66
|
+
const failureCases = uncoveredFindings.map((f) => buildFailureCase(f)).join('\n');
|
|
67
|
+
suites.push(` <testsuite name="findings" tests="${uncoveredFindings.length}" failures="${uncoveredFindings.length}">\n` +
|
|
68
|
+
failureCases + '\n' +
|
|
69
|
+
` </testsuite>`);
|
|
70
|
+
}
|
|
71
|
+
const totalTests = results.testDesigns.reduce((sum, d) => sum + d.testCases.length, 0) + highFindings.length;
|
|
72
|
+
const totalFailures = highFindings.length;
|
|
73
|
+
return `<?xml version="1.0" encoding="UTF-8"?>\n` +
|
|
74
|
+
`<testsuites name="e2e-agents: ${escapeXml(results.workflow)}" ` +
|
|
75
|
+
`tests="${totalTests}" failures="${totalFailures}" ` +
|
|
76
|
+
`time="0">\n` +
|
|
77
|
+
` <properties>\n` +
|
|
78
|
+
` <property name="changedFiles" value="${results.changedFiles}" />\n` +
|
|
79
|
+
` <property name="impactedFlows" value="${results.impactedFlows}" />\n` +
|
|
80
|
+
` <property name="cost" value="${results.cost}" />\n` +
|
|
81
|
+
` <property name="tokens" value="${results.tokens}" />\n` +
|
|
82
|
+
` </properties>\n` +
|
|
83
|
+
suites.join('\n') + '\n' +
|
|
84
|
+
`</testsuites>\n`;
|
|
85
|
+
},
|
|
86
|
+
};
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
2
|
+
// See LICENSE.txt for license information.
|
|
3
|
+
function severityToLevel(severity) {
|
|
4
|
+
switch (severity.toLowerCase()) {
|
|
5
|
+
case 'high':
|
|
6
|
+
case 'critical':
|
|
7
|
+
return 'error';
|
|
8
|
+
case 'medium':
|
|
9
|
+
return 'warning';
|
|
10
|
+
case 'low':
|
|
11
|
+
case 'info':
|
|
12
|
+
return 'note';
|
|
13
|
+
default:
|
|
14
|
+
return 'note';
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function riskToLevel(risk) {
|
|
18
|
+
switch (risk.toLowerCase()) {
|
|
19
|
+
case 'high':
|
|
20
|
+
return 'warning';
|
|
21
|
+
case 'medium':
|
|
22
|
+
return 'note';
|
|
23
|
+
default:
|
|
24
|
+
return 'none';
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export const sarifReporter = {
|
|
28
|
+
name: 'sarif',
|
|
29
|
+
extension: '.sarif',
|
|
30
|
+
format(results) {
|
|
31
|
+
const rules = [];
|
|
32
|
+
const sarifResults = [];
|
|
33
|
+
const ruleIds = new Set();
|
|
34
|
+
function ensureRule(id, description, level) {
|
|
35
|
+
if (!ruleIds.has(id)) {
|
|
36
|
+
ruleIds.add(id);
|
|
37
|
+
rules.push({
|
|
38
|
+
id,
|
|
39
|
+
shortDescription: { text: description },
|
|
40
|
+
defaultConfiguration: { level },
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
// Findings -> results
|
|
45
|
+
for (const finding of results.findings) {
|
|
46
|
+
const level = severityToLevel(finding.severity);
|
|
47
|
+
const ruleId = `finding/${finding.severity}`;
|
|
48
|
+
ensureRule(ruleId, `Finding (${finding.severity})`, level);
|
|
49
|
+
sarifResults.push({
|
|
50
|
+
ruleId,
|
|
51
|
+
level,
|
|
52
|
+
message: { text: `${finding.title}: ${finding.description}` },
|
|
53
|
+
properties: {
|
|
54
|
+
severity: finding.severity,
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
// Strategy entries without matching test designs -> coverage gap results
|
|
59
|
+
const designedFlows = new Set(results.testDesigns.map((d) => d.flowName));
|
|
60
|
+
for (const entry of results.strategyEntries) {
|
|
61
|
+
if (!designedFlows.has(entry.flowName)) {
|
|
62
|
+
const ruleId = 'coverage/gap';
|
|
63
|
+
ensureRule(ruleId, 'Missing test coverage for impacted flow', 'warning');
|
|
64
|
+
sarifResults.push({
|
|
65
|
+
ruleId,
|
|
66
|
+
level: 'warning',
|
|
67
|
+
message: {
|
|
68
|
+
text: `Flow "${entry.flowName}" (${entry.flowId}) has strategy but no test design. ` +
|
|
69
|
+
`Priority: ${entry.priority}, approach: ${entry.approach}.`,
|
|
70
|
+
},
|
|
71
|
+
properties: {
|
|
72
|
+
flowId: entry.flowId,
|
|
73
|
+
priority: entry.priority,
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
// High-risk cross-impacts -> warning results
|
|
79
|
+
for (const impact of results.crossImpacts) {
|
|
80
|
+
const level = riskToLevel(impact.riskLevel);
|
|
81
|
+
if (level === 'none') {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
const ruleId = `cross-impact/${impact.riskLevel}`;
|
|
85
|
+
ensureRule(ruleId, `Cross-impact (${impact.riskLevel} risk)`, level);
|
|
86
|
+
sarifResults.push({
|
|
87
|
+
ruleId,
|
|
88
|
+
level,
|
|
89
|
+
message: {
|
|
90
|
+
text: `Cross-impact: "${impact.sourceFamily}" affects "${impact.affectedFamily}" ` +
|
|
91
|
+
`with ${impact.riskLevel} risk.`,
|
|
92
|
+
},
|
|
93
|
+
properties: {
|
|
94
|
+
sourceFamily: impact.sourceFamily,
|
|
95
|
+
affectedFamily: impact.affectedFamily,
|
|
96
|
+
riskLevel: impact.riskLevel,
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
const run = {
|
|
101
|
+
tool: {
|
|
102
|
+
driver: {
|
|
103
|
+
name: 'e2e-agents',
|
|
104
|
+
version: '1.8.5',
|
|
105
|
+
informationUri: 'https://github.com/mattermost/e2e-agents',
|
|
106
|
+
rules,
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
results: sarifResults,
|
|
110
|
+
invocations: [
|
|
111
|
+
{
|
|
112
|
+
executionSuccessful: true,
|
|
113
|
+
properties: {
|
|
114
|
+
workflow: results.workflow,
|
|
115
|
+
changedFiles: results.changedFiles,
|
|
116
|
+
impactedFlows: results.impactedFlows,
|
|
117
|
+
cost: results.cost,
|
|
118
|
+
tokens: results.tokens,
|
|
119
|
+
warnings: results.warnings,
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
],
|
|
123
|
+
};
|
|
124
|
+
const sarif = {
|
|
125
|
+
$schema: 'https://raw.githubusercontent.com/oasis-tcs/sarif-spec/main/sarif-2.1/schema/sarif-schema-2.1.0.json',
|
|
126
|
+
version: '2.1.0',
|
|
127
|
+
runs: [run],
|
|
128
|
+
};
|
|
129
|
+
return JSON.stringify(sarif, null, 2) + '\n';
|
|
130
|
+
},
|
|
131
|
+
};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
2
|
+
// See LICENSE.txt for license information.
|
|
3
|
+
const DEFAULT_CONFIG = {
|
|
4
|
+
failureThreshold: 3,
|
|
5
|
+
cooldownMs: 60000,
|
|
6
|
+
};
|
|
7
|
+
export class CircuitBreaker {
|
|
8
|
+
constructor(config = {}) {
|
|
9
|
+
this.state = 'closed';
|
|
10
|
+
this.failures = 0;
|
|
11
|
+
this.lastFailureTime = 0;
|
|
12
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
13
|
+
}
|
|
14
|
+
/** Returns the derived state without mutating internal state. */
|
|
15
|
+
get currentState() {
|
|
16
|
+
if (this.state === 'open' && Date.now() - this.lastFailureTime >= this.config.cooldownMs) {
|
|
17
|
+
return 'half-open';
|
|
18
|
+
}
|
|
19
|
+
return this.state;
|
|
20
|
+
}
|
|
21
|
+
get isOpen() {
|
|
22
|
+
return this.currentState === 'open';
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Execute a function with circuit breaker protection.
|
|
26
|
+
* If the circuit is open, the fallback is called instead.
|
|
27
|
+
*/
|
|
28
|
+
async call(fn, fallback) {
|
|
29
|
+
// Transition from open to half-open if cooldown has elapsed
|
|
30
|
+
if (this.state === 'open') {
|
|
31
|
+
if (Date.now() - this.lastFailureTime >= this.config.cooldownMs) {
|
|
32
|
+
this.state = 'half-open';
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
return fallback();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
// At this point state is 'closed' or 'half-open'
|
|
39
|
+
const stateBeforeCall = this.state;
|
|
40
|
+
try {
|
|
41
|
+
const result = await fn();
|
|
42
|
+
this.onSuccess();
|
|
43
|
+
return result;
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
const shouldCount = !this.config.shouldCount || this.config.shouldCount(error);
|
|
47
|
+
if (shouldCount) {
|
|
48
|
+
this.onFailure();
|
|
49
|
+
}
|
|
50
|
+
// In half-open state, a failure re-opens the circuit
|
|
51
|
+
if (stateBeforeCall === 'half-open') {
|
|
52
|
+
throw error;
|
|
53
|
+
}
|
|
54
|
+
// In closed state, if failures hit threshold the circuit opened
|
|
55
|
+
if (shouldCount && this.failures >= this.config.failureThreshold) {
|
|
56
|
+
return fallback();
|
|
57
|
+
}
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
onSuccess() {
|
|
62
|
+
this.failures = 0;
|
|
63
|
+
this.state = 'closed';
|
|
64
|
+
}
|
|
65
|
+
onFailure() {
|
|
66
|
+
this.failures++;
|
|
67
|
+
this.lastFailureTime = Date.now();
|
|
68
|
+
if (this.failures >= this.config.failureThreshold) {
|
|
69
|
+
this.state = 'open';
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/** Reset the circuit breaker to closed state */
|
|
73
|
+
reset() {
|
|
74
|
+
this.state = 'closed';
|
|
75
|
+
this.failures = 0;
|
|
76
|
+
this.lastFailureTime = 0;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
2
|
+
// See LICENSE.txt for license information.
|
|
3
|
+
const DEFAULT_RETRY_CONFIG = {
|
|
4
|
+
maxRetries: 2,
|
|
5
|
+
baseDelayMs: 1000,
|
|
6
|
+
maxDelayMs: 10000,
|
|
7
|
+
jitter: true,
|
|
8
|
+
};
|
|
9
|
+
/** Errors that should be retried (transient failures) */
|
|
10
|
+
function isRetryable(error) {
|
|
11
|
+
if (!(error instanceof Error))
|
|
12
|
+
return false;
|
|
13
|
+
const msg = error.message.toLowerCase();
|
|
14
|
+
// Rate limits
|
|
15
|
+
if (msg.includes('rate limit') || msg.includes('429') || msg.includes('too many requests'))
|
|
16
|
+
return true;
|
|
17
|
+
// Server errors
|
|
18
|
+
if (msg.includes('500') || msg.includes('502') || msg.includes('503') || msg.includes('504'))
|
|
19
|
+
return true;
|
|
20
|
+
if (msg.includes('internal server error') || msg.includes('bad gateway') || msg.includes('service unavailable'))
|
|
21
|
+
return true;
|
|
22
|
+
// Network errors
|
|
23
|
+
if (msg.includes('econnreset') || msg.includes('econnrefused') || msg.includes('etimedout'))
|
|
24
|
+
return true;
|
|
25
|
+
if (msg.includes('socket hang up') || msg.includes('network error'))
|
|
26
|
+
return true;
|
|
27
|
+
// Overloaded
|
|
28
|
+
if (msg.includes('overloaded') || msg.includes('capacity'))
|
|
29
|
+
return true;
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
function computeDelay(attempt, config) {
|
|
33
|
+
const exponential = Math.min(config.baseDelayMs * Math.pow(2, attempt), config.maxDelayMs);
|
|
34
|
+
if (!config.jitter)
|
|
35
|
+
return exponential;
|
|
36
|
+
// Full jitter: random between 0 and exponential
|
|
37
|
+
return Math.floor(Math.random() * exponential);
|
|
38
|
+
}
|
|
39
|
+
export async function withRetry(fn, config = {}) {
|
|
40
|
+
const cfg = { ...DEFAULT_RETRY_CONFIG, ...config };
|
|
41
|
+
let lastError;
|
|
42
|
+
for (let attempt = 0; attempt <= cfg.maxRetries; attempt++) {
|
|
43
|
+
try {
|
|
44
|
+
return await fn();
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
lastError = error;
|
|
48
|
+
if (attempt >= cfg.maxRetries || !isRetryable(error)) {
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
51
|
+
const delay = computeDelay(attempt, cfg);
|
|
52
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
throw lastError;
|
|
56
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
2
|
+
// See LICENSE.txt for license information.
|
|
3
|
+
/**
|
|
4
|
+
* Secret scanning and sanitization utilities.
|
|
5
|
+
* Prevents API keys and credentials from leaking into artifacts, logs, and output.
|
|
6
|
+
*
|
|
7
|
+
* Patterns are stored WITHOUT the global flag to avoid shared mutable lastIndex state.
|
|
8
|
+
* New RegExp instances with /g are created per call for safe concurrent usage.
|
|
9
|
+
*/
|
|
10
|
+
const SECRET_PATTERNS = [
|
|
11
|
+
// Anthropic API keys (must be checked before generic sk- pattern)
|
|
12
|
+
/sk-ant-[a-zA-Z0-9_-]{20,}/,
|
|
13
|
+
// OpenAI API keys (negative lookahead to avoid matching Anthropic keys)
|
|
14
|
+
/sk-(?!ant-)[a-zA-Z0-9]{20,}/,
|
|
15
|
+
// Generic API key patterns
|
|
16
|
+
/(?:api[_-]?key|api[_-]?secret|access[_-]?token|auth[_-]?token)['":\s=]+['"]?([a-zA-Z0-9_\-./]{20,})['"]?/i,
|
|
17
|
+
// Bearer tokens
|
|
18
|
+
/Bearer\s+[a-zA-Z0-9_\-./]{20,}/,
|
|
19
|
+
// AWS keys
|
|
20
|
+
/AKIA[0-9A-Z]{16}/,
|
|
21
|
+
// GitHub tokens
|
|
22
|
+
/gh[ps]_[a-zA-Z0-9]{36,}/,
|
|
23
|
+
/github_pat_[a-zA-Z0-9_]{22,}/,
|
|
24
|
+
];
|
|
25
|
+
/**
|
|
26
|
+
* Sanitize a string by replacing detected secrets with [REDACTED].
|
|
27
|
+
*/
|
|
28
|
+
export function sanitizeSecrets(text) {
|
|
29
|
+
let result = text;
|
|
30
|
+
for (const pattern of SECRET_PATTERNS) {
|
|
31
|
+
result = result.replace(new RegExp(pattern, 'gi'), '[REDACTED]');
|
|
32
|
+
}
|
|
33
|
+
return result;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Check if a string contains any detectable secrets.
|
|
37
|
+
*/
|
|
38
|
+
export function containsSecrets(text) {
|
|
39
|
+
for (const pattern of SECRET_PATTERNS) {
|
|
40
|
+
if (new RegExp(pattern, 'i').test(text))
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Deep-sanitize a JSON-serializable object.
|
|
47
|
+
* Recursively walks all string values and sanitizes them.
|
|
48
|
+
* Tracks seen objects to prevent stack overflow on circular references.
|
|
49
|
+
*/
|
|
50
|
+
export function sanitizeObject(obj, _seen) {
|
|
51
|
+
if (typeof obj === 'string')
|
|
52
|
+
return sanitizeSecrets(obj);
|
|
53
|
+
if (obj === null || typeof obj !== 'object')
|
|
54
|
+
return obj;
|
|
55
|
+
const seen = _seen ?? new WeakSet();
|
|
56
|
+
if (seen.has(obj))
|
|
57
|
+
return '[Circular]';
|
|
58
|
+
seen.add(obj);
|
|
59
|
+
if (Array.isArray(obj))
|
|
60
|
+
return obj.map((item) => sanitizeObject(item, seen));
|
|
61
|
+
const result = {};
|
|
62
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
63
|
+
result[key] = sanitizeObject(value, seen);
|
|
64
|
+
}
|
|
65
|
+
return result;
|
|
66
|
+
}
|