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.
@@ -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
- Jun 30, 2026 2:47:09 AM com.google.cloud.datastore.emulator.firestore.websocket.WebSocketServer start
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
- Jun 30, 2026 2:47:19 AM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
23
+ Jul 01, 2026 10:16:12 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
24
24
  INFO: Detected non-HTTP/2 connection.
25
- Jun 30, 2026 2:47:20 AM io.grpc.netty.TcpMetrics loadEpollInfo
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
- Jun 30, 2026 2:47:20 AM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
27
+ Jul 01, 2026 10:16:12 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
28
28
  INFO: Detected HTTP/2 connection.
29
- Jun 30, 2026 2:47:21 AM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
29
+ Jul 01, 2026 10:16:14 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
30
30
  INFO: Detected HTTP/2 connection.
31
- Jun 30, 2026 2:47:21 AM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
31
+ Jul 01, 2026 10:16:14 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
32
32
  INFO: Detected HTTP/2 connection.
33
- Jun 30, 2026 2:47:30 AM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
33
+ Jul 01, 2026 10:16:23 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
34
34
  INFO: Detected HTTP/2 connection.
35
- Jun 30, 2026 2:47:30 AM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
35
+ Jul 01, 2026 10:16:23 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
36
36
  INFO: Detected HTTP/2 connection.
37
- Jun 30, 2026 2:47:30 AM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
37
+ Jul 01, 2026 10:16:24 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
38
38
  INFO: Detected HTTP/2 connection.
39
- Jun 30, 2026 2:47:30 AM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
39
+ Jul 01, 2026 10:16:24 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
40
40
  INFO: Detected HTTP/2 connection.
41
- Jun 30, 2026 2:47:30 AM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
41
+ Jul 01, 2026 10:16:24 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
42
42
  INFO: Detected HTTP/2 connection.
43
- Jun 30, 2026 2:47:31 AM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
43
+ Jul 01, 2026 10:16:24 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
44
44
  INFO: Detected HTTP/2 connection.
45
- Jun 30, 2026 2:47:31 AM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
45
+ Jul 01, 2026 10:16:24 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
46
46
  INFO: Detected HTTP/2 connection.
47
- Jun 30, 2026 2:47:31 AM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
47
+ Jul 01, 2026 10:16:24 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
48
48
  INFO: Detected HTTP/2 connection.
49
- Jun 30, 2026 2:47:31 AM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
49
+ Jul 01, 2026 10:16:24 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
50
50
  INFO: Detected HTTP/2 connection.
51
- Jun 30, 2026 2:47:31 AM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
51
+ Jul 01, 2026 10:16:24 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
52
52
  INFO: Detected HTTP/2 connection.
53
- Jun 30, 2026 2:47:31 AM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
53
+ Jul 01, 2026 10:16:24 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
54
54
  INFO: Detected HTTP/2 connection.
55
- Jun 30, 2026 2:47:31 AM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
55
+ Jul 01, 2026 10:16:24 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
56
56
  INFO: Detected HTTP/2 connection.
57
- Jun 30, 2026 2:47:31 AM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
57
+ Jul 01, 2026 10:16:24 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
58
58
  INFO: Detected HTTP/2 connection.
59
- Jun 30, 2026 2:47:31 AM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
59
+ Jul 01, 2026 10:16:24 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
60
60
  INFO: Detected HTTP/2 connection.
61
- Jun 30, 2026 2:47:31 AM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
61
+ Jul 01, 2026 10:16:24 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
62
62
  INFO: Detected HTTP/2 connection.
63
- Jun 30, 2026 2:47:31 AM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
63
+ Jul 01, 2026 10:16:24 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
64
64
  INFO: Detected HTTP/2 connection.
65
- Jun 30, 2026 2:47:31 AM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
65
+ Jul 01, 2026 10:16:24 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
66
66
  INFO: Detected HTTP/2 connection.
67
- Jun 30, 2026 2:47:31 AM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
67
+ Jul 01, 2026 10:16:24 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
68
68
  INFO: Detected HTTP/2 connection.
69
- Jun 30, 2026 2:47:31 AM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
69
+ Jul 01, 2026 10:16:24 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
70
70
  INFO: Detected HTTP/2 connection.
71
- Jun 30, 2026 2:47:31 AM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
71
+ Jul 01, 2026 10:16:24 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
72
72
  INFO: Detected HTTP/2 connection.
73
- Jun 30, 2026 2:47:31 AM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
73
+ Jul 01, 2026 10:16:24 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
74
74
  INFO: Detected HTTP/2 connection.
75
- Jun 30, 2026 2:47:31 AM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
75
+ Jul 01, 2026 10:16:24 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
76
76
  INFO: Detected HTTP/2 connection.
77
- Jun 30, 2026 2:47:31 AM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
77
+ Jul 01, 2026 10:16:24 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
78
78
  INFO: Detected HTTP/2 connection.
