Skip to content

Commit 7898ef8

Browse files
committed
test(cli): pin ALLOW_TOKEN_MAP against ALLOW_TOKENS in the drift guard
allow-tokens-pinned.test.ts covered ALLOW_TOKENS (the --allow validator) and sdk's AllowTokenEnum, but not ALLOW_TOKEN_MAP (lib/allow.ts) — the structure that actually grants a validated token's permission. A token present in ALLOW_TOKENS but missing from the map would pass validation cleanly and tokensToAllowOptions would silently grant nothing for it: --allow <token> looks accepted but authorizes no destructive drop, and the diff blocks it anyway with no indication the flag was ever a no-op. Same drift class that left adopt-view broken in sdk's config schema since 0.20.4, but worse here because there's no error to notice. - cli/src/lib/allow.ts: export ALLOW_TOKEN_MAP (was module-local) so the test can import the real structure instead of a hardcoded copy. - allow-tokens-pinned.test.ts: two new assertions — ALLOW_TOKEN_MAP's key set matches ALLOW_TOKENS in both directions, and its AllowOptions values are unique (no two tokens silently grant the same permission). Verified non-vacuous: removing an entry from ALLOW_TOKEN_MAP makes the new assertion fail (confirmed with adopt-view specifically); restoring it goes green again.
1 parent f9bd8e2 commit 7898ef8

2 files changed

Lines changed: 51 additions & 12 deletions

File tree

‎server/typescript/packages/cli/src/lib/allow.ts‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,12 @@
55
import type { AllowOptions, Change } from "@metaobjectsdev/migrate-ts";
66

77
// Map CLI allow tokens → migrate-ts AllowOptions field names.
8-
const ALLOW_TOKEN_MAP: Record<string, keyof AllowOptions> = {
8+
// Exported (not just module-local) so allow-tokens-pinned.test.ts can pin its
9+
// key set against ALLOW_TOKENS (args.ts). ALLOW_TOKENS is the *validator* —
10+
// this map is what actually *grants* the permission; a token present in
11+
// ALLOW_TOKENS but missing here would pass validation and silently grant
12+
// nothing, on a destructive operation.
13+
export const ALLOW_TOKEN_MAP: Record<string, keyof AllowOptions> = {
914
"drop-column": "dropColumn",
1015
"drop-table": "dropTable",
1116
"type-change": "typeChange",
Lines changed: 45 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,33 @@
11
/**
2-
* Drift guard between the CLI's authoritative `--allow` token list
3-
* (`ALLOW_TOKENS`, `lib/args.ts` — the list that actually validates
4-
* `--allow <csv>`) and `sdk`'s `AllowTokenEnum` (`config.ts` — the list that
5-
* validates the STATIC `migrate.allow` array in `.metaobjects/config.json`).
2+
* Drift guard across the three token-bearing structures behind `--allow`:
63
*
7-
* These silently drifted before this test existed: sdk's enum had only 6 of
8-
* the 11 real tokens, missing `drop-check`, `drop-view`, `drop-view-cascade`,
9-
* `adopt-view` and `drop-identity-default`. A user who set any of those five
10-
* in `.metaobjects/config.json`'s `migrate.allow` got a schema rejection for
11-
* a flag the CLI itself accepted fine on the command line — `adopt-view` had
4+
* - `ALLOW_TOKENS` (`lib/args.ts`) — the CLI's authoritative list; what
5+
* actually VALIDATES `--allow <csv>`.
6+
* - `AllowTokenEnum` (`sdk`'s `config.ts`) — validates the STATIC
7+
* `migrate.allow` array in `.metaobjects/config.json`.
8+
* - `ALLOW_TOKEN_MAP` (`lib/allow.ts`) — what actually GRANTS the permission,
9+
* translating a validated token into the `AllowOptions` field `diff()`
10+
* reads.
11+
*
12+
* `ALLOW_TOKENS` and `AllowTokenEnum` silently drifted before this test
13+
* existed: sdk's enum had only 6 of the 11 real tokens, missing
14+
* `drop-check`, `drop-view`, `drop-view-cascade`, `adopt-view` and
15+
* `drop-identity-default`. A user who set any of those five in
16+
* `.metaobjects/config.json`'s `migrate.allow` got a schema rejection for a
17+
* flag the CLI itself accepted fine on the command line — `adopt-view` had
1218
* shipped since 0.20.4 and was affected the whole time.
1319
*
14-
* Import BOTH sides rather than hardcoding a third "expected" list here — a
15-
* hardcoded list would just be a fourth copy that can itself drift.
20+
* `ALLOW_TOKEN_MAP` is a distinct, worse failure mode if it drifts from
21+
* `ALLOW_TOKENS`: a token present in `ALLOW_TOKENS` (and `AllowTokenEnum`)
22+
* but missing from the map passes validation cleanly and then
23+
* `tokensToAllowOptions` silently grants NOTHING for it — the user believes
24+
* `--allow <token>` authorized a destructive drop; it didn't, and the diff
25+
* blocks it anyway with no indication the flag was ever a no-op. That is a
26+
* silent-failure mode on exactly the path this whole feature exists to
27+
* protect.
28+
*
29+
* Import ALL of these rather than hardcoding a fourth "expected" list here —
30+
* a hardcoded list would just be a fifth copy that can itself drift.
1631
*
1732
* Package-dependency direction: `cli` depends on `sdk` (`workspace:*`), not
1833
* the other way around, so this test can only live in `cli` — `sdk` cannot
@@ -22,6 +37,7 @@
2237
*/
2338
import { test, expect, describe } from "bun:test";
2439
import { ALLOW_TOKENS } from "../../src/lib/args.js";
40+
import { ALLOW_TOKEN_MAP } from "../../src/lib/allow.js";
2541
import { AllowTokenEnum } from "@metaobjectsdev/sdk";
2642

2743
describe("--allow token lists stay pinned across packages", () => {
@@ -36,4 +52,22 @@ describe("--allow token lists stay pinned across packages", () => {
3652
expect(missingFromCli).toEqual([]);
3753
expect(sdkTokens.size).toBe(cliTokens.size);
3854
});
55+
56+
test("ALLOW_TOKEN_MAP grants a permission for every validated token, and nothing extra", () => {
57+
const cliTokens = new Set<string>(ALLOW_TOKENS);
58+
const mapKeys = new Set<string>(Object.keys(ALLOW_TOKEN_MAP));
59+
60+
const validatedButNotGranted = [...cliTokens].filter((t) => !mapKeys.has(t));
61+
const grantedButNotValidated = [...mapKeys].filter((t) => !cliTokens.has(t));
62+
63+
expect(validatedButNotGranted).toEqual([]);
64+
expect(grantedButNotValidated).toEqual([]);
65+
expect(mapKeys.size).toBe(cliTokens.size);
66+
});
67+
68+
test("ALLOW_TOKEN_MAP's AllowOptions fields are unique — no two tokens grant the same permission", () => {
69+
const fields = Object.values(ALLOW_TOKEN_MAP);
70+
const uniqueFields = new Set(fields);
71+
expect(uniqueFields.size).toBe(fields.length);
72+
});
3973
});

0 commit comments

Comments
 (0)