Skip to content

Commit b269c74

Browse files
committed
Fix formatting not being applied prior to filters
1 parent 429b59d commit b269c74

File tree

3 files changed

+330
-37
lines changed

3 files changed

+330
-37
lines changed

services/backend-api/client/src/pages/MessageBuilder.tsx

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -606,12 +606,7 @@ const MessageBuilderContent: React.FC = () => {
606606
)}
607607
{/* Problems Section - Mobile Tabs */}
608608
{!isDesktop && (
609-
<Box
610-
borderTop="1px"
611-
borderColor="gray.600"
612-
transition="padding-bottom 0.3s ease"
613-
// flex={1}
614-
>
609+
<Box borderTop="1px" borderColor="gray.600">
615610
<Tabs
616611
colorScheme="blue"
617612
variant="line"

services/user-feeds-next/src/delivery/delivery.test.ts

Lines changed: 282 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -800,6 +800,288 @@ describe("diagnostic recording during deliverArticles execution", () => {
800800
});
801801
});
802802

803+
describe("filter execution order - filters should operate on formatted content", () => {
804+
it("should match filter on formatted markdown content, not raw HTML", async () => {
805+
const { startDeliveryPreviewContext, getDeliveryPreviewResultsForArticle } =
806+
await import("../delivery-preview");
807+
const { deliverArticles } = await import(".");
808+
809+
const store = createInMemoryDeliveryRecordStore();
810+
// Article with raw HTML that will be formatted to markdown
811+
const article: Article = {
812+
flattened: {
813+
id: "article-html-1",
814+
idHash: "hash-html-1",
815+
description: "<strong>Important</strong> announcement",
816+
},
817+
raw: {},
818+
};
819+
820+
// Filter looking for markdown bold syntax (after formatting)
821+
const filterExpression: LogicalExpression = {
822+
type: ExpressionType.Logical,
823+
op: LogicalExpressionOperator.And,
824+
children: [
825+
{
826+
type: ExpressionType.Relational,
827+
op: RelationalExpressionOperator.Contains,
828+
left: { type: RelationalExpressionLeft.Article, value: "description" },
829+
right: { type: RelationalExpressionRight.String, value: "**Important**" },
830+
},
831+
],
832+
};
833+
834+
const mediumWithFilter = {
835+
id: "medium-html-format-test",
836+
filters: { expression: filterExpression },
837+
details: {
838+
guildId: "guild-123",
839+
channel: { id: "channel-123" },
840+
},
841+
};
842+
843+
let previews: DeliveryPreviewStageResult[] = [];
844+
845+
await startDeliveryPreviewContext("hash-html-1", async () => {
846+
await store.startContext(async () => {
847+
await deliverArticles([article], [mediumWithFilter], {
848+
feedId: "feed-1",
849+
feedUrl: "https://example.com/feed.xml",
850+
articleDayLimit: 100,
851+
discordClient: createTestDiscordRestClient(),
852+
deliveryRecordStore: store,
853+
});
854+
});
855+
previews = getDeliveryPreviewResultsForArticle("hash-html-1");
856+
});
857+
858+
const filterDiagnostic = previews.find(
859+
(d) => d.stage === DeliveryPreviewStage.MediumFilter
860+
);
861+
assert.notStrictEqual(filterDiagnostic, undefined);
862+
// Should PASS because HTML <strong> is converted to markdown ** before filtering
863+
assert.strictEqual(
864+
filterDiagnostic!.status,
865+
DeliveryPreviewStageStatus.Passed,
866+
"Filter should match on formatted markdown (**Important**), not raw HTML"
867+
);
868+
});
869+
870+
it("should NOT match raw HTML tags in filter after formatting", async () => {
871+
const { startDeliveryPreviewContext, getDeliveryPreviewResultsForArticle } =
872+
await import("../delivery-preview");
873+
const { deliverArticles } = await import(".");
874+
875+
const store = createInMemoryDeliveryRecordStore();
876+
const article: Article = {
877+
flattened: {
878+
id: "article-html-2",
879+
idHash: "hash-html-2",
880+
description: "<strong>Text</strong>",
881+
},
882+
raw: {},
883+
};
884+
885+
// Filter looking for literal HTML tag - should NOT match after formatting
886+
const filterExpression: LogicalExpression = {
887+
type: ExpressionType.Logical,
888+
op: LogicalExpressionOperator.And,
889+
children: [
890+
{
891+
type: ExpressionType.Relational,
892+
op: RelationalExpressionOperator.Contains,
893+
left: { type: RelationalExpressionLeft.Article, value: "description" },
894+
right: { type: RelationalExpressionRight.String, value: "<strong>" },
895+
},
896+
],
897+
};
898+
899+
const mediumWithFilter = {
900+
id: "medium-html-tag-test",
901+
filters: { expression: filterExpression },
902+
details: {
903+
guildId: "guild-123",
904+
channel: { id: "channel-123" },
905+
},
906+
};
907+
908+
let previews: DeliveryPreviewStageResult[] = [];
909+
910+
await startDeliveryPreviewContext("hash-html-2", async () => {
911+
await store.startContext(async () => {
912+
await deliverArticles([article], [mediumWithFilter], {
913+
feedId: "feed-1",
914+
feedUrl: "https://example.com/feed.xml",
915+
articleDayLimit: 100,
916+
discordClient: createTestDiscordRestClient(),
917+
deliveryRecordStore: store,
918+
});
919+
});
920+
previews = getDeliveryPreviewResultsForArticle("hash-html-2");
921+
});
922+
923+
const filterDiagnostic = previews.find(
924+
(d) => d.stage === DeliveryPreviewStage.MediumFilter
925+
);
926+
assert.notStrictEqual(filterDiagnostic, undefined);
927+
// Should FAIL because <strong> tag no longer exists after formatting to **
928+
assert.strictEqual(
929+
filterDiagnostic!.status,
930+
DeliveryPreviewStageStatus.Failed,
931+
"Filter should NOT find raw HTML tags after formatting"
932+
);
933+
});
934+
935+
it("should allow filtering on custom placeholder values", async () => {
936+
const { startDeliveryPreviewContext, getDeliveryPreviewResultsForArticle } =
937+
await import("../delivery-preview");
938+
const { deliverArticles } = await import(".");
939+
940+
const store = createInMemoryDeliveryRecordStore();
941+
const article: Article = {
942+
flattened: {
943+
id: "article-cp-1",
944+
idHash: "hash-cp-1",
945+
title: "Article about cats",
946+
},
947+
raw: {},
948+
};
949+
950+
// Filter on custom placeholder value (uppercase version of title)
951+
const filterExpression: LogicalExpression = {
952+
type: ExpressionType.Logical,
953+
op: LogicalExpressionOperator.And,
954+
children: [
955+
{
956+
type: ExpressionType.Relational,
957+
op: RelationalExpressionOperator.Contains,
958+
left: { type: RelationalExpressionLeft.Article, value: "custom::upper" },
959+
right: { type: RelationalExpressionRight.String, value: "CATS" },
960+
},
961+
],
962+
};
963+
964+
const mediumWithFilter = {
965+
id: "medium-cp-test",
966+
filters: { expression: filterExpression },
967+
details: {
968+
guildId: "guild-123",
969+
channel: { id: "channel-123" },
970+
customPlaceholders: [
971+
{
972+
id: "cp-1",
973+
referenceName: "upper",
974+
sourcePlaceholder: "title",
975+
steps: [{ type: "UPPERCASE" }],
976+
},
977+
],
978+
},
979+
};
980+
981+
let previews: DeliveryPreviewStageResult[] = [];
982+
983+
await startDeliveryPreviewContext("hash-cp-1", async () => {
984+
await store.startContext(async () => {
985+
await deliverArticles([article], [mediumWithFilter], {
986+
feedId: "feed-1",
987+
feedUrl: "https://example.com/feed.xml",
988+
articleDayLimit: 100,
989+
discordClient: createTestDiscordRestClient(),
990+
deliveryRecordStore: store,
991+
});
992+
});
993+
previews = getDeliveryPreviewResultsForArticle("hash-cp-1");
994+
});
995+
996+
const filterDiagnostic = previews.find(
997+
(d) => d.stage === DeliveryPreviewStage.MediumFilter
998+
);
999+
assert.notStrictEqual(filterDiagnostic, undefined);
1000+
// Should PASS because custom placeholder is processed before filtering
1001+
assert.strictEqual(
1002+
filterDiagnostic!.status,
1003+
DeliveryPreviewStageStatus.Passed,
1004+
"Filter should be able to match on custom placeholder values"
1005+
);
1006+
});
1007+
1008+
it("should show formatted content in explainBlocked diagnostic", async () => {
1009+
const { startDeliveryPreviewContext, getDeliveryPreviewResultsForArticle } =
1010+
await import("../delivery-preview");
1011+
const { deliverArticles } = await import(".");
1012+
1013+
const store = createInMemoryDeliveryRecordStore();
1014+
const article: Article = {
1015+
flattened: {
1016+
id: "article-explain-1",
1017+
idHash: "hash-explain-1",
1018+
author: '<a href="http://example.com">John Doe</a>',
1019+
},
1020+
raw: {},
1021+
};
1022+
1023+
// Filter that will NOT match (looking for "Jane")
1024+
const filterExpression: LogicalExpression = {
1025+
type: ExpressionType.Logical,
1026+
op: LogicalExpressionOperator.And,
1027+
children: [
1028+
{
1029+
type: ExpressionType.Relational,
1030+
op: RelationalExpressionOperator.Contains,
1031+
left: { type: RelationalExpressionLeft.Article, value: "author" },
1032+
right: { type: RelationalExpressionRight.String, value: "Jane" },
1033+
},
1034+
],
1035+
};
1036+
1037+
const mediumWithFilter = {
1038+
id: "medium-explain-test",
1039+
filters: { expression: filterExpression },
1040+
details: {
1041+
guildId: "guild-123",
1042+
channel: { id: "channel-123" },
1043+
},
1044+
};
1045+
1046+
let previews: DeliveryPreviewStageResult[] = [];
1047+
1048+
await startDeliveryPreviewContext("hash-explain-1", async () => {
1049+
await store.startContext(async () => {
1050+
await deliverArticles([article], [mediumWithFilter], {
1051+
feedId: "feed-1",
1052+
feedUrl: "https://example.com/feed.xml",
1053+
articleDayLimit: 100,
1054+
discordClient: createTestDiscordRestClient(),
1055+
deliveryRecordStore: store,
1056+
});
1057+
});
1058+
previews = getDeliveryPreviewResultsForArticle("hash-explain-1");
1059+
});
1060+
1061+
const filterDiagnostic = previews.find(
1062+
(d) => d.stage === DeliveryPreviewStage.MediumFilter
1063+
);
1064+
assert.notStrictEqual(filterDiagnostic, undefined);
1065+
assert.strictEqual(filterDiagnostic!.status, DeliveryPreviewStageStatus.Failed);
1066+
1067+
const details = filterDiagnostic!.details as {
1068+
explainBlocked: Array<{ truncatedReferenceValue: string }>;
1069+
};
1070+
assert.ok(details.explainBlocked.length > 0);
1071+
1072+
// truncatedReferenceValue should show formatted markdown link, not raw HTML
1073+
const referenceValue = details.explainBlocked[0]?.truncatedReferenceValue ?? "";
1074+
assert.ok(
1075+
!referenceValue.includes("<a"),
1076+
`truncatedReferenceValue should not contain raw HTML tags, got: ${referenceValue}`
1077+
);
1078+
assert.ok(
1079+
referenceValue.includes("[John Doe]") || referenceValue.includes("John Doe"),
1080+
`truncatedReferenceValue should contain formatted text, got: ${referenceValue}`
1081+
);
1082+
});
1083+
});
1084+
8031085
// Helper to create a delivery state for testing
8041086
function createDeliveryState(
8051087
id: string,

0 commit comments

Comments
 (0)