@jant/core 0.7.0 → 0.7.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/bin/commands/export.js +3 -1
- package/bin/commands/import-site.js +689 -216
- package/bin/commands/setup.js +136 -0
- package/bin/commands/site/export.js +71 -34
- package/bin/commands/site/pull-media.js +2 -6
- package/bin/commands/site/snapshot/export.js +18 -60
- package/bin/commands/site/snapshot/import.js +22 -23
- package/bin/lib/d1-query.js +87 -2
- package/bin/lib/hugo-markdown.js +4 -0
- package/bin/lib/site-pull-media.js +21 -28
- package/bin/lib/site-selection.js +10 -1
- package/bin/lib/site-snapshot.js +338 -3
- package/bin/lib/sql-export.js +68 -5
- package/bin/lib/wrangler-cli.js +9 -0
- package/bin/lib/zip-archive.js +187 -0
- package/dist/{app-K_Aa1MMn.js → app-ZcaI1kPN.js} +732 -217
- package/dist/client/.vite/manifest.json +20 -20
- package/dist/client/_assets/chunks/{create-editor-CD3FhrOB.js → create-editor-B-m7X7S5.js} +50 -46
- package/dist/client/_assets/chunks/{sortable-list-CgaL2jCs.js → sortable-list-BJyd-LXE.js} +1 -1
- package/dist/client/_assets/chunks/{unsafe-svg-0QCkP0vZ.js → unsafe-svg-BkscJx69.js} +2 -2
- package/dist/client/_assets/{client-GiYENVw8.css → client-DAhoqPdr.css} +1 -1
- package/dist/client/_assets/{client-auth-k8gJ6QJj.js → client-auth-CAMfTrKW.js} +1 -1
- package/dist/client/_assets/{client-compose-B_kDtWkW.js → client-compose-D6rExKVK.js} +1 -1
- package/dist/client/_assets/{client-VnFxJN7G.js → client-knSEyUJO.js} +1 -1
- package/dist/client/_assets/{client-manage-M90aSTOk.js → client-manage-sVOkqVbH.js} +1 -1
- package/dist/client/_assets/{client-settings-DfYs9n1F.js → client-settings-CGR_nZ78.js} +7 -7
- package/dist/{github-sync-BPAvT999.js → github-sync-Gw4orAHk.js} +799 -110
- package/dist/index.js +2 -2
- package/dist/node.js +9 -5
- package/package.json +13 -3
- package/src/__tests__/dev-scripts.test.ts +203 -0
- package/src/__tests__/export-collection-order.test.ts +276 -0
- package/src/__tests__/export-feed-ids.test.ts +184 -0
- package/src/__tests__/export-feed-order.test.ts +294 -0
- package/src/__tests__/export-hugo-build.test.ts +130 -0
- package/src/__tests__/export-import-roundtrip.test.ts +94 -0
- package/src/__tests__/export-service.test.ts +611 -28
- package/src/__tests__/export-smart-collection.test.ts +329 -0
- package/src/__tests__/helpers/hugo-site.ts +146 -0
- package/src/__tests__/import-site-command.test.ts +487 -1
- package/src/__tests__/mise-config.test.ts +75 -4
- package/src/__tests__/node-dev-tasks.test.ts +264 -0
- package/src/__tests__/site-export-canonical-import.test.ts +149 -0
- package/src/__tests__/snapshot-canonical-replay.test.ts +179 -0
- package/src/__tests__/snapshot-settings.test.ts +97 -0
- package/src/__tests__/snapshot-tables.test.ts +219 -0
- package/src/__tests__/sql-export.test.ts +173 -0
- package/src/__tests__/zip-archive.test.ts +106 -0
- package/src/app.tsx +15 -6
- package/src/client/components/__tests__/jant-settings-avatar.test.ts +1 -1
- package/src/client/components/__tests__/jant-settings-general.test.ts +21 -1
- package/src/client/components/jant-repo-picker-types.ts +6 -1
- package/src/client/components/jant-repo-picker.ts +4 -7
- package/src/client/components/jant-settings-general.ts +17 -12
- package/src/client/tiptap/__tests__/list-editing.test.ts +224 -86
- package/src/client/tiptap/__tests__/mark-exit.test.ts +1 -2
- package/src/client/tiptap/__tests__/markdown-clipboard.test.ts +26 -0
- package/src/client/tiptap/extensions.ts +0 -3
- package/src/client/tiptap/structural-keymap.ts +158 -47
- package/src/db/__tests__/d1-query.test.ts +56 -1
- package/src/i18n/locales/settings/en.po +8 -8
- package/src/i18n/locales/settings/en.ts +1 -1
- package/src/i18n/locales/settings/zh-Hans.po +8 -8
- package/src/i18n/locales/settings/zh-Hans.ts +1 -1
- package/src/i18n/locales/settings/zh-Hant.po +8 -8
- package/src/i18n/locales/settings/zh-Hant.ts +1 -1
- package/src/lib/__tests__/github-sync-repo-name.test.ts +40 -0
- package/src/lib/__tests__/image.test.ts +27 -1
- package/src/lib/__tests__/markdown-to-tiptap.test.ts +105 -0
- package/src/lib/__tests__/markdown.test.ts +10 -0
- package/src/lib/__tests__/resolve-config.test.ts +67 -0
- package/src/lib/__tests__/schemas.test.ts +27 -1
- package/src/lib/__tests__/timeline.test.ts +87 -0
- package/src/lib/__tests__/tiptap-to-markdown.test.ts +172 -4
- package/src/lib/discover.ts +3 -1
- package/src/lib/github-sync-repo-name.ts +45 -0
- package/src/lib/hugo-markdown.ts +46 -0
- package/src/lib/image.ts +17 -4
- package/src/lib/markdown-manager.ts +392 -2
- package/src/lib/post-body-html.ts +11 -4
- package/src/lib/resolve-config.ts +58 -1
- package/src/lib/schemas.ts +50 -5
- package/src/lib/thread-fold.ts +3 -3
- package/src/lib/timeline.ts +1 -1
- package/src/lib/tiptap-to-markdown.ts +13 -7
- package/src/lib/url.ts +20 -0
- package/src/lib/view.ts +2 -2
- package/src/node/__tests__/cli-setup.test.ts +163 -0
- package/src/node/__tests__/cli-site-snapshot.test.ts +67 -0
- package/src/node/__tests__/cli-snapshot-meta.test.ts +20 -0
- package/src/node/__tests__/runtime.test.ts +38 -0
- package/src/node/index.ts +2 -0
- package/src/node/request-handler.ts +3 -1
- package/src/routes/api/__tests__/posts.test.ts +24 -0
- package/src/routes/api/__tests__/upload.test.ts +34 -0
- package/src/routes/api/export.ts +3 -3
- package/src/routes/api/internal/sites.ts +0 -1
- package/src/routes/api/posts.ts +2 -0
- package/src/routes/api/public/posts.ts +21 -1
- package/src/routes/api/upload.ts +10 -3
- package/src/routes/compose.tsx +4 -0
- package/src/routes/dash/__tests__/github-sync-app.test.ts +365 -0
- package/src/routes/dash/settings.tsx +212 -70
- package/src/routes/pages/__tests__/post-page-round-trips.test.ts +211 -0
- package/src/routes/pages/__tests__/thread-order.test.ts +179 -0
- package/src/routes/pages/archive.tsx +17 -11
- package/src/routes/pages/featured.tsx +11 -5
- package/src/routes/pages/page.tsx +53 -31
- package/src/routes/pages/search.tsx +4 -2
- package/src/runtime/__tests__/readiness.test.ts +23 -0
- package/src/runtime/node.ts +49 -0
- package/src/runtime/readiness.ts +15 -0
- package/src/services/__tests__/bootstrap-setup-instance.test.ts +230 -0
- package/src/services/__tests__/custom-url.test.ts +34 -0
- package/src/services/__tests__/github-app-installations.test.ts +153 -0
- package/src/services/__tests__/github-sync-push.test.ts +46 -0
- package/src/services/__tests__/media.test.ts +31 -0
- package/src/services/__tests__/path.test.ts +56 -0
- package/src/services/__tests__/post-timeline.test.ts +127 -0
- package/src/services/__tests__/post.test.ts +74 -0
- package/src/services/bootstrap.ts +263 -44
- package/src/services/custom-url.ts +2 -2
- package/src/services/export-theme/layouts/_default/alias.html +27 -1
- package/src/services/export-theme/layouts/_default/list.html +2 -63
- package/src/services/export-theme/layouts/_default/rss.xml +27 -38
- package/src/services/export-theme/layouts/collections/list.html +3 -3
- package/src/services/export-theme/layouts/featured/list.html +1 -4
- package/src/services/export-theme/layouts/index.html +40 -26
- package/src/services/export-theme/layouts/partials/collection-members.html +100 -0
- package/src/services/export-theme/layouts/partials/collection-threads.html +33 -0
- package/src/services/export-theme/layouts/partials/featured-members.html +35 -0
- package/src/services/export-theme/layouts/partials/featured-thread.html +1 -1
- package/src/services/export-theme/layouts/partials/footer.html +1 -1
- package/src/services/export-theme/layouts/partials/head.html +1 -1
- package/src/services/export-theme/layouts/partials/header.html +5 -3
- package/src/services/export-theme/layouts/partials/jant-data.html +23 -0
- package/src/services/export-theme/layouts/partials/latest-members.html +48 -0
- package/src/services/export-theme/layouts/partials/smart-collection-members.html +129 -0
- package/src/services/export-theme/layouts/partials/thread-preview.html +1 -1
- package/src/services/export-theme/layouts/post/list.html +1 -1
- package/src/services/export-theme/layouts/smart_collection/list.html +24 -0
- package/src/services/export-theme/styles/main.css +0 -1
- package/src/services/export-theme/theme.toml +1 -1
- package/src/services/export.ts +620 -71
- package/src/services/github-app-installations.ts +171 -4
- package/src/services/github-sync.ts +69 -15
- package/src/services/mcp.ts +19 -1
- package/src/services/media.ts +8 -1
- package/src/services/path.ts +34 -1
- package/src/services/post.ts +214 -39
- package/src/services/site-admin.ts +3 -7
- package/src/services/site.ts +83 -31
- package/src/styles/ui.css +7 -1
- package/src/types/app-context.ts +16 -0
- package/src/types/bindings.ts +5 -0
- package/src/types/operations.ts +10 -0
- package/src/ui/dash/settings/GeneralContent.tsx +10 -8
- package/src/client/tiptap/exitable-marks.ts +0 -73
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local Node Dev Task Guard
|
|
3
|
+
*
|
|
4
|
+
* `db-node-bootstrap-shell` and `db-node-clean` were added in 4bc0bfe8 and
|
|
5
|
+
* never touched the Node database. The shell migrated it, then set up the
|
|
6
|
+
* local Wrangler D1 database and wrote credentials to `.dev.vars`, leaving the
|
|
7
|
+
* Node site at `/setup`. Clean deleted `packages/core/.data`, a directory the
|
|
8
|
+
* Node runtime no longer uses. Both exited 0, so nothing noticed.
|
|
9
|
+
*
|
|
10
|
+
* These tests run each task's own script through `dev/run-script.mjs` against
|
|
11
|
+
* a throwaway directory, and check the database and env file it leaves. The
|
|
12
|
+
* runner starts Vite, hence the timeouts.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { spawnSync } from "node:child_process";
|
|
16
|
+
import {
|
|
17
|
+
existsSync,
|
|
18
|
+
mkdirSync,
|
|
19
|
+
mkdtempSync,
|
|
20
|
+
readFileSync,
|
|
21
|
+
rmSync,
|
|
22
|
+
writeFileSync,
|
|
23
|
+
} from "node:fs";
|
|
24
|
+
import { tmpdir } from "node:os";
|
|
25
|
+
import { join, resolve } from "node:path";
|
|
26
|
+
import Database from "better-sqlite3";
|
|
27
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
28
|
+
import { writeScriptEnvValues } from "../../dev/script-env.js";
|
|
29
|
+
import { DEFAULT_NAVIGATION_PROFILE } from "../types/constants.js";
|
|
30
|
+
|
|
31
|
+
const CORE_DIR = resolve(import.meta.dirname, "../..");
|
|
32
|
+
|
|
33
|
+
/** Variables that would point a task somewhere other than the temp directory. */
|
|
34
|
+
const TARGET_VARIABLES = [
|
|
35
|
+
"AUTH_SECRET",
|
|
36
|
+
"DATABASE_URL",
|
|
37
|
+
"DATA_DIR",
|
|
38
|
+
"DEMO_EMAIL",
|
|
39
|
+
"DEMO_PASSWORD",
|
|
40
|
+
"DEV_API_TOKEN",
|
|
41
|
+
"LOCAL_STORAGE_PATH",
|
|
42
|
+
"PORT",
|
|
43
|
+
"SITE_RESOLUTION_MODE",
|
|
44
|
+
"STORAGE_DRIVER",
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
function taskEnv(
|
|
48
|
+
overrides: Record<string, string>,
|
|
49
|
+
): Record<string, string | undefined> {
|
|
50
|
+
const env: Record<string, string | undefined> = {
|
|
51
|
+
...process.env,
|
|
52
|
+
// No announcement to a Discover directory from a test database.
|
|
53
|
+
DISCOVER_PING_URL: "",
|
|
54
|
+
};
|
|
55
|
+
for (const name of TARGET_VARIABLES) {
|
|
56
|
+
delete env[name];
|
|
57
|
+
}
|
|
58
|
+
return { ...env, ...overrides };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function runScript(
|
|
62
|
+
script: string,
|
|
63
|
+
args: string[],
|
|
64
|
+
env: Record<string, string | undefined>,
|
|
65
|
+
) {
|
|
66
|
+
const result = spawnSync(
|
|
67
|
+
process.execPath,
|
|
68
|
+
["dev/run-script.mjs", `dev/scripts/${script}`, ...args],
|
|
69
|
+
{ cwd: CORE_DIR, encoding: "utf8", env, timeout: 120_000 },
|
|
70
|
+
);
|
|
71
|
+
expect(result.error).toBeUndefined();
|
|
72
|
+
return { ...result, output: `${result.stdout}\n${result.stderr}` };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function envFileLines(path: string): string[] {
|
|
76
|
+
return readFileSync(path, "utf8").split("\n");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
describe("local Node dev tasks", () => {
|
|
80
|
+
const tempDirs: string[] = [];
|
|
81
|
+
|
|
82
|
+
afterEach(() => {
|
|
83
|
+
for (const dir of tempDirs.splice(0)) {
|
|
84
|
+
rmSync(dir, { recursive: true, force: true });
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
function makeTempDir(prefix: string): string {
|
|
89
|
+
const dir = mkdtempSync(join(tmpdir(), prefix));
|
|
90
|
+
tempDirs.push(dir);
|
|
91
|
+
return dir;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
it(
|
|
95
|
+
"db-node-bootstrap-shell sets up the Node database and writes dev credentials to the env file",
|
|
96
|
+
{ timeout: 300_000 },
|
|
97
|
+
() => {
|
|
98
|
+
const dir = makeTempDir("jant-bootstrap-shell-");
|
|
99
|
+
const envPath = join(dir, "test.env");
|
|
100
|
+
const ownLines = ["# Local settings", `DATA_DIR=${dir}`];
|
|
101
|
+
writeFileSync(envPath, `${ownLines.join("\n")}\n`);
|
|
102
|
+
const env = taskEnv({ JANT_ENV_FILE: envPath });
|
|
103
|
+
|
|
104
|
+
const first = runScript("bootstrap-node-dev.ts", [], env);
|
|
105
|
+
expect(first.status, first.output).toBe(0);
|
|
106
|
+
expect(first.stdout).toContain("created the site and the dev account");
|
|
107
|
+
expect(first.stdout).not.toMatch(/do(es)? not sign in/);
|
|
108
|
+
|
|
109
|
+
// The file's own lines stay as they were; the credentials follow as one
|
|
110
|
+
// block below a blank line.
|
|
111
|
+
const written = envFileLines(envPath);
|
|
112
|
+
expect(written.slice(0, 3)).toEqual([...ownLines, ""]);
|
|
113
|
+
expect(written.slice(3)).toEqual([
|
|
114
|
+
expect.stringMatching(/^AUTH_SECRET=\S{32,}$/),
|
|
115
|
+
expect.stringMatching(/^DEV_API_TOKEN=jnt_dev_[0-9a-f]{32}$/),
|
|
116
|
+
"DEMO_EMAIL=debug@jant.test",
|
|
117
|
+
"DEMO_PASSWORD=jant-dev-debug-login",
|
|
118
|
+
"",
|
|
119
|
+
]);
|
|
120
|
+
|
|
121
|
+
const sqlite = new Database(join(dir, "jant.sqlite"), {
|
|
122
|
+
readonly: true,
|
|
123
|
+
});
|
|
124
|
+
try {
|
|
125
|
+
expect(
|
|
126
|
+
sqlite
|
|
127
|
+
.prepare(
|
|
128
|
+
`SELECT "user"."email" FROM "user" JOIN "account" ON "account"."user_id" = "user"."id" WHERE "account"."provider_id" = 'credential'`,
|
|
129
|
+
)
|
|
130
|
+
.all(),
|
|
131
|
+
).toEqual([{ email: "debug@jant.test" }]);
|
|
132
|
+
expect(
|
|
133
|
+
sqlite
|
|
134
|
+
.prepare(
|
|
135
|
+
`SELECT "value" FROM "site_setting" WHERE "key" = 'ONBOARDING_STATUS'`,
|
|
136
|
+
)
|
|
137
|
+
.get(),
|
|
138
|
+
).toEqual({ value: "completed" });
|
|
139
|
+
expect(
|
|
140
|
+
(
|
|
141
|
+
sqlite.prepare(`SELECT "system_key" FROM "nav_item"`).all() as {
|
|
142
|
+
system_key: string;
|
|
143
|
+
}[]
|
|
144
|
+
)
|
|
145
|
+
.map((row) => row.system_key)
|
|
146
|
+
.sort(),
|
|
147
|
+
).toEqual([...DEFAULT_NAVIGATION_PROFILE.systemKeys].sort());
|
|
148
|
+
expect(
|
|
149
|
+
sqlite.prepare(`SELECT COUNT(*) AS count FROM "post"`).get(),
|
|
150
|
+
).toEqual({ count: 0 });
|
|
151
|
+
} finally {
|
|
152
|
+
sqlite.close();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// A second run finds the site set up, and keeps the generated secrets.
|
|
156
|
+
const afterFirst = readFileSync(envPath, "utf8");
|
|
157
|
+
const second = runScript("bootstrap-node-dev.ts", [], env);
|
|
158
|
+
expect(second.status, second.output).toBe(0);
|
|
159
|
+
expect(second.stdout).toContain("already set up, left unchanged");
|
|
160
|
+
expect(second.stdout).not.toMatch(/do(es)? not sign in/);
|
|
161
|
+
expect(readFileSync(envPath, "utf8")).toBe(afterFirst);
|
|
162
|
+
|
|
163
|
+
// A new password is recorded, but not set on the existing account, and
|
|
164
|
+
// the task says so.
|
|
165
|
+
const third = runScript(
|
|
166
|
+
"bootstrap-node-dev.ts",
|
|
167
|
+
["another-password"],
|
|
168
|
+
env,
|
|
169
|
+
);
|
|
170
|
+
expect(third.status, third.output).toBe(0);
|
|
171
|
+
expect(third.stdout).toContain(
|
|
172
|
+
"debug@jant.test has a different password",
|
|
173
|
+
);
|
|
174
|
+
expect(envFileLines(envPath)).toContain("DEMO_PASSWORD=another-password");
|
|
175
|
+
},
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
it(
|
|
179
|
+
"db-node-clean deletes the SQLite database, its WAL files, and media, and nothing else",
|
|
180
|
+
{ timeout: 150_000 },
|
|
181
|
+
() => {
|
|
182
|
+
const dir = makeTempDir("jant-clean-");
|
|
183
|
+
const deleted = [
|
|
184
|
+
join(dir, "jant.sqlite"),
|
|
185
|
+
join(dir, "jant.sqlite-wal"),
|
|
186
|
+
join(dir, "jant.sqlite-shm"),
|
|
187
|
+
join(dir, "media"),
|
|
188
|
+
];
|
|
189
|
+
const kept = join(dir, "notes.txt");
|
|
190
|
+
mkdirSync(join(dir, "media/2026"), { recursive: true });
|
|
191
|
+
writeFileSync(join(dir, "media/2026/photo.webp"), "image");
|
|
192
|
+
for (const file of deleted.slice(0, 3)) {
|
|
193
|
+
writeFileSync(file, "");
|
|
194
|
+
}
|
|
195
|
+
writeFileSync(kept, "keep");
|
|
196
|
+
|
|
197
|
+
const result = runScript(
|
|
198
|
+
"clean-node-dev.ts",
|
|
199
|
+
[],
|
|
200
|
+
taskEnv({ JANT_ENV_FILE: "", DATA_DIR: dir }),
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
expect(result.status, result.output).toBe(0);
|
|
204
|
+
expect(deleted.filter((path) => existsSync(path))).toEqual([]);
|
|
205
|
+
expect(existsSync(kept)).toBe(true);
|
|
206
|
+
},
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
it("db-node-clean refuses a Postgres database", { timeout: 150_000 }, () => {
|
|
210
|
+
const result = runScript(
|
|
211
|
+
"clean-node-dev.ts",
|
|
212
|
+
[],
|
|
213
|
+
taskEnv({
|
|
214
|
+
JANT_ENV_FILE: "",
|
|
215
|
+
DATABASE_URL: "postgres://jant:jant@127.0.0.1:9/jant_dev",
|
|
216
|
+
}),
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
expect(result.status).not.toBe(0);
|
|
220
|
+
expect(result.output).toContain(
|
|
221
|
+
"db-node-clean only supports Node SQLite development databases.",
|
|
222
|
+
);
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
describe("writeScriptEnvValues", () => {
|
|
227
|
+
const tempDirs: string[] = [];
|
|
228
|
+
|
|
229
|
+
afterEach(() => {
|
|
230
|
+
for (const dir of tempDirs.splice(0)) {
|
|
231
|
+
rmSync(dir, { recursive: true, force: true });
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it("replaces keys in place and appends new ones as one block", () => {
|
|
236
|
+
const dir = mkdtempSync(join(tmpdir(), "jant-script-env-"));
|
|
237
|
+
tempDirs.push(dir);
|
|
238
|
+
const envPath = join(dir, ".env.node");
|
|
239
|
+
writeFileSync(
|
|
240
|
+
envPath,
|
|
241
|
+
"# Database\nDATABASE_URL=file:./data/jant.sqlite\nDEMO_PASSWORD=old\n\n\n",
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
writeScriptEnvValues(envPath, {
|
|
245
|
+
DEMO_PASSWORD: "new",
|
|
246
|
+
AUTH_SECRET: "secret",
|
|
247
|
+
DEV_API_TOKEN: "token",
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
expect(readFileSync(envPath, "utf8")).toBe(
|
|
251
|
+
"# Database\nDATABASE_URL=file:./data/jant.sqlite\nDEMO_PASSWORD=new\n\nAUTH_SECRET=secret\nDEV_API_TOKEN=token\n",
|
|
252
|
+
);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it("writes a new file without a leading blank line", () => {
|
|
256
|
+
const dir = mkdtempSync(join(tmpdir(), "jant-script-env-"));
|
|
257
|
+
tempDirs.push(dir);
|
|
258
|
+
const envPath = join(dir, ".env.node");
|
|
259
|
+
|
|
260
|
+
writeScriptEnvValues(envPath, { AUTH_SECRET: "secret" });
|
|
261
|
+
|
|
262
|
+
expect(readFileSync(envPath, "utf8")).toBe("AUTH_SECRET=secret\n");
|
|
263
|
+
});
|
|
264
|
+
});
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical Site-Export Import Guard
|
|
3
|
+
*
|
|
4
|
+
* `sites/demo-source/canonical/site-export/` is a committed `jant site export`.
|
|
5
|
+
* `mise run db-node-load-demo` loads it the portable way: migrate a local Node
|
|
6
|
+
* database, set it up, serve the app on a loopback port, and run
|
|
7
|
+
* `jant site import` against that server.
|
|
8
|
+
*
|
|
9
|
+
* Nothing ran that path, and it broke twice without a sign. From b259f4e3 the
|
|
10
|
+
* task called `site import` without the site URL the command had come to
|
|
11
|
+
* require. From 6c79f0b4 the importer sent boolean settings the settings route
|
|
12
|
+
* rejects, so importing any export stopped before it created a post. This test
|
|
13
|
+
* runs the task's own script against a throwaway SQLite database, so a change
|
|
14
|
+
* to the importer, the API it calls, the fixture, or the task fails here.
|
|
15
|
+
*
|
|
16
|
+
* `JANT_ENV_FILE` is empty for the whole suite (`vitest.config.ts`), and the
|
|
17
|
+
* script honours it, so a developer's `.env.node` is neither read nor
|
|
18
|
+
* rewritten. The script runs through `dev/run-script.mjs`, which starts Vite,
|
|
19
|
+
* hence the timeout.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { spawnSync } from "node:child_process";
|
|
23
|
+
import { mkdtempSync, readdirSync, rmSync } from "node:fs";
|
|
24
|
+
import { tmpdir } from "node:os";
|
|
25
|
+
import { basename, join, resolve } from "node:path";
|
|
26
|
+
import Database from "better-sqlite3";
|
|
27
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
28
|
+
import { __test__ as importSite } from "../../bin/commands/import-site.js";
|
|
29
|
+
|
|
30
|
+
const CORE_DIR = resolve(import.meta.dirname, "../..");
|
|
31
|
+
const CANONICAL_DIR = resolve(
|
|
32
|
+
CORE_DIR,
|
|
33
|
+
"../../sites/demo-source/canonical/site-export",
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
function count(sqlite: Database.Database, sql: string): number {
|
|
37
|
+
return (sqlite.prepare(sql).get() as { count: number }).count;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
describe("canonical demo site-export", () => {
|
|
41
|
+
const tempDirs: string[] = [];
|
|
42
|
+
|
|
43
|
+
afterEach(() => {
|
|
44
|
+
for (const dir of tempDirs.splice(0)) {
|
|
45
|
+
rmSync(dir, { recursive: true, force: true });
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it(
|
|
50
|
+
"imports through `jant site import` into a local Node database at head",
|
|
51
|
+
{ timeout: 150_000 },
|
|
52
|
+
async () => {
|
|
53
|
+
const dir = mkdtempSync(join(tmpdir(), "jant-load-demo-"));
|
|
54
|
+
tempDirs.push(dir);
|
|
55
|
+
const databasePath = join(dir, "jant.sqlite");
|
|
56
|
+
|
|
57
|
+
const env: Record<string, string | undefined> = {
|
|
58
|
+
...process.env,
|
|
59
|
+
JANT_ENV_FILE: "",
|
|
60
|
+
DATABASE_URL: `file:${databasePath}`,
|
|
61
|
+
DATA_DIR: dir,
|
|
62
|
+
LOCAL_STORAGE_PATH: join(dir, "media"),
|
|
63
|
+
STORAGE_DRIVER: "local",
|
|
64
|
+
// No announcement to a Discover directory from a test database.
|
|
65
|
+
DISCOVER_PING_URL: "",
|
|
66
|
+
// Every request off this machine goes to a closed port, so the export
|
|
67
|
+
// has to import from its own files. The importer falls back to
|
|
68
|
+
// fetching a file from the exported site's URL; with the network up,
|
|
69
|
+
// that fallback hid icons it failed to find in the export.
|
|
70
|
+
NODE_USE_ENV_PROXY: "1",
|
|
71
|
+
HTTP_PROXY: "http://127.0.0.1:9",
|
|
72
|
+
HTTPS_PROXY: "http://127.0.0.1:9",
|
|
73
|
+
NO_PROXY: "127.0.0.1",
|
|
74
|
+
};
|
|
75
|
+
delete env.SITE_RESOLUTION_MODE;
|
|
76
|
+
for (const name of [
|
|
77
|
+
"http_proxy",
|
|
78
|
+
"https_proxy",
|
|
79
|
+
"no_proxy",
|
|
80
|
+
"all_proxy",
|
|
81
|
+
"ALL_PROXY",
|
|
82
|
+
]) {
|
|
83
|
+
delete env[name];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const result = spawnSync(
|
|
87
|
+
process.execPath,
|
|
88
|
+
["dev/run-script.mjs", "dev/scripts/import-node-demo-site-export.ts"],
|
|
89
|
+
{ cwd: CORE_DIR, encoding: "utf8", env, timeout: 120_000 },
|
|
90
|
+
);
|
|
91
|
+
const output = `${result.stdout}\n${result.stderr}`;
|
|
92
|
+
|
|
93
|
+
expect(result.error).toBeUndefined();
|
|
94
|
+
expect(result.status, output).toBe(0);
|
|
95
|
+
expect(result.stdout).toContain("(none: JANT_ENV_FILE is empty)");
|
|
96
|
+
// The importer reports a step it gave up on as a warning and carries on,
|
|
97
|
+
// the way it once dropped the site avatar.
|
|
98
|
+
expect(output).not.toMatch(/^Warning:/m);
|
|
99
|
+
|
|
100
|
+
const { rootBundles, collectionBundles } =
|
|
101
|
+
await importSite.walkHugoContent(CANONICAL_DIR);
|
|
102
|
+
const siteConfig = await importSite.loadSiteConfig(CANONICAL_DIR);
|
|
103
|
+
const replyCount = rootBundles.reduce(
|
|
104
|
+
(sum: number, root: { children: unknown[] }) =>
|
|
105
|
+
sum + root.children.length,
|
|
106
|
+
0,
|
|
107
|
+
);
|
|
108
|
+
// An empty fixture and an import that did nothing would agree on zero.
|
|
109
|
+
expect(rootBundles.length).toBeGreaterThan(0);
|
|
110
|
+
const avatarFile = basename(siteConfig.extra.jant.site_avatar_url);
|
|
111
|
+
const postMediaFiles = readdirSync(
|
|
112
|
+
join(CANONICAL_DIR, "static/media"),
|
|
113
|
+
).filter((name) => name !== avatarFile);
|
|
114
|
+
|
|
115
|
+
const sqlite = new Database(databasePath, { readonly: true });
|
|
116
|
+
try {
|
|
117
|
+
expect(count(sqlite, `SELECT COUNT(*) AS count FROM "user"`)).toBe(1);
|
|
118
|
+
expect(count(sqlite, `SELECT COUNT(*) AS count FROM "post"`)).toBe(
|
|
119
|
+
rootBundles.length + replyCount,
|
|
120
|
+
);
|
|
121
|
+
expect(
|
|
122
|
+
count(sqlite, `SELECT COUNT(*) AS count FROM "collection"`),
|
|
123
|
+
).toBe(collectionBundles.length);
|
|
124
|
+
expect(
|
|
125
|
+
count(
|
|
126
|
+
sqlite,
|
|
127
|
+
`SELECT COUNT(*) AS count FROM "media" WHERE "storage_key" LIKE '%/files/%'`,
|
|
128
|
+
),
|
|
129
|
+
).toBe(postMediaFiles.length);
|
|
130
|
+
|
|
131
|
+
const settings = new Map(
|
|
132
|
+
(
|
|
133
|
+
sqlite
|
|
134
|
+
.prepare(`SELECT "key", "value" FROM "site_setting"`)
|
|
135
|
+
.all() as { key: string; value: string }[]
|
|
136
|
+
).map(({ key, value }) => [key, value]),
|
|
137
|
+
);
|
|
138
|
+
expect(settings.get("SITE_NAME")).toBe(siteConfig.title);
|
|
139
|
+
expect(settings.get("SITE_AVATAR")).toMatch(/\/assets\/avatar\//);
|
|
140
|
+
expect(settings.get("SITE_FAVICON_ICO")).toBeTruthy();
|
|
141
|
+
expect(settings.get("SITE_FAVICON_APPLE_TOUCH")).toMatch(
|
|
142
|
+
/\/assets\/favicon\/apple-touch-icon\.png$/,
|
|
143
|
+
);
|
|
144
|
+
} finally {
|
|
145
|
+
sqlite.close();
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
);
|
|
149
|
+
});
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical Snapshot Replay Guard
|
|
3
|
+
*
|
|
4
|
+
* `sites/demo-source/canonical/snapshot/` is a committed SQL dump. The nightly
|
|
5
|
+
* demo rebuild migrates demo.jant.me's database to whatever `main` says, then
|
|
6
|
+
* replays that dump into it — so the dump has to stay loadable against head,
|
|
7
|
+
* however long ago it was exported.
|
|
8
|
+
*
|
|
9
|
+
* Nothing checked that. A migration that renamed a column, added a NOT NULL
|
|
10
|
+
* without a default, or reordered the snapshot registries would leave the
|
|
11
|
+
* committed dump unloadable, and the first sign of it would be the reset job
|
|
12
|
+
* failing at ~03:00 UTC — which nothing reports either. This test moves that
|
|
13
|
+
* failure to the pull request that causes it.
|
|
14
|
+
*
|
|
15
|
+
* `createTestDatabase()` applies every migration in order and enables foreign
|
|
16
|
+
* keys, so the replay here is checked at least as strictly as the real target.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { readFileSync } from "node:fs";
|
|
20
|
+
import { resolve } from "node:path";
|
|
21
|
+
import { describe, expect, it } from "vitest";
|
|
22
|
+
import {
|
|
23
|
+
DEFER_FOREIGN_KEYS_SQL,
|
|
24
|
+
SNAPSHOT_TABLES,
|
|
25
|
+
assertSnapshotMeta,
|
|
26
|
+
buildReplaceSql,
|
|
27
|
+
getSnapshotBootstrapSite,
|
|
28
|
+
getSnapshotSelectSql,
|
|
29
|
+
normalizeD1Sql,
|
|
30
|
+
rewriteSnapshotSiteIdentifiers,
|
|
31
|
+
} from "../../bin/lib/site-snapshot.js";
|
|
32
|
+
import { DEFAULT_TEST_SITE_ID, createTestDatabase } from "./helpers/db.js";
|
|
33
|
+
|
|
34
|
+
const CANONICAL_DIR = resolve(
|
|
35
|
+
import.meta.dirname,
|
|
36
|
+
"../../../../sites/demo-source/canonical/snapshot",
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
function readCanonicalSql(): string {
|
|
40
|
+
const meta = JSON.parse(
|
|
41
|
+
readFileSync(resolve(CANONICAL_DIR, "meta.json"), "utf8"),
|
|
42
|
+
);
|
|
43
|
+
assertSnapshotMeta(meta);
|
|
44
|
+
|
|
45
|
+
const snapshotSite = getSnapshotBootstrapSite(meta);
|
|
46
|
+
if (!snapshotSite) {
|
|
47
|
+
throw new Error("Canonical snapshot has no embedded site metadata.");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const rawDbSql = readFileSync(resolve(CANONICAL_DIR, "db.sql"), "utf8");
|
|
51
|
+
return normalizeD1Sql(
|
|
52
|
+
rewriteSnapshotSiteIdentifiers(
|
|
53
|
+
rawDbSql,
|
|
54
|
+
snapshotSite.id,
|
|
55
|
+
DEFAULT_TEST_SITE_ID,
|
|
56
|
+
),
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
describe("canonical demo snapshot", () => {
|
|
61
|
+
it("replays into a database migrated to head", () => {
|
|
62
|
+
const { sqlite } = createTestDatabase();
|
|
63
|
+
const dbSql = readCanonicalSql();
|
|
64
|
+
|
|
65
|
+
expect(() => {
|
|
66
|
+
sqlite.exec(buildReplaceSql(DEFAULT_TEST_SITE_ID));
|
|
67
|
+
sqlite.exec(dbSql);
|
|
68
|
+
}).not.toThrow();
|
|
69
|
+
|
|
70
|
+
// Guard against a vacuous pass: an empty dump would also "not throw".
|
|
71
|
+
const posts = sqlite
|
|
72
|
+
.prepare(`SELECT COUNT(*) AS count FROM "post" WHERE "site_id" = ?`)
|
|
73
|
+
.get(DEFAULT_TEST_SITE_ID) as { count: number };
|
|
74
|
+
expect(posts.count).toBeGreaterThan(0);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("restores the language setup its posts are stamped for", () => {
|
|
78
|
+
// The demo publishes in two languages. When the snapshot carried each
|
|
79
|
+
// post's `language` but not `MULTILINGUAL_ENABLED`, every rebuild left
|
|
80
|
+
// demo.jant.me serving no per-language views at all, and the Chinese side
|
|
81
|
+
// of the demo disappeared at ~00:00 UTC each night.
|
|
82
|
+
const { sqlite } = createTestDatabase();
|
|
83
|
+
|
|
84
|
+
sqlite.exec(buildReplaceSql(DEFAULT_TEST_SITE_ID));
|
|
85
|
+
sqlite.exec(readCanonicalSql());
|
|
86
|
+
|
|
87
|
+
const languages = sqlite
|
|
88
|
+
.prepare(
|
|
89
|
+
`SELECT DISTINCT "language" FROM "post" WHERE "site_id" = ? AND "language" IS NOT NULL`,
|
|
90
|
+
)
|
|
91
|
+
.all(DEFAULT_TEST_SITE_ID) as { language: string }[];
|
|
92
|
+
if (languages.length < 2) return;
|
|
93
|
+
|
|
94
|
+
const settings = sqlite
|
|
95
|
+
.prepare(
|
|
96
|
+
`SELECT "key", "value" FROM "site_setting" WHERE "site_id" = ? AND "key" IN ('SITE_LANGUAGE', 'MULTILINGUAL_ENABLED', 'ADDITIONAL_LANGUAGES')`,
|
|
97
|
+
)
|
|
98
|
+
.all(DEFAULT_TEST_SITE_ID) as { key: string; value: string }[];
|
|
99
|
+
const value = (key: string) =>
|
|
100
|
+
settings.find((row) => row.key === key)?.value ?? "";
|
|
101
|
+
|
|
102
|
+
expect(value("MULTILINGUAL_ENABLED")).toBe("true");
|
|
103
|
+
|
|
104
|
+
const served = new Set([
|
|
105
|
+
value("SITE_LANGUAGE"),
|
|
106
|
+
...value("ADDITIONAL_LANGUAGES").split(",").filter(Boolean),
|
|
107
|
+
]);
|
|
108
|
+
expect(
|
|
109
|
+
languages.map((row) => row.language).filter((tag) => !served.has(tag)),
|
|
110
|
+
).toEqual([]);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("replays twice, the way a nightly rebuild does", () => {
|
|
114
|
+
// `--replace` runs against a site that already holds the previous rebuild's
|
|
115
|
+
// rows, so the delete order matters as much as the insert order. A first
|
|
116
|
+
// load into an empty database would never exercise it.
|
|
117
|
+
const { sqlite } = createTestDatabase();
|
|
118
|
+
const dbSql = readCanonicalSql();
|
|
119
|
+
|
|
120
|
+
sqlite.exec(buildReplaceSql(DEFAULT_TEST_SITE_ID));
|
|
121
|
+
sqlite.exec(dbSql);
|
|
122
|
+
|
|
123
|
+
expect(() => {
|
|
124
|
+
sqlite.exec(buildReplaceSql(DEFAULT_TEST_SITE_ID));
|
|
125
|
+
sqlite.exec(dbSql);
|
|
126
|
+
}).not.toThrow();
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
describe("snapshot export queries", () => {
|
|
131
|
+
it("runs every content table's export query against head", () => {
|
|
132
|
+
// The registry, the SELECT statement and the schema have to agree on
|
|
133
|
+
// column names, not just table names. A stale `ORDER BY` column would
|
|
134
|
+
// otherwise only surface the next time someone exported a snapshot.
|
|
135
|
+
const { sqlite } = createTestDatabase();
|
|
136
|
+
|
|
137
|
+
for (const table of SNAPSHOT_TABLES) {
|
|
138
|
+
expect(() =>
|
|
139
|
+
sqlite.prepare(getSnapshotSelectSql(table, DEFAULT_TEST_SITE_ID)).all(),
|
|
140
|
+
).not.toThrow();
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// Snapshots written before posts were ordered parents-first put a reply
|
|
145
|
+
// older than its root ahead of the root. The import checks foreign keys at
|
|
146
|
+
// commit on SQLite and D1, so those snapshots still load.
|
|
147
|
+
it("loads a snapshot that lists a reply before its root", () => {
|
|
148
|
+
const { sqlite } = createTestDatabase();
|
|
149
|
+
const post = (id: string, threadId: string, replyToId: string | null) =>
|
|
150
|
+
`INSERT INTO "post" ("id", "site_id", "format", "status", "visibility", "reply_to_id", "thread_id", "published_at", "last_activity_at", "created_at", "updated_at", "thread_updated_at") VALUES ('${id}', '${DEFAULT_TEST_SITE_ID}', 'note', 'published', 'public', ${replyToId ? `'${replyToId}'` : "NULL"}, '${threadId}', 1, 1, 1, 1, 1);`;
|
|
151
|
+
const dump = [
|
|
152
|
+
post(
|
|
153
|
+
"pst_01aaaaaaaaaaaaaaaaaaaaaaab",
|
|
154
|
+
"pst_01aaaaaaaaaaaaaaaaaaaaaaaa",
|
|
155
|
+
"pst_01aaaaaaaaaaaaaaaaaaaaaaaa",
|
|
156
|
+
),
|
|
157
|
+
post(
|
|
158
|
+
"pst_01aaaaaaaaaaaaaaaaaaaaaaaa",
|
|
159
|
+
"pst_01aaaaaaaaaaaaaaaaaaaaaaaa",
|
|
160
|
+
null,
|
|
161
|
+
),
|
|
162
|
+
].join("\n");
|
|
163
|
+
|
|
164
|
+
expect(() =>
|
|
165
|
+
sqlite.transaction(() => {
|
|
166
|
+
sqlite.exec(buildReplaceSql(DEFAULT_TEST_SITE_ID));
|
|
167
|
+
sqlite.exec(dump);
|
|
168
|
+
})(),
|
|
169
|
+
).toThrow(/FOREIGN KEY/);
|
|
170
|
+
|
|
171
|
+
expect(() =>
|
|
172
|
+
sqlite.transaction(() => {
|
|
173
|
+
sqlite.exec(DEFER_FOREIGN_KEYS_SQL);
|
|
174
|
+
sqlite.exec(buildReplaceSql(DEFAULT_TEST_SITE_ID));
|
|
175
|
+
sqlite.exec(dump);
|
|
176
|
+
})(),
|
|
177
|
+
).not.toThrow();
|
|
178
|
+
});
|
|
179
|
+
});
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Snapshot Setting Registry Guard
|
|
3
|
+
*
|
|
4
|
+
* `jant site snapshot export/import` filters `site_setting` through the
|
|
5
|
+
* hand-written `SNAPSHOT_SETTING_KEYS` allowlist in `bin/lib/site-snapshot.js`.
|
|
6
|
+
* `bin/` is plain JavaScript, outside the type system, so a new setting key
|
|
7
|
+
* compiles, lints and tests clean while the snapshot silently skips it.
|
|
8
|
+
*
|
|
9
|
+
* That already happened. `MULTILINGUAL_ENABLED` and `ADDITIONAL_LANGUAGES`
|
|
10
|
+
* arrived with multilingual content and never reached the allowlist, while the
|
|
11
|
+
* whole-row `post` dump kept carrying `language` and `translation_group_id`.
|
|
12
|
+
* Restoring a multilingual site therefore produced posts stamped per language
|
|
13
|
+
* on a site that served no per-language views — and the nightly demo rebuild
|
|
14
|
+
* reset `demo.jant.me` to a single-language site every night.
|
|
15
|
+
*
|
|
16
|
+
* This test closes the gap the same way `snapshot-tables.test.ts` does for
|
|
17
|
+
* tables: every DB-backed setting has to be named in one of the two
|
|
18
|
+
* registries, so a new key forces a decision instead of defaulting to
|
|
19
|
+
* "forgotten".
|
|
20
|
+
*
|
|
21
|
+
* `envOnly` keys are the one exemption, and are asserted to be in neither
|
|
22
|
+
* list: they resolve from the environment and never reach `site_setting`, so
|
|
23
|
+
* there is no row for an export to read or for `--replace` to clear.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { describe, expect, it } from "vitest";
|
|
27
|
+
import {
|
|
28
|
+
SNAPSHOT_EXCLUDED_SETTING_KEYS,
|
|
29
|
+
SNAPSHOT_SETTING_KEYS,
|
|
30
|
+
} from "../../bin/lib/site-snapshot.js";
|
|
31
|
+
import { CONFIG_FIELDS } from "../types/config.js";
|
|
32
|
+
|
|
33
|
+
type ConfigKey = keyof typeof CONFIG_FIELDS;
|
|
34
|
+
|
|
35
|
+
const ALL_KEYS = Object.keys(CONFIG_FIELDS) as ConfigKey[];
|
|
36
|
+
|
|
37
|
+
/** Keys that can hold a `site_setting` row — everything not env-resolved. */
|
|
38
|
+
const DB_BACKED_KEYS = ALL_KEYS.filter(
|
|
39
|
+
(key) => !("envOnly" in CONFIG_FIELDS[key] && CONFIG_FIELDS[key].envOnly),
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
/** Keys resolved from the environment, which never reach `site_setting`. */
|
|
43
|
+
const ENV_ONLY_KEYS = ALL_KEYS.filter((key) => !DB_BACKED_KEYS.includes(key));
|
|
44
|
+
|
|
45
|
+
describe("snapshot setting registry", () => {
|
|
46
|
+
it("names every DB-backed setting in exactly one registry", () => {
|
|
47
|
+
const carried = new Set<string>(SNAPSHOT_SETTING_KEYS);
|
|
48
|
+
const excluded = new Set<string>(SNAPSHOT_EXCLUDED_SETTING_KEYS);
|
|
49
|
+
|
|
50
|
+
const unregistered = DB_BACKED_KEYS.filter(
|
|
51
|
+
(key) => !carried.has(key) && !excluded.has(key),
|
|
52
|
+
);
|
|
53
|
+
const registeredTwice = DB_BACKED_KEYS.filter(
|
|
54
|
+
(key) => carried.has(key) && excluded.has(key),
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
expect(
|
|
58
|
+
{ unregistered, registeredTwice },
|
|
59
|
+
"Add each new DB-backed setting to SNAPSHOT_SETTING_KEYS (it belongs to " +
|
|
60
|
+
"the site's published content) or to SNAPSHOT_EXCLUDED_SETTING_KEYS " +
|
|
61
|
+
"(with the reason), in bin/lib/site-snapshot.js.",
|
|
62
|
+
).toEqual({ unregistered: [], registeredTwice: [] });
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("leaves env-only settings out of both registries", () => {
|
|
66
|
+
const listed = ENV_ONLY_KEYS.filter(
|
|
67
|
+
(key) =>
|
|
68
|
+
SNAPSHOT_SETTING_KEYS.includes(key) ||
|
|
69
|
+
SNAPSHOT_EXCLUDED_SETTING_KEYS.includes(key),
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
expect(listed).toEqual([]);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("registers no key that CONFIG_FIELDS does not define", () => {
|
|
76
|
+
const known = new Set<string>(ALL_KEYS);
|
|
77
|
+
const unknown = [
|
|
78
|
+
...SNAPSHOT_SETTING_KEYS,
|
|
79
|
+
...SNAPSHOT_EXCLUDED_SETTING_KEYS,
|
|
80
|
+
].filter((key) => !known.has(key));
|
|
81
|
+
|
|
82
|
+
expect(unknown).toEqual([]);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("carries the whole language trio, so restored posts keep their views", () => {
|
|
86
|
+
// `post.language` and `post.translation_group_id` ride along in the
|
|
87
|
+
// whole-row `post` dump. A snapshot that carried those but not the switch
|
|
88
|
+
// restored a site whose per-language views had silently disappeared.
|
|
89
|
+
expect(SNAPSHOT_SETTING_KEYS).toEqual(
|
|
90
|
+
expect.arrayContaining([
|
|
91
|
+
"SITE_LANGUAGE",
|
|
92
|
+
"MULTILINGUAL_ENABLED",
|
|
93
|
+
"ADDITIONAL_LANGUAGES",
|
|
94
|
+
]),
|
|
95
|
+
);
|
|
96
|
+
});
|
|
97
|
+
});
|