Skip to content

Commit ab03083

Browse files
authored
feat: add feedV2 support for highlight items (#3772)
1 parent 235e284 commit ab03083

9 files changed

Lines changed: 905 additions & 112 deletions

File tree

__tests__/feeds.ts

Lines changed: 337 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
12551592
describe('query feedByConfig', () => {
12561593
const variables = {
12571594
first: 10,

0 commit comments

Comments
 (0)