Skip to content

Commit edb2db1

Browse files
committed
test(dogfood): the upgrade ceremony over the 18.0 fixture is a shared helper (upgrade-harness/ceremony.ts)
Every suite that boots the 18.0 fixture under the ownership boot guard needs it: the guard refuses an 18.0 database until the ceremony has certified it. Claude-Session: https://claude.ai/code/session_014UATXWqAXqkaZkbG92ox6V Co-authored-by: Claude <noreply@anthropic.com>
1 parent d28f4cf commit edb2db1

2 files changed

Lines changed: 60 additions & 23 deletions

File tree

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

Lines changed: 5 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,9 @@
4444
// ⛔ Additive change passes: a row HEAD's boot ADDS (an activity shard, a
4545
// seeder's row) is not a failure, so counts are compared as "at least", and
4646
// every fixture row is looked up by id.
47-
import { spawnSync } from 'node:child_process';
4847
import { copyFileSync, mkdtempSync, readFileSync, rmSync } from 'node:fs';
49-
import { createRequire } from 'node:module';
5048
import { tmpdir } from 'node:os';
51-
import { dirname, join } from 'node:path';
52-
import { fileURLToPath } from 'node:url';
49+
import { join } from 'node:path';
5350
import { DatabaseSync } from 'node:sqlite';
5451
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
5552
import { isOrganizationOwnershipBootRefusal } from '@objectstack/runtime';
@@ -58,12 +55,10 @@ import type { VerifyStack } from '@objectstack/verify';
5855
import { columnsOf, type FixtureManifest } from './upgrade-harness/fixture-manifest.js';
5956
import { FIXTURE_NAMES, FIXTURE_PEOPLE, bootFixture } from './upgrade-harness/populate-fixture.js';
6057
import { loadSqlIntoSqlite } from './upgrade-harness/sqlite-text-dump.js';
58+
import { OWNERSHIP_MARKER, runOwnershipCeremony } from './upgrade-harness/ceremony.js';
6159

6260
const FIXTURE = new URL('./upgrade-harness/fixture-18.0/', import.meta.url);
63-
const SHOWCASE_DIR = fileURLToPath(new URL('../../../../examples/app-showcase/', import.meta.url));
64-
const CLI_ROOT = dirname(createRequire(import.meta.url).resolve('@objectstack/cli/package.json'));
65-
const CLI_BIN = join(CLI_ROOT, 'bin', 'run.js');
66-
const MARKER = 'adr-0131-organization-ownership';
61+
const MARKER = OWNERSHIP_MARKER;
6762
const manifest = JSON.parse(readFileSync(new URL('manifest.json', FIXTURE), 'utf8')) as FixtureManifest;
6863
const fixtureSql = readFileSync(new URL('database.sql', FIXTURE), 'utf8');
6964

@@ -84,22 +79,9 @@ async function tryBoot(file: string): Promise<{ stack?: VerifyStack; error?: unk
8479
}
8580
}
8681

87-
/**
88-
* The ceremony on `file`, as an operator runs it: the plan, then the apply of
89-
* that reviewed plan file — the real binary, in the showcase's directory, so it
90-
* composes what the deployment serves. The apply report.
91-
*/
82+
/** The ceremony on `file`, as an operator runs it (`runOwnershipCeremony`). */
9283
function ceremony(dir: string, file: string, tag: string): Record<string, unknown> {
93-
const run = (args: string[]) => spawnSync(process.execPath, [
94-
CLI_BIN, 'migrate', 'organization-ownership', '--database-url', `file:${file}`, ...args,
95-
], { cwd: SHOWCASE_DIR, encoding: 'utf8', timeout: 300_000, env: { ...process.env, NO_COLOR: '1' } });
96-
const planFile = join(dir, `${tag}-plan.json`);
97-
const plan = run(['--out', planFile]);
98-
expect(plan.status, `the plan: ${plan.stdout}\n${plan.stderr}`).toBe(0);
99-
const reportFile = join(dir, `${tag}-apply.json`);
100-
const apply = run(['--apply', '--plan-file', planFile, '--backup-taken', '--out', reportFile, '--json']);
101-
expect(apply.status, `the apply: ${apply.stdout}\n${apply.stderr}`).toBe(0);
102-
return JSON.parse(readFileSync(reportFile, 'utf8')) as Record<string, unknown>;
84+
return runOwnershipCeremony(dir, file, tag);
10385
}
10486

