@oscharko-dev/keiko-workflows 0.2.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/dist/.tsbuildinfo +1 -0
- package/dist/bug-investigation/context.d.ts +7 -0
- package/dist/bug-investigation/context.d.ts.map +1 -0
- package/dist/bug-investigation/context.js +119 -0
- package/dist/bug-investigation/descriptor.d.ts +4 -0
- package/dist/bug-investigation/descriptor.d.ts.map +1 -0
- package/dist/bug-investigation/descriptor.js +46 -0
- package/dist/bug-investigation/emit.d.ts +13 -0
- package/dist/bug-investigation/emit.d.ts.map +1 -0
- package/dist/bug-investigation/emit.js +35 -0
- package/dist/bug-investigation/events.d.ts +2 -0
- package/dist/bug-investigation/events.d.ts.map +1 -0
- package/dist/bug-investigation/events.js +6 -0
- package/dist/bug-investigation/failure-parse.d.ts +4 -0
- package/dist/bug-investigation/failure-parse.d.ts.map +1 -0
- package/dist/bug-investigation/failure-parse.js +154 -0
- package/dist/bug-investigation/guard.d.ts +3 -0
- package/dist/bug-investigation/guard.d.ts.map +1 -0
- package/dist/bug-investigation/guard.js +69 -0
- package/dist/bug-investigation/index.d.ts +8 -0
- package/dist/bug-investigation/index.d.ts.map +1 -0
- package/dist/bug-investigation/index.js +13 -0
- package/dist/bug-investigation/internal.d.ts +39 -0
- package/dist/bug-investigation/internal.d.ts.map +1 -0
- package/dist/bug-investigation/internal.js +65 -0
- package/dist/bug-investigation/memory.d.ts +5 -0
- package/dist/bug-investigation/memory.d.ts.map +1 -0
- package/dist/bug-investigation/memory.js +91 -0
- package/dist/bug-investigation/model-loop.d.ts +5 -0
- package/dist/bug-investigation/model-loop.d.ts.map +1 -0
- package/dist/bug-investigation/model-loop.js +225 -0
- package/dist/bug-investigation/parse.d.ts +4 -0
- package/dist/bug-investigation/parse.d.ts.map +1 -0
- package/dist/bug-investigation/parse.js +125 -0
- package/dist/bug-investigation/prompt.d.ts +5 -0
- package/dist/bug-investigation/prompt.d.ts.map +1 -0
- package/dist/bug-investigation/prompt.js +122 -0
- package/dist/bug-investigation/report.d.ts +24 -0
- package/dist/bug-investigation/report.d.ts.map +1 -0
- package/dist/bug-investigation/report.js +151 -0
- package/dist/bug-investigation/stages.d.ts +14 -0
- package/dist/bug-investigation/stages.d.ts.map +1 -0
- package/dist/bug-investigation/stages.js +247 -0
- package/dist/bug-investigation/types.d.ts +88 -0
- package/dist/bug-investigation/types.d.ts.map +1 -0
- package/dist/bug-investigation/types.js +6 -0
- package/dist/bug-investigation/verify-stage.d.ts +11 -0
- package/dist/bug-investigation/verify-stage.d.ts.map +1 -0
- package/dist/bug-investigation/verify-stage.js +91 -0
- package/dist/bug-investigation/workflow.d.ts +3 -0
- package/dist/bug-investigation/workflow.d.ts.map +1 -0
- package/dist/bug-investigation/workflow.js +85 -0
- package/dist/contextpack/assemble.d.ts +35 -0
- package/dist/contextpack/assemble.d.ts.map +1 -0
- package/dist/contextpack/assemble.js +431 -0
- package/dist/contextpack/compaction.d.ts +23 -0
- package/dist/contextpack/compaction.d.ts.map +1 -0
- package/dist/contextpack/compaction.js +68 -0
- package/dist/contextpack/index.d.ts +9 -0
- package/dist/contextpack/index.d.ts.map +1 -0
- package/dist/contextpack/index.js +8 -0
- package/dist/contextpack/microIndex.d.ts +29 -0
- package/dist/contextpack/microIndex.d.ts.map +1 -0
- package/dist/contextpack/microIndex.js +98 -0
- package/dist/contextpack/reranker.d.ts +15 -0
- package/dist/contextpack/reranker.d.ts.map +1 -0
- package/dist/contextpack/reranker.js +31 -0
- package/dist/descriptor.d.ts +2 -0
- package/dist/descriptor.d.ts.map +1 -0
- package/dist/descriptor.js +1 -0
- package/dist/governed-handoff.d.ts +6 -0
- package/dist/governed-handoff.d.ts.map +1 -0
- package/dist/governed-handoff.js +86 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13 -0
- package/dist/planner/anchors.d.ts +17 -0
- package/dist/planner/anchors.d.ts.map +1 -0
- package/dist/planner/anchors.js +291 -0
- package/dist/planner/explorationPlanner.d.ts +9 -0
- package/dist/planner/explorationPlanner.d.ts.map +1 -0
- package/dist/planner/explorationPlanner.js +15 -0
- package/dist/planner/governor.d.ts +16 -0
- package/dist/planner/governor.d.ts.map +1 -0
- package/dist/planner/governor.js +106 -0
- package/dist/planner/index.d.ts +11 -0
- package/dist/planner/index.d.ts.map +1 -0
- package/dist/planner/index.js +8 -0
- package/dist/planner/intent.d.ts +8 -0
- package/dist/planner/intent.d.ts.map +1 -0
- package/dist/planner/intent.js +140 -0
- package/dist/planner/plan.d.ts +43 -0
- package/dist/planner/plan.d.ts.map +1 -0
- package/dist/planner/plan.js +237 -0
- package/dist/promptEnhancer/index.d.ts +23 -0
- package/dist/promptEnhancer/index.d.ts.map +1 -0
- package/dist/promptEnhancer/index.js +282 -0
- package/dist/qualityIntelligence/__tests__/fixtures/runEntryFixtures.d.ts +30 -0
- package/dist/qualityIntelligence/__tests__/fixtures/runEntryFixtures.d.ts.map +1 -0
- package/dist/qualityIntelligence/__tests__/fixtures/runEntryFixtures.js +114 -0
- package/dist/qualityIntelligence/cancellation.d.ts +20 -0
- package/dist/qualityIntelligence/cancellation.d.ts.map +1 -0
- package/dist/qualityIntelligence/cancellation.js +55 -0
- package/dist/qualityIntelligence/descriptors.d.ts +41 -0
- package/dist/qualityIntelligence/descriptors.d.ts.map +1 -0
- package/dist/qualityIntelligence/descriptors.js +105 -0
- package/dist/qualityIntelligence/index.d.ts +11 -0
- package/dist/qualityIntelligence/index.d.ts.map +1 -0
- package/dist/qualityIntelligence/index.js +11 -0
- package/dist/qualityIntelligence/modelRoutedTestDesign.d.ts +100 -0
- package/dist/qualityIntelligence/modelRoutedTestDesign.d.ts.map +1 -0
- package/dist/qualityIntelligence/modelRoutedTestDesign.js +620 -0
- package/dist/qualityIntelligence/runEntries.d.ts +60 -0
- package/dist/qualityIntelligence/runEntries.d.ts.map +1 -0
- package/dist/qualityIntelligence/runEntries.js +243 -0
- package/dist/qualityIntelligence/runtimeCommon.d.ts +106 -0
- package/dist/qualityIntelligence/runtimeCommon.d.ts.map +1 -0
- package/dist/qualityIntelligence/runtimeCommon.js +258 -0
- package/dist/qualityIntelligence/scopedRegeneration.d.ts +26 -0
- package/dist/qualityIntelligence/scopedRegeneration.d.ts.map +1 -0
- package/dist/qualityIntelligence/scopedRegeneration.js +35 -0
- package/dist/ranking/filter.d.ts +20 -0
- package/dist/ranking/filter.d.ts.map +1 -0
- package/dist/ranking/filter.js +99 -0
- package/dist/ranking/index.d.ts +9 -0
- package/dist/ranking/index.d.ts.map +1 -0
- package/dist/ranking/index.js +8 -0
- package/dist/ranking/rank.d.ts +21 -0
- package/dist/ranking/rank.d.ts.map +1 -0
- package/dist/ranking/rank.js +160 -0
- package/dist/ranking/scoring.d.ts +13 -0
- package/dist/ranking/scoring.d.ts.map +1 -0
- package/dist/ranking/scoring.js +39 -0
- package/dist/ranking/signals.d.ts +20 -0
- package/dist/ranking/signals.d.ts.map +1 -0
- package/dist/ranking/signals.js +145 -0
- package/dist/unit-tests/context.d.ts +7 -0
- package/dist/unit-tests/context.d.ts.map +1 -0
- package/dist/unit-tests/context.js +129 -0
- package/dist/unit-tests/conventions.d.ts +5 -0
- package/dist/unit-tests/conventions.d.ts.map +1 -0
- package/dist/unit-tests/conventions.js +87 -0
- package/dist/unit-tests/descriptor.d.ts +5 -0
- package/dist/unit-tests/descriptor.d.ts.map +1 -0
- package/dist/unit-tests/descriptor.js +43 -0
- package/dist/unit-tests/emit.d.ts +13 -0
- package/dist/unit-tests/emit.d.ts.map +1 -0
- package/dist/unit-tests/emit.js +35 -0
- package/dist/unit-tests/events.d.ts +2 -0
- package/dist/unit-tests/events.d.ts.map +1 -0
- package/dist/unit-tests/events.js +6 -0
- package/dist/unit-tests/frontend.d.ts +42 -0
- package/dist/unit-tests/frontend.d.ts.map +1 -0
- package/dist/unit-tests/frontend.js +281 -0
- package/dist/unit-tests/index.d.ts +9 -0
- package/dist/unit-tests/index.d.ts.map +1 -0
- package/dist/unit-tests/index.js +15 -0
- package/dist/unit-tests/internal.d.ts +36 -0
- package/dist/unit-tests/internal.d.ts.map +1 -0
- package/dist/unit-tests/internal.js +43 -0
- package/dist/unit-tests/model-loop.d.ts +6 -0
- package/dist/unit-tests/model-loop.d.ts.map +1 -0
- package/dist/unit-tests/model-loop.js +98 -0
- package/dist/unit-tests/parse.d.ts +7 -0
- package/dist/unit-tests/parse.d.ts.map +1 -0
- package/dist/unit-tests/parse.js +68 -0
- package/dist/unit-tests/prompt.d.ts +6 -0
- package/dist/unit-tests/prompt.d.ts.map +1 -0
- package/dist/unit-tests/prompt.js +139 -0
- package/dist/unit-tests/report.d.ts +26 -0
- package/dist/unit-tests/report.d.ts.map +1 -0
- package/dist/unit-tests/report.js +104 -0
- package/dist/unit-tests/stages.d.ts +12 -0
- package/dist/unit-tests/stages.d.ts.map +1 -0
- package/dist/unit-tests/stages.js +202 -0
- package/dist/unit-tests/strategy.d.ts +6 -0
- package/dist/unit-tests/strategy.d.ts.map +1 -0
- package/dist/unit-tests/strategy.js +36 -0
- package/dist/unit-tests/target-guard.d.ts +5 -0
- package/dist/unit-tests/target-guard.d.ts.map +1 -0
- package/dist/unit-tests/target-guard.js +29 -0
- package/dist/unit-tests/types.d.ts +74 -0
- package/dist/unit-tests/types.d.ts.map +1 -0
- package/dist/unit-tests/types.js +6 -0
- package/dist/unit-tests/verify-stage.d.ts +10 -0
- package/dist/unit-tests/verify-stage.d.ts.map +1 -0
- package/dist/unit-tests/verify-stage.js +56 -0
- package/dist/unit-tests/workflow.d.ts +3 -0
- package/dist/unit-tests/workflow.d.ts.map +1 -0
- package/dist/unit-tests/workflow.js +69 -0
- package/package.json +38 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
// Prompt construction (ADR-0008 D1/D7). Builds the system + user ChatMessage array for the single
|
|
2
|
+
// generation call. PURE: no IO, no clock, no randomness — the same inputs always yield the same
|
|
3
|
+
// messages. The system message specifies the model-output contract (a fenced ```diff block plus
|
|
4
|
+
// optional labeled prose) that parse.ts consumes (steering note B). Context excerpts handed in are
|
|
5
|
+
// already redacted by #5; this module does no redaction.
|
|
6
|
+
const OUTPUT_CONTRACT = "Respond with the unified diff that adds the tests inside a single fenced code block opened " +
|
|
7
|
+
"with ```diff and closed with ```. After the block you MAY add `## Covered behavior` and " +
|
|
8
|
+
"`## Known gaps` sections in prose. Output ONLY a unified diff for test files — never modify " +
|
|
9
|
+
"source files. The first non-empty line inside the fence MUST be `--- /dev/null` for a new " +
|
|
10
|
+
"test file or `--- a/<existing-test-path>` for an existing test file, followed by " +
|
|
11
|
+
"`+++ b/<test-path>` and at least one `@@` hunk. Do not output `*** Begin Patch`, file trees, " +
|
|
12
|
+
"plain prose, markdown bullets, or escaped newline markers like `\\n+`/`\\n-` inside the diff " +
|
|
13
|
+
"fence; every diff line must be separated by a real newline.";
|
|
14
|
+
// Per-style discipline (Issue #1203). The output contract is identical for every style — a reviewable
|
|
15
|
+
// unified diff for test files — but the test idioms differ by the convention-driven style.
|
|
16
|
+
const STYLE_DESCRIPTOR = {
|
|
17
|
+
unit: "rigorous, deterministic unit tests",
|
|
18
|
+
component: "rigorous React component tests",
|
|
19
|
+
interaction: "rigorous React component interaction tests",
|
|
20
|
+
"accessibility-smoke": "rigorous React component tests with an accessibility smoke check",
|
|
21
|
+
"browser-smoke": "a minimal Playwright browser smoke test",
|
|
22
|
+
unsupported: "unit tests",
|
|
23
|
+
};
|
|
24
|
+
const STYLE_GUIDANCE = {
|
|
25
|
+
unit: [
|
|
26
|
+
"Cover edge cases explicitly: null, undefined, empty, zero, boundary, negative, and error paths.",
|
|
27
|
+
],
|
|
28
|
+
component: [
|
|
29
|
+
"Render the component with @testing-library/react render(), query through screen by accessible " +
|
|
30
|
+
"role/name or visible text, and assert user-visible output. Do not assert implementation details " +
|
|
31
|
+
"or internal state. Cover empty, loading, and error states where the component supports them.",
|
|
32
|
+
],
|
|
33
|
+
interaction: [
|
|
34
|
+
"Render the component with @testing-library/react, drive interactions with @testing-library/" +
|
|
35
|
+
"user-event (const user = userEvent.setup(); await user.click(...) / user.type(...)), and assert " +
|
|
36
|
+
"the resulting accessible UI changes. Do not assert internal state.",
|
|
37
|
+
],
|
|
38
|
+
"accessibility-smoke": [
|
|
39
|
+
"Render the component with @testing-library/react and query by accessible role/name so the test " +
|
|
40
|
+
"exercises the accessibility tree. Include at least one accessibility assertion: where the project " +
|
|
41
|
+
"provides an axe matcher, assert the rendered output has no detectable accessibility violations.",
|
|
42
|
+
],
|
|
43
|
+
"browser-smoke": [
|
|
44
|
+
'Write a minimal Playwright browser smoke test: import { test, expect } from "@playwright/test", ' +
|
|
45
|
+
"navigate to the route under test, and assert that one key user-visible element is visible. Keep " +
|
|
46
|
+
"it a happy-path smoke test, not an exhaustive suite. Place it under the project's end-to-end test " +
|
|
47
|
+
"directory.",
|
|
48
|
+
],
|
|
49
|
+
unsupported: [],
|
|
50
|
+
};
|
|
51
|
+
function systemContent(conventions, strategy) {
|
|
52
|
+
const lines = [
|
|
53
|
+
`You are a senior engineer writing ${STYLE_DESCRIPTOR[strategy.style]} for existing code.`,
|
|
54
|
+
runnerLine(conventions, strategy),
|
|
55
|
+
`Place new tests using the project's "${conventions.fileNamingStyle}" naming convention.`,
|
|
56
|
+
locationLine(conventions, strategy),
|
|
57
|
+
...STYLE_GUIDANCE[strategy.style],
|
|
58
|
+
OUTPUT_CONTRACT,
|
|
59
|
+
];
|
|
60
|
+
return `${lines.join("\n")}${assertionStyleBlock(conventions)}`;
|
|
61
|
+
}
|
|
62
|
+
function runnerLine(conventions, strategy) {
|
|
63
|
+
if (strategy.style === "browser-smoke" || strategy.verification === "playwright") {
|
|
64
|
+
return "Browser test runner: Playwright.";
|
|
65
|
+
}
|
|
66
|
+
return `Test framework: ${conventions.framework}.`;
|
|
67
|
+
}
|
|
68
|
+
function locationLine(conventions, strategy) {
|
|
69
|
+
if (strategy.style === "browser-smoke") {
|
|
70
|
+
return conventions.testDirs.length > 0
|
|
71
|
+
? `Project test directories: ${conventions.testDirs.join(", ")}; place browser smoke tests under the project's Playwright/end-to-end test directory.`
|
|
72
|
+
: "No dedicated test directory detected; place browser smoke tests under the project's Playwright/end-to-end test directory.";
|
|
73
|
+
}
|
|
74
|
+
return conventions.testDirs.length > 0
|
|
75
|
+
? `Project test directories: ${conventions.testDirs.join(", ")}.`
|
|
76
|
+
: "No dedicated test directory detected; place tests beside their source.";
|
|
77
|
+
}
|
|
78
|
+
function assertionStyleBlock(conventions) {
|
|
79
|
+
if (conventions.assertionStyleSamples.length === 0) {
|
|
80
|
+
return "";
|
|
81
|
+
}
|
|
82
|
+
const samples = conventions.assertionStyleSamples
|
|
83
|
+
.map((sample, idx) => `Example test ${String(idx + 1)}:\n${sample}`)
|
|
84
|
+
.join("\n\n");
|
|
85
|
+
return `\n\nMatch the assertion and structure style of these existing tests:\n${samples}`;
|
|
86
|
+
}
|
|
87
|
+
function targetAction(strategy) {
|
|
88
|
+
switch (strategy.style) {
|
|
89
|
+
case "component":
|
|
90
|
+
return "Write React component tests";
|
|
91
|
+
case "interaction":
|
|
92
|
+
return "Write React component interaction tests";
|
|
93
|
+
case "accessibility-smoke":
|
|
94
|
+
return "Write React component tests with an accessibility smoke check";
|
|
95
|
+
case "browser-smoke":
|
|
96
|
+
return "Write a minimal Playwright browser smoke test for the route or application entry point";
|
|
97
|
+
case "unit":
|
|
98
|
+
case "unsupported":
|
|
99
|
+
return "Write unit tests";
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function targetDescription(target, strategy) {
|
|
103
|
+
const action = targetAction(strategy);
|
|
104
|
+
if (target.kind === "file") {
|
|
105
|
+
return target.targetFunction === undefined
|
|
106
|
+
? `${action} for the public API in ${target.filePath}.`
|
|
107
|
+
: `${action} for the function ${target.targetFunction} in ${target.filePath}.`;
|
|
108
|
+
}
|
|
109
|
+
if (target.kind === "module") {
|
|
110
|
+
return `${action} for the source files in the module directory ${target.moduleDir}.`;
|
|
111
|
+
}
|
|
112
|
+
return `${action} for these changed files: ${target.filePaths.join(", ")}.`;
|
|
113
|
+
}
|
|
114
|
+
function contextBlock(pack) {
|
|
115
|
+
if (pack.selected.length === 0) {
|
|
116
|
+
return "";
|
|
117
|
+
}
|
|
118
|
+
const entries = pack.selected
|
|
119
|
+
.map((entry) => `--- ${entry.path} ---\n${entry.excerpt}`)
|
|
120
|
+
.join("\n\n");
|
|
121
|
+
return `\n\nRepository context:\n${entries}`;
|
|
122
|
+
}
|
|
123
|
+
function userContent(input, strategy, pack, rejectionReason) {
|
|
124
|
+
const retry = rejectionReason === undefined
|
|
125
|
+
? ""
|
|
126
|
+
: `\n\nThe previous diff was rejected: ${rejectionReason}. Produce a corrected diff that ` +
|
|
127
|
+
"modifies ONLY test files.";
|
|
128
|
+
return `${targetDescription(input.target, strategy)}${contextBlock(pack)}${retry}`;
|
|
129
|
+
}
|
|
130
|
+
// rejectionReason is appended on a retry (D8) so the model can correct an invalid/out-of-scope
|
|
131
|
+
// diff; it is undefined on the first attempt. The core signature is (input, conventions, strategy,
|
|
132
|
+
// pack); the optional 5th argument carries retry state without breaking that contract. The strategy
|
|
133
|
+
// (Issue #1203) drives the per-style test idioms in the system message.
|
|
134
|
+
export function buildPrompt(input, conventions, strategy, pack, rejectionReason) {
|
|
135
|
+
return [
|
|
136
|
+
{ role: "system", content: systemContent(conventions, strategy) },
|
|
137
|
+
{ role: "user", content: userContent(input, strategy, pack, rejectionReason) },
|
|
138
|
+
];
|
|
139
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { PatchFileChange } from "@oscharko-dev/keiko-tools";
|
|
2
|
+
import type { VerificationAuditSummary } from "@oscharko-dev/keiko-verification";
|
|
3
|
+
import type { TestStyle, TestVerification } from "./frontend.js";
|
|
4
|
+
import type { UnitTestWorkflowReport, WorkflowStatus } from "./types.js";
|
|
5
|
+
export interface ReportParts {
|
|
6
|
+
readonly status: WorkflowStatus;
|
|
7
|
+
readonly modelId: string;
|
|
8
|
+
readonly durationMs: number;
|
|
9
|
+
readonly patchFiles: readonly PatchFileChange[];
|
|
10
|
+
readonly dryRunPreview: string | undefined;
|
|
11
|
+
readonly proposedDiff: string | undefined;
|
|
12
|
+
readonly coveredBehavior: string | undefined;
|
|
13
|
+
readonly knownGaps: string | undefined;
|
|
14
|
+
readonly nextActions: readonly string[];
|
|
15
|
+
readonly failureReason?: string | undefined;
|
|
16
|
+
readonly verificationSummary: VerificationAuditSummary | undefined;
|
|
17
|
+
readonly verificationSkipReason: string | undefined;
|
|
18
|
+
readonly modelCallCount: number;
|
|
19
|
+
readonly patchRetryCount: number;
|
|
20
|
+
readonly testStyle?: TestStyle | undefined;
|
|
21
|
+
readonly verification?: TestVerification | undefined;
|
|
22
|
+
readonly limitation?: string | undefined;
|
|
23
|
+
}
|
|
24
|
+
export declare function assembleReport(parts: ReportParts): UnitTestWorkflowReport;
|
|
25
|
+
export declare function renderMarkdownReport(report: UnitTestWorkflowReport): string;
|
|
26
|
+
//# sourceMappingURL=report.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"report.d.ts","sourceRoot":"","sources":["../../src/unit-tests/report.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AACjE,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,kCAAkC,CAAC;AACjF,OAAO,KAAK,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACjE,OAAO,KAAK,EAAiB,sBAAsB,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AA6BxF,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAC;IAChC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,UAAU,EAAE,SAAS,eAAe,EAAE,CAAC;IAChD,QAAQ,CAAC,aAAa,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3C,QAAQ,CAAC,YAAY,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1C,QAAQ,CAAC,eAAe,EAAE,MAAM,GAAG,SAAS,CAAC;IAC7C,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,CAAC;IACvC,QAAQ,CAAC,WAAW,EAAE,SAAS,MAAM,EAAE,CAAC;IACxC,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5C,QAAQ,CAAC,mBAAmB,EAAE,wBAAwB,GAAG,SAAS,CAAC;IACnE,QAAQ,CAAC,sBAAsB,EAAE,MAAM,GAAG,SAAS,CAAC;IACpD,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,GAAG,SAAS,CAAC;IAC3C,QAAQ,CAAC,YAAY,CAAC,EAAE,gBAAgB,GAAG,SAAS,CAAC;IACrD,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC1C;AAMD,wBAAgB,cAAc,CAAC,KAAK,EAAE,WAAW,GAAG,sBAAsB,CAqBzE;AAuCD,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,sBAAsB,GAAG,MAAM,CAiB3E"}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// Report assembly and Markdown rendering (ADR-0008 D3, steering note A). assembleReport composes
|
|
2
|
+
// the JSON-serializable UnitTestWorkflowReport from pipeline stage outputs; renderMarkdownReport
|
|
3
|
+
// produces the CLI text path. ALL prose (coveredBehavior, knownGaps, nextActions), the dry-run
|
|
4
|
+
// preview, the proposed diff, verificationSkipReason, and addedTestFiles[].path are redacted via
|
|
5
|
+
// redact() here so nothing the report carries can leak a secret — defence in depth on top of the
|
|
6
|
+
// redaction already applied upstream. Pure: no IO, no clock; the caller injects durationMs and counters.
|
|
7
|
+
import { redact } from "@oscharko-dev/keiko-security";
|
|
8
|
+
const TEST_CASE_PREFIXES = ["test(", "it(", "describe("];
|
|
9
|
+
// Best-effort count of added test cases: added (`+`) lines whose trimmed text begins with a known
|
|
10
|
+
// test-case opener. Not authoritative — purely informational for the report.
|
|
11
|
+
function estimateTestCount(file) {
|
|
12
|
+
let count = 0;
|
|
13
|
+
for (const hunk of file.hunks) {
|
|
14
|
+
for (const line of hunk.lines) {
|
|
15
|
+
if (!line.startsWith("+")) {
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
const trimmed = line.slice(1).trimStart();
|
|
19
|
+
if (TEST_CASE_PREFIXES.some((prefix) => trimmed.startsWith(prefix))) {
|
|
20
|
+
count += 1;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return count;
|
|
25
|
+
}
|
|
26
|
+
function toAddedTestFiles(files) {
|
|
27
|
+
return files.map((file) => ({
|
|
28
|
+
path: redact(file.path),
|
|
29
|
+
estimatedTestCount: estimateTestCount(file),
|
|
30
|
+
}));
|
|
31
|
+
}
|
|
32
|
+
function redactOptional(value) {
|
|
33
|
+
return value === undefined ? undefined : redact(value);
|
|
34
|
+
}
|
|
35
|
+
export function assembleReport(parts) {
|
|
36
|
+
return {
|
|
37
|
+
workflowId: "unit-test-generation",
|
|
38
|
+
status: parts.status,
|
|
39
|
+
modelId: parts.modelId,
|
|
40
|
+
durationMs: parts.durationMs,
|
|
41
|
+
dryRunPreview: redactOptional(parts.dryRunPreview),
|
|
42
|
+
proposedDiff: redactOptional(parts.proposedDiff),
|
|
43
|
+
addedTestFiles: toAddedTestFiles(parts.patchFiles),
|
|
44
|
+
coveredBehavior: redactOptional(parts.coveredBehavior),
|
|
45
|
+
knownGaps: redactOptional(parts.knownGaps),
|
|
46
|
+
nextActions: parts.nextActions.map((action) => redact(action)),
|
|
47
|
+
failureReason: redactOptional(parts.failureReason),
|
|
48
|
+
verificationSummary: parts.verificationSummary,
|
|
49
|
+
verificationSkipReason: redactOptional(parts.verificationSkipReason),
|
|
50
|
+
modelCallCount: parts.modelCallCount,
|
|
51
|
+
patchRetryCount: parts.patchRetryCount,
|
|
52
|
+
testStyle: parts.testStyle,
|
|
53
|
+
verification: parts.verification,
|
|
54
|
+
limitation: redactOptional(parts.limitation),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
function sectionIf(heading, body) {
|
|
58
|
+
return body === undefined ? [] : [`## ${heading}`, body, ""];
|
|
59
|
+
}
|
|
60
|
+
function fileLines(report) {
|
|
61
|
+
if (report.addedTestFiles.length === 0) {
|
|
62
|
+
return [];
|
|
63
|
+
}
|
|
64
|
+
const rows = report.addedTestFiles.map((f) => `- ${f.path} (~${String(f.estimatedTestCount)} test case(s))`);
|
|
65
|
+
return ["## Test files", ...rows, ""];
|
|
66
|
+
}
|
|
67
|
+
function verificationLine(report) {
|
|
68
|
+
if (report.verificationSummary !== undefined) {
|
|
69
|
+
return [`## Verification`, `Status: ${report.verificationSummary.overallStatus}`, ""];
|
|
70
|
+
}
|
|
71
|
+
if (report.verificationSkipReason !== undefined) {
|
|
72
|
+
return [`## Verification`, report.verificationSkipReason, ""];
|
|
73
|
+
}
|
|
74
|
+
return [];
|
|
75
|
+
}
|
|
76
|
+
function styleLine(report) {
|
|
77
|
+
if (report.testStyle === undefined) {
|
|
78
|
+
return [];
|
|
79
|
+
}
|
|
80
|
+
const verification = report.verification === undefined || report.verification === "none"
|
|
81
|
+
? ""
|
|
82
|
+
: ` · verification: ${report.verification}`;
|
|
83
|
+
return [`Test style: ${report.testStyle}${verification}`, ""];
|
|
84
|
+
}
|
|
85
|
+
// A human-readable Markdown report for the CLI text path. Every field is already redacted by
|
|
86
|
+
// assembleReport, so rendering is plain string composition.
|
|
87
|
+
export function renderMarkdownReport(report) {
|
|
88
|
+
return [
|
|
89
|
+
`# Unit-test generation: ${report.status}`,
|
|
90
|
+
`Model: ${report.modelId} · ${String(report.durationMs)}ms · ` +
|
|
91
|
+
`${String(report.modelCallCount)} model call(s) · ${String(report.patchRetryCount)} retry(ies)`,
|
|
92
|
+
"",
|
|
93
|
+
...styleLine(report),
|
|
94
|
+
...sectionIf("Limitation", report.limitation),
|
|
95
|
+
...fileLines(report),
|
|
96
|
+
...sectionIf("Covered behavior", report.coveredBehavior),
|
|
97
|
+
...sectionIf("Known gaps", report.knownGaps),
|
|
98
|
+
...sectionIf("Failure", report.failureReason),
|
|
99
|
+
...verificationLine(report),
|
|
100
|
+
...(report.nextActions.length > 0
|
|
101
|
+
? ["## Next actions", ...report.nextActions.map((a) => `- ${a}`), ""]
|
|
102
|
+
: []),
|
|
103
|
+
].join("\n");
|
|
104
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { WorkspaceInfo } from "@oscharko-dev/keiko-workspace";
|
|
2
|
+
import { type AcceptedPatch, type ModelLoopResult, type RunState } from "./internal.js";
|
|
3
|
+
import type { TestStrategy } from "./frontend.js";
|
|
4
|
+
import type { UnitTestWorkflowReport } from "./types.js";
|
|
5
|
+
export declare function rejectedReport(state: RunState, loop: ModelLoopResult, strategy: TestStrategy): UnitTestWorkflowReport;
|
|
6
|
+
export declare function unsupportedReport(state: RunState, strategy: TestStrategy): UnitTestWorkflowReport;
|
|
7
|
+
export declare function dryRunReport(state: RunState, loop: ModelLoopResult, accepted: AcceptedPatch, strategy: TestStrategy): UnitTestWorkflowReport;
|
|
8
|
+
export declare function cancelledReport(state: RunState, loop: ModelLoopResult, accepted: AcceptedPatch | undefined, strategy?: TestStrategy): UnitTestWorkflowReport;
|
|
9
|
+
export declare function failedReport(state: RunState, error: unknown): UnitTestWorkflowReport;
|
|
10
|
+
export declare function emitCompleted(state: RunState, report: UnitTestWorkflowReport): UnitTestWorkflowReport;
|
|
11
|
+
export declare function finishPipeline(state: RunState, workspace: WorkspaceInfo, loop: ModelLoopResult, strategy: TestStrategy): Promise<UnitTestWorkflowReport>;
|
|
12
|
+
//# sourceMappingURL=stages.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stages.d.ts","sourceRoot":"","sources":["../../src/unit-tests/stages.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAC;AAInE,OAAO,EAEL,KAAK,aAAa,EAClB,KAAK,eAAe,EACpB,KAAK,QAAQ,EACd,MAAM,eAAe,CAAC;AACvB,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAClD,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AAWzD,wBAAgB,cAAc,CAC5B,KAAK,EAAE,QAAQ,EACf,IAAI,EAAE,eAAe,EACrB,QAAQ,EAAE,YAAY,GACrB,sBAAsB,CAoBxB;AAKD,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,YAAY,GAAG,sBAAsB,CAsBjG;AAED,wBAAgB,YAAY,CAC1B,KAAK,EAAE,QAAQ,EACf,IAAI,EAAE,eAAe,EACrB,QAAQ,EAAE,aAAa,EACvB,QAAQ,EAAE,YAAY,GACrB,sBAAsB,CAmBxB;AAED,wBAAgB,eAAe,CAC7B,KAAK,EAAE,QAAQ,EACf,IAAI,EAAE,eAAe,EACrB,QAAQ,EAAE,aAAa,GAAG,SAAS,EACnC,QAAQ,CAAC,EAAE,YAAY,GACtB,sBAAsB,CAkBxB;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,OAAO,GAAG,sBAAsB,CAoBpF;AA+ED,wBAAgB,aAAa,CAC3B,KAAK,EAAE,QAAQ,EACf,MAAM,EAAE,sBAAsB,GAC7B,sBAAsB,CAOxB;AAID,wBAAsB,cAAc,CAClC,KAAK,EAAE,QAAQ,EACf,SAAS,EAAE,aAAa,EACxB,IAAI,EAAE,eAAe,EACrB,QAAQ,EAAE,YAAY,GACrB,OAAO,CAAC,sBAAsB,CAAC,CAWjC"}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
// Terminal-report stages (ADR-0008 D3/D5). Each function maps a pipeline outcome — rejected,
|
|
2
|
+
// dry-run, applied+verified, cancelled, or failed — to the redacted UnitTestWorkflowReport via
|
|
3
|
+
// assembleReport. applyPatch (apply mode) is the one IO boundary here and is fail-closed in #6
|
|
4
|
+
// (applyEnabled gates the write). finishPipeline selects the branch; emitCompleted stamps the
|
|
5
|
+
// terminal event. All prose/diff redaction happens inside assembleReport.
|
|
6
|
+
import { redact } from "@oscharko-dev/keiko-security";
|
|
7
|
+
import { applyPatch, CommandCancelledError, renderDryRun, } from "@oscharko-dev/keiko-tools";
|
|
8
|
+
import { nodeWorkspaceWriter } from "@oscharko-dev/keiko-tools/internal/writer";
|
|
9
|
+
import { nodeWorkspaceFs } from "@oscharko-dev/keiko-workspace/internal/fs";
|
|
10
|
+
import { createScopedWriter } from "../governed-handoff.js";
|
|
11
|
+
import { assembleReport } from "./report.js";
|
|
12
|
+
import { runWorkflowVerification } from "./verify-stage.js";
|
|
13
|
+
import { nextActionsFor, } from "./internal.js";
|
|
14
|
+
// The content-free strategy fields surfaced on every report assembled after strategy selection
|
|
15
|
+
// (Issue #1203). `undefined` strategy (an early failure before selection) leaves them absent.
|
|
16
|
+
function strategyFields(strategy) {
|
|
17
|
+
return { testStyle: strategy?.style, verification: strategy?.verification };
|
|
18
|
+
}
|
|
19
|
+
export function rejectedReport(state, loop, strategy) {
|
|
20
|
+
return assembleReport({
|
|
21
|
+
status: "rejected",
|
|
22
|
+
modelId: state.input.modelId,
|
|
23
|
+
durationMs: state.now() - state.startedAt,
|
|
24
|
+
patchFiles: [],
|
|
25
|
+
dryRunPreview: undefined,
|
|
26
|
+
proposedDiff: undefined,
|
|
27
|
+
coveredBehavior: undefined,
|
|
28
|
+
knownGaps: undefined,
|
|
29
|
+
nextActions: [
|
|
30
|
+
`The model did not produce an in-scope test patch (${loop.lastRejectionCode ?? "unknown"})`,
|
|
31
|
+
],
|
|
32
|
+
failureReason: undefined,
|
|
33
|
+
verificationSummary: undefined,
|
|
34
|
+
verificationSkipReason: undefined,
|
|
35
|
+
modelCallCount: loop.modelCallCount,
|
|
36
|
+
patchRetryCount: loop.patchRetryCount,
|
|
37
|
+
...strategyFields(strategy),
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
// The target's frontend stack is unsupported (Issue #1203 AC5): a clear, reviewable limitation is
|
|
41
|
+
// reported BEFORE any model call, so no fabricated browser/component test is produced. The status is
|
|
42
|
+
// "rejected" (no patch was produced) and `modelCallCount` is 0.
|
|
43
|
+
export function unsupportedReport(state, strategy) {
|
|
44
|
+
return assembleReport({
|
|
45
|
+
status: "rejected",
|
|
46
|
+
modelId: state.input.modelId,
|
|
47
|
+
durationMs: state.now() - state.startedAt,
|
|
48
|
+
patchFiles: [],
|
|
49
|
+
dryRunPreview: undefined,
|
|
50
|
+
proposedDiff: undefined,
|
|
51
|
+
coveredBehavior: undefined,
|
|
52
|
+
knownGaps: undefined,
|
|
53
|
+
nextActions: [
|
|
54
|
+
"No test was generated: the target's frontend test stack is unsupported. " +
|
|
55
|
+
"Add a supported test stack (for React components, @testing-library/react) and retry.",
|
|
56
|
+
],
|
|
57
|
+
failureReason: undefined,
|
|
58
|
+
verificationSummary: undefined,
|
|
59
|
+
verificationSkipReason: "verification skipped: unsupported frontend stack, no test generated",
|
|
60
|
+
modelCallCount: 0,
|
|
61
|
+
patchRetryCount: 0,
|
|
62
|
+
limitation: strategy.reason,
|
|
63
|
+
...strategyFields(strategy),
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
export function dryRunReport(state, loop, accepted, strategy) {
|
|
67
|
+
const files = accepted.validation.files.map((f) => f.path);
|
|
68
|
+
return assembleReport({
|
|
69
|
+
status: "dry-run",
|
|
70
|
+
modelId: state.input.modelId,
|
|
71
|
+
durationMs: state.now() - state.startedAt,
|
|
72
|
+
patchFiles: accepted.validation.files,
|
|
73
|
+
dryRunPreview: renderDryRun(accepted.validation),
|
|
74
|
+
proposedDiff: accepted.diff,
|
|
75
|
+
coveredBehavior: accepted.coveredBehavior,
|
|
76
|
+
knownGaps: accepted.knownGaps,
|
|
77
|
+
nextActions: nextActionsFor(false, files),
|
|
78
|
+
failureReason: undefined,
|
|
79
|
+
verificationSummary: undefined,
|
|
80
|
+
verificationSkipReason: "verification skipped: dry-run, no files written",
|
|
81
|
+
modelCallCount: loop.modelCallCount,
|
|
82
|
+
patchRetryCount: loop.patchRetryCount,
|
|
83
|
+
...strategyFields(strategy),
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
export function cancelledReport(state, loop, accepted, strategy) {
|
|
87
|
+
return assembleReport({
|
|
88
|
+
status: "cancelled",
|
|
89
|
+
modelId: state.input.modelId,
|
|
90
|
+
durationMs: state.now() - state.startedAt,
|
|
91
|
+
patchFiles: accepted?.validation.files ?? [],
|
|
92
|
+
dryRunPreview: accepted === undefined ? undefined : renderDryRun(accepted.validation),
|
|
93
|
+
proposedDiff: accepted?.diff,
|
|
94
|
+
coveredBehavior: undefined,
|
|
95
|
+
knownGaps: undefined,
|
|
96
|
+
nextActions: ["The workflow was cancelled before completion"],
|
|
97
|
+
failureReason: undefined,
|
|
98
|
+
verificationSummary: undefined,
|
|
99
|
+
verificationSkipReason: "verification skipped: cancelled",
|
|
100
|
+
modelCallCount: loop.modelCallCount,
|
|
101
|
+
patchRetryCount: loop.patchRetryCount,
|
|
102
|
+
...strategyFields(strategy),
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
export function failedReport(state, error) {
|
|
106
|
+
const message = redact(error instanceof Error ? error.message : "unexpected workflow failure");
|
|
107
|
+
const errorCode = error instanceof Error ? error.name : "UNKNOWN";
|
|
108
|
+
state.emitter.emit({ type: "workflow:failed", errorCode, message });
|
|
109
|
+
return assembleReport({
|
|
110
|
+
status: "failed",
|
|
111
|
+
modelId: state.input.modelId,
|
|
112
|
+
durationMs: state.now() - state.startedAt,
|
|
113
|
+
patchFiles: [],
|
|
114
|
+
dryRunPreview: undefined,
|
|
115
|
+
proposedDiff: undefined,
|
|
116
|
+
coveredBehavior: undefined,
|
|
117
|
+
knownGaps: undefined,
|
|
118
|
+
nextActions: [`Inspect the error and retry: ${message}`],
|
|
119
|
+
failureReason: message,
|
|
120
|
+
verificationSummary: undefined,
|
|
121
|
+
verificationSkipReason: undefined,
|
|
122
|
+
modelCallCount: state.progress.modelCallCount,
|
|
123
|
+
patchRetryCount: state.progress.patchRetryCount,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
function resolveApplyWriter(state, workspace) {
|
|
127
|
+
if (state.deps.workflowHandoff === undefined) {
|
|
128
|
+
return state.deps.writer;
|
|
129
|
+
}
|
|
130
|
+
return createScopedWriter(state.deps.writer ?? nodeWorkspaceWriter, workspace.root, state.deps.workflowHandoff.patchScope.editablePaths);
|
|
131
|
+
}
|
|
132
|
+
function completedReport(state, loop, accepted, applyResult, verification, strategy) {
|
|
133
|
+
return assembleReport({
|
|
134
|
+
status: "completed",
|
|
135
|
+
modelId: state.input.modelId,
|
|
136
|
+
durationMs: state.now() - state.startedAt,
|
|
137
|
+
patchFiles: accepted.validation.files,
|
|
138
|
+
dryRunPreview: renderDryRun(accepted.validation),
|
|
139
|
+
proposedDiff: accepted.diff,
|
|
140
|
+
coveredBehavior: accepted.coveredBehavior,
|
|
141
|
+
knownGaps: accepted.knownGaps,
|
|
142
|
+
nextActions: nextActionsFor(true, applyResult.changedFiles),
|
|
143
|
+
failureReason: undefined,
|
|
144
|
+
verificationSummary: verification.summary,
|
|
145
|
+
verificationSkipReason: verification.skipReason,
|
|
146
|
+
modelCallCount: loop.modelCallCount,
|
|
147
|
+
patchRetryCount: loop.patchRetryCount,
|
|
148
|
+
...strategyFields(strategy),
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
async function applyAndVerify(state, workspace, loop, accepted, strategy) {
|
|
152
|
+
const fs = state.deps.fs ?? nodeWorkspaceFs;
|
|
153
|
+
const writer = resolveApplyWriter(state, workspace);
|
|
154
|
+
let applyResult;
|
|
155
|
+
try {
|
|
156
|
+
applyResult = applyPatch(workspace, accepted.diff, {
|
|
157
|
+
applyEnabled: true,
|
|
158
|
+
signal: state.signal,
|
|
159
|
+
fs,
|
|
160
|
+
...(writer === undefined ? {} : { writer }),
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
if (error instanceof CommandCancelledError) {
|
|
165
|
+
return cancelledReport(state, loop, accepted, strategy);
|
|
166
|
+
}
|
|
167
|
+
throw error;
|
|
168
|
+
}
|
|
169
|
+
state.emitter.emit({
|
|
170
|
+
type: "workflow:patch:applied",
|
|
171
|
+
changedFiles: applyResult.changedFiles.length,
|
|
172
|
+
created: applyResult.created.length,
|
|
173
|
+
deleted: applyResult.deleted.length,
|
|
174
|
+
});
|
|
175
|
+
if (state.signal.aborted) {
|
|
176
|
+
return cancelledReport(state, loop, accepted, strategy);
|
|
177
|
+
}
|
|
178
|
+
const verification = await runWorkflowVerification(state, workspace, fs);
|
|
179
|
+
return completedReport(state, loop, accepted, applyResult, verification, strategy);
|
|
180
|
+
}
|
|
181
|
+
export function emitCompleted(state, report) {
|
|
182
|
+
state.emitter.emit({
|
|
183
|
+
type: "workflow:completed",
|
|
184
|
+
status: report.status,
|
|
185
|
+
durationMs: report.durationMs,
|
|
186
|
+
});
|
|
187
|
+
return report;
|
|
188
|
+
}
|
|
189
|
+
// Selects the terminal branch from the model-loop result: rejected (no patch), cancelled (abort
|
|
190
|
+
// before apply), apply+verify (apply mode), or dry-run (default).
|
|
191
|
+
export async function finishPipeline(state, workspace, loop, strategy) {
|
|
192
|
+
if (loop.accepted === undefined) {
|
|
193
|
+
return rejectedReport(state, loop, strategy);
|
|
194
|
+
}
|
|
195
|
+
if (state.signal.aborted) {
|
|
196
|
+
return cancelledReport(state, loop, loop.accepted, strategy);
|
|
197
|
+
}
|
|
198
|
+
if (state.input.apply === true) {
|
|
199
|
+
return applyAndVerify(state, workspace, loop, loop.accepted, strategy);
|
|
200
|
+
}
|
|
201
|
+
return dryRunReport(state, loop, loop.accepted, strategy);
|
|
202
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { ContextPack, WorkspaceFs, WorkspaceInfo } from "@oscharko-dev/keiko-workspace";
|
|
2
|
+
import { type TestStrategy } from "./frontend.js";
|
|
3
|
+
import type { TestConventions, UnitTestTarget } from "./types.js";
|
|
4
|
+
export declare function strategyAnchorPath(target: UnitTestTarget): string;
|
|
5
|
+
export declare function resolveTestStrategy(workspace: WorkspaceInfo, target: UnitTestTarget, conventions: TestConventions, pack: ContextPack, fs?: WorkspaceFs): TestStrategy;
|
|
6
|
+
//# sourceMappingURL=strategy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"strategy.d.ts","sourceRoot":"","sources":["../../src/unit-tests/strategy.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAC;AAC7F,OAAO,EAA2C,KAAK,YAAY,EAAE,MAAM,eAAe,CAAC;AAC3F,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAQlE,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,cAAc,GAAG,MAAM,CAQjE;AAQD,wBAAgB,mBAAmB,CACjC,SAAS,EAAE,aAAa,EACxB,MAAM,EAAE,cAAc,EACtB,WAAW,EAAE,eAAe,EAC5B,IAAI,EAAE,WAAW,EACjB,EAAE,GAAE,WAA6B,GAChC,YAAY,CASd"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// Resolves the convention-driven test strategy for a workflow target (Issue #1203). Bridges the pure
|
|
2
|
+
// frontend detector/selector (frontend.ts) to the workflow's UnitTestTarget and the already-assembled
|
|
3
|
+
// ContextPack: it derives the anchor source path, detects the project's frontend test stack from local
|
|
4
|
+
// manifests/config, samples the target's redacted source excerpt from the pack, and selects the style.
|
|
5
|
+
// The only effect is the manifest/config read delegated to the injected WorkspaceFs.
|
|
6
|
+
import { nodeWorkspaceFs } from "@oscharko-dev/keiko-workspace/internal/fs";
|
|
7
|
+
import { detectFrontendStack, selectTestStrategy } from "./frontend.js";
|
|
8
|
+
function toPosix(path) {
|
|
9
|
+
return path.split("\\").join("/");
|
|
10
|
+
}
|
|
11
|
+
// The single source path the strategy is anchored on: the file under test, the first changed file, or
|
|
12
|
+
// the module directory. Used both for stack detection (walk-up) and component classification.
|
|
13
|
+
export function strategyAnchorPath(target) {
|
|
14
|
+
if (target.kind === "file") {
|
|
15
|
+
return toPosix(target.filePath);
|
|
16
|
+
}
|
|
17
|
+
if (target.kind === "changedFiles") {
|
|
18
|
+
return toPosix(target.filePaths[0] ?? "");
|
|
19
|
+
}
|
|
20
|
+
return toPosix(target.moduleDir);
|
|
21
|
+
}
|
|
22
|
+
// The target file's redacted source excerpt, if it was selected into the ContextPack. A secondary
|
|
23
|
+
// classification signal for non-`.tsx` modules; absent when the target was dropped for budget.
|
|
24
|
+
function targetSourceExcerpt(pack, anchorPath) {
|
|
25
|
+
return pack.selected.find((entry) => toPosix(entry.path) === anchorPath)?.excerpt;
|
|
26
|
+
}
|
|
27
|
+
export function resolveTestStrategy(workspace, target, conventions, pack, fs = nodeWorkspaceFs) {
|
|
28
|
+
const anchorPath = strategyAnchorPath(target);
|
|
29
|
+
const stack = detectFrontendStack(workspace, fs, anchorPath);
|
|
30
|
+
return selectTestStrategy({
|
|
31
|
+
targetPath: anchorPath,
|
|
32
|
+
targetSource: targetSourceExcerpt(pack, anchorPath),
|
|
33
|
+
framework: conventions.framework,
|
|
34
|
+
stack,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { type WorkspaceFs, type WorkspaceInfo } from "@oscharko-dev/keiko-workspace";
|
|
2
|
+
import type { UnitTestTarget } from "./types.js";
|
|
3
|
+
export declare function targetInputPaths(target: UnitTestTarget): readonly string[];
|
|
4
|
+
export declare function assertTargetWithinWorkspace(workspace: WorkspaceInfo, target: UnitTestTarget, fs: WorkspaceFs): void;
|
|
5
|
+
//# sourceMappingURL=target-guard.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"target-guard.d.ts","sourceRoot":"","sources":["../../src/unit-tests/target-guard.ts"],"names":[],"mappings":"AAOA,OAAO,EAKL,KAAK,WAAW,EAChB,KAAK,aAAa,EACnB,MAAM,+BAA+B,CAAC;AACvC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAMjD,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,cAAc,GAAG,SAAS,MAAM,EAAE,CAQ1E;AAED,wBAAgB,2BAA2B,CACzC,SAAS,EAAE,aAAa,EACxB,MAAM,EAAE,cAAc,EACtB,EAAE,EAAE,WAAW,GACd,IAAI,CASN"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// Workspace-containment guard for the unit-test workflow input target (issue #641 / ADR-0005 D2).
|
|
2
|
+
// Runs BEFORE any model call so escaped, denied, or realpath-escape targets fail closed with
|
|
3
|
+
// modelCallCount=0 and no diff. Mirrors the discovery boundary rules: lexical containment via
|
|
4
|
+
// resolveWithinWorkspace, denylist via isDenied on the normalized relative path, and realpath
|
|
5
|
+
// containment via assertContainedRealPath for existing targets. Pure with respect to non-FS state.
|
|
6
|
+
import { relative } from "node:path";
|
|
7
|
+
import { PathDeniedError, assertContainedRealPath, isDenied, resolveWithinWorkspace, } from "@oscharko-dev/keiko-workspace";
|
|
8
|
+
function toPosix(path) {
|
|
9
|
+
return path.split("\\").join("/");
|
|
10
|
+
}
|
|
11
|
+
export function targetInputPaths(target) {
|
|
12
|
+
if (target.kind === "file") {
|
|
13
|
+
return [target.filePath];
|
|
14
|
+
}
|
|
15
|
+
if (target.kind === "module") {
|
|
16
|
+
return [target.moduleDir];
|
|
17
|
+
}
|
|
18
|
+
return target.filePaths;
|
|
19
|
+
}
|
|
20
|
+
export function assertTargetWithinWorkspace(workspace, target, fs) {
|
|
21
|
+
for (const candidate of targetInputPaths(target)) {
|
|
22
|
+
const absolute = resolveWithinWorkspace(workspace.root, candidate);
|
|
23
|
+
const normalizedRelative = toPosix(relative(workspace.root, absolute));
|
|
24
|
+
if (normalizedRelative !== "" && isDenied(normalizedRelative)) {
|
|
25
|
+
throw new PathDeniedError(`path is denied by workspace policy: ${candidate}`, candidate);
|
|
26
|
+
}
|
|
27
|
+
assertContainedRealPath(fs, workspace.root, absolute, "unit-test-target");
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { MemoryWorkflowPort } from "@oscharko-dev/keiko-contracts";
|
|
2
|
+
import type { WorkflowHandoffRequest } from "@oscharko-dev/keiko-contracts/workflow-handoff";
|
|
3
|
+
import type { ModelPort } from "@oscharko-dev/keiko-harness";
|
|
4
|
+
import type { SpawnFn, WorkspaceWriter } from "@oscharko-dev/keiko-tools";
|
|
5
|
+
import type { TestFramework, WorkspaceFs } from "@oscharko-dev/keiko-workspace";
|
|
6
|
+
import type { VerificationAuditSummary } from "@oscharko-dev/keiko-verification";
|
|
7
|
+
import type { WorkflowEventSink } from "./events.js";
|
|
8
|
+
import type { TestStyle, TestVerification } from "./frontend.js";
|
|
9
|
+
import type { FileNamingStyle, WorkflowLimits, WorkflowStatus } from "@oscharko-dev/keiko-contracts";
|
|
10
|
+
export type { WorkflowStatus, FileNamingStyle, WorkflowLimits, } from "@oscharko-dev/keiko-contracts";
|
|
11
|
+
export { DEFAULT_WORKFLOW_LIMITS } from "@oscharko-dev/keiko-contracts";
|
|
12
|
+
export type { ComponentFramework, FrontendTestStack, TestStrategy, TestStyle, TestVerification, } from "./frontend.js";
|
|
13
|
+
export type UnitTestTarget = {
|
|
14
|
+
readonly kind: "file";
|
|
15
|
+
readonly filePath: string;
|
|
16
|
+
readonly targetFunction?: string | undefined;
|
|
17
|
+
} | {
|
|
18
|
+
readonly kind: "module";
|
|
19
|
+
readonly moduleDir: string;
|
|
20
|
+
} | {
|
|
21
|
+
readonly kind: "changedFiles";
|
|
22
|
+
readonly filePaths: readonly string[];
|
|
23
|
+
};
|
|
24
|
+
export interface TestConventions {
|
|
25
|
+
readonly framework: TestFramework;
|
|
26
|
+
readonly testDirs: readonly string[];
|
|
27
|
+
readonly fileNamingStyle: FileNamingStyle;
|
|
28
|
+
readonly assertionStyleSamples: readonly string[];
|
|
29
|
+
}
|
|
30
|
+
export interface UnitTestWorkflowInput {
|
|
31
|
+
readonly workspaceRoot: string;
|
|
32
|
+
readonly target: UnitTestTarget;
|
|
33
|
+
readonly apply?: boolean | undefined;
|
|
34
|
+
readonly modelId: string;
|
|
35
|
+
readonly limits?: Partial<WorkflowLimits> | undefined;
|
|
36
|
+
}
|
|
37
|
+
export interface UnitTestWorkflowDeps {
|
|
38
|
+
readonly model: ModelPort;
|
|
39
|
+
readonly fs?: WorkspaceFs | undefined;
|
|
40
|
+
readonly writer?: WorkspaceWriter | undefined;
|
|
41
|
+
readonly spawn?: SpawnFn | undefined;
|
|
42
|
+
readonly now?: (() => number) | undefined;
|
|
43
|
+
readonly idSource?: (() => string) | undefined;
|
|
44
|
+
readonly sink?: WorkflowEventSink | undefined;
|
|
45
|
+
readonly processEnv?: NodeJS.ProcessEnv | undefined;
|
|
46
|
+
readonly signal?: AbortSignal | undefined;
|
|
47
|
+
readonly memoryPort?: MemoryWorkflowPort | undefined;
|
|
48
|
+
readonly workflowHandoff?: WorkflowHandoffRequest | undefined;
|
|
49
|
+
}
|
|
50
|
+
export interface AddedTestFile {
|
|
51
|
+
readonly path: string;
|
|
52
|
+
readonly estimatedTestCount: number;
|
|
53
|
+
}
|
|
54
|
+
export interface UnitTestWorkflowReport {
|
|
55
|
+
readonly workflowId: "unit-test-generation";
|
|
56
|
+
readonly status: WorkflowStatus;
|
|
57
|
+
readonly modelId: string;
|
|
58
|
+
readonly durationMs: number;
|
|
59
|
+
readonly dryRunPreview?: string | undefined;
|
|
60
|
+
readonly proposedDiff?: string | undefined;
|
|
61
|
+
readonly addedTestFiles: readonly AddedTestFile[];
|
|
62
|
+
readonly coveredBehavior?: string | undefined;
|
|
63
|
+
readonly knownGaps?: string | undefined;
|
|
64
|
+
readonly nextActions: readonly string[];
|
|
65
|
+
readonly failureReason?: string | undefined;
|
|
66
|
+
readonly verificationSummary?: VerificationAuditSummary | undefined;
|
|
67
|
+
readonly verificationSkipReason?: string | undefined;
|
|
68
|
+
readonly modelCallCount: number;
|
|
69
|
+
readonly patchRetryCount: number;
|
|
70
|
+
readonly testStyle?: TestStyle | undefined;
|
|
71
|
+
readonly verification?: TestVerification | undefined;
|
|
72
|
+
readonly limitation?: string | undefined;
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=types.d.ts.map
|