@remit/imap-worker 0.0.71 → 0.0.73
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -2
- package/src/handlers/append-sent-message.ts +1 -1
- package/src/handlers/empty-trash.test.ts +118 -3
- package/src/handlers/empty-trash.ts +90 -31
- package/src/handlers/flag-push-placement-race.test.ts +18 -4
- package/src/handlers/flag-push.ts +1 -1
- package/src/handlers/mailbox-management.ts +1 -1
- package/src/handlers/message-copy.test.ts +56 -25
- package/src/handlers/message-copy.ts +43 -20
- package/src/handlers/message-delete-terminal.ts +11 -7
- package/src/handlers/message-delete.test.ts +225 -88
- package/src/handlers/message-delete.ts +92 -12
- package/src/handlers/message-move-terminal.test.ts +65 -20
- package/src/handlers/message-move-terminal.ts +10 -6
- package/src/handlers/message-move.test.ts +26 -17
- package/src/handlers/message-move.ts +27 -10
- package/src/handlers/placement-move-push.ts +2 -1
- package/src/handlers/sync-message-body.ts +2 -2
- package/src/handlers/sync-messages.ts +1 -1
- package/src/filter-config.test.ts +0 -171
- package/src/filter-config.ts +0 -63
- package/src/handlers/restore-source-placement.ts +0 -135
- package/src/handlers/thread-message-rows.ts +0 -77
- package/src/is-not-found.ts +0 -17
- package/src/message-embedder.test.ts +0 -39
- package/src/message-embedder.ts +0 -33
|
@@ -4,6 +4,10 @@ import type { ThreadMessageItem } from "@remit/data-ports";
|
|
|
4
4
|
import type { Logger } from "@remit/logger-lambda";
|
|
5
5
|
import { renderMetrics, resetMetrics } from "@remit/logger-lambda";
|
|
6
6
|
import { noopLogger } from "@remit/logger-lambda/noop-logger";
|
|
7
|
+
import {
|
|
8
|
+
buildThreadMessageTrashUpdate,
|
|
9
|
+
buildThreadMessageUndelete,
|
|
10
|
+
} from "@remit/mailbox-service";
|
|
7
11
|
import type { MessageDeleteEvent } from "../events.js";
|
|
8
12
|
import {
|
|
9
13
|
deleteAllThreadMessagesForMessage,
|
|
@@ -12,10 +16,6 @@ import {
|
|
|
12
16
|
MESSAGE_DELETE_MAX_ATTEMPTS,
|
|
13
17
|
type MessageDeleteDeps,
|
|
14
18
|
} from "./message-delete.js";
|
|
15
|
-
import {
|
|
16
|
-
buildThreadMessageTrashUpdate,
|
|
17
|
-
buildThreadMessageUndelete,
|
|
18
|
-
} from "./thread-message-rows.js";
|
|
19
19
|
|
|
20
20
|
describe("getMessageDeleteMaxAttempts — env-derived threshold (#980)", () => {
|
|
21
21
|
it("parses the injected env var", () => {
|
|
@@ -293,7 +293,9 @@ interface Harness {
|
|
|
293
293
|
mailboxError?: Error;
|
|
294
294
|
connection: Connection;
|
|
295
295
|
threadMessageUpdateError?: Error;
|
|
296
|
-
messageRow: { messageIdHeader?: string } | undefined;
|
|
296
|
+
messageRow: { messageIdHeader?: string; status?: string } | undefined;
|
|
297
|
+
/** The row was settled by another lane, so every transition loses. */
|
|
298
|
+
transitionLost?: boolean;
|
|
297
299
|
destinationSearchUids: number[];
|
|
298
300
|
// A Message-ID SEARCH is answered by whichever folder was opened last, so a
|
|
299
301
|
// case can put the same header in both. Default: the source still holds it,
|
|
@@ -396,7 +398,10 @@ const fresh = (): Harness => ({
|
|
|
396
398
|
calls: [],
|
|
397
399
|
account: { accountId: "acc-1", accountConfigId: "cfg-1" },
|
|
398
400
|
mailbox: { mailboxId: "src-mbx", uidValidity: 1, cursorState: undefined },
|
|
399
|
-
|
|
401
|
+
// What the API writes before it enqueues: a move to Trash records `moving`,
|
|
402
|
+
// a permanent delete records `deleting`, and the handler's guard asks one
|
|
403
|
+
// question of both — is a mutation still outstanding on this row.
|
|
404
|
+
messageRow: { messageIdHeader: MESSAGE_ID_HEADER, status: "moving" },
|
|
400
405
|
destinationSearchUids: [],
|
|
401
406
|
sourceSearchUids: [10],
|
|
402
407
|
openBoxPath: "INBOX",
|
|
@@ -431,7 +436,10 @@ const deps = (): MessageDeleteDeps =>
|
|
|
431
436
|
return h.messageRow ? [h.messageRow] : [];
|
|
432
437
|
},
|
|
433
438
|
updateUid: record("message.updateUid"),
|
|
434
|
-
|
|
439
|
+
transitionPlacement: async (...args: unknown[]) => {
|
|
440
|
+
h.calls.push({ method: "message.transitionPlacement", args });
|
|
441
|
+
return h.transitionLost ? undefined : { messageId: "msg-1" };
|
|
442
|
+
},
|
|
435
443
|
update: record("message.update"),
|
|
436
444
|
delete: record("message.delete"),
|
|
437
445
|
},
|
|
@@ -502,17 +510,60 @@ const permanentEvent: MessageDeleteEvent = {
|
|
|
502
510
|
operation: "permanent_delete",
|
|
503
511
|
} as MessageDeleteEvent;
|
|
504
512
|
|
|
513
|
+
/**
|
|
514
|
+
* A permanent delete's row carries `deleting`, which is the state its own guard
|
|
515
|
+
* asks for — the move to Trash's is `moving`, and the default fixture is that.
|
|
516
|
+
* Called at the point of the handler call so the row is right for the event.
|
|
517
|
+
*/
|
|
518
|
+
const permanent = (): MessageDeleteEvent => {
|
|
519
|
+
if (h.messageRow) h.messageRow.status = "deleting";
|
|
520
|
+
return permanentEvent;
|
|
521
|
+
};
|
|
522
|
+
|
|
505
523
|
const called = (method: string): Call[] =>
|
|
506
524
|
h.calls.filter((c) => c.method === method);
|
|
507
525
|
|
|
526
|
+
/**
|
|
527
|
+
* The transient attempt marker: the one transition that writes nothing but the
|
|
528
|
+
* attempt's own sync status, predicated on this delete still being outstanding.
|
|
529
|
+
* Told apart from the hand-back transitions, which write a whole placement.
|
|
530
|
+
*/
|
|
531
|
+
/**
|
|
532
|
+
* The hand-back transitions: the ones that write a whole placement back onto
|
|
533
|
+
* the row. Told apart from the attempt marker, which writes only a sync status,
|
|
534
|
+
* and from the claim that precedes a removal, which writes only a status.
|
|
535
|
+
*/
|
|
536
|
+
const restoreCalls = (): Call[] =>
|
|
537
|
+
called("message.transitionPlacement").filter(
|
|
538
|
+
(candidate) =>
|
|
539
|
+
(candidate.args[2] as { mailboxId?: string }).mailboxId !== undefined,
|
|
540
|
+
);
|
|
541
|
+
|
|
542
|
+
const attemptMarkerSyncStatus = (): string | undefined => {
|
|
543
|
+
const call = called("message.transitionPlacement").find(
|
|
544
|
+
(candidate) => Object.keys(candidate.args[2] as object).length === 1,
|
|
545
|
+
);
|
|
546
|
+
return (call?.args[2] as { syncStatus?: string })?.syncStatus;
|
|
547
|
+
};
|
|
548
|
+
|
|
508
549
|
// The one write that puts a given-up delete back on its source pair. It clears
|
|
509
550
|
// `moving` in the same write as the pair, which is what keeps a later delete
|
|
510
|
-
// from waiting on a mutation that has terminated (#1005), and it
|
|
511
|
-
//
|
|
512
|
-
//
|
|
551
|
+
// from waiting on a mutation that has terminated (#1005), and it is a
|
|
552
|
+
// transition rather than a plain update so a row another lane already settled
|
|
553
|
+
// keeps its own placement (imap-mutations R3).
|
|
513
554
|
const restoredToSource = (syncStatus: string): unknown[] => [
|
|
514
555
|
"msg-1",
|
|
515
|
-
{
|
|
556
|
+
{ status: ["moving", "deleting"] },
|
|
557
|
+
{
|
|
558
|
+
mailboxId: "src-mbx",
|
|
559
|
+
uid: 10,
|
|
560
|
+
status: "active",
|
|
561
|
+
syncStatus,
|
|
562
|
+
// A give-up names the mutation it gave up on, so the reading pane can
|
|
563
|
+
// offer the way out for THAT one (#1229). `none` rides along with every
|
|
564
|
+
// other sync status, so the field never outlives its gate.
|
|
565
|
+
abandonedMutation: syncStatus === "abandoned" ? "delete" : "none",
|
|
566
|
+
},
|
|
516
567
|
];
|
|
517
568
|
|
|
518
569
|
// Label order in the rendered text is prom-client's, not ours.
|
|
@@ -661,11 +712,7 @@ describe("handleMessageDelete", () => {
|
|
|
661
712
|
0,
|
|
662
713
|
"an unconfirmed move must never revert the listing row to the source",
|
|
663
714
|
);
|
|
664
|
-
assert.equal(
|
|
665
|
-
(called("message.update")[0]?.args[1] as { syncStatus?: string })
|
|
666
|
-
?.syncStatus,
|
|
667
|
-
"failed",
|
|
668
|
-
);
|
|
715
|
+
assert.equal(attemptMarkerSyncStatus(), "failed");
|
|
669
716
|
assert.equal(h.disconnectCount, 1);
|
|
670
717
|
});
|
|
671
718
|
|
|
@@ -699,7 +746,7 @@ describe("handleMessageDelete", () => {
|
|
|
699
746
|
it("settles on the first attempt when the row carries no Message-ID header", async () => {
|
|
700
747
|
h.connection.moveMessages = async () => ({ uidMap: new Map() });
|
|
701
748
|
sourceNoLongerHoldsTheUid();
|
|
702
|
-
h.messageRow = {};
|
|
749
|
+
h.messageRow = { status: "moving" };
|
|
703
750
|
|
|
704
751
|
await handleMessageDelete(moveEvent, noopLogger, 1, deps());
|
|
705
752
|
|
|
@@ -738,6 +785,79 @@ describe("handleMessageDelete", () => {
|
|
|
738
785
|
assert.equal(called("emitEvent").length, 0);
|
|
739
786
|
});
|
|
740
787
|
|
|
788
|
+
// A lost SQS acknowledgement is the ordinary way this happens: the delete
|
|
789
|
+
// ran, `settleTrashMoveConfirmed` wrote the Trash uid, and the record came
|
|
790
|
+
// back anyway. Without the guard its three siblings carry (MESSAGE_MOVE,
|
|
791
|
+
// MESSAGE_COPY, FLAG_PUSH) the redelivery MOVEs a uid the source no longer
|
|
792
|
+
// holds, exhausts, and the terminal resolver reads the source's honest
|
|
793
|
+
// "gone" as grounds to delete rows that are correct — taking spamReport,
|
|
794
|
+
// classificationState, category and the Undo target's `originalMailboxId`
|
|
795
|
+
// with them, none of which the resync re-projection can rebuild.
|
|
796
|
+
it("skips a redelivery of a delete that already settled, rather than re-running it", async () => {
|
|
797
|
+
h.messageRow = { messageIdHeader: MESSAGE_ID_HEADER, status: "active" };
|
|
798
|
+
|
|
799
|
+
await handleMessageDelete(moveEvent, noopLogger, 1, deps());
|
|
800
|
+
|
|
801
|
+
assert.equal(h.getConnectionCount, 0, "no IMAP command is issued");
|
|
802
|
+
assert.equal(called("message.updateUid").length, 0);
|
|
803
|
+
assert.equal(called("message.delete").length, 0);
|
|
804
|
+
assert.equal(called("threadMessage.delete").length, 0);
|
|
805
|
+
assert.equal(called("threadMessage.deleteMany").length, 0);
|
|
806
|
+
assert.equal(called("message.transitionPlacement").length, 0);
|
|
807
|
+
assert.equal(called("emitEvent").length, 0);
|
|
808
|
+
});
|
|
809
|
+
|
|
810
|
+
// The guard asks for the state THIS delete wrote, not for "is anything
|
|
811
|
+
// outstanding". A move to Trash records `moving`, a permanent delete
|
|
812
|
+
// records `deleting`, and a row in the other one belongs to a different
|
|
813
|
+
// mutation — reading it as unsettled is what would let a redelivery
|
|
814
|
+
// finish somebody else's work against a uid it never owned.
|
|
815
|
+
it("proceeds on a move-to-trash row still marked moving, which is its own work", async () => {
|
|
816
|
+
h.messageRow = { messageIdHeader: MESSAGE_ID_HEADER, status: "moving" };
|
|
817
|
+
|
|
818
|
+
await handleMessageDelete(moveEvent, noopLogger, 1, deps());
|
|
819
|
+
|
|
820
|
+
assert.equal(h.getConnectionCount, 1, "the delete runs");
|
|
821
|
+
});
|
|
822
|
+
|
|
823
|
+
it("skips a move-to-trash whose row a permanent delete has claimed", async () => {
|
|
824
|
+
h.messageRow = { messageIdHeader: MESSAGE_ID_HEADER, status: "deleting" };
|
|
825
|
+
|
|
826
|
+
await handleMessageDelete(moveEvent, noopLogger, 1, deps());
|
|
827
|
+
|
|
828
|
+
assert.equal(h.getConnectionCount, 0, "no IMAP command is issued");
|
|
829
|
+
assert.equal(called("message.updateUid").length, 0);
|
|
830
|
+
assert.equal(called("emitEvent").length, 0);
|
|
831
|
+
});
|
|
832
|
+
|
|
833
|
+
it("proceeds on a permanent delete's own deleting row", async () => {
|
|
834
|
+
h.messageRow = { messageIdHeader: MESSAGE_ID_HEADER, status: "deleting" };
|
|
835
|
+
|
|
836
|
+
await handleMessageDelete(permanent(), noopLogger, 1, deps());
|
|
837
|
+
|
|
838
|
+
assert.equal(h.getConnectionCount, 1, "the expunge runs");
|
|
839
|
+
});
|
|
840
|
+
|
|
841
|
+
it("skips a permanent delete whose row a move has claimed", async () => {
|
|
842
|
+
h.messageRow = { messageIdHeader: MESSAGE_ID_HEADER, status: "moving" };
|
|
843
|
+
|
|
844
|
+
await handleMessageDelete(permanentEvent, noopLogger, 1, deps());
|
|
845
|
+
|
|
846
|
+
assert.equal(h.getConnectionCount, 0, "no expunge is issued");
|
|
847
|
+
assert.equal(called("message.delete").length, 0);
|
|
848
|
+
assert.equal(called("emitEvent").length, 0);
|
|
849
|
+
});
|
|
850
|
+
|
|
851
|
+
it("skips a redelivered permanent delete the same way", async () => {
|
|
852
|
+
h.messageRow = { messageIdHeader: MESSAGE_ID_HEADER, status: "active" };
|
|
853
|
+
|
|
854
|
+
await handleMessageDelete(permanentEvent, noopLogger, 1, deps());
|
|
855
|
+
|
|
856
|
+
assert.equal(h.getConnectionCount, 0);
|
|
857
|
+
assert.equal(called("message.delete").length, 0);
|
|
858
|
+
assert.equal(called("emitEvent").length, 0);
|
|
859
|
+
});
|
|
860
|
+
|
|
741
861
|
// One Message-ID can have several server copies in one account while
|
|
742
862
|
// `deriveMessageId` gives them one local row. A source that still holds
|
|
743
863
|
// the uid proves the MOVE did not happen, so any hit at the destination
|
|
@@ -759,16 +879,12 @@ describe("handleMessageDelete", () => {
|
|
|
759
879
|
);
|
|
760
880
|
assert.equal(called("threadMessage.update").length, 0);
|
|
761
881
|
assert.equal(called("message.delete").length, 0);
|
|
762
|
-
assert.equal(
|
|
763
|
-
(called("message.update")[0]?.args[1] as { syncStatus?: string })
|
|
764
|
-
?.syncStatus,
|
|
765
|
-
"failed",
|
|
766
|
-
);
|
|
882
|
+
assert.equal(attemptMarkerSyncStatus(), "failed");
|
|
767
883
|
});
|
|
768
884
|
|
|
769
885
|
// Issue #1098. A source that still holds the uid at the ceiling means the
|
|
770
886
|
// MOVE never took effect, so the row goes back to the source folder: the
|
|
771
|
-
// optimistic
|
|
887
|
+
// optimistic transition left `mailboxId` on Trash while `uid` still
|
|
772
888
|
// named the source's message, and settling `status` to `active` on top of
|
|
773
889
|
// that pair switched off every guard that reads `status === "moving"`.
|
|
774
890
|
// The UI then resolved a Trash `mailboxId` to "Delete permanently", an
|
|
@@ -795,10 +911,12 @@ describe("handleMessageDelete", () => {
|
|
|
795
911
|
|
|
796
912
|
// `mailboxId` and `uid` name the same message again, and the same write
|
|
797
913
|
// settles `status` out of `moving`, so no later delete of this message
|
|
798
|
-
// waits on a mutation that has terminated.
|
|
914
|
+
// waits on a mutation that has terminated. It also carries the give-up
|
|
915
|
+
// (#1153) and names it a delete (#1229): the user asked for one, it did
|
|
916
|
+
// not happen, and nothing is coming to try again.
|
|
799
917
|
assert.deepEqual(
|
|
800
|
-
called("message.
|
|
801
|
-
restoredToSource("
|
|
918
|
+
called("message.transitionPlacement").at(-1)?.args,
|
|
919
|
+
restoredToSource("abandoned"),
|
|
802
920
|
);
|
|
803
921
|
assert.equal(
|
|
804
922
|
called("message.update").length,
|
|
@@ -827,7 +945,7 @@ describe("handleMessageDelete", () => {
|
|
|
827
945
|
assert.equal(await imapFailures("MESSAGE_DELETE_EXHAUSTED"), 1);
|
|
828
946
|
});
|
|
829
947
|
|
|
830
|
-
// The optimistic
|
|
948
|
+
// The optimistic transition already pointed the row at Trash. The
|
|
831
949
|
// server has now confirmed the message never left the source, so the row
|
|
832
950
|
// and the server disagree about where the mail is, and only a resync of
|
|
833
951
|
// both folders settles that. Without it the user is shown the message in
|
|
@@ -871,11 +989,7 @@ describe("handleMessageDelete", () => {
|
|
|
871
989
|
|
|
872
990
|
assert.equal(called("message.delete").length, 0);
|
|
873
991
|
assert.equal(called("threadMessage.deleteMany").length, 0);
|
|
874
|
-
assert.equal(
|
|
875
|
-
(called("message.update")[0]?.args[1] as { syncStatus?: string })
|
|
876
|
-
?.syncStatus,
|
|
877
|
-
"failed",
|
|
878
|
-
);
|
|
992
|
+
assert.equal(attemptMarkerSyncStatus(), "failed");
|
|
879
993
|
});
|
|
880
994
|
|
|
881
995
|
it("settles at the ceiling instead of dead-lettering undiagnosed", async () => {
|
|
@@ -899,8 +1013,8 @@ describe("handleMessageDelete", () => {
|
|
|
899
1013
|
await handleMessageDelete(moveEvent, noopLogger, 3, deps());
|
|
900
1014
|
|
|
901
1015
|
assert.deepEqual(
|
|
902
|
-
called("message.
|
|
903
|
-
restoredToSource("
|
|
1016
|
+
called("message.transitionPlacement").at(-1)?.args,
|
|
1017
|
+
restoredToSource("abandoned"),
|
|
904
1018
|
);
|
|
905
1019
|
assert.equal(await imapFailures("MESSAGE_DELETE_EXHAUSTED"), 1);
|
|
906
1020
|
});
|
|
@@ -926,11 +1040,7 @@ describe("handleMessageDelete", () => {
|
|
|
926
1040
|
assert.equal(called("message.delete").length, 0);
|
|
927
1041
|
assert.equal(called("threadMessage.deleteMany").length, 0);
|
|
928
1042
|
assert.equal(called("threadMessage.update").length, 0);
|
|
929
|
-
assert.equal(
|
|
930
|
-
(called("message.update")[0]?.args[1] as { syncStatus?: string })
|
|
931
|
-
?.syncStatus,
|
|
932
|
-
"failed",
|
|
933
|
-
);
|
|
1043
|
+
assert.equal(attemptMarkerSyncStatus(), "failed");
|
|
934
1044
|
assert.equal(h.disconnectCount, 1);
|
|
935
1045
|
});
|
|
936
1046
|
});
|
|
@@ -964,8 +1074,8 @@ describe("handleMessageDelete", () => {
|
|
|
964
1074
|
// invisible syncStatus on a row that claims Trash. A refusal keeps
|
|
965
1075
|
// `failed`: the product turned down something the user asked for.
|
|
966
1076
|
assert.deepEqual(
|
|
967
|
-
called("message.
|
|
968
|
-
restoredToSource("
|
|
1077
|
+
called("message.transitionPlacement")[0]?.args,
|
|
1078
|
+
restoredToSource("abandoned"),
|
|
969
1079
|
);
|
|
970
1080
|
assert.deepEqual(called("threadMessage.update")[0]?.args[2], {
|
|
971
1081
|
uid: 10,
|
|
@@ -987,13 +1097,13 @@ describe("handleMessageDelete", () => {
|
|
|
987
1097
|
assert.equal(called("connection.deleteMessages").length, 0);
|
|
988
1098
|
assert.equal(called("message.delete").length, 0);
|
|
989
1099
|
assert.deepEqual(
|
|
990
|
-
called("message.
|
|
991
|
-
restoredToSource("
|
|
1100
|
+
called("message.transitionPlacement")[0]?.args,
|
|
1101
|
+
restoredToSource("abandoned"),
|
|
992
1102
|
);
|
|
993
1103
|
});
|
|
994
1104
|
|
|
995
1105
|
it("expunges on the server and removes every thread row before the message row", async () => {
|
|
996
|
-
await handleMessageDelete(
|
|
1106
|
+
await handleMessageDelete(permanent(), noopLogger, 1, deps());
|
|
997
1107
|
|
|
998
1108
|
assert.deepEqual(called("connection.deleteMessages")[0]?.args, [[10]]);
|
|
999
1109
|
assert.equal(called("threadMessage.delete").length, 2);
|
|
@@ -1012,7 +1122,7 @@ describe("handleMessageDelete", () => {
|
|
|
1012
1122
|
};
|
|
1013
1123
|
sourceNoLongerHoldsTheUid();
|
|
1014
1124
|
|
|
1015
|
-
await handleMessageDelete(
|
|
1125
|
+
await handleMessageDelete(permanent(), noopLogger, 1, deps());
|
|
1016
1126
|
|
|
1017
1127
|
assert.equal(called("message.delete").length, 1);
|
|
1018
1128
|
assert.equal(called("threadMessage.delete").length, 2);
|
|
@@ -1048,11 +1158,7 @@ describe("handleMessageDelete", () => {
|
|
|
1048
1158
|
0,
|
|
1049
1159
|
"a move-to-trash never probes: absence from the source is its success signature",
|
|
1050
1160
|
);
|
|
1051
|
-
assert.equal(
|
|
1052
|
-
(called("message.update")[0]?.args[1] as { syncStatus?: string })
|
|
1053
|
-
?.syncStatus,
|
|
1054
|
-
"failed",
|
|
1055
|
-
);
|
|
1161
|
+
assert.equal(attemptMarkerSyncStatus(), "failed");
|
|
1056
1162
|
});
|
|
1057
1163
|
|
|
1058
1164
|
it("keeps the rows on a NONEXISTENT move-to-trash whose source still holds the uid", async () => {
|
|
@@ -1067,11 +1173,7 @@ describe("handleMessageDelete", () => {
|
|
|
1067
1173
|
|
|
1068
1174
|
assert.equal(called("message.delete").length, 0);
|
|
1069
1175
|
assert.equal(called("threadMessage.delete").length, 0);
|
|
1070
|
-
assert.equal(
|
|
1071
|
-
(called("message.update")[0]?.args[1] as { syncStatus?: string })
|
|
1072
|
-
?.syncStatus,
|
|
1073
|
-
"failed",
|
|
1074
|
-
);
|
|
1176
|
+
assert.equal(attemptMarkerSyncStatus(), "failed");
|
|
1075
1177
|
});
|
|
1076
1178
|
|
|
1077
1179
|
it("keeps the rows when a permanent delete's source still holds the uid", async () => {
|
|
@@ -1083,7 +1185,7 @@ describe("handleMessageDelete", () => {
|
|
|
1083
1185
|
h.connection.fetchMessages = async () => [];
|
|
1084
1186
|
|
|
1085
1187
|
await assert.rejects(
|
|
1086
|
-
handleMessageDelete(
|
|
1188
|
+
handleMessageDelete(permanent(), noopLogger, 1, deps()),
|
|
1087
1189
|
/NONEXISTENT/,
|
|
1088
1190
|
);
|
|
1089
1191
|
|
|
@@ -1108,17 +1210,13 @@ describe("handleMessageDelete", () => {
|
|
|
1108
1210
|
};
|
|
1109
1211
|
|
|
1110
1212
|
await assert.rejects(
|
|
1111
|
-
handleMessageDelete(
|
|
1213
|
+
handleMessageDelete(permanent(), noopLogger, 1, deps()),
|
|
1112
1214
|
/NONEXISTENT mailbox does not exist/,
|
|
1113
1215
|
);
|
|
1114
1216
|
|
|
1115
1217
|
assert.equal(called("message.delete").length, 0);
|
|
1116
1218
|
assert.equal(called("threadMessage.delete").length, 0);
|
|
1117
|
-
assert.equal(
|
|
1118
|
-
(called("message.update")[0]?.args[1] as { syncStatus?: string })
|
|
1119
|
-
?.syncStatus,
|
|
1120
|
-
"failed",
|
|
1121
|
-
);
|
|
1219
|
+
assert.equal(attemptMarkerSyncStatus(), "failed");
|
|
1122
1220
|
});
|
|
1123
1221
|
|
|
1124
1222
|
it("abandons rather than creating the destination on TRYCREATE", async () => {
|
|
@@ -1133,8 +1231,8 @@ describe("handleMessageDelete", () => {
|
|
|
1133
1231
|
|
|
1134
1232
|
assert.equal(called("connection.createMailbox").length, 0);
|
|
1135
1233
|
assert.deepEqual(
|
|
1136
|
-
called("message.
|
|
1137
|
-
restoredToSource("
|
|
1234
|
+
called("message.transitionPlacement")[0]?.args,
|
|
1235
|
+
restoredToSource("abandoned"),
|
|
1138
1236
|
);
|
|
1139
1237
|
assert.deepEqual(called("threadMessage.update")[0]?.args[2], {
|
|
1140
1238
|
uid: 10,
|
|
@@ -1143,6 +1241,49 @@ describe("handleMessageDelete", () => {
|
|
|
1143
1241
|
});
|
|
1144
1242
|
});
|
|
1145
1243
|
|
|
1244
|
+
// Removing the Message row is destructive, and the refusal paths reach it on
|
|
1245
|
+
// whatever the row happens to be — an unrecognised operation is answered on
|
|
1246
|
+
// an `active` row as readily as on a `deleting` one. So the removal is
|
|
1247
|
+
// claimed first, and a row with no delete outstanding is left alone.
|
|
1248
|
+
it("never removes a row no delete is outstanding on", async () => {
|
|
1249
|
+
h.messageRow = { messageIdHeader: MESSAGE_ID_HEADER, status: "active" };
|
|
1250
|
+
h.allThreadMessages = [];
|
|
1251
|
+
h.transitionLost = true;
|
|
1252
|
+
const malformed = {
|
|
1253
|
+
...moveEvent,
|
|
1254
|
+
operation: undefined,
|
|
1255
|
+
} as unknown as MessageDeleteEvent;
|
|
1256
|
+
|
|
1257
|
+
await handleMessageDelete(malformed, noopLogger, 1, deps());
|
|
1258
|
+
|
|
1259
|
+
assert.deepEqual(
|
|
1260
|
+
called("message.transitionPlacement")[0]?.args[1],
|
|
1261
|
+
{ status: ["moving", "deleting"] },
|
|
1262
|
+
"the removal is claimed off a placement this delete owns",
|
|
1263
|
+
);
|
|
1264
|
+
assert.equal(
|
|
1265
|
+
called("message.delete").length,
|
|
1266
|
+
0,
|
|
1267
|
+
"and a lost claim removes nothing",
|
|
1268
|
+
);
|
|
1269
|
+
});
|
|
1270
|
+
|
|
1271
|
+
it("removes the row once the claim on it wins", async () => {
|
|
1272
|
+
h.messageRow = { messageIdHeader: MESSAGE_ID_HEADER, status: "deleting" };
|
|
1273
|
+
h.allThreadMessages = [];
|
|
1274
|
+
const malformed = {
|
|
1275
|
+
...moveEvent,
|
|
1276
|
+
operation: undefined,
|
|
1277
|
+
} as unknown as MessageDeleteEvent;
|
|
1278
|
+
|
|
1279
|
+
await handleMessageDelete(malformed, noopLogger, 1, deps());
|
|
1280
|
+
|
|
1281
|
+
assert.deepEqual(
|
|
1282
|
+
called("message.delete").map((c) => c.args[0]),
|
|
1283
|
+
["msg-1"],
|
|
1284
|
+
);
|
|
1285
|
+
});
|
|
1286
|
+
|
|
1146
1287
|
it("abandons an event minted under an unknown contract, before connecting", async () => {
|
|
1147
1288
|
const unversioned = {
|
|
1148
1289
|
...moveEvent,
|
|
@@ -1154,8 +1295,8 @@ describe("handleMessageDelete", () => {
|
|
|
1154
1295
|
assert.equal(h.getConnectionCount, 0);
|
|
1155
1296
|
assert.equal(called("connection.deleteMessages").length, 0);
|
|
1156
1297
|
assert.deepEqual(
|
|
1157
|
-
called("message.
|
|
1158
|
-
restoredToSource("
|
|
1298
|
+
called("message.transitionPlacement")[0]?.args,
|
|
1299
|
+
restoredToSource("abandoned"),
|
|
1159
1300
|
);
|
|
1160
1301
|
assert.deepEqual(called("threadMessage.update")[0]?.args[2], {
|
|
1161
1302
|
uid: 10,
|
|
@@ -1185,7 +1326,7 @@ describe("handleMessageDelete", () => {
|
|
|
1185
1326
|
// and a full sync brings it back.
|
|
1186
1327
|
h.allThreadMessages = [];
|
|
1187
1328
|
const unversioned = {
|
|
1188
|
-
...
|
|
1329
|
+
...permanent(),
|
|
1189
1330
|
schemaVersion: undefined,
|
|
1190
1331
|
} as unknown as MessageDeleteEvent;
|
|
1191
1332
|
|
|
@@ -1210,11 +1351,7 @@ describe("handleMessageDelete", () => {
|
|
|
1210
1351
|
/server exploded/,
|
|
1211
1352
|
);
|
|
1212
1353
|
|
|
1213
|
-
assert.equal(
|
|
1214
|
-
(called("message.update")[0]?.args[1] as { syncStatus?: string })
|
|
1215
|
-
?.syncStatus,
|
|
1216
|
-
"failed",
|
|
1217
|
-
);
|
|
1354
|
+
assert.equal(attemptMarkerSyncStatus(), "failed");
|
|
1218
1355
|
});
|
|
1219
1356
|
|
|
1220
1357
|
// Issue #1203. The pause is thrown by the openBox guard before the MOVE is
|
|
@@ -1233,7 +1370,7 @@ describe("handleMessageDelete", () => {
|
|
|
1233
1370
|
"cursor_invalid",
|
|
1234
1371
|
);
|
|
1235
1372
|
assert.deepEqual(
|
|
1236
|
-
called("message.
|
|
1373
|
+
called("message.transitionPlacement")[0]?.args,
|
|
1237
1374
|
restoredToSource("synced"),
|
|
1238
1375
|
"the row goes back to the source pair, which is the set the rebuild adjudicates",
|
|
1239
1376
|
);
|
|
@@ -1288,7 +1425,7 @@ describe("handleMessageDelete", () => {
|
|
|
1288
1425
|
// the server has just confirmed on the axis a UIDVALIDITY change leaves
|
|
1289
1426
|
// intact.
|
|
1290
1427
|
assert.deepEqual(
|
|
1291
|
-
called("message.
|
|
1428
|
+
called("message.transitionPlacement")[0]?.args,
|
|
1292
1429
|
restoredToSource("synced"),
|
|
1293
1430
|
);
|
|
1294
1431
|
});
|
|
@@ -1320,8 +1457,8 @@ describe("handleMessageDelete", () => {
|
|
|
1320
1457
|
// The source pair is the set its own cursor rebuild walks and adjudicates
|
|
1321
1458
|
// by Message-ID, so handing the row back there is the reconcile path.
|
|
1322
1459
|
assert.deepEqual(
|
|
1323
|
-
called("message.
|
|
1324
|
-
restoredToSource("
|
|
1460
|
+
called("message.transitionPlacement")[0]?.args,
|
|
1461
|
+
restoredToSource("abandoned"),
|
|
1325
1462
|
);
|
|
1326
1463
|
assert.equal(
|
|
1327
1464
|
loggedWith("alert", "message_delete_paused_placement_unproven").length,
|
|
@@ -1339,7 +1476,7 @@ describe("handleMessageDelete", () => {
|
|
|
1339
1476
|
it("settles a paused delete broken when the MOVE was issued and neither folder can be asked", async () => {
|
|
1340
1477
|
h.connection.moveMessages = async () => ({ uidMap: new Map() });
|
|
1341
1478
|
sourceNoLongerHoldsTheUid();
|
|
1342
|
-
h.messageRow = {};
|
|
1479
|
+
h.messageRow = { status: "moving" };
|
|
1343
1480
|
let opened = 0;
|
|
1344
1481
|
h.connection.openBox = async (path: string) => {
|
|
1345
1482
|
h.openBoxPath = path;
|
|
@@ -1353,8 +1490,8 @@ describe("handleMessageDelete", () => {
|
|
|
1353
1490
|
await handleMessageDelete(moveEvent, capturingLog(), 1, deps());
|
|
1354
1491
|
|
|
1355
1492
|
assert.deepEqual(
|
|
1356
|
-
called("message.
|
|
1357
|
-
restoredToSource("
|
|
1493
|
+
called("message.transitionPlacement")[0]?.args,
|
|
1494
|
+
restoredToSource("abandoned"),
|
|
1358
1495
|
"the first attempt reaches this settle with the MOVE already issued, so `synced` claims what nothing confirmed",
|
|
1359
1496
|
);
|
|
1360
1497
|
assert.equal(
|
|
@@ -1379,7 +1516,7 @@ describe("handleMessageDelete", () => {
|
|
|
1379
1516
|
assert.equal(dropped.length, 1);
|
|
1380
1517
|
assert.equal(dropped[0]?.level, "warn");
|
|
1381
1518
|
assert.deepEqual(
|
|
1382
|
-
called("message.
|
|
1519
|
+
called("message.transitionPlacement")[0]?.args,
|
|
1383
1520
|
restoredToSource("synced"),
|
|
1384
1521
|
);
|
|
1385
1522
|
});
|
|
@@ -1412,7 +1549,7 @@ describe("handleMessageDelete", () => {
|
|
|
1412
1549
|
};
|
|
1413
1550
|
h.allThreadMessages = [];
|
|
1414
1551
|
|
|
1415
|
-
await handleMessageDelete(
|
|
1552
|
+
await handleMessageDelete(permanent(), capturingLog(), 1, deps());
|
|
1416
1553
|
|
|
1417
1554
|
assert.deepEqual(
|
|
1418
1555
|
called("emitEvent").map((c) => c.args[0]),
|
|
@@ -1432,7 +1569,7 @@ describe("handleMessageDelete", () => {
|
|
|
1432
1569
|
};
|
|
1433
1570
|
h.allThreadMessages = [];
|
|
1434
1571
|
|
|
1435
|
-
await handleMessageDelete(
|
|
1572
|
+
await handleMessageDelete(permanent(), capturingLog(), 1, deps());
|
|
1436
1573
|
|
|
1437
1574
|
assert.deepEqual(
|
|
1438
1575
|
h.logs.filter((line) => line.level === "error"),
|
|
@@ -1453,7 +1590,7 @@ describe("handleMessageDelete", () => {
|
|
|
1453
1590
|
it("keeps alerting when a refused delete removed the last listing rows", async () => {
|
|
1454
1591
|
h.allThreadMessages = [];
|
|
1455
1592
|
const unversioned = {
|
|
1456
|
-
...
|
|
1593
|
+
...permanent(),
|
|
1457
1594
|
schemaVersion: undefined,
|
|
1458
1595
|
} as unknown as MessageDeleteEvent;
|
|
1459
1596
|
|
|
@@ -1482,7 +1619,7 @@ describe("handleMessageDelete", () => {
|
|
|
1482
1619
|
|
|
1483
1620
|
assert.equal(called("message.updateUid").length, 0);
|
|
1484
1621
|
assert.deepEqual(
|
|
1485
|
-
called("message.
|
|
1622
|
+
called("message.transitionPlacement")[0]?.args,
|
|
1486
1623
|
restoredToSource("synced"),
|
|
1487
1624
|
);
|
|
1488
1625
|
});
|
|
@@ -1520,7 +1657,7 @@ describe("handleMessageDelete", () => {
|
|
|
1520
1657
|
|
|
1521
1658
|
assert.equal(h.getConnectionCount, 0);
|
|
1522
1659
|
assert.deepEqual(
|
|
1523
|
-
called("message.
|
|
1660
|
+
called("message.transitionPlacement")[0]?.args,
|
|
1524
1661
|
restoredToSource("synced"),
|
|
1525
1662
|
);
|
|
1526
1663
|
});
|
|
@@ -1540,12 +1677,12 @@ describe("handleMessageDelete", () => {
|
|
|
1540
1677
|
};
|
|
1541
1678
|
h.allThreadMessages = [];
|
|
1542
1679
|
|
|
1543
|
-
await handleMessageDelete(
|
|
1680
|
+
await handleMessageDelete(permanent(), noopLogger, 1, deps());
|
|
1544
1681
|
|
|
1545
1682
|
assert.equal(h.getConnectionCount, 0);
|
|
1546
1683
|
assert.deepEqual(called("message.delete")[0]?.args, ["msg-1"]);
|
|
1547
1684
|
assert.equal(
|
|
1548
|
-
|
|
1685
|
+
restoreCalls().length,
|
|
1549
1686
|
0,
|
|
1550
1687
|
"a row no listing can reach is never restored",
|
|
1551
1688
|
);
|
|
@@ -1563,7 +1700,7 @@ describe("handleMessageDelete", () => {
|
|
|
1563
1700
|
|
|
1564
1701
|
assert.deepEqual(called("message.delete")[0]?.args, ["msg-1"]);
|
|
1565
1702
|
assert.equal(
|
|
1566
|
-
|
|
1703
|
+
restoreCalls().length,
|
|
1567
1704
|
0,
|
|
1568
1705
|
"restoring a Message no listing can reach is the silent vanish, not a hand-back",
|
|
1569
1706
|
);
|