@openzeppelin/upgrades-core 1.43.0 → 1.44.1

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.
Files changed (83) hide show
  1. package/artifacts/@openzeppelin/contracts-v5/proxy/ERC1967/ERC1967Proxy.sol/ERC1967Proxy.json +3 -3
  2. package/artifacts/@openzeppelin/contracts-v5/proxy/beacon/BeaconProxy.sol/BeaconProxy.json +3 -3
  3. package/artifacts/@openzeppelin/contracts-v5/proxy/beacon/UpgradeableBeacon.sol/UpgradeableBeacon.json +2 -2
  4. package/artifacts/@openzeppelin/contracts-v5/proxy/transparent/ProxyAdmin.sol/ProxyAdmin.json +2 -2
  5. package/artifacts/@openzeppelin/contracts-v5/proxy/transparent/TransparentUpgradeableProxy.sol/ITransparentUpgradeableProxy.json +2 -2
  6. package/artifacts/@openzeppelin/contracts-v5/proxy/transparent/TransparentUpgradeableProxy.sol/TransparentUpgradeableProxy.json +3 -3
  7. package/artifacts/build-info-v5.json +5159 -2886
  8. package/dist/solidity-version.json +1 -1
  9. package/dist/storage/extract.d.ts.map +1 -1
  10. package/dist/storage/extract.js +14 -0
  11. package/dist/storage/extract.js.map +1 -1
  12. package/dist/storage/index.d.ts.map +1 -1
  13. package/dist/storage/index.js +23 -0
  14. package/dist/storage/index.js.map +1 -1
  15. package/dist/storage/layout.d.ts +1 -0
  16. package/dist/storage/layout.d.ts.map +1 -1
  17. package/dist/storage/layout.js.map +1 -1
  18. package/dist/storage/namespace.d.ts.map +1 -1
  19. package/dist/storage/namespace.js +47 -1
  20. package/dist/storage/namespace.js.map +1 -1
  21. package/dist/storage-custom-layout.test.d.ts +2 -0
  22. package/dist/storage-custom-layout.test.d.ts.map +1 -0
  23. package/dist/storage-custom-layout.test.js +314 -0
  24. package/dist/storage-custom-layout.test.js.map +1 -0
  25. package/dist/utils/erc7201.d.ts +10 -0
  26. package/dist/utils/erc7201.d.ts.map +1 -0
  27. package/dist/utils/erc7201.js +24 -0
  28. package/dist/utils/erc7201.js.map +1 -0
  29. package/dist/utils/erc7201.test.d.ts +2 -0
  30. package/dist/utils/erc7201.test.d.ts.map +1 -0
  31. package/dist/utils/erc7201.test.js +11 -0
  32. package/dist/utils/erc7201.test.js.map +1 -0
  33. package/dist/utils/integer-literals.d.ts +15 -0
  34. package/dist/utils/integer-literals.d.ts.map +1 -0
  35. package/dist/utils/integer-literals.js +34 -0
  36. package/dist/utils/integer-literals.js.map +1 -0
  37. package/dist/utils/integer-literals.test.d.ts +2 -0
  38. package/dist/utils/integer-literals.test.d.ts.map +1 -0
  39. package/dist/utils/integer-literals.test.js +57 -0
  40. package/dist/utils/integer-literals.test.js.map +1 -0
  41. package/dist/utils/make-namespaced.d.ts.map +1 -1
  42. package/dist/utils/make-namespaced.js +4 -0
  43. package/dist/utils/make-namespaced.js.map +1 -1
  44. package/dist/utils/make-namespaced.test.js +4 -0
  45. package/dist/utils/make-namespaced.test.js.map +1 -1
  46. package/dist/utils/stabilize-layout.d.ts +1 -0
  47. package/dist/utils/stabilize-layout.d.ts.map +1 -1
  48. package/dist/utils/stabilize-layout.js +1 -0
  49. package/dist/utils/stabilize-layout.js.map +1 -1
  50. package/dist/validate/run/initializer.js +5 -2
  51. package/dist/validate/run/initializer.js.map +1 -1
  52. package/dist/validate-initializers.test.js +11 -1
  53. package/dist/validate-initializers.test.js.map +1 -1
  54. package/package.json +9 -7
  55. package/src/solidity-version.json +1 -1
  56. package/src/storage/extract.ts +21 -0
  57. package/src/storage/index.ts +29 -0
  58. package/src/storage/layout.ts +1 -0
  59. package/src/storage/namespace.ts +67 -1
  60. package/src/storage-0.8.test.ts.md +4 -0
  61. package/src/storage-0.8.test.ts.snap +0 -0
  62. package/src/storage-custom-layout.test.ts +358 -0
  63. package/src/storage-custom-layout.test.ts.md +353 -0
  64. package/src/storage-custom-layout.test.ts.snap +0 -0
  65. package/src/storage-memory-0.5.test.ts.md +2 -0
  66. package/src/storage-memory-0.5.test.ts.snap +0 -0
  67. package/src/storage-namespaced-layout.test.ts.md +1 -0
  68. package/src/storage-namespaced-layout.test.ts.snap +0 -0
  69. package/src/storage-namespaced.test.ts.md +2 -0
  70. package/src/storage-namespaced.test.ts.snap +0 -0
  71. package/src/storage.test.ts.md +2 -0
  72. package/src/storage.test.ts.snap +0 -0
  73. package/src/utils/erc7201.test.ts +10 -0
  74. package/src/utils/erc7201.ts +25 -0
  75. package/src/utils/integer-literals.test.ts +73 -0
  76. package/src/utils/integer-literals.ts +29 -0
  77. package/src/utils/make-namespaced.test.ts +7 -0
  78. package/src/utils/make-namespaced.test.ts.md +50 -0
  79. package/src/utils/make-namespaced.test.ts.snap +0 -0
  80. package/src/utils/make-namespaced.ts +5 -0
  81. package/src/utils/stabilize-layout.ts +1 -0
  82. package/src/validate/run/initializer.ts +4 -1
  83. package/src/validate-initializers.test.ts +12 -1
