Files
SeedPass/js/packages/core/test/entryMod.test.ts
T
thePR0M3TH3ANandClaude Fable 5 4f2b8a79bf TS port slice 7: entry modify/archive/links + util generate-password
- core entryMod: modify_entry port with the per-kind allowed-field matrix,
  policy merging, blacklisted-field cleanup and timestamp touching;
  archive/restore; typed links (add/remove/get with Python's normalization,
  dedupe on target/relation/note, self-link and missing-target refusal).
- Parity fixture entry_mods.json: the real Python EntryManager applies a
  10-op modification sequence at a later pinned clock; TS replays the same
  ops from the creation state and matches byte-for-byte.
- CLI: entry modify/archive/unarchive/links/link-add/link-remove,
  entry totp-codes (explicit egress, honors --at and archived state),
  util generate-password (Python parity: index 0 + gen v1 defaults, full
  policy flags — v1/v2 outputs verified against password fixtures).
- capabilities: egress command list updated.
- 148 core parity tests (Node + jsdom) + 22 CLI tests green.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-14 13:14:25 -04:00

100 lines
3.1 KiB
TypeScript

/**
* Modification parity: replay the exact op sequence the Python EntryManager
* performed in the entry_mods fixture and require byte-for-byte equality.
*/
import { describe, expect, it } from "vitest";
import { entriesIndex, entryMods } from "@seedpass/test-vectors";
import {
modifyEntry,
archiveEntry,
restoreEntry,
addLink,
removeLink,
getLinks,
normalizeLinks,
type Clock,
type VaultIndex,
} from "@seedpass/core";
const modClock: Clock = { nowUnix: () => entryMods.mod_unix };
function creationState(): VaultIndex {
return JSON.parse(JSON.stringify(entriesIndex.entries)) as VaultIndex;
}
describe("entry modification parity", () => {
it("replays the Python modification sequence byte-for-byte", () => {
const index = creationState();
// Mirrors gen_entry_mods in scripts/generate_ts_port_fixtures.py
modifyEntry(
index,
"0",
{
username: "alice2",
url: "https://example.org",
notes: "updated note",
tags: ["web", "prod"],
min_digits: 3,
special_mode: "safe",
},
modClock,
);
modifyEntry(index, "1", { period: 60, digits: 8 }, modClock);
modifyEntry(index, "5", { label: "api-token-renamed", value: "rotated-value" }, modClock);
archiveEntry(index, "6", modClock);
restoreEntry(index, "6", modClock);
archiveEntry(index, "3", modClock);
addLink(index, "0", 1, { relation: "totp", note: "2fa for site", clock: modClock });
addLink(index, "0", 5, { relation: "related_to", clock: modClock });
addLink(index, "5", 0, { clock: modClock });
removeLink(index, "0", 5, { clock: modClock });
expect(JSON.parse(JSON.stringify(index))).toEqual(entryMods.entries);
});
it("rejects fields the entry kind does not allow", () => {
const index = creationState();
expect(() => modifyEntry(index, "1", { username: "x" }, modClock)).toThrow(
/does not support fields: username/,
);
expect(() => modifyEntry(index, "0", { period: 60 }, modClock)).toThrow(
/does not support fields: period/,
);
});
it("refuses self-referential links and missing targets", () => {
const index = creationState();
expect(() => addLink(index, "0", 0, { clock: modClock })).toThrow("Self-referential");
expect(() => addLink(index, "0", 99, { clock: modClock })).toThrow("Entry not found");
});
it("resolves link target metadata", () => {
const index = creationState();
addLink(index, "0", 1, { relation: "totp", clock: modClock });
const links = getLinks(index, "0");
expect(links).toEqual([
{
target_id: 1,
relation: "totp",
note: "",
target_label: "example-totp",
target_kind: "totp",
},
]);
});
it("normalizes and dedupes links like Python", () => {
expect(
normalizeLinks([
{ target_id: "3", relation: " Related_To ", note: " x " },
{ target_id: 3, relation: "related_to", note: "x" },
{ target_id: -1, relation: "r" },
{ target_id: 4, relation: "" },
"junk",
]),
).toEqual([{ target_id: 3, relation: "related_to", note: "x" }]);
});
});