Skip to content

Commit 81e6edd

Browse files
test(dogfood): fixture-18.0.0 — the upgrade harness boots a database the published 18.0.0 created (#24911)
Fixes #24884 Clause-②: no ## What `fixture-18.0.0`: a database the published `@objectstack/*@18.0.0` created, added to the 18.x upgrade harness, with one leg pinning that HEAD boots it. - **The fixture** (`packages/qa/dogfood/test/upgrade-harness/fixture-18.0.0/`). It was generated by `scripts/gen-upgrade-harness-fixture.mjs --ref @objectstack/cli@18.0.0 --checkout CHECKOUT --out …/fixture-18.0.0`, from a built checkout of the `@objectstack/cli@18.0.0` tag (`06cab4849a`). That checkout's `packages/cli/dist` (660 files) and `packages/runtime/dist` are sha256-identical to the npm 18.0.0 tarballs. The populator and the dump are the ones `fixture-18.0` uses: the same showcase app and the same seed actions. Its 18.0.0 shape, read from the dump: - birth marker `{shapeGeneration: 1, platformVersion: "18.0.0"}`; - the tenant column is nullable (`sys_share_link.organization_id varchar(255)`); - 15 COALESCE unique indexes; - `sys_secret` and `sys_flow_credential` carry no `organization_id`. The census covers all 14 reshaped tables. - **The leg** (`upgrade-18x-harness.dogfood.test.ts`, a new `describe`). It runs on a fresh copy of the fixture: 1. the copy is birth-marked at 18.0.0; 2. HEAD's `os migrate plan --json` (the real binary, in the showcase's directory) reports no `destructive` change; 3. HEAD boots it with no ownership refusal and no boot error, the fixture's people sign in, and the fixture's task is served; 4. once HEAD has stopped, every 18.0.0 row of every reshaped table is still there, with its key columns. - **`fixture-18.0` is unchanged.** It stays the pre-release shape, and its legs run as before. ## Why the populator and census changed 18.0.0 differs from the pre-release in two ways the shared populator did not handle: - **The webhook.** 18.0.0 seeds no `sys_webhook` mirror row for a declared webhook. So, when no mirror row exists, the populator switches `showcase_task_changed` on through `POST /security/_activation/webhook/:name`, which is the door the 18.0.0 showcase docblock names. It also authors an organization webhook (`upgrade_harness_task_changed`), so `sys_webhook` still holds the row an 18.0.0 install holds. The pre-release path, which PATCHes the mirror row, is unchanged. - **The census.** `sys_secret` and `sys_flow_credential` are deployment-level at birth in 18.0.0. A new `dropsOrganization` flag on those two entries lets the census leave `organization_id` out of their key columns where the table never had it. Every other missing key column is still refused. There is also a generator fix. Its copy filter (anything whose name starts with `fixture-`) also skipped `fixture-manifest.ts`, so a `--checkout` run used the checkout's own census, not HEAD's. It now skips only the fixture directories. ## Verification - `npx vitest run test/upgrade-18x-harness.dogfood.test.ts`: 12 passed (12), which is the 8 existing `fixture-18.0` cases plus the 4 new ones. - `npx vitest run test/audit-mail-bodies.dogfood.test.ts test/identity-mail-read-gate.dogfood.test.ts`, the other suites that import the populator: 29 passed (29). - Reverse check of the drift assertion: on a copy of the fixture with an undeclared physical column added, HEAD's plan reports `{kind: unmapped_column, category: destructive, op: drop_column}`, so the filter is not vacuous. - `pnpm --filter @objectstack/dogfood typecheck` exits 0. eslint `--no-inline-config` on the changed files exits 0. `check:cross-package-test-inputs`, `check:test-source-alias` and `check:doc-authoring` exit 0. ## Size and release - 7 files, 2,229 insertions and 15 deletions. That is under 3,000 changed lines; the dump and the manifest are 2,061 of them. - Nothing publishes: `@objectstack/dogfood` is private and `scripts/` ships in no package. So there is no changeset, and the PR carries `skip-changeset`. - No ADR, driver or engine file is touched. ## Acceptance notes The published 18.0.0 verification (R1, R1b, R4, and R3 with the ceremony) is reported on #24884 and to the epic seat. Its two documentation findings are release-owned, so they need a docs-only change. They are not fixed here: - the CLI CHANGELOG's 18.0.0 section still carries the superseded "18.0 supports fresh `single` installs only" entry; - `content/docs/upgrading.mdx` has no v18 row and does not mention the ceremony. --- _Generated by [Claude Code](https://claude.ai/code/session_01YaviApKPNAuQLZVQarNfxP)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent e2c2355 commit 81e6edd

7 files changed

Lines changed: 2229 additions & 15 deletions

File tree

‎packages/qa/dogfood/test/upgrade-18x-harness.dogfood.test.ts‎

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ import type { VerifyStack } from '@objectstack/verify';
5555
import { columnsOf, type FixtureManifest } from './upgrade-harness/fixture-manifest.js';
5656
import { FIXTURE_NAMES, FIXTURE_PEOPLE, bootFixture } from './upgrade-harness/populate-fixture.js';
5757
import { loadSqlIntoSqlite } from './upgrade-harness/sqlite-text-dump.js';
58-
import { OWNERSHIP_MARKER, runOwnershipCeremony } from './upgrade-harness/ceremony.js';
58+
import { OWNERSHIP_MARKER, runOwnershipCeremony, runSchemaPlan } from './upgrade-harness/ceremony.js';
5959

6060
const FIXTURE = new URL('./upgrade-harness/fixture-18.0/', import.meta.url);
6161
const MARKER = OWNERSHIP_MARKER;
@@ -293,3 +293,80 @@ describe(`[#24522] an 18.0 database (fixture from ${manifest.ref} @ ${manifest.s
293293
expect(booted.error).toBeUndefined();
294294
}, 900_000);
295295
});
296+
297+
// [#24884] R2 from the GA: a database the published 18.0.0 created
298+
// (`upgrade-harness/fixture-18.0.0/`, generated from the
299+
// `@objectstack/cli@18.0.0` tag) is birth-marked, so it needs no ceremony.
300+
// HEAD's schema plan over it holds no destructive change, HEAD boots it with
301+
// no refusal and serves its people and its task, and once HEAD has stopped
302+
// every 18.0.0 row of every reshaped table is still there with its key
303+
// columns. 18.0.0's own shape is the baseline: a tenant column still nullable,
304+
// and the COALESCE form of a unique index over it. A later step that reshapes
305+
// either owes this database a migration, and this leg is where it shows.
306+
const GA_FIXTURE = new URL('./upgrade-harness/fixture-18.0.0/', import.meta.url);
307+
const gaManifest = JSON.parse(readFileSync(new URL('manifest.json', GA_FIXTURE), 'utf8')) as FixtureManifest;
308+
const gaFixtureSql = readFileSync(new URL('database.sql', GA_FIXTURE), 'utf8');
309+
310+
describe(`[#24884] an 18.0.0 database (fixture from ${gaManifest.ref} @ ${gaManifest.sha.slice(0, 12)}) boots HEAD with no refusal and no destructive drift`, () => {
311+
const dir = mkdtempSync(join(tmpdir(), 'dogfood-upgrade-18-0-0-'));
312+
const file = join(dir, 'ga.db');
313+
let stack: VerifyStack | undefined;
314+
315+
beforeAll(() => {
316+
loadSqlIntoSqlite(gaFixtureSql, file);
317+
});
318+
319+
afterAll(async () => {
320+
await stack?.stop();
321+
rmSync(dir, { recursive: true, force: true });
322+
});
323+
324+
it('the fixture is an 18.0.0 database: birth-marked, and covering every reshaped table', () => {
325+
for (const t of gaManifest.reshaped) expect(t.count, `${t.table} holds rows in the fixture`).toBeGreaterThan(0);
326+
const db = new DatabaseSync(file, { readOnly: true });
327+
try {
328+
const marker = db.prepare('SELECT details FROM sys_migration WHERE id = ?').get(MARKER) as { details?: unknown } | undefined;
329+
expect(JSON.parse(String(marker?.details)).birth).toMatchObject({ shapeGeneration: 1, platformVersion: '18.0.0' });
330+
} finally {
331+
db.close();
332+
}
333+
});
334+
335+
it('HEAD\'s schema plan over it holds no destructive change', () => {
336+
const plan = runSchemaPlan(file);
337+
expect(plan.changes.filter((c) => c.category === 'destructive'), 'destructive drift').toEqual([]);
338+
}, 320_000);
339+
340+
it('HEAD boots it with no refusal, and serves the fixture\'s people and task', async () => {
341+
const booted = await tryBoot(file);
342+
stack = booted.stack;
343+
expect(refusalIn(booted.error), String((booted.error as Error | undefined)?.message)).toBeUndefined();
344+
expect(booted.error, String((booted.error as Error | undefined)?.stack ?? booted.error)).toBeUndefined();
345+
const adminTok = await stack!.signIn(FIXTURE_PEOPLE.admin.email, FIXTURE_PEOPLE.admin.password);
346+
expect(await stack!.signIn(FIXTURE_PEOPLE.memberA.email, FIXTURE_PEOPLE.memberA.password)).toBeTruthy();
347+
const res = await stack!.apiAs(adminTok, 'GET', '/data/showcase_task');
348+
expect(res.status).toBe(200);
349+
expect(rowsOf(await res.json()).map((r) => String(r?.title))).toContain(`${FIXTURE_NAMES.task} — edited`);
350+
}, 240_000);
351+
352+
it('every 18.0.0 row of every reshaped table is still there, with its key columns', async () => {
353+
await stack?.stop();
354+
stack = undefined;
355+
const db = new DatabaseSync(file, { readOnly: true });
356+
try {
357+
for (const t of gaManifest.reshaped) {
358+
for (const fixtureRow of t.rows) {
359+
const id = String(fixtureRow.id);
360+
const now = t.physical
361+
.map((physical) => db.prepare(`SELECT * FROM "${physical}" WHERE id = ?`).get(id) as Record<string, unknown> | undefined)
362+
.find(Boolean);
363+
expect(now, `${t.table} row ${id} is kept`).toBeDefined();
364+
const kept = Object.fromEntries(t.keyColumns.map((c) => [c, now![c] === null || now![c] === undefined ? null : String(now![c])]));
365+
expect(kept, `${t.table} row ${id}`).toEqual(fixtureRow);
366+
}
367+
}
368+
} finally {
369+
db.close();
370+
}
371+
});
372+
});

‎packages/qa/dogfood/test/upgrade-harness/ceremony.ts‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,30 @@ export function runOwnershipCeremony(dir: string, file: string, tag: string): Re
4141
return JSON.parse(readFileSync(reportFile, 'utf8')) as Record<string, unknown>;
4242
}
4343

