@orangepro/orangepro-mcp 0.2.29 → 0.2.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +246 -199
- package/dist/local/analyze/coverage.js +93 -12
- package/dist/local/analyze/coverageArtifacts.js +8 -1
- package/dist/local/autoProve.js +8 -6
- package/dist/local/cli.js +10 -2
- package/dist/local/cliArgs.js +2 -0
- package/dist/local/generate/draftGuidance.js +30 -0
- package/dist/local/generate/generator.js +105 -18
- package/dist/local/generate/prompt.js +23 -4
- package/dist/local/generate/promptV5.js +24 -1
- package/dist/local/generate/providers.js +28 -15
- package/dist/local/operations.js +93 -13
- package/dist/local/pack/coverageReport.js +13 -3
- package/dist/local/viz/behaviorReportData.js +40 -3
- package/dist/local/viz/behaviorReportHtml.js +43 -5
- package/docs/local-proof-kit.md +8 -0
- package/package.json +1 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
const COVERAGE_SUITE_MANIFEST = ".orangepro/coverage-suites.json";
|
|
3
4
|
const GO_COVERAGE_CANDIDATES = new Set([
|
|
4
5
|
"coverage.out",
|
|
5
6
|
"cover.out",
|
|
@@ -47,6 +48,48 @@ function symbolLanguage(n) {
|
|
|
47
48
|
return "java";
|
|
48
49
|
return "other";
|
|
49
50
|
}
|
|
51
|
+
function normalizedArtifactPath(value) {
|
|
52
|
+
const normalized = value.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
53
|
+
if (!normalized || path.posix.isAbsolute(normalized) || normalized.split("/").includes(".."))
|
|
54
|
+
return null;
|
|
55
|
+
return normalized;
|
|
56
|
+
}
|
|
57
|
+
function coverageSuiteManifest(root) {
|
|
58
|
+
const out = new Map();
|
|
59
|
+
try {
|
|
60
|
+
const raw = JSON.parse(readFileSync(path.join(root, COVERAGE_SUITE_MANIFEST), "utf8"));
|
|
61
|
+
for (const [artifactPath, value] of Object.entries(raw.artifacts ?? {})) {
|
|
62
|
+
const rel = normalizedArtifactPath(artifactPath);
|
|
63
|
+
if (!rel || !value || typeof value !== "object")
|
|
64
|
+
continue;
|
|
65
|
+
const suite = value.suite;
|
|
66
|
+
if (suite !== "unit" && suite !== "integration")
|
|
67
|
+
continue;
|
|
68
|
+
const command = value.command;
|
|
69
|
+
out.set(rel, { suite, ...(typeof command === "string" && command.trim() ? { command: command.trim() } : {}) });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
// Missing or malformed provenance is fail-visible as unclassified coverage.
|
|
74
|
+
}
|
|
75
|
+
return out;
|
|
76
|
+
}
|
|
77
|
+
function inferredCoverageSuite(rel) {
|
|
78
|
+
const normalized = `/${rel.toLowerCase().replace(/[^a-z0-9/._-]+/g, "-")}/`;
|
|
79
|
+
if (/(?:^|[/_.-])(integration|e2e|end-to-end)(?:[/_.-]|$)/.test(normalized))
|
|
80
|
+
return "integration";
|
|
81
|
+
if (/(?:^|[/_.-])unit(?:[/_.-]|$)/.test(normalized))
|
|
82
|
+
return "unit";
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
export function coverageSuiteForArtifact(root, rel) {
|
|
86
|
+
const normalized = normalizedArtifactPath(rel) ?? rel.replace(/\\/g, "/");
|
|
87
|
+
const declared = coverageSuiteManifest(root).get(normalized);
|
|
88
|
+
if (declared)
|
|
89
|
+
return { suite: declared.suite, suite_source: "manifest", ...(declared.command ? { command: declared.command } : {}) };
|
|
90
|
+
const inferred = inferredCoverageSuite(normalized);
|
|
91
|
+
return inferred ? { suite: inferred, suite_source: "inferred" } : { suite: "unclassified", suite_source: "unclassified" };
|
|
92
|
+
}
|
|
50
93
|
function artifactCandidates(root, files) {
|
|
51
94
|
const out = new Map();
|
|
52
95
|
const add = (candidate) => {
|
|
@@ -79,7 +122,11 @@ function artifactCandidates(root, files) {
|
|
|
79
122
|
catch {
|
|
80
123
|
/* no generated coverage dir */
|
|
81
124
|
}
|
|
82
|
-
return [...out.values()]
|
|
125
|
+
return [...out.values()]
|
|
126
|
+
.map((candidate) => {
|
|
127
|
+
return { ...candidate, ...coverageSuiteForArtifact(root, candidate.path) };
|
|
128
|
+
})
|
|
129
|
+
.sort((a, b) => a.path.localeCompare(b.path));
|
|
83
130
|
}
|
|
84
131
|
function discoverNestedCoverageArtifacts(root) {
|
|
85
132
|
const out = [];
|
|
@@ -109,7 +156,7 @@ function discoverNestedCoverageArtifacts(root) {
|
|
|
109
156
|
}
|
|
110
157
|
};
|
|
111
158
|
visit("");
|
|
112
|
-
return out;
|
|
159
|
+
return out.map((candidate) => ({ ...candidate, ...coverageSuiteForArtifact(root, candidate.path) }));
|
|
113
160
|
}
|
|
114
161
|
function goModuleForDir(dir, goModulesByDir) {
|
|
115
162
|
for (;;) {
|
|
@@ -424,13 +471,14 @@ function parseJacocoXml(root, rel, codeFiles) {
|
|
|
424
471
|
};
|
|
425
472
|
}
|
|
426
473
|
function parseCoverageArtifact(root, candidate, goModulesByDir, codeFiles) {
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
474
|
+
const parsed = candidate.format === "go-coverprofile"
|
|
475
|
+
? parseGoCoverprofile(root, candidate.path, goModulesByDir, codeFiles)
|
|
476
|
+
: candidate.format === "lcov"
|
|
477
|
+
? parseLcov(root, candidate.path, codeFiles)
|
|
478
|
+
: candidate.format === "coverage-py"
|
|
479
|
+
? parseCoveragePyXml(root, candidate.path, codeFiles)
|
|
480
|
+
: parseJacocoXml(root, candidate.path, codeFiles);
|
|
481
|
+
return { ...parsed, ranges: parsed.ranges.map((range) => ({ ...range, suite: candidate.suite })) };
|
|
434
482
|
}
|
|
435
483
|
function overlaps(aStart, aEnd, bStart, bEnd) {
|
|
436
484
|
return aStart <= bEnd && bStart <= aEnd;
|
|
@@ -451,7 +499,14 @@ export function applyRuntimeCoverage(root, files, nodes, goModulesByDir) {
|
|
|
451
499
|
skippedArtifacts.push(skipped);
|
|
452
500
|
if (ranges.length === 0)
|
|
453
501
|
continue;
|
|
454
|
-
const stat = artifactStats.get(artifact.path) ?? {
|
|
502
|
+
const stat = artifactStats.get(artifact.path) ?? {
|
|
503
|
+
files: new Set(),
|
|
504
|
+
covered_ranges: 0,
|
|
505
|
+
format: artifact.format,
|
|
506
|
+
suite: artifact.suite,
|
|
507
|
+
suite_source: artifact.suite_source,
|
|
508
|
+
...(artifact.command ? { command: artifact.command } : {})
|
|
509
|
+
};
|
|
455
510
|
for (const range of ranges) {
|
|
456
511
|
const list = rangesByFile.get(range.file);
|
|
457
512
|
if (list)
|
|
@@ -467,6 +522,9 @@ export function applyRuntimeCoverage(root, files, nodes, goModulesByDir) {
|
|
|
467
522
|
return undefined;
|
|
468
523
|
const byLanguage = {};
|
|
469
524
|
const coveredSymbols = new Set();
|
|
525
|
+
const unitSymbols = new Set();
|
|
526
|
+
const integrationSymbols = new Set();
|
|
527
|
+
const unclassifiedSymbols = new Set();
|
|
470
528
|
let totalEligible = 0;
|
|
471
529
|
let symbolsWithSpans = 0;
|
|
472
530
|
const ingestedLanguages = new Set([...artifactStats.values()].map((s) => languageForFormat(s.format)));
|
|
@@ -491,12 +549,20 @@ export function applyRuntimeCoverage(root, files, nodes, goModulesByDir) {
|
|
|
491
549
|
if (overlapping.length === 0)
|
|
492
550
|
continue;
|
|
493
551
|
const formats = [...new Set(overlapping.map((r) => r.format))].sort();
|
|
552
|
+
const suites = [...new Set(overlapping.map((r) => r.suite ?? "unclassified"))].sort();
|
|
494
553
|
coveredSymbols.add(n.external_id);
|
|
554
|
+
if (suites.includes("unit"))
|
|
555
|
+
unitSymbols.add(n.external_id);
|
|
556
|
+
if (suites.includes("integration"))
|
|
557
|
+
integrationSymbols.add(n.external_id);
|
|
558
|
+
if (suites.includes("unclassified"))
|
|
559
|
+
unclassifiedSymbols.add(n.external_id);
|
|
495
560
|
bucket.covered++;
|
|
496
561
|
n.properties = {
|
|
497
562
|
...n.properties,
|
|
498
563
|
runtime_covered: true,
|
|
499
|
-
runtime_coverage_formats: formats
|
|
564
|
+
runtime_coverage_formats: formats,
|
|
565
|
+
runtime_coverage_suites: suites
|
|
500
566
|
};
|
|
501
567
|
}
|
|
502
568
|
for (const bucket of Object.values(byLanguage))
|
|
@@ -505,6 +571,9 @@ export function applyRuntimeCoverage(root, files, nodes, goModulesByDir) {
|
|
|
505
571
|
artifacts: [...artifactStats.entries()].map(([path, s]) => ({
|
|
506
572
|
path,
|
|
507
573
|
format: s.format,
|
|
574
|
+
suite: s.suite,
|
|
575
|
+
suite_source: s.suite_source,
|
|
576
|
+
...(s.command ? { command: s.command } : {}),
|
|
508
577
|
files: s.files.size,
|
|
509
578
|
covered_ranges: s.covered_ranges
|
|
510
579
|
})),
|
|
@@ -513,6 +582,18 @@ export function applyRuntimeCoverage(root, files, nodes, goModulesByDir) {
|
|
|
513
582
|
symbols_with_spans: symbolsWithSpans,
|
|
514
583
|
covered_symbols: coveredSymbols.size,
|
|
515
584
|
covered_pct: pct(coveredSymbols.size, totalEligible),
|
|
516
|
-
by_language: byLanguage
|
|
585
|
+
by_language: byLanguage,
|
|
586
|
+
by_suite: {
|
|
587
|
+
unit: unitSymbols.size,
|
|
588
|
+
integration: integrationSymbols.size,
|
|
589
|
+
overlap: [...unitSymbols].filter((id) => integrationSymbols.has(id)).length,
|
|
590
|
+
unclassified: unclassifiedSymbols.size,
|
|
591
|
+
union: coveredSymbols.size,
|
|
592
|
+
unit_pct: pct(unitSymbols.size, totalEligible),
|
|
593
|
+
integration_pct: pct(integrationSymbols.size, totalEligible),
|
|
594
|
+
overlap_pct: pct([...unitSymbols].filter((id) => integrationSymbols.has(id)).length, totalEligible),
|
|
595
|
+
unclassified_pct: pct(unclassifiedSymbols.size, totalEligible),
|
|
596
|
+
union_pct: pct(coveredSymbols.size, totalEligible)
|
|
597
|
+
}
|
|
517
598
|
};
|
|
518
599
|
}
|
|
@@ -2,6 +2,7 @@ import { spawnSync } from "node:child_process";
|
|
|
2
2
|
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { reportProgress } from "../util/progress.js";
|
|
5
|
+
import { coverageSuiteForArtifact } from "./coverage.js";
|
|
5
6
|
const DEFAULT_TIMEOUT_MS = 120_000;
|
|
6
7
|
const DEFAULT_BUDGET_MS = 600_000;
|
|
7
8
|
const GENERATED_COVERAGE_DIR = ".orangepro/coverage";
|
|
@@ -129,6 +130,10 @@ export function prepareRuntimeCoverage(root, opts = {}) {
|
|
|
129
130
|
warnings.push("No local coverage generators found, so OrangePro did not generate runtime coverage.");
|
|
130
131
|
}
|
|
131
132
|
const artifacts = detectCoverageArtifacts(absRoot);
|
|
133
|
+
const unclassified = artifacts.filter((artifact) => artifact.ingestible && artifact.suite === "unclassified");
|
|
134
|
+
if (unclassified.length > 0) {
|
|
135
|
+
warnings.push(`${unclassified.length} ingestible coverage artifact(s) have no unit/integration provenance. Add ${COVERAGE_SUITE_MANIFEST_HELP} so OrangePro reports the suites separately.`);
|
|
136
|
+
}
|
|
132
137
|
const suggested_commands = suggestCoverageCommands(absRoot);
|
|
133
138
|
return {
|
|
134
139
|
root: absRoot,
|
|
@@ -139,10 +144,12 @@ export function prepareRuntimeCoverage(root, opts = {}) {
|
|
|
139
144
|
warnings
|
|
140
145
|
};
|
|
141
146
|
}
|
|
147
|
+
const COVERAGE_SUITE_MANIFEST_HELP = ".orangepro/coverage-suites.json with an artifacts map, for example {\"coverage/unit.coverprofile\":{\"suite\":\"unit\",\"command\":\"make unit-test-coverage\"}}";
|
|
142
148
|
export function detectCoverageArtifacts(root) {
|
|
143
149
|
const absRoot = path.resolve(root);
|
|
144
150
|
const out = new Map();
|
|
145
|
-
const add = (
|
|
151
|
+
const add = (raw) => {
|
|
152
|
+
const info = { ...raw, ...coverageSuiteForArtifact(absRoot, raw.path) };
|
|
146
153
|
const abs = path.join(absRoot, info.path);
|
|
147
154
|
if (!existsSync(abs))
|
|
148
155
|
return;
|
package/dist/local/autoProve.js
CHANGED
|
@@ -18,7 +18,7 @@ import { generateTests, readDeclaredDeps, unresolvedLocalImports } from "./gener
|
|
|
18
18
|
import { GENERATED_DIR, runHintsFor } from "./generate/runHints.js";
|
|
19
19
|
import { rankRiskGaps } from "./score/risk.js";
|
|
20
20
|
import { resolveProviderConfig } from "./localConfig.js";
|
|
21
|
-
import { buildProvider } from "./generate/providers.js";
|
|
21
|
+
import { buildProvider, DeterministicProvider } from "./generate/providers.js";
|
|
22
22
|
import { resolveContained } from "./reprove/paths.js";
|
|
23
23
|
import { buildRtm } from "./rtm.js";
|
|
24
24
|
import { loadLedger } from "./ledger.js";
|
|
@@ -834,10 +834,12 @@ export async function autoProve(root, opts, deps) {
|
|
|
834
834
|
attempts: ex.attempts
|
|
835
835
|
};
|
|
836
836
|
}
|
|
837
|
-
//
|
|
838
|
-
//
|
|
839
|
-
const
|
|
840
|
-
|
|
837
|
+
// Provider gate applies ONLY to the generation lane. Explicit deterministic mode is a
|
|
838
|
+
// valid offline provider; otherwise a real provider key/config is required.
|
|
839
|
+
const deterministic = opts.provider === "deterministic"
|
|
840
|
+
|| /^(1|true|yes)$/i.test(String(deps.env.ORANGEPRO_ALLOW_DETERMINISTIC ?? ""));
|
|
841
|
+
const providerConfig = deterministic ? null : resolveProviderConfig(deps.env, { provider: opts.provider, model: opts.model });
|
|
842
|
+
if (!providerConfig && !deterministic) {
|
|
841
843
|
const status = ex.proven > 0 ? "proven-run" : ex.attempted > 0 ? "ran-no-proof" : "skipped-no-key";
|
|
842
844
|
return {
|
|
843
845
|
ran: ex.attempted > 0,
|
|
@@ -851,7 +853,7 @@ export async function autoProve(root, opts, deps) {
|
|
|
851
853
|
attempts: ex.attempts
|
|
852
854
|
};
|
|
853
855
|
}
|
|
854
|
-
const provider = buildProvider(providerConfig);
|
|
856
|
+
const provider = deterministic ? new DeterministicProvider() : buildProvider(providerConfig);
|
|
855
857
|
const generate = deps.generate ?? generateTests;
|
|
856
858
|
const reader = fileReaderFor(sourceRoot);
|
|
857
859
|
// Generation gets only the budget the existing-tests lane left unspent, so existing +
|
package/dist/local/cli.js
CHANGED
|
@@ -78,7 +78,7 @@ const HELP = `opro — OrangePro (local-first, BYOK, metadata-only artifacts)
|
|
|
78
78
|
|
|
79
79
|
Usage:
|
|
80
80
|
opro # one-command start: analyze, optional AI links + flows, report, RTM, agent handoff
|
|
81
|
-
opro start [path] [--base <ref>] [--no-ai] [--no-ai-flows] [--generate-coverage] [--prompt-version v5] [--json]
|
|
81
|
+
opro start [path] [--base <ref>] [--no-ai] [--no-ai-flows] [--generate-coverage] [--proof-limit 5] [--generate-limit 20] [--prompt-version v5] [--json]
|
|
82
82
|
opro roast [path] [--limit 5] [--json] # keyless: find passing tests whose targeted mutant still survives
|
|
83
83
|
opro init
|
|
84
84
|
opro setup # interactive: choose a default model provider + model (saved locally)
|
|
@@ -199,6 +199,8 @@ async function main() {
|
|
|
199
199
|
aiAll: asBool(flags["ai-all"], false),
|
|
200
200
|
aiFlows: !asBool(flags["no-ai-flows"], false),
|
|
201
201
|
autoLimit: numericFlag(flags["auto-limit"]),
|
|
202
|
+
proofLimit: numericFlag(flags["proof-limit"]),
|
|
203
|
+
generateLimit: numericFlag(flags["generate-limit"]),
|
|
202
204
|
noAuto: asBool(flags["no-auto"], false),
|
|
203
205
|
promptVersion: flags["prompt-version"] === "v5" ? "v5" : undefined,
|
|
204
206
|
provider: typeof flags.provider === "string" ? flags.provider : undefined,
|
|
@@ -225,6 +227,10 @@ async function main() {
|
|
|
225
227
|
out(" Proof next: no behavior is dynamically proven yet; run from a coding agent with a model key and follow the generated-test proof handoff.");
|
|
226
228
|
}
|
|
227
229
|
const ap = res.auto_prove;
|
|
230
|
+
const gen = res.generation;
|
|
231
|
+
out(` Test generation: ${gen.status} — ${gen.runnable}/${gen.generated} runnable, ${Math.max(0, gen.drafts - gen.runnable)} blocked draft(s)`);
|
|
232
|
+
if (gen.reason)
|
|
233
|
+
out(` generation reason: ${gen.reason}`);
|
|
228
234
|
if (ap.status === "skipped-no-key") {
|
|
229
235
|
out(` Dynamic proof: skipped — ${ap.reason ?? "no provider key"}`);
|
|
230
236
|
}
|
|
@@ -527,7 +533,9 @@ async function main() {
|
|
|
527
533
|
if (res.artifacts.length) {
|
|
528
534
|
out(" found:");
|
|
529
535
|
for (const a of res.artifacts) {
|
|
530
|
-
out(` ${a.path} (${a.language}, ${a.format}${a.ingestible ? ", ingestible now" : ", detected only"})`);
|
|
536
|
+
out(` ${a.path} (${a.language}, ${a.format}, suite=${a.suite} [${a.suite_source}]${a.ingestible ? ", ingestible now" : ", detected only"})`);
|
|
537
|
+
if (a.command)
|
|
538
|
+
out(` command: ${a.command}`);
|
|
531
539
|
}
|
|
532
540
|
}
|
|
533
541
|
else {
|
package/dist/local/cliArgs.js
CHANGED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/** Classify only high-confidence generator/validator diagnostics. Ambiguous
|
|
2
|
+
* failures stay unknown rather than being mislabeled as a repo setup problem. */
|
|
3
|
+
export function classifyGeneratedDraftBlocker(reason) {
|
|
4
|
+
const text = reason ?? "";
|
|
5
|
+
if (/timed?\s*out|timeout|ETIMEDOUT|signal:\s*killed/i.test(text))
|
|
6
|
+
return "validation_timeout";
|
|
7
|
+
if (/(?:^|\b)(?:go|gofmt|python3|pytest|mvn|gradle|node|npm|vitest|jest|mocha)(?:\b[^.\n]*)?(?:not found|ENOENT|not installed)/i.test(text) ||
|
|
8
|
+
/(?:test runner|toolchain).*(?:missing|not found|not installed)/i.test(text))
|
|
9
|
+
return "toolchain_or_runner";
|
|
10
|
+
if (/no required module provides package|cannot find module|module not found|unresolved import|imports module-path package/i.test(text)) {
|
|
11
|
+
return "unresolved_import";
|
|
12
|
+
}
|
|
13
|
+
if (/syntax check failed|undefined:|unknown field|redeclared in this block|expected (?:declaration|operand|'[^']+'|"[^"]+"|[^ ]+),? found|literal not terminated|cannot assign/i.test(text))
|
|
14
|
+
return "generated_code";
|
|
15
|
+
return "unknown";
|
|
16
|
+
}
|
|
17
|
+
export function generatedDraftRemediation(reason) {
|
|
18
|
+
switch (classifyGeneratedDraftBlocker(reason)) {
|
|
19
|
+
case "generated_code":
|
|
20
|
+
return "Regenerate or repair the draft using symbols and APIs that exist in this package; installing dependencies will not fix this compiler error.";
|
|
21
|
+
case "unresolved_import":
|
|
22
|
+
return "Verify that the generated import path exists and is already declared by this repo; install dependencies only when the repo expects that import, then regenerate.";
|
|
23
|
+
case "toolchain_or_runner":
|
|
24
|
+
return "Install or configure the named toolchain/test runner, then re-run `opro start`.";
|
|
25
|
+
case "validation_timeout":
|
|
26
|
+
return "Warm the dependency cache or raise the relevant OrangePro validation timeout, then re-run `opro start`.";
|
|
27
|
+
default:
|
|
28
|
+
return "Review the named blocker, repair or regenerate the draft as needed, then re-run `opro start`; do not assume dependencies are missing.";
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -19,6 +19,7 @@ import { languageOf } from "../analyze/classify.js";
|
|
|
19
19
|
import { buildGroundedUserPrompt, buildRawUserPrompt, buildSystemPrompt, PROMPT_VERSION } from "./prompt.js";
|
|
20
20
|
import { buildBatchGenerationSystemPromptV5, buildBatchGenerationUserPromptV5, buildPlanningRepairSystemPromptV5, buildPlanningRepairUserPromptV5, buildPlanningSystemPromptV5, buildPlanningUserPromptV5, hasRepairableScenarioStructure, parseBatchGeneratedTests, parsePlannedScenariosStrict, scenarioTiesBackToRaw, PROMPT_VERSION_V5 } from "./promptV5.js";
|
|
21
21
|
import { BUCKET_LABEL, deriveBucketSignals, selectLocalBuckets } from "./buckets.js";
|
|
22
|
+
import { generatedDraftRemediation } from "./draftGuidance.js";
|
|
22
23
|
const MAX_LIMIT = 5;
|
|
23
24
|
const DEFAULT_LIMIT = 3;
|
|
24
25
|
const MAX_RELATED_FILES = 4;
|
|
@@ -26,7 +27,11 @@ const SYMBOL_EXCERPT_CONTEXT_LINES = 3;
|
|
|
26
27
|
const MAX_EXCERPT_CHARS = 8000;
|
|
27
28
|
const MAX_TARGET_TYPE_EXCERPT_CHARS = 5000;
|
|
28
29
|
const STATIC_CHECK_TIMEOUT_MS = 3000;
|
|
29
|
-
|
|
30
|
+
// Large Go monorepos can spend tens of seconds populating a cold module cache
|
|
31
|
+
// before the target package is compiled. Keep the check authoritative instead
|
|
32
|
+
// of downgrading valid generated tests while dependencies are still downloading.
|
|
33
|
+
const GO_COMPILE_CHECK_TIMEOUT_MS = 120000;
|
|
34
|
+
const V5_GENERATION_MAX_TOKENS = 4000;
|
|
30
35
|
/** A source ref that names a test file (used to place a generated test next to it). */
|
|
31
36
|
const TEST_REF_RE = /(\.(test|spec)\.[cm]?[jt]sx?$)|((^|\/)test\.[cm]?[jt]sx?$)|(_test\.[a-z]+$)|(_spec\.[a-z]+$)|((^|\/)test_[^/]+\.[a-z]+$)/i;
|
|
32
37
|
function areaOf(relPath) {
|
|
@@ -518,6 +523,9 @@ export function gatherContext(graph, behavior, framework, fileReader) {
|
|
|
518
523
|
// Subject imports are TS/JS lines — feeding them to a pytest/go target would
|
|
519
524
|
// produce an unparseable test in the other language.
|
|
520
525
|
const fwLower = framework.toLowerCase();
|
|
526
|
+
const goContext = fwLower.includes("go")
|
|
527
|
+
? goGenerationImportContext(graph.workspace.root, [...relatedFiles, ...testFiles], fileReader)
|
|
528
|
+
: { go_import_paths: [], go_module_paths: [], go_target_package_paths: [] };
|
|
521
529
|
const subjectImports = fwLower.includes("pytest") || fwLower.includes("python") || fwLower.includes("go") || fwLower.includes("junit") || fwLower.includes("java")
|
|
522
530
|
? []
|
|
523
531
|
: subjectImportsFor(graph, testFiles);
|
|
@@ -561,6 +569,9 @@ export function gatherContext(graph, behavior, framework, fileReader) {
|
|
|
561
569
|
// MISSING, never a re-derivation of a test that already exists.
|
|
562
570
|
existing_tests: examples.slice(0, 10),
|
|
563
571
|
subject_imports: subjectImports,
|
|
572
|
+
...(goContext.go_import_paths.length ? { go_import_paths: goContext.go_import_paths } : {}),
|
|
573
|
+
...(goContext.go_module_paths.length ? { go_module_paths: goContext.go_module_paths } : {}),
|
|
574
|
+
...(goContext.go_target_package_paths.length ? { go_target_package_paths: goContext.go_target_package_paths } : {}),
|
|
564
575
|
...(flowChain ? { flow_chain: flowChain } : {})
|
|
565
576
|
};
|
|
566
577
|
// entity_ids are graph external_ids: the behavior plus related file paths. A
|
|
@@ -1578,10 +1589,6 @@ function goStaticIssue(body) {
|
|
|
1578
1589
|
if (!/\bfunc\s+Test[A-Za-z0-9_]*\s*\(\s*t\s+\*testing\.T\s*\)/m.test(body)) {
|
|
1579
1590
|
return "Go test is missing a func Test...(t *testing.T) entrypoint.";
|
|
1580
1591
|
}
|
|
1581
|
-
const external = imports.filter((spec) => spec.split("/")[0].includes("."));
|
|
1582
|
-
if (external.length) {
|
|
1583
|
-
return `Go test imports module-path package(s) ${external.join(", ")}; OrangePro cannot verify module imports resolve from the generated file. Prefer same-package or stdlib-only code.`;
|
|
1584
|
-
}
|
|
1585
1592
|
if (!commandAvailable("gofmt"))
|
|
1586
1593
|
return "gofmt not found; cannot verify Go syntax.";
|
|
1587
1594
|
const { dir, file } = writeTempStaticFile("go", body);
|
|
@@ -1611,26 +1618,58 @@ function findGoModuleRoot(startDir, workspaceRoot) {
|
|
|
1611
1618
|
}
|
|
1612
1619
|
return null;
|
|
1613
1620
|
}
|
|
1614
|
-
function
|
|
1621
|
+
function unusedImportLineNumbers(output, tempRel) {
|
|
1622
|
+
const escaped = tempRel.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1623
|
+
const diagnostic = new RegExp(`(?:^|\\n)(?:\\./)?${escaped}:(\\d+):\\d+:\\s+[^\\n]*\\bimported\\b[^\\n]*\\bnot used\\b`, "g");
|
|
1624
|
+
return new Set([...output.matchAll(diagnostic)].map((match) => Number(match[1])).filter(Number.isFinite));
|
|
1625
|
+
}
|
|
1626
|
+
function removeLines(body, lineNumbers) {
|
|
1627
|
+
if (!lineNumbers.size)
|
|
1628
|
+
return body;
|
|
1629
|
+
return body
|
|
1630
|
+
.split(/\r?\n/)
|
|
1631
|
+
.filter((_line, index) => !lineNumbers.has(index + 1))
|
|
1632
|
+
.join("\n");
|
|
1633
|
+
}
|
|
1634
|
+
function goCompile(body, workspaceRoot, relatedFiles) {
|
|
1635
|
+
const externalImports = goImportSpecs(body).filter((spec) => spec.split("/")[0].includes("."));
|
|
1636
|
+
const unverifiedImportIssue = externalImports.length
|
|
1637
|
+
? `Go test imports module-path package(s) ${externalImports.join(", ")}, but OrangePro could not run the target-package compile check to verify they resolve.`
|
|
1638
|
+
: null;
|
|
1615
1639
|
const sourceFile = relatedFiles.find((rel) => /\.go$/i.test(rel) && !TEST_REF_RE.test(rel));
|
|
1616
1640
|
if (!sourceFile)
|
|
1617
|
-
return
|
|
1641
|
+
return { body, issue: unverifiedImportIssue };
|
|
1618
1642
|
const packageDir = path.resolve(workspaceRoot, path.dirname(sourceFile));
|
|
1619
1643
|
if (!existsSync(packageDir) || !findGoModuleRoot(packageDir, workspaceRoot))
|
|
1620
|
-
return
|
|
1644
|
+
return { body, issue: unverifiedImportIssue };
|
|
1621
1645
|
if (!commandAvailable("go"))
|
|
1622
|
-
return "go not found; cannot verify generated Go test compiles.";
|
|
1646
|
+
return { body, issue: "go not found; cannot verify generated Go test compiles." };
|
|
1623
1647
|
const tempRel = `orangepro_compile_${process.pid}_${shortHash(body)}_test.go`;
|
|
1624
1648
|
const tempFile = path.join(packageDir, tempRel);
|
|
1649
|
+
let checkedBody = body;
|
|
1625
1650
|
try {
|
|
1626
|
-
writeFileSync(tempFile,
|
|
1651
|
+
writeFileSync(tempFile, checkedBody);
|
|
1627
1652
|
execFileSync("go", ["test", "-run", "^$", "."], { cwd: packageDir, stdio: "pipe", timeout: GO_COMPILE_CHECK_TIMEOUT_MS });
|
|
1628
|
-
return null;
|
|
1653
|
+
return { body: checkedBody, issue: null };
|
|
1629
1654
|
}
|
|
1630
1655
|
catch (e) {
|
|
1631
1656
|
const err = e;
|
|
1632
1657
|
const output = `${err.stdout?.toString() ?? ""}${err.stderr?.toString() ?? ""}`;
|
|
1633
|
-
|
|
1658
|
+
const withoutUnusedImports = removeLines(checkedBody, unusedImportLineNumbers(output, tempRel));
|
|
1659
|
+
if (withoutUnusedImports !== checkedBody) {
|
|
1660
|
+
checkedBody = withoutUnusedImports;
|
|
1661
|
+
try {
|
|
1662
|
+
writeFileSync(tempFile, checkedBody);
|
|
1663
|
+
execFileSync("go", ["test", "-run", "^$", "."], { cwd: packageDir, stdio: "pipe", timeout: GO_COMPILE_CHECK_TIMEOUT_MS });
|
|
1664
|
+
return { body: checkedBody, issue: null };
|
|
1665
|
+
}
|
|
1666
|
+
catch (retryError) {
|
|
1667
|
+
const retry = retryError;
|
|
1668
|
+
const retryOutput = `${retry.stdout?.toString() ?? ""}${retry.stderr?.toString() ?? ""}`;
|
|
1669
|
+
return { body: checkedBody, issue: `Go compile check failed: ${shortStaticDiag(retryOutput || retry.message || "unknown error")}` };
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
return { body: checkedBody, issue: `Go compile check failed: ${shortStaticDiag(output || err.message || "unknown error")}` };
|
|
1634
1673
|
}
|
|
1635
1674
|
finally {
|
|
1636
1675
|
rmSync(tempFile, { force: true });
|
|
@@ -1659,6 +1698,37 @@ function goImportSpecs(body) {
|
|
|
1659
1698
|
}
|
|
1660
1699
|
return specs;
|
|
1661
1700
|
}
|
|
1701
|
+
function goGenerationImportContext(workspaceRoot, files, fileReader) {
|
|
1702
|
+
const goFiles = files.filter((file) => /\.go$/i.test(file));
|
|
1703
|
+
const moduleRoots = dedupe(goFiles
|
|
1704
|
+
.map((file) => findGoModuleRoot(path.resolve(workspaceRoot, path.dirname(file)), workspaceRoot))
|
|
1705
|
+
.filter((root) => Boolean(root)));
|
|
1706
|
+
const modulePathByRoot = new Map(moduleRoots.map((root) => {
|
|
1707
|
+
try {
|
|
1708
|
+
return [root, readFileSync(path.join(root, "go.mod"), "utf8").match(/^\s*module\s+(\S+)/m)?.[1] ?? ""];
|
|
1709
|
+
}
|
|
1710
|
+
catch {
|
|
1711
|
+
return [root, ""];
|
|
1712
|
+
}
|
|
1713
|
+
}));
|
|
1714
|
+
const go_module_paths = dedupe([...modulePathByRoot.values()].filter(Boolean));
|
|
1715
|
+
const go_target_package_paths = dedupe(goFiles
|
|
1716
|
+
.map((file) => {
|
|
1717
|
+
const sourceDir = path.resolve(workspaceRoot, path.dirname(file));
|
|
1718
|
+
const moduleRoot = findGoModuleRoot(sourceDir, workspaceRoot);
|
|
1719
|
+
const modulePath = moduleRoot ? modulePathByRoot.get(moduleRoot) : "";
|
|
1720
|
+
if (!moduleRoot || !modulePath)
|
|
1721
|
+
return "";
|
|
1722
|
+
const packageDir = path.relative(moduleRoot, sourceDir).split(path.sep).join("/");
|
|
1723
|
+
return packageDir && packageDir !== "." ? `${modulePath}/${packageDir}` : modulePath;
|
|
1724
|
+
})
|
|
1725
|
+
.filter(Boolean));
|
|
1726
|
+
const targetPaths = new Set(go_target_package_paths);
|
|
1727
|
+
const go_import_paths = dedupe(goFiles
|
|
1728
|
+
.flatMap((file) => goImportSpecs(fileReader(file) ?? ""))
|
|
1729
|
+
.filter((spec) => spec.split("/")[0].includes(".") && !targetPaths.has(spec))).slice(0, 40);
|
|
1730
|
+
return { go_import_paths, go_module_paths, go_target_package_paths };
|
|
1731
|
+
}
|
|
1662
1732
|
function hasBalancedBraces(body) {
|
|
1663
1733
|
let depth = 0;
|
|
1664
1734
|
let stringQuote = null;
|
|
@@ -2176,8 +2246,13 @@ export async function generateTests(graph, opts, provider, fileReader, clock = s
|
|
|
2176
2246
|
body = applyPythonSrcLayoutImports(body, relatedFiles);
|
|
2177
2247
|
}
|
|
2178
2248
|
body = ensureFrameworkScaffold(body, framework);
|
|
2249
|
+
let compileIssue = null;
|
|
2250
|
+
if (framework.toLowerCase().includes("go")) {
|
|
2251
|
+
const checked = goCompile(body, graph.workspace.root, relatedFiles);
|
|
2252
|
+
body = checked.body;
|
|
2253
|
+
compileIssue = checked.issue;
|
|
2254
|
+
}
|
|
2179
2255
|
const staticIssue = staticFormatIssue(body, framework);
|
|
2180
|
-
const compileIssue = framework.toLowerCase().includes("go") ? goCompileIssue(body, graph.workspace.root, relatedFiles) : null;
|
|
2181
2256
|
const runnable = hasAssertion(body, framework) && !staticIssue && !compileIssue;
|
|
2182
2257
|
generated.push({
|
|
2183
2258
|
id: `${run_id}-t${generated.length + 1}`,
|
|
@@ -2309,7 +2384,8 @@ export async function generateTests(graph, opts, provider, fileReader, clock = s
|
|
|
2309
2384
|
reportProgress(`Generating "${gc.ctx.behavior_title}" [v5 batch: ${selected.length}]…`);
|
|
2310
2385
|
completions.push(await provider.complete({
|
|
2311
2386
|
system: buildBatchGenerationSystemPromptV5(),
|
|
2312
|
-
user: buildBatchGenerationUserPromptV5({ ...gc.ctx, scenarios: selected })
|
|
2387
|
+
user: buildBatchGenerationUserPromptV5({ ...gc.ctx, scenarios: selected }),
|
|
2388
|
+
maxTokens: V5_GENERATION_MAX_TOKENS
|
|
2313
2389
|
}));
|
|
2314
2390
|
}
|
|
2315
2391
|
catch (e) {
|
|
@@ -2319,7 +2395,8 @@ export async function generateTests(graph, opts, provider, fileReader, clock = s
|
|
|
2319
2395
|
try {
|
|
2320
2396
|
completions.push(await provider.complete({
|
|
2321
2397
|
system: buildBatchGenerationSystemPromptV5(),
|
|
2322
|
-
user: buildBatchGenerationUserPromptV5({ ...gc.ctx, scenarios: [scenario] })
|
|
2398
|
+
user: buildBatchGenerationUserPromptV5({ ...gc.ctx, scenarios: [scenario] }),
|
|
2399
|
+
maxTokens: V5_GENERATION_MAX_TOKENS
|
|
2323
2400
|
}));
|
|
2324
2401
|
}
|
|
2325
2402
|
catch (singleErr) {
|
|
@@ -2433,7 +2510,12 @@ export async function generateTests(graph, opts, provider, fileReader, clock = s
|
|
|
2433
2510
|
}
|
|
2434
2511
|
}
|
|
2435
2512
|
const importErrors = isResolverFramework(framework) ? unresolvedLocalImports(body, genTestAbs, graph.workspace.root, declaredDeps) : [];
|
|
2436
|
-
|
|
2513
|
+
let compileIssue = null;
|
|
2514
|
+
if (framework.toLowerCase().includes("go")) {
|
|
2515
|
+
const checked = goCompile(body, graph.workspace.root, relatedFiles);
|
|
2516
|
+
body = checked.body;
|
|
2517
|
+
compileIssue = checked.issue;
|
|
2518
|
+
}
|
|
2437
2519
|
const runnable = isRunnable(body, framework, import_provenance, importErrors) && !compileIssue;
|
|
2438
2520
|
if (!runnable) {
|
|
2439
2521
|
const reason = unresolved_reason ?? compileIssue ?? runnableFailureReason(body, framework, import_provenance, importErrors, declaredDeps);
|
|
@@ -2463,7 +2545,7 @@ export async function generateTests(graph, opts, provider, fileReader, clock = s
|
|
|
2463
2545
|
"",
|
|
2464
2546
|
// Concise blocker: first clause only — the full remedy is one line.
|
|
2465
2547
|
`Blocked by: ${reason.split(" — ")[0]}`,
|
|
2466
|
-
|
|
2548
|
+
`Fix: ${generatedDraftRemediation(reason)}`
|
|
2467
2549
|
].join("\n"), gc.ctx.source_excerpts, "//").body;
|
|
2468
2550
|
generated.push({
|
|
2469
2551
|
id: `${run_id}-t${generated.length + 1}`,
|
|
@@ -2651,7 +2733,12 @@ export async function generateTests(graph, opts, provider, fileReader, clock = s
|
|
|
2651
2733
|
// from where the test will live. A test whose own import won't load is never
|
|
2652
2734
|
// marked runnable.
|
|
2653
2735
|
const importErrors = isResolverFramework(framework) ? unresolvedLocalImports(body, genTestAbs, graph.workspace.root, declaredDeps) : [];
|
|
2654
|
-
|
|
2736
|
+
let compileIssue = null;
|
|
2737
|
+
if (framework.toLowerCase().includes("go")) {
|
|
2738
|
+
const checked = goCompile(body, graph.workspace.root, relatedFiles);
|
|
2739
|
+
body = checked.body;
|
|
2740
|
+
compileIssue = checked.issue;
|
|
2741
|
+
}
|
|
2655
2742
|
const runnable = isRunnable(body, framework, import_provenance, importErrors) && !compileIssue;
|
|
2656
2743
|
if (!runnable && !unresolved_reason) {
|
|
2657
2744
|
unresolved_reason = compileIssue ?? runnableFailureReason(body, framework, import_provenance, importErrors, declaredDeps);
|
|
@@ -32,8 +32,10 @@ export function buildSystemPrompt() {
|
|
|
32
32
|
"- Framework format rules:",
|
|
33
33
|
" - pytest: output a valid Python file with pytest-style `def test_...` functions and `assert` statements.",
|
|
34
34
|
" - Go: output a same-package `_test.go` body. Include `package <same package>`, `import \"testing\"`,",
|
|
35
|
-
" and `func Test...(t *testing.T)`.
|
|
36
|
-
"
|
|
35
|
+
" and `func Test...(t *testing.T)`. Do not import packages you do not use. Prefer stdlib and exact",
|
|
36
|
+
" OBSERVED GO IMPORT PATHS; never derive or invent a package subpath from a module name.",
|
|
37
|
+
" Do not qualify lowercase (unexported) identifiers from imported packages. Bare same-package",
|
|
38
|
+
" identifiers may be unexported because the generated test is rewritten into the target package.",
|
|
37
39
|
" - Java/JUnit: output a complete `.java` file, not a method fragment. Include a `class <Name>Test { ... }`,",
|
|
38
40
|
" the requested JUnit version's `@Test` import, and a JUnit assertion.",
|
|
39
41
|
" - TS/JS: output valid framework code for the named framework and use complete imports.",
|
|
@@ -78,6 +80,21 @@ export function buildGroundedUserPrompt(ctx, bucket) {
|
|
|
78
80
|
for (const imp of ctx.subject_imports)
|
|
79
81
|
lines.push(imp);
|
|
80
82
|
}
|
|
83
|
+
if (ctx.go_module_paths?.length) {
|
|
84
|
+
lines.push("GO MODULE IDENTITIES (module roots only — never guess package subpaths from these):");
|
|
85
|
+
for (const modulePath of ctx.go_module_paths)
|
|
86
|
+
lines.push(`- ${modulePath}`);
|
|
87
|
+
}
|
|
88
|
+
if (ctx.go_import_paths?.length) {
|
|
89
|
+
lines.push("OBSERVED GO IMPORT PATHS (exact repo-evidenced package paths; prefer these for non-stdlib imports):");
|
|
90
|
+
for (const importPath of ctx.go_import_paths)
|
|
91
|
+
lines.push(`- ${importPath}`);
|
|
92
|
+
}
|
|
93
|
+
if (ctx.go_target_package_paths?.length) {
|
|
94
|
+
lines.push("TARGET GO PACKAGE PATHS (do not import these; the test is rewritten into the same package):");
|
|
95
|
+
for (const targetPath of ctx.go_target_package_paths)
|
|
96
|
+
lines.push(`- ${targetPath}`);
|
|
97
|
+
}
|
|
81
98
|
if (ctx.source_excerpts.length) {
|
|
82
99
|
lines.push("SOURCE EXCERPTS:");
|
|
83
100
|
lines.push("Use these for understanding only; do not copy their lines verbatim into the test body.");
|
|
@@ -97,8 +114,10 @@ export function buildGroundedUserPrompt(ctx, bucket) {
|
|
|
97
114
|
}
|
|
98
115
|
else if (fw.includes("go")) {
|
|
99
116
|
lines.push("- Emit same-package Go test code only: `package ...`, `import \"testing\"`, and `func Test...(t *testing.T)`.");
|
|
100
|
-
lines.push("- Do not import
|
|
101
|
-
lines.push("-
|
|
117
|
+
lines.push("- Do not import packages you do not use.");
|
|
118
|
+
lines.push("- Prefer stdlib and exact OBSERVED GO IMPORT PATHS. Never invent a package path or derive a subpath from a GO MODULE IDENTITY.");
|
|
119
|
+
lines.push("- Never import a TARGET GO PACKAGE PATH; call its identifiers directly because this is a same-package test.");
|
|
120
|
+
lines.push("- Do not qualify lowercase (unexported) identifiers from imported packages; bare same-package identifiers may be unexported.");
|
|
102
121
|
}
|
|
103
122
|
else if (fw.includes("junit") || fw.includes("java")) {
|
|
104
123
|
if (fw.includes("junit4")) {
|
|
@@ -33,7 +33,15 @@ export function getFrameworkRules(framework) {
|
|
|
33
33
|
return "Python: `def test_...` with `assert`. Use `# Concern:` and `# Technique:` comments.";
|
|
34
34
|
}
|
|
35
35
|
if (fw.includes("go")) {
|
|
36
|
-
return
|
|
36
|
+
return [
|
|
37
|
+
"Go: same-package `_test.go` file. `func Test...(t *testing.T)`.",
|
|
38
|
+
"MUST start with `package <name>` matching the package under test.",
|
|
39
|
+
"MUST import `\"testing\"`; do not import any package you do not use.",
|
|
40
|
+
"Prefer stdlib and exact OBSERVED GO IMPORT PATHS. Never invent a package path or derive a subpath from a GO MODULE IDENTITY.",
|
|
41
|
+
"Never import a TARGET GO PACKAGE PATH; call its identifiers directly because this is a same-package test.",
|
|
42
|
+
"Do not qualify lowercase (unexported) identifiers from imported packages; bare same-package identifiers may be unexported.",
|
|
43
|
+
"Prefer stdlib `testing` over testify unless testify appears in OBSERVED GO IMPORT PATHS."
|
|
44
|
+
].join(" ");
|
|
37
45
|
}
|
|
38
46
|
if (fw.includes("junit4") || fw.includes("java4")) {
|
|
39
47
|
return "JUnit 4: `import org.junit.Test;` + `import static org.junit.Assert.*;`. Complete .java file.";
|
|
@@ -179,6 +187,21 @@ export function buildBatchGenerationUserPromptV5(ctx) {
|
|
|
179
187
|
lines.push(`ACTORS: ${ctx.actors.join(", ")}`);
|
|
180
188
|
lines.push(`FRAMEWORK: ${ctx.framework} | LAYER: ${ctx.test_layer}`);
|
|
181
189
|
lines.push(`FRAMEWORK RULES: ${getFrameworkRules(ctx.framework)}`);
|
|
190
|
+
if (ctx.go_module_paths?.length) {
|
|
191
|
+
lines.push("GO MODULE IDENTITIES (module roots only — never guess package subpaths from these):");
|
|
192
|
+
for (const modulePath of ctx.go_module_paths)
|
|
193
|
+
lines.push(` ${modulePath}`);
|
|
194
|
+
}
|
|
195
|
+
if (ctx.go_import_paths?.length) {
|
|
196
|
+
lines.push("OBSERVED GO IMPORT PATHS (exact repo-evidenced package paths):");
|
|
197
|
+
for (const importPath of ctx.go_import_paths)
|
|
198
|
+
lines.push(` ${importPath}`);
|
|
199
|
+
}
|
|
200
|
+
if (ctx.go_target_package_paths?.length) {
|
|
201
|
+
lines.push("TARGET GO PACKAGE PATHS (do not import; this is a same-package test):");
|
|
202
|
+
for (const targetPath of ctx.go_target_package_paths)
|
|
203
|
+
lines.push(` ${targetPath}`);
|
|
204
|
+
}
|
|
182
205
|
lines.push("");
|
|
183
206
|
if (ctx.flow_chain?.length) {
|
|
184
207
|
lines.push("FLOW CHAIN:");
|