@vellumai/assistant 0.10.1-dev.202606240723.ea25efe → 0.10.1-dev.202606241108.5d35d48
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/__tests__/db-migration-rollback.test.ts +205 -171
- package/src/__tests__/migration-import-from-url.test.ts +2 -2
- package/src/memory/db-init.ts +8 -493
- package/src/memory/migrations/__tests__/run-migrations.test.ts +2 -2
- package/src/memory/migrations/run-migrations.ts +90 -6
- package/src/memory/migrations/validate-migration-state.ts +101 -66
- package/src/memory/steps.ts +569 -0
- package/src/runtime/routes/identity-routes.ts +3 -2
- package/src/runtime/routes/migration-rollback-routes.ts +4 -3
- package/src/runtime/routes/migration-routes.ts +2 -1
- package/src/memory/migrations/registry.ts +0 -573
package/package.json
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
import { createHash } from "node:crypto";
|
|
14
14
|
import { Database } from "bun:sqlite";
|
|
15
|
-
import {
|
|
15
|
+
import { describe, expect, test } from "bun:test";
|
|
16
16
|
|
|
17
17
|
import { drizzle } from "drizzle-orm/bun-sqlite";
|
|
18
18
|
|
|
@@ -69,16 +69,16 @@ import { migrateRenameThreadStartersCheckpointsDown } from "../memory/migrations
|
|
|
69
69
|
import { migrateBackfillAudioAttachmentMimeTypesDown } from "../memory/migrations/191-backfill-audio-attachment-mime-types.js";
|
|
70
70
|
import { migrateLlmUsageAttribution } from "../memory/migrations/235-llm-usage-attribution.js";
|
|
71
71
|
import {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
} from "../memory/migrations/registry.js";
|
|
76
|
-
import { runMigrationSteps } from "../memory/migrations/run-migrations.js";
|
|
72
|
+
type MigrationStep,
|
|
73
|
+
runMigrationSteps,
|
|
74
|
+
} from "../memory/migrations/run-migrations.js";
|
|
77
75
|
import {
|
|
76
|
+
type MigrationValidationResult,
|
|
78
77
|
rollbackMemoryMigration,
|
|
79
78
|
validateMigrationState,
|
|
80
79
|
} from "../memory/migrations/validate-migration-state.js";
|
|
81
80
|
import * as schema from "../memory/schema.js";
|
|
81
|
+
import { migrationSteps } from "../memory/steps.js";
|
|
82
82
|
|
|
83
83
|
// ---------------------------------------------------------------------------
|
|
84
84
|
// Helpers
|
|
@@ -547,7 +547,7 @@ describe("schema-drift recovery: migration handles unexpected schema state", ()
|
|
|
547
547
|
// structured diagnostic data. Assert directly on the returned result rather
|
|
548
548
|
// than re-deriving the crashed list from the raw DB — this verifies the
|
|
549
549
|
// function itself detects the crash, not just that the data is present.
|
|
550
|
-
const result: MigrationValidationResult = validateMigrationState(db);
|
|
550
|
+
const result: MigrationValidationResult = validateMigrationState(db, migrationSteps);
|
|
551
551
|
expect(result.crashed).toContain("step:migrateJobDeferrals");
|
|
552
552
|
expect(result.crashed).not.toContain(
|
|
553
553
|
"step:migrateMemoryEntityRelationDedup",
|
|
@@ -575,22 +575,24 @@ describe("schema-drift recovery: migration handles unexpected schema state", ()
|
|
|
575
575
|
|
|
576
576
|
// validateMigrationState throws an IntegrityError on dependency violations
|
|
577
577
|
// to block daemon startup with an inconsistent schema.
|
|
578
|
-
expect(() => validateMigrationState(db)).toThrow(
|
|
578
|
+
expect(() => validateMigrationState(db, migrationSteps)).toThrow(
|
|
579
579
|
"Migration dependency violations detected",
|
|
580
580
|
);
|
|
581
|
-
expect(() => validateMigrationState(db)).toThrow(
|
|
582
|
-
"
|
|
581
|
+
expect(() => validateMigrationState(db, migrationSteps)).toThrow(
|
|
582
|
+
"migrateMemoryItemsFingerprintScopeUnique",
|
|
583
583
|
);
|
|
584
584
|
|
|
585
|
-
// Sanity-check: confirm the
|
|
586
|
-
// violation detection is grounded in real schema intent.
|
|
587
|
-
const
|
|
588
|
-
(
|
|
589
|
-
);
|
|
590
|
-
expect(saltedEntry).toBeTruthy();
|
|
591
|
-
expect(saltedEntry!.dependsOn).toContain(
|
|
592
|
-
"migration_memory_items_fingerprint_scope_unique_v1",
|
|
585
|
+
// Sanity-check: confirm the steps list also declares this dependency, so
|
|
586
|
+
// the violation detection is grounded in real schema intent.
|
|
587
|
+
const saltedStep = migrationSteps.find(
|
|
588
|
+
(s) => typeof s !== "function" && s.name === "migrateMemoryItemsScopeSaltedFingerprints",
|
|
593
589
|
);
|
|
590
|
+
expect(saltedStep).toBeTruthy();
|
|
591
|
+
if (saltedStep && typeof saltedStep !== "function") {
|
|
592
|
+
expect(saltedStep.dependsOn).toContain(
|
|
593
|
+
"migrateMemoryItemsFingerprintScopeUnique",
|
|
594
|
+
);
|
|
595
|
+
}
|
|
594
596
|
});
|
|
595
597
|
|
|
596
598
|
test("validateMigrationState: no checkpoints table is handled gracefully", () => {
|
|
@@ -599,7 +601,7 @@ describe("schema-drift recovery: migration handles unexpected schema state", ()
|
|
|
599
601
|
const db = createTestDb();
|
|
600
602
|
// Deliberately do NOT create memory_checkpoints.
|
|
601
603
|
|
|
602
|
-
expect(() => validateMigrationState(db)).not.toThrow();
|
|
604
|
+
expect(() => validateMigrationState(db, migrationSteps)).not.toThrow();
|
|
603
605
|
});
|
|
604
606
|
|
|
605
607
|
test("migrateMemoryItemsFingerprintScopeUnique: old schema with UNIQUE on fingerprint is migrated", () => {
|
|
@@ -859,26 +861,40 @@ describe("schema-drift recovery: migration handles unexpected schema state", ()
|
|
|
859
861
|
}
|
|
860
862
|
});
|
|
861
863
|
|
|
862
|
-
test("
|
|
863
|
-
//
|
|
864
|
-
// than the previous
|
|
865
|
-
//
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
864
|
+
test("migrationSteps: rollback versions are strictly monotonically increasing", () => {
|
|
865
|
+
// Steps ordering invariant: every rollback entry's version must be strictly
|
|
866
|
+
// greater than the previous rollback entry across the full steps list. A
|
|
867
|
+
// violation here would mean the rollback ordering guarantees documented in
|
|
868
|
+
// the migration comments cannot be relied upon.
|
|
869
|
+
const rollbackVersions: number[] = [];
|
|
870
|
+
for (const step of migrationSteps) {
|
|
871
|
+
if (typeof step === "function") continue;
|
|
872
|
+
if (!step.rollback) continue;
|
|
873
|
+
for (const entry of step.rollback) {
|
|
874
|
+
rollbackVersions.push(entry.version);
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
rollbackVersions.sort((a, b) => a - b);
|
|
878
|
+
for (let i = 1; i < rollbackVersions.length; i++) {
|
|
879
|
+
const prev = rollbackVersions[i - 1];
|
|
880
|
+
const curr = rollbackVersions[i];
|
|
881
|
+
expect(curr).toBeGreaterThan(prev);
|
|
870
882
|
}
|
|
871
883
|
});
|
|
872
884
|
|
|
873
|
-
test("
|
|
874
|
-
// Schema drift guard: if a migration declares a dependency on a
|
|
875
|
-
// doesn't exist in the
|
|
876
|
-
// can never be satisfied. This test ensures all
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
885
|
+
test("migrationSteps: all dependsOn references point to existing step names", () => {
|
|
886
|
+
// Schema drift guard: if a migration declares a dependency on a step name
|
|
887
|
+
// that doesn't exist in the steps list, the dependency check in
|
|
888
|
+
// validateMigrationState can never be satisfied. This test ensures all
|
|
889
|
+
// declared dependencies are valid.
|
|
890
|
+
const allNames = new Set(
|
|
891
|
+
migrationSteps.map((s) => (typeof s === "function" ? s.name : s.name)),
|
|
892
|
+
);
|
|
893
|
+
for (const step of migrationSteps) {
|
|
894
|
+
if (typeof step === "function") continue;
|
|
895
|
+
if (!step.dependsOn) continue;
|
|
896
|
+
for (const dep of step.dependsOn) {
|
|
897
|
+
expect(allNames.has(dep)).toBe(true);
|
|
882
898
|
}
|
|
883
899
|
}
|
|
884
900
|
});
|
|
@@ -938,26 +954,7 @@ describe("schema-drift recovery: migration handles unexpected schema state", ()
|
|
|
938
954
|
// ---------------------------------------------------------------------------
|
|
939
955
|
|
|
940
956
|
describe("rollbackMemoryMigration", () => {
|
|
941
|
-
// Track test entries pushed onto MIGRATION_REGISTRY so we can restore after
|
|
942
|
-
// each test. This avoids polluting the real registry across test runs.
|
|
943
|
-
let registrySnapshot: MigrationRegistryEntry[];
|
|
944
|
-
|
|
945
|
-
function saveRegistry() {
|
|
946
|
-
registrySnapshot = [...MIGRATION_REGISTRY];
|
|
947
|
-
}
|
|
948
|
-
|
|
949
|
-
function restoreRegistry() {
|
|
950
|
-
MIGRATION_REGISTRY.length = 0;
|
|
951
|
-
MIGRATION_REGISTRY.push(...registrySnapshot);
|
|
952
|
-
}
|
|
953
|
-
|
|
954
|
-
afterEach(() => {
|
|
955
|
-
restoreRegistry();
|
|
956
|
-
});
|
|
957
|
-
|
|
958
957
|
test("rolls back checkpoint-tracked migrations in reverse version order", () => {
|
|
959
|
-
saveRegistry();
|
|
960
|
-
|
|
961
958
|
const db = createTestDb();
|
|
962
959
|
const raw = getRaw(db);
|
|
963
960
|
bootstrapCheckpointsTable(raw);
|
|
@@ -968,47 +965,58 @@ describe("rollbackMemoryMigration", () => {
|
|
|
968
965
|
const now = Date.now();
|
|
969
966
|
|
|
970
967
|
// Use very high version numbers to avoid colliding with real registry entries.
|
|
971
|
-
const
|
|
968
|
+
const testSteps: MigrationStep[] = [
|
|
972
969
|
{
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
970
|
+
name: "test_rollback_v1000",
|
|
971
|
+
run: () => {},
|
|
972
|
+
rollback: [
|
|
973
|
+
{
|
|
974
|
+
version: 1000,
|
|
975
|
+
description: "test migration v1000",
|
|
976
|
+
down: () => {
|
|
977
|
+
downCalls.push("test_rollback_v1000");
|
|
978
|
+
},
|
|
979
|
+
},
|
|
980
|
+
],
|
|
980
981
|
},
|
|
981
982
|
{
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
983
|
+
name: "test_rollback_v1001",
|
|
984
|
+
run: () => {},
|
|
985
|
+
rollback: [
|
|
986
|
+
{
|
|
987
|
+
version: 1001,
|
|
988
|
+
description: "test migration v1001",
|
|
989
|
+
down: () => {
|
|
990
|
+
downCalls.push("test_rollback_v1001");
|
|
991
|
+
},
|
|
992
|
+
},
|
|
993
|
+
],
|
|
989
994
|
},
|
|
990
995
|
{
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
996
|
+
name: "test_rollback_v1002",
|
|
997
|
+
run: () => {},
|
|
998
|
+
rollback: [
|
|
999
|
+
{
|
|
1000
|
+
version: 1002,
|
|
1001
|
+
description: "test migration v1002",
|
|
1002
|
+
down: () => {
|
|
1003
|
+
downCalls.push("test_rollback_v1002");
|
|
1004
|
+
},
|
|
1005
|
+
},
|
|
1006
|
+
],
|
|
998
1007
|
},
|
|
999
1008
|
];
|
|
1000
1009
|
|
|
1001
|
-
MIGRATION_REGISTRY.push(...testEntries);
|
|
1002
|
-
|
|
1003
1010
|
// Simulate all three migrations as completed via their step checkpoints.
|
|
1004
|
-
for (const entry of
|
|
1011
|
+
for (const entry of testSteps) {
|
|
1012
|
+
if (typeof entry === "function") continue;
|
|
1005
1013
|
raw.exec(
|
|
1006
|
-
`INSERT INTO memory_checkpoints (key, value, updated_at) VALUES ('step:${entry.
|
|
1014
|
+
`INSERT INTO memory_checkpoints (key, value, updated_at) VALUES ('step:${entry.name}', '1', ${now})`,
|
|
1007
1015
|
);
|
|
1008
1016
|
}
|
|
1009
1017
|
|
|
1010
1018
|
// Roll back to version 1000 — should roll back v1002 and v1001 (version > 1000).
|
|
1011
|
-
const rolledBack = rollbackMemoryMigration(db, 1000);
|
|
1019
|
+
const rolledBack = rollbackMemoryMigration(db, 1000, testSteps);
|
|
1012
1020
|
|
|
1013
1021
|
// Verify returned keys.
|
|
1014
1022
|
expect(rolledBack).toEqual(["test_rollback_v1002", "test_rollback_v1001"]);
|
|
@@ -1048,8 +1056,6 @@ describe("rollbackMemoryMigration", () => {
|
|
|
1048
1056
|
* must clear the step checkpoint too, otherwise the next upgrade skips the
|
|
1049
1057
|
* step and never restores the rolled-back schema.
|
|
1050
1058
|
*/
|
|
1051
|
-
saveRegistry();
|
|
1052
|
-
|
|
1053
1059
|
const db = createTestDb();
|
|
1054
1060
|
const raw = getRaw(db);
|
|
1055
1061
|
bootstrapCheckpointsTable(raw);
|
|
@@ -1063,7 +1069,7 @@ describe("rollbackMemoryMigration", () => {
|
|
|
1063
1069
|
|
|
1064
1070
|
// GIVEN a registry-backed migration whose forward body is a named step that
|
|
1065
1071
|
// creates a table. The step runner records `step:<functionName>` after a
|
|
1066
|
-
// successful run; the
|
|
1072
|
+
// successful run; the step's rollback entry maps to that key so rollback
|
|
1067
1073
|
// can find and clear it.
|
|
1068
1074
|
function migrateTestStepRollback(database: DrizzleDb): void {
|
|
1069
1075
|
getSqliteFrom(database).exec(
|
|
@@ -1071,18 +1077,25 @@ describe("rollbackMemoryMigration", () => {
|
|
|
1071
1077
|
);
|
|
1072
1078
|
}
|
|
1073
1079
|
|
|
1074
|
-
//
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1080
|
+
// The test step carries rollback metadata so rollbackMemoryMigration can
|
|
1081
|
+
// find it in the steps list.
|
|
1082
|
+
const testSteps: MigrationStep[] = [
|
|
1083
|
+
{
|
|
1084
|
+
name: "migrateTestStepRollback",
|
|
1085
|
+
run: migrateTestStepRollback,
|
|
1086
|
+
rollback: [
|
|
1087
|
+
{
|
|
1088
|
+
version: 4000,
|
|
1089
|
+
description: "test step rollback",
|
|
1090
|
+
down: (database) => {
|
|
1091
|
+
getSqliteFrom(database).exec(
|
|
1092
|
+
`DROP TABLE IF EXISTS test_step_rollback_data`,
|
|
1093
|
+
);
|
|
1094
|
+
},
|
|
1095
|
+
},
|
|
1096
|
+
],
|
|
1084
1097
|
},
|
|
1085
|
-
|
|
1098
|
+
];
|
|
1086
1099
|
|
|
1087
1100
|
// AND the step has run once through the runner, recording the step checkpoint.
|
|
1088
1101
|
await runMigrationSteps(db, [migrateTestStepRollback]);
|
|
@@ -1096,7 +1109,7 @@ describe("rollbackMemoryMigration", () => {
|
|
|
1096
1109
|
).toBeTruthy();
|
|
1097
1110
|
|
|
1098
1111
|
// AND the migration has since been rolled back below its version.
|
|
1099
|
-
rollbackMemoryMigration(db, 3999);
|
|
1112
|
+
rollbackMemoryMigration(db, 3999, testSteps);
|
|
1100
1113
|
expect(hasTable()).toBe(false);
|
|
1101
1114
|
|
|
1102
1115
|
// WHEN the runner executes the forward steps again on a later upgrade.
|
|
@@ -1107,8 +1120,6 @@ describe("rollbackMemoryMigration", () => {
|
|
|
1107
1120
|
});
|
|
1108
1121
|
|
|
1109
1122
|
test("handles transaction failure in down() — rolls back and preserves checkpoint", () => {
|
|
1110
|
-
saveRegistry();
|
|
1111
|
-
|
|
1112
1123
|
const db = createTestDb();
|
|
1113
1124
|
const raw = getRaw(db);
|
|
1114
1125
|
bootstrapCheckpointsTable(raw);
|
|
@@ -1126,21 +1137,27 @@ describe("rollbackMemoryMigration", () => {
|
|
|
1126
1137
|
`INSERT INTO test_rollback_data (id, value) VALUES ('row-1', 'original')`,
|
|
1127
1138
|
);
|
|
1128
1139
|
|
|
1129
|
-
//
|
|
1130
|
-
//
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1140
|
+
// The test step carries rollback metadata so rollbackMemoryMigration can
|
|
1141
|
+
// find it in the steps list.
|
|
1142
|
+
const testSteps: MigrationStep[] = [
|
|
1143
|
+
{
|
|
1144
|
+
name: "test_fail_down_v3000",
|
|
1145
|
+
run: () => {},
|
|
1146
|
+
rollback: [
|
|
1147
|
+
{
|
|
1148
|
+
version: 3000,
|
|
1149
|
+
description: "test migration with failing down()",
|
|
1150
|
+
down: (database) => {
|
|
1151
|
+
const sqlite = getSqliteFrom(database);
|
|
1152
|
+
// This UPDATE will trigger our failure trigger.
|
|
1153
|
+
sqlite.exec(
|
|
1154
|
+
`UPDATE test_rollback_data SET value = 'rolled-back' WHERE id = 'row-1'`,
|
|
1155
|
+
);
|
|
1156
|
+
},
|
|
1157
|
+
},
|
|
1158
|
+
],
|
|
1142
1159
|
},
|
|
1143
|
-
|
|
1160
|
+
];
|
|
1144
1161
|
|
|
1145
1162
|
// Mark as completed (via step checkpoint).
|
|
1146
1163
|
raw.exec(
|
|
@@ -1158,7 +1175,7 @@ describe("rollbackMemoryMigration", () => {
|
|
|
1158
1175
|
// Rollback should throw because down() fails.
|
|
1159
1176
|
let threw = false;
|
|
1160
1177
|
try {
|
|
1161
|
-
rollbackMemoryMigration(db, 2999);
|
|
1178
|
+
rollbackMemoryMigration(db, 2999, testSteps);
|
|
1162
1179
|
} catch {
|
|
1163
1180
|
threw = true;
|
|
1164
1181
|
}
|
|
@@ -1187,8 +1204,6 @@ describe("rollbackMemoryMigration", () => {
|
|
|
1187
1204
|
});
|
|
1188
1205
|
|
|
1189
1206
|
test("down() with its own BEGIN/COMMIT succeeds without nested-transaction errors", () => {
|
|
1190
|
-
saveRegistry();
|
|
1191
|
-
|
|
1192
1207
|
const db = createTestDb();
|
|
1193
1208
|
const raw = getRaw(db);
|
|
1194
1209
|
bootstrapCheckpointsTable(raw);
|
|
@@ -1206,23 +1221,30 @@ describe("rollbackMemoryMigration", () => {
|
|
|
1206
1221
|
`INSERT INTO test_self_txn_data (id, value) VALUES ('row-1', 'migrated')`,
|
|
1207
1222
|
);
|
|
1208
1223
|
|
|
1209
|
-
//
|
|
1224
|
+
// The test step carries rollback metadata so rollbackMemoryMigration can
|
|
1225
|
+
// find it in the steps list. The down() manages its own transaction —
|
|
1210
1226
|
// this previously caused nested-transaction errors when rollbackMemoryMigration
|
|
1211
1227
|
// wrapped every down() call in BEGIN/COMMIT.
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1228
|
+
const testSteps: MigrationStep[] = [
|
|
1229
|
+
{
|
|
1230
|
+
name: "test_self_txn_down_v3500",
|
|
1231
|
+
run: () => {},
|
|
1232
|
+
rollback: [
|
|
1233
|
+
{
|
|
1234
|
+
version: 3500,
|
|
1235
|
+
description: "test migration with self-transactional down()",
|
|
1236
|
+
down: (database) => {
|
|
1237
|
+
const sqlite = getSqliteFrom(database);
|
|
1238
|
+
sqlite.exec("BEGIN");
|
|
1239
|
+
sqlite.exec(
|
|
1240
|
+
`UPDATE test_self_txn_data SET value = 'original' WHERE id = 'row-1'`,
|
|
1241
|
+
);
|
|
1242
|
+
sqlite.exec("COMMIT");
|
|
1243
|
+
},
|
|
1244
|
+
},
|
|
1245
|
+
],
|
|
1224
1246
|
},
|
|
1225
|
-
|
|
1247
|
+
];
|
|
1226
1248
|
|
|
1227
1249
|
// Mark as completed (via step checkpoint).
|
|
1228
1250
|
raw.exec(
|
|
@@ -1230,7 +1252,7 @@ describe("rollbackMemoryMigration", () => {
|
|
|
1230
1252
|
);
|
|
1231
1253
|
|
|
1232
1254
|
// This should succeed — no nested transaction error.
|
|
1233
|
-
const rolledBack = rollbackMemoryMigration(db, 3499);
|
|
1255
|
+
const rolledBack = rollbackMemoryMigration(db, 3499, testSteps);
|
|
1234
1256
|
|
|
1235
1257
|
expect(rolledBack).toEqual(["test_self_txn_down_v3500"]);
|
|
1236
1258
|
|
|
@@ -1251,37 +1273,43 @@ describe("rollbackMemoryMigration", () => {
|
|
|
1251
1273
|
});
|
|
1252
1274
|
|
|
1253
1275
|
test("no-op when already at target version", () => {
|
|
1254
|
-
saveRegistry();
|
|
1255
|
-
|
|
1256
1276
|
const db = createTestDb();
|
|
1257
1277
|
const raw = getRaw(db);
|
|
1258
1278
|
bootstrapCheckpointsTable(raw);
|
|
1259
1279
|
|
|
1260
1280
|
const now = Date.now();
|
|
1261
1281
|
|
|
1262
|
-
//
|
|
1282
|
+
// Test steps with rollback metadata — they should NOT have down() called.
|
|
1263
1283
|
const downCalls: string[] = [];
|
|
1264
1284
|
|
|
1265
|
-
|
|
1285
|
+
const testSteps: MigrationStep[] = [
|
|
1266
1286
|
{
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1287
|
+
name: "test_noop_v4000",
|
|
1288
|
+
run: () => {},
|
|
1289
|
+
rollback: [
|
|
1290
|
+
{
|
|
1291
|
+
version: 4000,
|
|
1292
|
+
description: "test noop v4000",
|
|
1293
|
+
down: () => {
|
|
1294
|
+
downCalls.push("test_noop_v4000");
|
|
1295
|
+
},
|
|
1296
|
+
},
|
|
1297
|
+
],
|
|
1274
1298
|
},
|
|
1275
1299
|
{
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1300
|
+
name: "test_noop_v4001",
|
|
1301
|
+
run: () => {},
|
|
1302
|
+
rollback: [
|
|
1303
|
+
{
|
|
1304
|
+
version: 4001,
|
|
1305
|
+
description: "test noop v4001",
|
|
1306
|
+
down: () => {
|
|
1307
|
+
downCalls.push("test_noop_v4001");
|
|
1308
|
+
},
|
|
1309
|
+
},
|
|
1310
|
+
],
|
|
1283
1311
|
},
|
|
1284
|
-
|
|
1312
|
+
];
|
|
1285
1313
|
|
|
1286
1314
|
// Mark both as completed (via step checkpoints).
|
|
1287
1315
|
raw.exec(
|
|
@@ -1292,7 +1320,7 @@ describe("rollbackMemoryMigration", () => {
|
|
|
1292
1320
|
);
|
|
1293
1321
|
|
|
1294
1322
|
// Roll back to version >= latest applied migration — should be a no-op.
|
|
1295
|
-
const rolledBack = rollbackMemoryMigration(db, 4001);
|
|
1323
|
+
const rolledBack = rollbackMemoryMigration(db, 4001, testSteps);
|
|
1296
1324
|
|
|
1297
1325
|
expect(rolledBack).toEqual([]);
|
|
1298
1326
|
expect(downCalls).toEqual([]);
|
|
@@ -1312,14 +1340,12 @@ describe("rollbackMemoryMigration", () => {
|
|
|
1312
1340
|
expect(cp4001!.value).toBe("1");
|
|
1313
1341
|
|
|
1314
1342
|
// Also verify with a target version greater than the latest.
|
|
1315
|
-
const rolledBack2 = rollbackMemoryMigration(db, 9999);
|
|
1343
|
+
const rolledBack2 = rollbackMemoryMigration(db, 9999, testSteps);
|
|
1316
1344
|
expect(rolledBack2).toEqual([]);
|
|
1317
1345
|
expect(downCalls).toEqual([]);
|
|
1318
1346
|
});
|
|
1319
1347
|
|
|
1320
1348
|
test("respects dependency ordering on rollback (children rolled back before parents)", () => {
|
|
1321
|
-
saveRegistry();
|
|
1322
|
-
|
|
1323
1349
|
const db = createTestDb();
|
|
1324
1350
|
const raw = getRaw(db);
|
|
1325
1351
|
bootstrapCheckpointsTable(raw);
|
|
@@ -1332,27 +1358,35 @@ describe("rollbackMemoryMigration", () => {
|
|
|
1332
1358
|
// Since the child has a higher version number, rolling back in reverse
|
|
1333
1359
|
// version order means the child (v5001) is rolled back BEFORE the parent
|
|
1334
1360
|
// (v5000), which is the correct dependency-safe ordering.
|
|
1335
|
-
|
|
1361
|
+
const testSteps: MigrationStep[] = [
|
|
1336
1362
|
{
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1363
|
+
name: "test_parent_v5000",
|
|
1364
|
+
run: () => {},
|
|
1365
|
+
rollback: [
|
|
1366
|
+
{
|
|
1367
|
+
version: 5000,
|
|
1368
|
+
description: "test parent migration",
|
|
1369
|
+
down: () => {
|
|
1370
|
+
downCalls.push("test_parent_v5000");
|
|
1371
|
+
},
|
|
1372
|
+
},
|
|
1373
|
+
],
|
|
1344
1374
|
},
|
|
1345
1375
|
{
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
version: 5001,
|
|
1376
|
+
name: "test_child_v5001",
|
|
1377
|
+
run: () => {},
|
|
1349
1378
|
dependsOn: ["test_parent_v5000"],
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1379
|
+
rollback: [
|
|
1380
|
+
{
|
|
1381
|
+
version: 5001,
|
|
1382
|
+
description: "test child migration depending on parent",
|
|
1383
|
+
down: () => {
|
|
1384
|
+
downCalls.push("test_child_v5001");
|
|
1385
|
+
},
|
|
1386
|
+
},
|
|
1387
|
+
],
|
|
1354
1388
|
},
|
|
1355
|
-
|
|
1389
|
+
];
|
|
1356
1390
|
|
|
1357
1391
|
// Both are completed (via step checkpoints).
|
|
1358
1392
|
raw.exec(
|
|
@@ -1363,7 +1397,7 @@ describe("rollbackMemoryMigration", () => {
|
|
|
1363
1397
|
);
|
|
1364
1398
|
|
|
1365
1399
|
// Roll back to version 4999 — both should be rolled back, child first.
|
|
1366
|
-
const rolledBack = rollbackMemoryMigration(db, 4999);
|
|
1400
|
+
const rolledBack = rollbackMemoryMigration(db, 4999, testSteps);
|
|
1367
1401
|
|
|
1368
1402
|
expect(rolledBack).toEqual(["test_child_v5001", "test_parent_v5000"]);
|
|
1369
1403
|
|
|
@@ -421,8 +421,8 @@ describe("handleMigrationImport — JSON {url} body", () => {
|
|
|
421
421
|
describe("handleMigrationImport — no-swap path omits newer-migration warning", () => {
|
|
422
422
|
test("credentials-only bundle does not inherit live-DB migration warnings", async () => {
|
|
423
423
|
// Seed the live workspace DB with a step:* checkpoint that's NOT
|
|
424
|
-
// in the
|
|
425
|
-
// version" and would otherwise push a warning into the report. With
|
|
424
|
+
// in the known step list. validateMigrationState treats this as a
|
|
425
|
+
// "newer version" and would otherwise push a warning into the report. With
|
|
426
426
|
// the gate in appendNewerMigrationWarningsIfAny the warning must be
|
|
427
427
|
// suppressed when the import didn't modify the workspace.
|
|
428
428
|
const dbDir = join(testWorkspaceRoot, "data", "db");
|