You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix(cli,migrate): close gaps found verifying the #787 review fixes
Follow-up to 1d14412, from an adversarial cross-check of that commit.
resolve-eql's private `columnExists` used a bare `to_regclass($1)`. That form
parses and case-folds unquoted identifiers, so on a Prisma-style "User" table
the probe reported the column missing, the recorded pairing was treated as
stale, and the #772 fail-closed silently did not fire — falling through to the
sole/convention rules and resolving the guess it exists to prevent. migrate
already documents this exact anti-pattern (REGCLASS_SQL) and its test asserts
`not.toMatch(/to_regclass\(\$1\)/)`. Moved to a shared, case-exact
`columnExists` export and deleted the CLI copy; the test double is now
case-exact too, so it cannot hide a regression.
The placeholder guard read the harvested export map while the `db push` /
`db validate` guard it mirrors reads `getEncryptConfig().tables`. Those
disagree in both directions on one file: `schemas: [placeholderTable]` minus
the `export` keyword fell through to "table not found … Available: (none)" —
the error the guard replaces — and a stale placeholder export beside real
tables wrongly fired it. Now reads the same source.
cutover's `via:'sole'` refusal was nested inside `version === 3`; drop's is
top-level. Equivalent today, but cutover's v2 ladder does an irreversible
rename plus config promotion, so a restored v2 classification or a v4 family
would let cutover rename on a guess drop refuses. Hoisted to match.
Text that was false:
- the scaffold `stash init` writes into every customer project (and both
fixtures) claimed `stash encrypt` commands refuse to run; only backfill does
- skills/stash-cli listed the client as loaded by `schema build` and `encrypt *`,
and omitted `db push`
- skills/stash-cli and skills/stash-encryption still said cutover on a backfilled
v3 column exits 0. Since `.cipherstash/` is gitignored, a clone or CI runner
hits the new `sole` refusal on a pure-v3 table with an unconventional column
name — the changeset's "pure-v3 unaffected" was wrong too
- both `sole` messages said "the table's only EQL column"; pickEncryptedColumn
excludes the plaintext column first, so it fires with two
- the remedy said to drive the v2 lifecycle "directly"; there is no CLI route,
so it now says to run the eql_v2 SQL yourself
- vitest.config.ts and packages/cli/AGENTS.md claimed the unit suite is
self-contained. It is not: @cipherstash/stack is still reached via
migrate/src/backfill.ts. The alias removed one of two couplings
typecheck:scaffold ran `tsc` directly, bypassing turbo, so it needed a prior
build and passed only by accident of step ordering behind steps that read as
independently droppable. Now a turbo task with dependsOn ^build.
Added scripts/__tests__/cli-vitest-alias.test.mjs — the CLI's alias map sits
outside the vitest.shared.ts guard and must, since stackSourceAlias's '@/'
would clobber the CLI's own. Runs in test:scripts, ahead of the build-dependent
suite.
Verified: frozen-lockfile install clean; 891 CLI unit, 41 migrate, 93 scripts,
76 pty e2e; code:check 0 errors; scaffold gate green through turbo with
packages/stack/dist absent.
Copy file name to clipboardExpand all lines: packages/cli/AGENTS.md
+13-2Lines changed: 13 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -6,11 +6,22 @@ This package has **two** Vitest configs. Run the right one for the change.
6
6
7
7
| Command | Config | Scope | Needs build? |
8
8
| --- | --- | --- | --- |
9
-
|`pnpm --filter stash test`|`vitest.config.ts`| Unit tests under `src/__tests__/**` and `src/**/__tests__/**`|No|
9
+
|`pnpm --filter stash test`|`vitest.config.ts`| Unit tests under `src/__tests__/**` and `src/**/__tests__/**`|**Partly** — needs `@cipherstash/stack` built (see below). Turbo's `^build` supplies it in CI.|
10
10
|`pnpm --filter stash test:e2e`|`vitest.integration.config.ts`| E2E tests under `tests/e2e/**.e2e.test.ts` driving the built `dist/bin/stash.js` through a real pty (`node-pty`) |**Yes** — run `pnpm --filter stash build` first, or use the turbo `test:e2e` task which depends on `build`. |
11
11
12
12
The unit config explicitly excludes `tests/e2e/**` so the default `pnpm test`
13
-
stays fast and self-contained.
13
+
stays fast.
14
+
15
+
It is **not** fully self-contained, despite running standalone in CI. Some `src`
16
+
modules import workspace packages that publish `./dist` only, so an unbuilt
17
+
workspace fails at collection with `Failed to resolve entry for package …`
18
+
rather than at an assertion. `vitest.config.ts` aliases `@cipherstash/migrate`
19
+
to its source to remove one such coupling; `@cipherstash/stack` remains, reached
20
+
via `packages/migrate/src/backfill.ts` and a direct import in
`Error: ${stashConfig.client} still contains the placeholder table \`${PLACEHOLDER_TABLE_NAME}\` that \`stash init\` wrote.\n\nDeclare your encrypted columns and pass those tables to Encryption({ schemas: [...] }) in that file, then re-run this command.`,
// recognises `eql_v3_*` only, so a non-null `info` is always version 3 —
109
+
// but the v2 ladder below performs an irreversible rename plus config
110
+
// promotion. Were v2 classification restored, or a v4 family added, a
111
+
// nested guard would let `cutover` rename on a guess that `drop` refuses
112
+
// (#787 review).
113
+
if(info?.via==='sole'){
114
+
p.log.error(
115
+
`${options.table}.${info.column} (${info.domain}) is the only EQL column left on ${options.table} once "${options.column}" itself is excluded, but nothing confirms it encrypts "${options.column}" — refusing to report a cut-over outcome on that guess. If "${options.column}" pairs with a legacy eql_v2_encrypted column, resolution cannot see it (this command resolves EQL v3 counterparts only): complete that column's v2 lifecycle yourself with the eql_v2 SQL — \`SELECT eql_v2.rename_encrypted_columns();\` plus the config promotion — since no stash command can drive it here. Otherwise record the pairing: re-run \`stash encrypt backfill --table ${options.table} --column ${options.column} --encrypted-column <the column that actually encrypts ${options.column}>\`.`,
116
+
)
117
+
exitCode=1
118
+
return
119
+
}
120
+
98
121
if(info?.version===3){
99
122
constencryptedColumn=info.column
100
123
101
-
// `via: 'sole'` means only that this is the table's ONE EQL v3 column —
102
-
// nothing ties it to the plaintext column the user named. On a mixed
103
-
// table (a v2 pair the classifier no longer sees, plus one unrelated v3
104
-
// column) that guess is simply wrong, and reporting "nothing to do for
105
-
// EQL v3" for it told a scripted rollout the cut-over had succeeded when
106
-
// the v2 rename never ran. `drop.ts` already refuses a `'sole'` match for
107
-
// the same reason (#772 review, finding 7).
108
-
if(info.via==='sole'){
109
-
p.log.error(
110
-
`${options.table}.${encryptedColumn} (${info.domain}) is the table's only EQL v3 column, but nothing confirms it encrypts "${options.column}" — refusing to report a cut-over outcome on that guess. If "${options.column}" pairs with a legacy eql_v2_encrypted column, resolution cannot see it (this command resolves EQL v3 counterparts only) — drive that column's v2 lifecycle against its own encrypted column directly. Otherwise record the pairing: re-run \`stash encrypt backfill --table ${options.table} --column ${options.column} --encrypted-column <the column that actually encrypts ${options.column}>\`.`,
111
-
)
112
-
exitCode=1
113
-
return
114
-
}
115
124
if(state?.phase==='dropped'){
116
125
// Terminal phase — the lifecycle already finished. Not an error and
117
126
// not "finish the backfill": there is nothing left to backfill.
Copy file name to clipboardExpand all lines: packages/cli/src/commands/encrypt/drop.ts
+1-1Lines changed: 1 addition & 1 deletion
Original file line number
Diff line number
Diff line change
@@ -113,7 +113,7 @@ export async function dropCommand(options: DropCommandOptions) {
113
113
// live `DROP COLUMN` on the plaintext at exit 0 (#772 review, finding 7).
114
114
if(info?.via==='sole'){
115
115
p.log.error(
116
-
`${options.table}.${info.column} (${info.domain}) is the table's only encrypted column, but nothing confirms it encrypts "${options.column}" — refusing to generate an irreversible drop on that guess. Identify the column that actually encrypts "${options.column}" and record that pairing: re-run \`stash encrypt backfill --table ${options.table} --column ${options.column} --encrypted-column <name>\` (which writes it to the manifest), or set "encryptedColumn" for this column in .cipherstash/migrations.json. If "${options.column}" pairs with a legacy eql_v2_encrypted column, resolution cannot see it (this command resolves EQL v3 counterparts only) — drive that column's v2 lifecycle against its own encrypted column directly, and do not record ${info.column}.`,
116
+
`${options.table}.${info.column} (${info.domain}) is the only EQL column left on ${options.table} once "${options.column}" itself is excluded, but nothing confirms it encrypts "${options.column}" — refusing to generate an irreversible drop on that guess. Identify the column that actually encrypts "${options.column}" and record that pairing: re-run \`stash encrypt backfill --table ${options.table} --column ${options.column} --encrypted-column <name>\` (which writes it to the manifest), or set "encryptedColumn" for this column in .cipherstash/migrations.json. If "${options.column}" pairs with a legacy eql_v2_encrypted column, resolution cannot see it (this command resolves EQL v3 counterparts only): complete that column's v2 lifecycle yourself with the eql_v2 SQL, since no stash command can drive it here — and do not record ${info.column}.`,
0 commit comments