79
- Jun 30, 2026 2:47:31 AM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
79
+ Jul 01, 2026 10:16:24 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
80
80
  INFO: Detected HTTP/2 connection.
81
- Jun 30, 2026 2:47:31 AM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
81
+ Jul 01, 2026 10:16:24 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
82
82
  INFO: Detected HTTP/2 connection.
83
- Jun 30, 2026 2:47:31 AM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
83
+ Jul 01, 2026 10:16:24 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
84
84
  INFO: Detected HTTP/2 connection.
85
- Jun 30, 2026 2:47:31 AM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
85
+ Jul 01, 2026 10:16:24 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
86
86
  INFO: Detected HTTP/2 connection.
87
- Jun 30, 2026 2:47:31 AM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
87
+ Jul 01, 2026 10:16:24 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
88
88
  INFO: Detected HTTP/2 connection.
89
- Jun 30, 2026 2:47:31 AM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
89
+ Jul 01, 2026 10:16:24 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
90
90
  INFO: Detected HTTP/2 connection.
91
- Jun 30, 2026 2:47:31 AM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
91
+ Jul 01, 2026 10:16:24 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
92
92
  INFO: Detected HTTP/2 connection.
93
- Jun 30, 2026 2:47:31 AM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
93
+ Jul 01, 2026 10:16:24 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
94
94
  INFO: Detected HTTP/2 connection.
95
- Jun 30, 2026 2:47:31 AM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
95
+ Jul 01, 2026 10:16:24 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
96
96
  INFO: Detected HTTP/2 connection.
97
- Jun 30, 2026 2:47:31 AM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
97
+ Jul 01, 2026 10:16:24 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
98
98
  INFO: Detected HTTP/2 connection.
99
- Jun 30, 2026 2:47:31 AM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
99
+ Jul 01, 2026 10:16:24 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
100
100
  INFO: Detected HTTP/2 connection.
101
- Jun 30, 2026 2:47:31 AM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
101
+ Jul 01, 2026 10:16:24 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
102
102
  INFO: Detected HTTP/2 connection.
103
- Jun 30, 2026 2:47:31 AM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
103
+ Jul 01, 2026 10:16:24 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
104
104
  INFO: Detected HTTP/2 connection.
105
- Jun 30, 2026 2:47:31 AM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
105
+ Jul 01, 2026 10:16:24 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
106
106
  INFO: Detected HTTP/2 connection.
107
- Jun 30, 2026 2:47:31 AM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
107
+ Jul 01, 2026 10:16:24 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
108
108
  INFO: Detected HTTP/2 connection.
109
- Jun 30, 2026 2:47:31 AM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
109
+ Jul 01, 2026 10:16:24 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
110
110
  INFO: Detected HTTP/2 connection.
111
- Jun 30, 2026 2:47:31 AM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
111
+ Jul 01, 2026 10:16:24 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
112
112
  INFO: Detected HTTP/2 connection.
113
- Jun 30, 2026 2:47:31 AM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
113
+ Jul 01, 2026 10:16:24 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
114
114
  INFO: Detected HTTP/2 connection.
115
- Jun 30, 2026 2:47:31 AM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
115
+ Jul 01, 2026 10:16:24 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
116
116
  INFO: Detected HTTP/2 connection.
117
- Jun 30, 2026 2:47:31 AM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
117
+ Jul 01, 2026 10:16:25 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
118
118
  INFO: Detected HTTP/2 connection.
119
- Jun 30, 2026 2:47:31 AM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
119
+ Jul 01, 2026 10:16:25 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
120
120
  INFO: Detected HTTP/2 connection.
121
- Jun 30, 2026 2:47:31 AM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
121
+ Jul 01, 2026 10:16:25 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
122
122
  INFO: Detected HTTP/2 connection.
123
- Jun 30, 2026 2:47:31 AM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
123
+ Jul 01, 2026 10:16:25 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
124
124
  INFO: Detected HTTP/2 connection.
125
- Jun 30, 2026 2:47:31 AM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
125
+ Jul 01, 2026 10:16:25 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
126
126
  INFO: Detected HTTP/2 connection.
127
- Jun 30, 2026 2:47:31 AM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
127
+ Jul 01, 2026 10:16:25 PM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
128
128
  INFO: Detected HTTP/2 connection.
129
- Jun 30, 2026 2:47:31 AM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
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
- Jun 30, 2026 2:47:13 AM com.google.cloud.pubsub.testing.v1.Main main
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
- Jun 30, 2026 2:47:14 AM com.google.cloud.pubsub.testing.v1.Main main
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
- Jun 30, 2026 2:47:20 AM io.gapi.emulators.netty.HttpVersionRoutingHandler channelRead
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 (resolveNewsletterSources) handles everything.
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 (resolveNewsletterSources). Without
441
- // this, the test always passes pre-fetched parent sources via opts.sources,
442
- // bypassing feed resolution entirely.
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 blog-auto-publisher cron: source type detection,
4
- * feed processing, Firestore tracking, hash determinism, and source resolution.
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 publisherPath = path.resolve(__dirname, '../../../src/manager/events/cron/daily/blog-auto-publisher.js');
13
- const { contentSourceHash, isURL } = require(publisherPath);
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(publisherPath);
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(publisherPath);
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(publisherPath);
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(publisherPath);
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(publisherPath);
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 (resolveSource)
301
+ // UNIFIED SOURCE RESOLUTION (resolveSources / resolvePick)
327
302
  // ============================
