ic-mops 2.11.0 → 2.12.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.
- package/CHANGELOG.md +7 -0
- package/bundle/cli.tgz +0 -0
- package/commands/init.ts +17 -4
- package/dist/commands/init.js +11 -4
- package/dist/helpers/migrations.js +30 -5
- package/dist/package.json +1 -1
- package/dist/tests/migrate.test.js +56 -0
- package/helpers/migrations.ts +39 -5
- package/package.json +1 -1
- package/tests/__snapshots__/migrate.test.ts.snap +43 -7
- package/tests/migrate/with-next/migrations/20250101_000000_Init.mo +3 -1
- package/tests/migrate/with-next/migrations/20250201_000000_AddName.mo +3 -1
- package/tests/migrate/with-next/migrations/20250301_000000_AddEmail.mo +3 -5
- package/tests/migrate/with-next/next-migration/20250401_000000_RenameId.mo +3 -5
- package/tests/migrate/with-next/types/State.mo +7 -0
- package/tests/migrate.test.ts +72 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
## Next
|
|
4
4
|
|
|
5
|
+
## 2.12.1
|
|
6
|
+
- `mops check`/`build`/`check-stable` skip migration staging when only the pending `next` migration is needed, so `moc` diagnostics reference the real `next-migration/<file>` path.
|
|
7
|
+
|
|
8
|
+
## 2.12.0
|
|
9
|
+
- Migration staging directory moved from `.mops/.migrations/<canister>/` to `<parent-of-chain>/.migrations-<canister>/`, so migration files can import shared modules from sibling folders (e.g. a `types/` folder next to `migrations/`) — relative imports now resolve to the same target whether moc reads the original chain dir or the staged one. The staged dir self-stamps a `.gitignore` so it doesn't pollute `git status`; `mops init` now also adds `.migrations-*/` to the project `.gitignore`
|
|
10
|
+
- `[canisters.<name>.migrations]` now requires `chain` and `next` to share the same parent directory (any layout where the parents differed is rejected with a clear error). The default layout `chain = "migrations"` + `next = "next-migration"` already satisfies this. For per-canister setups, use sibling subdirectories, e.g. `chain = "src/backend/migrations"` + `next = "src/backend/next-migration"`
|
|
11
|
+
|
|
5
12
|
## 2.11.0
|
|
6
13
|
- Add `mops migrate new <Name>` and `mops migrate freeze` commands for managing enhanced migration chains
|
|
7
14
|
- Add `[canisters.<name>.migrations]` config section with `chain`, `next`, `check-limit`, and `build-limit` fields
|
package/bundle/cli.tgz
CHANGED
|
Binary file
|
package/commands/init.ts
CHANGED
|
@@ -282,16 +282,29 @@ async function applyInit({
|
|
|
282
282
|
await template("github-workflow:mops-test");
|
|
283
283
|
}
|
|
284
284
|
|
|
285
|
-
// add
|
|
285
|
+
// add mops-managed paths to .gitignore
|
|
286
286
|
{
|
|
287
287
|
let gitignore = path.join(process.cwd(), ".gitignore");
|
|
288
288
|
let gitignoreData = existsSync(gitignore)
|
|
289
289
|
? readFileSync(gitignore).toString()
|
|
290
290
|
: "";
|
|
291
|
-
|
|
291
|
+
const additions: string[] = [];
|
|
292
292
|
if (!gitignoreData.includes(".mops")) {
|
|
293
|
-
|
|
294
|
-
|
|
293
|
+
additions.push(".mops");
|
|
294
|
+
}
|
|
295
|
+
if (!gitignoreData.includes(".migrations-")) {
|
|
296
|
+
additions.push(".migrations-*/");
|
|
297
|
+
}
|
|
298
|
+
if (additions.length > 0) {
|
|
299
|
+
let lf = gitignoreData.endsWith("\n") ? "\n" : "";
|
|
300
|
+
writeFileSync(
|
|
301
|
+
gitignore,
|
|
302
|
+
`${gitignoreData}\n${additions.join("\n")}${lf}`.trimStart(),
|
|
303
|
+
);
|
|
304
|
+
console.log(
|
|
305
|
+
chalk.green("Added"),
|
|
306
|
+
`${additions.join(", ")} to .gitignore`,
|
|
307
|
+
);
|
|
295
308
|
}
|
|
296
309
|
}
|
|
297
310
|
|
package/dist/commands/init.js
CHANGED
|
@@ -226,16 +226,23 @@ async function applyInit({ type, config, setupWorkflow, addTest, copyrightOwner,
|
|
|
226
226
|
if (setupWorkflow) {
|
|
227
227
|
await template("github-workflow:mops-test");
|
|
228
228
|
}
|
|
229
|
-
// add
|
|
229
|
+
// add mops-managed paths to .gitignore
|
|
230
230
|
{
|
|
231
231
|
let gitignore = path.join(process.cwd(), ".gitignore");
|
|
232
232
|
let gitignoreData = existsSync(gitignore)
|
|
233
233
|
? readFileSync(gitignore).toString()
|
|
234
234
|
: "";
|
|
235
|
-
|
|
235
|
+
const additions = [];
|
|
236
236
|
if (!gitignoreData.includes(".mops")) {
|
|
237
|
-
|
|
238
|
-
|
|
237
|
+
additions.push(".mops");
|
|
238
|
+
}
|
|
239
|
+
if (!gitignoreData.includes(".migrations-")) {
|
|
240
|
+
additions.push(".migrations-*/");
|
|
241
|
+
}
|
|
242
|
+
if (additions.length > 0) {
|
|
243
|
+
let lf = gitignoreData.endsWith("\n") ? "\n" : "";
|
|
244
|
+
writeFileSync(gitignore, `${gitignoreData}\n${additions.join("\n")}${lf}`.trimStart());
|
|
245
|
+
console.log(chalk.green("Added"), `${additions.join(", ")} to .gitignore`);
|
|
239
246
|
}
|
|
240
247
|
}
|
|
241
248
|
// install deps
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readdirSync, symlinkSync } from "node:fs";
|
|
2
|
-
import { join, resolve } from "node:path";
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, symlinkSync, writeFileSync, } from "node:fs";
|
|
2
|
+
import { dirname, join, resolve } from "node:path";
|
|
3
3
|
import { rm } from "node:fs/promises";
|
|
4
4
|
import chalk from "chalk";
|
|
5
5
|
import { cliError } from "../error.js";
|
|
6
|
-
import { resolveConfigPath } from "../mops.js";
|
|
7
|
-
|
|
6
|
+
import { getRootDir, resolveConfigPath } from "../mops.js";
|
|
7
|
+
function stagedMigrationsDir(chainDir, canisterName) {
|
|
8
|
+
return join(dirname(chainDir), `.migrations-${canisterName}`);
|
|
9
|
+
}
|
|
8
10
|
export function getMigrationFiles(dir) {
|
|
9
11
|
if (!existsSync(dir)) {
|
|
10
12
|
return [];
|
|
@@ -44,6 +46,19 @@ export function validateMigrationsConfig(migrations, canisterName) {
|
|
|
44
46
|
cliError(`[canisters.${canisterName}.migrations] ${field} must be a positive integer`);
|
|
45
47
|
}
|
|
46
48
|
}
|
|
49
|
+
if (migrations.next) {
|
|
50
|
+
const parentOf = (p) => dirname(resolve(getRootDir(), p));
|
|
51
|
+
const chainParent = parentOf(migrations.chain);
|
|
52
|
+
const nextParent = parentOf(migrations.next);
|
|
53
|
+
if (chainParent !== nextParent) {
|
|
54
|
+
cliError(`[canisters.${canisterName}.migrations] "chain" and "next" must live in the same parent directory.\n` +
|
|
55
|
+
` chain = "${migrations.chain}" (parent: ${chainParent})\n` +
|
|
56
|
+
` next = "${migrations.next}" (parent: ${nextParent})\n` +
|
|
57
|
+
"Place them in the same parent directory, e.g.:\n" +
|
|
58
|
+
' chain = "migrations"\n' +
|
|
59
|
+
' next = "next-migration"');
|
|
60
|
+
}
|
|
61
|
+
}
|
|
47
62
|
}
|
|
48
63
|
export async function prepareMigrationArgs(migrations, canisterName, mode, verbose) {
|
|
49
64
|
const noOp = {
|
|
@@ -83,9 +98,19 @@ export async function prepareMigrationArgs(migrations, canisterName, mode, verbo
|
|
|
83
98
|
cleanup: async () => { },
|
|
84
99
|
};
|
|
85
100
|
}
|
|
86
|
-
|
|
101
|
+
// Shortcut: when only the pending next migration is needed (empty chain or
|
|
102
|
+
// trimmed to 1), point moc at next-migration/ so diagnostics use the real path.
|
|
103
|
+
if (nextFile && nextDir && (chainFiles.length === 0 || limit === 1)) {
|
|
104
|
+
const migrationArgs = [`--enhanced-migration=${nextDir}`];
|
|
105
|
+
if (isTrimming) {
|
|
106
|
+
migrationArgs.push("-A=M0254");
|
|
107
|
+
}
|
|
108
|
+
return { migrationArgs, cleanup: async () => { } };
|
|
109
|
+
}
|
|
110
|
+
const tempDir = stagedMigrationsDir(chainDir, canisterName);
|
|
87
111
|
await rm(tempDir, { recursive: true, force: true });
|
|
88
112
|
mkdirSync(tempDir, { recursive: true });
|
|
113
|
+
writeFileSync(join(tempDir, ".gitignore"), "*\n");
|
|
89
114
|
const filesToInclude = isTrimming
|
|
90
115
|
? allMigrations.slice(-limit)
|
|
91
116
|
: allMigrations;
|
package/dist/package.json
CHANGED
|
@@ -112,6 +112,41 @@ describe("migrate", () => {
|
|
|
112
112
|
await patchMigrations(cwd, "check-limit = 2");
|
|
113
113
|
await cliSnapshot(["check", "--verbose"], { cwd }, 0);
|
|
114
114
|
});
|
|
115
|
+
test("error inside a chain migration reports its file location", async () => {
|
|
116
|
+
const cwd = await makeTempFixture("with-next");
|
|
117
|
+
await writeFile(path.join(cwd, "migrations", "20250301_000000_AddEmail.mo"), 'import State "../types/State";\n' +
|
|
118
|
+
"module {\n" +
|
|
119
|
+
" public func migration(old : State.V2) : State.V3 {\n" +
|
|
120
|
+
" { old with email = 42 };\n" +
|
|
121
|
+
" };\n" +
|
|
122
|
+
"};\n");
|
|
123
|
+
await cliSnapshot(["check"], { cwd }, 1);
|
|
124
|
+
});
|
|
125
|
+
async function corruptNextMigration(cwd) {
|
|
126
|
+
const nextDir = path.join(cwd, "next-migration");
|
|
127
|
+
const nextFile = readdirSync(nextDir).find((f) => f.endsWith(".mo"));
|
|
128
|
+
await writeFile(path.join(nextDir, nextFile), 'import State "../types/State";\n' +
|
|
129
|
+
"module {\n" +
|
|
130
|
+
" public func migration(old : State.V3) : State.V4 {\n" +
|
|
131
|
+
' { id = "wrong"; name = old.name; email = old.email };\n' +
|
|
132
|
+
" };\n" +
|
|
133
|
+
"};\n");
|
|
134
|
+
}
|
|
135
|
+
test("check-limit=1 with pending next reports real next-migration path on error", async () => {
|
|
136
|
+
const cwd = await makeTempFixture("with-next");
|
|
137
|
+
await patchMigrations(cwd, "check-limit = 1");
|
|
138
|
+
await corruptNextMigration(cwd);
|
|
139
|
+
await cliSnapshot(["check"], { cwd }, 1);
|
|
140
|
+
});
|
|
141
|
+
test("empty chain with pending next reports real next-migration path on error", async () => {
|
|
142
|
+
const cwd = await makeTempFixture("with-next");
|
|
143
|
+
const chainDir = path.join(cwd, "migrations");
|
|
144
|
+
for (const f of readdirSync(chainDir).filter((f) => f.endsWith(".mo"))) {
|
|
145
|
+
await rm(path.join(chainDir, f));
|
|
146
|
+
}
|
|
147
|
+
await corruptNextMigration(cwd);
|
|
148
|
+
await cliSnapshot(["check"], { cwd }, 1);
|
|
149
|
+
});
|
|
115
150
|
});
|
|
116
151
|
describe("build", () => {
|
|
117
152
|
test("build produces .most with full migration chain", async () => {
|
|
@@ -145,6 +180,27 @@ describe("migrate", () => {
|
|
|
145
180
|
await cliSnapshot(["check"], { cwd }, 1);
|
|
146
181
|
});
|
|
147
182
|
});
|
|
183
|
+
describe("sibling validation", () => {
|
|
184
|
+
async function patchNextDir(cwd, nextValue) {
|
|
185
|
+
const tomlPath = path.join(cwd, "mops.toml");
|
|
186
|
+
const toml = readFileSync(tomlPath, "utf-8");
|
|
187
|
+
await writeFile(tomlPath, toml.replace('next = "next-migration"', `next = "${nextValue}"`));
|
|
188
|
+
}
|
|
189
|
+
test("errors from `mops check` when chain and next have different parents", async () => {
|
|
190
|
+
const cwd = await makeTempFixture("with-next");
|
|
191
|
+
await patchNextDir(cwd, "other/next-migration");
|
|
192
|
+
const result = await cli(["check"], { cwd });
|
|
193
|
+
expect(result.exitCode).toBe(1);
|
|
194
|
+
expect(result.stderr).toMatch(/same parent directory/i);
|
|
195
|
+
});
|
|
196
|
+
test("errors from `mops migrate new` too — validation runs in every entry point", async () => {
|
|
197
|
+
const cwd = await makeTempFixture("basic");
|
|
198
|
+
await patchNextDir(cwd, "other/next-migration");
|
|
199
|
+
const result = await cli(["migrate", "new", "Test"], { cwd });
|
|
200
|
+
expect(result.exitCode).toBe(1);
|
|
201
|
+
expect(result.stderr).toMatch(/same parent directory/i);
|
|
202
|
+
});
|
|
203
|
+
});
|
|
148
204
|
describe("conflict detection", () => {
|
|
149
205
|
test("errors when both [migrations] and --enhanced-migration in args", async () => {
|
|
150
206
|
const cwd = await makeTempFixture("basic");
|
package/helpers/migrations.ts
CHANGED
|
@@ -1,12 +1,20 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
existsSync,
|
|
3
|
+
mkdirSync,
|
|
4
|
+
readdirSync,
|
|
5
|
+
symlinkSync,
|
|
6
|
+
writeFileSync,
|
|
7
|
+
} from "node:fs";
|
|
8
|
+
import { dirname, join, resolve } from "node:path";
|
|
3
9
|
import { rm } from "node:fs/promises";
|
|
4
10
|
import chalk from "chalk";
|
|
5
11
|
import { cliError } from "../error.js";
|
|
6
|
-
import { resolveConfigPath } from "../mops.js";
|
|
12
|
+
import { getRootDir, resolveConfigPath } from "../mops.js";
|
|
7
13
|
import { MigrationsConfig } from "../types.js";
|
|
8
14
|
|
|
9
|
-
|
|
15
|
+
function stagedMigrationsDir(chainDir: string, canisterName: string): string {
|
|
16
|
+
return join(dirname(chainDir), `.migrations-${canisterName}`);
|
|
17
|
+
}
|
|
10
18
|
|
|
11
19
|
export interface MigrationArgsResult {
|
|
12
20
|
migrationArgs: string[];
|
|
@@ -70,6 +78,21 @@ export function validateMigrationsConfig(
|
|
|
70
78
|
);
|
|
71
79
|
}
|
|
72
80
|
}
|
|
81
|
+
if (migrations.next) {
|
|
82
|
+
const parentOf = (p: string) => dirname(resolve(getRootDir(), p));
|
|
83
|
+
const chainParent = parentOf(migrations.chain);
|
|
84
|
+
const nextParent = parentOf(migrations.next);
|
|
85
|
+
if (chainParent !== nextParent) {
|
|
86
|
+
cliError(
|
|
87
|
+
`[canisters.${canisterName}.migrations] "chain" and "next" must live in the same parent directory.\n` +
|
|
88
|
+
` chain = "${migrations.chain}" (parent: ${chainParent})\n` +
|
|
89
|
+
` next = "${migrations.next}" (parent: ${nextParent})\n` +
|
|
90
|
+
"Place them in the same parent directory, e.g.:\n" +
|
|
91
|
+
' chain = "migrations"\n' +
|
|
92
|
+
' next = "next-migration"',
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
73
96
|
}
|
|
74
97
|
|
|
75
98
|
export async function prepareMigrationArgs(
|
|
@@ -130,9 +153,20 @@ export async function prepareMigrationArgs(
|
|
|
130
153
|
};
|
|
131
154
|
}
|
|
132
155
|
|
|
133
|
-
|
|
156
|
+
// Shortcut: when only the pending next migration is needed (empty chain or
|
|
157
|
+
// trimmed to 1), point moc at next-migration/ so diagnostics use the real path.
|
|
158
|
+
if (nextFile && nextDir && (chainFiles.length === 0 || limit === 1)) {
|
|
159
|
+
const migrationArgs = [`--enhanced-migration=${nextDir}`];
|
|
160
|
+
if (isTrimming) {
|
|
161
|
+
migrationArgs.push("-A=M0254");
|
|
162
|
+
}
|
|
163
|
+
return { migrationArgs, cleanup: async () => {} };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const tempDir = stagedMigrationsDir(chainDir, canisterName);
|
|
134
167
|
await rm(tempDir, { recursive: true, force: true });
|
|
135
168
|
mkdirSync(tempDir, { recursive: true });
|
|
169
|
+
writeFileSync(join(tempDir, ".gitignore"), "*\n");
|
|
136
170
|
|
|
137
171
|
const filesToInclude = isTrimming
|
|
138
172
|
? allMigrations.slice(-limit)
|
package/package.json
CHANGED
|
@@ -33,12 +33,12 @@ actor {
|
|
|
33
33
|
|
|
34
34
|
exports[`migrate build build-limit counts next migration as part of the chain 1`] = `
|
|
35
35
|
"// Version: 4.0.0
|
|
36
|
+
type V2__933402648 = {a : Nat; name : Text};
|
|
37
|
+
type V3__40989327 = {a : Nat; email : Text; name : Text};
|
|
38
|
+
type V4__364034734 = {email : Text; id : Nat; name : Text};
|
|
36
39
|
{
|
|
37
|
-
"20250301_000000_AddEmail" :
|
|
38
|
-
|
|
39
|
-
"20250401_000000_RenameId" :
|
|
40
|
-
(old : {a : Nat; email : Text; name : Text}) ->
|
|
41
|
-
{email : Text; id : Nat; name : Text}
|
|
40
|
+
"20250301_000000_AddEmail" : (old : V2__933402648) -> V3__40989327;
|
|
41
|
+
"20250401_000000_RenameId" : (old : V3__40989327) -> V4__364034734
|
|
42
42
|
}
|
|
43
43
|
actor {
|
|
44
44
|
stable email : Text;
|
|
@@ -75,16 +75,52 @@ exports[`migrate check check with trimming shows reduced chain 1`] = `
|
|
|
75
75
|
"stdout": "check Using --all-libs for richer diagnostics
|
|
76
76
|
migrations Prepared 2 migration(s) for backend (trimmed from 3)
|
|
77
77
|
check Checking canister backend:
|
|
78
|
-
<CACHE>moc-wrapper ["src/main.mo","--check","--all-libs","--default-persistent-actors","--enhanced-migration=.
|
|
78
|
+
<CACHE>moc-wrapper ["src/main.mo","--check","--all-libs","--default-persistent-actors","--enhanced-migration=.migrations-backend","-A=M0254"]
|
|
79
79
|
✓ backend
|
|
80
80
|
check-stable Generating stable types for src/main.mo
|
|
81
|
-
<CACHE>moc-wrapper ["--stable-types","-o",".mops/.check-stable/new.wasm","src/main.mo","--default-persistent-actors","--enhanced-migration=.
|
|
81
|
+
<CACHE>moc-wrapper ["--stable-types","-o",".mops/.check-stable/new.wasm","src/main.mo","--default-persistent-actors","--enhanced-migration=.migrations-backend","-A=M0254"]
|
|
82
82
|
check-stable Comparing deployed.most ↔ .mops/.check-stable/new.most
|
|
83
83
|
<CACHE>moc-wrapper ["--stable-compatible","deployed.most",".mops/.check-stable/new.most"]
|
|
84
84
|
✓ Stable compatibility check passed for canister 'backend'",
|
|
85
85
|
}
|
|
86
86
|
`;
|
|
87
87
|
|
|
88
|
+
exports[`migrate check check-limit=1 with pending next reports real next-migration path on error 1`] = `
|
|
89
|
+
{
|
|
90
|
+
"exitCode": 1,
|
|
91
|
+
"stderr": "next-migration/20250401_000000_RenameId.mo:4.12-4.19: type error [M0050], literal of type
|
|
92
|
+
Text
|
|
93
|
+
does not have expected type
|
|
94
|
+
Nat
|
|
95
|
+
✗ Check failed for canister backend (exit code: 1)",
|
|
96
|
+
"stdout": "",
|
|
97
|
+
}
|
|
98
|
+
`;
|
|
99
|
+
|
|
100
|
+
exports[`migrate check empty chain with pending next reports real next-migration path on error 1`] = `
|
|
101
|
+
{
|
|
102
|
+
"exitCode": 1,
|
|
103
|
+
"stderr": "next-migration/20250401_000000_RenameId.mo:4.12-4.19: type error [M0050], literal of type
|
|
104
|
+
Text
|
|
105
|
+
does not have expected type
|
|
106
|
+
Nat
|
|
107
|
+
✗ Check failed for canister backend (exit code: 1)",
|
|
108
|
+
"stdout": "",
|
|
109
|
+
}
|
|
110
|
+
`;
|
|
111
|
+
|
|
112
|
+
exports[`migrate check error inside a chain migration reports its file location 1`] = `
|
|
113
|
+
{
|
|
114
|
+
"exitCode": 1,
|
|
115
|
+
"stderr": ".migrations-backend/20250301_000000_AddEmail.mo:4.24-4.26: type error [M0050], literal of type
|
|
116
|
+
Nat
|
|
117
|
+
does not have expected type
|
|
118
|
+
Text
|
|
119
|
+
✗ Check failed for canister backend (exit code: 1)",
|
|
120
|
+
"stdout": "",
|
|
121
|
+
}
|
|
122
|
+
`;
|
|
123
|
+
|
|
88
124
|
exports[`migrate migrate freeze moves the file from next to chain 1`] = `
|
|
89
125
|
{
|
|
90
126
|
"exitCode": 0,
|
|
@@ -1,9 +1,7 @@
|
|
|
1
|
+
import State "../types/State";
|
|
2
|
+
|
|
1
3
|
module {
|
|
2
|
-
public func migration(old :
|
|
3
|
-
id : Nat;
|
|
4
|
-
name : Text;
|
|
5
|
-
email : Text;
|
|
6
|
-
} {
|
|
4
|
+
public func migration(old : State.V3) : State.V4 {
|
|
7
5
|
{ id = old.a; name = old.name; email = old.email };
|
|
8
6
|
};
|
|
9
7
|
};
|
package/tests/migrate.test.ts
CHANGED
|
@@ -154,6 +154,51 @@ describe("migrate", () => {
|
|
|
154
154
|
await patchMigrations(cwd, "check-limit = 2");
|
|
155
155
|
await cliSnapshot(["check", "--verbose"], { cwd }, 0);
|
|
156
156
|
});
|
|
157
|
+
|
|
158
|
+
test("error inside a chain migration reports its file location", async () => {
|
|
159
|
+
const cwd = await makeTempFixture("with-next");
|
|
160
|
+
await writeFile(
|
|
161
|
+
path.join(cwd, "migrations", "20250301_000000_AddEmail.mo"),
|
|
162
|
+
'import State "../types/State";\n' +
|
|
163
|
+
"module {\n" +
|
|
164
|
+
" public func migration(old : State.V2) : State.V3 {\n" +
|
|
165
|
+
" { old with email = 42 };\n" +
|
|
166
|
+
" };\n" +
|
|
167
|
+
"};\n",
|
|
168
|
+
);
|
|
169
|
+
await cliSnapshot(["check"], { cwd }, 1);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
async function corruptNextMigration(cwd: string): Promise<void> {
|
|
173
|
+
const nextDir = path.join(cwd, "next-migration");
|
|
174
|
+
const nextFile = readdirSync(nextDir).find((f) => f.endsWith(".mo"))!;
|
|
175
|
+
await writeFile(
|
|
176
|
+
path.join(nextDir, nextFile),
|
|
177
|
+
'import State "../types/State";\n' +
|
|
178
|
+
"module {\n" +
|
|
179
|
+
" public func migration(old : State.V3) : State.V4 {\n" +
|
|
180
|
+
' { id = "wrong"; name = old.name; email = old.email };\n' +
|
|
181
|
+
" };\n" +
|
|
182
|
+
"};\n",
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
test("check-limit=1 with pending next reports real next-migration path on error", async () => {
|
|
187
|
+
const cwd = await makeTempFixture("with-next");
|
|
188
|
+
await patchMigrations(cwd, "check-limit = 1");
|
|
189
|
+
await corruptNextMigration(cwd);
|
|
190
|
+
await cliSnapshot(["check"], { cwd }, 1);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test("empty chain with pending next reports real next-migration path on error", async () => {
|
|
194
|
+
const cwd = await makeTempFixture("with-next");
|
|
195
|
+
const chainDir = path.join(cwd, "migrations");
|
|
196
|
+
for (const f of readdirSync(chainDir).filter((f) => f.endsWith(".mo"))) {
|
|
197
|
+
await rm(path.join(chainDir, f));
|
|
198
|
+
}
|
|
199
|
+
await corruptNextMigration(cwd);
|
|
200
|
+
await cliSnapshot(["check"], { cwd }, 1);
|
|
201
|
+
});
|
|
157
202
|
});
|
|
158
203
|
|
|
159
204
|
describe("build", () => {
|
|
@@ -207,6 +252,33 @@ describe("migrate", () => {
|
|
|
207
252
|
});
|
|
208
253
|
});
|
|
209
254
|
|
|
255
|
+
describe("sibling validation", () => {
|
|
256
|
+
async function patchNextDir(cwd: string, nextValue: string): Promise<void> {
|
|
257
|
+
const tomlPath = path.join(cwd, "mops.toml");
|
|
258
|
+
const toml = readFileSync(tomlPath, "utf-8");
|
|
259
|
+
await writeFile(
|
|
260
|
+
tomlPath,
|
|
261
|
+
toml.replace('next = "next-migration"', `next = "${nextValue}"`),
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
test("errors from `mops check` when chain and next have different parents", async () => {
|
|
266
|
+
const cwd = await makeTempFixture("with-next");
|
|
267
|
+
await patchNextDir(cwd, "other/next-migration");
|
|
268
|
+
const result = await cli(["check"], { cwd });
|
|
269
|
+
expect(result.exitCode).toBe(1);
|
|
270
|
+
expect(result.stderr).toMatch(/same parent directory/i);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
test("errors from `mops migrate new` too — validation runs in every entry point", async () => {
|
|
274
|
+
const cwd = await makeTempFixture("basic");
|
|
275
|
+
await patchNextDir(cwd, "other/next-migration");
|
|
276
|
+
const result = await cli(["migrate", "new", "Test"], { cwd });
|
|
277
|
+
expect(result.exitCode).toBe(1);
|
|
278
|
+
expect(result.stderr).toMatch(/same parent directory/i);
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
|
|
210
282
|
describe("conflict detection", () => {
|
|
211
283
|
test("errors when both [migrations] and --enhanced-migration in args", async () => {
|
|
212
284
|
const cwd = await makeTempFixture("basic");
|