Skip to content

Commit d4e977b

Browse files
committed
test(cli): the os dev self-heal pin stages the legacy global unique, the safe drift that survives the plain organization key part
Claude-Session: https://claude.ai/code/session_01NnXVu3vF2TKFLMi5Me4CUd Co-authored-by: Claude <noreply@anthropic.com>
1 parent c1f0005 commit d4e977b

1 file changed

Lines changed: 61 additions & 23 deletions

File tree

‎packages/cli/test/dev-standalone-self-heal.integration.test.ts‎

Lines changed: 61 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,27 @@
2323
*
2424
* ## What each leg pins
2525
*
26-
* - boot 1 — a fresh `os dev` creates the NULL-safe index; with
27-
* `OS_TELEMETRY_DB=0` it provisions no telemetry sibling (the opt-out).
28-
* - boot 2 — after the card's step 3 restages the bare index, a plain `os dev`
29-
* restart puts the NULL-safe one back, says `auto-reconciled`, and provisions
26+
* - boot 1 — a fresh `os dev` creates the plain `(organization_id, qa_code)`
27+
* composite (ADR-0131 D1: no COALESCE) and no legacy single-column UNIQUE;
28+
* with `OS_TELEMETRY_DB=0` it provisions no telemetry sibling (the opt-out).
29+
* - boot 2 — after the staging step plants the legacy platform-wide UNIQUE on
30+
* `qa_code` beside the composite, a plain `os dev` restart drops it (a pure
31+
* relaxation, the composite survives), says `auto-reconciled`, and provisions
3032
* `A.telemetry.db` (default-on in dev for a file-backed SQLite primary).
3133
* - boot 3 — a production boot (`os serve`, `NODE_ENV` unset) on the restaged
3234
* file reports the drift and leaves it: production never auto-migrates.
3335
*
36+
* ⚠️ Why not the drift the card staged. #21733 reproduced with the ADR-0120 D4
37+
* tightening — a bare composite against the NULL-safe expectation, a
38+
* `recreate_index` the duplicate probe recategorised `safe` (the table above).
39+
* ADR-0131 D1 retired it: the expectation is the plain composite now, so a bare
40+
* composite MATCHES and there is no drift at all. The defect is about the
41+
* standalone stack never applying the self-heal's `safe` subset, not about any
42+
* one op, so the staging carries the `safe` op that is still live:
43+
* `replace_unique_index`, the legacy global UNIQUE an older release left (the
44+
* same fixture `src/utils/schema-migrate.dev-self-heal-fence.integration.test.ts`
45+
* stages).
46+
*
3447
* The host-config path is pinned unchanged where it is decided
3548
* (`src/utils/storage-driver.test.ts`), and both hosts are held to one answer
3649
* per driver kind by `src/utils/dev-self-heal-host-parity.test.ts`; the
@@ -53,6 +66,7 @@ import { tmpdir } from 'node:os';
5366
import { join, resolve } from 'node:path';
5467
import { fileURLToPath } from 'node:url';
5568
import Database from 'better-sqlite3';
69+
import { legacyUniqueIndexNames } from '@objectstack/driver-sql';
5670
import {
5771
childEnv,
5872
E2E_SECRET_KEY,
@@ -72,7 +86,13 @@ const READY = /Press Ctrl\+C to stop/;
7286
const BOOT_TIMEOUT_MS = 180_000;
7387
const ALL_BOOTS_TIMEOUT_MS = 3 * (BOOT_TIMEOUT_MS + 30_000);
7488

89+
/** The organization composite the declaration asks for (ADR-0131 D1: plain key parts). */
7590
const INDEX = 'uniq_scaf_item_organization_id_qa_code';
91+
/**
92+
* The legacy platform-wide single-column UNIQUE, spelled by the driver's own
93+
* `legacyUniqueIndexNames` — its `uniq_` arm, the one the differ recognises.
94+
*/
95+
const LEGACY = legacyUniqueIndexNames('scaf_item', 'qa_code')[1];
7696

7797
/** The card's blank scaffold: one object, an org-scoped unique field, no plugins, no datasources. */
7898
const SCAFFOLD = {
@@ -86,8 +106,10 @@ const SCAFFOLD = {
86106
};
87107

88108
interface BootRecord {
89-
/** The index DDL `sqlite_master` holds after the boot stopped. */
109+
/** The composite's DDL `sqlite_master` holds after the boot stopped. */
90110
indexSql: string | undefined;
111+
/** The legacy global UNIQUE's DDL after the boot stopped; `undefined` when absent. */
112+
legacySql: string | undefined;
91113
telemetryExists: boolean;
92114
output: string;
93115
}
@@ -97,10 +119,10 @@ let dir: string;
97119
let db: string;
98120
const record: Partial<Record<'fresh' | 'restart' | 'production', BootRecord | Error>> = {};
99121

100-
function readIndexSql(): string | undefined {
122+
function readIndexSql(name: string): string | undefined {
101123
const handle = new Database(db, { readonly: true });
102124
try {
103-
const row = handle.prepare("SELECT sql FROM sqlite_master WHERE type = 'index' AND name = ?").get(INDEX) as
125+
const row = handle.prepare("SELECT sql FROM sqlite_master WHERE type = 'index' AND name = ?").get(name) as
104126
| { sql: string }
105127
| undefined;
106128
return row?.sql;
@@ -109,11 +131,16 @@ function readIndexSql(): string | undefined {
109131
}
110132
}
111133

112-
/** The card's step 3: replace the NULL-safe unique index with the bare one an older release left. */
113-
function stageBareIndex(): void {
134+
/**
135+
* The staging step: plant, beside the plain composite, the platform-wide UNIQUE
136+
* on `qa_code` a pre-#3696 release left — the state every additive boot after
137+
* such an upgrade produces, since the sync creates the composite by name and
138+
* never drops anything. Idempotent, so it restages after a heal.
139+
*/
140+
function stageLegacyIndex(): void {
114141
const handle = new Database(db);
115142
try {
116-
handle.exec(`DROP INDEX ${INDEX}; CREATE UNIQUE INDEX ${INDEX} ON scaf_item (organization_id, qa_code);`);
143+
handle.exec(`CREATE UNIQUE INDEX IF NOT EXISTS ${LEGACY} ON scaf_item (qa_code);`);
117144
} finally {
118145
handle.close();
119146
}
@@ -200,7 +227,12 @@ async function stopGroup(child: ChildProcess): Promise<void> {
200227
async function recordBoot(leg: 'fresh' | 'restart' | 'production', boot: Promise<string>): Promise<void> {
201228
try {
202229
const output = await boot;
203-
record[leg] = { indexSql: readIndexSql(), telemetryExists: existsSync(join(dir, 'A.telemetry.db')), output };
230+
record[leg] = {
231+
indexSql: readIndexSql(INDEX),
232+
legacySql: readIndexSql(LEGACY),
233+
telemetryExists: existsSync(join(dir, 'A.telemetry.db')),
234+
output,
235+
};
204236
} catch (error) {
205237
record[leg] = error instanceof Error ? error : new Error(String(error));
206238
}
@@ -213,7 +245,9 @@ function recorded(leg: 'fresh' | 'restart' | 'production'): BootRecord {
213245
return r;
214246
}
215247

216-
const isNullSafe = (sql: string | undefined): boolean => /COALESCE\(`?organization_id`?/.test(sql ?? '');
248+
/** The composite in its ADR-0131 D1 form: both key parts plain, no COALESCE. */
249+
const isPlainComposite = (sql: string | undefined): boolean =>
250+
/\(\s*[`"]?organization_id[`"]?\s*,\s*[`"]?qa_code[`"]?\s*\)/.test(sql ?? '') && !/COALESCE/i.test(sql ?? '');
217251

218252
beforeAll(async () => {
219253
requireBuiltCli(RUN_JS_RESOLVES_FROM_DIST);
@@ -224,12 +258,12 @@ beforeAll(async () => {
224258
// Boot 1: fresh, with the telemetry opt-out.
225259
await recordBoot('fresh', bootUntilReady(['dev', '--no-watch', '-d', 'file:A.db', '-p', KERNEL_ASSIGNED_PORT], { OS_TELEMETRY_DB: '0' }));
226260

227-
// Boot 2: the card's restart, on the restaged bare index, telemetry default-on.
228-
if (existsSync(db)) stageBareIndex();
261+
// Boot 2: the card's restart, on the staged legacy index, telemetry default-on.
262+
if (existsSync(db)) stageLegacyIndex();
229263
await recordBoot('restart', bootUntilReady(['dev', '--no-watch', '-d', 'file:A.db', '-p', KERNEL_ASSIGNED_PORT], {}));
230264

231-
// Boot 3: production (`os serve`, NODE_ENV unset) on the restaged bare index.
232-
if (existsSync(db)) stageBareIndex();
265+
// Boot 3: production (`os serve`, NODE_ENV unset) on the restaged legacy index.
266+
if (existsSync(db)) stageLegacyIndex();
233267
await recordBoot('production', bootUntilReady(['serve', 'objectstack.config.ts', '-p', KERNEL_ASSIGNED_PORT], {
234268
OS_DATABASE_URL: 'file:A.db',
235269
OS_AUTH_SECRET: 'e2e-21733-production-leg-secret-not-for-real-use',
@@ -243,16 +277,19 @@ afterAll(async () => {
243277
});
244278

245279
describe('#21733 — a plain `os dev` (standalone stack) self-heals and provisions telemetry', () => {
246-
it('a fresh `os dev` creates the NULL-safe index, and OS_TELEMETRY_DB=0 provisions no sibling', () => {
280+
it('a fresh `os dev` creates the plain organization composite, and OS_TELEMETRY_DB=0 provisions no sibling', () => {
247281
const fresh = recorded('fresh');
248-
expect(isNullSafe(fresh.indexSql), `fresh boot index: ${fresh.indexSql}`).toBe(true);
282+
expect(isPlainComposite(fresh.indexSql), `fresh boot index: ${fresh.indexSql}`).toBe(true);
283+
expect(fresh.legacySql, 'the fresh boot created the legacy global unique itself').toBeUndefined();
249284
expect(fresh.telemetryExists, 'OS_TELEMETRY_DB=0 still created A.telemetry.db').toBe(false);
250285
});
251286

252-
it('a plain `os dev` restart puts the staged bare unique index back NULL-safe', () => {
287+
it('a plain `os dev` restart relaxes the staged legacy global unique to the organization composite', () => {
253288
const restart = recorded('restart');
254-
expect(isNullSafe(restart.indexSql), `the restart left the bare index: ${restart.indexSql}`).toBe(true);
255-
expect(restart.output).toContain('[schema-drift] auto-reconciled recreate_index on scaf_item');
289+
expect(restart.legacySql, `the restart left the legacy index: ${restart.legacySql}`).toBeUndefined();
290+
// A relaxation, not a removal: the per-organization constraint survives.
291+
expect(isPlainComposite(restart.indexSql), `the restart lost the composite: ${restart.indexSql}`).toBe(true);
292+
expect(restart.output).toContain('[schema-drift] auto-reconciled replace_unique_index on scaf_item.qa_code');
256293
});
257294

258295
it('the same restart provisions the `telemetry` sibling next to the file-backed primary', () => {
@@ -262,8 +299,9 @@ describe('#21733 — a plain `os dev` (standalone stack) self-heals and provisio
262299

263300
it('a production boot reports the staged drift and does not auto-migrate it', () => {
264301
const production = recorded('production');
265-
expect(production.output).toContain(`index '${INDEX}' tightens`);
302+
expect(production.output).toContain(`a legacy platform-wide UNIQUE index (${LEGACY})`);
266303
expect(production.output).not.toContain('[schema-drift] auto-reconciled');
267-
expect(isNullSafe(production.indexSql), `production auto-migrated: ${production.indexSql}`).toBe(false);
304+
expect(production.legacySql, 'production auto-migrated the legacy index away').toBeDefined();
305+
expect(isPlainComposite(production.indexSql)).toBe(true);
268306
});
269307
});

0 commit comments

Comments
 (0)