Skip to content

Commit ec063d8

Browse files
committed
fix(driver-turso,cli): the OS_ARTIFACT_URL boot gate keeps its warn-and-boot answer on a Turso remote primary
The remote face's schema sync creates no declared NOT NULL, so a healthy remote database reads as destructive tighten_not_null, and the gate's remedy cannot run on that transport. TursoDriver declares driftReportCaveat (a sentence on the remote face, null elsewhere) and the gate, reading that declaration, warns and boots, applying and refusing nothing. Also from the contract review: the embedded-replica drift control is back beside the remote suite; the remote detector reads its catalog in three round trips per call; catalog reads refuse a non-row answer; a created index counts as applied only with the uniqueness and key columns asked for; the parity suite is cited by its real name. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Lr3rg6qTp9r38exA5DHEEH
1 parent 39b9abd commit ec063d8

5 files changed

Lines changed: 368 additions & 59 deletions

File tree

‎packages/cli/src/utils/artifact-boot-migration.test.ts‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,3 +214,37 @@ describe('formatDestructiveDriftRefusal', () => {
214214
expect(text).toContain('never skipped in silence');
215215
});
216216
});
217+
218+
describe('runArtifactBootMigrationGate — a driver that declares its report must not be acted on', () => {
219+
// [#23816] The Turso REMOTE face declares `driftReportCaveat`: its schema
220+
// sync does not create a declared NOT NULL, so a healthy remote database
221+
// reports destructive `tighten_not_null`, and the refusal's remedy cannot
222+
// run on that transport. The gate keeps its "did not run" answer there.
223+
const CAVEAT = 'the transport cannot act on its own report';
224+
225+
it('warns with the reason, applies nothing and refuses nothing, and never reads the report', async () => {
226+
const { driver, applyCalls } = fakeDriver([entry('destructive'), entry('safe')]);
227+
const declared: SqlDriverLike = { ...driver, driftReportCaveat: CAVEAT };
228+
const warned: string[] = [];
229+
230+
const verdict = await runArtifactBootMigrationGate({
231+
driver: declared, artifactDisplay: ARTIFACT, warn: (m) => warned.push(m),
232+
});
233+
234+
expect(verdict).toEqual({ ok: true, applied: [], skipped: [], destructive: [] });
235+
expect(driver.detectManagedDrift).not.toHaveBeenCalled();
236+
expect(applyCalls).toHaveLength(0);
237+
expect(warned).toHaveLength(1);
238+
expect(warned[0]).toContain(CAVEAT);
239+
expect(warned[0]).toContain('Boot continues');
240+
});
241+
242+
it('the control: a null caveat is no caveat, and the same report refuses the boot', async () => {
243+
const { driver } = fakeDriver([entry('destructive')]);
244+
const verdict = await runArtifactBootMigrationGate({
245+
driver: { ...driver, driftReportCaveat: null }, artifactDisplay: ARTIFACT,
246+
});
247+
expect(verdict.ok).toBe(false);
248+
expect(driver.detectManagedDrift).toHaveBeenCalledTimes(1);
249+
});
250+
});

‎packages/cli/src/utils/artifact-boot-migration.ts‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@
2525
* - **nothing is skipped silently** — the third state, "shrug and serve", is
2626
* the one this gate exists to delete.
2727
*
28+
* One driver-declared exception, and it is loud: a driver whose
29+
* `driftReportCaveat` says its report must not be acted on unattended (the
30+
* Turso REMOTE face, #23816) gets a warning naming that reason, and the gate
31+
* applies and refuses nothing — as when the report cannot be read at all.
32+
*
2833
* `needs_confirm` drift is applied along with `safe`: `os migrate apply`'s own
2934
* split is `category !== 'destructive' || allowDestructive`, so "the
3035
* `--allow-destructive` class" the acceptance names is exactly
@@ -120,6 +125,18 @@ export async function runArtifactBootMigrationGate(opts: {
120125
// no drift to classify and nothing for this policy to decide.
121126
if (!driver) return { ok: true, applied: [], skipped: [], destructive: [] };
122127

128+
// [#23816] A driver that declares its report must not be acted on
129+
// unattended gets this gate's "did not run" answer: warn, apply nothing,
130+
// refuse nothing. The Turso REMOTE face declares one: its schema sync does
131+
// not create a declared NOT NULL, so a healthy remote database reads as
132+
// destructive drift, and the refusal's remedy cannot run on that transport.
133+
// Read from the driver's declaration, never from its transport name.
134+
const caveat = driver.driftReportCaveat;
135+
if (typeof caveat === 'string' && caveat) {
136+
warn(` ⚠ Did not check the physical schema against the artifact: ${caveat}. Boot continues.`);
137+
return { ok: true, applied: [], skipped: [], destructive: [] };
138+
}
139+
123140
let drift: ManagedDriftEntry[];
124141
try {
125142
drift = await driver.detectManagedDrift();

‎packages/cli/src/utils/schema-migrate.ts‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,13 @@ export interface SqlDriverLike {
8080
* caller's own refusal branch can tell an operator which it was.
8181
*/
8282
planMediaColumnMove?: () => Promise<MediaColumnMoveScan>;
83+
/**
84+
* [#23816] Why this driver's drift report must not be acted on unattended,
85+
* or `null` / absent when it may be — `TursoDriver`'s REMOTE face declares
86+
* one. Read by the artifact-pinned boot gate (`./artifact-boot-migration.ts`),
87+
* which then neither applies nor refuses on the report.
88+
*/
89+
readonly driftReportCaveat?: string | null;
8390
/** Deferred-DDL surface (#3917) — optional, so a driver without it still boots. */
8491
setDeferredDdl?: (deferred: boolean) => void;
8592
previewDeferredSchemaWork?: () => Promise<PendingSchemaWork[]>;

0 commit comments

Comments
 (0)