@@ -16,6 +16,7 @@ import {
1616 Keyword ,
1717 MachineSource ,
1818 Post ,
19+ PostHighlight ,
1920 PostKeyword ,
2021 PostTag ,
2122 PostType ,
@@ -1252,6 +1253,342 @@ describe('query feed', () => {
12521253 } ) ;
12531254} ) ;
12541255
1256+ describe ( 'query feedV2' , ( ) => {
1257+ const variables = {
1258+ ranking : Ranking . POPULARITY ,
1259+ first : 10 ,
1260+ version : 20 ,
1261+ } ;
1262+
1263+ const QUERY = `
1264+ query FeedV2($ranking: Ranking, $first: Int, $after: String, $version: Int, $unreadOnly: Boolean, $supportedTypes: [String!], $highlightsLimit: Int, $noAi: Boolean) {
1265+ feedV2(ranking: $ranking, first: $first, after: $after, version: $version, unreadOnly: $unreadOnly, supportedTypes: $supportedTypes, highlightsLimit: $highlightsLimit, noAi: $noAi) {
1266+ pageInfo {
1267+ endCursor
1268+ hasNextPage
1269+ }
1270+ edges {
1271+ cursor
1272+ node {
1273+ __typename
1274+ ... on FeedPostItem {
1275+ feedMeta
1276+ post {
1277+ id
1278+ title
1279+ type
1280+ }
1281+ }
1282+ ... on FeedHighlightsItem {
1283+ feedMeta
1284+ highlights {
1285+ id
1286+ headline
1287+ post {
1288+ id
1289+ title
1290+ }
1291+ }
1292+ }
1293+ }
1294+ }
1295+ }
1296+ }
1297+ ` ;
1298+
1299+ it ( 'should not authorize when not logged-in' , ( ) =>
1300+ testQueryErrorCode ( client , { query : QUERY , variables } , 'UNAUTHENTICATED' ) ) ;
1301+
1302+ it ( 'should pass highlights_limit only when highlights are supported' , async ( ) => {
1303+ loggedUser = '1' ;
1304+ await saveFeedFixtures ( ) ;
1305+
1306+ nock ( 'http://localhost:6002' )
1307+ . post ( '/config' )
1308+ . reply ( 200 , {
1309+ user_id : '1' ,
1310+ config : {
1311+ providers : { } ,
1312+ } ,
1313+ } ) ;
1314+ nock ( 'http://localhost:6000' )
1315+ . post ( '/feed.json' , ( body ) => {
1316+ expect ( body . allowed_post_types ) . toEqual ( [ 'article' ] ) ;
1317+ expect ( body . highlights_limit ) . toEqual ( 4 ) ;
1318+ return true ;
1319+ } )
1320+ . reply ( 200 , {
1321+ data : [ { post_id : 'p1' } ] ,
1322+ cursor : 'b' ,
1323+ } ) ;
1324+
1325+ const res = await client . query ( QUERY , {
1326+ variables : {
1327+ ...variables ,
1328+ supportedTypes : [ 'article' , 'highlight' ] ,
1329+ highlightsLimit : 4 ,
1330+ } ,
1331+ } ) ;
1332+
1333+ expect ( res . errors ) . toBeFalsy ( ) ;
1334+ expect ( res . data . feedV2 . edges ) . toHaveLength ( 1 ) ;
1335+ } ) ;
1336+
1337+ it ( 'should include no-ai blocked tags and title words when the saved setting is enabled' , async ( ) => {
1338+ loggedUser = '1' ;
1339+ await saveFeedFixtures ( ) ;
1340+ await saveFixtures ( con , Settings , [
1341+ {
1342+ userId : '1' ,
1343+ flags : {
1344+ noAiFeedEnabled : true ,
1345+ } ,
1346+ } ,
1347+ ] ) ;
1348+
1349+ nock ( 'http://localhost:6002' )
1350+ . post ( '/config' )
1351+ . reply ( 200 , {
1352+ user_id : '1' ,
1353+ config : {
1354+ providers : { } ,
1355+ } ,
1356+ } ) ;
1357+ nock ( 'http://localhost:6000' )
1358+ . post ( '/feed.json' , ( body ) => {
1359+ expect ( body . blocked_tags ) . toEqual (
1360+ expect . arrayContaining ( [ 'golang' , 'ai' , 'openai' ] ) ,
1361+ ) ;
1362+ expect ( body . blocked_title_words ) . toEqual (
1363+ expect . arrayContaining ( [ 'Claude' , 'Elon Musk' ] ) ,
1364+ ) ;
1365+
1366+ return true ;
1367+ } )
1368+ . reply ( 200 , {
1369+ data : [ { post_id : 'p1' } , { post_id : 'p4' } ] ,
1370+ cursor : 'b' ,
1371+ } ) ;
1372+
1373+ const res = await client . query ( QUERY , {
1374+ variables : {
1375+ ...variables ,
1376+ version : 20 ,
1377+ } ,
1378+ } ) ;
1379+
1380+ expect ( res . errors ) . toBeFalsy ( ) ;
1381+ expect ( res . data . feedV2 . edges . length ) . toEqual ( 2 ) ;
1382+ } ) ;
1383+
1384+ it ( 'should include no-ai blocked tags and title words for TIME ranking when the saved setting is enabled' , async ( ) => {
1385+ loggedUser = '1' ;
1386+ await saveFeedFixtures ( ) ;
1387+ await saveFixtures ( con , Settings , [
1388+ {
1389+ userId : '1' ,
1390+ flags : {
1391+ noAiFeedEnabled : true ,
1392+ } ,
1393+ } ,
1394+ ] ) ;
1395+
1396+ nock ( 'http://localhost:6002' )
1397+ . post ( '/config' )
1398+ . reply ( 200 , {
1399+ user_id : '1' ,
1400+ config : {
1401+ providers : { } ,
1402+ } ,
1403+ } ) ;
1404+ nock ( 'http://localhost:6000' )
1405+ . post ( '/feed.json' , ( body ) => {
1406+ expect ( body . blocked_tags ) . toEqual (
1407+ expect . arrayContaining ( [ 'golang' , 'ai' , 'openai' ] ) ,
1408+ ) ;
1409+ expect ( body . blocked_title_words ) . toEqual (
1410+ expect . arrayContaining ( [ 'Claude' , 'Elon Musk' ] ) ,
1411+ ) ;
1412+ expect ( body . feed_config_name ) . toBe ( 'for_you_by_date' ) ;
1413+
1414+ return true ;
1415+ } )
1416+ . reply ( 200 , {
1417+ data : [ { post_id : 'p1' } , { post_id : 'p4' } ] ,
1418+ cursor : 'b' ,
1419+ } ) ;
1420+
1421+ const res = await client . query ( QUERY , {
1422+ variables : {
1423+ ...variables ,
1424+ ranking : Ranking . TIME ,
1425+ version : 20 ,
1426+ } ,
1427+ } ) ;
1428+
1429+ expect ( res . errors ) . toBeFalsy ( ) ;
1430+ expect ( res . data . feedV2 . edges . length ) . toEqual ( 2 ) ;
1431+ } ) ;
1432+
1433+ it ( 'should return mixed post and highlight items' , async ( ) => {
1434+ loggedUser = '1' ;
1435+ await saveFeedFixtures ( ) ;
1436+ await con . getRepository ( PostHighlight ) . save ( [
1437+ {
1438+ id : '3c75fab6-e28b-431d-ab54-a927708de085' ,
1439+ postId : 'p1' ,
1440+ channel : 'happening-now' ,
1441+ highlightedAt : new Date ( '2026-03-19T10:10:00.000Z' ) ,
1442+ headline : 'First highlight' ,
1443+ } ,
1444+ {
1445+ id : 'c2e332bf-83ac-4651-8a05-8e19fbefc5ac' ,
1446+ postId : 'p4' ,
1447+ channel : 'happening-now' ,
1448+ highlightedAt : new Date ( '2026-03-19T10:20:00.000Z' ) ,
1449+ headline : 'Second highlight' ,
1450+ } ,
1451+ ] ) ;
1452+
1453+ nock ( 'http://localhost:6002' )
1454+ . post ( '/config' )
1455+ . reply ( 200 , {
1456+ user_id : '1' ,
1457+ config : {
1458+ providers : { } ,
1459+ } ,
1460+ } ) ;
1461+ nock ( 'http://localhost:6000' )
1462+ . post ( '/feed.json' )
1463+ . reply ( 200 , {
1464+ data : [
1465+ { post_id : 'p1' , metadata : { p : 'post' } } ,
1466+ {
1467+ type : 'highlight' ,
1468+ highlight_ids : [
1469+ '3c75fab6-e28b-431d-ab54-a927708de085' ,
1470+ 'c2e332bf-83ac-4651-8a05-8e19fbefc5ac' ,
1471+ ] ,
1472+ metadata : { p : 'highlight' } ,
1473+ } ,
1474+ { post_id : 'p4' } ,
1475+ ] ,
1476+ cursor : 'next-cursor' ,
1477+ } ) ;
1478+
1479+ const res = await client . query ( QUERY , {
1480+ variables : {
1481+ ...variables ,
1482+ supportedTypes : [ 'article' , 'highlight' ] ,
1483+ highlightsLimit : 2 ,
1484+ } ,
1485+ } ) ;
1486+
1487+ expect ( res . errors ) . toBeFalsy ( ) ;
1488+ expect ( res . data . feedV2 ) . toEqual ( {
1489+ pageInfo : {
1490+ endCursor : 'next-cursor' ,
1491+ hasNextPage : false ,
1492+ } ,
1493+ edges : [
1494+ {
1495+ cursor : 'next-cursor' ,
1496+ node : {
1497+ __typename : 'FeedPostItem' ,
1498+ feedMeta : base64 ( '{"p":"post"}' ) ,
1499+ post : {
1500+ id : 'p1' ,
1501+ title : 'P1' ,
1502+ type : 'article' ,
1503+ } ,
1504+ } ,
1505+ } ,
1506+ {
1507+ cursor : 'next-cursor' ,
1508+ node : {
1509+ __typename : 'FeedHighlightsItem' ,
1510+ feedMeta : base64 ( '{"p":"highlight"}' ) ,
1511+ highlights : [
1512+ {
1513+ id : '3c75fab6-e28b-431d-ab54-a927708de085' ,
1514+ headline : 'First highlight' ,
1515+ post : {
1516+ id : 'p1' ,
1517+ title : 'P1' ,
1518+ } ,
1519+ } ,
1520+ {
1521+ id : 'c2e332bf-83ac-4651-8a05-8e19fbefc5ac' ,
1522+ headline : 'Second highlight' ,
1523+ post : {
1524+ id : 'p4' ,
1525+ title : 'P4' ,
1526+ } ,
1527+ } ,
1528+ ] ,
1529+ } ,
1530+ } ,
1531+ {
1532+ cursor : 'next-cursor' ,
1533+ node : {
1534+ __typename : 'FeedPostItem' ,
1535+ feedMeta : null ,
1536+ post : {
1537+ id : 'p4' ,
1538+ title : 'P4' ,
1539+ type : 'article' ,
1540+ } ,
1541+ } ,
1542+ } ,
1543+ ] ,
1544+ } ) ;
1545+ } ) ;
1546+
1547+ it ( 'should apply the same post filtering as feed for returned post items' , async ( ) => {
1548+ loggedUser = '1' ;
1549+ await saveFeedFixtures ( ) ;
1550+ await con . getRepository ( Post ) . update ( { id : 'p4' } , { banned : true } ) ;
1551+
1552+ nock ( 'http://localhost:6002' )
1553+ . post ( '/config' )
1554+ . reply ( 200 , {
1555+ user_id : '1' ,
1556+ config : {
1557+ providers : { } ,
1558+ } ,
1559+ } ) ;
1560+ nock ( 'http://localhost:6000' )
1561+ . post ( '/feed.json' )
1562+ . reply ( 200 , {
1563+ data : [ { post_id : 'p1' } , { post_id : 'p4' } ] ,
1564+ cursor : 'next-cursor' ,
1565+ } ) ;
1566+
1567+ const res = await client . query ( QUERY , {
1568+ variables : {
1569+ ...variables ,
1570+ supportedTypes : [ 'article' ] ,
1571+ } ,
1572+ } ) ;
1573+
1574+ expect ( res . errors ) . toBeFalsy ( ) ;
1575+ expect ( res . data . feedV2 . edges ) . toEqual ( [
1576+ {
1577+ cursor : 'next-cursor' ,
1578+ node : {
1579+ __typename : 'FeedPostItem' ,
1580+ feedMeta : null ,
1581+ post : {
1582+ id : 'p1' ,
1583+ title : 'P1' ,
1584+ type : 'article' ,
1585+ } ,
1586+ } ,
1587+ } ,
1588+ ] ) ;
1589+ } ) ;
1590+ } ) ;
1591+
12551592describe ( 'query feedByConfig' , ( ) => {
12561593 const variables = {
12571594 first : 10 ,
0 commit comments