10587
/** The ceremony's marker on `file`: verified, blocking 0, at this release's generation. */
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
//
3+
// [18.x step 6] The upgrade ceremony over a copy of the 18.0 fixture, as an
4+
// operator runs it — the plan, then the apply of that reviewed plan file,
5+
// through the real `os migrate organization-ownership` binary in the
6+
// showcase's directory, so it composes what the deployment serves. Shared by
7+
// the upgrade harness and every suite that boots the 18.0 fixture under the
8+
// ownership boot guard: the guard refuses an 18.0 database until the ceremony
9+
// has certified it.
10+
import { spawnSync } from 'node:child_process';
11+
import { copyFileSync, readFileSync } from 'node:fs';
12+
import { createRequire } from 'node:module';
13+
import { dirname, join } from 'node:path';
14+
import { fileURLToPath } from 'node:url';
15+
import { loadSqlIntoSqlite } from './sqlite-text-dump.js';
16+
17+
const SHOWCASE_DIR = fileURLToPath(new URL('../../../../../examples/app-showcase/', import.meta.url));
18+
const CLI_ROOT = dirname(createRequire(import.meta.url).resolve('@objectstack/cli/package.json'));
19+
const CLI_BIN = join(CLI_ROOT, 'bin', 'run.js');
20+
21+
/** The ceremony's sys_migration row: its marker. */
22+
export const OWNERSHIP_MARKER = 'adr-0131-organization-ownership';
23+
24+
/**
25+
* Run the ceremony on `file`: the plan into `dir/<tag>-plan.json`, then
26+
* `--apply --plan-file … --backup-taken`. The apply report. Throws, with both
27+
* runs' output, when either exits non-zero.
28+
*/
29+
export function runOwnershipCeremony(dir: string, file: string, tag: string): Record<string, unknown> {
30+
const run = (args: string[]) => spawnSync(process.execPath, [
31+
CLI_BIN, 'migrate', 'organization-ownership', '--database-url', `file:${file}`, ...args,
32+
], { cwd: SHOWCASE_DIR, encoding: 'utf8', timeout: 300_000, env: { ...process.env, NO_COLOR: '1' } });
33+
const planFile = join(dir, `${tag}-plan.json`);
34+
const plan = run(['--out', planFile]);
35+
if (plan.status !== 0) throw new Error(`the ceremony's plan exited ${plan.status}:\n${plan.stdout}\n${plan.stderr}`);
36+
const reportFile = join(dir, `${tag}-apply.json`);
37+
const apply = run(['--apply', '--plan-file', planFile, '--backup-taken', '--out', reportFile, '--json']);
38+
if (apply.status !== 0) throw new Error(`the ceremony's apply exited ${apply.status}:\n${apply.stdout}\n${apply.stderr}`);
39+
return JSON.parse(readFileSync(reportFile, 'utf8')) as Record<string, unknown>;
40+
}
41+
42+
/**
43+
* A copy of the 18.0 fixture the ceremony has certified, at `dir/<tag>.db`,
44+
* ready to boot under the guard. The ceremony runs on a working copy and the
45+
* certified bytes are copied out, so no boot ever opens the path the
46+
* ceremony's process held (the stale-image hazard the harness measured).
47+
*/
48+
export function ceremoniedFixture(dir: string, fixtureSql: string, tag: string): { file: string; report: Record<string, unknown> } {
49+
const work = join(dir, `${tag}.ceremony.db`);
50+
loadSqlIntoSqlite(fixtureSql, work);
51+
const report = runOwnershipCeremony(dir, work, tag);
52+
const file = join(dir, `${tag}.db`);
53+
copyFileSync(work, file);
54+
return { file, report };
55+
}

0 commit comments

Comments
 (0)