Skip to content

Commit 039b6e6

Browse files
committed
refactor(webapp): derive the graced-flag write routing from one table
Both global write routes carried their own copy of two answers: which flag keys are graced, and which are server-computed. The JSON API named all four derived keys in a destructure and both primaries in its branch condition. So adding a graced group needed an edit in three files, and missing one would either write an unstamped flip or accept a stamp from a request body. Both answers now come from the group table. touchesGracedGroup and withoutDerivedKeys are exported and used by the route, so a new group needs no route change at all. The global page's managed-cloud refusal moves into lockedFlagsInPayload, a pure function, for the same reason: it encoded the locked-flag policy inline where nothing could test it. That is what closes the coverage gap. The routes previously held branch logic reachable only through an authenticated request, so it went untested while the function underneath it was well covered. The logic is now pure and tested directly, including that only a graced PRIMARY selects the stamped path: a body holding just a stamp must not reset a cutover clock.
1 parent 86402de commit 039b6e6

5 files changed

Lines changed: 145 additions & 12 deletions

File tree

apps/webapp/app/routes/admin.api.v1.feature-flags.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,12 @@ import { json } from "@remix-run/server-runtime";
33
import { prisma } from "~/db.server";
44
import { env } from "~/env.server";
55
import { requireAdminApiRequest } from "~/services/personalAccessToken.server";
6-
import { applyGlobalGracedFlips, makeSetMultipleFlags } from "~/v3/featureFlags.server";
6+
import {
7+
applyGlobalGracedFlips,
8+
makeSetMultipleFlags,
9+
touchesGracedGroup,
10+
withoutDerivedKeys,
11+
} from "~/v3/featureFlags.server";
712
import { validatePartialFeatureFlags } from "~/v3/featureFlags";
813

