@atproto/bsky 0.0.16 → 0.0.18
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/CHANGELOG.md +14 -0
- package/dist/cache/read-through.d.ts +30 -0
- package/dist/config.d.ts +18 -0
- package/dist/context.d.ts +6 -6
- package/dist/daemon/config.d.ts +15 -0
- package/dist/daemon/context.d.ts +15 -0
- package/dist/daemon/index.d.ts +23 -0
- package/dist/daemon/logger.d.ts +3 -0
- package/dist/daemon/notifications.d.ts +18 -0
- package/dist/daemon/services.d.ts +11 -0
- package/dist/db/database-schema.d.ts +1 -2
- package/dist/db/index.js +16 -1
- package/dist/db/index.js.map +3 -3
- package/dist/db/migrations/20231205T000257238Z-remove-did-cache.d.ts +3 -0
- package/dist/db/migrations/index.d.ts +1 -0
- package/dist/did-cache.d.ts +10 -7
- package/dist/index.d.ts +4 -0
- package/dist/index.js +1921 -938
- package/dist/index.js.map +3 -3
- package/dist/indexer/context.d.ts +2 -0
- package/dist/indexer/index.d.ts +1 -0
- package/dist/lexicon/index.d.ts +12 -0
- package/dist/lexicon/lexicons.d.ts +134 -0
- package/dist/lexicon/types/com/atproto/admin/deleteAccount.d.ts +25 -0
- package/dist/lexicon/types/com/atproto/temp/importRepo.d.ts +32 -0
- package/dist/lexicon/types/com/atproto/temp/pushBlob.d.ts +25 -0
- package/dist/lexicon/types/com/atproto/temp/transferAccount.d.ts +42 -0
- package/dist/logger.d.ts +1 -0
- package/dist/redis.d.ts +10 -1
- package/dist/services/actor/index.d.ts +18 -4
- package/dist/services/actor/views.d.ts +5 -7
- package/dist/services/feed/index.d.ts +6 -4
- package/dist/services/feed/views.d.ts +5 -4
- package/dist/services/index.d.ts +3 -7
- package/dist/services/label/index.d.ts +10 -4
- package/dist/services/moderation/index.d.ts +0 -1
- package/dist/services/types.d.ts +3 -0
- package/dist/services/util/notification.d.ts +5 -0
- package/dist/services/util/post.d.ts +6 -6
- package/dist/util/retry.d.ts +1 -6
- package/package.json +6 -6
- package/src/api/app/bsky/actor/searchActorsTypeahead.ts +1 -1
- package/src/cache/read-through.ts +151 -0
- package/src/config.ts +90 -1
- package/src/context.ts +7 -7
- package/src/daemon/config.ts +60 -0
- package/src/daemon/context.ts +27 -0
- package/src/daemon/index.ts +78 -0
- package/src/daemon/logger.ts +6 -0
- package/src/daemon/notifications.ts +54 -0
- package/src/daemon/services.ts +22 -0
- package/src/db/database-schema.ts +0 -2
- package/src/db/migrations/20231205T000257238Z-remove-did-cache.ts +14 -0
- package/src/db/migrations/index.ts +1 -0
- package/src/did-cache.ts +33 -56
- package/src/feed-gen/index.ts +0 -4
- package/src/index.ts +55 -16
- package/src/indexer/context.ts +5 -0
- package/src/indexer/index.ts +10 -7
- package/src/lexicon/index.ts +50 -0
- package/src/lexicon/lexicons.ts +156 -0
- package/src/lexicon/types/com/atproto/admin/deleteAccount.ts +38 -0
- package/src/lexicon/types/com/atproto/temp/importRepo.ts +45 -0
- package/src/lexicon/types/com/atproto/temp/pushBlob.ts +39 -0
- package/src/lexicon/types/com/atproto/temp/transferAccount.ts +62 -0
- package/src/logger.ts +2 -0
- package/src/redis.ts +43 -3
- package/src/services/actor/index.ts +55 -7
- package/src/services/actor/views.ts +16 -13
- package/src/services/feed/index.ts +27 -13
- package/src/services/feed/views.ts +20 -10
- package/src/services/index.ts +14 -14
- package/src/services/indexing/index.ts +7 -10
- package/src/services/indexing/plugins/post.ts +13 -0
- package/src/services/label/index.ts +66 -22
- package/src/services/moderation/index.ts +1 -1
- package/src/services/moderation/status.ts +1 -4
- package/src/services/types.ts +4 -0
- package/src/services/util/notification.ts +70 -0
- package/src/util/retry.ts +1 -44
- package/tests/admin/get-repo.test.ts +5 -3
- package/tests/admin/moderation.test.ts +2 -2
- package/tests/admin/repo-search.test.ts +1 -0
- package/tests/algos/hot-classic.test.ts +1 -2
- package/tests/auth.test.ts +1 -1
- package/tests/auto-moderator/labeler.test.ts +19 -20
- package/tests/auto-moderator/takedowns.test.ts +16 -10
- package/tests/blob-resolver.test.ts +4 -2
- package/tests/daemon.test.ts +191 -0
- package/tests/did-cache.test.ts +20 -5
- package/tests/handle-invalidation.test.ts +1 -5
- package/tests/indexing.test.ts +20 -13
- package/tests/redis-cache.test.ts +231 -0
- package/tests/seeds/basic.ts +3 -0
- package/tests/subscription/repo.test.ts +4 -7
- package/tests/views/profile.test.ts +0 -1
- package/tests/views/thread.test.ts +73 -78
- package/tests/views/threadgating.test.ts +38 -0
- package/dist/db/tables/did-cache.d.ts +0 -10
- package/dist/feed-gen/best-of-follows.d.ts +0 -29
- package/dist/feed-gen/whats-hot.d.ts +0 -29
- package/dist/feed-gen/with-friends.d.ts +0 -3
- package/dist/label-cache.d.ts +0 -19
- package/src/db/tables/did-cache.ts +0 -13
- package/src/feed-gen/best-of-follows.ts +0 -77
- package/src/feed-gen/whats-hot.ts +0 -101
- package/src/feed-gen/with-friends.ts +0 -43
- package/src/label-cache.ts +0 -90
- package/tests/algos/whats-hot.test.ts +0 -118
- package/tests/algos/with-friends.test.ts +0 -145
package/src/label-cache.ts
DELETED
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
import { wait } from '@atproto/common'
|
|
2
|
-
import { PrimaryDatabase } from './db'
|
|
3
|
-
import { Label } from './db/tables/label'
|
|
4
|
-
import { labelerLogger as log } from './logger'
|
|
5
|
-
|
|
6
|
-
export class LabelCache {
|
|
7
|
-
bySubject: Record<string, Label[]> = {}
|
|
8
|
-
latestLabel = ''
|
|
9
|
-
refreshes = 0
|
|
10
|
-
|
|
11
|
-
destroyed = false
|
|
12
|
-
|
|
13
|
-
constructor(public db: PrimaryDatabase) {}
|
|
14
|
-
|
|
15
|
-
start() {
|
|
16
|
-
this.poll()
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
async fullRefresh() {
|
|
20
|
-
const allLabels = await this.db.db.selectFrom('label').selectAll().execute()
|
|
21
|
-
this.wipeCache()
|
|
22
|
-
this.processLabels(allLabels)
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
async partialRefresh() {
|
|
26
|
-
const labels = await this.db.db
|
|
27
|
-
.selectFrom('label')
|
|
28
|
-
.selectAll()
|
|
29
|
-
.where('cts', '>', this.latestLabel)
|
|
30
|
-
.execute()
|
|
31
|
-
this.processLabels(labels)
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
async poll() {
|
|
35
|
-
try {
|
|
36
|
-
if (this.destroyed) return
|
|
37
|
-
if (this.refreshes >= 120) {
|
|
38
|
-
await this.fullRefresh()
|
|
39
|
-
this.refreshes = 0
|
|
40
|
-
} else {
|
|
41
|
-
await this.partialRefresh()
|
|
42
|
-
this.refreshes++
|
|
43
|
-
}
|
|
44
|
-
} catch (err) {
|
|
45
|
-
log.error(
|
|
46
|
-
{ err, latestLabel: this.latestLabel, refreshes: this.refreshes },
|
|
47
|
-
'label cache failed to refresh',
|
|
48
|
-
)
|
|
49
|
-
}
|
|
50
|
-
await wait(500)
|
|
51
|
-
this.poll()
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
processLabels(labels: Label[]) {
|
|
55
|
-
for (const label of labels) {
|
|
56
|
-
if (label.cts > this.latestLabel) {
|
|
57
|
-
this.latestLabel = label.cts
|
|
58
|
-
}
|
|
59
|
-
this.bySubject[label.uri] ??= []
|
|
60
|
-
this.bySubject[label.uri].push(label)
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
wipeCache() {
|
|
65
|
-
this.bySubject = {}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
stop() {
|
|
69
|
-
this.destroyed = true
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
forSubject(subject: string, includeNeg = false): Label[] {
|
|
73
|
-
const labels = this.bySubject[subject] ?? []
|
|
74
|
-
return includeNeg ? labels : labels.filter((l) => l.neg === false)
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
forSubjects(subjects: string[], includeNeg?: boolean): Label[] {
|
|
78
|
-
let labels: Label[] = []
|
|
79
|
-
const alreadyAdded = new Set<string>()
|
|
80
|
-
for (const subject of subjects) {
|
|
81
|
-
if (alreadyAdded.has(subject)) {
|
|
82
|
-
continue
|
|
83
|
-
}
|
|
84
|
-
const subLabels = this.forSubject(subject, includeNeg)
|
|
85
|
-
labels = [...labels, ...subLabels]
|
|
86
|
-
alreadyAdded.add(subject)
|
|
87
|
-
}
|
|
88
|
-
return labels
|
|
89
|
-
}
|
|
90
|
-
}
|
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
import { HOUR } from '@atproto/common'
|
|
2
|
-
import AtpAgent, { AtUri } from '@atproto/api'
|
|
3
|
-
import { TestNetwork, SeedClient } from '@atproto/dev-env'
|
|
4
|
-
import basicSeed from '../seeds/basic'
|
|
5
|
-
import { makeAlgos } from '../../src'
|
|
6
|
-
|
|
7
|
-
describe.skip('algo whats-hot', () => {
|
|
8
|
-
let network: TestNetwork
|
|
9
|
-
let agent: AtpAgent
|
|
10
|
-
let sc: SeedClient
|
|
11
|
-
|
|
12
|
-
// account dids, for convenience
|
|
13
|
-
let alice: string
|
|
14
|
-
let bob: string
|
|
15
|
-
let carol: string
|
|
16
|
-
|
|
17
|
-
const feedPublisherDid = 'did:example:feed-publisher'
|
|
18
|
-
const feedUri = AtUri.make(
|
|
19
|
-
feedPublisherDid,
|
|
20
|
-
'app.bsky.feed.generator',
|
|
21
|
-
'whats-hot',
|
|
22
|
-
).toString()
|
|
23
|
-
|
|
24
|
-
beforeAll(async () => {
|
|
25
|
-
network = await TestNetwork.create({
|
|
26
|
-
dbPostgresSchema: 'bsky_algo_whats_hot',
|
|
27
|
-
bsky: { algos: makeAlgos(feedPublisherDid) },
|
|
28
|
-
})
|
|
29
|
-
agent = new AtpAgent({ service: network.bsky.url })
|
|
30
|
-
sc = network.getSeedClient()
|
|
31
|
-
await basicSeed(sc)
|
|
32
|
-
|
|
33
|
-
alice = sc.dids.alice
|
|
34
|
-
bob = sc.dids.bob
|
|
35
|
-
carol = sc.dids.carol
|
|
36
|
-
await network.processAll()
|
|
37
|
-
await network.bsky.processAll()
|
|
38
|
-
})
|
|
39
|
-
|
|
40
|
-
afterAll(async () => {
|
|
41
|
-
await network.close()
|
|
42
|
-
})
|
|
43
|
-
|
|
44
|
-
it('returns well liked posts', async () => {
|
|
45
|
-
const img = await sc.uploadFile(
|
|
46
|
-
alice,
|
|
47
|
-
'tests/sample-img/key-landscape-small.jpg',
|
|
48
|
-
'image/jpeg',
|
|
49
|
-
)
|
|
50
|
-
const one = await sc.post(carol, 'carol is in the chat')
|
|
51
|
-
const two = await sc.post(carol, "it's me, carol")
|
|
52
|
-
const three = await sc.post(alice, 'first post', undefined, [img])
|
|
53
|
-
const four = await sc.post(bob, 'bobby boi')
|
|
54
|
-
const five = await sc.post(bob, 'another one')
|
|
55
|
-
|
|
56
|
-
for (let i = 0; i < 20; i++) {
|
|
57
|
-
const name = `user${i}`
|
|
58
|
-
await sc.createAccount(name, {
|
|
59
|
-
handle: `user${i}.test`,
|
|
60
|
-
email: `user${i}@test.com`,
|
|
61
|
-
password: 'password',
|
|
62
|
-
})
|
|
63
|
-
await sc.like(sc.dids[name], three.ref) // will be down-regulated by time
|
|
64
|
-
if (i > 3) {
|
|
65
|
-
await sc.like(sc.dids[name], one.ref)
|
|
66
|
-
}
|
|
67
|
-
if (i > 5) {
|
|
68
|
-
await sc.like(sc.dids[name], two.ref)
|
|
69
|
-
}
|
|
70
|
-
if (i > 7) {
|
|
71
|
-
await sc.like(sc.dids[name], four.ref)
|
|
72
|
-
await sc.like(sc.dids[name], five.ref)
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
await network.bsky.processAll()
|
|
76
|
-
|
|
77
|
-
// move the 3rd post 5 hours into the past to check gravity
|
|
78
|
-
await network.bsky.ctx.db
|
|
79
|
-
.getPrimary()
|
|
80
|
-
.db.updateTable('post')
|
|
81
|
-
.where('uri', '=', three.ref.uriStr)
|
|
82
|
-
.set({ indexedAt: new Date(Date.now() - 5 * HOUR).toISOString() })
|
|
83
|
-
.execute()
|
|
84
|
-
|
|
85
|
-
await network.bsky.ctx.db
|
|
86
|
-
.getPrimary()
|
|
87
|
-
.refreshMaterializedView('algo_whats_hot_view')
|
|
88
|
-
|
|
89
|
-
const res = await agent.api.app.bsky.feed.getFeed(
|
|
90
|
-
{ feed: feedUri },
|
|
91
|
-
{ headers: await network.serviceHeaders(alice) },
|
|
92
|
-
)
|
|
93
|
-
expect(res.data.feed[0].post.uri).toBe(one.ref.uriStr)
|
|
94
|
-
expect(res.data.feed[1].post.uri).toBe(two.ref.uriStr)
|
|
95
|
-
const indexOfThird = res.data.feed.findIndex(
|
|
96
|
-
(item) => item.post.uri === three.ref.uriStr,
|
|
97
|
-
)
|
|
98
|
-
// doesn't quite matter where this cam in but it should be down-regulated pretty severely from gravity
|
|
99
|
-
expect(indexOfThird).toBeGreaterThan(3)
|
|
100
|
-
})
|
|
101
|
-
|
|
102
|
-
it('paginates', async () => {
|
|
103
|
-
const res = await agent.api.app.bsky.feed.getFeed(
|
|
104
|
-
{ feed: feedUri },
|
|
105
|
-
{ headers: await network.serviceHeaders(alice) },
|
|
106
|
-
)
|
|
107
|
-
const first = await agent.api.app.bsky.feed.getFeed(
|
|
108
|
-
{ feed: feedUri, limit: 3 },
|
|
109
|
-
{ headers: await network.serviceHeaders(alice) },
|
|
110
|
-
)
|
|
111
|
-
const second = await agent.api.app.bsky.feed.getFeed(
|
|
112
|
-
{ feed: feedUri, cursor: first.data.cursor },
|
|
113
|
-
{ headers: await network.serviceHeaders(alice) },
|
|
114
|
-
)
|
|
115
|
-
|
|
116
|
-
expect([...first.data.feed, ...second.data.feed]).toEqual(res.data.feed)
|
|
117
|
-
})
|
|
118
|
-
})
|
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
import AtpAgent, { AtUri } from '@atproto/api'
|
|
2
|
-
import userSeed from '../seeds/users'
|
|
3
|
-
import { makeAlgos } from '../../src'
|
|
4
|
-
import { TestNetwork, SeedClient, RecordRef } from '@atproto/dev-env'
|
|
5
|
-
|
|
6
|
-
describe.skip('algo with friends', () => {
|
|
7
|
-
let network: TestNetwork
|
|
8
|
-
let agent: AtpAgent
|
|
9
|
-
let sc: SeedClient
|
|
10
|
-
|
|
11
|
-
// account dids, for convenience
|
|
12
|
-
let alice: string
|
|
13
|
-
let bob: string
|
|
14
|
-
let carol: string
|
|
15
|
-
let dan: string
|
|
16
|
-
|
|
17
|
-
const feedPublisherDid = 'did:example:feed-publisher'
|
|
18
|
-
const feedUri = AtUri.make(
|
|
19
|
-
feedPublisherDid,
|
|
20
|
-
'app.bsky.feed.generator',
|
|
21
|
-
'with-friends',
|
|
22
|
-
).toString()
|
|
23
|
-
|
|
24
|
-
beforeAll(async () => {
|
|
25
|
-
network = await TestNetwork.create({
|
|
26
|
-
dbPostgresSchema: 'bsky_algo_with_friends',
|
|
27
|
-
bsky: { algos: makeAlgos(feedPublisherDid) },
|
|
28
|
-
})
|
|
29
|
-
agent = new AtpAgent({ service: network.bsky.url })
|
|
30
|
-
sc = network.getSeedClient()
|
|
31
|
-
await userSeed(sc)
|
|
32
|
-
|
|
33
|
-
alice = sc.dids.alice
|
|
34
|
-
bob = sc.dids.bob
|
|
35
|
-
carol = sc.dids.carol
|
|
36
|
-
dan = sc.dids.dan
|
|
37
|
-
await network.processAll()
|
|
38
|
-
await network.bsky.processAll()
|
|
39
|
-
})
|
|
40
|
-
|
|
41
|
-
afterAll(async () => {
|
|
42
|
-
await network.close()
|
|
43
|
-
})
|
|
44
|
-
|
|
45
|
-
let expectedFeed: string[]
|
|
46
|
-
|
|
47
|
-
it('setup', async () => {
|
|
48
|
-
for (let i = 0; i < 10; i++) {
|
|
49
|
-
const name = `user${i}`
|
|
50
|
-
await sc.createAccount(name, {
|
|
51
|
-
handle: `user${i}.test`,
|
|
52
|
-
email: `user${i}@test.com`,
|
|
53
|
-
password: 'password',
|
|
54
|
-
})
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const hitLikeThreshold = async (ref: RecordRef) => {
|
|
58
|
-
for (let i = 0; i < 10; i++) {
|
|
59
|
-
const name = `user${i}`
|
|
60
|
-
await sc.like(sc.dids[name], ref)
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// bob and dan are mutuals of alice, all userN are out-of-network.
|
|
65
|
-
await sc.follow(alice, bob)
|
|
66
|
-
await sc.follow(alice, carol)
|
|
67
|
-
await sc.follow(alice, dan)
|
|
68
|
-
await sc.follow(bob, alice)
|
|
69
|
-
await sc.follow(dan, alice)
|
|
70
|
-
const one = await sc.post(bob, 'one')
|
|
71
|
-
const two = await sc.post(bob, 'two')
|
|
72
|
-
const three = await sc.post(carol, 'three')
|
|
73
|
-
const four = await sc.post(carol, 'four')
|
|
74
|
-
const five = await sc.post(dan, 'five')
|
|
75
|
-
const six = await sc.post(dan, 'six')
|
|
76
|
-
const seven = await sc.post(sc.dids.user0, 'seven')
|
|
77
|
-
const eight = await sc.post(sc.dids.user0, 'eight')
|
|
78
|
-
const nine = await sc.post(sc.dids.user1, 'nine')
|
|
79
|
-
const ten = await sc.post(sc.dids.user1, 'ten')
|
|
80
|
-
|
|
81
|
-
// 1, 2, 3, 4, 6, 8, 10 hit like threshold
|
|
82
|
-
await hitLikeThreshold(one.ref)
|
|
83
|
-
await hitLikeThreshold(two.ref)
|
|
84
|
-
await hitLikeThreshold(three.ref)
|
|
85
|
-
await hitLikeThreshold(four.ref)
|
|
86
|
-
await hitLikeThreshold(six.ref)
|
|
87
|
-
await hitLikeThreshold(eight.ref)
|
|
88
|
-
await hitLikeThreshold(ten.ref)
|
|
89
|
-
|
|
90
|
-
// 1, 4, 7, 8, 10 liked by mutual
|
|
91
|
-
await sc.like(bob, one.ref)
|
|
92
|
-
await sc.like(dan, four.ref)
|
|
93
|
-
await sc.like(bob, seven.ref)
|
|
94
|
-
await sc.like(dan, eight.ref)
|
|
95
|
-
await sc.like(bob, nine.ref)
|
|
96
|
-
await sc.like(dan, ten.ref)
|
|
97
|
-
|
|
98
|
-
// all liked by non-mutual
|
|
99
|
-
await sc.like(carol, one.ref)
|
|
100
|
-
await sc.like(carol, two.ref)
|
|
101
|
-
await sc.like(carol, three.ref)
|
|
102
|
-
await sc.like(carol, four.ref)
|
|
103
|
-
await sc.like(carol, five.ref)
|
|
104
|
-
await sc.like(carol, six.ref)
|
|
105
|
-
await sc.like(carol, seven.ref)
|
|
106
|
-
await sc.like(carol, eight.ref)
|
|
107
|
-
await sc.like(carol, nine.ref)
|
|
108
|
-
await sc.like(carol, ten.ref)
|
|
109
|
-
|
|
110
|
-
await network.bsky.processAll()
|
|
111
|
-
|
|
112
|
-
expectedFeed = [
|
|
113
|
-
ten.ref.uriStr,
|
|
114
|
-
eight.ref.uriStr,
|
|
115
|
-
four.ref.uriStr,
|
|
116
|
-
one.ref.uriStr,
|
|
117
|
-
]
|
|
118
|
-
})
|
|
119
|
-
|
|
120
|
-
it('returns popular in & out of network posts', async () => {
|
|
121
|
-
const res = await agent.api.app.bsky.feed.getFeed(
|
|
122
|
-
{ feed: feedUri },
|
|
123
|
-
{ headers: await network.serviceHeaders(alice) },
|
|
124
|
-
)
|
|
125
|
-
const feedUris = res.data.feed.map((i) => i.post.uri)
|
|
126
|
-
expect(feedUris).toEqual(expectedFeed)
|
|
127
|
-
})
|
|
128
|
-
|
|
129
|
-
it('paginates', async () => {
|
|
130
|
-
const res = await agent.api.app.bsky.feed.getFeed(
|
|
131
|
-
{ feed: feedUri },
|
|
132
|
-
{ headers: await network.serviceHeaders(alice) },
|
|
133
|
-
)
|
|
134
|
-
const first = await agent.api.app.bsky.feed.getFeed(
|
|
135
|
-
{ feed: feedUri, limit: 2 },
|
|
136
|
-
{ headers: await network.serviceHeaders(alice) },
|
|
137
|
-
)
|
|
138
|
-
const second = await agent.api.app.bsky.feed.getFeed(
|
|
139
|
-
{ feed: feedUri, cursor: first.data.cursor },
|
|
140
|
-
{ headers: await network.serviceHeaders(alice) },
|
|
141
|
-
)
|
|
142
|
-
|
|
143
|
-
expect([...first.data.feed, ...second.data.feed]).toEqual(res.data.feed)
|
|
144
|
-
})
|
|
145
|
-
})
|