feat(config): add migration framework

This commit is contained in:
Erik Kotsiuba
2026-05-02 17:09:58 +02:00
parent 69c1ca3803
commit 288caf49de
10 changed files with 288 additions and 70 deletions
+42
View File
@@ -0,0 +1,42 @@
import assert from "node:assert/strict";
import test from "node:test";
import { DEFAULT_ASK_CONFIG } from "../src/config/defaults.ts";
import { migrateAskConfig } from "../src/config/migrate.ts";
import { CURRENT_ASK_CONFIG_SCHEMA_VERSION } from "../src/config/migrations/index.ts";
const INVALID_CONFIG_PATTERN = /Config was invalid or unsupported/;
const currentConfigFile = {
schemaVersion: CURRENT_ASK_CONFIG_SCHEMA_VERSION,
behaviour: DEFAULT_ASK_CONFIG.behaviour,
keymaps: DEFAULT_ASK_CONFIG.keymaps,
};
test("config migration framework accepts the current schema version", () => {
const result = migrateAskConfig(currentConfigFile);
assert.equal(result.migrated, false);
assert.deepEqual(result.config, DEFAULT_ASK_CONFIG);
});
test("config migration framework rejects unsupported future schema versions", () => {
assert.throws(
() =>
migrateAskConfig({
...currentConfigFile,
schemaVersion: CURRENT_ASK_CONFIG_SCHEMA_VERSION + 1,
}),
INVALID_CONFIG_PATTERN
);
});
test("config migration framework rejects unversioned config files", () => {
assert.throws(
() =>
migrateAskConfig({
behaviour: DEFAULT_ASK_CONFIG.behaviour,
keymaps: DEFAULT_ASK_CONFIG.keymaps,
}),
INVALID_CONFIG_PATTERN
);
});
+50 -5
View File
@@ -111,13 +111,12 @@ test("config store loads current config version without rewriting", async () =>
await rm(dirname(path), { force: true, recursive: true });
});
test("config store migrates the legacy extensions config path", async () => {
test("config store migrates the legacy root config path into extensions", async () => {
const root = await import("node:fs/promises").then(({ mkdtemp }) =>
mkdtemp(join(tmpdir(), "pi-ask-config-legacy-"))
);
const path = join(root, "eko24ive-pi-ask.json");
const legacyPath = join(root, "extensions", "eko24ive-pi-ask.json");
await mkdir(dirname(legacyPath), { recursive: true });
const path = join(root, "extensions", "eko24ive-pi-ask.json");
const legacyPath = join(root, "eko24ive-pi-ask.json");
await writeFile(
legacyPath,
JSON.stringify({
@@ -131,7 +130,7 @@ test("config store migrates the legacy extensions config path", async () => {
keymaps: DEFAULT_ASK_CONFIG.keymaps,
})
);
const store = new AskConfigStore(path, legacyPath);
const store = new AskConfigStore(path, [legacyPath]);
const result = await store.ensureLoaded();
@@ -156,6 +155,52 @@ test("config store migrates the legacy extensions config path", async () => {
await rm(root, { force: true, recursive: true });
});
test("config store leaves legacy root config when extensions config exists", async () => {
const root = await import("node:fs/promises").then(({ mkdtemp }) =>
mkdtemp(join(tmpdir(), "pi-ask-config-conflict-"))
);
const path = join(root, "extensions", "eko24ive-pi-ask.json");
const legacyPath = join(root, "eko24ive-pi-ask.json");
await mkdir(dirname(path), { recursive: true });
await writeFile(
path,
JSON.stringify({
schemaVersion: 1,
behaviour: {
autoSubmitWhenAnsweredWithoutNotes: false,
confirmDismissWhenDirty: true,
doublePressReviewShortcuts: true,
showFooterHints: true,
},
keymaps: DEFAULT_ASK_CONFIG.keymaps,
})
);
await writeFile(
legacyPath,
JSON.stringify({
schemaVersion: 1,
behaviour: {
autoSubmitWhenAnsweredWithoutNotes: true,
confirmDismissWhenDirty: true,
doublePressReviewShortcuts: true,
showFooterHints: false,
},
keymaps: DEFAULT_ASK_CONFIG.keymaps,
})
);
const store = new AskConfigStore(path, [legacyPath]);
const result = await store.ensureLoaded();
assert.equal(
result.config.behaviour.autoSubmitWhenAnsweredWithoutNotes,
false
);
assert.equal(result.config.behaviour.showFooterHints, true);
assert.ok(await readFile(legacyPath, "utf-8"));
await rm(root, { force: true, recursive: true });
});
test("config store falls back only keymaps when configured keymaps are invalid", async () => {
const path = await makeTempPath("pi-ask-config-invalid-keymaps-");
await mkdir(dirname(path), { recursive: true });