Files
NetMesh/application/state/sftp/transferSettlementObservation.test.ts

172 lines
9.2 KiB
TypeScript
Raw Permalink Normal View History

import assert from "node:assert/strict";
import test from "node:test";
import type { TransferTask } from "../../../domain/models";
import { createSftpTransferCenterStore } from "../sftpTransferCenterStore";
function child(): TransferTask {
return {
id: "child", parentTaskId: "root", directoryEntryIndex: 0, directoryEntryIdentity: "a".repeat(64),
sourcePath: "/source/a", targetPath: "/target/a", fileName: "a",
sourceConnectionId: "local", targetConnectionId: "remote", direction: "upload",
totalBytes: 1, transferredBytes: 0, speed: 0, startTime: 0, isDirectory: false, status: "transferring",
};
}
for (const status of ["completed", "failed", "cancelled"] as const) {
test(`settlement observation retains exact ${status} before history pruning`, () => {
const store = createSftpTransferCenterStore();
const task = child();
store.upsertTasks([{ ...task, id: "root", parentTaskId: undefined, isDirectory: true }, task]);
const observation = store.observeTaskSettlement(task);
store.patchTask(task.id, { status, error: status === "failed" ? "write failed" : undefined });
assert.equal(observation.read()?.status, status);
if (status === "completed") assert.equal(store.getTask(task.id), undefined);
observation.dispose();
assert.equal(observation.read(), undefined);
});
}
test("another file reusing an id and index is not completion evidence", () => {
const store = createSftpTransferCenterStore();
const task = child();
store.upsertTasks([task]);
const observation = store.observeTaskSettlement(task);
store.upsertTasks([{ ...task, directoryEntryIdentity: "b".repeat(64), status: "completed" }]);
assert.equal(observation.read(), undefined);
observation.dispose();
});
test("a disposed observer receives no later completion", () => {
const store = createSftpTransferCenterStore();
const task = child();
store.upsertTasks([task]);
const observation = store.observeTaskSettlement(task);
observation.dispose();
store.patchTask(task.id, { status: "completed" });
assert.equal(observation.read(), undefined);
});
test("explicit dispatch refreshes failed identity without overwriting checkpoints", () => {
const store = createSftpTransferCenterStore();
const task = { ...child(), status: "failed" as const, checkpointBytes: 7 };
store.upsertTasks([task]);
const next = { ...task, directoryEntryIdentity: "b".repeat(64), checkpointBytes: 0 };
assert.equal(store.admitTaskRun(next), "ready");
assert.equal(store.getTask(task.id)?.status, "transferring");
assert.equal(store.getTask(task.id)?.directoryEntryIdentity, next.directoryEntryIdentity);
assert.equal(store.getTask(task.id)?.checkpointBytes, 7);
});
for (const status of ["cancelled", "completed", "paused", "pausing"] as const) {
test(`dispatch cannot revive a retained ${status} row`, () => {
const store = createSftpTransferCenterStore();
const task = child();
store.upsertTasks([
{ ...task, id: "root", parentTaskId: undefined },
{ ...task, status },
]);
assert.equal(store.admitTaskRun(task), status === "pausing" ? "paused" : status);
assert.equal(store.getTask(task.id)?.status, status);
});
}
test("dispatch preserves active lifecycle guards and avoids replacing unchanged rows", () => {
const store = createSftpTransferCenterStore();
const task = { ...child(), lifecycleEpoch: 9 };
store.upsertTasks([task]);
const before = store.getTask(task.id);
assert.equal(store.admitTaskRun(task), "ready");
assert.equal(store.getTask(task.id), before);
assert.equal(store.admitTaskRun({ ...task, directoryEntryIdentity: "b".repeat(64) }), "ready");
assert.equal(store.getTask(task.id)?.lifecycleEpoch, 9);
});
test("dispatch rejects a later pause or cancellation before changing identity", async (t) => {
const { latchTransferPause, resetTransferPauseLatchesForTests } = await import("./transferPauseLatch");
const { markTransferCancelledTree, settleTransferCancelTree } = await import("./transferCancelLatch");
const store = createSftpTransferCenterStore();
const task = { ...child(), status: "failed" as const };
store.upsertTasks([task]);
t.after(() => { resetTransferPauseLatchesForTests(); settleTransferCancelTree("root", [task.id]); });
latchTransferPause("root");
assert.equal(store.admitTaskRun(task), "paused");
resetTransferPauseLatchesForTests();
markTransferCancelledTree("root", [task.id]);
assert.equal(store.admitTaskRun(task), "cancelled");
assert.equal(store.getTask(task.id)?.status, "failed");
});
for (const nextStatus of ["transferring", "completed", "cancelled"] as const) {
test(`dispatch waits through a later pause until ${nextStatus}`, async (t) => {
const { sftpTransferCenterStore: store } = await import("../sftpTransferCenterStore");
const { runTransferAndWaitForOwner } = await import("./waitForTransferOwner");
const task = { ...child(), id: `paused-admission-${nextStatus}`, parentTaskId: undefined };
store.upsertTasks([{ ...task, status: "paused" }]);
let starts = 0;
let abort = false;
const running = runTransferAndWaitForOwner(task, async () => { starts += 1; return {}; }, () => abort);
// Attach rejection handling before the cancellation event is delivered.
const settled = running.then(() => "completed", (error: Error) => error.message);
t.after(async () => { abort = true; await settled; store.dismiss(task.id); });
await new Promise((resolve) => setTimeout(resolve, 30));
assert.equal(starts, 0);
assert.equal(store.getTask(task.id)?.status, "paused");
if (nextStatus !== "cancelled") store.patchTask(task.id, { status: "transferring", lifecycleEpoch: 1 });
if (nextStatus === "cancelled") {
const { markTransferCancelledTree, settleTransferCancelTree } = await import("./transferCancelLatch");
markTransferCancelledTree(task.id, []);
t.after(() => settleTransferCancelTree(task.id, []));
} else store.patchTask(task.id, { status: nextStatus });
const outcome = await Promise.race([settled, new Promise((resolve) => setTimeout(() => resolve("still-waiting"), 1000))]);
assert.equal(outcome, nextStatus === "cancelled" ? "Transfer cancelled" : "completed");
assert.equal(starts, nextStatus === "transferring" ? 1 : 0);
});
}
test("completed dispatch consumes only exact identity and never restarts", async (t) => {
const { sftpTransferCenterStore: store } = await import("../sftpTransferCenterStore");
const { runTransferAndWaitForOwner } = await import("./waitForTransferOwner");
const task = { ...child(), id: "completed-admission", parentTaskId: undefined };
store.upsertTasks([{ ...task, status: "completed" }]);
t.after(() => store.dismiss(task.id));
const start = async () => { assert.fail("completed transfer must not restart"); };
await runTransferAndWaitForOwner(task, start, () => false);
await assert.rejects(runTransferAndWaitForOwner({ ...task, directoryEntryIdentity: "b".repeat(64) }, start, () => false), /identity changed/);
assert.equal(store.getTask(task.id)?.status, "completed");
});
test("a resumed retry does not inherit failed settlement captured while admission was paused", async (t) => {
const { sftpTransferCenterStore: store } = await import("../sftpTransferCenterStore");
const { runTransferAndWaitForOwner } = await import("./waitForTransferOwner");
const { latchTransferPause, resetTransferPauseLatchesForTests } = await import("./transferPauseLatch");
const task = { ...child(), id: "failed-paused-admission", parentTaskId: undefined };
store.upsertTasks([{ ...task, status: "failed", error: "previous attempt failed" }]);
latchTransferPause(task.id);
let abort = false;
const running = runTransferAndWaitForOwner(task, async () => {
store.patchTask(task.id, { status: "completed" });
return { superseded: true };
}, () => abort);
const settled = running.then(() => "completed", (error: Error) => error.message);
t.after(async () => { abort = true; resetTransferPauseLatchesForTests(); await settled; store.dismiss(task.id); });
// An unrelated lifecycle publication captures the previous failed row while waiting.
store.upsertTasks([{ ...child(), id: "unrelated-admission", parentTaskId: undefined }]);
t.after(() => store.dismiss("unrelated-admission"));
resetTransferPauseLatchesForTests();
assert.equal(await Promise.race([settled, new Promise((resolve) => setTimeout(() => resolve("still-waiting"), 1000))]), "completed");
});
test("fresh directory recovery authorizes only an unchanged retained pause under an active parent", () => {
const store = createSftpTransferCenterStore();
const task = child();
store.upsertTasks([{ ...task, id: "root", parentTaskId: undefined, isDirectory: true, status: "pending" }, { ...task, status: "paused", lifecycleEpoch: 4 }]);
const paused = store.getTask(task.id)!;
assert.equal(store.admitTaskRun(task), "paused", "ordinary live dispatch must respect cross-window pause");
assert.equal(store.admitTaskRun(task, paused), "ready");
store.patchTask(task.id, { status: "paused", lifecycleEpoch: 5 });
assert.equal(store.admitTaskRun(task, paused), "paused", "newer child pause invalidates fresh recovery permission");
const currentPause = store.getTask(task.id)!;
store.patchTask("root", { status: "paused", lifecycleEpoch: 6 });
assert.equal(store.admitTaskRun(task, currentPause), "paused", "parent pause still wins");
});