From 86160e682cf7d4dfa3b2ca052ac4bbe5701e245e Mon Sep 17 00:00:00 2001 From: galuis116 Date: Sun, 26 Jul 2026 06:50:17 -0700 Subject: [PATCH] fix(engine): warn on out-of-range self-plagiarism similarityThreshold normalizeSelfPlagiarismPolicy only warned for non-number thresholds and let resolveSelfPlagiarismConfig silently clamp values like 5 or -1. Reject non-finite and out-of-[0,1] numbers with a warning and the default. Closes #8862 --- .../loopover-engine/src/miner-goal-spec.ts | 24 ++++++++------- .../test/miner-goal-spec-parser.test.ts | 30 +++++++++++++++++++ 2 files changed, 44 insertions(+), 10 deletions(-) diff --git a/packages/loopover-engine/src/miner-goal-spec.ts b/packages/loopover-engine/src/miner-goal-spec.ts index c6ee75a57b..a494e6659f 100644 --- a/packages/loopover-engine/src/miner-goal-spec.ts +++ b/packages/loopover-engine/src/miner-goal-spec.ts @@ -255,18 +255,22 @@ function normalizeSelfPlagiarismPolicy( warnings.push(`MinerGoalSpec field "${field}" must be a mapping; falling back to defaults.`); return fallback; } - const resolved = resolveSelfPlagiarismConfig(value); const record = value as Record; - if ( - record.similarityThreshold !== undefined && - typeof record.similarityThreshold !== "number" - ) { - warnings.push( - `MinerGoalSpec field "${field}.similarityThreshold" must be a number; falling back to ${fallback.similarityThreshold}.`, - ); - return fallback; + if (record.similarityThreshold !== undefined) { + if (typeof record.similarityThreshold !== "number" || !Number.isFinite(record.similarityThreshold)) { + warnings.push( + `MinerGoalSpec field "${field}.similarityThreshold" must be a number; falling back to ${fallback.similarityThreshold}.`, + ); + return fallback; + } + if (record.similarityThreshold < 0 || record.similarityThreshold > 1) { + warnings.push( + `MinerGoalSpec field "${field}.similarityThreshold" must be between 0 and 1; falling back to ${fallback.similarityThreshold}.`, + ); + return fallback; + } } - return resolved; + return resolveSelfPlagiarismConfig(value); } function normalizeKillSwitchPolicy( diff --git a/packages/loopover-engine/test/miner-goal-spec-parser.test.ts b/packages/loopover-engine/test/miner-goal-spec-parser.test.ts index 2727745eea..7077ae5d49 100644 --- a/packages/loopover-engine/test/miner-goal-spec-parser.test.ts +++ b/packages/loopover-engine/test/miner-goal-spec-parser.test.ts @@ -215,6 +215,36 @@ test("parseMinerGoalSpec: malformed fields fall back independently with targeted assert.match(warningText, /truncated an over-long entry/i); }); +test("parseMinerGoalSpec: selfPlagiarism.similarityThreshold out of [0,1] warns and falls back (#8862)", () => { + const tooHigh = parseMinerGoalSpec({ + wantedPaths: ["src/**"], + selfPlagiarism: { similarityThreshold: 5 }, + }); + assert.deepEqual(tooHigh.spec.selfPlagiarism, { similarityThreshold: 0.85 }); + assert.match(tooHigh.warnings.join(" "), /selfPlagiarism\.similarityThreshold.*between 0 and 1/i); + + const tooLow = parseMinerGoalSpec({ + wantedPaths: ["src/**"], + selfPlagiarism: { similarityThreshold: -1 }, + }); + assert.deepEqual(tooLow.spec.selfPlagiarism, { similarityThreshold: 0.85 }); + assert.match(tooLow.warnings.join(" "), /selfPlagiarism\.similarityThreshold.*between 0 and 1/i); + + const nonFinite = parseMinerGoalSpec({ + wantedPaths: ["src/**"], + selfPlagiarism: { similarityThreshold: Number.NaN }, + }); + assert.deepEqual(nonFinite.spec.selfPlagiarism, { similarityThreshold: 0.85 }); + assert.match(nonFinite.warnings.join(" "), /selfPlagiarism\.similarityThreshold.*must be a number/i); + + const valid = parseMinerGoalSpec({ + wantedPaths: ["src/**"], + selfPlagiarism: { similarityThreshold: 0.9 }, + }); + assert.deepEqual(valid.spec.selfPlagiarism, { similarityThreshold: 0.9 }); + assert.deepEqual(valid.warnings, []); +}); + test("parseMinerGoalSpec: unknown-only or default-only content stays absent with a fallback warning", () => { const unknownOnly = parseMinerGoalSpec({ mystery: true }); assert.equal(unknownOnly.present, false);