@@ -6,6 +6,7 @@ import { StorageOperation, StorageItem, StorageLayoutComparator } from './compar
6
6
  import { LayoutCompatibilityReport } from './report';
7
7
  import { ValidationOptions, withValidationDefaults } from '../validate/overrides';
8
8
  import { logNote, logWarning } from '../utils/log';
9
+ import { normalizeUint256Literal } from '../utils/integer-literals';
9
10
 
10
11
  export function assertStorageUpgradeSafe(
11
12
  original: StorageLayout,
@@ -36,11 +37,39 @@ export function assertStorageUpgradeSafe(
36
37
  }
37
38
  }
38
39
 
40
+ function validateBaseSlotUnchanged(original: StorageLayout, updated: StorageLayout) {
41
+ const origNormalized = normalizeUint256Literal(original.baseSlot);
42
+ const updatedNormalized = normalizeUint256Literal(updated.baseSlot);
43
+
44
+ if (origNormalized !== updatedNormalized) {
45
+ throw new UpgradesError(
46
+ `Base slot for custom storage layout changed from ${origNormalized} to ${updatedNormalized}`,
47
+ () => `\
48
+ Do not change the base slot during upgrades when using custom storage layout.
49
+
50
+ If you need to change the base slot, ensure you also do the following:
51
+ 1. Adjust all storage variables to continue using their original slots.
52
+ 2. Configure your Solidity compiler options to include storage layouts according to https://docs.openzeppelin.com/upgrades-plugins/api-core#compile_contracts_with_storage_layouts
53
+ 3. Recompile both the original and updated contracts.
54
+ 4. Run the storage layout check again.`,
55
+ );
56
+ }
57
+ }
58
+
39
59
  export function getStorageUpgradeReport(
40
60
  original: StorageLayout,
41
61
  updated: StorageLayout,
42
62
  opts: Required<ValidationOptions>,
43
63
  ): LayoutCompatibilityReport {
64
+ if (opts.unsafeSkipStorageCheck) {
65
+ return new LayoutCompatibilityReport([]);
66
+ }
67
+
68
+ if (original.storage.some(item => item.slot === undefined) || updated.storage.some(item => item.slot === undefined)) {
69
+ // Compiler was not configured to output detailed storage layout with slot information
70
+ validateBaseSlotUnchanged(original, updated);
71
+ }
72
+
44
73
  const originalDetailed = getDetailedLayout(original);
45
74
  const updatedDetailed = getDetailedLayout(updated);
46
75
  const originalDetailedNamespaces = getDetailedNamespacedLayout(original);
@@ -12,6 +12,7 @@ export interface StorageLayout {
12
12
  types: Record<string, TypeItem>;
13
13
  flat?: boolean;
14
14
  namespaces?: Record<string, StorageItem[]>;
15
+ baseSlot?: string;
15
16
  }
16
17
 
17
18
  export type StorageField<Type = string> = StorageItem<Type> | StructMember<Type>;
@@ -8,6 +8,9 @@ import { Node } from 'solidity-ast/node';
8
8
  import { CompilationContext, getTypeMembers, loadLayoutType } from './extract';
9
9
  import { UpgradesError } from '../error';
10
10
  import * as versions from 'compare-versions';
11
+ import { normalizeUint256Literal } from '../utils/integer-literals';
12
+ import { calculateERC7201StorageLocation, ERC7201_FORMULA_PREFIX } from '../utils/erc7201';
13
+ import { logWarning } from '../utils/log';
11
14
 
12
15
  /**
13
16
  * Loads a contract's namespaces and namespaced type information into the storage layout.
@@ -57,8 +60,10 @@ export function loadNamespaces(
57
60
  );
58
61
  }
59
62
 
63
+ warnIfCustomLayoutAndNamespacesFound(namespacesWithSrc, decodeSrc, origContext.contractDef);
64
+
60
65
  // Add to layout without the namespaced structs' src locations, since those are no longer needed
61
- // as they were only used to give duplicate namespace errors above.
66
+ // as they were only used to give duplicate namespace errors in `addContractNamespacesWithSrc` above.
62
67
  layout.namespaces = Object.fromEntries(
63
68
  Object.entries(namespacesWithSrc).map(([id, namespaceWithSrc]) => [id, namespaceWithSrc.namespace]),
64
69
  );
@@ -78,6 +83,19 @@ Use a unique namespace id for each struct annotated with '@custom:storage-locati
78
83
  }
79
84
  }
80
85
 
86
+ class CustomLayoutClashWithNamespaceError extends UpgradesError {
87
+ constructor(id: string, contractName: string, src: string) {
88
+ super(
89
+ `Custom layout for contract ${contractName} clashes with the storage location for namespace ${id}`,
90
+ () => `\
91
+ The namespace ${id} is defined in a struct at the following location:
92
+ - ${src}
93
+
94
+ Specify a different custom layout for the contract or use a different namespace for the struct.`,
95
+ );
96
+ }
97
+ }
98
+
81
99
  /**
82
100
  * Namespaced storage items, along with the original source location of the namespace struct.
83
101
  */
@@ -130,6 +148,8 @@ function addContractNamespacesWithSrc(
130
148
  origSrc,
131
149
  );
132
150
  } else {
151
+ checkCustomLayoutClashWithNamespace(origContractDef, storageLocation, leastDerivedContractName, origSrc);
152
+
133
153
  namespacesWithSrc[storageLocation] = {
134
154
  namespace: getNamespacedStorageItems(node, decodeSrc, layout, contractContext, origContractDef),
135
155
  src: origSrc,
@@ -140,6 +160,52 @@ function addContractNamespacesWithSrc(
140
160
  }
141
161
  }
142
162
 
163
+ /**
164
+ * Checks if a custom layout's base slot clashes with a namespace's base slot, and throws an error if it does.
165
+ *
166
+ * @param contractDef The contract definition to check.
167
+ * @param storageLocation The namespace storage location string, e.g. `erc7201:<NAMESPACE_ID>`.
168
+ * @param contractName The name of the contract.
169
+ * @param src The original source location of the struct with the namespace.
170
+ * @throws CustomLayoutClashWithNamespaceError if the custom layout clashes with the namespace.
171
+ */
172
+ function checkCustomLayoutClashWithNamespace(
173
+ contractDef: ContractDefinition,
174
+ storageLocation: string,
175
+ contractName: string,
176
+ src: string,
177
+ ) {
178
+ if (contractDef.storageLayout !== undefined && storageLocation.startsWith(ERC7201_FORMULA_PREFIX)) {
179
+ const namespaceId = storageLocation.substring(ERC7201_FORMULA_PREFIX.length);
180
+ const calculatedStorageLocation = calculateERC7201StorageLocation(namespaceId);
181
+ if (contractDef.storageLayout.baseSlotExpression.nodeType === 'Literal') {
182
+ const baseSlotNormalized = normalizeUint256Literal(contractDef.storageLayout.baseSlotExpression.value); // TODO: when Solidity supports an erc7201 helper function, also check that for clash
183
+ if (baseSlotNormalized === calculatedStorageLocation) {
184
+ throw new CustomLayoutClashWithNamespaceError(storageLocation, contractName, src);
185
+ }
186
+ }
187
+ }
188
+ }
189
+
190
+ function warnIfCustomLayoutAndNamespacesFound(
191
+ namespacesWithSrc: Record<string, NamespaceWithSrc>,
192
+ decodeSrc: SrcDecoder,
193
+ origContractDef: ContractDefinition,
194
+ ) {
195
+ // TODO: when Solidity supports an erc7201 helper function, only give this warning if the custom storage layout is not using the erc7201 helper function
196
+ if (Object.entries(namespacesWithSrc).length > 0 && origContractDef.storageLayout !== undefined) {
197
+ const contractName = origContractDef.canonicalName ?? origContractDef.name;
198
+ logWarning(
199
+ `${decodeSrc(origContractDef.storageLayout)}: Ensure custom storage layout does not overlap with namespaced storage layout.`,
200
+ [
201
+ `Contract \`${contractName}\` has both custom storage layout and namespaces.`,
202
+ 'The Upgrades Plugin validates that they do not use the same base slot, but does not check for overlaps.',
203
+ "Ensure the base slot for the contract's `layout at` specifier is chosen such that its storage variables do not overlap with any namespaces.",
204
+ ],
205
+ );
206
+ }
207
+ }
208
+
143
209
  /**
144
210
  * Gets the storage location string from the `@custom:storage-location` annotation.
145
211
  *
@@ -9,6 +9,7 @@ Generated by [AVA](https://avajs.dev).
9
9
  > Snapshot 1
10
10
 
11
11
  {
12
+ baseSlot: undefined,
12
13
  namespaces: [],
13
14
  storage: [
14
15
  {
@@ -47,6 +48,7 @@ Generated by [AVA](https://avajs.dev).
47
48
  > Snapshot 1
48
49
 
49
50
  {
51
+ baseSlot: undefined,
50
52
  namespaces: [],
51
53
  storage: [
52
54
  {
@@ -85,6 +87,7 @@ Generated by [AVA](https://avajs.dev).
85
87
  > Snapshot 1
86
88
 
87
89
  {
90
+ baseSlot: undefined,
88
91
  namespaces: [],
89
92
  storage: [
90
93
  {
@@ -120,6 +123,7 @@ Generated by [AVA](https://avajs.dev).
120
123
  > Snapshot 1
121
124
 
122
125
  {
126
+ baseSlot: undefined,
123
127
  namespaces: [],
124
128
  storage: [
125
129
  {
Binary file
@@ -0,0 +1,358 @@
1
+ import _test, { TestFn } from 'ava';
2
+ import { ContractDefinition } from 'solidity-ast';
3
+ import { findAll, astDereferencer } from 'solidity-ast/utils';
4
+ import { artifacts } from 'hardhat';
5
+
6
+ import { SolcOutput } from './solc-api';
7
+ import { getStorageUpgradeErrors } from './storage';
8
+ import { StorageLayout } from './storage/layout';
9
+ import { extractStorageLayout } from './storage/extract';
10
+ import { stabilizeStorageLayout } from './utils/stabilize-layout';
11
+
12
+ interface Context {
13
+ extractStorageLayout: (contract: string) => ReturnType<typeof extractStorageLayout>;
14
+ extractStorageLayoutFallback: (contract: string) => ReturnType<typeof extractStorageLayout>;
15
+ }
16
+
17
+ const test = _test as TestFn<Context>;
18
+
19
+ test.before(async t => {
20
+ const buildInfo = await artifacts.getBuildInfo('contracts/test/CustomLayout.sol:CustomLayout');
21
+ if (buildInfo === undefined) {
22
+ throw new Error('Build info not found');
23
+ }
24
+ const solcOutput: SolcOutput = buildInfo.output;
25
+ const contracts: Record<string, ContractDefinition> = {};
26
+ const storageLayouts: Record<string, StorageLayout> = {};
27
+ for (const def of findAll('ContractDefinition', solcOutput.sources['contracts/test/CustomLayout.sol'].ast)) {
28
+ contracts[def.name] = def;
29
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
30
+ storageLayouts[def.name] = solcOutput.contracts['contracts/test/CustomLayout.sol'][def.name].storageLayout!;
31
+ }
32
+ const deref = astDereferencer(solcOutput);
33
+
34
+ t.context.extractStorageLayout = name =>
35
+ extractStorageLayout(contracts[name], dummyDecodeSrc, deref, storageLayouts[name]);
36
+
37
+ // For testing fallback behavior when storage layout is missing from compilation output
38
+ t.context.extractStorageLayoutFallback = name =>
39
+ extractStorageLayout(contracts[name], dummyDecodeSrc, deref, undefined);
40
+ });
41
+
42
+ const dummyDecodeSrc = () => 'file.sol:1';
43
+
44
+ test('CustomLayout', t => {
45
+ const layout = t.context.extractStorageLayout('CustomLayout');
46
+ t.snapshot(stabilizeStorageLayout(layout));
47
+ });
48
+
49
+ test('CustomLayout - fallback', t => {
50
+ const layout = t.context.extractStorageLayoutFallback('CustomLayout');
51
+ t.snapshot(stabilizeStorageLayout(layout));
52
+ });
53
+
54
+ test('CustomLayout same root - ok', t => {
55
+ const v1 = t.context.extractStorageLayout('CustomLayout');
56
+ const v2 = t.context.extractStorageLayout('CustomLayout_Same_Root_Ok');
57
+ const comparison = getStorageUpgradeErrors(v1, v2);
58
+ t.deepEqual(comparison, []);
59
+ });
60
+
61
+ test('CustomLayout same root - fallback - ok', t => {
62
+ const v1 = t.context.extractStorageLayoutFallback('CustomLayout');
63
+ const v2 = t.context.extractStorageLayoutFallback('CustomLayout_Same_Root_Ok');
64
+ const comparison = getStorageUpgradeErrors(v1, v2);
65
+ t.deepEqual(comparison, []);
66
+ });
67
+
68
+ test('CustomLayout same root decimal - ok', t => {
69
+ const v1 = t.context.extractStorageLayout('CustomLayout');
70
+ const v2 = t.context.extractStorageLayout('CustomLayout_Same_Root_Decimal_Ok');
71
+ const comparison = getStorageUpgradeErrors(v1, v2);
72
+ t.deepEqual(comparison, []);
73
+ });
74
+
75
+ test('CustomLayout same root decimal - fallback - ok', t => {
76
+ const v1 = t.context.extractStorageLayoutFallback('CustomLayout');
77
+ const v2 = t.context.extractStorageLayoutFallback('CustomLayout_Same_Root_Decimal_Ok');
78
+ const comparison = getStorageUpgradeErrors(v1, v2);
79
+ t.deepEqual(comparison, []);
80
+ });
81
+
82
+ test('CustomLayout same root scientific notation - ok', t => {
83
+ const v1 = t.context.extractStorageLayout('CustomLayout');
84
+ const v2 = t.context.extractStorageLayout('CustomLayout_Same_Root_ScientificNotation_Ok');
85
+ const comparison = getStorageUpgradeErrors(v1, v2);
86
+ t.deepEqual(comparison, []);
87
+ });
88
+
89
+ test('CustomLayout same root scientific notation - fallback - ok', t => {
90
+ const v1 = t.context.extractStorageLayoutFallback('CustomLayout');
91
+ const v2 = t.context.extractStorageLayoutFallback('CustomLayout_Same_Root_ScientificNotation_Ok');
92
+ const comparison = getStorageUpgradeErrors(v1, v2);
93
+ t.deepEqual(comparison, []);
94
+ });
95
+
96
+ test('CustomLayout changed root - bad', t => {
97
+ const v1 = t.context.extractStorageLayout('CustomLayout');
98
+ const v2 = t.context.extractStorageLayout('CustomLayout_Changed_Root_Bad');
99
+ const comparison = getStorageUpgradeErrors(v1, v2);
100
+ t.like(comparison, {
101
+ length: 2,
102
+ 0: {
103
+ kind: 'layoutchange',
104
+ change: {
105
+ slot: {
106
+ from: '1',
107
+ to: '2',
108
+ },
109
+ },
110
+ original: { label: 'x' },
111
+ updated: { label: 'x' },
112
+ },
113
+ 1: {
114
+ kind: 'layoutchange',
115
+ change: {
116
+ slot: {
117
+ from: '2',
118
+ to: '3',
119
+ },
120
+ },
121
+ original: { label: 'y' },
122
+ updated: { label: 'y' },
123
+ },
124
+ });
125
+ });
126
+
127
+ test('CustomLayout changed root - fallback - bad', t => {
128
+ const v1 = t.context.extractStorageLayoutFallback('CustomLayout');
129
+ const v2 = t.context.extractStorageLayoutFallback('CustomLayout_Changed_Root_Bad');
130
+ const error = t.throws(() => getStorageUpgradeErrors(v1, v2));
131
+ t.snapshot(error?.message);
132
+ });
133
+
134
+ test('CustomLayout changed root decimal - bad', t => {
135
+ const v1 = t.context.extractStorageLayout('CustomLayout');
136
+ const v2 = t.context.extractStorageLayout('CustomLayout_Changed_Root_Decimal_Bad');
137
+ const comparison = getStorageUpgradeErrors(v1, v2);
138
+ t.like(comparison, {
139
+ length: 2,
140
+ 0: {
141
+ kind: 'layoutchange',
142
+ change: {
143
+ slot: {
144
+ from: '1',
145
+ to: '2',
146
+ },
147
+ },
148
+ original: { label: 'x' },
149
+ updated: { label: 'x' },
150
+ },
151
+ 1: {
152
+ kind: 'layoutchange',
153
+ change: {
154
+ slot: {
155
+ from: '2',
156
+ to: '3',
157
+ },
158
+ },
159
+ original: { label: 'y' },
160
+ updated: { label: 'y' },
161
+ },
162
+ });
163
+ });
164
+
165
+ test('CustomLayout changed root decimal - fallback - bad', t => {
166
+ const v1 = t.context.extractStorageLayoutFallback('CustomLayout');
167
+ const v2 = t.context.extractStorageLayoutFallback('CustomLayout_Changed_Root_Decimal_Bad');
168
+ const error = t.throws(() => getStorageUpgradeErrors(v1, v2));
169
+ t.snapshot(error?.message);
170
+ });
171
+
172
+ test('CustomLayout changed root scientific notation - bad', t => {
173
+ const v1 = t.context.extractStorageLayout('CustomLayout');
174
+ const v2 = t.context.extractStorageLayout('CustomLayout_Changed_Root_ScientificNotation_Bad');
175
+ const comparison = getStorageUpgradeErrors(v1, v2);
176
+ t.like(comparison, {
177
+ length: 2,
178
+ 0: {
179
+ kind: 'layoutchange',
180
+ change: {
181
+ slot: {
182
+ from: '1',
183
+ to: '20000000000',
184
+ },
185
+ },
186
+ original: { label: 'x' },
187
+ updated: { label: 'x' },
188
+ },
189
+ 1: {
190
+ kind: 'layoutchange',
191
+ change: {
192
+ slot: {
193
+ from: '2',
194
+ to: '20000000001',
195
+ },
196
+ },
197
+ original: { label: 'y' },
198
+ updated: { label: 'y' },
199
+ },
200
+ });
201
+ });
202
+
203
+ test('CustomLayout changed root scientific notation - fallback - bad', t => {
204
+ const v1 = t.context.extractStorageLayoutFallback('CustomLayout');
205
+ const v2 = t.context.extractStorageLayoutFallback('CustomLayout_Changed_Root_ScientificNotation_Bad');
206
+ const error = t.throws(() => getStorageUpgradeErrors(v1, v2));
207
+ t.snapshot(error?.message);
208
+ });
209
+
210
+ test('CustomLayout changed to default - bad', t => {
211
+ const v1 = t.context.extractStorageLayout('CustomLayout');
212
+ const v2 = t.context.extractStorageLayout('CustomLayout_Changed_To_Default_Bad');
213
+ const comparison = getStorageUpgradeErrors(v1, v2);
214
+ t.like(comparison, {
215
+ length: 2,
216
+ 0: {
217
+ kind: 'layoutchange',
218
+ change: {
219
+ slot: {
220
+ from: '1',
221
+ to: '0',
222
+ },
223
+ },
224
+ original: { label: 'x' },
225
+ updated: { label: 'x' },
226
+ },
227
+ 1: {
228
+ kind: 'layoutchange',
229
+ change: {
230
+ slot: {
231
+ from: '2',
232
+ to: '1',
233
+ },
234
+ },
235
+ original: { label: 'y' },
236
+ updated: { label: 'y' },
237
+ },
238
+ });
239
+ });
240
+
241
+ test('CustomLayout changed to default - fallback - bad', t => {
242
+ const v1 = t.context.extractStorageLayoutFallback('CustomLayout');
243
+ const v2 = t.context.extractStorageLayoutFallback('CustomLayout_Changed_To_Default_Bad');
244
+ const error = t.throws(() => getStorageUpgradeErrors(v1, v2));
245
+ t.snapshot(error?.message);
246
+ });
247
+
248
+ test('CustomLayout changed root - ok', t => {
249
+ const v1 = t.context.extractStorageLayout('CustomLayout');
250
+ const v2 = t.context.extractStorageLayout('CustomLayout_Changed_Root_Ok');
251
+ const comparison = getStorageUpgradeErrors(v1, v2);
252
+ t.deepEqual(comparison, []);
253
+ });
254
+
255
+ test('CustomLayout changed root - fallback - bad (no layout info)', t => {
256
+ const v1 = t.context.extractStorageLayoutFallback('CustomLayout');
257
+ const v2 = t.context.extractStorageLayoutFallback('CustomLayout_Changed_Root_Ok');
258
+ const error = t.throws(() => getStorageUpgradeErrors(v1, v2));
259
+ t.snapshot(error?.message);
260
+ });
261
+
262
+ test('CustomLayout changed root - fallback - unsafeSkipStorageCheck', t => {
263
+ const v1 = t.context.extractStorageLayoutFallback('CustomLayout');
264
+ const v2 = t.context.extractStorageLayoutFallback('CustomLayout_Changed_Root_Ok');
265
+ const comparison = getStorageUpgradeErrors(v1, v2, { unsafeSkipStorageCheck: true });
266
+ t.deepEqual(comparison, []);
267
+ });
268
+
269
+ test('CustomLayout changed to default - ok', t => {
270
+ const v1 = t.context.extractStorageLayout('CustomLayout');
271
+ const v2 = t.context.extractStorageLayout('CustomLayout_Changed_To_Default_Ok');
272
+ const comparison = getStorageUpgradeErrors(v1, v2);
273
+ t.deepEqual(comparison, []);
274
+ });
275
+
276
+ test('CustomLayout changed to default - fallback - bad (no layout info)', t => {
277
+ const v1 = t.context.extractStorageLayoutFallback('CustomLayout');
278
+ const v2 = t.context.extractStorageLayoutFallback('CustomLayout_Changed_To_Default_Ok');
279
+ const error = t.throws(() => getStorageUpgradeErrors(v1, v2));
280
+ t.snapshot(error?.message);
281
+ });
282
+
283
+ test('CustomLayout changed to default - fallback - unsafeSkipStorageCheck', t => {
284
+ const v1 = t.context.extractStorageLayoutFallback('CustomLayout');
285
+ const v2 = t.context.extractStorageLayoutFallback('CustomLayout_Changed_To_Default_Ok');
286
+ const comparison = getStorageUpgradeErrors(v1, v2, { unsafeSkipStorageCheck: true });
287
+ t.deepEqual(comparison, []);
288
+ });
289
+
290
+ test('CustomLayout unsupported node type', t => {
291
+ const error = t.throws(() => t.context.extractStorageLayout('CustomLayout_Unsupported_Node_Type'));
292
+ t.snapshot(error?.message);
293
+ });
294
+
295
+ test('Namespaced - default to custom layout without regular variables - ok', t => {
296
+ const v1 = t.context.extractStorageLayout('Namespaced_Default_Layout');
297
+ const v2 = t.context.extractStorageLayout('Namespaced_Custom_Layout_Unaffected');
298
+ const comparison = getStorageUpgradeErrors(v1, v2);
299
+ t.deepEqual(comparison, []);
300
+ });
301
+
302
+ test('Namespaced - default to custom layout without regular variables - fallback - ok', t => {
303
+ const v1 = t.context.extractStorageLayoutFallback('Namespaced_Default_Layout');
304
+ const v2 = t.context.extractStorageLayoutFallback('Namespaced_Custom_Layout_Unaffected');
305
+ const comparison = getStorageUpgradeErrors(v1, v2);
306
+ t.deepEqual(comparison, []);
307
+ });
308
+
309
+ test('Namespaced - custom layout clash', t => {
310
+ const error = t.throws(() => t.context.extractStorageLayout('Namespaced_Custom_Layout_Clash'));
311
+ t.snapshot(error?.message);
312
+ });
313
+
314
+ test('Namespaced - custom layout clash decimal', t => {
315
+ const error = t.throws(() => t.context.extractStorageLayout('Namespaced_Custom_Layout_Clash_Decimal'));
316
+ t.snapshot(error?.message);
317
+ });
318
+
319
+ test('Namespaced - custom layout clash extra colon', t => {
320
+ const error = t.throws(() => t.context.extractStorageLayout('Namespaced_Custom_Layout_Clash_Extra_Colon'));
321
+ t.snapshot(error?.message);
322
+ });
323
+
324
+ test('CustomLayout no namespace formula - ok', t => {
325
+ const layout = t.context.extractStorageLayout('CustomLayout_No_Namespace_Formula');
326
+ t.snapshot(stabilizeStorageLayout(layout));
327
+ });
328
+
329
+ test('CustomLayout unknown namespace formula - ok', t => {
330
+ const layout = t.context.extractStorageLayout('CustomLayout_Unknown_Namespace_Formula');
331
+ t.snapshot(stabilizeStorageLayout(layout));
332
+ });
333
+
334
+ test('CustomLayout multiple namespaces', t => {
335
+ const layout = t.context.extractStorageLayout('CustomLayout_Multiple_Namespaces');
336
+ t.snapshot(stabilizeStorageLayout(layout));
337
+ });
338
+
339
+ test('Gap - changed root - ok', t => {
340
+ const v1 = t.context.extractStorageLayout('Gap');
341
+ const v2 = t.context.extractStorageLayout('Gap_Changed_Root_Ok');
342
+ const comparison = getStorageUpgradeErrors(v1, v2);
343
+ t.deepEqual(comparison, []);
344
+ });
345
+
346
+ test('Gap - changed root - fallback - bad (no layout info)', t => {
347
+ const v1 = t.context.extractStorageLayoutFallback('Gap');
348
+ const v2 = t.context.extractStorageLayoutFallback('Gap_Changed_Root_Ok');
349
+ const error = t.throws(() => getStorageUpgradeErrors(v1, v2));
350
+ t.snapshot(error?.message);
351
+ });
352
+
353
+ test('Gap - changed root - fallback - unsafeSkipStorageCheck', t => {
354
+ const v1 = t.context.extractStorageLayoutFallback('Gap');
355
+ const v2 = t.context.extractStorageLayoutFallback('Gap_Changed_Root_Ok');
356
+ const comparison = getStorageUpgradeErrors(v1, v2, { unsafeSkipStorageCheck: true });
357
+ t.deepEqual(comparison, []);
358
+ });