backend-manager 5.10.2 → 5.11.0
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/CLAUDE.md +3 -1
- package/docs/admin-post-route.md +8 -5
- package/docs/cdp-debugging.md +15 -30
- package/docs/ghostii.md +18 -11
- package/docs/marketing-campaigns.md +11 -7
- package/package.json +1 -1
- package/src/cli/index.js +7 -1
- package/src/defaults/CLAUDE.md +2 -0
- package/src/manager/events/cron/daily/blog-auto-publisher.js +61 -150
- package/src/manager/libraries/content/source-resolver.js +296 -109
- package/src/manager/libraries/email/generators/newsletter.js +147 -69
- package/src/manager/routes/admin/post/post.js +69 -1
- package/src/test/fixtures/firebase-project/.temp/test-mode.json +1 -1
- package/src/test/fixtures/firebase-project/database-debug.log +8 -8
- package/src/test/fixtures/firebase-project/firestore-debug.log +55 -57
- package/src/test/fixtures/firebase-project/pubsub-debug.log +3 -3
- package/templates/backend-manager-config.json +1 -0
- package/test/email/newsletter-generate.js +4 -5
- package/test/helpers/content/blog-auto-publisher.js +116 -132
- package/test/helpers/content/ghostii-feed-integration.js +2 -2
- package/test/routes/admin/post-convert-image.js +158 -0
- package/test/routes/admin/post-download-error.js +89 -0
|
@@ -2,7 +2,7 @@ WARNING: A terminally deprecated method in sun.misc.Unsafe has been called
|
|
|
2
2
|
WARNING: sun.misc.Unsafe::allocateMemory has been called by io.netty.util.internal.PlatformDependent0$2 (file:/Users/ian/.cache/firebase/emulators/cloud-firestore-emulator-v1.21.0.jar)
|
|
3
3
|
WARNING: Please consider reporting this to the maintainers of class io.netty.util.internal.PlatformDependent0$2
|
|
4
4
|
WARNING: sun.misc.Unsafe::allocateMemory will be removed in a future release
|
|
5
|
-
|
|
5
|
+
Jul 01, 2026 10:16:02 PM com.google.cloud.datastore.emulator.firestore.websocket.WebSocketServer start
|
|
6
6
|
INFO: Started WebSocket server on ws://127.0.0.1:9150
|
|
7
7
|
|
|
8
8
|
API endpoint: http://127.0.0.1:8080
|
|
@@ -20,115 +20,113 @@ If you are running a Firestore in Datastore Mode project, run:
|
|
|
20
20
|
Note: Support for Datastore Mode is in preview. If you encounter any bugs please file at https://github.com/firebase/firebase-tools/issues.
|
|
21
21
|
Dev App Server is now running.
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
Jul 01, 2026 10:16:12 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
|
|
24
24
|
INFO: Detected non-HTTP/2 connection.
|
|
25
|
-
|
|
25
|
+
Jul 01, 2026 10:16:12 PM io.grpc.netty.TcpMetrics loadEpollInfo
|
|
26
26
|
INFO: Epoll available during static init of TcpMetrics:false
|
|
27
|
-
|
|
27
|
+
Jul 01, 2026 10:16:12 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
|
|
28
28
|
INFO: Detected HTTP/2 connection.
|
|
29
|
-
|
|
29
|
+
Jul 01, 2026 10:16:14 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
|
|
30
30
|
INFO: Detected HTTP/2 connection.
|
|
31
|
-
|
|
31
|
+
Jul 01, 2026 10:16:14 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
|
|
32
32
|
INFO: Detected HTTP/2 connection.
|
|
33
|
-
|
|
33
|
+
Jul 01, 2026 10:16:23 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
|
|
34
34
|
INFO: Detected HTTP/2 connection.
|
|
35
|
-
|
|
35
|
+
Jul 01, 2026 10:16:23 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
|
|
36
36
|
INFO: Detected HTTP/2 connection.
|
|
37
|
-
|
|
37
|
+
Jul 01, 2026 10:16:24 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
|
|
38
38
|
INFO: Detected HTTP/2 connection.
|
|
39
|
-
|
|
39
|
+
Jul 01, 2026 10:16:24 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
|
|
40
40
|
INFO: Detected HTTP/2 connection.
|
|
41
|
-
|
|
41
|
+
Jul 01, 2026 10:16:24 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
|
|
42
42
|
INFO: Detected HTTP/2 connection.
|
|
43
|
-
|
|
43
|
+
Jul 01, 2026 10:16:24 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
|
|
44
44
|
INFO: Detected HTTP/2 connection.
|
|
45
|
-
|
|
45
|
+
Jul 01, 2026 10:16:24 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
|
|
46
46
|
INFO: Detected HTTP/2 connection.
|
|
47
|
-
|
|
47
|
+
Jul 01, 2026 10:16:24 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
|
|
48
48
|
INFO: Detected HTTP/2 connection.
|
|
49
|
-
|
|
49
|
+
Jul 01, 2026 10:16:24 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
|
|
50
50
|
INFO: Detected HTTP/2 connection.
|
|
51
|
-
|
|
51
|
+
Jul 01, 2026 10:16:24 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
|
|
52
52
|
INFO: Detected HTTP/2 connection.
|
|
53
|
-
|
|
53
|
+
Jul 01, 2026 10:16:24 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
|
|
54
54
|
INFO: Detected HTTP/2 connection.
|
|
55
|
-
|
|
55
|
+
Jul 01, 2026 10:16:24 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
|
|
56
56
|
INFO: Detected HTTP/2 connection.
|
|
57
|
-
|
|
57
|
+
Jul 01, 2026 10:16:24 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
|
|
58
58
|
INFO: Detected HTTP/2 connection.
|
|
59
|
-
|
|
59
|
+
Jul 01, 2026 10:16:24 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
|
|
60
60
|
INFO: Detected HTTP/2 connection.
|
|
61
|
-
|
|
61
|
+
Jul 01, 2026 10:16:24 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
|
|
62
62
|
INFO: Detected HTTP/2 connection.
|
|
63
|
-
|
|
63
|
+
Jul 01, 2026 10:16:24 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
|
|
64
64
|
INFO: Detected HTTP/2 connection.
|
|
65
|
-
|
|
65
|
+
Jul 01, 2026 10:16:24 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
|
|
66
66
|
INFO: Detected HTTP/2 connection.
|
|
67
|
-
|
|
67
|
+
Jul 01, 2026 10:16:24 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
|
|
68
68
|
INFO: Detected HTTP/2 connection.
|
|
69
|
-
|
|
69
|
+
Jul 01, 2026 10:16:24 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
|
|
70
70
|
INFO: Detected HTTP/2 connection.
|
|
71
|
-
|
|
71
|
+
Jul 01, 2026 10:16:24 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
|
|
72
72
|
INFO: Detected HTTP/2 connection.
|
|
73
|
-
|
|
73
|
+
Jul 01, 2026 10:16:24 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
|
|
74
74
|
INFO: Detected HTTP/2 connection.
|
|
75
|
-
|
|
75
|
+
Jul 01, 2026 10:16:24 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
|
|
76
76
|
INFO: Detected HTTP/2 connection.
|
|
77
|
-
|
|
77
|
+
Jul 01, 2026 10:16:24 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
|
|
78
78
|
INFO: Detected HTTP/2 connection.
|
|
79
|
-
|
|
79
|
+
Jul 01, 2026 10:16:24 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
|
|
80
80
|
INFO: Detected HTTP/2 connection.
|
|
81
|
-
|
|
81
|
+
Jul 01, 2026 10:16:24 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
|
|
82
82
|
INFO: Detected HTTP/2 connection.
|
|
83
|
-
|
|
83
|
+
Jul 01, 2026 10:16:24 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
|
|
84
84
|
INFO: Detected HTTP/2 connection.
|
|
85
|
-
|
|
85
|
+
Jul 01, 2026 10:16:24 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
|
|
86
86
|
INFO: Detected HTTP/2 connection.
|
|
87
|
-
|
|
87
|
+
Jul 01, 2026 10:16:24 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
|
|
88
88
|
INFO: Detected HTTP/2 connection.
|
|
89
|
-
|
|
89
|
+
Jul 01, 2026 10:16:24 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
|
|
90
90
|
INFO: Detected HTTP/2 connection.
|
|
91
|
-
|
|
91
|
+
Jul 01, 2026 10:16:24 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
|
|
92
92
|
INFO: Detected HTTP/2 connection.
|
|
93
|
-
|
|
93
|
+
Jul 01, 2026 10:16:24 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
|
|
94
94
|
INFO: Detected HTTP/2 connection.
|
|
95
|
-
|
|
95
|
+
Jul 01, 2026 10:16:24 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
|
|
96
96
|
INFO: Detected HTTP/2 connection.
|
|
97
|
-
|
|
97
|
+
Jul 01, 2026 10:16:24 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
|
|
98
98
|
INFO: Detected HTTP/2 connection.
|
|
99
|
-
|
|
99
|
+
Jul 01, 2026 10:16:24 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
|
|
100
100
|
INFO: Detected HTTP/2 connection.
|
|
101
|
-
|
|
101
|
+
Jul 01, 2026 10:16:24 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
|
|
102
102
|
INFO: Detected HTTP/2 connection.
|
|
103
|
-
|
|
103
|
+
Jul 01, 2026 10:16:24 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
|
|
104
104
|
INFO: Detected HTTP/2 connection.
|
|
105
|
-
|
|
105
|
+
Jul 01, 2026 10:16:24 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
|
|
106
106
|
INFO: Detected HTTP/2 connection.
|
|
107
|
-
|
|
107
|
+
Jul 01, 2026 10:16:24 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
|
|
108
108
|
INFO: Detected HTTP/2 connection.
|
|
109
|
-
|
|
109
|
+
Jul 01, 2026 10:16:24 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
|
|
110
110
|
INFO: Detected HTTP/2 connection.
|
|
111
|
-
|
|
111
|
+
Jul 01, 2026 10:16:24 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
|
|
112
112
|
INFO: Detected HTTP/2 connection.
|
|
113
|
-
|
|
113
|
+
Jul 01, 2026 10:16:24 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
|
|
114
114
|
INFO: Detected HTTP/2 connection.
|
|
115
|
-
|
|
115
|
+
Jul 01, 2026 10:16:24 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
|
|
116
116
|
INFO: Detected HTTP/2 connection.
|
|
117
|
-
|
|
117
|
+
Jul 01, 2026 10:16:25 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
|
|
118
118
|
INFO: Detected HTTP/2 connection.
|
|
119
|
-
|
|
119
|
+
Jul 01, 2026 10:16:25 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
|
|
120
120
|
INFO: Detected HTTP/2 connection.
|
|
121
|
-
|
|
121
|
+
Jul 01, 2026 10:16:25 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
|
|
122
122
|
INFO: Detected HTTP/2 connection.
|
|
123
|
-
|
|
123
|
+
Jul 01, 2026 10:16:25 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
|
|
124
124
|
INFO: Detected HTTP/2 connection.
|
|
125
|
-
|
|
125
|
+
Jul 01, 2026 10:16:25 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
|
|
126
126
|
INFO: Detected HTTP/2 connection.
|
|
127
|
-
|
|
127
|
+
Jul 01, 2026 10:16:25 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
|
|
128
128
|
INFO: Detected HTTP/2 connection.
|
|
129
|
-
|
|
130
|
-
INFO: Detected HTTP/2 connection.
|
|
131
|
-
Jun 30, 2026 2:47:32 AM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
|
|
129
|
+
Jul 01, 2026 10:16:25 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
|
|
132
130
|
INFO: Detected HTTP/2 connection.
|
|
133
131
|
*** shutting down gRPC server since JVM is shutting down
|
|
134
132
|
*** server shut down
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
This is the Google Pub/Sub fake.
|
|
2
2
|
Implementation may be incomplete or differ from the real system.
|
|
3
|
-
|
|
3
|
+
Jul 01, 2026 10:16:06 PM com.google.cloud.pubsub.testing.v1.Main main
|
|
4
4
|
INFO: IAM integration is disabled. IAM policy methods and ACL checks are not supported
|
|
5
5
|
SLF4J(W): No SLF4J providers were found.
|
|
6
6
|
SLF4J(W): Defaulting to no-operation (NOP) logger implementation
|
|
@@ -9,9 +9,9 @@ WARNING: A terminally deprecated method in sun.misc.Unsafe has been called
|
|
|
9
9
|
WARNING: sun.misc.Unsafe::allocateMemory has been called by io.netty.util.internal.PlatformDependent0$2 (file:/Users/ian/.cache/firebase/emulators/pubsub-emulator-0.8.33/pubsub-emulator/lib/cloud-pubsub-emulator-0.8.33-all.jar)
|
|
10
10
|
WARNING: Please consider reporting this to the maintainers of class io.netty.util.internal.PlatformDependent0$2
|
|
11
11
|
WARNING: sun.misc.Unsafe::allocateMemory will be removed in a future release
|
|
12
|
-
|
|
12
|
+
Jul 01, 2026 10:16:07 PM com.google.cloud.pubsub.testing.v1.Main main
|
|
13
13
|
INFO: Server started, listening on 8085
|
|
14
|
-
|
|
14
|
+
Jul 01, 2026 10:16:13 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
|
|
15
15
|
INFO: Detected HTTP/2 connection.
|
|
16
16
|
*** shutting down gRPC server since JVM is shutting down
|
|
17
17
|
*** server shut down
|
|
@@ -150,6 +150,7 @@
|
|
|
150
150
|
content: [
|
|
151
151
|
{
|
|
152
152
|
sources: ['$parent'], // '$parent' | '$feed:https://...' | '$brand'
|
|
153
|
+
sourceCount: 6, // how many sources are picked (randomly) per issue
|
|
153
154
|
categories: [], // e.g., ['social-media', 'marketing'] — topic constraints + AI categorization
|
|
154
155
|
links: [], // links to inject into generated content
|
|
155
156
|
instructions: '', // free-form text passed to the AI
|
|
@@ -333,7 +333,7 @@ module.exports = {
|
|
|
333
333
|
// inserts `api.` at call time. PARENT_API_URL env override is honored
|
|
334
334
|
// verbatim for one-off testing against a different parent.
|
|
335
335
|
// When NEWSLETTER_SOURCE is set, skip parent URL checks and source fetching —
|
|
336
|
-
// the generator's built-in resolver (
|
|
336
|
+
// the generator's built-in resolver (resolveSources) handles everything.
|
|
337
337
|
let sources = [];
|
|
338
338
|
|
|
339
339
|
if (!env.NEWSLETTER_SOURCE) {
|
|
@@ -437,9 +437,9 @@ module.exports = {
|
|
|
437
437
|
console.log(`[extended] uploading to itw-creative-works/newsletter-assets/${config.brand?.id}/${campaignId || '<auto-id>'}/ + Beehiiv draft`);
|
|
438
438
|
|
|
439
439
|
// NEWSLETTER_SOURCE overrides the configured sources[] and lets the
|
|
440
|
-
// generator's built-in resolver run (
|
|
441
|
-
// this, the test always passes
|
|
442
|
-
//
|
|
440
|
+
// generator's built-in resolver run (resolveSources — the unified
|
|
441
|
+
// blog/newsletter resolver). Without this, the test always passes
|
|
442
|
+
// pre-fetched parent sources via opts.sources, bypassing resolution.
|
|
443
443
|
// NEWSLETTER_SOURCE='$feed:https://feeds.arstechnica.com/arstechnica/index'
|
|
444
444
|
// NEWSLETTER_SOURCE='$parent' (default behavior, explicit)
|
|
445
445
|
const useResolverSources = !!env.NEWSLETTER_SOURCE;
|
|
@@ -455,7 +455,6 @@ module.exports = {
|
|
|
455
455
|
{ name: `${config.brand?.name || 'Brand'} Newsletter — Iteration ${stamp}` },
|
|
456
456
|
{
|
|
457
457
|
sources: useResolverSources ? undefined : sources,
|
|
458
|
-
skipClaim: true, // We manage the claim/release lifecycle ourselves
|
|
459
458
|
skipImages: !!env.NEWSLETTER_NO_IMAGES,
|
|
460
459
|
// The article is GENERATED whenever the brand's config.article.enabled is on
|
|
461
460
|
// (exercises the Ghostii write + URL/CTA path), but only PUBLISHED to the
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Test: content/blog-auto-publisher
|
|
3
|
-
* Tests for the
|
|
4
|
-
*
|
|
3
|
+
* Tests for the shared source-resolver (used by both blog + newsletter crons):
|
|
4
|
+
* source type detection, Firestore tracking, hash determinism, and the
|
|
5
|
+
* unified pick/fallback resolution.
|
|
5
6
|
*
|
|
6
7
|
* Run: npx mgr test helpers/content/blog-auto-publisher
|
|
7
8
|
*
|
|
@@ -9,34 +10,8 @@
|
|
|
9
10
|
* processing and Firestore tracking tests run against the real emulator.
|
|
10
11
|
*/
|
|
11
12
|
const path = require('path');
|
|
12
|
-
const
|
|
13
|
-
const { contentSourceHash, isURL } = require(
|
|
14
|
-
|
|
15
|
-
// --- Sample feed XML for testing ---
|
|
16
|
-
const SAMPLE_RSS = `<?xml version="1.0"?>
|
|
17
|
-
<rss version="2.0">
|
|
18
|
-
<channel>
|
|
19
|
-
<title>Test Feed</title>
|
|
20
|
-
<item>
|
|
21
|
-
<guid>feed-item-1</guid>
|
|
22
|
-
<title>Feed Article One</title>
|
|
23
|
-
<link>https://example.com/article-1</link>
|
|
24
|
-
<description>Description of article one.</description>
|
|
25
|
-
</item>
|
|
26
|
-
<item>
|
|
27
|
-
<guid>feed-item-2</guid>
|
|
28
|
-
<title>Feed Article Two</title>
|
|
29
|
-
<link>https://example.com/article-2</link>
|
|
30
|
-
<description>Description of article two.</description>
|
|
31
|
-
</item>
|
|
32
|
-
<item>
|
|
33
|
-
<guid>feed-item-3</guid>
|
|
34
|
-
<title>Feed Article Three</title>
|
|
35
|
-
<link>https://example.com/article-3</link>
|
|
36
|
-
<description>Description of article three.</description>
|
|
37
|
-
</item>
|
|
38
|
-
</channel>
|
|
39
|
-
</rss>`;
|
|
13
|
+
const resolverPath = path.resolve(__dirname, '../../../src/manager/libraries/content/source-resolver.js');
|
|
14
|
+
const { contentSourceHash, isURL } = require(resolverPath);
|
|
40
15
|
|
|
41
16
|
module.exports = {
|
|
42
17
|
description: 'content/blog-auto-publisher',
|
|
@@ -179,7 +154,7 @@ module.exports = {
|
|
|
179
154
|
return assert.ok(true, 'skipped: no emulator');
|
|
180
155
|
}
|
|
181
156
|
|
|
182
|
-
const { trackContentSource } = require(
|
|
157
|
+
const { trackContentSource } = require(resolverPath);
|
|
183
158
|
const origin = '$feed:https://test-feed.com/rss';
|
|
184
159
|
const url = 'https://test-feed.com/article-1';
|
|
185
160
|
const docId = contentSourceHash(origin, url);
|
|
@@ -226,7 +201,7 @@ module.exports = {
|
|
|
226
201
|
return assert.ok(true, 'skipped: no emulator');
|
|
227
202
|
}
|
|
228
203
|
|
|
229
|
-
const { trackContentSource } = require(
|
|
204
|
+
const { trackContentSource } = require(resolverPath);
|
|
230
205
|
const origin = '$parent';
|
|
231
206
|
const url = 'https://parent-server.com/source-42';
|
|
232
207
|
const docId = contentSourceHash(origin, url);
|
|
@@ -256,7 +231,7 @@ module.exports = {
|
|
|
256
231
|
return assert.ok(true, 'skipped: no emulator');
|
|
257
232
|
}
|
|
258
233
|
|
|
259
|
-
const { getProcessedItemIds, trackContentSource } = require(
|
|
234
|
+
const { getProcessedItemIds, trackContentSource } = require(resolverPath);
|
|
260
235
|
const feedUrl = 'https://processed-test.com/rss';
|
|
261
236
|
const origin = `$feed:${feedUrl}`;
|
|
262
237
|
|
|
@@ -294,7 +269,7 @@ module.exports = {
|
|
|
294
269
|
return assert.ok(true, 'skipped: no emulator');
|
|
295
270
|
}
|
|
296
271
|
|
|
297
|
-
const { getProcessedItemIds, trackContentSource } = require(
|
|
272
|
+
const { getProcessedItemIds, trackContentSource } = require(resolverPath);
|
|
298
273
|
|
|
299
274
|
// Track item in feed A
|
|
300
275
|
await trackContentSource(admin, {
|
|
@@ -316,132 +291,141 @@ module.exports = {
|
|
|
316
291
|
{
|
|
317
292
|
name: 'getProcessedItemIds-returns-empty-set-without-admin',
|
|
318
293
|
async run({ assert }) {
|
|
319
|
-
const { getProcessedItemIds } = require(
|
|
294
|
+
const { getProcessedItemIds } = require(resolverPath);
|
|
320
295
|
const ids = await getProcessedItemIds(null, 'https://example.com/feed');
|
|
321
296
|
assert.equal(ids.size, 0, 'returns empty Set when admin is null');
|
|
322
297
|
},
|
|
323
298
|
},
|
|
324
299
|
|
|
325
300
|
// ============================
|
|
326
|
-
// SOURCE RESOLUTION (
|
|
301
|
+
// UNIFIED SOURCE RESOLUTION (resolveSources / resolvePick)
|
|
327
302
|
// ============================
|
|
328
303
|
{
|
|
329
|
-
name: '
|
|
330
|
-
async run({ assert }) {
|
|
331
|
-
const {
|
|
332
|
-
const
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
assert.
|
|
343
|
-
assert.ok(result.description.includes('Focus on AI'), 'description includes instructions');
|
|
344
|
-
assert.equal(result.sourceContent, '', 'no sourceContent for $brand');
|
|
345
|
-
assert.equal(result.trackingData, undefined, 'no trackingData for $brand');
|
|
304
|
+
name: 'resolveSources-brand-pick-resolves-seed',
|
|
305
|
+
async run({ assert, Manager }) {
|
|
306
|
+
const { resolveSources } = require(resolverPath);
|
|
307
|
+
const assistant = Manager.Assistant();
|
|
308
|
+
|
|
309
|
+
const resolved = await resolveSources({
|
|
310
|
+
sources: ['$brand'],
|
|
311
|
+
count: 1,
|
|
312
|
+
assistant,
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
assert.equal(resolved.length, 1, 'one source resolved');
|
|
316
|
+
assert.equal(resolved[0].type, 'brand', 'type is brand');
|
|
317
|
+
assert.equal(resolved[0].trackingData, null, 'no trackingData for $brand');
|
|
346
318
|
},
|
|
347
319
|
},
|
|
348
320
|
|
|
349
321
|
{
|
|
350
|
-
name: '
|
|
351
|
-
async run({ assert }) {
|
|
352
|
-
const {
|
|
353
|
-
const
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
assert.
|
|
364
|
-
assert.
|
|
365
|
-
assert.ok(result.description.includes('growth, engagement'), 'description includes keywords');
|
|
322
|
+
name: 'resolveSources-text-pick-resolves-content',
|
|
323
|
+
async run({ assert, Manager }) {
|
|
324
|
+
const { resolveSources } = require(resolverPath);
|
|
325
|
+
const assistant = Manager.Assistant();
|
|
326
|
+
|
|
327
|
+
const resolved = await resolveSources({
|
|
328
|
+
sources: ['Write about blockchain technology'],
|
|
329
|
+
count: 1,
|
|
330
|
+
assistant,
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
assert.equal(resolved.length, 1, 'one source resolved');
|
|
334
|
+
assert.equal(resolved[0].type, 'text', 'type is text');
|
|
335
|
+
assert.equal(resolved[0].content, 'Write about blockchain technology', 'content is the text seed');
|
|
336
|
+
assert.equal(resolved[0].trackingData, null, 'no trackingData for text');
|
|
366
337
|
},
|
|
367
338
|
},
|
|
368
339
|
|
|
369
340
|
{
|
|
370
|
-
name: '
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
const
|
|
374
|
-
const
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
assert.equal(result
|
|
341
|
+
name: 'resolvePick-feed-failure-never-falls-back-to-brand',
|
|
342
|
+
timeout: 30000,
|
|
343
|
+
async run({ assert, Manager }) {
|
|
344
|
+
const { createResolverState, resolvePick } = require(resolverPath);
|
|
345
|
+
const assistant = Manager.Assistant();
|
|
346
|
+
|
|
347
|
+
// $brand IS listed in the pool — the fallback chain must STILL never
|
|
348
|
+
// land on it. Feed fails (nonexistent domain), no $parent listed → null.
|
|
349
|
+
const state = createResolverState({
|
|
350
|
+
sources: ['$feed:https://nonexistent-feed.invalid/feed.xml', '$brand'],
|
|
351
|
+
assistant,
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
const result = await resolvePick(state, '$feed:https://nonexistent-feed.invalid/feed.xml');
|
|
355
|
+
assert.equal(result, null, 'feed failure returns null — never falls back to $brand');
|
|
385
356
|
},
|
|
386
357
|
},
|
|
387
358
|
|
|
388
359
|
{
|
|
389
|
-
name: '
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
const
|
|
393
|
-
const
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
360
|
+
name: 'resolvePick-feed-falls-back-to-other-feeds-then-parent',
|
|
361
|
+
timeout: 30000,
|
|
362
|
+
async run({ assert, Manager }) {
|
|
363
|
+
const { createResolverState, resolvePick } = require(resolverPath);
|
|
364
|
+
const assistant = Manager.Assistant();
|
|
365
|
+
|
|
366
|
+
// Both feeds dead, $parent listed but no Manager → parent unreachable → null.
|
|
367
|
+
// Exercises the full chain (same feed → other feeds → parent) without throwing.
|
|
368
|
+
const state = createResolverState({
|
|
369
|
+
sources: [
|
|
370
|
+
'$feed:https://nonexistent-a.invalid/feed.xml',
|
|
371
|
+
'$feed:https://nonexistent-b.invalid/feed.xml',
|
|
372
|
+
'$parent',
|
|
373
|
+
],
|
|
374
|
+
assistant,
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
const result = await resolvePick(state, '$feed:https://nonexistent-a.invalid/feed.xml');
|
|
378
|
+
assert.equal(result, null, 'chain exhausts feeds then parent, returns null');
|
|
379
|
+
assert.ok(state.feedCache.has('https://nonexistent-a.invalid/feed.xml'), 'first feed was tried');
|
|
380
|
+
assert.ok(state.feedCache.has('https://nonexistent-b.invalid/feed.xml'), 'second feed was tried as fallback');
|
|
381
|
+
assert.ok(Array.isArray(state.parentPool), 'parent pool was attempted after feeds');
|
|
404
382
|
},
|
|
405
383
|
},
|
|
406
384
|
|
|
407
385
|
{
|
|
408
|
-
name: '
|
|
409
|
-
async run({ assert }) {
|
|
410
|
-
const {
|
|
411
|
-
const
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
};
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
assert.
|
|
423
|
-
assert.equal(result.sourceContent, '', 'no sourceContent on fallback');
|
|
386
|
+
name: 'resolvePick-parent-only-falls-back-to-parent',
|
|
387
|
+
async run({ assert, Manager }) {
|
|
388
|
+
const { createResolverState, resolvePick } = require(resolverPath);
|
|
389
|
+
const assistant = Manager.Assistant();
|
|
390
|
+
|
|
391
|
+
// Parent unreachable (no Manager). Feeds ARE listed — but $parent must
|
|
392
|
+
// NOT fall back to them.
|
|
393
|
+
const state = createResolverState({
|
|
394
|
+
sources: ['$parent', '$feed:https://nonexistent.invalid/feed.xml', '$brand'],
|
|
395
|
+
assistant,
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
const result = await resolvePick(state, '$parent');
|
|
399
|
+
assert.equal(result, null, 'parent failure returns null — never falls to feeds or $brand');
|
|
400
|
+
assert.equal(state.feedCache.size, 0, 'no feed was touched by the parent chain');
|
|
424
401
|
},
|
|
425
402
|
},
|
|
426
403
|
|
|
427
404
|
{
|
|
428
|
-
name: '
|
|
429
|
-
async run({ assert }) {
|
|
430
|
-
const {
|
|
431
|
-
const
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
const
|
|
443
|
-
|
|
444
|
-
|
|
405
|
+
name: 'resolveSources-empty-pool-returns-empty',
|
|
406
|
+
async run({ assert, Manager }) {
|
|
407
|
+
const { resolveSources } = require(resolverPath);
|
|
408
|
+
const assistant = Manager.Assistant();
|
|
409
|
+
|
|
410
|
+
const resolved = await resolveSources({ sources: [], count: 3, assistant });
|
|
411
|
+
assert.equal(resolved.length, 0, 'empty pool resolves nothing');
|
|
412
|
+
},
|
|
413
|
+
},
|
|
414
|
+
|
|
415
|
+
{
|
|
416
|
+
name: 'resolveSources-dead-feed-only-pool-returns-empty',
|
|
417
|
+
timeout: 30000,
|
|
418
|
+
async run({ assert, Manager }) {
|
|
419
|
+
const { resolveSources } = require(resolverPath);
|
|
420
|
+
const assistant = Manager.Assistant();
|
|
421
|
+
|
|
422
|
+
const resolved = await resolveSources({
|
|
423
|
+
sources: ['$feed:https://nonexistent.invalid/feed.xml'],
|
|
424
|
+
count: 2,
|
|
425
|
+
assistant,
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
assert.equal(resolved.length, 0, 'dead-feed-only pool resolves nothing (no $brand invented)');
|
|
445
429
|
},
|
|
446
430
|
},
|
|
447
431
|
|
|
@@ -13,9 +13,9 @@
|
|
|
13
13
|
const path = require('path');
|
|
14
14
|
const jetpack = require('fs-jetpack');
|
|
15
15
|
const fetch = require('wonderful-fetch');
|
|
16
|
-
const
|
|
16
|
+
const resolverPath = path.resolve(__dirname, '../../../src/manager/libraries/content/source-resolver.js');
|
|
17
17
|
const { parseFeed, extractArticleContent } = require('../../../src/manager/libraries/content/feed-parser.js');
|
|
18
|
-
const { contentSourceHash, getProcessedItemIds, trackContentSource } = require(
|
|
18
|
+
const { contentSourceHash, getProcessedItemIds, trackContentSource } = require(resolverPath);
|
|
19
19
|
|
|
20
20
|
const EXTENDED = !!process.env.TEST_EXTENDED_MODE;
|
|
21
21
|
const USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36';
|