Skip to content

Commit e637862

Browse files
test(catalog-v2): cover atmn PUT of version-anchored license parents
Lock the four-direct per-version path so adding a child feature keeps anchors, omits sibling_versions, restates overlays, and drafts a collapsed migration. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent eab77fb commit e637862

5 files changed

Lines changed: 895 additions & 7 deletions

File tree

server/tests/integration/catalog-v2/plans/CASES.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -979,6 +979,28 @@ follow each parent version from the child sibling it actually points at.
979979
| Parent `all_versions` follows every linked version from its anchored child sibling; anchors stay put ||
980980
| Diverged child items: v1 is 100 Messages, v2 is 50 + Words; add Dashboard on v2 `all_versions` → parent v1 is 100 + Dashboard (no Words), parent v2 is 50 + Words + Dashboard ||
981981

982+
#### Atmn PUT — four pinned directs — `mix/atmn-put-direct-versions.test.ts`
983+
984+
Same diverged fixture. Each version is a direct `{ version_slug }` row;
985+
parents restate `licenses[]` (optional child `version_slug`). No
986+
`all_versions` / `propagate`.
987+
988+
| Case | Status |
989+
|---|---|
990+
| Preview of child v1+v2 + parent v1+v2 → each row omits `sibling_versions` ||
991+
| PUT add Dashboard on both children, restate parent licenses → all four get Dashboard; Words stay v2-only; anchors stay ||
992+
| Restate `licenses[]` without `version_slug` → stay on the current child row ||
993+
| Identical re-PUT → preview `none`; `plan_license` row ids unchanged ||
994+
| Customers on all four + `draft: true` → one draft, collapsed `{ plan_id }` filters, child add Dashboard + parent `upsert_licenses` add Dashboard ||
995+
996+
#### Atmn PUT lanes — `mix/atmn-put-direct-version-lanes.test.ts`
997+
998+
| Case | Status |
999+
|---|---|
1000+
| Dashboard only on child v2 → parent v1 unchanged; parent v2 gets it ||
1001+
| Preview: both parents `license_action: explicit`; `license_changes` add Dashboard; no unlink ||
1002+
| Restated overlays (v1=80, v2=40) stay; Dashboard still flows ||
1003+
9821004
### lifecycle/ — archive or remove an anchored child version — `lifecycle/anchored-version-remove.test.ts`
9831005

