Skip to content

Commit ff5edde

Browse files
kriszypclaude
andcommitted
test: expand v4→v5 upgrade matrix with clustering config and hdb_status sub-cases
Closes #1187. - Adds version matrix comment documenting HARPER_LEGACY_V43_PATH through V47_PATH env vars so CI can run the same suite against each v4 minor. - Adds hdb_status GTM table backward-compat suite: seeds a record via v4, upgrades to v5, verifies the record is readable (checks both data and system databases in case v5 migrated the table). - Adds v4.3.x clustering: config key suite: starts v4.3, seeds data, appends the old-style `clustering:` YAML block directly to the on-disk config file (bypassing HARPER_SET_CONFIG, which predates v4.3), then upgrades to v5 and asserts no crash and data survival. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 88c94e6 commit ff5edde

1 file changed

Lines changed: 222 additions & 4 deletions

File tree

integrationTests/upgrade/4.x-upgrade.test.ts

Lines changed: 222 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,27 @@
11
/**
2-
* This tests that transaction log replay works on crash. There is a bunch of data written to the system
3-
* database, so replay needs to work for harper to startup.
2+
* Upgrade compatibility tests: v4.x → v5.
3+
*
4+
* Tests transaction log replay on crash (data written to system DB before kill),
5+
* downgrade-and-re-upgrade round-trips, LMDB→RocksDB migration, and v4-specific
6+
* backward-compat sub-cases (hdb_status GTM table, clustering: config key).
7+
*
8+
* ## Version matrix
9+
*
10+
* The suite is parameterized by env vars pointing at legacy Harper installations:
11+
*
12+
* HARPER_LEGACY_VERSION_PATH — primary; maps to "the v4.x build under test" (any minor)
13+
* HARPER_LEGACY_V43_PATH — v4.3.x-specific build (for version-gated sub-tests)
14+
* HARPER_LEGACY_V44_PATH — v4.4.x-specific build
15+
* HARPER_LEGACY_V45_PATH — v4.5.x-specific build
16+
* HARPER_LEGACY_V46_PATH — v4.6.x-specific build
17+
* HARPER_LEGACY_V47_PATH — v4.7.x-specific build
18+
*
19+
* To run the matrix in CI, launch once per env var, e.g.:
20+
* HARPER_LEGACY_VERSION_PATH=/opt/harper-4.3 npm run test:integration -- upgrade
21+
* HARPER_LEGACY_VERSION_PATH=/opt/harper-4.7 npm run test:integration -- upgrade
22+
*
23+
* Sub-tests that require a specific minor may use the version-specific var instead
24+
* of the generic one, and skip when that var is absent.
425
*/
526
import { suite, test, before, after } from 'node:test';
627
import {
@@ -10,9 +31,9 @@ import {
1031
type ContextWithHarper,
1132
killHarper,
1233
} from '@harperfast/integration-testing';
13-
import { ok, deepStrictEqual } from 'node:assert';
34+
import { ok, deepStrictEqual, strictEqual } from 'node:assert';
1435
import { join } from 'node:path';
15-
import { existsSync, readdirSync, statSync } from 'node:fs';
36+
import { existsSync, readdirSync, statSync, writeFileSync } from 'node:fs';
1637

1738
const WIDGET_COUNT = 60;
1839
const buildWidgets = () =>
@@ -254,3 +275,200 @@ suite(
254275
});
255276
}
256277
);
278+
279+
// ---------------------------------------------------------------------------
280+
// hdb_status GTM table backward-compat
281+
//
282+
// Verifies that a record written to data.hdb_status in v4 is still readable
283+
// via the v5 REST API after upgrade. Uses HARPER_LEGACY_VERSION_PATH (same
284+
// as the primary suite) and is skipped when it is absent.
285+
//
286+
// Note: hdb_status is a system-managed table; not all v4 minor versions expose
287+
// it through the public upsert API. The before() hook skips the upsert when the
288+
// table is absent (table-not-found error), and the test is skipped in that case.
289+
// ---------------------------------------------------------------------------
290+
291+
let hdbStatusSeeded = false;
292+
293+
suite(
294+
'v4->v5: hdb_status GTM table backward-compat',
295+
{ skip: !legacyPath || testsBun || process.platform === 'win32' },
296+
(ctx: ContextWithHarper) => {
297+
before(async () => {
298+
await startHarper(ctx, {
299+
config: {},
300+
env: {
301+
TC_AGREEMENT: 'yes',
302+
REPLICATION_HOSTNAME: 'localhost',
303+
},
304+
harperBinPath: join(legacyPath!, 'bin', 'harperdb.js'),
305+
});
306+
307+
// Write a sentinel record to data.hdb_status in v4.
308+
// Not all v4 builds expose hdb_status via the operations API; if upsert
309+
// fails with a table-not-found-style error we note it and skip assertion below.
310+
try {
311+
await sendOperation(ctx.harper, {
312+
operation: 'upsert',
313+
database: 'data',
314+
table: 'hdb_status',
315+
records: [{ id: 1, status: 200, message: 'ok' }],
316+
});
317+
hdbStatusSeeded = true;
318+
} catch (err: any) {
319+
// Table absent on this v4 minor — acceptable; the test will skip.
320+
if (
321+
!String(err?.message ?? err)
322+
.toLowerCase()
323+
.includes('not found')
324+
)
325+
throw err;
326+
}
327+
});
328+
329+
after(async () => {
330+
await teardownHarper(ctx);
331+
});
332+
333+
test('hdb_status record is readable after upgrade to v5', async (t) => {
334+
if (!hdbStatusSeeded) {
335+
t.skip('hdb_status table not available via operations API on this v4 build');
336+
return;
337+
}
338+
339+
await killHarper(ctx);
340+
341+
// Start v5 on the same dataRootDir — upgrade directives run automatically.
342+
await startHarper(ctx, { config: {}, env: {} });
343+
344+
// In v4, hdb_status lived in the data database. v5 may migrate it to system.
345+
// Try data first; fall back to system so the test covers both locations.
346+
let rows: any[] | null = null;
347+
try {
348+
rows = await sendOperation(ctx.harper, {
349+
operation: 'search_by_conditions',
350+
database: 'data',
351+
table: 'hdb_status',
352+
conditions: [{ attribute: 'id', comparator: 'equals', value: 1 }],
353+
});
354+
} catch {
355+
// data.hdb_status may have been migrated to system
356+
}
357+
358+
if (!rows || rows.length === 0) {
359+
rows = await sendOperation(ctx.harper, {
360+
operation: 'search_by_conditions',
361+
database: 'system',
362+
table: 'hdb_status',
363+
conditions: [{ attribute: 'id', comparator: 'equals', value: 1 }],
364+
});
365+
}
366+
367+
ok(
368+
Array.isArray(rows) && rows.length === 1,
369+
`expected 1 hdb_status row in data or system after upgrade, got ${JSON.stringify(rows)}`
370+
);
371+
strictEqual(rows![0].status, 200, 'hdb_status.status should be 200 after upgrade');
372+
strictEqual(rows![0].message, 'ok', 'hdb_status.message should be "ok" after upgrade');
373+
});
374+
}
375+
);
376+
377+
// ---------------------------------------------------------------------------
378+
// v4.3.x clustering: config key backward-compat
379+
//
380+
// v4.3.x used `clustering: { enabled: true, ... }` in harper.json. v5 renamed
381+
// this key. This test verifies that v5 either migrates the old key gracefully
382+
// or emits an actionable error — it must not crash silently.
383+
//
384+
// Requires HARPER_LEGACY_V43_PATH to point at a v4.3.x installation. Skipped
385+
// when the env var is absent so the CI matrix can omit the v4.3 slot without
386+
// breaking the suite.
387+
// ---------------------------------------------------------------------------
388+
389+
const legacyV43Path = process.env.HARPER_LEGACY_V43_PATH;
390+
391+
suite(
392+
'v4.3.x→v5: clustering: config key does not cause silent failure',
393+
{ skip: !legacyV43Path || testsBun || process.platform === 'win32' },
394+
(ctx: ContextWithHarper) => {
395+
before(async () => {
396+
// Start v4.3.x without any special config — HARPER_SET_CONFIG is a v5
397+
// mechanism that v4.3 predates, so the clustering key must be injected
398+
// by writing directly to the on-disk config file after the data dir is
399+
// created. The first startHarper call just populates ctx.harper.dataRootDir.
400+
await startHarper(ctx, {
401+
config: {},
402+
env: {
403+
TC_AGREEMENT: 'yes',
404+
REPLICATION_HOSTNAME: 'localhost',
405+
},
406+
harperBinPath: join(legacyV43Path!, 'bin', 'harperdb.js'),
407+
});
408+
409+
// Seed a small table so we have something to verify survives.
410+
await sendOperation(ctx.harper, {
411+
operation: 'create_table',
412+
table: 'cluster_compat_test',
413+
primary_key: 'id',
414+
attributes: [{ name: 'id', type: 'ID' }],
415+
});
416+
await sendOperation(ctx.harper, {
417+
operation: 'upsert',
418+
table: 'cluster_compat_test',
419+
records: [{ id: 'sentinel' }],
420+
});
421+
422+
// Write the old-style clustering: config key directly into the on-disk
423+
// config file so that v5 reads it on startup (not v4 — v4 is already up).
424+
// v4.3 used harperdb-config.yaml; fall back to harper-config.yaml if absent.
425+
const legacyConfigPath = join(ctx.harper.dataRootDir, 'harperdb-config.yaml');
426+
const newConfigPath = join(ctx.harper.dataRootDir, 'harper-config.yaml');
427+
const configPath = existsSync(legacyConfigPath) ? legacyConfigPath : newConfigPath;
428+
429+
// Append the old-style clustering block to the existing config file.
430+
// YAML block append: a trailing newline then a top-level clustering key.
431+
const clusteringYaml = [
432+
'',
433+
'# v4.3 legacy clustering key — injected by upgrade compat test',
434+
'clustering:',
435+
' enabled: true',
436+
' nodeName: test-node',
437+
' server:',
438+
' port: 12345',
439+
'',
440+
].join('\n');
441+
writeFileSync(configPath, clusteringYaml, { flag: 'a' });
442+
});
443+
444+
after(async () => {
445+
await teardownHarper(ctx);
446+
});
447+
448+
test('v5 starts successfully despite old clustering: config key', async () => {
449+
await killHarper(ctx);
450+
451+
// startHarper throws HarperStartupError on non-zero exit or timeout.
452+
// A successful return here means Harper started and is ready — no crash.
453+
await startHarper(ctx, { config: {}, env: {} });
454+
455+
ok(
456+
ctx.harper.process.exitCode === null,
457+
'v5 Harper process must still be running after startup with old clustering config'
458+
);
459+
460+
// Optionally verify seeded data survived: checks data integrity not just
461+
// startup, but a silent partial-boot (ready signal before tables open)
462+
// would be caught here.
463+
const rows = await sendOperation(ctx.harper, {
464+
operation: 'search_by_conditions',
465+
table: 'cluster_compat_test',
466+
conditions: [{ attribute: 'id', comparator: 'equals', value: 'sentinel' }],
467+
});
468+
ok(
469+
Array.isArray(rows) && rows.length === 1,
470+
`expected sentinel row to survive upgrade, got ${JSON.stringify(rows)}`
471+
);
472+
});
473+
}
474+
);

0 commit comments

Comments
 (0)