328
303
  {
329
- name: 'resolveSource-brand-returns-description-no-sourceContent',
330
- async run({ assert }) {
331
- const { resolveSource } = require(publisherPath);
332
- const mockAssistant = { log() {}, error() {} };
333
- const entry = {
334
- brand: { brand: { name: 'Test', description: 'A test brand', id: 'test' } },
335
- instructions: 'Focus on AI',
336
- tone: 'professional',
337
- categories: ['tech'],
338
- keywords: ['AI', 'automation'],
339
- };
340
-
341
- const result = await resolveSource(mockAssistant, '$brand', entry, null, null);
342
- assert.ok(result.description.includes('Test'), 'description includes brand name');
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: 'resolveSource-brand-includes-tone-and-keywords',
351
- async run({ assert }) {
352
- const { resolveSource } = require(publisherPath);
353
- const mockAssistant = { log() {}, error() {} };
354
- const entry = {
355
- brand: { brand: { name: 'Test', description: 'A test brand', id: 'test' } },
356
- instructions: '',
357
- tone: 'casual',
358
- categories: ['marketing', 'social-media'],
359
- keywords: ['growth', 'engagement'],
360
- };
361
-
362
- const result = await resolveSource(mockAssistant, '$brand', entry, null, null);
363
- assert.ok(result.description.includes('casual'), 'description includes tone');
364
- assert.ok(result.description.includes('marketing, social-media'), 'description includes categories');
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: 'resolveSource-text-returns-suggestion-in-description',
371
- async run({ assert }) {
372
- const { resolveSource } = require(publisherPath);
373
- const mockAssistant = { log() {}, error() {} };
374
- const entry = {
375
- brand: { brand: { name: 'Test', description: 'A test brand', id: 'test' } },
376
- instructions: '',
377
- tone: 'professional',
378
- categories: [],
379
- keywords: [],
380
- };
381
-
382
- const result = await resolveSource(mockAssistant, 'Write about blockchain technology', entry, null, null);
383
- assert.ok(result.description.includes('blockchain technology'), 'text source in description');
384
- assert.equal(result.sourceContent, '', 'no sourceContent for text');
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: 'resolveSource-feed-falls-back-to-brand-without-admin',
390
- async run({ assert }) {
391
- const { resolveSource } = require(publisherPath);
392
- const mockAssistant = { log() {}, error() {} };
393
- const entry = {
394
- brand: { brand: { name: 'Test', description: 'A test brand', id: 'test' } },
395
- instructions: '',
396
- tone: 'professional',
397
- categories: [],
398
- keywords: [],
399
- };
400
-
401
- const result = await resolveSource(mockAssistant, '$feed:https://nonexistent.example.com/feed', entry, null, null);
402
- assert.ok(result.description, 'falls back to $brand and returns description');
403
- assert.equal(result.sourceContent, '', 'no sourceContent on fallback');
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: 'resolveSource-parent-falls-back-to-brand-without-manager',
409
- async run({ assert }) {
410
- const { resolveSource } = require(publisherPath);
411
- const mockAssistant = { log() {}, error() {} };
412
- const entry = {
413
- brand: { brand: { name: 'Test', description: 'A test brand', id: 'test' } },
414
- instructions: '',
415
- tone: 'professional',
416
- categories: [],
417
- keywords: [],
418
- };
419
-
420
- // Manager is null, so getParentApiUrl() can't be called — falls back to $brand
421
- const result = await resolveSource(mockAssistant, '$parent', entry, null, null);
422
- assert.ok(result.description, 'falls back to $brand and returns description');
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: 'resolveSource-parent-falls-back-when-no-parent-url',
429
- async run({ assert }) {
430
- const { resolveSource } = require(publisherPath);
431
- const mockAssistant = { log() {}, error() {} };
432
- const entry = {
433
- brand: { brand: { name: 'Test', description: 'A test brand', id: 'test' } },
434
- instructions: '',
435
- tone: 'professional',
436
- categories: [],
437
- keywords: [],
438
- };
439
-
440
- // Manager with no parent URL configured
441
- const mockManager = { getParentApiUrl: () => null };
442
- const result = await resolveSource(mockAssistant, '$parent', entry, null, mockManager);
443
- assert.ok(result.description, 'falls back to $brand when no parent URL');
444
- assert.equal(result.sourceContent, '', 'no sourceContent on fallback');
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 publisherPath = path.resolve(__dirname, '../../../src/manager/events/cron/daily/blog-auto-publisher.js');
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(publisherPath);
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';