@orangepro/orangepro-mcp 0.2.33 → 0.2.34

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.
@@ -2169,7 +2169,10 @@ export async function generateTests(graph, opts, provider, fileReader, clock = s
2169
2169
  const systemPrompt = opts.systemPrompt ?? buildSystemPrompt();
2170
2170
  const promptVersion = inputMode === "graph_grounded" && opts.prompt_version === "v5" ? PROMPT_VERSION_V5 : PROMPT_VERSION;
2171
2171
  const created_at = clock();
2172
- const runSeed = shortHash(created_at + provider.modelName + runTargets.map((t) => t.external_id).join(","));
2172
+ const runSeed = shortHash(created_at +
2173
+ provider.modelName +
2174
+ runTargets.map((t) => t.external_id).join(",") +
2175
+ JSON.stringify(opts.existing_generated_test_titles ?? []));
2173
2176
  const run_id = `local-gen-${runSeed}`;
2174
2177
  const generated = [];
2175
2178
  const missing = [];
@@ -2287,6 +2290,10 @@ export async function generateTests(graph, opts, provider, fileReader, clock = s
2287
2290
  if (generated.length >= limit)
2288
2291
  break;
2289
2292
  const gc = gatherContext(graph, behavior, framework, fileReader);
2293
+ const existingGeneratedTitles = dedupe(opts.existing_generated_test_titles ?? []);
2294
+ if (existingGeneratedTitles.length) {
2295
+ gc.ctx.existing_tests = dedupe([...gc.ctx.existing_tests, ...existingGeneratedTitles]);
2296
+ }
2290
2297
  reportProgress(`Planning "${gc.ctx.behavior_title}" [v5]…`);
2291
2298
  let scenarios = [];
2292
2299
  // Transport first: a network/timeout failure is NOT malformed JSON, so it does
@@ -2377,6 +2384,19 @@ export async function generateTests(graph, opts, provider, fileReader, clock = s
2377
2384
  continue;
2378
2385
  }
2379
2386
  }
2387
+ if (existingGeneratedTitles.length) {
2388
+ const normalizedExistingTitles = new Set(existingGeneratedTitles.map((title) => title.trim().toLowerCase()));
2389
+ const beforeDuplicateFilter = scenarios.length;
2390
+ scenarios = scenarios.filter((scenario) => {
2391
+ const scenarioTitle = scenario.title.trim().toLowerCase();
2392
+ const fullTitle = `${gc.ctx.behavior_title} — ${scenario.title}`.trim().toLowerCase();
2393
+ return !normalizedExistingTitles.has(scenarioTitle) && !normalizedExistingTitles.has(fullTitle);
2394
+ });
2395
+ const duplicateCount = beforeDuplicateFilter - scenarios.length;
2396
+ if (duplicateCount > 0) {
2397
+ warnings.push(`Dropped ${duplicateCount} already-generated v5 scenario(s) for "${gc.ctx.behavior_title}" during top-up.`);
2398
+ }
2399
+ }
2380
2400
  if (scenarios.length === 0) {
2381
2401
  missing.push({
2382
2402
  external_id: behavior.external_id,
@@ -1669,37 +1669,67 @@ export async function opStart(root, opts = {}, deps = defaultDeps()) {
1669
1669
  else if (!opts.noAuto && generationProviderConfigured && opts.ai !== false && generationLimit > 0) {
1670
1670
  try {
1671
1671
  const graphForGeneration = loadGraph(workspacePaths(root).graphPath);
1672
- const generatedTargets = new Set((graphForGeneration.generated_tests ?? []).map((t) => t.target_symbol_external_id).filter((id) => Boolean(id)));
1673
- const targetIds = rankPriorityGaps(graphForGeneration, {
1672
+ const generatedTestsByTarget = new Map();
1673
+ for (const test of graphForGeneration.generated_tests ?? []) {
1674
+ const targetId = test.target_symbol_external_id;
1675
+ if (!targetId || test.stale === true)
1676
+ continue;
1677
+ const tests = generatedTestsByTarget.get(targetId) ?? [];
1678
+ tests.push(test);
1679
+ generatedTestsByTarget.set(targetId, tests);
1680
+ }
1681
+ const targetPlans = rankPriorityGaps(graphForGeneration, {
1674
1682
  repoRoot: root,
1675
1683
  limit: generationLimit,
1676
1684
  provenIds: provenSymbolIds(graphForGeneration, loadLedger(root))
1677
1685
  })
1678
- .map((gap) => gap.id)
1679
- .filter((id) => !generatedTargets.has(id));
1680
- if (!targetIds.length) {
1686
+ .map((gap) => {
1687
+ const existingTests = generatedTestsByTarget.get(gap.id) ?? [];
1688
+ return {
1689
+ id: gap.id,
1690
+ existingTests,
1691
+ deficit: Math.max(0, 2 - existingTests.length)
1692
+ };
1693
+ })
1694
+ .filter((target) => target.deficit > 0);
1695
+ if (!targetPlans.length) {
1681
1696
  generationResult = {
1682
1697
  ...generationResult,
1683
1698
  status: "no_targets",
1684
- reason: "No eligible ungenerated risk targets were found."
1699
+ reason: "Every eligible priority flow already has two generated tests."
1685
1700
  };
1686
1701
  }
1687
1702
  else {
1688
- reportProgress(`generate: drafting tests for top ${targetIds.length} risk target(s)`, { current: 6, total: 8 });
1703
+ reportProgress(`generate: filling test gaps for ${targetPlans.length} priority flow(s)`, { current: 6, total: 8 });
1689
1704
  const generatedDrafts = [];
1690
- for (const targetId of targetIds) {
1691
- const generated = await opGenerate(root, {
1692
- ...providerOpts,
1693
- // One ranked flow per generation run. Give each flow room for its
1694
- // two highest-risk missing tests instead of sharing a batch budget.
1695
- target_ids: [targetId],
1696
- limit: 2,
1697
- // The offline deterministic stand-in emits the established v2 scaffold; v5 is
1698
- // a two-phase model planning protocol and must not be selected implicitly for it.
1699
- prompt_version: opts.promptVersion ?? (deterministicGeneration ? "v2" : "v5")
1700
- }, providerDeps);
1701
- generatedDrafts.push(...generated.generated_tests);
1702
- warnings.push(...generated.warnings.map((w) => `generate: ${w}`));
1705
+ const incompleteTargets = [];
1706
+ for (const target of targetPlans) {
1707
+ let remaining = target.deficit;
1708
+ const existingTitles = target.existingTests.map((test) => test.title);
1709
+ // A provider can return one accepted scenario when two were requested.
1710
+ // Make at most one bounded follow-up for the remaining slot, carrying
1711
+ // prior titles so the planner cannot silently duplicate the first test.
1712
+ for (let attempt = 0; attempt < 2 && remaining > 0; attempt++) {
1713
+ const generated = await opGenerate(root, {
1714
+ ...providerOpts,
1715
+ target_ids: [target.id],
1716
+ limit: remaining,
1717
+ pin_unchanged: false,
1718
+ existing_generated_test_titles: existingTitles,
1719
+ // The offline deterministic stand-in emits the established v2 scaffold; v5 is
1720
+ // a two-phase model planning protocol and must not be selected implicitly for it.
1721
+ prompt_version: opts.promptVersion ?? (deterministicGeneration ? "v2" : "v5")
1722
+ }, providerDeps);
1723
+ const freshForTarget = generated.generated_tests.filter((test) => test.target_symbol_external_id === target.id && !test.pinned);
1724
+ generatedDrafts.push(...freshForTarget);
1725
+ warnings.push(...generated.warnings.map((w) => `generate: ${w}`));
1726
+ if (freshForTarget.length === 0)
1727
+ break;
1728
+ existingTitles.push(...freshForTarget.map((test) => test.title));
1729
+ remaining = Math.max(0, remaining - freshForTarget.length);
1730
+ }
1731
+ if (remaining > 0)
1732
+ incompleteTargets.push(target.id);
1703
1733
  }
1704
1734
  const blockers = {};
1705
1735
  for (const draft of generatedDrafts.filter((test) => test.runnable === false)) {
@@ -1707,22 +1737,27 @@ export async function opStart(root, opts = {}, deps = defaultDeps()) {
1707
1737
  blockers[blocker] = (blockers[blocker] ?? 0) + 1;
1708
1738
  }
1709
1739
  const runnable = generatedDrafts.filter((test) => test.runnable !== false).length;
1740
+ const shortfallReason = incompleteTargets.length
1741
+ ? `${incompleteTargets.length} priority flow(s) remain below two generated tests because the provider returned no additional accepted distinct scenario.`
1742
+ : undefined;
1710
1743
  generationResult = {
1711
1744
  status: generatedDrafts.length === 0
1712
1745
  ? "no_results"
1713
- : generatedDrafts.some((test) => test.runnable === false)
1746
+ : generatedDrafts.some((test) => test.runnable === false) || incompleteTargets.length > 0
1714
1747
  ? "completed_with_blockers"
1715
1748
  : "completed",
1716
- requested: targetIds.length,
1749
+ requested: targetPlans.length,
1717
1750
  generated: generatedDrafts.length,
1718
1751
  runnable,
1719
1752
  drafts: generatedDrafts.length - runnable,
1720
1753
  blockers,
1721
1754
  ...(generatedDrafts.length === 0
1722
- ? { reason: "The provider returned no generated-test drafts for the selected risk targets." }
1723
- : {})
1755
+ ? { reason: shortfallReason ?? "The provider returned no generated-test drafts for the selected risk targets." }
1756
+ : shortfallReason
1757
+ ? { reason: shortfallReason }
1758
+ : {})
1724
1759
  };
1725
- if (generatedDrafts.length === 0)
1760
+ if (generationResult.reason)
1726
1761
  warnings.push(`generate: ${generationResult.reason}`);
1727
1762
  }
1728
1763
  }
@@ -1948,6 +1983,7 @@ export async function opGenerate(root, opts = {}, deps = defaultDeps()) {
1948
1983
  limit: opts.limit,
1949
1984
  input_mode: opts.input_mode,
1950
1985
  prompt_version: opts.prompt_version,
1986
+ existing_generated_test_titles: opts.existing_generated_test_titles,
1951
1987
  // Persisting lane: a runnable draft for an unchanged target is reused as-is
1952
1988
  // rather than re-bought from the model on every run.
1953
1989
  pin_unchanged: opts.pin_unchanged ?? true
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orangepro/orangepro-mcp",
3
- "version": "0.2.33",
3
+ "version": "0.2.34",
4
4
  "private": false,
5
5
  "description": "OrangePro (`opro`) — a local-first, BYOK CLI + MCP server that builds an evidence graph from a local checkout, ingests runtime coverage, and generates grounded tests. Metadata-only exports; no source upload; generated tests stay local.",
6
6
  "license": "MIT",