Annehmen war bisher nur ein Etikett: Man stellte den Status um, und es passierte nichts. Kunde, Projekt, Aufgabenliste, Termin und Zugang musste man danach von Hand in fünf Schritten nachbauen. Der alte Code gab das im Kommentar selbst zu -- schlug der zweite von zwei Aufrufen fehl, stand der Kunde schon in der Datenbank und man durfte es nicht noch einmal versuchen. Jetzt macht das EIN Aufruf, ganz oder gar nicht: Kunde finden oder anlegen, Projekt anlegen, die komplette Aufgabenliste des Pakets einsetzen, Liefertermin berechnen, Einladungslink erzeugen. Der Kunde verfolgt ab diesem Moment alles in seinem Portal. Die Zeit läuft wirklich: - Eigenes Datumsfeld (termin_am) neben dem freien Text. Aus 'Mitte Oktober' kann man keine verbleibenden Tage rechnen. - Werktage statt Kalendertage, inklusive luxemburgischer Feiertage. Die beweglichen werden über die Osterformel berechnet statt gepflegt -- eine Liste ist im übernächsten Jahr lautlos falsch. - Vorschlag je Paket (Onepager 10, Website 20, Shop 30 Werktage), überschreibbar vor dem Bestätigen. - Verbleibende Zeit wird SERVERSEITIG gerechnet. Der Browser kennt die Feiertage nicht; zwei verschiedene Zahlen für denselben Termin wären schlimmer als gar keine. Ablehnen mit Grund und vorbereiteter Absage in fünf Sprachen, Text serverseitig erzeugt und vor dem Abschicken lesbar. Ein laufendes Projekt lässt sich nicht nachträglich als Anfrage ablehnen. Sechs Fehler dabei gefunden: - Ein unbekanntes Paket hätte GAR KEINEN Termin bekommen statt des Ersatzwerts. Zwei Funktionen lasen dieselbe Tabelle, nur eine hatte einen Rückfallwert. Eine fehlende Zahl fällt nirgends auf. - wd_anfragen hat keine Spalte 'firma' -- better-sqlite3 weist undefined ab, die ganze Annahme wäre gescheitert. - Migrationen stehen in einer ausdrücklichen Liste; 0015 fehlte darin. - datumKurz() wurde aufgerufen, gab es aber nicht. Der Fehler wäre erst NACH dem Anlegen aufgetreten. - Der Installations-Hinweis lag fest über dem Annehmen-Knopf und machte ihn auf dem Handy untreffbar. Die Seite reserviert jetzt Platz dafür. - 'richttermin' ist freier Text, lief aber durch einen Datumsformatierer: Der Kunde las 'Invalid Date' an der wichtigsten Stelle seines Projekts. Geprüft: 49 Prüfungen der Terminrechnung gegen nachschlagbare Osterdaten und Wochentage, 70 gegen eine echte Datenbank (darunter: zweiter Klick legt nichts doppelt an, Abbruch mittendrin lässt NICHTS zurück, gesperrter Kunde wird nicht still entsperrt), 42 im Browser auf Computer und Handy, 40 im Portal. Alle bestehenden Prüfungen weiter grün. Co-Authored-By: Claude Opus 5 <[email protected]>
265 lines
15 KiB
JavaScript
265 lines
15 KiB
JavaScript
/* =====================================================================
|
|
test-annahme.mjs — Anfrage annehmen und ablehnen
|
|
|
|
Das Annehmen ist der folgenreichste Knopf der ganzen Seite: Ein Klick
|
|
legt einen Kunden an, ein Projekt, eine Aufgabenliste, einen Termin
|
|
und einen Zugang. Geht dabei etwas schief, entsteht Halbes, das man
|
|
von Hand wieder auseinanderklauben muss — und beim Kunden ist der
|
|
Eindruck schon entstanden.
|
|
|
|
Deshalb wird hier nicht nur der gute Fall geprüft, sondern vor allem:
|
|
- der ZWEITE Klick (darf kein zweites Projekt anlegen)
|
|
- der Abbruch mittendrin (darf NICHTS zurücklassen)
|
|
- der schon bekannte Kunde (darf keinen Zwilling erzeugen)
|
|
- der gesperrte Kunde (darf nicht still entsperrt werden)
|
|
===================================================================== */
|
|
import { mkdtempSync, rmSync, writeSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { randomBytes, createHmac } from "node:crypto";
|
|
|
|
const L = (...a) => writeSync(1, a.join(" ") + "\n");
|
|
const tmp = mkdtempSync(join(tmpdir(), "wd-ann-"));
|
|
process.env.DB_PATH = join(tmp, "t.db");
|
|
process.env.ENCRYPTION_KEY = randomBytes(32).toString("base64");
|
|
process.env.WEBDESIGN_API_SECRET = randomBytes(24).toString("hex");
|
|
process.env.WD_BASIS_URL = "https://dogfather-universe.com";
|
|
|
|
const { initDb, db } = await import("./db.js");
|
|
initDb();
|
|
const geheim = await import("./lib/webdesign-geheimnisse.js");
|
|
await geheim.geheimnisseLaden();
|
|
const { vorlagenSicherstellen } = await import("./lib/webdesign-vorlagen.js");
|
|
vorlagenSicherstellen();
|
|
const A = await import("./routes/webdesign-annahme.js");
|
|
|
|
let ok = 0, fehler = 0;
|
|
const pruefe = (n, b, z) => b
|
|
? (ok++, L(" ok " + n))
|
|
: (fehler++, L(" FEHL " + n + (z !== undefined ? " -> " + z : "")));
|
|
|
|
function ausweis(rolle = "dogfather", gueltigMs = 60_000) {
|
|
const nutzlast = Buffer.from(JSON.stringify({
|
|
bereich: "wd-admin", rolle, exp: Date.now() + gueltigMs,
|
|
})).toString("base64url");
|
|
const sig = createHmac("sha256", process.env.WEBDESIGN_API_SECRET)
|
|
.update(nutzlast).digest("base64url");
|
|
return nutzlast + "." + sig;
|
|
}
|
|
const REQ = (extra = {}) => ({
|
|
headers: { authorization: "Bearer " + ausweis() }, cookies: {},
|
|
params: {}, body: {}, query: {}, ...extra,
|
|
});
|
|
function antwortFaenger() {
|
|
let out = null;
|
|
return { res: { status() { return this; }, json(o) { out = o; return this; },
|
|
setHeader() {}, end() {}, writeHead() { return this; } }, holen: () => out };
|
|
}
|
|
async function ruf(fn, extra) {
|
|
const f = antwortFaenger();
|
|
await fn(REQ(extra), f.res);
|
|
return f.holen();
|
|
}
|
|
|
|
const anfrageAnlegen = (id, email, paket = "website", name = "Anna Muster") => {
|
|
db.prepare(
|
|
`INSERT INTO wd_anfragen (id, nummer, name, email, telefon, paket, details, status, sprache, erstellt_am)
|
|
VALUES (?,?,?,?,?,?,?, 'neu', 'de', ?)`
|
|
).run(id, "A-" + id, name, email, "+352 000", paket, "Bitte modern und schlicht.", new Date().toISOString());
|
|
};
|
|
|
|
L("\n1. VORSCHAU — was passiert, BEVOR etwas passiert?");
|
|
anfrageAnlegen("a1", "[email protected]");
|
|
let v = await ruf(A.annahmeVorschau, { params: { id: "a1" } });
|
|
pruefe("die Vorschau antwortet", v && v.ok === true, JSON.stringify(v).slice(0, 120));
|
|
pruefe("sie schlaegt einen Titel vor", v.titelVorschlag === "Website für Anna Muster", v.titelVorschlag);
|
|
pruefe("sie schlaegt einen echten Termin vor", /^\d{4}-\d{2}-\d{2}$/.test(v.terminVorschlag || ""), v.terminVorschlag);
|
|
pruefe("mit 20 Werktagen fuer eine Website", v.werktage === 20, v.werktage);
|
|
pruefe("sie kennt die Zahl der Arbeitsschritte", v.schritte > 5, v.schritte);
|
|
pruefe("sie sagt, dass der Kunde neu ist", v.kundeNeu === true);
|
|
pruefe("und dass noch nichts angenommen wurde", v.bereitsAngenommen === null);
|
|
pruefe("die Vorschau hat NICHTS angelegt",
|
|
db.prepare(`SELECT COUNT(*) AS n FROM wd_projekte`).get().n === 0);
|
|
|
|
L("\n2. ANNEHMEN — ein Klick, und alles steht");
|
|
let r = await ruf(A.anfrageAnnehmen, { params: { id: "a1" } });
|
|
pruefe("die Annahme gelingt", r && r.ok === true, JSON.stringify(r).slice(0, 200));
|
|
pruefe("es gibt eine Projektnummer", /^P-/.test(r.projektNummer || ""), r.projektNummer);
|
|
pruefe("der Kunde wurde angelegt", r.kundeNeu === true);
|
|
pruefe("es gibt einen Einladungslink", /einladung=/.test(r.einladungsLink || ""), r.einladungsLink);
|
|
|
|
const projekt = db.prepare(`SELECT * FROM wd_projekte WHERE id = ?`).get(r.projektId);
|
|
pruefe("das Projekt haengt an der Anfrage", projekt.anfrage_id === "a1");
|
|
pruefe("es hat einen echten Liefertermin", /^\d{4}-\d{2}-\d{2}$/.test(projekt.termin_am || ""), projekt.termin_am);
|
|
pruefe("der Zeitpunkt der Annahme ist festgehalten", !!projekt.angenommen_am);
|
|
pruefe("die zugesagten Werktage sind festgehalten", projekt.termin_werktage === 20, projekt.termin_werktage);
|
|
/* Der erste Zug liegt beim Kunden -- ohne Unterlagen kann nichts
|
|
beginnen. Stuende hier "dogfather", saehe der Kunde "wird bearbeitet"
|
|
und wuesste nicht, dass auf ihn gewartet wird. */
|
|
pruefe("der erste Zug liegt beim KUNDEN", projekt.wartet_auf === "kunde", projekt.wartet_auf);
|
|
pruefe("es gibt einen naechsten Schritt im Klartext",
|
|
/Unterlagen/.test(projekt.naechster_schritt || ""), projekt.naechster_schritt);
|
|
|
|
const aufgaben = db.prepare(`SELECT COUNT(*) AS n FROM wd_aufgaben WHERE projekt_id = ?`).get(r.projektId);
|
|
pruefe("die Aufgabenliste aus der Vorlage steht", aufgaben.n > 5, aufgaben.n);
|
|
pruefe("und die Antwort nennt dieselbe Zahl", r.schritte === aufgaben.n, r.schritte + " vs. " + aufgaben.n);
|
|
|
|
const kunde = db.prepare(`SELECT * FROM wd_kunden WHERE id = ?`).get(r.kundeId);
|
|
pruefe("der Kunde ist freigeschaltet", kunde.freigeschaltet === 1);
|
|
pruefe("seine Sprache wurde uebernommen", kunde.sprache === "de", kunde.sprache);
|
|
pruefe("seine Telefonnummer wurde uebernommen", !!kunde.telefon);
|
|
|
|
const anf = db.prepare(`SELECT * FROM wd_anfragen WHERE id = 'a1'`).get();
|
|
pruefe("die Anfrage steht auf angenommen", anf.status === "angenommen", anf.status);
|
|
pruefe("und ist mit dem Kunden verknuepft", anf.kunde_id === r.kundeId);
|
|
|
|
L("\n3. DER ZWEITE KLICK — darf nichts doppelt anlegen");
|
|
const vorher = db.prepare(`SELECT COUNT(*) AS n FROM wd_projekte`).get().n;
|
|
const r2 = await ruf(A.anfrageAnnehmen, { params: { id: "a1" } });
|
|
pruefe("der zweite Klick meldet KEINEN Fehler", r2 && r2.ok === true, JSON.stringify(r2).slice(0, 120));
|
|
pruefe("er sagt, dass schon angenommen war", r2.bereitsAngenommen === true);
|
|
pruefe("er liefert dieselbe Projektnummer", r2.projektNummer === r.projektNummer);
|
|
pruefe("und es entstand KEIN zweites Projekt",
|
|
db.prepare(`SELECT COUNT(*) AS n FROM wd_projekte`).get().n === vorher,
|
|
db.prepare(`SELECT COUNT(*) AS n FROM wd_projekte`).get().n + " statt " + vorher);
|
|
pruefe("auch keine zweite Aufgabenliste",
|
|
db.prepare(`SELECT COUNT(*) AS n FROM wd_aufgaben WHERE projekt_id = ?`).get(r.projektId).n === aufgaben.n);
|
|
|
|
L("\n4. EIN KUNDE, DER SCHON DA IST — kein Zwilling");
|
|
/* Bewusst mit anderer Gross-/Kleinschreibung: "Anna@" und "anna@" sind
|
|
derselbe Mensch. Zwei Kundeneintraege fuer eine Person sind spaeter
|
|
kaum wieder zu trennen -- Nachrichten und Projekte haengen dann an
|
|
verschiedenen Zugaengen. */
|
|
anfrageAnlegen("a2", "[email protected]", "onepager");
|
|
const kundenVorher = db.prepare(`SELECT COUNT(*) AS n FROM wd_kunden`).get().n;
|
|
const r3 = await ruf(A.anfrageAnnehmen, { params: { id: "a2" } });
|
|
pruefe("die zweite Anfrage wird angenommen", r3 && r3.ok === true, JSON.stringify(r3).slice(0, 120));
|
|
pruefe("KEIN zweiter Kundeneintrag trotz anderer Schreibweise",
|
|
db.prepare(`SELECT COUNT(*) AS n FROM wd_kunden`).get().n === kundenVorher,
|
|
db.prepare(`SELECT COUNT(*) AS n FROM wd_kunden`).get().n);
|
|
pruefe("es ist derselbe Kunde", r3.kundeId === r.kundeId);
|
|
pruefe("kein neuer Einladungslink -- er hat ja schon einen Zugang",
|
|
r3.einladungsLink === null, String(r3.einladungsLink));
|
|
pruefe("aber ein eigenes, zweites Projekt", r3.projektId !== r.projektId);
|
|
pruefe("Onepager bekommt 10 Werktage",
|
|
db.prepare(`SELECT termin_werktage AS w FROM wd_projekte WHERE id = ?`).get(r3.projektId).w === 10);
|
|
|
|
L("\n5. EIN GESPERRTER KUNDE DARF NICHT STILL ENTSPERRT WERDEN");
|
|
db.prepare(`UPDATE wd_kunden SET gesperrt = 1, freigeschaltet = 0 WHERE id = ?`).run(r.kundeId);
|
|
anfrageAnlegen("a3", "[email protected]", "shop");
|
|
await ruf(A.anfrageAnnehmen, { params: { id: "a3" } });
|
|
const nachher = db.prepare(`SELECT gesperrt, freigeschaltet FROM wd_kunden WHERE id = ?`).get(r.kundeId);
|
|
pruefe("er bleibt gesperrt", nachher.gesperrt === 1, nachher.gesperrt);
|
|
pruefe("und wird nicht nebenbei freigeschaltet", nachher.freigeschaltet === 0, nachher.freigeschaltet);
|
|
db.prepare(`UPDATE wd_kunden SET gesperrt = 0, freigeschaltet = 1 WHERE id = ?`).run(r.kundeId);
|
|
|
|
L("\n6. EIGENER TERMIN STATT VORSCHLAG");
|
|
anfrageAnlegen("a4", "[email protected]", "website", "Bert Beispiel");
|
|
const r4 = await ruf(A.anfrageAnnehmen, { params: { id: "a4" }, body: { termin: "2026-12-24" } });
|
|
pruefe("ein eigener Termin wird uebernommen",
|
|
db.prepare(`SELECT termin_am AS t FROM wd_projekte WHERE id = ?`).get(r4.projektId).t === "2026-12-24");
|
|
anfrageAnlegen("a5", "[email protected]", "website", "Carla Muster");
|
|
const r5 = await ruf(A.anfrageAnnehmen, { params: { id: "a5" }, body: { termin: "31.13.2026" } });
|
|
pruefe("ein unmoegliches Datum wird abgewiesen", r5.ok === false, JSON.stringify(r5).slice(0, 120));
|
|
pruefe("und dabei entsteht NICHTS",
|
|
db.prepare(`SELECT COUNT(*) AS n FROM wd_projekte WHERE anfrage_id = 'a5'`).get().n === 0);
|
|
|
|
L("\n7. LAUFENDE BETREUUNG — kein Liefertermin, kein Countdown");
|
|
anfrageAnlegen("a6", "[email protected]", "betreuung", "Dora Muster");
|
|
const r6 = await ruf(A.anfrageAnnehmen, { params: { id: "a6" } });
|
|
pruefe("sie wird angenommen", r6.ok === true);
|
|
pruefe("aber ohne Liefertermin",
|
|
db.prepare(`SELECT termin_am AS t FROM wd_projekte WHERE id = ?`).get(r6.projektId).t === null);
|
|
|
|
L("\n8. OHNE E-MAIL GEHT ES NICHT — und es bleibt nichts liegen");
|
|
db.prepare(
|
|
`INSERT INTO wd_anfragen (id, nummer, name, email, paket, status, sprache, erstellt_am)
|
|
VALUES ('a7','A-a7','Ohne Mail','', 'website', 'neu','de',?)`
|
|
).run(new Date().toISOString());
|
|
const r7 = await ruf(A.anfrageAnnehmen, { params: { id: "a7" } });
|
|
pruefe("die Annahme wird abgelehnt", r7.ok === false, JSON.stringify(r7).slice(0, 140));
|
|
pruefe("und es entstand kein Kunde ohne Adresse",
|
|
db.prepare(`SELECT COUNT(*) AS n FROM wd_kunden WHERE email = ''`).get().n === 0);
|
|
|
|
L("\n9. ABBRUCH MITTENDRIN — es darf NICHTS zurueckbleiben");
|
|
/* Der wichtigste Fall. Geprueft wird, indem der Vorlagenschritt zum
|
|
Scheitern gebracht wird: Die Tabelle wd_aufgaben bekommt kurz einen
|
|
Ausloeser, der jedes Einfuegen verweigert. Ohne Transaktion blieben
|
|
Kunde und Projekt stehen -- ein Zugang, hinter dem eine leere Seite
|
|
wartet. */
|
|
db.exec(`CREATE TRIGGER pruef_stopp BEFORE INSERT ON wd_aufgaben
|
|
BEGIN SELECT RAISE(ABORT, 'absichtlicher Testfehler'); END;`);
|
|
anfrageAnlegen("a8", "[email protected]", "website", "Emil Muster");
|
|
const kundenVorAbbruch = db.prepare(`SELECT COUNT(*) AS n FROM wd_kunden`).get().n;
|
|
const projekteVorAbbruch = db.prepare(`SELECT COUNT(*) AS n FROM wd_projekte`).get().n;
|
|
/* Vorher zaehlen statt eine feste Zahl zu erwarten: Bis hierher wurden
|
|
drei Kunden voellig zurecht angelegt, also gibt es drei Zugaenge. Die
|
|
erste Fassung dieser Pruefung verlangte "hoechstens einer" und schlug
|
|
deshalb fehl, obwohl alles stimmte -- eine feste Erwartung an einen
|
|
mitwachsenden Bestand ist immer falsch. */
|
|
const zugaengeVorAbbruch = db.prepare(`SELECT COUNT(*) AS n FROM wd_kunden_token`).get().n;
|
|
const r8 = await ruf(A.anfrageAnnehmen, { params: { id: "a8" } });
|
|
db.exec(`DROP TRIGGER pruef_stopp;`);
|
|
|
|
pruefe("der Abbruch wird gemeldet", r8 && r8.ok === false, JSON.stringify(r8).slice(0, 160));
|
|
pruefe("KEIN Kunde blieb zurueck",
|
|
db.prepare(`SELECT COUNT(*) AS n FROM wd_kunden`).get().n === kundenVorAbbruch,
|
|
db.prepare(`SELECT COUNT(*) AS n FROM wd_kunden`).get().n + " statt " + kundenVorAbbruch);
|
|
pruefe("KEIN Projekt blieb zurueck",
|
|
db.prepare(`SELECT COUNT(*) AS n FROM wd_projekte`).get().n === projekteVorAbbruch);
|
|
pruefe("die Anfrage steht NICHT faelschlich auf angenommen",
|
|
db.prepare(`SELECT status FROM wd_anfragen WHERE id = 'a8'`).get().status === "neu");
|
|
pruefe("und der Abbruch hinterliess KEINEN neuen Zugang",
|
|
db.prepare(`SELECT COUNT(*) AS n FROM wd_kunden_token`).get().n === zugaengeVorAbbruch,
|
|
db.prepare(`SELECT COUNT(*) AS n FROM wd_kunden_token`).get().n + " statt " + zugaengeVorAbbruch);
|
|
|
|
L("\n10. ABLEHNEN");
|
|
anfrageAnlegen("b1", "[email protected]", "shop", "Frank Muster");
|
|
const vorschau = await ruf(A.absageVorschau, { params: { id: "b1" }, query: { grund: "budget" } });
|
|
pruefe("die Absage-Vorschau antwortet", vorschau.ok === true);
|
|
pruefe("sie nennt den Kunden beim Namen", /Frank Muster/.test(vorschau.text), vorschau.text.slice(0, 60));
|
|
pruefe("sie nennt den Grund im Klartext", /Budget/.test(vorschau.text));
|
|
pruefe("die Vorschau veraendert NICHTS",
|
|
db.prepare(`SELECT status FROM wd_anfragen WHERE id = 'b1'`).get().status === "neu");
|
|
|
|
const ab = await ruf(A.anfrageAblehnen, { params: { id: "b1" }, body: { grund: "budget", senden: false } });
|
|
pruefe("das Ablehnen gelingt", ab.ok === true, JSON.stringify(ab).slice(0, 120));
|
|
const b1 = db.prepare(`SELECT * FROM wd_anfragen WHERE id = 'b1'`).get();
|
|
pruefe("der Status steht auf abgelehnt", b1.status === "abgelehnt");
|
|
pruefe("der Grund ist auswertbar hinterlegt", b1.ablehnung_grund === "budget", b1.ablehnung_grund);
|
|
pruefe("ohne Senden ist nichts verschickt", b1.absage_gesendet_am === null);
|
|
|
|
L("\n11. DER TEXT KOMMT IN DER SPRACHE DES KUNDEN");
|
|
for (const [sp, wort] of [["fr", "Bonjour"], ["pt", "Olá"], ["en", "Hello"], ["de-CH", "Grüsse"]]) {
|
|
const t = A.absageText(sp, "kein_zeitfenster", "Test");
|
|
pruefe("Absage auf " + sp, t.includes(wort), t.split("\n")[0]);
|
|
}
|
|
pruefe("eine unbekannte Sprache faellt auf Deutsch zurueck",
|
|
A.absageText("kli", "budget", "Test").includes("Hallo"));
|
|
pruefe("ein unbekannter Grund ergibt trotzdem einen vollstaendigen Text",
|
|
A.absageText("de", "gibtesnicht", "Test").includes("Dogfather Webdesign"));
|
|
|
|
L("\n12. EIN LAUFENDES PROJEKT KANN MAN NICHT NACHTRAEGLICH ABLEHNEN");
|
|
const abZuSpaet = await ruf(A.anfrageAblehnen, { params: { id: "a1" }, body: { grund: "budget" } });
|
|
pruefe("das wird verweigert", abZuSpaet.ok === false, JSON.stringify(abZuSpaet).slice(0, 140));
|
|
pruefe("mit Hinweis auf das Projekt", /P-/.test(abZuSpaet.error || ""), abZuSpaet.error);
|
|
pruefe("und die Anfrage bleibt angenommen",
|
|
db.prepare(`SELECT status FROM wd_anfragen WHERE id = 'a1'`).get().status === "angenommen");
|
|
|
|
L("\n13. DIE RECHTEPRUEFUNG GREIFT");
|
|
for (const [name, kopf] of [
|
|
["ohne Ausweis kein Annehmen", {}],
|
|
["mit gefaelschter Unterschrift kein Annehmen",
|
|
{ authorization: "Bearer " + ausweis().split(".")[0] + ".XXXX" }],
|
|
["mit abgelaufenem Ausweis kein Annehmen",
|
|
{ authorization: "Bearer " + ausweis("dogfather", -1000) }],
|
|
]) {
|
|
const f = antwortFaenger();
|
|
await A.anfrageAnnehmen({ headers: kopf, cookies: {}, params: { id: "b1" }, body: {} }, f.res);
|
|
pruefe(name, f.holen() && f.holen().ok === false, JSON.stringify(f.holen()).slice(0, 80));
|
|
}
|
|
|
|
L(`\n===== ${ok} bestanden, ${fehler} fehlgeschlagen =====`);
|
|
process.exitCode = fehler ? 1 : 0;
|
|
rmSync(tmp, { recursive: true, force: true });
|