@skyramp/mcp 0.3.8 → 0.4.0-rc.1
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/build/commands/commandLibrary.d.ts +1 -1
- package/build/commands/commandLibrary.js +3 -3
- package/build/commands/recommendTestsAndExecuteCommand.d.ts +1 -1
- package/build/commands/recommendTestsAndExecuteCommand.js +35 -20
- package/build/commands/testThisEndpointCommand.js +35 -19
- package/build/index.js +9 -3
- package/build/playwright/blueprintDigest.d.ts +15 -0
- package/build/playwright/blueprintDigest.js +152 -0
- package/build/playwright/blueprintDigestStore.d.ts +31 -0
- package/build/playwright/blueprintDigestStore.js +117 -0
- package/build/playwright/registerPlaywrightTools.js +60 -12
- package/build/playwright/traceRecordingPrompt.js +8 -7
- package/build/prompts/enhance-assertions/sharedAssertionRules.js +9 -8
- package/build/prompts/enhance-assertions/uiAssertionsPrompt.js +24 -2
- package/build/prompts/promptAssets.d.ts +20 -0
- package/build/prompts/promptAssets.js +55 -0
- package/build/prompts/sut-setup/modes/dockerComposePrompt.js +19 -5
- package/build/prompts/test-maintenance/actionsInstructions.d.ts +4 -0
- package/build/prompts/test-maintenance/actionsInstructions.js +14 -2
- package/build/prompts/test-maintenance/drift-analysis-prompt.d.ts +0 -10
- package/build/prompts/test-maintenance/drift-analysis-prompt.js +2 -11
- package/build/prompts/test-maintenance/uiDriftAnalysisSections.js +8 -4
- package/build/prompts/test-recommendation/diffExecutionPlan.d.ts +5 -22
- package/build/prompts/test-recommendation/diffExecutionPlan.js +37 -465
- package/build/prompts/test-recommendation/recommendationSections.d.ts +7 -17
- package/build/prompts/test-recommendation/recommendationSections.js +67 -309
- package/build/prompts/test-recommendation/recommendationShared.d.ts +19 -47
- package/build/prompts/test-recommendation/recommendationShared.js +49 -155
- package/build/prompts/test-recommendation/registerRecommendTestsPrompt.d.ts +0 -5
- package/build/prompts/test-recommendation/registerRecommendTestsPrompt.js +10 -153
- package/build/prompts/test-recommendation/test-recommendation-prompt.d.ts +2 -29
- package/build/prompts/test-recommendation/test-recommendation-prompt.js +32 -457
- package/build/prompts/testbot/planDeclarations.d.ts +6 -0
- package/build/prompts/testbot/planDeclarations.js +9 -0
- package/build/prompts/testbot/testbot-prompts.d.ts +8 -0
- package/build/prompts/testbot/testbot-prompts.js +256 -381
- package/build/recommendation/answers.d.ts +35 -0
- package/build/recommendation/answers.js +96 -0
- package/build/recommendation/registerPlan.d.ts +49 -0
- package/build/recommendation/registerPlan.js +117 -0
- package/build/recommendation/runVerifiers.d.ts +10 -0
- package/build/recommendation/runVerifiers.js +49 -0
- package/build/recommendation/subjectStep.d.ts +42 -0
- package/build/recommendation/subjectStep.js +86 -0
- package/build/recommendation/types.d.ts +163 -0
- package/build/recommendation/types.js +20 -0
- package/build/recommendation/verifierContracts.d.ts +382 -0
- package/build/recommendation/verifierContracts.js +263 -0
- package/build/recommendation/verifiers/changedFile.d.ts +2 -0
- package/build/recommendation/verifiers/changedFile.js +82 -0
- package/build/recommendation/verifiers/citedPath.d.ts +12 -0
- package/build/recommendation/verifiers/citedPath.js +35 -0
- package/build/recommendation/verifiers/coverage.d.ts +7 -0
- package/build/recommendation/verifiers/coverage.js +617 -0
- package/build/recommendation/verifiers/deliveredMatchesPlan.d.ts +11 -0
- package/build/recommendation/verifiers/deliveredMatchesPlan.js +33 -0
- package/build/recommendation/verifiers/endpointGrounded.d.ts +17 -0
- package/build/recommendation/verifiers/endpointGrounded.js +128 -0
- package/build/recommendation/verifiers/existingCoverage.d.ts +6 -0
- package/build/recommendation/verifiers/existingCoverage.js +51 -0
- package/build/recommendation/verifiers/expectedOutcome.d.ts +31 -0
- package/build/recommendation/verifiers/expectedOutcome.js +105 -0
- package/build/recommendation/verifiers/removedElementGuarded.d.ts +2 -0
- package/build/recommendation/verifiers/removedElementGuarded.js +57 -0
- package/build/recommendation/verifiers/reportedCategory.d.ts +26 -0
- package/build/recommendation/verifiers/reportedCategory.js +84 -0
- package/build/recommendation/verifiers/screenRoute.d.ts +10 -0
- package/build/recommendation/verifiers/screenRoute.js +118 -0
- package/build/recommendation/verifiers/statedDifference.d.ts +6 -0
- package/build/recommendation/verifiers/statedDifference.js +140 -0
- package/build/recommendation/verifiers/uiElementGrounded.d.ts +7 -0
- package/build/recommendation/verifiers/uiElementGrounded.js +318 -0
- package/build/resources/analysisResources.js +1 -114
- package/build/resources/testbotResource.js +23 -13
- package/build/services/ModularizationService.js +2 -1
- package/build/services/TestDiscoveryService.d.ts +3 -72
- package/build/services/TestDiscoveryService.js +10 -303
- package/build/services/containerEnv.d.ts +1 -1
- package/build/services/containerEnv.js +12 -0
- package/build/skills/fixTestImportErrorsSkill.d.ts +13 -0
- package/build/skills/fixTestImportErrorsSkill.js +20 -0
- package/build/toolNames.d.ts +1 -0
- package/build/toolNames.js +1 -0
- package/build/tools/code-refactor/enhanceAssertionsTool.js +3 -3
- package/build/tools/code-refactor/modularizationTool.js +2 -1
- package/build/tools/executeSkyrampTestTool.d.ts +80 -0
- package/build/tools/executeSkyrampTestTool.js +246 -19
- package/build/tools/generate-tests/generateBatchScenarioRestTool.js +6 -0
- package/build/tools/generate-tests/generateContractRestTool.js +3 -3
- package/build/tools/generate-tests/planGuard.d.ts +2 -2
- package/build/tools/generate-tests/planGuard.js +78 -18
- package/build/tools/one-click/oneClickTool.d.ts +0 -1
- package/build/tools/one-click/oneClickTool.js +0 -5
- package/build/tools/submitReportTool.d.ts +48 -42
- package/build/tools/submitReportTool.js +576 -193
- package/build/tools/test-management/actionsTool.js +72 -4
- package/build/tools/test-management/analyzeChangesTool.d.ts +144 -48
- package/build/tools/test-management/analyzeChangesTool.js +212 -1219
- package/build/tools/test-management/analyzeTestHealthTool.js +13 -24
- package/build/tools/test-management/index.d.ts +1 -0
- package/build/tools/test-management/index.js +1 -0
- package/build/tools/test-management/registerTestPlanTool.d.ts +795 -172
- package/build/tools/test-management/registerTestPlanTool.js +609 -542
- package/build/tools/test-management/resolveScreenTool.d.ts +75 -0
- package/build/tools/test-management/resolveScreenTool.js +289 -0
- package/build/types/BlueprintDigest.d.ts +34 -0
- package/build/types/BlueprintDigest.js +1 -0
- package/build/types/RepositoryAnalysis.d.ts +20 -1559
- package/build/types/RepositoryAnalysis.js +2 -58
- package/build/types/StepMethod.d.ts +40 -0
- package/build/types/StepMethod.js +77 -0
- package/build/types/TestAnalysis.d.ts +12 -0
- package/build/types/TestExecution.d.ts +4 -0
- package/build/types/TestRecommendation.d.ts +24 -24
- package/build/types/TestRecommendation.js +91 -89
- package/build/types/TestbotPromptOptions.d.ts +0 -4
- package/build/types/TestbotReport.d.ts +64 -2
- package/build/utils/AnalysisStateManager.d.ts +79 -113
- package/build/utils/AnalysisStateManager.js +147 -57
- package/build/utils/assertion-verify/api-shared-lints.js +1 -1
- package/build/utils/assertion-verify/metrics.js +85 -36
- package/build/utils/assertion-verify/ui-lints.d.ts +0 -5
- package/build/utils/assertion-verify/ui-lints.js +32 -0
- package/build/utils/branchDiff.d.ts +63 -31
- package/build/utils/branchDiff.js +242 -94
- package/build/utils/containedPath.d.ts +18 -0
- package/build/utils/containedPath.js +73 -0
- package/build/utils/dartRouteExtractor.d.ts +18 -34
- package/build/utils/dartRouteExtractor.js +101 -173
- package/build/utils/featureFlags.d.ts +12 -0
- package/build/utils/featureFlags.js +14 -0
- package/build/utils/frontendSelectors.d.ts +48 -27
- package/build/utils/frontendSelectors.js +241 -80
- package/build/utils/pathMatching.d.ts +2 -4
- package/build/utils/pathMatching.js +2 -4
- package/build/utils/planMatchKeys.d.ts +38 -47
- package/build/utils/planMatchKeys.js +143 -81
- package/build/utils/rebaselineSnapshots.d.ts +24 -0
- package/build/utils/rebaselineSnapshots.js +65 -0
- package/build/utils/removedUiElements.d.ts +22 -0
- package/build/utils/removedUiElements.js +106 -0
- package/build/utils/reportVerification.d.ts +2 -6
- package/build/utils/reportVerification.js +61 -2
- package/build/utils/screenRoutes.d.ts +66 -0
- package/build/utils/screenRoutes.js +727 -0
- package/build/utils/sourceRouteExtractor.js +320 -112
- package/build/utils/testFileClassification.d.ts +11 -2
- package/build/utils/testFileClassification.js +44 -2
- package/build/utils/testFixtures.d.ts +5 -0
- package/build/utils/testFixtures.js +13 -0
- package/build/utils/utils.d.ts +0 -1
- package/build/utils/utils.js +0 -11
- package/build/utils/versions.d.ts +3 -3
- package/build/utils/versions.js +1 -1
- package/build/workspace/workspace.d.ts +12 -12
- package/node_modules/playwright/lib/mcp/skyramp/assertHiddenTool.js +56 -0
- package/node_modules/playwright/lib/mcp/skyramp/assertTool.js +2 -1
- package/node_modules/playwright/lib/mcp/skyramp/loadTraceTool.js +10 -0
- package/node_modules/playwright/lib/mcp/skyramp/skyRampImport.js +4 -1
- package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +160 -1
- package/node_modules/playwright/lib/mcp/test/skyRampExport.js +4 -2
- package/node_modules/playwright/node_modules/playwright-core/lib/server/codegen/skyramp/jsonlReader.js +1 -0
- package/node_modules/playwright/node_modules/playwright-core/lib/server/recorder/recorderSignalProcessor.js +2 -0
- package/node_modules/playwright/node_modules/playwright-core/lib/server/recorder.js +5 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{index.-Id052Lr.js → index.B7KbSQcC.js} +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.html +1 -1
- package/node_modules/playwright/node_modules/playwright-core/package.json +1 -1
- package/node_modules/playwright/node_modules/playwright-core/src/server/codegen/skyramp/jsonlReader.ts +1 -1
- package/node_modules/playwright/node_modules/playwright-core/src/server/recorder/recorderSignalProcessor.ts +7 -0
- package/node_modules/playwright/node_modules/playwright-core/src/server/recorder.ts +6 -1
- package/node_modules/playwright/package.json +1 -1
- package/package.json +4 -3
- package/plugin/.claude-plugin/plugin.json +8 -0
- package/plugin/plugin.json +6 -0
- package/plugin/prompts/declaring-a-plan.md +20 -0
- package/plugin/prompts/generate-tests/context-fetching.md +4 -0
- package/plugin/prompts/generate-tests/execution-plan.md +63 -0
- package/plugin/prompts/generate-tests/generation.md +108 -0
- package/plugin/prompts/generate-tests/path-parameters.md +1 -0
- package/plugin/prompts/generate-tests/reasoning-protocol.md +17 -0
- package/plugin/prompts/generate-tests/tool-workflow-variants.md +61 -0
- package/plugin/prompts/generate-tests/tool-workflows.md +65 -0
- package/plugin/prompts/plan-tests.md +42 -0
- package/plugin/prompts/testbot-task1.md +82 -0
- package/plugin/skills/fix-test-import-errors/SKILL.md +98 -0
- package/build/prompts/test-recommendation/analysisOutputPrompt.d.ts +0 -84
- package/build/prompts/test-recommendation/analysisOutputPrompt.js +0 -369
- package/build/prompts/test-recommendation/fullRepoCatalog.d.ts +0 -7
- package/build/prompts/test-recommendation/fullRepoCatalog.js +0 -283
- package/build/prompts/test-recommendation/scopeAssessment.d.ts +0 -81
- package/build/prompts/test-recommendation/scopeAssessment.js +0 -359
- package/build/recommendation/budgeters/diversityBalancedBudgeter.d.ts +0 -7
- package/build/recommendation/budgeters/diversityBalancedBudgeter.js +0 -105
- package/build/recommendation/budgeters/fixedNBudgeter.d.ts +0 -7
- package/build/recommendation/budgeters/fixedNBudgeter.js +0 -11
- package/build/recommendation/budgeters/shared.d.ts +0 -32
- package/build/recommendation/budgeters/shared.js +0 -246
- package/build/recommendation/discriminators.d.ts +0 -37
- package/build/recommendation/discriminators.js +0 -379
- package/build/recommendation/diversity.d.ts +0 -47
- package/build/recommendation/diversity.js +0 -101
- package/build/recommendation/planRanker.d.ts +0 -65
- package/build/recommendation/planRanker.js +0 -83
- package/build/recommendation/testFixtures.d.ts +0 -25
- package/build/recommendation/testFixtures.js +0 -45
- package/build/types/FrontendIntegration.d.ts +0 -28
- package/build/types/FrontendIntegration.js +0 -22
- package/build/types/Recommendation.d.ts +0 -146
- package/build/types/Recommendation.js +0 -74
- package/build/utils/changedRoutes.d.ts +0 -29
- package/build/utils/changedRoutes.js +0 -87
- package/build/utils/frontendIntegration.d.ts +0 -9
- package/build/utils/frontendIntegration.js +0 -243
- package/build/utils/importerHop.d.ts +0 -135
- package/build/utils/importerHop.js +0 -489
- package/build/utils/pathAffinityClassification.d.ts +0 -49
- package/build/utils/pathAffinityClassification.js +0 -180
- package/build/utils/pythonMountPrefixes.d.ts +0 -25
- package/build/utils/pythonMountPrefixes.js +0 -347
- package/build/utils/repoScanner.d.ts +0 -34
- package/build/utils/repoScanner.js +0 -300
- package/build/utils/routeParsers.d.ts +0 -95
- package/build/utils/routeParsers.js +0 -951
- package/build/utils/scenarioDrafting.d.ts +0 -92
- package/build/utils/scenarioDrafting.js +0 -951
- package/build/utils/subjectEndpoints.d.ts +0 -19
- package/build/utils/subjectEndpoints.js +0 -98
- package/build/utils/uiPageEnumerator.d.ts +0 -172
- package/build/utils/uiPageEnumerator.js +0 -474
|
@@ -3,26 +3,27 @@ import { logger } from "../utils/logger.js";
|
|
|
3
3
|
import * as fs from "fs/promises";
|
|
4
4
|
import * as path from "path";
|
|
5
5
|
import { AnalyticsService } from "../services/AnalyticsService.js";
|
|
6
|
-
import {
|
|
7
|
-
import { TestType
|
|
6
|
+
import { SCENARIO_CATEGORIES, categoryExpectsRed, } from "../types/TestRecommendation.js";
|
|
7
|
+
import { TestType } from "../types/TestTypes.js";
|
|
8
|
+
import { STEP_METHOD_DESCRIPTION, stepMethodSchema } from "../types/StepMethod.js";
|
|
8
9
|
import { DriftAction } from "../types/TestAnalysis.js";
|
|
9
10
|
import { TestExecutionStatus } from "../types/TestExecution.js";
|
|
10
11
|
import { IssueFoundCategory } from "../types/TestbotReport.js";
|
|
11
|
-
import { StateManager, runArtifactDir, getTestsRepoDir, } from "../utils/AnalysisStateManager.js";
|
|
12
|
+
import { StateManager, runArtifactDir, getTestsRepoDir, resolveRunStatePath, } from "../utils/AnalysisStateManager.js";
|
|
12
13
|
import { toolError, testFileMatches } from "../utils/utils.js";
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
14
|
+
import { isTestbotEnabled, } from "../utils/featureFlags.js";
|
|
15
|
+
import { answerFor, unknownAnswerObjections } from "../recommendation/answers.js";
|
|
16
|
+
import { checkDeliveredMatchesPlan } from "../recommendation/verifiers/deliveredMatchesPlan.js";
|
|
17
|
+
import { checkReportedCategoryMatchesPlan, checkRequirementConflictReported, } from "../recommendation/verifiers/reportedCategory.js";
|
|
18
|
+
import { checkExpectedOutcomeAfterExecution, } from "../recommendation/verifiers/expectedOutcome.js";
|
|
15
19
|
import { findInvalidSourceCitations, findUnchangedFileClaims, listChangedFiles, listChangedFilesAcross, listChangedFilesAbs, } from "../utils/reportVerification.js";
|
|
16
20
|
import { isPlanOnlyMode } from "../utils/planOnlyMode.js";
|
|
17
21
|
import { getReportLanguage, isEnforcedReportLanguage, findLanguageViolations, findLanguageNearMisses, reportLanguageDisplayName, } from "../utils/reportLanguage.js";
|
|
18
22
|
import { canonicalTestPath, findAssertionRecordByFileName, rederiveAssertionOutcome, } from "./code-refactor/assertion-state.js";
|
|
19
23
|
import { rederiveReuseOutcome, reuseChainSkipped, samePath, } from "./code-refactor/reuse-state.js";
|
|
20
24
|
import { retrofitGate, } from "./code-refactor/retrofit-state.js";
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
// contract) — UI and E2E are on a separate blueprint-grounded pipeline and are
|
|
24
|
-
// NOT gated at generation time (see planGuard.ts wiring), so they are excluded
|
|
25
|
-
// here too rather than surprising the agent with a report-time-only gate.
|
|
25
|
+
// Mirrors the tools wired to planGuard: UI and E2E are not gated at generation
|
|
26
|
+
// time, so gating them here would be a report-time-only surprise.
|
|
26
27
|
const PLAN_GATED_TEST_TYPES = new Set([
|
|
27
28
|
TestType.CONTRACT,
|
|
28
29
|
TestType.INTEGRATION,
|
|
@@ -41,44 +42,6 @@ const PLAN_GATED_TEST_TYPES = new Set([
|
|
|
41
42
|
* `RUN_STATE_FILE_NAME` / testbot's `STATE_FILE_NAME`.
|
|
42
43
|
*/
|
|
43
44
|
export const REPORT_FILE_NAME = "testbot-result.txt";
|
|
44
|
-
/**
|
|
45
|
-
* The names to match a report entry against the approved plan's `name:` keys.
|
|
46
|
-
* `testId` is documented as `<testType>-<scenario-slug>`, so the plan's bare
|
|
47
|
-
* scenarioName only matches once that prefix is dropped — but agents also submit
|
|
48
|
-
* the bare name (both forms appear in SKYR-4123's runs), so try each.
|
|
49
|
-
*/
|
|
50
|
-
function planNameCandidates(testId, testType) {
|
|
51
|
-
const id = (testId ?? "").trim();
|
|
52
|
-
if (!id)
|
|
53
|
-
return [];
|
|
54
|
-
const prefix = `${testType}-`;
|
|
55
|
-
return id.toLowerCase().startsWith(prefix)
|
|
56
|
-
? [id, id.slice(prefix.length)]
|
|
57
|
-
: [id];
|
|
58
|
-
}
|
|
59
|
-
/**
|
|
60
|
-
* Split an `endpoint` field into one {method, path} per endpoint it names.
|
|
61
|
-
* A multi-step test writes them comma-separated ("POST /orders, DELETE
|
|
62
|
-
* /orders/{id}"); read as a single endpoint the whole string becomes one
|
|
63
|
-
* unmatchable path, so such an entry could match nothing (SKYR-4123).
|
|
64
|
-
*/
|
|
65
|
-
function parseEndpointField(endpoint) {
|
|
66
|
-
const parts = (endpoint ?? "")
|
|
67
|
-
.split(",")
|
|
68
|
-
.map((p) => p.trim())
|
|
69
|
-
.filter(Boolean);
|
|
70
|
-
if (parts.length === 0)
|
|
71
|
-
return [{}];
|
|
72
|
-
return parts.map((part) => {
|
|
73
|
-
const spaceIdx = part.indexOf(" ");
|
|
74
|
-
if (spaceIdx <= 0)
|
|
75
|
-
return { path: part };
|
|
76
|
-
return {
|
|
77
|
-
method: part.slice(0, spaceIdx),
|
|
78
|
-
path: part.slice(spaceIdx + 1).trim(),
|
|
79
|
-
};
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
45
|
// Drift actions that actually modify a test file. VERIFY and IGNORE are
|
|
83
46
|
// no-ops (the test was assessed but left unchanged), so they must not count
|
|
84
47
|
// toward "tests maintained" telemetry.
|
|
@@ -87,17 +50,20 @@ const MAINTENANCE_CHANGE_ACTIONS = new Set([
|
|
|
87
50
|
DriftAction.Regenerate,
|
|
88
51
|
DriftAction.Delete,
|
|
89
52
|
]);
|
|
53
|
+
/** Objection ids for an untested declared change, `coverage:change:<id>`. The
|
|
54
|
+
* change table reads the agent's answer back off them. */
|
|
55
|
+
const CHANGE_OBJECTION_PREFIX = "coverage:change:";
|
|
90
56
|
const TOOL_NAME = "skyramp_submit_report";
|
|
91
57
|
const DEFAULT_COMMIT_MESSAGE = "Added recommendations by Skyramp Testbot.";
|
|
92
58
|
// Per-repo attribution. In a multi-repo run, every report item carries the
|
|
93
|
-
// owner/repo it pertains to (the primary's owner/repo for primary
|
|
94
|
-
// related repo's owner/repo for
|
|
59
|
+
// owner/repo it pertains to (the primary's owner/repo for primary objections, or a
|
|
60
|
+
// related repo's owner/repo for objections from that repo's diff). Whitespace-only
|
|
95
61
|
// values are normalized to undefined so a blank string is never treated as a
|
|
96
62
|
// distinct repo. Single-repo runs may omit it.
|
|
97
63
|
const repositoryField = z
|
|
98
64
|
.string()
|
|
99
65
|
.optional()
|
|
100
|
-
.describe("The 'owner/repo' this item pertains to (e.g. 'letsramp/api-insight'). In a multi-repo run, ALWAYS set it — the primary's owner/repo for primary-repo
|
|
66
|
+
.describe("The 'owner/repo' this item pertains to (e.g. 'letsramp/api-insight'). In a multi-repo run, ALWAYS set it — the primary's owner/repo for primary-repo objections, or a related repo's owner/repo for objections derived from that repository's diff. May be omitted only in single-repo runs.");
|
|
101
67
|
/** Normalize a `repository` attribution: trim, and map a blank/whitespace-only
|
|
102
68
|
* value to undefined so it is never serialized as a distinct empty repo
|
|
103
69
|
* (downstream consumers treat absence as "the primary repo"). */
|
|
@@ -116,7 +82,7 @@ const testResultSchema = z.object({
|
|
|
116
82
|
.describe("Type of test. Do not include priority or other metadata in this field."),
|
|
117
83
|
endpoint: z
|
|
118
84
|
.string()
|
|
119
|
-
.describe("HTTP verb and path, e.g. 'GET /api/v1/products'"),
|
|
85
|
+
.describe("For contract and integration tests, the HTTP verb and path, e.g. 'GET /api/v1/products'. For ui and e2e tests, the page path the test opens, e.g. '/notifications'."),
|
|
120
86
|
status: z.enum(["Pass", "Fail", "Skipped"]).describe("Test execution result"),
|
|
121
87
|
details: z
|
|
122
88
|
.string()
|
|
@@ -131,15 +97,23 @@ const testResultSchema = z.object({
|
|
|
131
97
|
message: "testFilePath must be an absolute path",
|
|
132
98
|
})
|
|
133
99
|
.describe("Absolute path of the test file this result is for — the same path you passed to skyramp_execute_test's testFile param. Consumers basename it for display."),
|
|
100
|
+
reachedBehavior: z
|
|
101
|
+
.boolean()
|
|
102
|
+
.optional()
|
|
103
|
+
.describe("For a failing test only. `true` when the request that carries the behaviour under test was sent and answered by the code under test, and the assertion that failed is about that behaviour. " +
|
|
104
|
+
"`false` when the failure came earlier: a 404, 401, 403 or 5xx on a route the change adds; an expired token or a sign-in page; a control or setup step that failed; a generator or SDK error. " +
|
|
105
|
+
"If your own `details` text says the request was not sent or an earlier step tripped, this is `false`. Omit only if you cannot tell."),
|
|
134
106
|
repository: repositoryField,
|
|
135
107
|
});
|
|
136
108
|
// Structured grounding for UI recommendations. Lifted verbatim from a
|
|
137
109
|
// `browser_blueprint` element captured during this run. Source of truth for
|
|
138
110
|
// codegen (prefers testId > stableId > role+name for locator selection),
|
|
139
|
-
// the verifier (
|
|
111
|
+
// the verifier (matches on `testId`, `stableId`, accessible name, or role plus
|
|
112
|
+
// a repeating row's `contextText` — it reads no other field here), and
|
|
140
113
|
// any future tooling that wants reliable grounding data without prose
|
|
141
114
|
// parsing. `reasoning` is free-form prose constrained by the Blueprint
|
|
142
|
-
// Citation Invariant — every element cited
|
|
115
|
+
// Citation Invariant — every element cited SHOULD appear in `targetElements`.
|
|
116
|
+
// Prose only: nothing checks it, and it is on the list of unchecked rules.
|
|
143
117
|
// See testbot prompt step 4 for the full populate-and-render rules.
|
|
144
118
|
/**
|
|
145
119
|
* Accept an omitted key as an explicit `null`.
|
|
@@ -158,7 +132,7 @@ export const targetElementSchema = z.object({
|
|
|
158
132
|
.describe("ARIA role (e.g. 'button', 'heading', 'textbox', 'link'). Lifted verbatim from the captured blueprint element's `role` field."),
|
|
159
133
|
accessibleName: z
|
|
160
134
|
.string()
|
|
161
|
-
.describe("Computed accessible name (e.g. 'Save changes', 'Order Details'). Lifted verbatim from the captured blueprint element's `accessibleName` field.
|
|
135
|
+
.describe("Computed accessible name (e.g. 'Save changes', 'Order Details'). Lifted verbatim from the captured blueprint element's `accessibleName` field. In a REPORT entry, match the bolded name in `reasoning` character-for-character. A plan planned test has no `reasoning` field, so that half does not apply there."),
|
|
162
136
|
testId: nullWhenAbsent(z.string()).describe("data-testid attribute value (preferred locator handle), or null when the element has no data-testid. Lifted from the blueprint element's `testId`."),
|
|
163
137
|
stableId: nullWhenAbsent(z.string()).describe("Unique HTML id attribute value (fallback locator when no testId), or null. Lifted from the blueprint element's `stableId`."),
|
|
164
138
|
contextText: nullWhenAbsent(z.array(z.string())).describe("Disambiguating row text for elements inside repeating sections (table rows, list items): the row's surrounding non-interactive text. Lifted from the blueprint repeatingElement's items[].contextText. null for non-repeating elements."),
|
|
@@ -181,21 +155,13 @@ export const pageContextSchema = z.object({
|
|
|
181
155
|
pageHash: z
|
|
182
156
|
.string()
|
|
183
157
|
.optional()
|
|
184
|
-
.describe("Opaque hash of the captured page state (BlueprintCapture.pageHash).
|
|
158
|
+
.describe("Opaque hash of the captured page state (BlueprintCapture.pageHash). Recorded for a reader; NOT checked — the element check pools every capture of the page rather than narrowing by hash, so a stale one costs nothing. The tool that verifies elements says the same."),
|
|
185
159
|
});
|
|
186
160
|
/**
|
|
187
|
-
*
|
|
188
|
-
*
|
|
189
|
-
*
|
|
190
|
-
*
|
|
191
|
-
* ~2m40s to exactly this):
|
|
192
|
-
* - `pageContext: null` is always stripped — the field is optional but never
|
|
193
|
-
* nullable, so null is never a valid value, UI or not.
|
|
194
|
-
* - `targetElements: null` is stripped only on non-UI entries — on UI tests
|
|
195
|
-
* null is load-bearing ("blueprint capture failed", requires the
|
|
196
|
-
* '[no-blueprint-data]' markers) and must reach the refinements intact.
|
|
197
|
-
* Non-null values on non-UI entries still reach the refinements and are
|
|
198
|
-
* still rejected.
|
|
161
|
+
* A null that carries no meaning is stripped to omitted before validation.
|
|
162
|
+
* `pageContext: null` always: the field is optional, never nullable.
|
|
163
|
+
* `targetElements: null` on non-UI entries only — on a UI entry the null MEANS
|
|
164
|
+
* capture failed, so it must reach the refinements intact.
|
|
199
165
|
*/
|
|
200
166
|
function stripNullGroundingFields(val) {
|
|
201
167
|
if (!val || typeof val !== "object" || Array.isArray(val))
|
|
@@ -220,21 +186,27 @@ export const newTestSchema = z.preprocess(stripNullGroundingFields, z
|
|
|
220
186
|
testId: z
|
|
221
187
|
.string()
|
|
222
188
|
.describe("Human-readable kebab-case identifier, e.g. 'contract-get-products' or 'integration-users-orders-workflow'. Format: '<testType>-<method>-<resource>' for single-endpoint tests or '<testType>-<scenario-slug>' for multi-step tests. Must be unique within the report."),
|
|
189
|
+
plannedTestId: z
|
|
190
|
+
.string()
|
|
191
|
+
.optional()
|
|
192
|
+
.describe("The `plannedTestId` of the plan entry this test was approved under. Copy it from the plan you submitted to skyramp_register_test_plan — never invent one. " +
|
|
193
|
+
"Required on EVERY entry, UI included. That is deliberately wider than the approved-plan generation gate, which stays contract and integration only: " +
|
|
194
|
+
"this id is the exact key the report is joined back to the plan on, and an entry without one falls back to guessing from the test name and endpoint."),
|
|
223
195
|
testType: z
|
|
224
196
|
.nativeEnum(TestType)
|
|
225
197
|
.describe("Type of test created. Do not include priority or other metadata in this field."),
|
|
226
198
|
category: z
|
|
227
|
-
.
|
|
228
|
-
.describe("Test category
|
|
199
|
+
.enum(SCENARIO_CATEGORIES)
|
|
200
|
+
.describe("Test category. Write the same category the plan declared."),
|
|
229
201
|
endpoint: z
|
|
230
202
|
.string()
|
|
231
|
-
.describe("HTTP verb and path, e.g. 'GET /api/v1/products'"),
|
|
203
|
+
.describe("For contract and integration tests, the HTTP verb and path, e.g. 'GET /api/v1/products'. For ui and e2e tests, the page path the test opens, e.g. '/notifications'."),
|
|
232
204
|
fileName: z.string().describe("Name of the generated test file"),
|
|
233
205
|
description: z
|
|
234
206
|
.string()
|
|
235
207
|
.trim()
|
|
236
208
|
.min(1)
|
|
237
|
-
.describe("What the test does — the steps and assertions, not the bugs it finds. e.g. 'Creates a collection, adds a link, then verifies the link exists'. Do NOT describe expected failures or bugs here — those belong in issuesFound."),
|
|
209
|
+
.describe("What the test does — the steps and assertions, not the bugs it finds. e.g. 'Creates a collection, adds a link, then verifies the link exists'. Do NOT describe expected failures or bugs here — those belong in issuesFound. Name any UI element the way a person would, e.g. the disabled 'Mark all as read' button — never a builder term such as blueprint or DOM analyzer, and never identifier syntax such as role=button or accessibleName=X."),
|
|
238
210
|
scenarioFile: z
|
|
239
211
|
.string()
|
|
240
212
|
.optional()
|
|
@@ -249,19 +221,30 @@ export const newTestSchema = z.preprocess(stripNullGroundingFields, z
|
|
|
249
221
|
.describe("Path to the Playwright/UI trace file if used or created"),
|
|
250
222
|
reasoning: z
|
|
251
223
|
.string()
|
|
252
|
-
.describe("Why this test was created: what production risk it mitigates, what code pattern it targets, or what coverage gap it fills"),
|
|
224
|
+
.describe("Why this test was created: what production risk it mitigates, what code pattern it targets, or what coverage gap it fills. Name any UI element the way a person would, e.g. the disabled 'Mark all as read' button — never a builder term such as blueprint or DOM analyzer, and never identifier syntax such as role=button or accessibleName=X."),
|
|
253
225
|
repository: repositoryField,
|
|
254
226
|
targetElements: z
|
|
255
227
|
.array(targetElementSchema)
|
|
256
228
|
.min(1)
|
|
257
229
|
.nullable()
|
|
258
230
|
.optional()
|
|
259
|
-
.describe("UI tests only: structured grounding for one or more elements the test targets. Most tests target a single element (array length 1); render-state and multi-step UI tests target several (array length 2+). Each entry must be lifted verbatim from a captured blueprint element. Set to null when blueprint capture failed
|
|
231
|
+
.describe("UI tests only: structured grounding for one or more elements the test targets. Most tests target a single element (array length 1); render-state and multi-step UI tests target several (array length 2+). Each entry must be lifted verbatim from a captured blueprint element. Set to null when blueprint capture failed — the null IS the signal, so put no marker in description or reasoning. See Blueprint Citation Invariant in testbot prompt."),
|
|
260
232
|
pageContext: pageContextSchema
|
|
261
233
|
.optional()
|
|
262
234
|
.describe("UI tests only: page metadata for the test. Lifted from the BlueprintCapture used during grounding."),
|
|
263
235
|
})
|
|
264
236
|
.superRefine((rec, ctx) => {
|
|
237
|
+
// The join to the stored plan is this id and nothing else. Required for
|
|
238
|
+
// EVERY testType: the generation gate covers only contract and integration,
|
|
239
|
+
// and UI is where a delivery drifts furthest from the plan. Declared
|
|
240
|
+
// optional above and required here so the message says what to do.
|
|
241
|
+
if ((rec.plannedTestId ?? "").trim().length === 0) {
|
|
242
|
+
ctx.addIssue({
|
|
243
|
+
code: z.ZodIssueCode.custom,
|
|
244
|
+
path: ["plannedTestId"],
|
|
245
|
+
message: "plannedTestId is required on every newTestsCreated entry. Use the plannedTestId of the plan entry this test was approved under, from skyramp_register_test_plan.",
|
|
246
|
+
});
|
|
247
|
+
}
|
|
265
248
|
// targetElements / pageContext are UI-only.
|
|
266
249
|
if (rec.testType !== TestType.UI) {
|
|
267
250
|
for (const field of ["targetElements", "pageContext"]) {
|
|
@@ -288,10 +271,12 @@ export const newTestSchema = z.preprocess(stripNullGroundingFields, z
|
|
|
288
271
|
ctx.addIssue({
|
|
289
272
|
code: z.ZodIssueCode.custom,
|
|
290
273
|
path: ["targetElements"],
|
|
291
|
-
message: "targetElements is required for testType: 'ui'. Use null when blueprint capture failed
|
|
274
|
+
message: "targetElements is required for testType: 'ui'. Use null when blueprint capture failed; the null is the signal and needs no marker in description or reasoning.",
|
|
292
275
|
});
|
|
293
276
|
}
|
|
294
|
-
//
|
|
277
|
+
// Required beside an array, and ALLOWED beside null: the plan schema asks a
|
|
278
|
+
// UI planned test with no HTTP step for `pageContext.url`, so refusing it here
|
|
279
|
+
// would make a plan-legal shape illegal in the report.
|
|
295
280
|
if (Array.isArray(rec.targetElements) &&
|
|
296
281
|
rec.pageContext === undefined) {
|
|
297
282
|
ctx.addIssue({
|
|
@@ -300,31 +285,6 @@ export const newTestSchema = z.preprocess(stripNullGroundingFields, z
|
|
|
300
285
|
message: "pageContext is required for testType: 'ui' when targetElements is an array.",
|
|
301
286
|
});
|
|
302
287
|
}
|
|
303
|
-
if (rec.targetElements === null && rec.pageContext !== undefined) {
|
|
304
|
-
ctx.addIssue({
|
|
305
|
-
code: z.ZodIssueCode.custom,
|
|
306
|
-
path: ["pageContext"],
|
|
307
|
-
message: "pageContext must be omitted when targetElements is null (no blueprint data).",
|
|
308
|
-
});
|
|
309
|
-
}
|
|
310
|
-
// When targetElements is null, both description and reasoning must carry the '[no-blueprint-data]' marker.
|
|
311
|
-
if (rec.targetElements === null) {
|
|
312
|
-
if (!rec.reasoning.includes("[no-blueprint-data]")) {
|
|
313
|
-
ctx.addIssue({
|
|
314
|
-
code: z.ZodIssueCode.custom,
|
|
315
|
-
path: ["reasoning"],
|
|
316
|
-
message: "reasoning must contain '[no-blueprint-data]' when targetElements is null.",
|
|
317
|
-
});
|
|
318
|
-
}
|
|
319
|
-
if (rec.description &&
|
|
320
|
-
!rec.description.includes("[no-blueprint-data]")) {
|
|
321
|
-
ctx.addIssue({
|
|
322
|
-
code: z.ZodIssueCode.custom,
|
|
323
|
-
path: ["description"],
|
|
324
|
-
message: "description must contain '[no-blueprint-data]' when targetElements is null.",
|
|
325
|
-
});
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
288
|
}
|
|
329
289
|
}));
|
|
330
290
|
/** A citation string the submit-time check verifies and the report then ships.
|
|
@@ -338,6 +298,7 @@ const issueFoundSchema = z
|
|
|
338
298
|
description: z
|
|
339
299
|
.string()
|
|
340
300
|
.describe("One-line description. Do NOT prefix with the severity level — severity is a separate field. Include code logic bugs from the diff, test generation/execution failures, and environment misconfiguration. " +
|
|
301
|
+
"A bug that this change did not cause is still an issue: keep the test that found it, and say in this description that the bug predates the change. " +
|
|
341
302
|
"When sourceFile or sourceSymbol is set, quote the offending line inside backticks in this description — the exact code, copied from the file, so review tools can find it."),
|
|
342
303
|
severity: z
|
|
343
304
|
.enum(["critical", "high", "medium", "low"])
|
|
@@ -345,18 +306,26 @@ const issueFoundSchema = z
|
|
|
345
306
|
.describe("Issue severity. critical = feature broken/unusable (e.g. page doesn't load, data corruption). " +
|
|
346
307
|
"high = incorrect behavior (e.g. wrong calculation, stale data returned). " +
|
|
347
308
|
"medium = minor functional gap. low = cosmetic or informational. " +
|
|
348
|
-
"
|
|
309
|
+
"CHECKED: a run that wrote a test planned as `requirement_conflict` — the PR title or description, " +
|
|
310
|
+
"or a requirements file it names, states a requirement the implemented behavior contradicts — must " +
|
|
311
|
+
"report an issue here at 'critical' or 'high'. A stated requirement the code does not meet is not a " +
|
|
312
|
+
"medium. The check raises an objection, never a rejection."),
|
|
349
313
|
category: z
|
|
350
314
|
.nativeEnum(IssueFoundCategory)
|
|
351
315
|
.describe("Issue classification. bug = a product/code defect, e.g. found by a test or in the diff. " +
|
|
352
|
-
"lint = a linter or formatter
|
|
316
|
+
"lint = a linter or formatter objection (eslint, flake8, prettier). " +
|
|
353
317
|
"type = a type-check failure (tsc, mypy). " +
|
|
354
318
|
"config = environment or tooling misconfiguration (wrong workspace auth type, missing env var, setup command failure), " +
|
|
355
319
|
"and also every Skyramp tool or environment failure — a generation or execution tool error, an unreachable app, a missing credential, a failed capture. " +
|
|
356
320
|
"The report renders lint/type/config entries in a separate 'Configuration Errors' section so product bugs stay prominent under 'Issues Found'."),
|
|
357
321
|
repository: repositoryField,
|
|
322
|
+
plannedTestId: z
|
|
323
|
+
.string()
|
|
324
|
+
.optional()
|
|
325
|
+
.describe("The `plannedTestId` of the planned test this issue is about, when it is about one. Set it on the issue you record for a `requirement_conflict` test so the conflict is tied to the test that asserts it; otherwise leave it out."),
|
|
358
326
|
sourceFile: citationString.describe("Path of the application file whose code is missing or wrong, relative to the repository root (e.g. 'src/crud/products.py'). " +
|
|
359
327
|
"REQUIRED when category is 'bug'. " +
|
|
328
|
+
"For code that is MISSING — an unmounted router, an unregistered route, an import never added — name the file where the line should be, not the file that defines what is unmounted. " +
|
|
360
329
|
"Cite only a file you actually opened this run: this tool rejects the report if the path does not exist in the repository."),
|
|
361
330
|
sourceSymbol: citationString.describe("The function, method, or identifier inside sourceFile whose code is missing or wrong (e.g. 'delete_product'). " +
|
|
362
331
|
"Optional — a config file or template has no symbol to name. Set it whenever the file has one. " +
|
|
@@ -367,7 +336,7 @@ const issueFoundSchema = z
|
|
|
367
336
|
.positive()
|
|
368
337
|
.optional()
|
|
369
338
|
.describe("1-based line number in sourceFile. Optional and advisory. " +
|
|
370
|
-
"Give the line of the offending statement itself, not the line of the enclosing function's signature — review views place the
|
|
339
|
+
"Give the line of the offending statement itself, not the line of the enclosing function's signature — review views place the objection on exactly this line. " +
|
|
371
340
|
"The tool does not check that the line still holds the cited code, because line numbers drift."),
|
|
372
341
|
})
|
|
373
342
|
.superRefine((issue, ctx) => {
|
|
@@ -375,7 +344,7 @@ const issueFoundSchema = z
|
|
|
375
344
|
// without a file the entry is prose a reviewer cannot act on, and nothing
|
|
376
345
|
// downstream can place it in the diff. Only the file is required — a config file
|
|
377
346
|
// or a template legitimately has no symbol to name, and line numbers are
|
|
378
|
-
// advisory. The other categories are tooling
|
|
347
|
+
// advisory. The other categories are tooling objections that often have no single
|
|
379
348
|
// source location (an unreachable app, a missing env var), so the requirement is
|
|
380
349
|
// scoped to `bug` alone.
|
|
381
350
|
//
|
|
@@ -395,10 +364,9 @@ const issueFoundSchema = z
|
|
|
395
364
|
});
|
|
396
365
|
});
|
|
397
366
|
const scenarioStepSchema = z.object({
|
|
398
|
-
method:
|
|
399
|
-
.nativeEnum(HttpMethod)
|
|
367
|
+
method: stepMethodSchema
|
|
400
368
|
.optional()
|
|
401
|
-
.describe(
|
|
369
|
+
.describe(`${STEP_METHOD_DESCRIPTION} Omit it when the step does none of the three.`),
|
|
402
370
|
path: z
|
|
403
371
|
.string()
|
|
404
372
|
.optional()
|
|
@@ -428,8 +396,8 @@ export const additionalRecommendationSchema = z.preprocess(stripNullGroundingFie
|
|
|
428
396
|
.nativeEnum(TestType)
|
|
429
397
|
.describe("Type of test. Do not include priority or other metadata in this field."),
|
|
430
398
|
category: z
|
|
431
|
-
.
|
|
432
|
-
.describe("Test category
|
|
399
|
+
.enum(SCENARIO_CATEGORIES)
|
|
400
|
+
.describe("Test category, e.g. business_rule, error_handling, security_boundary. Write the same category the plan declared."),
|
|
433
401
|
primaryEndpoint: z
|
|
434
402
|
.string()
|
|
435
403
|
.optional()
|
|
@@ -441,10 +409,12 @@ export const additionalRecommendationSchema = z.preprocess(stripNullGroundingFie
|
|
|
441
409
|
// TODO: replace text with max(3) and check for regression
|
|
442
410
|
steps: z
|
|
443
411
|
.array(scenarioStepSchema)
|
|
412
|
+
.optional()
|
|
413
|
+
.default([])
|
|
444
414
|
.describe("Ordered sequence of API/UI steps in this test scenario (at most 3). Each API step must include method and path so the endpoints are explicit; a UI/E2E action step omits them. Include requestBody and responseBody only where they carry something a reader needs — the concrete values a claim rests on, or a list endpoint's array response; omit them otherwise."),
|
|
445
415
|
description: z
|
|
446
416
|
.string()
|
|
447
|
-
.describe("Walkthrough of what the test does — the steps and assertions. For multi-step scenarios, list the endpoints involved. The 'why it is valuable' belongs in reasoning."),
|
|
417
|
+
.describe("Walkthrough of what the test does — the steps and assertions. For multi-step scenarios, list the endpoints involved. The 'why it is valuable' belongs in reasoning. Name any UI element the way a person would, e.g. the disabled 'Mark all as read' button — never a builder term such as blueprint or DOM analyzer, and never identifier syntax such as role=button or accessibleName=X."),
|
|
448
418
|
priority: z
|
|
449
419
|
.preprocess((val) => (typeof val === "string" ? val.toLowerCase() : val), z.enum(["high", "medium", "low"]))
|
|
450
420
|
.describe("Priority level: high, medium, or low. First check diff relevance — does the test target an endpoint changed in this PR? HIGH: diff-relevant security/auth/error tests, cross-resource isolation for diff endpoints, CRUD lifecycle for NEW endpoints in the diff. MEDIUM: diff-relevant business-rule happy paths, multi-resource workflows involving diff endpoints, security/error tests for NON-diff endpoints. LOW: tests targeting only unchanged endpoints, trivially discoverable happy paths duplicating generated tests."),
|
|
@@ -462,14 +432,14 @@ export const additionalRecommendationSchema = z.preprocess(stripNullGroundingFie
|
|
|
462
432
|
.describe("Path to Playwright/UI trace file if available, e.g. 'tests/skyramp-playwright.zip'. UI tests need this; E2E tests need both frontend and backend traces."),
|
|
463
433
|
reasoning: z
|
|
464
434
|
.string()
|
|
465
|
-
.describe("Why this test is recommended: the specific production risk, business rule, or security boundary it would validate"),
|
|
435
|
+
.describe("Why this test is recommended: the specific production risk, business rule, or security boundary it would validate. Name any UI element the way a person would, e.g. the disabled 'Mark all as read' button — never a builder term such as blueprint or DOM analyzer, and never identifier syntax such as role=button or accessibleName=X."),
|
|
466
436
|
repository: repositoryField,
|
|
467
437
|
targetElements: z
|
|
468
438
|
.array(targetElementSchema)
|
|
469
439
|
.min(1)
|
|
470
440
|
.nullable()
|
|
471
441
|
.optional()
|
|
472
|
-
.describe("UI tests only: structured grounding for one or more elements the test
|
|
442
|
+
.describe("UI tests only: structured grounding for one or more elements the test would target. Most recommendations target a single element (array length 1); render-state and multi-step UI tests target several (array length 2+). Each entry must be lifted verbatim from a captured blueprint element. Set to null when no capture covers the screen — this test was never written, so null needs no marker. See Blueprint Citation Invariant in testbot prompt."),
|
|
473
443
|
pageContext: pageContextSchema
|
|
474
444
|
.optional()
|
|
475
445
|
.describe("UI tests only: page metadata for the test. Lifted from the BlueprintCapture used during grounding."),
|
|
@@ -499,7 +469,7 @@ export const additionalRecommendationSchema = z.preprocess(stripNullGroundingFie
|
|
|
499
469
|
ctx.addIssue({
|
|
500
470
|
code: z.ZodIssueCode.custom,
|
|
501
471
|
path: ["targetElements"],
|
|
502
|
-
message: "targetElements is required for testType: 'ui'. Use null when
|
|
472
|
+
message: "targetElements is required for testType: 'ui'. Use null when no capture covers the screen.",
|
|
503
473
|
});
|
|
504
474
|
}
|
|
505
475
|
if (Array.isArray(rec.targetElements) &&
|
|
@@ -510,29 +480,8 @@ export const additionalRecommendationSchema = z.preprocess(stripNullGroundingFie
|
|
|
510
480
|
message: "pageContext is required for testType: 'ui' when targetElements is an array.",
|
|
511
481
|
});
|
|
512
482
|
}
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
code: z.ZodIssueCode.custom,
|
|
516
|
-
path: ["pageContext"],
|
|
517
|
-
message: "pageContext must be omitted when targetElements is null (no blueprint data).",
|
|
518
|
-
});
|
|
519
|
-
}
|
|
520
|
-
if (rec.targetElements === null) {
|
|
521
|
-
if (!rec.reasoning.includes("[no-blueprint-data]")) {
|
|
522
|
-
ctx.addIssue({
|
|
523
|
-
code: z.ZodIssueCode.custom,
|
|
524
|
-
path: ["reasoning"],
|
|
525
|
-
message: "reasoning must contain '[no-blueprint-data]' when targetElements is null.",
|
|
526
|
-
});
|
|
527
|
-
}
|
|
528
|
-
if (!rec.description.includes("[no-blueprint-data]")) {
|
|
529
|
-
ctx.addIssue({
|
|
530
|
-
code: z.ZodIssueCode.custom,
|
|
531
|
-
path: ["description"],
|
|
532
|
-
message: "description must contain '[no-blueprint-data]' when targetElements is null.",
|
|
533
|
-
});
|
|
534
|
-
}
|
|
535
|
-
}
|
|
483
|
+
// No marker is asked for here: a recommendation was never written, so
|
|
484
|
+
// `targetElements: null` is its normal state rather than a lost capture.
|
|
536
485
|
}
|
|
537
486
|
}));
|
|
538
487
|
// TODO(multi-repo maintenance): no `repository` field yet — see readData() TODO below.
|
|
@@ -540,7 +489,7 @@ const testMaintenanceSchema = z.object({
|
|
|
540
489
|
testType: z.nativeEnum(TestType).describe("Type of test."),
|
|
541
490
|
endpoint: z
|
|
542
491
|
.string()
|
|
543
|
-
.describe("HTTP verb and path, e.g. 'GET /api/v1/products'"),
|
|
492
|
+
.describe("For contract and integration tests, the HTTP verb and path, e.g. 'GET /api/v1/products'. For ui and e2e tests, the page path the test opens, e.g. '/notifications'."),
|
|
544
493
|
testFilePath: z
|
|
545
494
|
.string()
|
|
546
495
|
.refine((p) => path.isAbsolute(p), {
|
|
@@ -582,6 +531,12 @@ const testMaintenanceDetailSchema = testMaintenanceLLMSchema.pick({
|
|
|
582
531
|
beforeDetails: true,
|
|
583
532
|
afterDetails: true,
|
|
584
533
|
});
|
|
534
|
+
/** The key an answer is matched by: the objection's id AND its message. The id
|
|
535
|
+
* alone is not unique — one verifier raises several different objections under
|
|
536
|
+
* it — and the message is what the reader judges the answer against. */
|
|
537
|
+
function answerKey(objection) {
|
|
538
|
+
return `${objection.objectionId}\n${objection.message}`;
|
|
539
|
+
}
|
|
585
540
|
/**
|
|
586
541
|
* Derive per-run analytics counts from a submitted report. These power the
|
|
587
542
|
* alpha-launch dashboards (tests generated/maintained, suite growth, bugs vs
|
|
@@ -734,6 +689,145 @@ function deduplicateById(items) {
|
|
|
734
689
|
}
|
|
735
690
|
return result;
|
|
736
691
|
}
|
|
692
|
+
/** Recommendation v2, post-execution half: the checks that need the tests to
|
|
693
|
+
* exist and to have run. Nothing here is a gate — the tests are already written,
|
|
694
|
+
* so an objection is a record in the report. */
|
|
695
|
+
/** One execution outcome per delivered test, joined by FILE and never by array
|
|
696
|
+
* position. Repository must match BEFORE the file comparison — `fileName` is a
|
|
697
|
+
* basename. A file can own several rows: failed if ANY failed, first failure
|
|
698
|
+
* carries the text; `Skipped` rows are evidence in neither direction. */
|
|
699
|
+
function collectExecutionOutcomes(delivered, results) {
|
|
700
|
+
// Absent names nothing to disagree with, so it matches any — the same rule
|
|
701
|
+
// `checkEveryDeliveredTestRan` uses.
|
|
702
|
+
const sameRepository = (a, b) => {
|
|
703
|
+
const left = (a ?? "").trim();
|
|
704
|
+
const right = (b ?? "").trim();
|
|
705
|
+
return left === "" || right === "" || left === right;
|
|
706
|
+
};
|
|
707
|
+
const outcomes = [];
|
|
708
|
+
for (const test of delivered) {
|
|
709
|
+
const plannedTestId = test.plannedTestId?.trim();
|
|
710
|
+
if (!plannedTestId)
|
|
711
|
+
continue;
|
|
712
|
+
const rows = results.filter((row) => row.status !== "Skipped" &&
|
|
713
|
+
sameRepository(row.repository, test.repository) &&
|
|
714
|
+
testFileMatches(row.testFilePath, test.fileName));
|
|
715
|
+
if (rows.length === 0)
|
|
716
|
+
continue;
|
|
717
|
+
const failed = rows.find((row) => row.status === "Fail");
|
|
718
|
+
outcomes.push({
|
|
719
|
+
plannedTestId,
|
|
720
|
+
passed: !failed,
|
|
721
|
+
...(failed ? { failureDetail: failed.details } : {}),
|
|
722
|
+
// Off the failing row, since that is the row the statement is about.
|
|
723
|
+
...(failed && typeof failed.reachedBehavior === "boolean"
|
|
724
|
+
? { reachedBehavior: failed.reachedBehavior }
|
|
725
|
+
: {}),
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
return outcomes;
|
|
729
|
+
}
|
|
730
|
+
/** One check, crash contained: the plan is read off disk, so a malformed field can
|
|
731
|
+
* throw and a throw here would lose the whole report. The id prefix is a separate
|
|
732
|
+
* argument because expectedOutcome's plan-time half mints its own. */
|
|
733
|
+
function runPostExecutionCheck(name, objectionIdPrefix, check) {
|
|
734
|
+
try {
|
|
735
|
+
return check();
|
|
736
|
+
}
|
|
737
|
+
catch (error) {
|
|
738
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
739
|
+
logger.warning(`Post-execution check ${name} threw: ${detail}`);
|
|
740
|
+
return [
|
|
741
|
+
{
|
|
742
|
+
objectionId: `${objectionIdPrefix}:crashed`,
|
|
743
|
+
verifier: name,
|
|
744
|
+
message: `The ${name} check could not run against this report.`,
|
|
745
|
+
evidence: `${name} threw: ${detail}`,
|
|
746
|
+
suggestion: "The report was written and the other checks still ran. A field of the stored plan is probably not the type the schema describes — re-submit the plan with skyramp_register_test_plan.",
|
|
747
|
+
},
|
|
748
|
+
];
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
// ── Report-time checks on what the customer will read ───────────────────────
|
|
752
|
+
/** One report entry per plan planned test. The expectedOutcome check keys results by
|
|
753
|
+
* `plannedTestId` and the LAST wins, so a second entry with one id silently chose
|
|
754
|
+
* which result answered for the planned test. The plan side keeps ids unique;
|
|
755
|
+
* nothing checked the reverse. */
|
|
756
|
+
function checkOneEntryPerCandidate(delivered) {
|
|
757
|
+
const byCandidate = new Map();
|
|
758
|
+
for (const test of delivered) {
|
|
759
|
+
const id = test.plannedTestId?.trim();
|
|
760
|
+
if (!id)
|
|
761
|
+
continue;
|
|
762
|
+
byCandidate.set(id, [...(byCandidate.get(id) ?? []), test.testId]);
|
|
763
|
+
}
|
|
764
|
+
return [...byCandidate]
|
|
765
|
+
.filter(([, testIds]) => testIds.length > 1)
|
|
766
|
+
.map(([plannedTestId, testIds]) => ({
|
|
767
|
+
objectionId: `oneEntryPerCandidate:${plannedTestId}`,
|
|
768
|
+
verifier: "oneEntryPerCandidate",
|
|
769
|
+
plannedTestId,
|
|
770
|
+
message: "Two report entries name one plan planned test. Only one of them can be the test that planned test asked for, and the checks that read the planned test's declaration cannot tell which.",
|
|
771
|
+
evidence: `planned test ${plannedTestId} claimed by: ${testIds.join(", ")}`,
|
|
772
|
+
suggestion: "Give each entry the plannedTestId of the planned test it really delivers, or plan the second test and use its own id.",
|
|
773
|
+
}));
|
|
774
|
+
}
|
|
775
|
+
/** Every delivered test has an execution result. The expectedOutcome check is silent for
|
|
776
|
+
* a planned test with no outcome, so a filename the results do not carry was the
|
|
777
|
+
* way past it. This is the check that says so. */
|
|
778
|
+
function checkEveryDeliveredTestRan(delivered, results) {
|
|
779
|
+
// A side that names no repository names none to disagree with, so it matches
|
|
780
|
+
// any. A single-repo run fills the field on one list and not the other often
|
|
781
|
+
// enough that requiring both to agree reported tests that had plainly run.
|
|
782
|
+
const sameRepository = (a, b) => {
|
|
783
|
+
const left = (a ?? "").trim();
|
|
784
|
+
const right = (b ?? "").trim();
|
|
785
|
+
return left === "" || right === "" || left === right;
|
|
786
|
+
};
|
|
787
|
+
return delivered
|
|
788
|
+
.filter((test) => !results.some((row) => sameRepository(row.repository, test.repository) &&
|
|
789
|
+
testFileMatches(row.testFilePath, test.fileName)))
|
|
790
|
+
.map((test) => {
|
|
791
|
+
// Say which half failed. "No row names that file" reads as a missing run
|
|
792
|
+
// when the file is there and only the repository differs.
|
|
793
|
+
const byName = results.filter((row) => testFileMatches(row.testFilePath, test.fileName));
|
|
794
|
+
const evidence = byName.length
|
|
795
|
+
? `reported as ${test.fileName} for ${test.repository?.trim() || "no repository"}, and the ${byName.length === 1 ? "row" : "rows"} naming that file ${byName.length === 1 ? "is" : "are"} for ${byName.map((row) => row.repository?.trim() || "no repository").join(", ")}`
|
|
796
|
+
: `reported as ${test.fileName}, no testResults row names that file`;
|
|
797
|
+
return {
|
|
798
|
+
objectionId: `deliveredTestRan:${test.testId}`,
|
|
799
|
+
verifier: "deliveredTestRan",
|
|
800
|
+
message: "This test is in the report and in no execution result. Nothing says whether it passes, so the checks that read its outcome stayed silent about it.",
|
|
801
|
+
evidence,
|
|
802
|
+
suggestion: "Run the test and add its result to `testResults` under the same file name, or take the entry out of the report.",
|
|
803
|
+
};
|
|
804
|
+
});
|
|
805
|
+
}
|
|
806
|
+
/** The four types a plan can express. `registerTestPlanTool`'s `PLAN_TEST_TYPES`
|
|
807
|
+
* is the same list; the report accepts every `TestType`, so an entry outside
|
|
808
|
+
* these four can never name a plan planned test and must not be asked to. */
|
|
809
|
+
const PLANNABLE_TEST_TYPES = new Set([
|
|
810
|
+
TestType.CONTRACT,
|
|
811
|
+
TestType.INTEGRATION,
|
|
812
|
+
TestType.E2E,
|
|
813
|
+
TestType.UI,
|
|
814
|
+
]);
|
|
815
|
+
function runPostExecutionChecks(plan, delivered, results, issues) {
|
|
816
|
+
const shipped = delivered
|
|
817
|
+
// A smoke, fuzz or load entry is filtered out rather than reported as
|
|
818
|
+
// unplanned: a plan cannot hold one, so the objection would name a mistake the
|
|
819
|
+
// agent has no way to fix.
|
|
820
|
+
.filter((test) => PLANNABLE_TEST_TYPES.has(test.testType))
|
|
821
|
+
.map((test) => test.plannedTestId?.trim())
|
|
822
|
+
.filter((id) => !!id)
|
|
823
|
+
.map((plannedTestId) => ({ plannedTestId }));
|
|
824
|
+
return [
|
|
825
|
+
...runPostExecutionCheck("deliveredMatchesPlan", "deliveredMatchesPlan", () => checkDeliveredMatchesPlan(plan, shipped)),
|
|
826
|
+
...runPostExecutionCheck("expectedOutcome", "expectedOutcome:executed", () => checkExpectedOutcomeAfterExecution(plan, collectExecutionOutcomes(delivered, results), issues)),
|
|
827
|
+
...runPostExecutionCheck("reportedCategory", "reportedCategory", () => checkReportedCategoryMatchesPlan(plan, delivered)),
|
|
828
|
+
...runPostExecutionCheck("requirementConflictReported", "requirementConflictReported", () => checkRequirementConflictReported(plan, delivered, issues)),
|
|
829
|
+
];
|
|
830
|
+
}
|
|
737
831
|
export function registerSubmitReportTool(server) {
|
|
738
832
|
server.registerTool(TOOL_NAME, {
|
|
739
833
|
annotations: {
|
|
@@ -742,8 +836,9 @@ export function registerSubmitReportTool(server) {
|
|
|
742
836
|
idempotentHint: false,
|
|
743
837
|
openWorldHint: false,
|
|
744
838
|
},
|
|
745
|
-
description: "Submit the final testbot report. Call this tool once after
|
|
746
|
-
"This is the ONLY way to submit the report — do NOT write the report to a file manually."
|
|
839
|
+
description: "Submit the final testbot report. Call this tool once, after all test analysis, generation and execution. " +
|
|
840
|
+
"This is the ONLY way to submit the report — do NOT write the report to a file manually. " +
|
|
841
|
+
"If a check objects to what you reported, the objection names an id: fix what you can, and answer the rest through `answers` on a second call.",
|
|
747
842
|
inputSchema: {
|
|
748
843
|
businessCaseAnalysis: z
|
|
749
844
|
.string()
|
|
@@ -752,11 +847,13 @@ export function registerSubmitReportTool(server) {
|
|
|
752
847
|
newTestsCreated: z
|
|
753
848
|
.array(newTestSchema)
|
|
754
849
|
.describe("List of new tests created. Use empty array [] if none."),
|
|
850
|
+
// Optional and unchecked: this run has no concept of a recommendation it
|
|
851
|
+
// does not write. Kept so an older caller still works.
|
|
755
852
|
additionalRecommendations: z
|
|
756
853
|
.array(additionalRecommendationSchema)
|
|
757
854
|
.optional()
|
|
758
855
|
.default([])
|
|
759
|
-
.describe("
|
|
856
|
+
.describe("Leave this empty or omit it. Every test this run recommends is a test it generates: a test worth writing goes in the plan you submit to skyramp_register_test_plan, and nothing caps how many tests a plan may hold."),
|
|
760
857
|
testMaintenanceDetails: z
|
|
761
858
|
.array(testMaintenanceDetailSchema)
|
|
762
859
|
.optional()
|
|
@@ -784,13 +881,27 @@ export function registerSubmitReportTool(server) {
|
|
|
784
881
|
.describe("Succinct commit message (if possible, under 72 chars) summarizing what Testbot did, " +
|
|
785
882
|
"e.g. 'add contract tests for /products endpoint' or 'update smoke tests for order API changes'. " +
|
|
786
883
|
"Used as both the git commit subject and the side PR title — the consumer applies truncation as needed."),
|
|
884
|
+
answers: z
|
|
885
|
+
.array(z
|
|
886
|
+
.object({
|
|
887
|
+
objectionId: z
|
|
888
|
+
.string()
|
|
889
|
+
.describe("The objection's id, exactly as this tool reported it after your last call. Answers are matched by the id AND the wording of the objection, never by position, so re-send the id as it was printed. An id this call raised no objection for closes nothing — a plan-time objection is answered through skyramp_register_test_plan instead."),
|
|
890
|
+
answer: z
|
|
891
|
+
.string()
|
|
892
|
+
.describe("Why the objection does not apply, or what you decided instead. A blank or whitespace-only answer closes nothing and the objection stays open."),
|
|
893
|
+
})
|
|
894
|
+
.strict())
|
|
895
|
+
.optional()
|
|
896
|
+
.default([])
|
|
897
|
+
.describe("One answer per check that objected to your previous call and that you do not intend to fix. An answered objection is published with your answer beside it and no longer counted open; an unanswered one is published on its own. Answers persist across calls, so an id answered once stays answered. There is no call limit."),
|
|
787
898
|
stateFile: z
|
|
788
899
|
.string()
|
|
789
|
-
.
|
|
790
|
-
"
|
|
791
|
-
"
|
|
792
|
-
"
|
|
793
|
-
"
|
|
900
|
+
.optional()
|
|
901
|
+
.describe("Optional. A Testbot run finds its own state file, so leave this out. " +
|
|
902
|
+
"Give a path only to name a state file the run would not find by itself. " +
|
|
903
|
+
"The report is written beside it, and it holds the execution results, the " +
|
|
904
|
+
"page captures and the videos this report needs."),
|
|
794
905
|
},
|
|
795
906
|
_meta: {
|
|
796
907
|
keywords: ["report", "summary", "testbot", "submit"],
|
|
@@ -802,6 +913,11 @@ export function registerSubmitReportTool(server) {
|
|
|
802
913
|
// below when a report language is enforced, spread into the analytics
|
|
803
914
|
// event in the finally. Empty (no properties) for non-enforced runs.
|
|
804
915
|
const languageTelemetry = {};
|
|
916
|
+
// The run's own state file, unless the agent named one.
|
|
917
|
+
const stateFile = resolveRunStatePath(params.stateFile);
|
|
918
|
+
if (!stateFile) {
|
|
919
|
+
return toolError("This run has no state file. Call skyramp_analyze_changes first — it writes the state file this report goes beside.");
|
|
920
|
+
}
|
|
805
921
|
// The report goes next to the state file, so in a Testbot run the state file has to
|
|
806
922
|
// be in the run directory — that is where the action reads the report back from.
|
|
807
923
|
// Two ways that breaks, both ending in a report nobody reads while this tool says
|
|
@@ -817,7 +933,7 @@ export function registerSubmitReportTool(server) {
|
|
|
817
933
|
"from $RUNNER_TEMP/skyramp. Set RUNNER_TEMP for the run. Retrying or changing " +
|
|
818
934
|
"the report will not help — this is an environment problem.");
|
|
819
935
|
}
|
|
820
|
-
const stateDir = path.dirname(
|
|
936
|
+
const stateDir = path.dirname(stateFile);
|
|
821
937
|
if (path.resolve(stateDir) !== path.resolve(runDir)) {
|
|
822
938
|
return toolError(`Cannot write the report: stateFile is in ${stateDir}, but this run's directory ` +
|
|
823
939
|
`is ${runDir}. The report is written next to the state file and the Testbot ` +
|
|
@@ -925,7 +1041,7 @@ export function registerSubmitReportTool(server) {
|
|
|
925
1041
|
const dedupedRecommendations = deduplicateById([
|
|
926
1042
|
...(params.additionalRecommendations ?? []),
|
|
927
1043
|
]);
|
|
928
|
-
const stateManager = StateManager.fromStatePath(
|
|
1044
|
+
const stateManager = StateManager.fromStatePath(stateFile);
|
|
929
1045
|
let stateData;
|
|
930
1046
|
try {
|
|
931
1047
|
// Only reads the primary repo — see the related-repo check below for why related
|
|
@@ -937,7 +1053,7 @@ export function registerSubmitReportTool(server) {
|
|
|
937
1053
|
return errorResult;
|
|
938
1054
|
}
|
|
939
1055
|
if (!stateData) {
|
|
940
|
-
errorResult = toolError(`stateFile not found or empty: ${
|
|
1056
|
+
errorResult = toolError(`stateFile not found or empty: ${stateFile}`);
|
|
941
1057
|
return errorResult;
|
|
942
1058
|
}
|
|
943
1059
|
// undefined + existing tests present means skyramp_actions was never called this
|
|
@@ -951,49 +1067,47 @@ export function registerSubmitReportTool(server) {
|
|
|
951
1067
|
"Call skyramp_actions (with recommendations: [] if no existing tests needed action) before skyramp_submit_report.");
|
|
952
1068
|
return errorResult;
|
|
953
1069
|
}
|
|
954
|
-
//
|
|
955
|
-
//
|
|
956
|
-
//
|
|
957
|
-
// generation-tool gate uses (GENERATE, or ADDITIONAL backfill when GENERATE
|
|
958
|
-
// is non-empty), so a generation the gate permitted is never rejected at
|
|
959
|
-
// report time. Absent approvedPlan (register tool never ran / never
|
|
960
|
-
// pre-seeded anything) → no check, back-compat.
|
|
1070
|
+
// Every gated entry names a plan planned test by `plannedTestId`, and NOT
|
|
1071
|
+
// conditionally on a plan existing: with none, every entry fails here, where
|
|
1072
|
+
// skipping the check would read exactly like a satisfied plan.
|
|
961
1073
|
//
|
|
962
|
-
//
|
|
963
|
-
//
|
|
964
|
-
//
|
|
965
|
-
//
|
|
966
|
-
|
|
967
|
-
|
|
1074
|
+
// The planned test must be of the entry's OWN test type, trimmed and lower-cased
|
|
1075
|
+
// because the plan's `testType` is free-form text; one stating no type is
|
|
1076
|
+
// accepted. EXEMPT: a plan whose `plannedTests` is not an array cannot be read,
|
|
1077
|
+
// and that is no evidence a test went unplanned. An ABSENT plan is not that.
|
|
1078
|
+
const planUnreadable = !!stateData.plan && !Array.isArray(stateData.plan.plannedTests);
|
|
1079
|
+
if (!planUnreadable) {
|
|
1080
|
+
const planCandidates = stateData.plan?.plannedTests ?? [];
|
|
1081
|
+
const plannedById = new Map();
|
|
1082
|
+
for (const plannedTest of planCandidates) {
|
|
1083
|
+
const id = plannedTest?.plannedTestId?.trim();
|
|
1084
|
+
// First wins: `buildRegistration` suffixes a repeat, so a shared id can
|
|
1085
|
+
// only come from a hand-edited plan.
|
|
1086
|
+
if (id && !plannedById.has(id))
|
|
1087
|
+
plannedById.set(id, plannedTest);
|
|
1088
|
+
}
|
|
968
1089
|
const unapproved = dedupedNewTests.filter((t) => {
|
|
969
1090
|
if (!PLAN_GATED_TEST_TYPES.has(t.testType))
|
|
970
1091
|
return false;
|
|
971
|
-
const
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
method,
|
|
977
|
-
path,
|
|
978
|
-
})));
|
|
1092
|
+
const plannedTest = plannedById.get((t.plannedTestId ?? "").trim());
|
|
1093
|
+
if (!plannedTest)
|
|
1094
|
+
return true;
|
|
1095
|
+
const plannedType = String(plannedTest.scenario?.testType ?? "").trim().toLowerCase();
|
|
1096
|
+
return plannedType !== "" && plannedType !== t.testType;
|
|
979
1097
|
});
|
|
980
1098
|
if (unapproved.length > 0) {
|
|
981
|
-
const
|
|
982
|
-
?
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
})
|
|
990
|
-
|
|
991
|
-
:
|
|
992
|
-
|
|
993
|
-
`skyramp_register_test_plan (plan ${approvedPlan.planId}) — neither its GENERATE list nor its ADDITIONAL backfill pool: ` +
|
|
994
|
-
`${unapproved.map((t) => `${t.testId} (${t.testType} ${t.endpoint})`).join(", ")}. ` +
|
|
995
|
-
`Approved GENERATE items: ${approvedList}. Call skyramp_register_test_plan to register this test before reporting it, ` +
|
|
996
|
-
`or remove it from newTestsCreated.`);
|
|
1099
|
+
const plannedList = plannedById.size > 0
|
|
1100
|
+
? planCandidates
|
|
1101
|
+
.map((plannedTest) => `${plannedTest?.plannedTestId} (${plannedTest?.scenario?.testType ?? "?"} ${plannedTest?.scenario?.scenarioName ?? "?"})`)
|
|
1102
|
+
.join("; ")
|
|
1103
|
+
: "(none — no plan was submitted this run)";
|
|
1104
|
+
errorResult = toolError(`${unapproved.length} newTestsCreated entr${unapproved.length === 1 ? "y" : "ies"} name no planned test of its own ` +
|
|
1105
|
+
`testType in the plan submitted to skyramp_register_test_plan: ` +
|
|
1106
|
+
`${unapproved
|
|
1107
|
+
.map((t) => `${t.testId} (${t.testType}, plannedTestId: ${(t.plannedTestId ?? "").trim() || "missing"})`)
|
|
1108
|
+
.join(", ")}. ` +
|
|
1109
|
+
`Planned planned tests: ${plannedList}. Submit a plan that contains these tests with skyramp_register_test_plan, ` +
|
|
1110
|
+
`then resubmit this report with each entry's plannedTestId copied from it — or remove the entries from newTestsCreated.`);
|
|
997
1111
|
return errorResult;
|
|
998
1112
|
}
|
|
999
1113
|
}
|
|
@@ -1016,12 +1130,20 @@ export function registerSubmitReportTool(server) {
|
|
|
1016
1130
|
// action/testType/endpoint/description come entirely from stateData.maintenanceVerdicts
|
|
1017
1131
|
// (written by skyramp_actions right after drift analysis) — the LLM never supplies
|
|
1018
1132
|
// testMaintenance directly, so there's nothing to re-transcribe here.
|
|
1133
|
+
// `pomFile` rides along per row and is stripped from the wire format below,
|
|
1134
|
+
// like testFilePath. It cannot be keyed by spec path: a spec emits one UPDATE
|
|
1135
|
+
// verdict PER page object, so a spec-keyed map keeps only the last.
|
|
1019
1136
|
const rawMaintenance = (stateData.maintenanceVerdicts ?? []).map((v) => ({
|
|
1020
1137
|
testFilePath: v.testFilePath,
|
|
1138
|
+
pomFile: v.pomFile,
|
|
1021
1139
|
testType: v.testType,
|
|
1022
1140
|
endpoint: v.endpoint,
|
|
1023
1141
|
action: v.action,
|
|
1024
|
-
|
|
1142
|
+
// A baseline refresh (SKYR-4298) leaves the spec untouched, so name the
|
|
1143
|
+
// replaced PNGs here — the reviewer's cue to open the image diff in the PR.
|
|
1144
|
+
description: v.rebaselineSnapshots?.length
|
|
1145
|
+
? `${v.rationale} (visual baseline refreshed: ${v.rebaselineSnapshots.join(", ")})`
|
|
1146
|
+
: v.rationale,
|
|
1025
1147
|
beforeDetails: "",
|
|
1026
1148
|
afterDetails: "",
|
|
1027
1149
|
}));
|
|
@@ -1135,7 +1257,7 @@ export function registerSubmitReportTool(server) {
|
|
|
1135
1257
|
`Testbot will not report file work that hasn't actually been made. Do NOT make a token edit to the claimed file just to satisfy this check.\n` +
|
|
1136
1258
|
`For a newTestsCreated claim: create the file, correct the claim's fileName to the file you actually created (see the changed files below), or remove the claim.\n` +
|
|
1137
1259
|
`For a testMaintenance claim: make the edit on the named file. That row is derived from the maintenance triage, not from this tool's input, so it cannot be corrected by changing what you pass here.\n` +
|
|
1138
|
-
`Unbacked
|
|
1260
|
+
`Unbacked changes:\n${unbacked.map((u) => ` - ${u}`).join("\n")}\n` +
|
|
1139
1261
|
`Files with actual working-tree changes:\n${changedList || " (none)"}`);
|
|
1140
1262
|
return errorResult;
|
|
1141
1263
|
}
|
|
@@ -1174,7 +1296,7 @@ export function registerSubmitReportTool(server) {
|
|
|
1174
1296
|
errorResult = toolError(citations.invalid.join("\n"));
|
|
1175
1297
|
return errorResult;
|
|
1176
1298
|
}
|
|
1177
|
-
// The checkout the citation resolved in owns the
|
|
1299
|
+
// The checkout the citation resolved in owns the objection, so the report
|
|
1178
1300
|
// states it. Left to the consumer, an unattributed related-repo path is
|
|
1179
1301
|
// resolved against the primary checkout and links the wrong repo's file.
|
|
1180
1302
|
citations.repository.forEach((repo, i) => {
|
|
@@ -1187,7 +1309,7 @@ export function registerSubmitReportTool(server) {
|
|
|
1187
1309
|
// generation — downstream scoring scripts don't expect them and fail if
|
|
1188
1310
|
// they encounter these string fields while traversing the object.
|
|
1189
1311
|
// Also normalize each item's `repository` (blank → undefined).
|
|
1190
|
-
// Checkout roots let the assertion-record matcher verify a
|
|
1312
|
+
// Checkout roots let the assertion-record matcher verify a planned test
|
|
1191
1313
|
// record actually lives in the row's repo (basename collisions across
|
|
1192
1314
|
// repos must not publish one spec's proof-of-work under another's name).
|
|
1193
1315
|
const assertionCheckouts = await stateManager
|
|
@@ -1197,6 +1319,246 @@ export function registerSubmitReportTool(server) {
|
|
|
1197
1319
|
const row = await attachReuseOutcome(normalizeRepository(rest), stateData.reuseOutcomes, stateData.reuseHandOffs, retrofitViews);
|
|
1198
1320
|
return attachAssertionOutcome(row, stateData.assertionOutcomes ?? {}, assertionCheckouts);
|
|
1199
1321
|
}));
|
|
1322
|
+
// The report enum has no `bug_caught` value, so which failure was on purpose
|
|
1323
|
+
// is derived from the plan rather than from agent prose. Read defensively:
|
|
1324
|
+
// the plan comes off disk.
|
|
1325
|
+
const failOnPurpose = new Set();
|
|
1326
|
+
for (const plannedTest of Array.isArray(stateData.plan?.plannedTests)
|
|
1327
|
+
? stateData.plan.plannedTests
|
|
1328
|
+
: []) {
|
|
1329
|
+
const id = plannedTest?.plannedTestId?.trim();
|
|
1330
|
+
if (!id)
|
|
1331
|
+
continue;
|
|
1332
|
+
// Off the category table, not a literal comparison: `changedFile`'s
|
|
1333
|
+
// neighbour normalises case, space and separator, and the two must agree.
|
|
1334
|
+
if (categoryExpectsRed(plannedTest?.scenario?.category) ||
|
|
1335
|
+
plannedTest?.declarations?.expected?.outcome === "fail") {
|
|
1336
|
+
failOnPurpose.add(id);
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
for (const test of sanitizedNewTests) {
|
|
1340
|
+
if (failOnPurpose.has((test.plannedTestId ?? "").trim())) {
|
|
1341
|
+
test.expectedToFail = true;
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
// Report-time checks on what the customer will read. They need no plan, so a
|
|
1345
|
+
// run without one still gets them.
|
|
1346
|
+
const reportChecks = [
|
|
1347
|
+
...runPostExecutionCheck("oneEntryPerCandidate", "oneEntryPerCandidate", () => checkOneEntryPerCandidate(dedupedNewTests)),
|
|
1348
|
+
// Not in the plan-only lane: that lane requires `testResults` to be empty,
|
|
1349
|
+
// so the check would flag every planned test.
|
|
1350
|
+
...(isPlanOnlyMode()
|
|
1351
|
+
? []
|
|
1352
|
+
: runPostExecutionCheck("deliveredTestRan", "deliveredTestRan", () => checkEveryDeliveredTestRan(dedupedNewTests, params.testResults))),
|
|
1353
|
+
];
|
|
1354
|
+
// Run against an EMPTY plan when none was submitted, never skipped: the gate
|
|
1355
|
+
// above covers contract and integration only. An empty plan raises exactly
|
|
1356
|
+
// the "delivered but unplanned" half, which is the true statement.
|
|
1357
|
+
let objections;
|
|
1358
|
+
let changeTable;
|
|
1359
|
+
{
|
|
1360
|
+
const postExecution = runPostExecutionChecks(stateData.plan ?? { plannedTests: [], registrationNumber: 0, answers: [], openObjections: [], answeredObjections: [] }, dedupedNewTests, params.testResults, params.issuesFound ?? []);
|
|
1361
|
+
// Read defensively, like the crash-contained checks above: the plan comes
|
|
1362
|
+
// off disk, so its declared array type holds only on the validated path.
|
|
1363
|
+
const stillOpen = Array.isArray(stateData.plan?.openObjections)
|
|
1364
|
+
? stateData.plan.openObjections
|
|
1365
|
+
: [];
|
|
1366
|
+
// An answered objection is published WITH its answer, beside the open ones:
|
|
1367
|
+
// any non-blank answer closes one, so publishing only the open would let a
|
|
1368
|
+
// single character erase the record. The reader judges the answer, not the
|
|
1369
|
+
// server.
|
|
1370
|
+
const answered = Array.isArray(stateData.plan?.answeredObjections)
|
|
1371
|
+
? stateData.plan.answeredObjections
|
|
1372
|
+
: [];
|
|
1373
|
+
// Closed by a blocker the server could not check. Published with the blocker
|
|
1374
|
+
// it names, so a reader can count how much of this run's accountability
|
|
1375
|
+
// rests on a claim nothing verified.
|
|
1376
|
+
const unverified = Array.isArray(stateData.plan?.unverifiedCloses)
|
|
1377
|
+
? stateData.plan.unverifiedCloses
|
|
1378
|
+
: [];
|
|
1379
|
+
// Post-execution objections are recomputed on every call, so one the agent
|
|
1380
|
+
// fixed is gone and one it did not is raised again. Recomputation loses the
|
|
1381
|
+
// ANSWER, which arrives on one call and is needed on every later one, so the
|
|
1382
|
+
// answered set is persisted below and merged with this call's answers.
|
|
1383
|
+
const acknowledged = params.answers ?? [];
|
|
1384
|
+
// The message is part of the key, not just the id: one id covers several
|
|
1385
|
+
// objections — `expectedOutcome:executed:<id>` is raised for a should-fail
|
|
1386
|
+
// test that passed, a should-pass one that failed, and a failure that never
|
|
1387
|
+
// reached its behaviour — so an id-only match would close the wrong one.
|
|
1388
|
+
const carried = (Array.isArray(stateData.reportObjections?.answeredObjections)
|
|
1389
|
+
? stateData.reportObjections.answeredObjections
|
|
1390
|
+
: []).filter((entry) => typeof entry?.objection?.objectionId === "string" &&
|
|
1391
|
+
typeof entry?.answer === "string" &&
|
|
1392
|
+
entry.answer.trim().length > 0);
|
|
1393
|
+
const carriedByKey = new Map(carried.map((entry) => [answerKey(entry.objection), entry]));
|
|
1394
|
+
// What the LAST call raised. Without it there is nothing to notice a
|
|
1395
|
+
// fixed objection's absence against.
|
|
1396
|
+
const previouslyRaised = (Array.isArray(stateData.reportObjections?.raisedObjections)
|
|
1397
|
+
? stateData.reportObjections.raisedObjections
|
|
1398
|
+
: []).filter((entry) => typeof entry?.objection?.objectionId === "string");
|
|
1399
|
+
// When an objection was first raised. Restamping it every call said an
|
|
1400
|
+
// objection answered on call 2 was raised on call 2, so a reader could not
|
|
1401
|
+
// tell how long it had stood (run 33984531266, sb19-home-dashboard). The
|
|
1402
|
+
// number comes off disk, so a non-number is treated as no record.
|
|
1403
|
+
const firstRaisedAt = new Map(previouslyRaised
|
|
1404
|
+
.filter((entry) => Number.isFinite(entry.submission))
|
|
1405
|
+
.map((entry) => [answerKey(entry.objection), entry.submission]));
|
|
1406
|
+
const previouslyResolved = (Array.isArray(stateData.reportObjections?.resolvedObjections)
|
|
1407
|
+
? stateData.reportObjections.resolvedObjections
|
|
1408
|
+
: []).filter((entry) => typeof entry?.objection?.objectionId === "string" &&
|
|
1409
|
+
typeof entry?.resolvedBy === "string");
|
|
1410
|
+
const submissionNumber = (stateData.reportObjections?.submissionCount ?? 0) + 1;
|
|
1411
|
+
const raised = [...postExecution, ...reportChecks];
|
|
1412
|
+
// Answerable = raised by THIS call or by an earlier one. The agent fixes an
|
|
1413
|
+
// objection and answers it in the same call: fixing it stops the check
|
|
1414
|
+
// raising it, so matching against this call alone called the answer unknown
|
|
1415
|
+
// and objected to the agent for doing both (run 33946409570, sb19).
|
|
1416
|
+
const answerable = [...raised, ...previouslyRaised.map((entry) => entry.objection)];
|
|
1417
|
+
const reportStageObjections = [
|
|
1418
|
+
...raised,
|
|
1419
|
+
...unknownAnswerObjections(answerable, acknowledged, {
|
|
1420
|
+
scope: "this run raised",
|
|
1421
|
+
suggestion: "Answer only the ids this tool has listed on this run. A plan-time objection is answered through skyramp_register_test_plan's own answers, not here.",
|
|
1422
|
+
}),
|
|
1423
|
+
];
|
|
1424
|
+
const raisedNow = new Set(reportStageObjections.map(answerKey));
|
|
1425
|
+
// A call with no execution result gives these checks nothing to judge, so
|
|
1426
|
+
// their silence is not evidence: only a call carrying results can resolve an
|
|
1427
|
+
// earlier objection.
|
|
1428
|
+
const judgedThisCall = (params.testResults ?? []).length > 0;
|
|
1429
|
+
// Resolved = raised earlier, absent from a call that could have raised it.
|
|
1430
|
+
// One that comes back drops out: publishing it as both says two things.
|
|
1431
|
+
const resolvedObjections = [
|
|
1432
|
+
...previouslyResolved,
|
|
1433
|
+
...(judgedThisCall
|
|
1434
|
+
? previouslyRaised
|
|
1435
|
+
.filter((entry) => !raisedNow.has(answerKey(entry.objection)))
|
|
1436
|
+
.map((entry) => ({
|
|
1437
|
+
objection: entry.objection,
|
|
1438
|
+
resolvedBy: `submission ${submissionNumber}`,
|
|
1439
|
+
}))
|
|
1440
|
+
: []),
|
|
1441
|
+
].filter((entry, index, all) => !raisedNow.has(answerKey(entry.objection)) &&
|
|
1442
|
+
all.findIndex((other) => answerKey(other.objection) === answerKey(entry.objection)) === index);
|
|
1443
|
+
const answeredReportObjections = [];
|
|
1444
|
+
const openReportObjections = [];
|
|
1445
|
+
for (const objection of reportStageObjections) {
|
|
1446
|
+
const answer = answerFor(objection, acknowledged) ?? carriedByKey.get(answerKey(objection))?.answer;
|
|
1447
|
+
if (answer)
|
|
1448
|
+
answeredReportObjections.push({ objection, answer });
|
|
1449
|
+
else
|
|
1450
|
+
openReportObjections.push(objection);
|
|
1451
|
+
}
|
|
1452
|
+
objections = [
|
|
1453
|
+
...stillOpen.map((objection) => ({
|
|
1454
|
+
...objection,
|
|
1455
|
+
stage: "plan",
|
|
1456
|
+
})),
|
|
1457
|
+
...answered
|
|
1458
|
+
.filter((entry) => entry?.objection)
|
|
1459
|
+
.map((entry) => ({
|
|
1460
|
+
...entry.objection,
|
|
1461
|
+
stage: "plan",
|
|
1462
|
+
answer: typeof entry.answer === "string" ? entry.answer : "",
|
|
1463
|
+
})),
|
|
1464
|
+
...unverified
|
|
1465
|
+
.filter((entry) => entry?.objection)
|
|
1466
|
+
.map((entry) => ({
|
|
1467
|
+
...entry.objection,
|
|
1468
|
+
stage: "plan",
|
|
1469
|
+
answer: typeof entry.answer === "string" ? entry.answer : "",
|
|
1470
|
+
unverifiedBlocker: typeof entry.blocker === "string" ? entry.blocker : "",
|
|
1471
|
+
})),
|
|
1472
|
+
...openReportObjections.map((objection) => ({
|
|
1473
|
+
...objection,
|
|
1474
|
+
stage: "post-execution",
|
|
1475
|
+
})),
|
|
1476
|
+
...answeredReportObjections.map(({ objection, answer }) => ({
|
|
1477
|
+
...objection,
|
|
1478
|
+
stage: "post-execution",
|
|
1479
|
+
answer,
|
|
1480
|
+
})),
|
|
1481
|
+
...resolvedObjections.map(({ objection, resolvedBy }) => ({
|
|
1482
|
+
...objection,
|
|
1483
|
+
stage: "post-execution",
|
|
1484
|
+
resolvedBy,
|
|
1485
|
+
})),
|
|
1486
|
+
];
|
|
1487
|
+
// The change table: what the agent said the diff makes, and what it
|
|
1488
|
+
// delivered against each promise. Read defensively — the plan comes off
|
|
1489
|
+
// disk, so its declared array types hold only on the validated path.
|
|
1490
|
+
const declaredChanges = Array.isArray(stateData.plan?.changes)
|
|
1491
|
+
? stateData.plan.changes
|
|
1492
|
+
: [];
|
|
1493
|
+
if (declaredChanges.length > 0) {
|
|
1494
|
+
const changesByCandidate = new Map();
|
|
1495
|
+
for (const plannedTest of Array.isArray(stateData.plan?.plannedTests)
|
|
1496
|
+
? stateData.plan.plannedTests
|
|
1497
|
+
: []) {
|
|
1498
|
+
const id = plannedTest?.plannedTestId?.trim();
|
|
1499
|
+
const cited = plannedTest?.declarations?.changes;
|
|
1500
|
+
if (id && Array.isArray(cited)) {
|
|
1501
|
+
changesByCandidate.set(id, cited.map((entry) => String(entry ?? "").trim()));
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
const answerByChange = new Map();
|
|
1505
|
+
for (const objection of objections) {
|
|
1506
|
+
if (!objection.objectionId.startsWith(CHANGE_OBJECTION_PREFIX))
|
|
1507
|
+
continue;
|
|
1508
|
+
const answer = typeof objection.answer === "string" ? objection.answer.trim() : "";
|
|
1509
|
+
if (answer)
|
|
1510
|
+
answerByChange.set(objection.objectionId.slice(CHANGE_OBJECTION_PREFIX.length), answer);
|
|
1511
|
+
}
|
|
1512
|
+
changeTable = declaredChanges.map((change) => {
|
|
1513
|
+
const id = String(change?.id ?? "").trim();
|
|
1514
|
+
const testedBy = dedupedNewTests
|
|
1515
|
+
.map((test) => (test.plannedTestId ?? "").trim())
|
|
1516
|
+
.filter((plannedTestId) => plannedTestId && (changesByCandidate.get(plannedTestId) ?? []).includes(id));
|
|
1517
|
+
const answer = answerByChange.get(id);
|
|
1518
|
+
return {
|
|
1519
|
+
id,
|
|
1520
|
+
text: String(change?.text ?? ""),
|
|
1521
|
+
source: String(change?.source ?? ""),
|
|
1522
|
+
testedBy,
|
|
1523
|
+
...(answer ? { answer } : {}),
|
|
1524
|
+
};
|
|
1525
|
+
});
|
|
1526
|
+
}
|
|
1527
|
+
// An answer outlives both the call it was given on and the calls where
|
|
1528
|
+
// its objection does not fire, so the persisted set is this call's answers
|
|
1529
|
+
// plus every carried answer this call did not re-raise. The objections
|
|
1530
|
+
// are recomputed, so a stored copy would have no reader.
|
|
1531
|
+
const answeredNow = new Set(answeredReportObjections.map(({ objection }) => answerKey(objection)));
|
|
1532
|
+
// The report is the deliverable, so a failed state write must not cost
|
|
1533
|
+
// it; the answer is simply repeated on the next call.
|
|
1534
|
+
try {
|
|
1535
|
+
await stateManager.writeData({
|
|
1536
|
+
...stateData,
|
|
1537
|
+
reportObjections: {
|
|
1538
|
+
answeredObjections: [
|
|
1539
|
+
...answeredReportObjections,
|
|
1540
|
+
...carried.filter((entry) => !answeredNow.has(answerKey(entry.objection))),
|
|
1541
|
+
],
|
|
1542
|
+
// A call that judged nothing keeps the earlier entries it did not
|
|
1543
|
+
// re-raise: unresolved, merely unjudged.
|
|
1544
|
+
raisedObjections: [
|
|
1545
|
+
...reportStageObjections.map((objection) => ({
|
|
1546
|
+
objection,
|
|
1547
|
+
submission: firstRaisedAt.get(answerKey(objection)) ?? submissionNumber,
|
|
1548
|
+
})),
|
|
1549
|
+
...(judgedThisCall
|
|
1550
|
+
? []
|
|
1551
|
+
: previouslyRaised.filter((entry) => !raisedNow.has(answerKey(entry.objection)))),
|
|
1552
|
+
],
|
|
1553
|
+
resolvedObjections,
|
|
1554
|
+
submissionCount: submissionNumber,
|
|
1555
|
+
},
|
|
1556
|
+
});
|
|
1557
|
+
}
|
|
1558
|
+
catch (error) {
|
|
1559
|
+
logger.warning(`Could not persist report objections: ${error instanceof Error ? error.message : String(error)}`);
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1200
1562
|
const report = {
|
|
1201
1563
|
businessCaseAnalysis: params.businessCaseAnalysis,
|
|
1202
1564
|
newTestsCreated: sanitizedNewTests,
|
|
@@ -1206,7 +1568,10 @@ export function registerSubmitReportTool(server) {
|
|
|
1206
1568
|
// TODO(multi-repo maintenance): map(normalizeRepository) once testMaintenanceSchema
|
|
1207
1569
|
// has a repository field (see TODO above).
|
|
1208
1570
|
testMaintenance: testMaintenance
|
|
1209
|
-
? await Promise.all(testMaintenance.map(async ({ testFilePath, ...row }) => {
|
|
1571
|
+
? await Promise.all(testMaintenance.map(async ({ testFilePath, pomFile, ...row }) => {
|
|
1572
|
+
// `pomFile` is the file skyramp_actions told the agent to edit;
|
|
1573
|
+
// `testFilePath` stays the spec, which is what ran. Name both when
|
|
1574
|
+
// they differ, or a page-object edit is reported against the spec.
|
|
1210
1575
|
// Maintenance rows still carry the absolute path here, so the
|
|
1211
1576
|
// assertion summary uses an EXACT canonical-path lookup — no
|
|
1212
1577
|
// basename ambiguity. This is what carries the maintenance
|
|
@@ -1220,6 +1585,10 @@ export function registerSubmitReportTool(server) {
|
|
|
1220
1585
|
return {
|
|
1221
1586
|
...row,
|
|
1222
1587
|
fileName: path.basename(testFilePath),
|
|
1588
|
+
...(pomFile &&
|
|
1589
|
+
path.basename(pomFile) !== path.basename(testFilePath)
|
|
1590
|
+
? { editedFileName: path.basename(pomFile) }
|
|
1591
|
+
: {}),
|
|
1223
1592
|
...(assertions ? { assertions } : {}),
|
|
1224
1593
|
};
|
|
1225
1594
|
}))
|
|
@@ -1234,13 +1603,15 @@ export function registerSubmitReportTool(server) {
|
|
|
1234
1603
|
}),
|
|
1235
1604
|
issuesFound,
|
|
1236
1605
|
nextSteps: params.nextSteps ?? [],
|
|
1606
|
+
...(changeTable ? { changes: changeTable } : {}),
|
|
1607
|
+
...(objections ? { objections } : {}),
|
|
1237
1608
|
commitMessage: (params.commitMessage ?? "").replace(/[\r\n]+/g, " ").trim() ||
|
|
1238
1609
|
DEFAULT_COMMIT_MESSAGE,
|
|
1239
1610
|
};
|
|
1240
1611
|
const reportJson = JSON.stringify(report, null, 2);
|
|
1241
1612
|
// Beside the state file, which was read successfully above — so this directory is
|
|
1242
1613
|
// known to exist and no mkdir is needed. See REPORT_FILE_NAME.
|
|
1243
|
-
const reportPath = path.join(path.dirname(
|
|
1614
|
+
const reportPath = path.join(path.dirname(stateFile), REPORT_FILE_NAME);
|
|
1244
1615
|
logger.info("Submitting testbot report", {
|
|
1245
1616
|
outputFile: reportPath,
|
|
1246
1617
|
payloadBytes: reportJson.length,
|
|
@@ -1253,11 +1624,23 @@ export function registerSubmitReportTool(server) {
|
|
|
1253
1624
|
outputFile: reportPath,
|
|
1254
1625
|
elapsedMs: elapsed,
|
|
1255
1626
|
});
|
|
1627
|
+
// Addressed to the AGENT in the second person, each naming a next move: the
|
|
1628
|
+
// report is already written when this returns and the tool can be called
|
|
1629
|
+
// again, so the instruction is actionable. An answered objection is
|
|
1630
|
+
// published but not listed back, which would read as a second demand.
|
|
1631
|
+
const open = objections?.filter((objection) => objection.stage === "post-execution" && objection.answer === undefined) ?? [];
|
|
1256
1632
|
return {
|
|
1257
1633
|
content: [
|
|
1258
1634
|
{
|
|
1259
1635
|
type: "text",
|
|
1260
|
-
text: `Report submitted successfully to ${reportPath}
|
|
1636
|
+
text: `Report submitted successfully to ${reportPath}` +
|
|
1637
|
+
(open.length === 0
|
|
1638
|
+
? ""
|
|
1639
|
+
: `\n\n${open.length} check${open.length === 1 ? "" : "s"} objected to what was reported. ` +
|
|
1640
|
+
`Every one is published in the report as it stands; fix what you can and call this tool again.\n` +
|
|
1641
|
+
open
|
|
1642
|
+
.map((objection) => `- ${objection.objectionId} — ${objection.message}\n evidence: ${objection.evidence}\n next: ${objection.suggestion}`)
|
|
1643
|
+
.join("\n")),
|
|
1261
1644
|
},
|
|
1262
1645
|
],
|
|
1263
1646
|
};
|