feat: publish Velum copies beside project folder
This commit is contained in:
@@ -3,7 +3,7 @@ import { basename, dirname, join } from "node:path";
|
|||||||
import type { PlaceholderMapping } from "../types";
|
import type { PlaceholderMapping } from "../types";
|
||||||
import { canonicalPerson } from "./project-review";
|
import { canonicalPerson } from "./project-review";
|
||||||
import {
|
import {
|
||||||
createRun, digest, isRestorableDocument, listRuns, loadProjectWhitelist,
|
createRun, digest, isRestorableDocument, listRuns, loadProjectWhitelist, publishProjectCopies,
|
||||||
replaceProjectFilesInPlace, resolveWithin, restorePath, restoreText,
|
replaceProjectFilesInPlace, resolveWithin, restorePath, restoreText,
|
||||||
saveProjectWhitelist, saveRun,
|
saveProjectWhitelist, saveRun,
|
||||||
} from "./velum-project";
|
} from "./velum-project";
|
||||||
@@ -128,6 +128,7 @@ export async function reviseProjectRun(args: {
|
|||||||
Object.assign(textReplacements, plan.replacements);
|
Object.assign(textReplacements, plan.replacements);
|
||||||
let replacementAttempted = false;
|
let replacementAttempted = false;
|
||||||
let whitelistChanged = false;
|
let whitelistChanged = false;
|
||||||
|
let publishedCopyPath: string | undefined;
|
||||||
try {
|
try {
|
||||||
for (const file of previous.files) {
|
for (const file of previous.files) {
|
||||||
const source = resolveWithin(join(previousDirectory, "files"), file.output);
|
const source = resolveWithin(join(previousDirectory, "files"), file.output);
|
||||||
@@ -163,12 +164,18 @@ export async function reviseProjectRun(args: {
|
|||||||
await saveRun(directory, run);
|
await saveRun(directory, run);
|
||||||
replacementAttempted = true;
|
replacementAttempted = true;
|
||||||
await replaceProjectFilesInPlace(previous.projectPath, directory, run.files);
|
await replaceProjectFilesInPlace(previous.projectPath, directory, run.files);
|
||||||
} else await saveRun(directory, run);
|
} else {
|
||||||
|
publishedCopyPath = await publishProjectCopies(run, directory);
|
||||||
|
run.copyPath = publishedCopyPath;
|
||||||
|
await saveRun(directory, run);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Keep the backup if replacing the project folder was attempted, even when
|
// Keep the backup if replacing the project folder was attempted, even when
|
||||||
// its own rollback reports an error.
|
// its own rollback reports an error.
|
||||||
if (!replacementAttempted) await fs.rm(directory, { recursive: true, force: true });
|
if (!replacementAttempted) {
|
||||||
else await fs.rm(join(directory, "run.json"), { force: true });
|
await fs.rm(directory, { recursive: true, force: true });
|
||||||
|
if (publishedCopyPath) await fs.rm(publishedCopyPath, { recursive: true, force: true });
|
||||||
|
} else await fs.rm(join(directory, "run.json"), { force: true });
|
||||||
if (whitelistChanged) {
|
if (whitelistChanged) {
|
||||||
try { await saveProjectWhitelist(previous.projectPath, previousWhitelist, support); }
|
try { await saveProjectWhitelist(previous.projectPath, previousWhitelist, support); }
|
||||||
catch (rollbackError) {
|
catch (rollbackError) {
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ export type ProjectRun = {
|
|||||||
commentAuthorCoverage?: boolean;
|
commentAuthorCoverage?: boolean;
|
||||||
/** Word tracked-change authors and deleted text were reviewed. */
|
/** Word tracked-change authors and deleted text were reviewed. */
|
||||||
reviewAuthorCoverage?: boolean;
|
reviewAuthorCoverage?: boolean;
|
||||||
|
/** Visible copy next to the source project; immutable snapshots remain in Raycast support. */
|
||||||
|
copyPath?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const DOCUMENTS = new Set([".docx", ".xlsx", ".pptx", ".pdf"]);
|
const DOCUMENTS = new Set([".docx", ".xlsx", ".pptx", ".pdf"]);
|
||||||
@@ -299,6 +301,39 @@ export async function saveRun(directory: string, run: ProjectRun): Promise<void>
|
|||||||
await fs.rename(tmp, dest);
|
await fs.rename(tmp, dest);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function runCopiesPath(run: ProjectRun, directory: string): string {
|
||||||
|
return run.copyPath ?? join(directory, "files");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Publish only output files next to the project, without the private mapping. */
|
||||||
|
export async function publishProjectCopies(run: ProjectRun, directory: string): Promise<string> {
|
||||||
|
const projectPath = canonicalProjectPath(run.projectPath);
|
||||||
|
const parent = dirname(projectPath);
|
||||||
|
const target = join(parent, `${basename(projectPath)}-Velum-${run.id}`);
|
||||||
|
try {
|
||||||
|
await fs.lstat(target);
|
||||||
|
throw new Error(`Kopienordner bereits vorhanden: ${target}`);
|
||||||
|
} catch (error) {
|
||||||
|
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
||||||
|
}
|
||||||
|
const staging = await fs.mkdtemp(join(parent, `.${basename(projectPath)}-Velum-`));
|
||||||
|
try {
|
||||||
|
for (const file of run.files) {
|
||||||
|
const source = resolveWithin(join(directory, "files"), file.output);
|
||||||
|
if (!(await fs.lstat(source)).isFile()) throw new Error(`Keine reguläre Ausgabedatei: ${file.output}`);
|
||||||
|
const destination = resolveWithin(staging, file.output);
|
||||||
|
await fs.mkdir(dirname(destination), { recursive: true, mode: 0o700 });
|
||||||
|
await fs.copyFile(source, destination, fsConstants.COPYFILE_EXCL);
|
||||||
|
await fs.chmod(destination, 0o600);
|
||||||
|
}
|
||||||
|
await fs.rename(staging, target);
|
||||||
|
return target;
|
||||||
|
} catch (error) {
|
||||||
|
await fs.rm(staging, { recursive: true, force: true });
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function listRuns(projectPath: string, support = environment.supportPath): Promise<Array<{
|
export async function listRuns(projectPath: string, support = environment.supportPath): Promise<Array<{
|
||||||
run: ProjectRun; directory: string;
|
run: ProjectRun; directory: string;
|
||||||
}>> {
|
}>> {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import type { EntityType, PlaceholderMapping } from "./types";
|
|||||||
import { getEntityTypes, pseudonymize, pseudonymizeDocument, requireProjectCapabilities } from "./velum";
|
import { getEntityTypes, pseudonymize, pseudonymizeDocument, requireProjectCapabilities } from "./velum";
|
||||||
import type { DocumentHit } from "./velum";
|
import type { DocumentHit } from "./velum";
|
||||||
import {
|
import {
|
||||||
createRun, digest, forceMasks, listRuns, loadProjectEntityTypes, loadProjectRules, parseProjectRules, replaceSpans,
|
createRun, digest, forceMasks, listRuns, loadProjectEntityTypes, loadProjectRules, parseProjectRules, publishProjectCopies, replaceSpans,
|
||||||
loadProjectWhitelist, matchesPreviousOutput, replaceProjectFilesInPlace, resolveWithin, safeRelativePath, saveProjectRules,
|
loadProjectWhitelist, matchesPreviousOutput, replaceProjectFilesInPlace, resolveWithin, safeRelativePath, saveProjectRules,
|
||||||
saveProjectEntityTypes, saveProjectWhitelist, saveRun, scanProjectFiles, toUtf16Offsets, whitelistKey,
|
saveProjectEntityTypes, saveProjectWhitelist, saveRun, scanProjectFiles, toUtf16Offsets, whitelistKey,
|
||||||
} from "./lib/velum-project";
|
} from "./lib/velum-project";
|
||||||
@@ -251,7 +251,7 @@ function HitReview(props: { root: string; rules: ProjectRule[]; whitelist: Proje
|
|||||||
return next;
|
return next;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
async function apply(inPlace = false) {
|
async function apply(inPlace = true) {
|
||||||
if (!props.preview.drafts.length && !accepted.size) {
|
if (!props.preview.drafts.length && !accepted.size) {
|
||||||
try {
|
try {
|
||||||
const rejected = groups.map((group) => ({ type: group.type, original: group.original }));
|
const rejected = groups.map((group) => ({ type: group.type, original: group.original }));
|
||||||
@@ -278,6 +278,7 @@ function HitReview(props: { root: string; rules: ProjectRule[]; whitelist: Proje
|
|||||||
const toast = await showToast({ style: Toast.Style.Animated,
|
const toast = await showToast({ style: Toast.Style.Animated,
|
||||||
title: inPlace ? "Originaldateien werden ersetzt" : "Pseudonymisierte Kopien werden erstellt" });
|
title: inPlace ? "Originaldateien werden ersetzt" : "Pseudonymisierte Kopien werden erstellt" });
|
||||||
let directory: string | undefined;
|
let directory: string | undefined;
|
||||||
|
let publishedCopyPath: string | undefined;
|
||||||
let persisted = false;
|
let persisted = false;
|
||||||
try {
|
try {
|
||||||
const created = await createRun(props.root);
|
const created = await createRun(props.root);
|
||||||
@@ -336,6 +337,10 @@ function HitReview(props: { root: string; rules: ProjectRule[]; whitelist: Proje
|
|||||||
run.files.push(file);
|
run.files.push(file);
|
||||||
replacements.push(file);
|
replacements.push(file);
|
||||||
}
|
}
|
||||||
|
if (!inPlace) {
|
||||||
|
publishedCopyPath = await publishProjectCopies(run, directory);
|
||||||
|
run.copyPath = publishedCopyPath;
|
||||||
|
}
|
||||||
await saveRun(directory, run);
|
await saveRun(directory, run);
|
||||||
persisted = true;
|
persisted = true;
|
||||||
if (inPlace) {
|
if (inPlace) {
|
||||||
@@ -355,11 +360,12 @@ function HitReview(props: { root: string; rules: ProjectRule[]; whitelist: Proje
|
|||||||
toast.style = Toast.Style.Success;
|
toast.style = Toast.Style.Success;
|
||||||
toast.title = inPlace ? `${replacements.length} Dateien ersetzt` : `${run.files.length} Kopien erstellt`;
|
toast.title = inPlace ? `${replacements.length} Dateien ersetzt` : `${run.files.length} Kopien erstellt`;
|
||||||
toast.message = inPlace ? "Die Sicherung ist im Raycast-Datenordner gespeichert."
|
toast.message = inPlace ? "Die Sicherung ist im Raycast-Datenordner gespeichert."
|
||||||
: "Die Kopien sind im Raycast-Datenordner gespeichert.";
|
: "Die Kopien liegen neben dem Projektordner.";
|
||||||
setCompleted({ directory: join(directory, inPlace ? "backup" : "files"), inPlace, files: run.files.length });
|
setCompleted({ directory: inPlace ? join(directory, "backup") : publishedCopyPath!, inPlace, files: run.files.length });
|
||||||
await showInFinder(inPlace ? props.root : join(directory, "files")).catch(() => undefined);
|
await showInFinder(inPlace ? props.root : publishedCopyPath!).catch(() => undefined);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (directory && !persisted) await fs.rm(directory, { recursive: true, force: true });
|
if (directory && !persisted) await fs.rm(directory, { recursive: true, force: true });
|
||||||
|
if (publishedCopyPath && !persisted) await fs.rm(publishedCopyPath, { recursive: true, force: true });
|
||||||
toast.style = Toast.Style.Failure;
|
toast.style = Toast.Style.Failure;
|
||||||
toast.title = "Pseudonymisierung fehlgeschlagen";
|
toast.title = "Pseudonymisierung fehlgeschlagen";
|
||||||
toast.message = `${(error as Error).message}${persisted && directory ? ` Kopien/Sicherung: ${directory}` : ""}`;
|
toast.message = `${(error as Error).message}${persisted && directory ? ` Kopien/Sicherung: ${directory}` : ""}`;
|
||||||
@@ -370,7 +376,7 @@ function HitReview(props: { root: string; rules: ProjectRule[]; whitelist: Proje
|
|||||||
<List.Item icon={Icon.CheckCircle}
|
<List.Item icon={Icon.CheckCircle}
|
||||||
title={completed.inPlace ? "Projektdateien ersetzt" : `${completed.files} pseudonymisierte Kopien erstellt`}
|
title={completed.inPlace ? "Projektdateien ersetzt" : `${completed.files} pseudonymisierte Kopien erstellt`}
|
||||||
subtitle={completed.inPlace ? "Die Originale wurden gesichert."
|
subtitle={completed.inPlace ? "Die Originale wurden gesichert."
|
||||||
: "Die Originaldateien im Projektordner bleiben unverändert."}
|
: completed.directory}
|
||||||
actions={<ActionPanel>
|
actions={<ActionPanel>
|
||||||
<Action title={completed.inPlace ? "Sicherung im Finder anzeigen" : "Kopien im Finder anzeigen"}
|
<Action title={completed.inPlace ? "Sicherung im Finder anzeigen" : "Kopien im Finder anzeigen"}
|
||||||
icon={Icon.Folder} onAction={() => showInFinder(completed.directory)} />
|
icon={Icon.Folder} onAction={() => showInFinder(completed.directory)} />
|
||||||
@@ -384,8 +390,8 @@ function HitReview(props: { root: string; rules: ProjectRule[]; whitelist: Proje
|
|||||||
<List.Item icon={Icon.Play} title="Pseudonymisierung starten"
|
<List.Item icon={Icon.Play} title="Pseudonymisierung starten"
|
||||||
subtitle={`${selectedGroups.size} Zuordnungen · ${people.length} Personen · ${props.preview.retained.length} unveränderte Dateien übernommen`}
|
subtitle={`${selectedGroups.size} Zuordnungen · ${people.length} Personen · ${props.preview.retained.length} unveränderte Dateien übernommen`}
|
||||||
actions={<ActionPanel>
|
actions={<ActionPanel>
|
||||||
<Action title="Pseudonymisierte Kopien erstellen" icon={Icon.Document} onAction={() => apply()} />
|
<Action title="Originaldateien ersetzen" icon={Icon.Pencil} onAction={() => apply()} />
|
||||||
<Action title="Originaldateien ersetzen" icon={Icon.Pencil} onAction={() => apply(true)} />
|
<Action title="Pseudonymisierte Kopien erstellen" icon={Icon.Document} onAction={() => apply(false)} />
|
||||||
<Action title="Zur Trefferauswahl" icon={Icon.ArrowLeft} onAction={() => setStage("selection")} />
|
<Action title="Zur Trefferauswahl" icon={Icon.ArrowLeft} onAction={() => setStage("selection")} />
|
||||||
</ActionPanel>} />
|
</ActionPanel>} />
|
||||||
</List.Section>
|
</List.Section>
|
||||||
@@ -409,8 +415,8 @@ function HitReview(props: { root: string; rules: ProjectRule[]; whitelist: Proje
|
|||||||
onAction={() => setPersonMerges((current) => {
|
onAction={() => setPersonMerges((current) => {
|
||||||
const next = { ...current }; delete next[person.placeholder]; return next;
|
const next = { ...current }; delete next[person.placeholder]; return next;
|
||||||
})} />}
|
})} />}
|
||||||
<Action title="Pseudonymisierte Kopien erstellen" icon={Icon.Document} onAction={() => apply()} />
|
<Action title="Originaldateien ersetzen" icon={Icon.Pencil} onAction={() => apply()} />
|
||||||
<Action title="Originaldateien ersetzen" icon={Icon.Pencil} onAction={() => apply(true)} />
|
<Action title="Pseudonymisierte Kopien erstellen" icon={Icon.Document} onAction={() => apply(false)} />
|
||||||
</ActionPanel>} />;
|
</ActionPanel>} />;
|
||||||
})}
|
})}
|
||||||
</List.Section>
|
</List.Section>
|
||||||
@@ -590,6 +596,6 @@ export default function Command() {
|
|||||||
{availableTypes.map((type) => <Form.TagPicker.Item key={type} value={type} title={labelForType(type)} />)}
|
{availableTypes.map((type) => <Form.TagPicker.Item key={type} value={type} title={labelForType(type)} />)}
|
||||||
</Form.TagPicker>
|
</Form.TagPicker>
|
||||||
<Form.Description title="Projekt-Whitelist" text={`${whitelist.length} Begriffe werden bei diesem Ordner nicht mehr vorgeschlagen. Über die Aktionen kannst du sie wieder freigeben.`} />
|
<Form.Description title="Projekt-Whitelist" text={`${whitelist.length} Begriffe werden bei diesem Ordner nicht mehr vorgeschlagen. Über die Aktionen kannst du sie wieder freigeben.`} />
|
||||||
<Form.Description title="Ablauf" text="Kategorien wählen, Zuordnungen auswählen, Personen bei Bedarf zusammenfassen und dann Kopien erstellen oder Originale ersetzen." />
|
<Form.Description title="Ablauf" text="Kategorien wählen, Zuordnungen auswählen und Personen bei Bedarf zusammenfassen. Standardmäßig werden die Originaldateien nach Bestätigung ersetzt; Kopien sind als Alternative verfügbar." />
|
||||||
</Form>;
|
</Form>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { homedir } from "node:os";
|
|||||||
import { basename, dirname, join } from "node:path";
|
import { basename, dirname, join } from "node:path";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { depseudonymizeDocument } from "./velum";
|
import { depseudonymizeDocument } from "./velum";
|
||||||
import { canonicalProjectPath, isRestorableDocument, listAllRuns, resolveWithin, restorePath, restoreText } from "./lib/velum-project";
|
import { canonicalProjectPath, isRestorableDocument, listAllRuns, resolveWithin, restorePath, restoreText, runCopiesPath } from "./lib/velum-project";
|
||||||
import type { ProjectRun } from "./lib/velum-project";
|
import type { ProjectRun } from "./lib/velum-project";
|
||||||
import { canonicalPerson, mergePeople } from "./lib/project-review";
|
import { canonicalPerson, mergePeople } from "./lib/project-review";
|
||||||
import { planProjectRevision, reviseProjectRun } from "./lib/project-revision";
|
import { planProjectRevision, reviseProjectRun } from "./lib/project-revision";
|
||||||
@@ -69,7 +69,7 @@ function RevisionReview(props: { saved: SavedRun; onSaved: () => Promise<void> }
|
|||||||
toast.title = "Projektstand aktualisiert";
|
toast.title = "Projektstand aktualisiert";
|
||||||
toast.message = `${plan.released.length} freigegeben · ${plan.merged.length} zusammengeführt`;
|
toast.message = `${plan.released.length} freigegeben · ${plan.merged.length} zusammengeführt`;
|
||||||
pop();
|
pop();
|
||||||
await showInFinder(inPlace ? run.projectPath : join(result.directory, "files")).catch(() => undefined);
|
await showInFinder(inPlace ? run.projectPath : runCopiesPath(result.run, result.directory)).catch(() => undefined);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.style = Toast.Style.Failure;
|
toast.style = Toast.Style.Failure;
|
||||||
toast.title = "Nachbearbeitung fehlgeschlagen";
|
toast.title = "Nachbearbeitung fehlgeschlagen";
|
||||||
@@ -199,8 +199,8 @@ function ProjectHistory(props: { projectName: string; entries: SavedRun[] }) {
|
|||||||
subtitle={`${run.files.length} Dateien`}
|
subtitle={`${run.files.length} Dateien`}
|
||||||
actions={<ActionPanel>
|
actions={<ActionPanel>
|
||||||
<Action title="Pseudonymisierte Kopien im Finder anzeigen" icon={Icon.Folder}
|
<Action title="Pseudonymisierte Kopien im Finder anzeigen" icon={Icon.Folder}
|
||||||
onAction={() => showInFinder(join(directory, "files"))} />
|
onAction={() => showInFinder(runCopiesPath(run, directory))} />
|
||||||
<Action.CopyToClipboard title="Speicherpfad kopieren" content={join(directory, "files")} />
|
<Action.CopyToClipboard title="Speicherpfad kopieren" content={runCopiesPath(run, directory)} />
|
||||||
<Action title="Dateien als Klartext exportieren" icon={Icon.Download}
|
<Action title="Dateien als Klartext exportieren" icon={Icon.Download}
|
||||||
onAction={() => push(<FilePicker run={run} directory={directory} />)} />
|
onAction={() => push(<FilePicker run={run} directory={directory} />)} />
|
||||||
</ActionPanel>} />)}
|
</ActionPanel>} />)}
|
||||||
@@ -233,8 +233,8 @@ export default function Command() {
|
|||||||
...(entries.length > 1 ? [{ text: `${entries.length} Stände` }] : [])]}
|
...(entries.length > 1 ? [{ text: `${entries.length} Stände` }] : [])]}
|
||||||
actions={<ActionPanel>
|
actions={<ActionPanel>
|
||||||
<Action title="Pseudonymisierte Kopien im Finder anzeigen" icon={Icon.Folder}
|
<Action title="Pseudonymisierte Kopien im Finder anzeigen" icon={Icon.Folder}
|
||||||
onAction={() => showInFinder(join(directory, "files"))} />
|
onAction={() => showInFinder(runCopiesPath(run, directory))} />
|
||||||
<Action.CopyToClipboard title="Speicherpfad kopieren" content={join(directory, "files")} />
|
<Action.CopyToClipboard title="Speicherpfad kopieren" content={runCopiesPath(run, directory)} />
|
||||||
<Action title="Aktuellen Stand nachbearbeiten" icon={Icon.Pencil}
|
<Action title="Aktuellen Stand nachbearbeiten" icon={Icon.Pencil}
|
||||||
onAction={() => push(<RevisionReview saved={{ run, directory }} onSaved={refresh} />)} />
|
onAction={() => push(<RevisionReview saved={{ run, directory }} onSaved={refresh} />)} />
|
||||||
<Action title="Aktuelle Dateien als Klartext exportieren" icon={Icon.Download}
|
<Action title="Aktuelle Dateien als Klartext exportieren" icon={Icon.Download}
|
||||||
|
|||||||
@@ -35,6 +35,9 @@ describe("Nachbearbeitung von Projektständen", () => {
|
|||||||
expect(result.run.files[0].output).toBe("Falsch Positiv-PERSON_02.txt");
|
expect(result.run.files[0].output).toBe("Falsch Positiv-PERSON_02.txt");
|
||||||
expect(await fs.readFile(join(result.directory, "files", result.run.files[0].output), "utf8"))
|
expect(await fs.readFile(join(result.directory, "files", result.run.files[0].output), "utf8"))
|
||||||
.toBe("Falsch Positiv traf PERSON_02 und PERSON_02. PERSON_02 blieb.");
|
.toBe("Falsch Positiv traf PERSON_02 und PERSON_02. PERSON_02 blieb.");
|
||||||
|
expect(result.run.copyPath?.startsWith(join(root, "Quelle-Velum-"))).toBe(true);
|
||||||
|
expect(await fs.readFile(join(result.run.copyPath!, result.run.files[0].output), "utf8"))
|
||||||
|
.toBe("Falsch Positiv traf PERSON_02 und PERSON_02. PERSON_02 blieb.");
|
||||||
expect(result.run.mapping.PERSON_03.merged_into).toBe("PERSON_02");
|
expect(result.run.mapping.PERSON_03.merged_into).toBe("PERSON_02");
|
||||||
expect(result.run.mapping.PERSON_04.merged_into).toBe("PERSON_02");
|
expect(result.run.mapping.PERSON_04.merged_into).toBe("PERSON_02");
|
||||||
expect(result.run.mapping.PERSON_01).toBeUndefined();
|
expect(result.run.mapping.PERSON_01).toBeUndefined();
|
||||||
|
|||||||
@@ -53,6 +53,29 @@ describe("Velum-Projektstände", () => {
|
|||||||
.toEqual([first.run.id, second.run.id].sort());
|
.toEqual([first.run.id, second.run.id].sort());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("legt pseudonymisierte Kopien neben dem Projekt ab und behält die Tabelle privat", async () => {
|
||||||
|
const root = await fs.mkdtemp(join(tmpdir(), "velum-sibling-copy-test-"));
|
||||||
|
roots.push(root);
|
||||||
|
const source = join(root, "Quelle");
|
||||||
|
const support = join(root, "Raycast");
|
||||||
|
await fs.mkdir(source);
|
||||||
|
const created = await project.createRun(source, support);
|
||||||
|
created.run.mapping = { PERSON_01: { original: "Max Mustermann", type: "PERSON" } };
|
||||||
|
created.run.files = [{ source: "Unterordner/Name.txt", output: "Unterordner/PERSON_01.txt", kind: "text" }];
|
||||||
|
await fs.mkdir(join(created.directory, "files", "Unterordner"));
|
||||||
|
await fs.writeFile(join(created.directory, "files", "Unterordner", "PERSON_01.txt"), "PERSON_01");
|
||||||
|
const copyPath = await project.publishProjectCopies(created.run, created.directory);
|
||||||
|
created.run.copyPath = copyPath;
|
||||||
|
await project.saveRun(created.directory, created.run);
|
||||||
|
expect(copyPath.startsWith(join(root, "Quelle-Velum-"))).toBe(true);
|
||||||
|
expect(project.runCopiesPath(created.run, created.directory)).toBe(copyPath);
|
||||||
|
expect(await fs.readFile(join(copyPath, "Unterordner", "PERSON_01.txt"), "utf8")).toBe("PERSON_01");
|
||||||
|
expect(await fs.readdir(copyPath)).toEqual(["Unterordner"]);
|
||||||
|
expect(await fs.readFile(join(created.directory, "run.json"), "utf8")).toContain("Max Mustermann");
|
||||||
|
await expect(project.publishProjectCopies(created.run, created.directory)).rejects.toThrow("bereits vorhanden");
|
||||||
|
expect((await project.listRuns(source, support))[0].run.copyPath).toBe(copyPath);
|
||||||
|
});
|
||||||
|
|
||||||
it("behält die Kategorieauswahl pro Projekt für den nächsten Lauf", async () => {
|
it("behält die Kategorieauswahl pro Projekt für den nächsten Lauf", async () => {
|
||||||
const root = await fs.mkdtemp(join(tmpdir(), "velum-entity-types-test-"));
|
const root = await fs.mkdtemp(join(tmpdir(), "velum-entity-types-test-"));
|
||||||
roots.push(root);
|
roots.push(root);
|
||||||
|
|||||||
Reference in New Issue
Block a user