914
export async function action({ request }: ActionFunctionArgs) {

apps/webapp/app/routes/admin.feature-flags.tsx

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
type FeatureFlagKey,
1515
type FlagControlType,
1616
getAllFlagControlTypes,
17+
lockedFlagsInPayload,
1718
validatePartialFeatureFlags,
1819
} from "~/v3/featureFlags";
1920
import { flags as getGlobalFlags, replaceGlobalFeatureFlags } from "~/v3/featureFlags.server";
@@ -96,17 +97,12 @@ export const action = dashboardAction(
9697

9798
const { isManagedCloud } = featuresForRequest(request);
9899

99-
// On managed cloud, reject if payload includes locked flags
100-
if (isManagedCloud) {
101-
const lockedInPayload = Object.keys(parsed.data.flags).filter((key) =>
102-
GLOBAL_LOCKED_FLAGS.includes(key)
100+
const lockedInPayload = lockedFlagsInPayload(Object.keys(parsed.data.flags), isManagedCloud);
101+
if (lockedInPayload.length > 0) {
102+
return json(
103+
{ error: `Cannot modify locked flags: ${lockedInPayload.join(", ")}` },
104+
{ status: 400 }
103105
);
104-
if (lockedInPayload.length > 0) {
105-
return json(
106-
{ error: `Cannot modify locked flags: ${lockedInPayload.join(", ")}` },
107-
{ status: 400 }
108-
);
109-
}
110106
}
111107

112108
const validationResult = validatePartialFeatureFlags(parsed.data.flags);

apps/webapp/app/v3/featureFlags.server.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,8 +212,14 @@ function gracedGroupFor(key: FeatureFlagKey) {
212212
return GRACED_GLOBAL_GROUPS.find((g) => g.primary === key || g.derived.includes(key));
213213
}
214214

215+
// True when a save changes any graced group, and therefore needs the stamped path. Derived from
216+
// the group table, so adding a group cannot leave a caller silently writing an unstamped flip.
217+
export function touchesGracedGroup(requestedFlags: Record<string, unknown>): boolean {
218+
return GRACED_GLOBAL_GROUPS.some((group) => requestedFlags[group.primary] !== undefined);
219+
}
220+
215221
// Strips every derived key: a grace stamp is computed here, never accepted from a caller.
216-
function withoutDerivedKeys(
222+
export function withoutDerivedKeys(
217223
requestedFlags: Partial<z.infer<typeof FeatureFlagCatalogSchema>>
218224
): Record<string, unknown> {
219225
const out: Record<string, unknown> = { ...requestedFlags };

apps/webapp/app/v3/featureFlags.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,21 @@ export const ORG_LOCKED_FLAGS: FeatureFlagKey[] = [
190190
FEATURE_FLAG.runOpsMintShardOverride,
191191
];
192192

193+
/**
194+
* Locked flags present in a payload the global page must refuse. On managed cloud the page never
195+
* offers them, so their presence means the request did not come from that page. Locally an admin
196+
* may unlock and edit them, so nothing is refused.
197+
*/
198+
export function lockedFlagsInPayload(
199+
payloadKeys: string[],
200+
isManagedCloud: boolean
201+
): FeatureFlagKey[] {
202+
if (!isManagedCloud) return [];
203+
return payloadKeys.filter((key): key is FeatureFlagKey =>
204+
GLOBAL_LOCKED_FLAGS.includes(key as FeatureFlagKey)
205+
);
206+
}
207+
193208
// Create a Zod schema from the existing catalog
194209
export const FeatureFlagCatalogSchema = z.object(FeatureFlagCatalog);
195210
export type FeatureFlagCatalog = z.infer<typeof FeatureFlagCatalogSchema>;
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
// The two global write routes used to carry their own copy of "which keys are graced" and "which
2+
// keys are derived". A new graced group would then need an edit in three places, and missing one
3+
// means an unstamped flip or a stamp written straight from a request body. Both routes now derive
4+
// both answers from the group table, and these tests pin that. Pure, no containers.
5+
import { describe, expect, it } from "vitest";
6+
import { FEATURE_FLAG, lockedFlagsInPayload } from "~/v3/featureFlags";
7+
import { touchesGracedGroup, withoutDerivedKeys } from "~/v3/featureFlags.server";
8+
9+
describe("touchesGracedGroup — decides whether a save needs the stamped path", () => {
10+
it("is true for a mint-kind change", () => {
11+
expect(touchesGracedGroup({ [FEATURE_FLAG.runOpsMintKind]: "runOpsId" })).toBe(true);
12+
});
13+
14+
it("is true for a shard-list change", () => {
15+
expect(touchesGracedGroup({ [FEATURE_FLAG.runOpsMintShardSet]: "a,b" })).toBe(true);
16+
});
17+
18+
it("is false for an ordinary flag, which writes directly", () => {
19+
expect(touchesGracedGroup({ [FEATURE_FLAG.mollifierEnabled]: true })).toBe(false);
20+
expect(touchesGracedGroup({})).toBe(false);
21+
});
22+
23+
it("is false when only a DERIVED key is present", () => {
24+
// A body carrying only a stamp changes no group. Treating it as a flip would let a caller
25+
// reset a cutover clock without touching the value the clock dates.
26+
expect(touchesGracedGroup({ [FEATURE_FLAG.runOpsMintKindPrev]: "cuid" })).toBe(false);
27+
expect(touchesGracedGroup({ [FEATURE_FLAG.runOpsMintShardSetPrev]: "a" })).toBe(false);
28+
});
29+
30+
it("covers every graced primary, so a new group needs no route edit", () => {
31+
// The routes no longer name these keys. If a group is added and this list is not, the next
32+
// assertion fails rather than the group silently skipping the stamp.
33+
const gracedPrimaries = [FEATURE_FLAG.runOpsMintKind, FEATURE_FLAG.runOpsMintShardSet];
34+
for (const key of gracedPrimaries) {
35+
expect(touchesGracedGroup({ [key]: "x" })).toBe(true);
36+
}
37+
// Every key the strip removes belongs to a group whose primary is one of the above.
38+
const derived = Object.keys(
39+
withoutDerivedKeys({
40+
[FEATURE_FLAG.runOpsMintKindPrev]: "cuid",
41+
[FEATURE_FLAG.runOpsMintKindFlippedAt]: "t",
42+
[FEATURE_FLAG.runOpsMintShardSetPrev]: "a",
43+
[FEATURE_FLAG.runOpsMintShardSetFlippedAt]: "t",
44+
} as Record<string, unknown>)
45+
);
46+
expect(derived).toEqual([]);
47+
});
48+
});
49+
50+
describe("withoutDerivedKeys — a stamp is never taken from a request body", () => {
51+
it("strips both stamps and keeps everything else", () => {
52+
const out = withoutDerivedKeys({
53+
[FEATURE_FLAG.runOpsMintKind]: "runOpsId",
54+
[FEATURE_FLAG.runOpsMintKindPrev]: "spoofed",
55+
[FEATURE_FLAG.runOpsMintKindFlippedAt]: "1999-01-01T00:00:00.000Z",
56+
[FEATURE_FLAG.runOpsMintShardSet]: "a,b",
57+
[FEATURE_FLAG.runOpsMintShardSetPrev]: "spoofed",
58+
[FEATURE_FLAG.runOpsMintShardSetFlippedAt]: "1999-01-01T00:00:00.000Z",
59+
[FEATURE_FLAG.mollifierEnabled]: true,
60+
} as Record<string, unknown>);
61+
62+
expect(out).toEqual({
63+
[FEATURE_FLAG.runOpsMintKind]: "runOpsId",
64+
[FEATURE_FLAG.runOpsMintShardSet]: "a,b",
65+
[FEATURE_FLAG.mollifierEnabled]: true,
66+
});
67+
});
68+
69+
it("does not mutate its input", () => {
70+
const input = { [FEATURE_FLAG.runOpsMintKindPrev]: "cuid" } as Record<string, unknown>;
71+
withoutDerivedKeys(input);
72+
expect(input[FEATURE_FLAG.runOpsMintKindPrev]).toBe("cuid");
73+
});
74+
});
75+
76+
describe("lockedFlagsInPayload — what the global page refuses", () => {
77+
it("refuses a locked flag on managed cloud, where the page never offers one", () => {
78+
const refused = lockedFlagsInPayload(
79+
[FEATURE_FLAG.taskEventRepository, FEATURE_FLAG.mollifierEnabled],
80+
true
81+
);
82+
expect(refused).toEqual([FEATURE_FLAG.taskEventRepository]);
83+
});
84+
85+
it("refuses the mint-shard pins, which are per-org only", () => {
86+
expect(lockedFlagsInPayload([FEATURE_FLAG.runOpsMintShard], true)).toEqual([
87+
FEATURE_FLAG.runOpsMintShard,
88+
]);
89+
expect(lockedFlagsInPayload([FEATURE_FLAG.runOpsMintShardEnvPins], true)).toEqual([
90+
FEATURE_FLAG.runOpsMintShardEnvPins,
91+
]);
92+
});
93+
94+
it("refuses a grace stamp, which the server owns", () => {
95+
expect(lockedFlagsInPayload([FEATURE_FLAG.runOpsMintShardSetFlippedAt], true)).toEqual([
96+
FEATURE_FLAG.runOpsMintShardSetFlippedAt,
97+
]);
98+
});
99+
100+
it("allows the shard list, because that is the page's ramp lever", () => {
101+
expect(lockedFlagsInPayload([FEATURE_FLAG.runOpsMintShardSet], true)).toEqual([]);
102+
});
103+
104+
it("refuses nothing when not managed cloud, where an admin may unlock and edit", () => {
105+
expect(lockedFlagsInPayload([FEATURE_FLAG.taskEventRepository], false)).toEqual([]);
106+
});
107+
108+
it("refuses nothing for an empty payload", () => {
109+
expect(lockedFlagsInPayload([], true)).toEqual([]);
110+
});
111+
});

0 commit comments

Comments
 (0)