44+
/** One change `os migrate plan --json` reports: the managed drift between the database and HEAD's objects. */
45+
export interface SchemaPlanChange {
46+
table?: string;
47+
category: 'safe' | 'needs_confirm' | 'destructive';
48+
[key: string]: unknown;
49+
}
50+
51+
/**
52+
* `os migrate plan --json` on `file`, the real binary in the showcase's
53+
* directory, as an operator reads the drift before serving a database. Writes
54+
* no row or table. The parsed document; throws, with the run's output, when it
55+
* exits non-zero.
56+
*/
57+
export function runSchemaPlan(file: string): { total: number; changes: SchemaPlanChange[]; pending: unknown[] } {
58+
const plan = spawnSync(process.execPath, [CLI_BIN, 'migrate', 'plan', '--json', '--database-url', `file:${file}`], {
59+
cwd: SHOWCASE_DIR,
60+
encoding: 'utf8',
61+
timeout: 300_000,
62+
env: { ...process.env, NO_COLOR: '1' },
63+
});
64+
if (plan.status !== 0) throw new Error(`os migrate plan exited ${plan.status}:\n${plan.stdout}\n${plan.stderr}`);
65+
return JSON.parse(plan.stdout);
66+
}
67+
4468
/**
4569
* A copy of the 18.0 fixture the ceremony has certified, at `dir/<tag>.db`,
4670
* ready to boot under the guard — reached the way an operator reaches it.

0 commit comments

Comments
 (0)