9841006
Remove/archive of a child version is blocked while any catalog link still
Lines changed: 310 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,310 @@
1+
/**
2+
* catalogV2 — atmn PUT lanes that the four-row Dashboard compose does not cover.
3+
*
4+
* Contract:
5+
* G Dashboard only on child v2 → parent v1 unchanged; parent v2 gets it
6+
* H preview: both parents license_action explicit; license_changes add
7+
* Dashboard; no unlink
8+
* I restated overlays (v1=80, v2=40) stay; Dashboard still flows
9+
*/
10+
11+
import { expect, test } from "bun:test";
12+
import { TestFeature } from "@tests/setup/v2Features.js";
13+
import { initScenario } from "@tests/utils/testInitUtils/initScenario.js";
14+
import chalk from "chalk";
15+
import { uniqueTestId } from "../../../utils/uniqueTestId.js";
16+
import {
17+
expectPlanPreviewRowCorrect,
18+
parsePlanPreview,
19+
} from "../../preview/utils/expectPlanPreview.js";
20+
import {
21+
atmnDirectPut,
22+
expectAnchoredParentLink,
23+
expectChildVersionItems,
24+
seedDivergedChildAnchors,
25+
} from "../utils/atmnPutDirectVersions.js";
26+
import {
27+
getFullPlan,
28+
messagesItem,
29+
messagesOverride,
30+
withCatalogPlans,
31+
} from "../utils/seedLicensePlans.js";
32+
33+
const seedParentOverlays = async ({
34+
autumn,
35+
childId,
36+
parentId,
37+
}: {
38+
autumn: Parameters<typeof seedDivergedChildAnchors>[0]["autumn"];
39+
childId: string;
40+
parentId: string;
41+
}) => {
42+
await autumn.catalogV2.update({
43+
plans: [
44+
{
45+
plan_id: parentId,
46+
version_slug: "v1",
47+
licenses: [
48+
{
49+
license_plan_id: childId,
50+
included: 2,
51+
version_slug: "v1",
52+
customize: messagesOverride(80),
53+
},
54+
],
55+
},
56+
{
57+
plan_id: parentId,
58+
version_slug: "v2",
59+
licenses: [
60+
{
61+
license_plan_id: childId,
62+
included: 2,
63+
version_slug: "v2",
64+
customize: messagesOverride(40),
65+
},
66+
],
67+
},
68+
],
69+
});
70+
};
71+
72+
const expectExplicitDashboardPreview = ({
73+
preview,
74+
childId,
75+
parentId,
76+
}: {
77+
preview: ReturnType<typeof parsePlanPreview>;
78+
childId: string;
79+
parentId: string;
80+
}) => {
81+
for (const currentVersion of [1, 2] as const) {
82+
expectPlanPreviewRowCorrect({
83+
preview,
84+
expected: {
85+
planId: childId,
86+
currentVersion,
87+
licenseParents: [
88+
{
89+
planId: parentId,
90+
version: currentVersion,
91+
licenseAction: "explicit",
92+
licenseChanges: [
93+
{
94+
action: "updated",
95+
license_plan_id: childId,
96+
plan_change: {},
97+
},
98+
],
99+
nestedItemChanges: [
100+
{
101+
action: "created",
102+
feature_id: TestFeature.Dashboard,
103+
},
104+
],
105+
},
106+
],
107+
},
108+
});
109+
expectPlanPreviewRowCorrect({
110+
preview,
111+
expected: {
112+
planId: parentId,
113+
currentVersion,
114+
licenseChanges: [
115+
{
116+
action: "updated",
117+
license_plan_id: childId,
118+
plan_change: {},
119+
},
120+
],
121+
},
122+
});
123+
const parentRow = preview.plans.find(
124+
(row) =>
125+
row.plan_id === parentId && row.version === currentVersion,
126+
);
127+
expect(
128+
parentRow?.plan_change?.license_changes?.some(
129+
(change) => change.action === "removed",
130+
),
131+
).toBe(false);
132+
expect(parentRow?.plan_change?.customize?.remove_licenses).toBeUndefined();
133+
}
134+
};
135+
136+
test.concurrent(
137+
`${chalk.yellowBright("catalogV2 plan-licenses: atmn PUT Dashboard on child v2 only does not leak to v1")}`,
138+
async () => {
139+
const { autumnV2_3, ctx } = await initScenario({ setup: [], actions: [] });
140+
const childId = uniqueTestId("cv2_lic_atmn_g_c");
141+
const parentId = uniqueTestId("cv2_lic_atmn_g_p");
142+
await withCatalogPlans({
143+
ctx,
144+
planIds: [childId, parentId],
145+
run: async () => {
146+
await seedDivergedChildAnchors({
147+
autumn: autumnV2_3,
148+
childId,
149+
parentId,
150+
});
151+
const childV1 = await getFullPlan({ ctx, planId: childId, version: 1 });
152+
const childV2 = await getFullPlan({ ctx, planId: childId, version: 2 });
153+
154+
await autumnV2_3.catalogV2.update({
155+
plans: atmnDirectPut({
156+
childId,
157+
parentId,
158+
childV1Items: [messagesItem(100)],
159+
}),
160+
});
161+
162+
await expectChildVersionItems({
163+
ctx,
164+
childId,
165+
version: 1,
166+
messagesAllowance: 100,
167+
hasDashboard: false,
168+
hasWords: false,
169+
});
170+
await expectChildVersionItems({
171+
ctx,
172+
childId,
173+
version: 2,
174+
messagesAllowance: 50,
175+
hasDashboard: true,
176+
hasWords: true,
177+
});
178+
await expectAnchoredParentLink({
179+
ctx,
180+
childId,
181+
parentId,
182+
parentVersion: 1,
183+
childInternalId: childV1.internal_id,
184+
childVersion: 1,
185+
expected: { messagesAllowance: 100 },
186+
});
187+
await expectAnchoredParentLink({
188+
ctx,
189+
childId,
190+
parentId,
191+
parentVersion: 2,
192+
childInternalId: childV2.internal_id,
193+
childVersion: 2,
194+
expected: {
195+
messagesAllowance: 50,
196+
hasDashboard: true,
197+
hasWords: true,
198+
},
199+
});
200+
},
201+
});
202+
},
203+
);
204+
205+
test.concurrent(
206+
`${chalk.yellowBright("catalogV2 plan-licenses: atmn PUT preview is explicit and does not unlink")}`,
207+
async () => {
208+
const { autumnV2_3, ctx } = await initScenario({ setup: [], actions: [] });
209+
const childId = uniqueTestId("cv2_lic_atmn_h_c");
210+
const parentId = uniqueTestId("cv2_lic_atmn_h_p");
211+
await withCatalogPlans({
212+
ctx,
213+
planIds: [childId, parentId],
214+
run: async () => {
215+
await seedDivergedChildAnchors({
216+
autumn: autumnV2_3,
217+
childId,
218+
parentId,
219+
});
220+
221+
const preview = parsePlanPreview(
222+
await autumnV2_3.catalogV2.previewUpdate({
223+
plans: atmnDirectPut({ childId, parentId }),
224+
}),
225+
);
226+
expectExplicitDashboardPreview({ preview, childId, parentId });
227+
},
228+
});
229+
},
230+
);
231+
232+
test.concurrent(
233+
`${chalk.yellowBright("catalogV2 plan-licenses: atmn PUT restates overlays and still flows Dashboard")}`,
234+
async () => {
235+
const { autumnV2_3, ctx } = await initScenario({ setup: [], actions: [] });
236+
const childId = uniqueTestId("cv2_lic_atmn_i_c");
237+
const parentId = uniqueTestId("cv2_lic_atmn_i_p");
238+
await withCatalogPlans({
239+
ctx,
240+
planIds: [childId, parentId],
241+
run: async () => {
242+
await seedDivergedChildAnchors({
243+
autumn: autumnV2_3,
244+
childId,
245+
parentId,
246+
});
247+
await seedParentOverlays({
248+
autumn: autumnV2_3,
249+
childId,
250+
parentId,
251+
});
252+
const childV1 = await getFullPlan({ ctx, planId: childId, version: 1 });
253+
const childV2 = await getFullPlan({ ctx, planId: childId, version: 2 });
254+
255+
await autumnV2_3.catalogV2.update({
256+
plans: atmnDirectPut({
257+
childId,
258+
parentId,
259+
parentV1Customize: messagesOverride(80),
260+
parentV2Customize: messagesOverride(40),
261+
}),
262+
});
263+
264+
await expectChildVersionItems({
265+
ctx,
266+
childId,
267+
version: 1,
268+
messagesAllowance: 100,
269+
hasDashboard: true,
270+
hasWords: false,
271+
});
272+
await expectChildVersionItems({
273+
ctx,
274+
childId,
275+
version: 2,
276+
messagesAllowance: 50,
277+
hasDashboard: true,
278+
hasWords: true,
279+
});
280+
await expectAnchoredParentLink({
281+
ctx,
282+
childId,
283+
parentId,
284+
parentVersion: 1,
285+
childInternalId: childV1.internal_id,
286+
childVersion: 1,
287+
expected: {
288+
messagesAllowance: 80,
289+
customized: true,
290+
hasDashboard: true,
291+
},
292+
});
293+
await expectAnchoredParentLink({
294+
ctx,
295+
childId,
296+
parentId,
297+
parentVersion: 2,
298+
childInternalId: childV2.internal_id,
299+
childVersion: 2,
300+
expected: {
301+
messagesAllowance: 40,
302+
customized: true,
303+
hasDashboard: true,
304+
hasWords: true,
305+
},
306+
});
307+
},
308+
});
309+
},
310+
);

0 commit comments

Comments
 (0)