Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions packages/loopover-engine/src/miner-goal-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -259,10 +259,16 @@ function normalizeSelfPlagiarismPolicy(
const record = value as Record<string, unknown>;
if (
record.similarityThreshold !== undefined &&
typeof record.similarityThreshold !== "number"
(typeof record.similarityThreshold !== "number" ||
!Number.isFinite(record.similarityThreshold) ||
record.similarityThreshold < 0 ||
record.similarityThreshold > 1)
) {
// An out-of-range or non-finite value previously fell through to self-plagiarism.ts's Math.min(1, Math.max(0, ...))
// and was silently clamped with no warning, marked "present" -- unlike every sibling numeric normalizer here.
// Warn and fall back to the default instead (#8862).
warnings.push(
`MinerGoalSpec field "${field}.similarityThreshold" must be a number; falling back to ${fallback.similarityThreshold}.`,
`MinerGoalSpec field "${field}.similarityThreshold" must be a number in [0, 1]; falling back to ${fallback.similarityThreshold}.`,
);
return fallback;
}
Expand Down
19 changes: 19 additions & 0 deletions test/unit/miner-goal-spec-parser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,25 @@ describe("MinerGoalSpec parser (#2301)", () => {
expect(arrayValue.warnings.join(" ")).toMatch(/selfPlagiarism.*must be a mapping/i);
});

it("warns and falls back for an out-of-range or non-finite similarityThreshold instead of silently clamping (#8862)", () => {
// A numeric value outside [0, 1] used to fall through to self-plagiarism.ts's Math.min(1, Math.max(0, value)),
// silently clamping (e.g. 5 -> 1) with zero warnings and the field marked "present" -- unlike every sibling
// numeric normalizer. Each of these must now warn AND fall back to the documented default of 0.85.
for (const badThreshold of [5, -1, Number.POSITIVE_INFINITY, Number.NaN]) {
const parsed = parseMinerGoalSpec({
wantedPaths: ["src/**"],
selfPlagiarism: { similarityThreshold: badThreshold },
});
expect(parsed.spec.selfPlagiarism).toEqual({ similarityThreshold: 0.85 });
expect(parsed.warnings.join(" ")).toMatch(/selfPlagiarism\.similarityThreshold.*must be a number in \[0, 1\]/i);
}

// An in-range value is still accepted verbatim with no warning.
const valid = parseMinerGoalSpec({ selfPlagiarism: { similarityThreshold: 0.42 } });
expect(valid.spec.selfPlagiarism).toEqual({ similarityThreshold: 0.42 });
expect(valid.warnings.join(" ")).not.toMatch(/similarityThreshold/i);
});

it("a killSwitch policy alone (all other fields default) marks the spec present", () => {
const parsed = parseMinerGoalSpec({ killSwitch: { paused: true } });
expect(parsed.present).toBe(true);
Expand Down