Files
NetMesh/application/state/sftp/pauseTransferOutcome.test.ts
zhaolei 3c72efcb7f
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
[Init] Initial commit - NetMesh terminal manager
2026-09-13 18:24:01 +08:00

175 lines
6.9 KiB
TypeScript

import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import test from "node:test";
import { createGlobalSftpTransferScheduler } from "./globalTransferScheduler.ts";
import {
allPauseResultsBenignOrSuccess,
allPauseResultsDeadTransfer,
isBenignPauseMiss,
isDeadTransferPauseMiss,
isHardPauseFailure,
planPartialPauseRollback,
resolveDirectoryPauseParentOutcome,
shouldLatchPauseWaiters,
} from "./pauseTransferOutcome.ts";
test("benign pause misses match store regex (no longer active / not found / session)", () => {
assert.equal(isBenignPauseMiss("Transfer is no longer active"), true);
assert.equal(isBenignPauseMiss("session not found"), true);
assert.equal(isBenignPauseMiss("SFTP session closed"), true);
assert.equal(isBenignPauseMiss("This transfer cannot be paused safely"), false);
assert.equal(isBenignPauseMiss("Pause unavailable"), false);
});
test("dead transfer pause misses are a strict subset of benign misses", () => {
assert.equal(isDeadTransferPauseMiss("Transfer is no longer active"), true);
assert.equal(isDeadTransferPauseMiss("session not found"), true);
assert.equal(isDeadTransferPauseMiss("SFTP session closed"), false);
assert.equal(allPauseResultsDeadTransfer([
{ success: false, reason: "Transfer is no longer active" },
]), true);
assert.equal(allPauseResultsDeadTransfer([
{ success: true },
{ success: false, reason: "Transfer is no longer active" },
]), false);
assert.equal(allPauseResultsDeadTransfer([
{ success: false, reason: "cannot be paused safely" },
]), false);
});
test("allPauseResultsBenignOrSuccess rejects mixed hard failures", () => {
assert.equal(allPauseResultsBenignOrSuccess([
{ success: true },
{ success: false, reason: "no longer active" },
]), true);
assert.equal(allPauseResultsBenignOrSuccess([
{ success: true },
{ success: false, reason: "cannot be paused safely" },
]), false);
assert.equal(allPauseResultsBenignOrSuccess([]), true);
});
test("directory parent stays paused even when some children hard-fail pause", () => {
// Latch-first: partial child failures must not unpause the folder parent.
assert.deepEqual(
resolveDirectoryPauseParentOutcome([
{ success: true },
{ success: false, reason: "cannot be paused safely" },
]),
{ kind: "paused", reason: "cannot be paused safely" },
);
assert.deepEqual(
resolveDirectoryPauseParentOutcome([
{ success: false, reason: "This transfer cannot be paused yet" },
]),
{ kind: "paused", reason: "This transfer cannot be paused yet" },
);
assert.deepEqual(
resolveDirectoryPauseParentOutcome([
{ success: false, reason: "Could not verify the saved transfer checkpoint" },
]),
{ kind: "paused", reason: "Could not verify the saved transfer checkpoint" },
);
assert.deepEqual(
resolveDirectoryPauseParentOutcome([
{ success: false, reason: "no longer active" },
{ success: true },
]),
{ kind: "paused" },
);
});
test("pause waiters latch only on successful pause", () => {
assert.equal(shouldLatchPauseWaiters({ pauseSucceeded: true }), true);
assert.equal(shouldLatchPauseWaiters({ pauseSucceeded: false }), false);
});
test("isHardPauseFailure treats missing result as hard", () => {
assert.equal(isHardPauseFailure(undefined), true);
assert.equal(isHardPauseFailure({ success: false, reason: "no longer active" }), false);
assert.equal(isHardPauseFailure({ success: true }), false);
});
test("planPartialPauseRollback unparks scheduler jobs and successful bridge pauses", () => {
// active: parent + 3 children. scheduler.pause succeeded for child-a (not in backendIds).
// bridge: child-b success, child-c hard fail, parent not in directory activeIds.
const plan = planPartialPauseRollback({
activeIds: ["child-a", "child-b", "child-c"],
backendIds: ["child-b", "child-c"],
bridgeResults: [
{ success: true },
{ success: false, reason: "cannot be paused safely" },
],
});
assert.deepEqual(plan.schedulerIdsToResume, ["child-a"]);
assert.deepEqual(plan.bridgeIdsToResume, ["child-b"]);
});
test("planPartialPauseRollback for single-file includes parent when only children hit bridge", () => {
// Single-file: activeIds = [parent, child]; scheduler parked parent, bridge paused child then hard-fails parent.
const plan = planPartialPauseRollback({
activeIds: ["parent", "child"],
backendIds: ["parent", "child"],
bridgeResults: [
{ success: false, reason: "cannot be paused safely" },
{ success: true },
],
});
assert.deepEqual(plan.schedulerIdsToResume, []);
assert.deepEqual(plan.bridgeIdsToResume, ["child"]);
});
test("mixed hard-fail rollback resumes real scheduler-parked jobs so work continues", async () => {
const scheduler = createGlobalSftpTransferScheduler();
let childAFinished = false;
const hold = { release: null as null | (() => void) };
const block = new Promise<void>((resolve) => {
hold.release = resolve;
});
// Concurrency 1: hold the only slot so child-a stays queued and can be pause()'d.
const holder = scheduler.run("owner", "holder", ["host-1"], () => 1, async () => {
await block;
});
const childA = scheduler.run("owner", "child-a", ["host-1"], () => 1, async () => {
childAFinished = true;
});
// Allow holder to become active and child-a to queue.
await new Promise((resolve) => setTimeout(resolve, 0));
assert.equal(scheduler.pause("child-a"), true, "child-a must be queued so pause parks it");
// Mimic panel path: activeIds include scheduler-parked + bridge targets; bridge hard-fails one.
const plan = planPartialPauseRollback({
activeIds: ["child-a", "child-b"],
backendIds: ["child-b"],
bridgeResults: [{ success: false, reason: "cannot be paused safely" }],
});
assert.deepEqual(plan.schedulerIdsToResume, ["child-a"]);
for (const id of plan.schedulerIdsToResume) {
assert.equal(scheduler.resume(id), true);
}
hold.release?.();
await holder;
await childA;
assert.equal(childAFinished, true, "parked child-a must run after rollback resume");
});
test("panel pause delegates to TransferRuntime; soft-control + rollback live in process-global control", () => {
// Dual soft-control path was removed: panel only calls transferRuntime.
const panelSource = readFileSync(new URL("./useSftpTransfers.ts", import.meta.url), "utf8");
assert.match(panelSource, /transferRuntime\.pause/);
assert.match(panelSource, /transferRuntime\.resume/);
assert.doesNotMatch(panelSource, /rollbackPartialPause/);
assert.doesNotMatch(panelSource, /resolveDirectoryPauseParentOutcome/);
// planPartialPauseRollback remains on the single soft-control plane.
const controlSource = readFileSync(new URL("./globalSftpTransferControl.ts", import.meta.url), "utf8");
assert.match(controlSource, /planPartialPauseRollback/);
assert.match(controlSource, /bridgeIdsToResume/);
assert.match(controlSource, /softPauseTransfer/);
assert.match(controlSource, /softResumeTransfer/);
});