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
65 changes: 65 additions & 0 deletions scripts/__tests__/sdk-release-metadata.spec.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import assert from 'node:assert/strict';
import test from 'node:test';

import { factsFromDiff } from '../sdk-release-metadata.mjs';

// factsFromDiff only reaches indexes.symbolScopes (scope resolution) for the
// kinds under test; an empty index leaves scope unresolved, which is fine — we
// assert on severity/prefix, not scope.
const EMPTY_INDEXES = { symbolScopes: new Map(), enumWireValues: new Map() };

const fieldAddedReport = (classification) => ({
changes: [
{
kind: 'model-modified',
name: 'OrganizationMembership',
classification: 'additive',
fieldChanges: [{ kind: 'field-added', fieldName: 'roles', classification }],
},
],
});

const addedFact = (report) =>
factsFromDiff(report, EMPTY_INDEXES).find((f) => f.kind === 'field-added');

// Regression: a new field is a feature, never a fix. The differ sometimes flags
// an added field `breaking` (reads it as a request-shape tightening); the
// backend-only severity cap used to collapse that to `fix`, so the field landed
// under **Fixes** in the changelog even though the release bumped minor.
test('field-added flagged breaking is capped to additive (feat), not fix', () => {
const fact = addedFact(fieldAddedReport('breaking'));
assert.equal(fact.severity, 'additive');
assert.equal(fact.prefix, 'feat');
});

test('field-added with an additive classification stays a feature', () => {
const fact = addedFact(fieldAddedReport('additive'));
assert.equal(fact.severity, 'additive');
assert.equal(fact.prefix, 'feat');
});

// A missing/odd classification would previously fall through severityToPrefix to
// `fix`; an addition is additive regardless.
test('field-added with no classification still resolves to a feature', () => {
const fact = addedFact(fieldAddedReport(undefined));
assert.equal(fact.severity, 'additive');
assert.equal(fact.prefix, 'feat');
});

// The cap must NOT leak to altering/removing changes — those stay a fix when the
// differ flags them breaking (field changes are backend-only, never major).
test('field-removed flagged breaking is still capped to fix', () => {
const report = {
changes: [
{
kind: 'model-modified',
name: 'OrganizationMembership',
classification: 'breaking',
fieldChanges: [{ kind: 'field-removed', fieldName: 'legacy', classification: 'breaking' }],
},
],
};
const fact = factsFromDiff(report, EMPTY_INDEXES).find((f) => f.kind === 'field-removed');
assert.equal(fact.severity, 'fix');
assert.equal(fact.prefix, 'fix');
});
10 changes: 10 additions & 0 deletions scripts/sdk-release-metadata.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -623,7 +623,17 @@ const BACKEND_ONLY_DIFF_KINDS = new Set([
'response-changed',
'request-body-changed',
]);
// Pure additions can't break a caller and aren't a fix — a new field/value is a
// feature. Floor these at `additive` (→ feat) regardless of what the differ
// classified them as: it sometimes flags an added field `breaking` (reads it as
// a request-shape tightening), which the backend-only cap below would otherwise
// collapse to `fix`, and a missing/odd classification would fall through to
// `fix` via severityToPrefix. Either way a new field would land under **Fixes**
// while the release bumped minor — see model-added/enum-added, hardcoded the
// same way.
const ADDITIVE_DIFF_KINDS = new Set(['field-added', 'value-added']);
function capSeverity(kind, severity) {
if (ADDITIVE_DIFF_KINDS.has(kind)) return 'additive';
return BACKEND_ONLY_DIFF_KINDS.has(kind) && severity === 'breaking' ? 'fix' : severity;
}

Expand Down