mirror of
https://github.com/PR0M3TH3AN/SeedPass.git
synced 2026-10-29 08:52:04 +00:00
This was the one remaining gap that could strand a real user: TS refused
any index below schema_version 4, which is every profile written before
tags were added.
- core/schema/migrations.ts ports seedpass.core.migrations exactly,
including the quirks: v1->v2 copies `website` to `label` and drops it
only for password entries; `kind` is NOT backfilled (Python leaves
pre-v2 entries `type`-only and falls back on read). Migration works on a
copy, so a failure can never leave a half-converted vault.
- parseVaultIndex now migrates on open and still refuses future versions;
pass {migrate:false} where silent upgrades are unwanted.
- Two bugs this surfaced: the version probe could not read a v0 index (no
schema_version field at all), and migrated legacy entries failed schema
validation because the discriminator `kind` was missing. The parser now
fills `kind` from `type` at validation time -- the same shape Python
writes for new entries, documented in the matrix.
Evidence: fixtures generated by running Python's own apply_migrations over
v0/v1/v2/v3 payloads, plus cross-impl phase E, which writes a real legacy
v2 index into a Python profile, opens it with the TS CLI, reveals a
migrated secret, and confirms Python still reads the profile afterwards.
162 core + 46 CLI tests green; 16/16 cross-implementation checks green.
Co-Authored-By: Claude Fable 5 <[email protected]>
72 lines
2.5 KiB
TypeScript
72 lines
2.5 KiB
TypeScript
/**
|
|
* Schema migration parity: TS must migrate legacy indexes to exactly what
|
|
* Python's apply_migrations produces, so an old Python profile opens here.
|
|
*/
|
|
|
|
import { describe, expect, it } from "vitest";
|
|
import { migrationCases, migrationLatestVersion } from "@seedpass/test-vectors";
|
|
import {
|
|
applyMigrations,
|
|
needsMigration,
|
|
parseVaultIndex,
|
|
SchemaMigrationError,
|
|
CURRENT_SCHEMA_VERSION,
|
|
UnsupportedSchemaVersionError,
|
|
} from "@seedpass/core";
|
|
|
|
describe("schema migrations", () => {
|
|
it("agrees with Python on the current version", () => {
|
|
expect(CURRENT_SCHEMA_VERSION).toBe(migrationLatestVersion);
|
|
});
|
|
|
|
it.each(migrationCases)("migrates $name exactly as Python does", (c) => {
|
|
expect(applyMigrations(c.input)).toEqual(c.migrated);
|
|
});
|
|
|
|
it("does not mutate the caller's payload", () => {
|
|
const input = migrationCases[0]!.input;
|
|
const snapshot = JSON.stringify(input);
|
|
applyMigrations(input);
|
|
expect(JSON.stringify(input)).toBe(snapshot);
|
|
});
|
|
|
|
it("is idempotent on an already-current index", () => {
|
|
const migrated = applyMigrations(migrationCases[0]!.input);
|
|
expect(applyMigrations(migrated)).toEqual(migrated);
|
|
expect(needsMigration(migrated)).toBe(false);
|
|
});
|
|
|
|
it("parseVaultIndex opens legacy indexes by migrating them", () => {
|
|
for (const c of migrationCases) {
|
|
const index = parseVaultIndex(c.input);
|
|
expect(index.schema_version).toBe(CURRENT_SCHEMA_VERSION);
|
|
expect(Object.keys(index.entries).length).toBeGreaterThan(0);
|
|
for (const entry of Object.values(index.entries)) {
|
|
expect(entry.tags).toBeDefined();
|
|
}
|
|
}
|
|
});
|
|
|
|
it("parseVaultIndex can refuse to migrate when asked", () => {
|
|
expect(() => parseVaultIndex(migrationCases[0]!.input, { migrate: false })).toThrow(
|
|
/needs migration/,
|
|
);
|
|
});
|
|
|
|
it("refuses future schema versions in both paths", () => {
|
|
const future = { schema_version: 99, entries: {} };
|
|
expect(() => applyMigrations(future)).toThrow(SchemaMigrationError);
|
|
expect(() => parseVaultIndex(future)).toThrow(UnsupportedSchemaVersionError);
|
|
});
|
|
|
|
it("recovers a v0 password entry's label from its website field", () => {
|
|
const v0 = migrationCases.find((c) => c.name === "v0")!;
|
|
const index = parseVaultIndex(v0.input);
|
|
const labels = Object.values(index.entries).map((e) => e.label);
|
|
expect(labels).toContain("legacy-site.example");
|
|
for (const entry of Object.values(index.entries)) {
|
|
expect(entry).not.toHaveProperty("website");
|
|
}
|
|
});
|
|
});
|