Files
budget2.0/src/export/json.test.ts
T
Lucas Winther b0e8145d2e
Continuous Integration / lint-and-test (push) Successful in 13s
Continuous Integration / docker-build-and-push (push) Successful in 35s
feat: support unassigned purchases and allocation status
Allow month-only purchases to start finance plans. Keep unbudgeted posts distinct from explicit zero rules in overspend reporting.
2026-09-30 13:10:10 +02:00

660 lines
25 KiB
TypeScript

import { test, expect, describe } from "bun:test";
import {
exportDatasetJson,
parseDatasetJson,
describeDataset,
ImportValidationError,
exportFilename,
} from "./json.ts";
import { addRecurringCost, updatePost, updateRecurringCost } from "../store/actions.ts";
import { createSeedDataset } from "../domain/seed.ts";
import { monthView, yearView } from "../domain/views.ts";
import type { Dataset } from "../domain/types.ts";
function populated(): Dataset {
const data = createSeedDataset("2026-01");
data.months[0]!.income = { amount: 20000, currency: "DKK" };
data.purchases.push({
id: "p1",
date: "2026-01-15",
description: "Console",
total: { amount: 3000, currency: "DKK" },
splitMode: "percent",
splits: [
{ postId: data.posts[0]!.id, value: 70, absorbsRemainder: true },
{ postId: data.posts[2]!.id, value: 30, absorbsRemainder: false },
],
schedule: {
slices: [
{ month: "2026-01", amount: { amount: 1000, currency: "DKK" } },
{ month: "2026-02", amount: { amount: 2000, currency: "DKK" } },
],
},
});
return data;
}
describe("round trip", () => {
test("export then import yields an equal dataset", () => {
const data = populated();
expect(parseDatasetJson(exportDatasetJson(data))).toEqual(data);
});
test("derived output is identical after a round trip", () => {
const data = populated();
const restored = parseDatasetJson(exportDatasetJson(data));
expect(monthView(restored, "2026-01")).toEqual(monthView(data, "2026-01"));
expect(yearView(restored, 2026)).toEqual(yearView(data, 2026));
});
test("export is human-readable, indented JSON", () => {
expect(exportDatasetJson(createSeedDataset("2026-01"))).toContain("\n ");
});
});
describe("validation", () => {
test("accepts a purchase with no post and preserves it through export", () => {
const data = populated();
data.purchases[0]!.splits = [];
expect(parseDatasetJson(exportDatasetJson(data)).purchases[0]!.splits).toEqual([]);
});
test("rejects malformed JSON", () => {
expect(() => parseDatasetJson("{not json")).toThrow(ImportValidationError);
});
test("rejects a dataset missing a required collection", () => {
const data = populated() as any;
delete data.purchases;
expect(() => parseDatasetJson(JSON.stringify(data))).toThrow(/purchases/);
});
test("rejects a non-array recurring field instead of silently importing it as empty (M7)", () => {
// The other four sibling collections already go through requireArray;
// recurring used to fall back to [] instead of being rejected here.
const data = populated() as any;
data.recurring = "not an array";
expect(() => parseDatasetJson(JSON.stringify(data))).toThrow(/recurring/);
});
test("rejects a purchase whose splits reference an unknown post", () => {
const data = populated();
data.purchases[0]!.splits[0]!.postId = "ghost";
expect(() => parseDatasetJson(JSON.stringify(data))).toThrow(/ghost/);
});
test("rejects a purchase with no remainder-absorbing split", () => {
const data = populated();
data.purchases[0]!.splits[0]!.absorbsRemainder = false;
expect(() => parseDatasetJson(JSON.stringify(data))).toThrow(/absorbsRemainder/);
});
test("rejects a purchase with two remainder-absorbing splits", () => {
const data = populated();
data.purchases[0]!.splits[1]!.absorbsRemainder = true;
expect(() => parseDatasetJson(JSON.stringify(data))).toThrow(/exactly one/i);
});
test("rejects an unsupported currency", () => {
const data = populated() as any;
// CHF, not GBP: GBP is one of the currencies the app ships with now.
data.settings.baseCurrency = "CHF";
expect(() => parseDatasetJson(JSON.stringify(data))).toThrow(/CHF/);
});
test("rejects a malformed MonthId", () => {
const data = populated();
data.months[0]!.id = "2026-1";
expect(() => parseDatasetJson(JSON.stringify(data))).toThrow(/2026-1/);
});
test("rejects a MonthId with an out-of-range month", () => {
const data = populated();
data.months[0]!.id = "2026-13";
expect(() => parseDatasetJson(JSON.stringify(data))).toThrow(/2026-13/);
});
});
describe("import rejects invalid folder graph", () => {
const folder = (id: string, parentId: string | null, order: number) => ({ id, name: id, parentId, order });
const parse = (data: Dataset) => parseDatasetJson(JSON.stringify(data));
test("valid nested tree parses", () => {
const data = populated();
data.folders = [folder("home", null, 0), folder("bills", "home", 0)];
data.posts[0]!.folderId = "bills";
expect(parse(data).posts[0]!.folderId).toBe("bills");
});
test("rejects duplicate folder IDs", () => {
const data = populated();
data.folders = [folder("same", null, 0), folder("same", null, 1)];
expect(() => parse(data)).toThrow(ImportValidationError);
});
test("rejects missing parent", () => {
const data = populated();
data.folders = [folder("child", "missing", 0)];
expect(() => parse(data)).toThrow(ImportValidationError);
});
test("rejects missing item folders", () => {
const data = populated();
data.posts[0]!.folderId = "missing";
expect(() => parse(data)).toThrow(ImportValidationError);
data.posts[0]!.folderId = null;
data.recurring.push({
id: "rent", name: "Rent", order: 3, folderId: "missing", archived: false,
amount: { amount: 100, currency: "DKK" }, startDate: "2026-01",
recurrence: { kind: "everyNMonths", n: 1 }, anchoring: "calendar",
splitMode: "percent", splits: [{ postId: data.posts[0]!.id, value: 100, absorbsRemainder: true }],
});
expect(() => parse(data)).toThrow(ImportValidationError);
});
test("rejects cycles", () => {
const data = populated();
data.folders = [folder("a", "b", 0), folder("b", "a", 0)];
expect(() => parse(data)).toThrow(ImportValidationError);
});
test("rejects duplicate sibling order across item kinds", () => {
const data = populated();
data.folders = [folder("bills", null, 0)];
data.posts[0]!.order = 0;
expect(() => parse(data)).toThrow(ImportValidationError);
});
test.each([
["", "Rent"], [" ", "Rent"], ["Games", ""], ["Games", " "],
])("blank item names (%j, %j) survive saved dataset round trips and v7 import", (postName, billName) => {
const data = populated();
const bill = addRecurringCost(data, {
name: "Rent", folderId: null, archived: false,
amount: { amount: 100, currency: "DKK" }, startDate: "2026-01",
recurrence: { kind: "everyNMonths", n: 1 }, anchoring: "calendar",
splitMode: "percent", splits: [{ postId: data.posts[0]!.id, value: 100, absorbsRemainder: true }],
});
updatePost(data, data.posts[0]!.id, { name: postName });
updateRecurringCost(data, bill.id, { name: billName });
expect(parseDatasetJson(exportDatasetJson(data))).toEqual(data);
const legacy = JSON.parse(exportDatasetJson(data));
legacy.settings.schemaVersion = 7;
delete legacy.folders;
for (const item of [...legacy.posts, ...legacy.recurring]) delete item.folderId;
const imported = parseDatasetJson(JSON.stringify(legacy));
expect(imported.posts[0]!.name).toBe(postName);
expect(imported.recurring[0]!.name).toBe(billName);
expect(imported.folders).toEqual([]);
});
test.each(["", " "])("still rejects blank folder names %j", name => {
const data = populated();
data.folders = [{ ...folder("home", null, data.posts.length), name }];
expect(() => parse(data)).toThrow(/empty name/);
});
});
test("describeDataset counts what an import would replace", () => {
expect(describeDataset(populated())).toEqual({
posts: 3,
months: 1,
purchases: 1,
});
});
test("exportFilename is stable and sortable", () => {
expect(exportFilename("2026-09")).toBe("budget-2026-09.json");
});
describe("purchase dates", () => {
test("accepts a full date and a month-only date", () => {
const data = populated();
data.purchases[0]!.date = "2026-01-15";
expect(() => parseDatasetJson(JSON.stringify(data))).not.toThrow();
data.purchases[0]!.date = "2026-01";
expect(() => parseDatasetJson(JSON.stringify(data))).not.toThrow();
});
test("rejects a malformed date instead of letting the fold throw later", () => {
const data = populated();
data.purchases[0]!.date = "not-a-date";
expect(() => parseDatasetJson(JSON.stringify(data))).toThrow(/not-a-date/);
});
test("rejects an out-of-range month in a purchase date", () => {
const data = populated();
data.purchases[0]!.date = "2026-13-01";
expect(() => parseDatasetJson(JSON.stringify(data))).toThrow(/2026-13-01/);
});
test("rejects an out-of-range day", () => {
const data = populated();
data.purchases[0]!.date = "2026-01-32";
expect(() => parseDatasetJson(JSON.stringify(data))).toThrow(/2026-01-32/);
});
});
describe("rule versions on import", () => {
const pct = (percent: number) => ({ kind: "percentOfIncome" as const, percent });
test("accepts a well-formed series", () => {
const data = populated();
data.posts[0]!.rules = [
{ from: "2026-01", rule: pct(10) },
{ from: "2026-07", rule: pct(15) },
];
expect(() => parseDatasetJson(JSON.stringify(data))).not.toThrow();
});
test("accepts an empty series — an unbudgeted post is legal", () => {
const data = populated();
data.posts[0]!.rules = [];
expect(() => parseDatasetJson(JSON.stringify(data))).not.toThrow();
});
test("rejects a post whose rules is missing or not an array", () => {
const data = populated() as any;
delete data.posts[0].rules;
expect(() => parseDatasetJson(JSON.stringify(data))).toThrow(/rules/);
});
test("rejects a malformed from month", () => {
const data = populated();
data.posts[0]!.rules = [{ from: "2026-1", rule: pct(10) }];
expect(() => parseDatasetJson(JSON.stringify(data))).toThrow(/2026-1/);
});
test("rejects an out-of-range from month", () => {
const data = populated();
data.posts[0]!.rules = [{ from: "2026-13", rule: pct(10) }];
expect(() => parseDatasetJson(JSON.stringify(data))).toThrow(/2026-13/);
});
test("rejects two versions sharing a month, which would be ambiguous", () => {
const data = populated();
data.posts[0]!.rules = [
{ from: "2026-04", rule: pct(10) },
{ from: "2026-04", rule: pct(15) },
];
expect(() => parseDatasetJson(JSON.stringify(data))).toThrow(/2026-04/);
});
test("does NOT reject a percentage above 100", () => {
const data = populated();
data.posts[0]!.rules = [{ from: "2026-01", rule: pct(150) }];
expect(() => parseDatasetJson(JSON.stringify(data))).not.toThrow();
});
test("does NOT reject a negative fixed amount", () => {
const data = populated();
data.posts[0]!.rules = [
{ from: "2026-01", rule: { kind: "fixed", amount: { amount: -500, currency: "DKK" } } },
];
expect(() => parseDatasetJson(JSON.stringify(data))).not.toThrow();
});
// Without this, toBase() throws MissingRateError deep inside the balance
// fold instead of at the import boundary.
test("rejects a fixed rule in an unsupported currency", () => {
const data = populated() as any;
data.posts[0].rules = [
{ from: "2026-01", rule: { kind: "fixed", amount: { amount: 500, currency: "XYZ" } } },
];
expect(() => parseDatasetJson(JSON.stringify(data))).toThrow(/XYZ/);
});
// Without this, resolveRule() falls through to the percentage branch and
// every figure downstream of it becomes NaN, silently.
test("rejects a rule of an unrecognized kind", () => {
const data = populated() as any;
data.posts[0].rules = [{ from: "2026-01", rule: { kind: "everySecondTuesday" } }];
expect(() => parseDatasetJson(JSON.stringify(data))).toThrow(/everySecondTuesday/);
});
test.each([
["missing percent", { kind: "percentOfIncome" }],
["string percent", { kind: "percentOfIncome", percent: "10" }],
["null percent", { kind: "percentOfIncome", percent: null }],
["missing fixed amount", { kind: "fixed", amount: { currency: "DKK" } }],
["string fixed amount", { kind: "fixed", amount: { amount: "500", currency: "DKK" } }],
["null fixed amount", { kind: "fixed", amount: { amount: null, currency: "DKK" } }],
])("rejects a %s before it can corrupt the fold", (_label, rule) => {
const data = populated() as any;
data.posts[0].rules = [{ from: "2026-01", rule }];
expect(() => parseDatasetJson(JSON.stringify(data))).toThrow(ImportValidationError);
});
});
describe("the decimal-places setting on import", () => {
test("a valid setting round-trips", () => {
const data = populated();
data.settings.digits = 0;
expect(parseDatasetJson(exportDatasetJson(data)).settings.digits).toBe(0);
});
test.each([
["missing", undefined],
["fractional", 1.5],
["negative", -1],
["beyond four places", 5],
["a string", "2"],
])("a %s setting is refused", (_label, value) => {
const data = populated() as any;
if (value === undefined) delete data.settings.digits;
else data.settings.digits = value;
expect(() => parseDatasetJson(JSON.stringify(data))).toThrow(ImportValidationError);
});
test("a per-currency digits left over in a hand-edited file is ignored, not refused", () => {
// An unknown field is not a reason to refuse the owner's only backup.
const data = populated() as any;
data.currencies[0].digits = 3;
const out = parseDatasetJson(JSON.stringify(data));
expect("digits" in out.currencies[0]!).toBe(false);
expect(out.settings.digits).toBe(2);
});
});
describe("recurring cost validation", () => {
function withRecurring(recurring: unknown[]): string {
const data: any = createSeedDataset("2026-01");
data.posts[0].id = "housing";
data.recurring = recurring;
return JSON.stringify(data);
}
const valid = {
id: "r1",
name: "Rent",
order: 3,
folderId: null,
archived: false,
amount: { amount: 8000, currency: "DKK" },
startDate: "2026-01",
recurrence: { kind: "everyNMonths", n: 1 },
anchoring: "calendar",
splitMode: "percent",
splits: [{ postId: "housing", value: 100, absorbsRemainder: true }],
};
test("a well-formed cost is accepted", () => {
expect(parseDatasetJson(withRecurring([valid])).recurring.length).toBe(1);
});
test("an undefined currency is refused", () => {
expect(() => parseDatasetJson(withRecurring([{ ...valid, amount: { amount: 1, currency: "XYZ" } }])))
.toThrow(/Unsupported currency/);
});
test("a malformed start date is refused", () => {
expect(() => parseDatasetJson(withRecurring([{ ...valid, startDate: "2026-13" }])))
.toThrow(/start date/);
});
test("a malformed endedFrom is refused", () => {
expect(() => parseDatasetJson(withRecurring([{ ...valid, endedFrom: "nope" }])))
.toThrow(/ended-from date/);
});
// Same residual-C1 gap as startDate: PURCHASE_DATE checks shape and digit
// range, not calendar validity, so "2026-02-30" passes it. §10: endedFrom
// carries no granularity rule, but it must still be a real date.
test("a calendar-impossible endedFrom is refused (day- or month-shaped)", () => {
expect(() => parseDatasetJson(withRecurring([{ ...valid, endedFrom: "2026-02-30" }])))
.toThrow(/calendar-impossible ended-from/);
expect(() => parseDatasetJson(withRecurring([{ ...valid, endedFrom: "2026-02-29" }]))) // not a leap year
.toThrow(/calendar-impossible ended-from/);
});
test("a valid endedFrom of either granularity is accepted, regardless of recurrence kind", () => {
expect(
parseDatasetJson(withRecurring([{ ...valid, endedFrom: "2026-06" }])).recurring.length,
).toBe(1);
expect(
parseDatasetJson(
withRecurring([{ ...valid, endedFrom: "2026-06-17" }]), // month-only recurrence, day-granular end
).recurring.length,
).toBe(1);
});
test("an interval below 1 is refused, not clamped", () => {
// Clamping would silently change the bill's schedule. The walk in
// occurrences.ts cannot terminate without this.
expect(() => parseDatasetJson(withRecurring([{ ...valid, recurrence: { kind: "everyNDays", n: 0 } }])))
.toThrow(/at least 1/);
});
test("a fractional interval is refused", () => {
expect(() => parseDatasetJson(withRecurring([{ ...valid, recurrence: { kind: "everyNDays", n: 1.5 } }])))
.toThrow(/whole number/);
});
test("everyNMonths accepts either startDate granularity (C1)", () => {
expect(parseDatasetJson(withRecurring([{ ...valid, startDate: "2026-01" }])).recurring.length).toBe(1);
expect(parseDatasetJson(withRecurring([{ ...valid, startDate: "2026-01-15" }])).recurring.length).toBe(1);
});
test("everyNDays with a month-only startDate is refused (C1)", () => {
// This is the exact reachable bug: occurrencesOf hands a month-only
// startDate straight to addDays for a day-granular kind and throws deep
// inside the fold. Reject it here instead, at the import boundary.
expect(() =>
parseDatasetJson(
withRecurring([{ ...valid, startDate: "2026-01", recurrence: { kind: "everyNDays", n: 30 } }]),
),
).toThrow(/month-only start date/);
});
test("everyNWeeks with a month-only startDate is refused (C1)", () => {
expect(() =>
parseDatasetJson(
withRecurring([
{ ...valid, startDate: "2026-01", recurrence: { kind: "everyNWeeks", n: 1, weekday: 2 } },
]),
),
).toThrow(/month-only start date/);
});
test("everyNDays with a day-granular startDate is accepted (C1)", () => {
expect(
parseDatasetJson(
withRecurring([{ ...valid, startDate: "2026-01-05", recurrence: { kind: "everyNDays", n: 30 } }]),
).recurring.length,
).toBe(1);
});
// Residual C1: PURCHASE_DATE/DAY_GRANULAR_DATE check shape and range
// ("31" is a valid day-of-month digit pair), not calendar validity.
// September has 30 days, so "2026-09-31" passed both regexes and the
// fold later threw "Invalid day in IsoDate" out of addDays/toDayOrdinal.
test("an impossible day on a day-granular kind is refused, not just shape-checked (residual C1)", () => {
expect(() =>
parseDatasetJson(
withRecurring([
{ ...valid, startDate: "2026-02-30", recurrence: { kind: "everyNDays", n: 1 } },
]),
),
).toThrow(/calendar-impossible/);
});
test("an impossible day is refused even under everyNMonths (residual C1)", () => {
expect(() =>
parseDatasetJson(withRecurring([{ ...valid, startDate: "2026-09-31" }])),
).toThrow(/calendar-impossible/);
});
test("leap-year and non-leap-year Februaries are accepted (residual C1)", () => {
expect(
parseDatasetJson(
withRecurring([
{ ...valid, startDate: "2024-02-29", recurrence: { kind: "everyNDays", n: 1 } }, // 2024 is a leap year
]),
).recurring.length,
).toBe(1);
expect(
parseDatasetJson(
withRecurring([
{ ...valid, startDate: "2026-02-28", recurrence: { kind: "everyNDays", n: 1 } }, // 2026 is not
]),
).recurring.length,
).toBe(1);
});
test("an unknown recurrence kind is refused", () => {
expect(() => parseDatasetJson(withRecurring([{ ...valid, recurrence: { kind: "everyFullMoon", n: 1 } }])))
.toThrow(/unknown kind/);
});
test("a weekday outside 0-6 is refused", () => {
expect(() =>
parseDatasetJson(withRecurring([{ ...valid, recurrence: { kind: "everyNWeeks", n: 1, weekday: 7 } }])),
).toThrow(/weekday/);
});
test("an unknown anchoring is refused", () => {
expect(() => parseDatasetJson(withRecurring([{ ...valid, anchoring: "whenever" }])))
.toThrow(/anchoring/);
});
test("splits must have exactly one remainder absorber", () => {
expect(() =>
parseDatasetJson(withRecurring([{
...valid,
splits: [
{ postId: "housing", value: 50, absorbsRemainder: true },
{ postId: "housing", value: 50, absorbsRemainder: true },
],
}])),
).toThrow(/absorbsRemainder/);
});
test("a split naming an unknown post is refused", () => {
expect(() =>
parseDatasetJson(withRecurring([{
...valid,
splits: [{ postId: "ghost", value: 100, absorbsRemainder: true }],
}])),
).toThrow(/unknown post/);
});
test("no splits at all is refused", () => {
expect(() => parseDatasetJson(withRecurring([{ ...valid, splits: [] }])))
.toThrow(/no splits/);
});
test("a null recurring cost entry is refused, not a raw TypeError", () => {
// `recurring: [null]` used to reach `cost.id`/`cost.name` unguarded and
// throw TypeError instead of the file's own ImportValidationError.
expect(() => parseDatasetJson(withRecurring([null]))).toThrow(ImportValidationError);
});
test("an unknown splitMode is refused", () => {
// charges.ts picks distributeByWeight/distributeByAmount on
// `splitMode === "percent"`; any other string silently takes the "fixed"
// branch and treats a percentage as an absolute amount.
expect(() => parseDatasetJson(withRecurring([{ ...valid, splitMode: "ratio" }])))
.toThrow(/splitMode/);
});
test("a non-numeric split value is refused", () => {
// A non-finite value would otherwise propagate as NaN through the fold,
// and roundMoney(NaN, digits) returns NaN rather than throwing.
expect(() =>
parseDatasetJson(withRecurring([{
...valid,
splits: [{ postId: "housing", value: "oops", absorbsRemainder: true }],
}])),
).toThrow(/finite number/);
});
test("a negative split value is still accepted", () => {
// Deliberately unchecked per AGENTS.md §3: a refund is a normal line.
// Only non-numbers are rejected, never a negative or over-100 value.
expect(() =>
parseDatasetJson(withRecurring([{
...valid,
splits: [{ postId: "housing", value: -50, absorbsRemainder: true }],
}])),
).not.toThrow();
});
});
describe("Purchase.source validation", () => {
function withSource(source: unknown): string {
const data: any = createSeedDataset("2026-01");
data.posts[0].id = "housing";
data.recurring = [];
data.purchases = [{
id: "p1",
date: "2026-01",
description: "Rent",
total: { amount: 8000, currency: "DKK" },
splitMode: "percent",
splits: [{ postId: "housing", value: 100, absorbsRemainder: true }],
schedule: null,
source,
}];
return JSON.stringify(data);
}
test("a purchase with no source is fine", () => {
const data: any = createSeedDataset("2026-01");
data.recurring = [];
expect(() => parseDatasetJson(JSON.stringify(data))).not.toThrow();
});
test("a source naming an unknown cost is refused", () => {
expect(() => parseDatasetJson(withSource({ recurringId: "ghost", occurrenceDate: "2026-01" })))
.toThrow(/unknown recurring cost/);
});
test("a source with a malformed occurrence date is refused", () => {
const data: any = JSON.parse(withSource({ recurringId: "r1", occurrenceDate: "nope" }));
data.recurring = [{
id: "r1", name: "Rent", order: 3, folderId: null, archived: false,
amount: { amount: 8000, currency: "DKK" },
startDate: "2026-01",
recurrence: { kind: "everyNMonths", n: 1 },
anchoring: "calendar",
splitMode: "percent",
splits: [{ postId: "housing", value: 100, absorbsRemainder: true }],
}];
expect(() => parseDatasetJson(JSON.stringify(data))).toThrow(/occurrence date/);
});
test("a source of null is refused, not a raw TypeError", () => {
// `source: null` used to reach `null.recurringId` unguarded — a plausible
// hand-edit, since the sibling field on the same object literal IS
// `schedule: null`. `source` is typed absent-or-object, never null, so
// this must fail as ImportValidationError, not TypeError.
expect(() => parseDatasetJson(withSource(null))).toThrow(ImportValidationError);
});
});
describe("purchase splits validation", () => {
// I1/I2 apply identically to purchases: the shared requireSplits() helper
// must not check purchases to a lower standard than recurring costs.
test("an unknown splitMode on a purchase is refused", () => {
const data: any = populated();
data.purchases[0].splitMode = "ratio";
expect(() => parseDatasetJson(JSON.stringify(data))).toThrow(/splitMode/);
});
test("a non-numeric split value on a purchase is refused", () => {
const data: any = populated();
data.purchases[0].splits[0].value = "oops";
expect(() => parseDatasetJson(JSON.stringify(data))).toThrow(/finite number/);
});
test("a negative split value on a purchase is still accepted", () => {
const data: any = populated();
data.purchases[0].splits[0].value = -70;
expect(() => parseDatasetJson(JSON.stringify(data))).not.toThrow();
});
test("a well-formed purchase still round-trips through the shared helper", () => {
const data = populated();
expect(parseDatasetJson(exportDatasetJson(data)).purchases.length).toBe(1);
});
});