@alumbwe/anvil 1.0.42 → 1.0.50
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/bin/anvil.js +18 -0
- package/package.json +13 -6
- package/src/agents/bundled-agents.generated.ts +1 -1
- package/src/index.tsx +27 -4
- package/src/login/plain-login.ts +60 -0
- package/src/login/supabase-auth.ts +346 -0
- package/src/utils/anvil-client.ts +4 -0
- package/src/utils/auth.ts +16 -4
- package/src/utils/config-dir.ts +1 -1
- package/vendor/@anvil/common/src/constants/deepseek-direct.ts +30 -1
- package/vendor/@anvil/common/src/util/project-ignore.ts +4 -3
- package/vendor/@anvil/sdk/dist/index.cjs +46 -10
- package/vendor/@anvil/sdk/dist/index.mjs +46 -10
- package/src/commands/__tests__/anvil-command-aliases.test.ts +0 -56
- package/src/commands/__tests__/bash-command.test.ts +0 -445
- package/src/commands/__tests__/command-args.test.ts +0 -349
- package/src/commands/__tests__/copy-conversation-clipboard.test.ts +0 -108
- package/src/commands/__tests__/copy-conversation.test.ts +0 -226
- package/src/commands/__tests__/diagnostics-command.test.ts +0 -35
- package/src/commands/__tests__/image.test.ts +0 -99
- package/src/commands/__tests__/init.test.ts +0 -456
- package/src/commands/__tests__/new-command-rotates-chat.test.ts +0 -91
- package/src/commands/__tests__/process-diagnostics.test.ts +0 -54
- package/src/commands/__tests__/prompt-builders.test.ts +0 -28
- package/src/commands/__tests__/router-input.test.ts +0 -298
- package/src/components/__tests__/ad-banner.test.tsx +0 -83
- package/src/components/__tests__/anvil-landing-heading-row.test.tsx +0 -91
- package/src/components/__tests__/anvil-model-selector.test.tsx +0 -519
- package/src/components/__tests__/anvil-offer-invariants.test.ts +0 -54
- package/src/components/__tests__/anvil-takeover-prompt.test.tsx +0 -212
- package/src/components/__tests__/chat-input-bar.test.tsx +0 -130
- package/src/components/__tests__/grid-layout.integration.test.tsx +0 -304
- package/src/components/__tests__/grid-layout.test.tsx +0 -1051
- package/src/components/__tests__/message-block.completion.test.tsx +0 -104
- package/src/components/__tests__/message-block.streaming.test.tsx +0 -78
- package/src/components/__tests__/message-with-agents.test.tsx +0 -579
- package/src/components/__tests__/multiline-input.test.tsx +0 -1132
- package/src/components/__tests__/queue-panel.test.tsx +0 -398
- package/src/components/__tests__/selectable-list.test.ts +0 -232
- package/src/components/__tests__/status-bar.test.tsx +0 -44
- package/src/components/__tests__/status-indicator.test.tsx +0 -204
- package/src/components/__tests__/terminal-thinking-orb.test.tsx +0 -111
- package/src/components/__tests__/user-error-banner.test.tsx +0 -102
- package/src/components/ask-user/__tests__/multiple-choice-form.test.ts +0 -432
- package/src/components/ask-user/__tests__/validation.test.ts +0 -213
- package/src/components/tools/__tests__/apply-patch.test.tsx +0 -84
- package/src/components/tools/__tests__/code-search.test.tsx +0 -45
- package/src/components/tools/__tests__/gravity-index.test.ts +0 -76
- package/src/components/tools/__tests__/render-ui.test.tsx +0 -92
- package/src/components/tools/__tests__/run-terminal-command.test.ts +0 -233
- package/src/contexts/__tests__/chat-runtime-context.test.tsx +0 -149
- package/src/data/slash-commands.test.ts +0 -24
- package/src/hooks/__tests__/holds-live-anvil-slot.test.ts +0 -49
- package/src/hooks/__tests__/session-fetch-signal.test.ts +0 -63
- package/src/hooks/__tests__/use-activity-query.test.ts +0 -1000
- package/src/hooks/__tests__/use-ask-user-bridge.test.ts +0 -176
- package/src/hooks/__tests__/use-auth-query.test.ts +0 -21
- package/src/hooks/__tests__/use-chat-input-remount.test.tsx +0 -66
- package/src/hooks/__tests__/use-clipboard.test.tsx +0 -73
- package/src/hooks/__tests__/use-connection-status.test.ts +0 -112
- package/src/hooks/__tests__/use-directory-browser.test.ts +0 -203
- package/src/hooks/__tests__/use-gravity-ad.test.ts +0 -73
- package/src/hooks/__tests__/use-grid-layout.test.ts +0 -349
- package/src/hooks/__tests__/use-input-history.test.ts +0 -699
- package/src/hooks/__tests__/use-message-queue-editing.test.tsx +0 -209
- package/src/hooks/__tests__/use-message-queue.test.tsx +0 -71
- package/src/hooks/__tests__/use-path-tab-completion.test.ts +0 -315
- package/src/hooks/__tests__/use-queue-controls.test.ts +0 -68
- package/src/hooks/__tests__/use-queue-ui.test.tsx +0 -83
- package/src/hooks/__tests__/use-searchable-list.test.ts +0 -359
- package/src/hooks/__tests__/use-send-message-timer.test.ts +0 -146
- package/src/hooks/__tests__/use-suggestion-engine-mention.test.ts +0 -361
- package/src/hooks/__tests__/use-suggestion-engine.test.ts +0 -485
- package/src/hooks/__tests__/use-terminal-focus.test.ts +0 -66
- package/src/hooks/__tests__/use-terminal-layout.test.ts +0 -675
- package/src/hooks/__tests__/use-timeout.test.ts +0 -330
- package/src/hooks/__tests__/use-usage-query.test.ts +0 -502
- package/src/hooks/__tests__/use-user-details-query.test.ts +0 -195
- package/src/hooks/helpers/__tests__/send-message.test.ts +0 -1918
- package/src/init/__tests__/init-direnv.test.ts +0 -526
- package/src/state/__tests__/chat-store-focus.test.ts +0 -22
- package/src/state/__tests__/feedback-store.test.ts +0 -257
- package/src/utils/__tests__/active-run.test.ts +0 -140
- package/src/utils/__tests__/activity-tracker.test.ts +0 -440
- package/src/utils/__tests__/agent-display.test.ts +0 -139
- package/src/utils/__tests__/analytics-client.test.ts +0 -262
- package/src/utils/__tests__/anonymous-id.test.ts +0 -74
- package/src/utils/__tests__/anvil-api.test.ts +0 -578
- package/src/utils/__tests__/anvil-instance-owner.test.ts +0 -69
- package/src/utils/__tests__/anvil-model-navigation.test.ts +0 -102
- package/src/utils/__tests__/anvil-premium-reset.test.ts +0 -79
- package/src/utils/__tests__/anvil-referral-cache.test.ts +0 -116
- package/src/utils/__tests__/anvil-session-api.test.ts +0 -215
- package/src/utils/__tests__/anvil-session-display.test.ts +0 -23
- package/src/utils/__tests__/anvil-streak-line.test.ts +0 -217
- package/src/utils/__tests__/arrays.test.ts +0 -264
- package/src/utils/__tests__/bash-context-processor.test.ts +0 -38
- package/src/utils/__tests__/block-processor.test.ts +0 -810
- package/src/utils/__tests__/chat-history.test.ts +0 -203
- package/src/utils/__tests__/chat-input-key-intercept.test.ts +0 -40
- package/src/utils/__tests__/chat-meta.test.ts +0 -130
- package/src/utils/__tests__/clipboard-linux.test.ts +0 -359
- package/src/utils/__tests__/clipboard.test.ts +0 -910
- package/src/utils/__tests__/code-search-summary.test.ts +0 -84
- package/src/utils/__tests__/collapse-helpers.test.ts +0 -1109
- package/src/utils/__tests__/error-handling.test.ts +0 -658
- package/src/utils/__tests__/exit-cleanly.test.ts +0 -110
- package/src/utils/__tests__/feedback-helpers.test.ts +0 -444
- package/src/utils/__tests__/feedback-submission.test.ts +0 -26
- package/src/utils/__tests__/fetch-usage.test.ts +0 -355
- package/src/utils/__tests__/fingerprint.test.ts +0 -144
- package/src/utils/__tests__/fixtures/terminal-command-broker-child.ts +0 -7
- package/src/utils/__tests__/fixtures/terminal-command-broker-entry.ts +0 -3
- package/src/utils/__tests__/fixtures/terminal-command-broker-owner.ts +0 -33
- package/src/utils/__tests__/format-elapsed-time.test.ts +0 -103
- package/src/utils/__tests__/format-timeout.test.ts +0 -87
- package/src/utils/__tests__/image-dimensions.test.ts +0 -249
- package/src/utils/__tests__/image-processor.test.ts +0 -209
- package/src/utils/__tests__/implementor-helpers.test.ts +0 -1458
- package/src/utils/__tests__/keyboard-actions.test.ts +0 -660
- package/src/utils/__tests__/layout-helpers.test.ts +0 -36
- package/src/utils/__tests__/lazy-response-ads.test.ts +0 -167
- package/src/utils/__tests__/log-shipper.test.ts +0 -53
- package/src/utils/__tests__/markdown-renderer.test.tsx +0 -421
- package/src/utils/__tests__/message-block-helpers.test.ts +0 -1263
- package/src/utils/__tests__/message-updater.test.ts +0 -645
- package/src/utils/__tests__/osc-timeout-scenarios.test.ts +0 -172
- package/src/utils/__tests__/pending-attachments.test.ts +0 -277
- package/src/utils/__tests__/polling-backoff.test.ts +0 -51
- package/src/utils/__tests__/queue-panel-actions.test.ts +0 -148
- package/src/utils/__tests__/run-state-storage.test.ts +0 -919
- package/src/utils/__tests__/safe-json.test.ts +0 -77
- package/src/utils/__tests__/sdk-event-handlers.test.ts +0 -685
- package/src/utils/__tests__/send-message-helpers.test.ts +0 -1898
- package/src/utils/__tests__/send-message-timer.test.ts +0 -64
- package/src/utils/__tests__/settings.test.ts +0 -99
- package/src/utils/__tests__/strings.test.ts +0 -192
- package/src/utils/__tests__/terminal-color-detection.test.ts +0 -425
- package/src/utils/__tests__/terminal-command-broker.test.ts +0 -528
- package/src/utils/__tests__/terminal-enter-detection.test.ts +0 -77
- package/src/utils/__tests__/terminal-io.test.ts +0 -137
- package/src/utils/__tests__/terminal-protocol-controller.test.ts +0 -119
- package/src/utils/__tests__/text-layout.test.ts +0 -82
- package/src/utils/__tests__/theme-platform-detection.test.ts +0 -125
- package/src/utils/__tests__/think-tag-parser.test.ts +0 -159
- package/src/utils/__tests__/trace-writer.test.ts +0 -223
- package/src/utils/__tests__/trim-chat-logs.test.ts +0 -78
- package/src/utils/__tests__/usage-banner-state.test.ts +0 -298
- package/src/utils/__tests__/validation-error-formatting.test.ts +0 -126
- package/src/utils/__tests__/windows-terminal-health.test.ts +0 -101
- package/src/utils/__tests__/write-file-atomic.test.ts +0 -127
- package/vendor/@anvil/common/src/__tests__/agent-validation.test.ts +0 -846
- package/vendor/@anvil/common/src/__tests__/anvil-model-availability.test.ts +0 -101
- package/vendor/@anvil/common/src/__tests__/anvil-models.test.ts +0 -1408
- package/vendor/@anvil/common/src/__tests__/anvil-public-data-use-copy.test.ts +0 -258
- package/vendor/@anvil/common/src/__tests__/anvil-referral-tiers.test.ts +0 -48
- package/vendor/@anvil/common/src/__tests__/anvil-spend-ceilings.test.ts +0 -282
- package/vendor/@anvil/common/src/__tests__/anvil-trust.test.ts +0 -526
- package/vendor/@anvil/common/src/__tests__/deepseek-direct.test.ts +0 -68
- package/vendor/@anvil/common/src/__tests__/disposable-email.test.ts +0 -132
- package/vendor/@anvil/common/src/__tests__/dynamic-agent-template-schema.test.ts +0 -422
- package/vendor/@anvil/common/src/__tests__/env-ci.test.ts +0 -167
- package/vendor/@anvil/common/src/__tests__/env-process.test.ts +0 -145
- package/vendor/@anvil/common/src/__tests__/foreign-client-shipped-agents.test.ts +0 -234
- package/vendor/@anvil/common/src/__tests__/foreign-client-signals.test.ts +0 -379
- package/vendor/@anvil/common/src/__tests__/free-agents.test.ts +0 -807
- package/vendor/@anvil/common/src/__tests__/gravity-capi.test.ts +0 -154
- package/vendor/@anvil/common/src/__tests__/handlesteps-parsing.test.ts +0 -246
- package/vendor/@anvil/common/src/__tests__/kimi-k3-god-only.test.ts +0 -77
- package/vendor/@anvil/common/src/__tests__/model-config.test.ts +0 -89
- package/vendor/@anvil/common/src/__tests__/project-file-tree.test.ts +0 -298
- package/vendor/@anvil/common/src/__tests__/provisioned-model-tiers.test.ts +0 -116
- package/vendor/@anvil/common/src/__tests__/reasoning-effort.test.ts +0 -167
- package/vendor/@anvil/common/src/__tests__/reddit-capi.test.ts +0 -132
- package/vendor/@anvil/common/src/__tests__/response-ad-positions.test.ts +0 -72
- package/vendor/@anvil/common/src/__tests__/user-state.test.ts +0 -30
- package/vendor/@anvil/common/src/constants/__tests__/anvil-onboarding-gate.test.ts +0 -63
- package/vendor/@anvil/common/src/constants/__tests__/anvil-onboarding.test.ts +0 -246
- package/vendor/@anvil/common/src/constants/__tests__/cf-worker-signals.test.ts +0 -106
- package/vendor/@anvil/common/src/testing/TESTING_PATTERNS.md +0 -351
- package/vendor/@anvil/common/src/testing/anvil-offer-invariants.ts +0 -169
- package/vendor/@anvil/common/src/testing/errors.ts +0 -33
- package/vendor/@anvil/common/src/testing/fixtures/agent-runtime.ts +0 -334
- package/vendor/@anvil/common/src/testing/impl/agent-runtime.ts +0 -6
- package/vendor/@anvil/common/src/testing/index.ts +0 -84
- package/vendor/@anvil/common/src/testing/mock-modules.ts +0 -53
- package/vendor/@anvil/common/src/testing/mock-types.ts +0 -123
- package/vendor/@anvil/common/src/testing/mocks/analytics.ts +0 -261
- package/vendor/@anvil/common/src/testing/mocks/child-process.ts +0 -93
- package/vendor/@anvil/common/src/testing/mocks/crypto.ts +0 -218
- package/vendor/@anvil/common/src/testing/mocks/database.ts +0 -337
- package/vendor/@anvil/common/src/testing/mocks/fetch.ts +0 -219
- package/vendor/@anvil/common/src/testing/mocks/filesystem.ts +0 -166
- package/vendor/@anvil/common/src/testing/mocks/index.ts +0 -101
- package/vendor/@anvil/common/src/testing/mocks/logger.ts +0 -135
- package/vendor/@anvil/common/src/testing/mocks/stream.ts +0 -313
- package/vendor/@anvil/common/src/testing/mocks/timers.ts +0 -132
- package/vendor/@anvil/common/src/testing/mocks/tree-sitter.ts +0 -127
- package/vendor/@anvil/common/src/testing/setup.ts +0 -282
- package/vendor/@anvil/common/src/tools/__tests__/compile-tool-definitions.test.ts +0 -34
- package/vendor/@anvil/common/src/tools/params/__tests__/coerce-to-array.test.ts +0 -213
- package/vendor/@anvil/common/src/tools/params/__tests__/read-docs.test.ts +0 -47
- package/vendor/@anvil/common/src/tools/params/__tests__/render-ui.test.ts +0 -65
- package/vendor/@anvil/common/src/tools/params/tool/__tests__/run-terminal-command-timeout.test.ts +0 -49
- package/vendor/@anvil/common/src/types/__tests__/dynamic-agent-template.test.ts +0 -20
- package/vendor/@anvil/common/src/util/__tests__/ad-user-agent.test.ts +0 -26
- package/vendor/@anvil/common/src/util/__tests__/analytics-dispatcher.test.ts +0 -122
- package/vendor/@anvil/common/src/util/__tests__/analytics-log.test.ts +0 -102
- package/vendor/@anvil/common/src/util/__tests__/analytics-sampling.test.ts +0 -143
- package/vendor/@anvil/common/src/util/__tests__/anvil-streak.test.ts +0 -202
- package/vendor/@anvil/common/src/util/__tests__/anvil-usage-summary.test.ts +0 -193
- package/vendor/@anvil/common/src/util/__tests__/axiom-only-log.test.ts +0 -140
- package/vendor/@anvil/common/src/util/__tests__/client-user-agent.test.ts +0 -148
- package/vendor/@anvil/common/src/util/__tests__/engagement-tracker.test.ts +0 -115
- package/vendor/@anvil/common/src/util/__tests__/env-file-path.test.ts +0 -57
- package/vendor/@anvil/common/src/util/__tests__/error-abort.test.ts +0 -774
- package/vendor/@anvil/common/src/util/__tests__/error-api-details.test.ts +0 -206
- package/vendor/@anvil/common/src/util/__tests__/file-read-limits.test.ts +0 -173
- package/vendor/@anvil/common/src/util/__tests__/format-code-search.test.ts +0 -60
- package/vendor/@anvil/common/src/util/__tests__/log-mirror.test.ts +0 -31
- package/vendor/@anvil/common/src/util/__tests__/messages.test.ts +0 -1252
- package/vendor/@anvil/common/src/util/__tests__/partial-json-delta.test.ts +0 -505
- package/vendor/@anvil/common/src/util/__tests__/path.test.ts +0 -22
- package/vendor/@anvil/common/src/util/__tests__/project-ignore.test.ts +0 -90
- package/vendor/@anvil/common/src/util/__tests__/promise.test.ts +0 -325
- package/vendor/@anvil/common/src/util/__tests__/rate-limit.test.ts +0 -28
- package/vendor/@anvil/common/src/util/__tests__/reddit-anvil-retention.test.ts +0 -130
- package/vendor/@anvil/common/src/util/__tests__/saxy.test.ts +0 -1008
- package/vendor/@anvil/common/src/util/__tests__/split-data.test.ts +0 -289
- package/vendor/@anvil/common/src/util/__tests__/string.test.ts +0 -239
- package/vendor/@anvil/common/src/util/__tests__/thread-title.test.ts +0 -70
- package/vendor/@anvil/common/src/util/__tests__/tool-result-media-order.test.ts +0 -108
- package/vendor/@anvil/common/src/util/__tests__/ttft-histogram.test.ts +0 -180
- package/vendor/@anvil/common/src/util/__tests__/with-timeout.test.ts +0 -24
- package/vendor/@anvil/common/src/util/__tests__/zoned-time.test.ts +0 -88
- package/vendor/@anvil/common/src/util/skills.test.ts +0 -44
- package/vendor/@anvil/sdk/dist/index.cjs.map +0 -440
- package/vendor/@anvil/sdk/dist/index.mjs.map +0 -440
|
@@ -1,132 +0,0 @@
|
|
|
1
|
-
import { createHash } from 'node:crypto'
|
|
2
|
-
|
|
3
|
-
import { describe, expect, test } from 'bun:test'
|
|
4
|
-
|
|
5
|
-
import {
|
|
6
|
-
buildRedditCustomConversionBody,
|
|
7
|
-
redditConversionId,
|
|
8
|
-
RedditCapiDeliveryError,
|
|
9
|
-
sendRedditCustomConversion,
|
|
10
|
-
} from '../reddit-capi'
|
|
11
|
-
|
|
12
|
-
const sha256 = (value: string) =>
|
|
13
|
-
createHash('sha256').update(value).digest('hex')
|
|
14
|
-
|
|
15
|
-
describe('Reddit CAPI', () => {
|
|
16
|
-
test('builds a documented v3 custom-event payload', () => {
|
|
17
|
-
const body = buildRedditCustomConversionBody({
|
|
18
|
-
customEventName: 'FirstPrompt',
|
|
19
|
-
conversionId: 'conversion-1',
|
|
20
|
-
actionSource: 'WEBSITE',
|
|
21
|
-
eventSourceUrl: 'https://anvil.com/chat?utm_source=reddit',
|
|
22
|
-
testId: 'test-123',
|
|
23
|
-
eventAt: 123,
|
|
24
|
-
user: {
|
|
25
|
-
email: 'Al.ice+Apple@Example.Com',
|
|
26
|
-
externalId: ' user-123 ',
|
|
27
|
-
ipAddress: '203.0.113.10',
|
|
28
|
-
userAgent: 'Browser',
|
|
29
|
-
clickId: 'reddit-click',
|
|
30
|
-
uuid: 'pixel-uuid',
|
|
31
|
-
},
|
|
32
|
-
})
|
|
33
|
-
|
|
34
|
-
expect(body).toEqual({
|
|
35
|
-
data: {
|
|
36
|
-
test_id: 'test-123',
|
|
37
|
-
events: [
|
|
38
|
-
{
|
|
39
|
-
event_at: 123,
|
|
40
|
-
action_source: 'WEBSITE',
|
|
41
|
-
event_source_url:
|
|
42
|
-
'https://anvil.com/chat?utm_source=reddit&rdt_cid=reddit-click',
|
|
43
|
-
type: {
|
|
44
|
-
tracking_type: 'CUSTOM',
|
|
45
|
-
custom_event_name: 'FirstPrompt',
|
|
46
|
-
},
|
|
47
|
-
click_id: 'reddit-click',
|
|
48
|
-
metadata: { conversion_id: 'conversion-1' },
|
|
49
|
-
user: {
|
|
50
|
-
email: sha256('alice@example.com'),
|
|
51
|
-
external_id: sha256('user-123'),
|
|
52
|
-
ip_address: '203.0.113.10',
|
|
53
|
-
user_agent: 'Browser',
|
|
54
|
-
uuid: 'pixel-uuid',
|
|
55
|
-
},
|
|
56
|
-
},
|
|
57
|
-
],
|
|
58
|
-
},
|
|
59
|
-
})
|
|
60
|
-
expect(body.data.events[0]?.user).not.toHaveProperty('click_id')
|
|
61
|
-
expect(body.data).not.toHaveProperty('partner')
|
|
62
|
-
})
|
|
63
|
-
|
|
64
|
-
test('uses a stable opaque conversion id per user and event', () => {
|
|
65
|
-
expect(redditConversionId('FirstPrompt', 'user-123')).toBe(
|
|
66
|
-
sha256('FirstPrompt:user-123'),
|
|
67
|
-
)
|
|
68
|
-
expect(redditConversionId('Retention1d', 'user-123')).not.toBe(
|
|
69
|
-
redditConversionId('FirstPrompt', 'user-123'),
|
|
70
|
-
)
|
|
71
|
-
})
|
|
72
|
-
|
|
73
|
-
test('retries one transient failure', async () => {
|
|
74
|
-
let attempts = 0
|
|
75
|
-
const result = await sendRedditCustomConversion({
|
|
76
|
-
enabled: true,
|
|
77
|
-
accessToken: 'secret',
|
|
78
|
-
customEventName: 'Retention1d',
|
|
79
|
-
conversionId: 'conversion-1',
|
|
80
|
-
actionSource: 'OTHER',
|
|
81
|
-
user: { externalId: 'user-1' },
|
|
82
|
-
sleepImpl: async () => {},
|
|
83
|
-
fetchImpl: (async () => {
|
|
84
|
-
attempts += 1
|
|
85
|
-
return attempts === 1
|
|
86
|
-
? new Response('retry', { status: 503 })
|
|
87
|
-
: Response.json({ data: { message: 'ok' } })
|
|
88
|
-
}) as unknown as typeof fetch,
|
|
89
|
-
})
|
|
90
|
-
|
|
91
|
-
expect(result).toBe('sent')
|
|
92
|
-
expect(attempts).toBe(2)
|
|
93
|
-
})
|
|
94
|
-
|
|
95
|
-
test('does not retry permanent failures', async () => {
|
|
96
|
-
let attempts = 0
|
|
97
|
-
await expect(
|
|
98
|
-
sendRedditCustomConversion({
|
|
99
|
-
enabled: true,
|
|
100
|
-
accessToken: 'secret',
|
|
101
|
-
customEventName: 'FirstPrompt',
|
|
102
|
-
conversionId: 'conversion-1',
|
|
103
|
-
actionSource: 'WEBSITE',
|
|
104
|
-
user: { externalId: 'user-1' },
|
|
105
|
-
sleepImpl: async () => {},
|
|
106
|
-
fetchImpl: (async () => {
|
|
107
|
-
attempts += 1
|
|
108
|
-
return new Response('invalid', { status: 400 })
|
|
109
|
-
}) as unknown as typeof fetch,
|
|
110
|
-
}),
|
|
111
|
-
).rejects.toBeInstanceOf(RedditCapiDeliveryError)
|
|
112
|
-
expect(attempts).toBe(1)
|
|
113
|
-
})
|
|
114
|
-
|
|
115
|
-
test('is disabled without production enablement or a token', async () => {
|
|
116
|
-
let called = false
|
|
117
|
-
const result = await sendRedditCustomConversion({
|
|
118
|
-
enabled: false,
|
|
119
|
-
accessToken: 'secret',
|
|
120
|
-
customEventName: 'FirstPrompt',
|
|
121
|
-
conversionId: 'conversion-1',
|
|
122
|
-
actionSource: 'WEBSITE',
|
|
123
|
-
user: {},
|
|
124
|
-
fetchImpl: (async () => {
|
|
125
|
-
called = true
|
|
126
|
-
return Response.json({})
|
|
127
|
-
}) as unknown as typeof fetch,
|
|
128
|
-
})
|
|
129
|
-
expect(result).toBe('disabled')
|
|
130
|
-
expect(called).toBe(false)
|
|
131
|
-
})
|
|
132
|
-
})
|
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from 'bun:test'
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
responseAdNodePositions,
|
|
5
|
-
responseAdSlotCount,
|
|
6
|
-
RESPONSE_AD_FIRST_NODE_COUNT,
|
|
7
|
-
RESPONSE_AD_NODE_STEP,
|
|
8
|
-
} from '../util/response-ad-positions'
|
|
9
|
-
|
|
10
|
-
describe('responseAdNodePositions', () => {
|
|
11
|
-
test('places nothing in a response too short to intersperse', () => {
|
|
12
|
-
expect(responseAdNodePositions({ nodeCount: 0, adCount: 3 })).toEqual([])
|
|
13
|
-
expect(responseAdNodePositions({ nodeCount: 1, adCount: 3 })).toEqual([])
|
|
14
|
-
expect(responseAdNodePositions({ nodeCount: 2, adCount: 3 })).toEqual([])
|
|
15
|
-
})
|
|
16
|
-
|
|
17
|
-
test('keeps the first ad after two nodes, then spaces ads every three nodes', () => {
|
|
18
|
-
expect(RESPONSE_AD_FIRST_NODE_COUNT).toBe(2)
|
|
19
|
-
expect(RESPONSE_AD_NODE_STEP).toBe(3)
|
|
20
|
-
expect(responseAdNodePositions({ nodeCount: 2, adCount: 4 })).toEqual([])
|
|
21
|
-
expect(responseAdNodePositions({ nodeCount: 3, adCount: 4 })).toEqual([1])
|
|
22
|
-
expect(responseAdNodePositions({ nodeCount: 4, adCount: 4 })).toEqual([1])
|
|
23
|
-
expect(responseAdNodePositions({ nodeCount: 6, adCount: 4 })).toEqual([1, 4])
|
|
24
|
-
expect(responseAdNodePositions({ nodeCount: 9, adCount: 4 })).toEqual([1, 4, 7])
|
|
25
|
-
})
|
|
26
|
-
|
|
27
|
-
test('eligible display slots are not capped at eight', () => {
|
|
28
|
-
expect(responseAdSlotCount({ nodeCount: 2 })).toBe(0)
|
|
29
|
-
expect(responseAdSlotCount({ nodeCount: 3 })).toBe(1)
|
|
30
|
-
expect(responseAdSlotCount({ nodeCount: 24 })).toBe(8)
|
|
31
|
-
expect(responseAdSlotCount({ nodeCount: 27 })).toBe(9)
|
|
32
|
-
expect(responseAdNodePositions({ nodeCount: 27, adCount: 9 })).toEqual([
|
|
33
|
-
1, 4, 7, 10, 13, 16, 19, 22, 25,
|
|
34
|
-
])
|
|
35
|
-
})
|
|
36
|
-
|
|
37
|
-
test('never places more ads than provided', () => {
|
|
38
|
-
expect(responseAdNodePositions({ nodeCount: 20, adCount: 2 })).toEqual([1, 4])
|
|
39
|
-
expect(responseAdNodePositions({ nodeCount: 20, adCount: 0 })).toEqual([])
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
test('positions are stable as the streaming response appends nodes', () => {
|
|
43
|
-
let previous: number[] = []
|
|
44
|
-
for (let nodeCount = 0; nodeCount <= 12; nodeCount++) {
|
|
45
|
-
const next = responseAdNodePositions({ nodeCount, adCount: 3 })
|
|
46
|
-
expect(next.slice(0, previous.length)).toEqual(previous)
|
|
47
|
-
previous = next
|
|
48
|
-
}
|
|
49
|
-
})
|
|
50
|
-
|
|
51
|
-
test('allows the first-ad offset to differ from recurring spacing', () => {
|
|
52
|
-
expect(
|
|
53
|
-
responseAdNodePositions({
|
|
54
|
-
nodeCount: 8,
|
|
55
|
-
adCount: 3,
|
|
56
|
-
firstAdAfterNodes: 3,
|
|
57
|
-
step: 2,
|
|
58
|
-
}),
|
|
59
|
-
).toEqual([2, 4, 6])
|
|
60
|
-
})
|
|
61
|
-
|
|
62
|
-
test('clamps non-positive placement settings to one', () => {
|
|
63
|
-
expect(
|
|
64
|
-
responseAdNodePositions({
|
|
65
|
-
nodeCount: 4,
|
|
66
|
-
adCount: 3,
|
|
67
|
-
firstAdAfterNodes: 0,
|
|
68
|
-
step: 0,
|
|
69
|
-
}),
|
|
70
|
-
).toEqual([0, 1, 2])
|
|
71
|
-
})
|
|
72
|
-
})
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import { describe, test, expect } from 'bun:test'
|
|
2
|
-
|
|
3
|
-
import { getUserState, UserState } from '../old-constants'
|
|
4
|
-
|
|
5
|
-
describe('getUserState', () => {
|
|
6
|
-
test('returns LOGGED_OUT for unauthenticated users regardless of credits', () => {
|
|
7
|
-
expect(getUserState(false, 1000)).toBe(UserState.LOGGED_OUT)
|
|
8
|
-
expect(getUserState(false, 0)).toBe(UserState.LOGGED_OUT)
|
|
9
|
-
})
|
|
10
|
-
|
|
11
|
-
test('returns GOOD_STANDING for users with >= 100 credits', () => {
|
|
12
|
-
expect(getUserState(true, 100)).toBe(UserState.GOOD_STANDING)
|
|
13
|
-
expect(getUserState(true, 500)).toBe(UserState.GOOD_STANDING)
|
|
14
|
-
})
|
|
15
|
-
|
|
16
|
-
test('returns ATTENTION_NEEDED for users with 20-99 credits', () => {
|
|
17
|
-
expect(getUserState(true, 99)).toBe(UserState.ATTENTION_NEEDED)
|
|
18
|
-
expect(getUserState(true, 20)).toBe(UserState.ATTENTION_NEEDED)
|
|
19
|
-
})
|
|
20
|
-
|
|
21
|
-
test('returns CRITICAL for users with 1-19 credits', () => {
|
|
22
|
-
expect(getUserState(true, 19)).toBe(UserState.CRITICAL)
|
|
23
|
-
expect(getUserState(true, 1)).toBe(UserState.CRITICAL)
|
|
24
|
-
})
|
|
25
|
-
|
|
26
|
-
test('returns DEPLETED for users with 0 or fewer credits', () => {
|
|
27
|
-
expect(getUserState(true, 0)).toBe(UserState.DEPLETED)
|
|
28
|
-
expect(getUserState(true, -100)).toBe(UserState.DEPLETED)
|
|
29
|
-
})
|
|
30
|
-
})
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'bun:test'
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
evaluateOnboardingRequirement,
|
|
5
|
-
parseOnboardingEnabled,
|
|
6
|
-
} from '../anvil-onboarding-gate'
|
|
7
|
-
|
|
8
|
-
describe('evaluateOnboardingRequirement', () => {
|
|
9
|
-
it('asks nobody while the switch is off', () => {
|
|
10
|
-
// The load-bearing polarity: absence of config disables the feature. The
|
|
11
|
-
// opposite convention would show a form nobody decided to launch to every
|
|
12
|
-
// user at once.
|
|
13
|
-
const result = evaluateOnboardingRequirement({
|
|
14
|
-
enabled: false,
|
|
15
|
-
complete: false,
|
|
16
|
-
})
|
|
17
|
-
expect(result.required).toBe(false)
|
|
18
|
-
if (result.required) throw new Error('unreachable')
|
|
19
|
-
expect(result.reason).toBe('gate_disabled')
|
|
20
|
-
})
|
|
21
|
-
|
|
22
|
-
it('asks anyone who has not answered, regardless of account age', () => {
|
|
23
|
-
// Deliberately no cutover: a long-standing account that never answered is
|
|
24
|
-
// exactly who we have the least data about. Skip and the seen-cookie are
|
|
25
|
-
// what keep this from being a burden.
|
|
26
|
-
expect(
|
|
27
|
-
evaluateOnboardingRequirement({ enabled: true, complete: false }).required,
|
|
28
|
-
).toBe(true)
|
|
29
|
-
})
|
|
30
|
-
|
|
31
|
-
it('stops asking once the answers are in', () => {
|
|
32
|
-
const result = evaluateOnboardingRequirement({
|
|
33
|
-
enabled: true,
|
|
34
|
-
complete: true,
|
|
35
|
-
})
|
|
36
|
-
expect(result.required).toBe(false)
|
|
37
|
-
if (result.required) throw new Error('unreachable')
|
|
38
|
-
expect(result.reason).toBe('already_complete')
|
|
39
|
-
})
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
describe('parseOnboardingEnabled', () => {
|
|
43
|
-
it('accepts the affirmative spellings', () => {
|
|
44
|
-
for (const raw of ['on', 'ON', ' true ', '1']) {
|
|
45
|
-
expect(parseOnboardingEnabled(raw)).toBe(true)
|
|
46
|
-
}
|
|
47
|
-
})
|
|
48
|
-
|
|
49
|
-
it('treats anything else as off', () => {
|
|
50
|
-
// Unset, empty and misspelt all fail the same safe way — nothing about a
|
|
51
|
-
// broken value should be able to switch a user-facing screen on.
|
|
52
|
-
for (const raw of [undefined, null, '', ' ', 'off', 'yes', 'enabled']) {
|
|
53
|
-
expect(parseOnboardingEnabled(raw)).toBe(false)
|
|
54
|
-
}
|
|
55
|
-
})
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
describe('seen cookie', () => {
|
|
59
|
-
it('is a stable name with a months-long life', () => {
|
|
60
|
-
// The name is read by the `/web` layout and written by the welcome page;
|
|
61
|
-
// changing it re-asks everyone, so it is pinned here on purpose.
|
|
62
|
-
})
|
|
63
|
-
})
|
|
@@ -1,246 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'bun:test'
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
classifyOnboardingOtherText,
|
|
5
|
-
ANVIL_ONBOARDING_QUESTIONS,
|
|
6
|
-
isOnboardingComplete,
|
|
7
|
-
ONBOARDING_LEGACY_OPTION_IDS,
|
|
8
|
-
ONBOARDING_OTHER_TEXT_MAX,
|
|
9
|
-
OTHER_OPTION_ID,
|
|
10
|
-
validateOnboardingSubmission,
|
|
11
|
-
type OnboardingAnswer,
|
|
12
|
-
} from '../anvil-onboarding'
|
|
13
|
-
|
|
14
|
-
/** A complete, valid submission. */
|
|
15
|
-
function fullAnswers(overrides: OnboardingAnswer[] = []): OnboardingAnswer[] {
|
|
16
|
-
const base: OnboardingAnswer[] = [
|
|
17
|
-
{ questionId: 'referral_source', optionIds: ['youtube'] },
|
|
18
|
-
{ questionId: 'role', optionIds: ['professional_dev'] },
|
|
19
|
-
{ questionId: 'proficiency', optionIds: ['advanced'] },
|
|
20
|
-
{ questionId: 'intended_use', optionIds: ['work', 'side_projects'] },
|
|
21
|
-
{ questionId: 'subscriptions', optionIds: ['cursor'] },
|
|
22
|
-
]
|
|
23
|
-
return base.map((a) => overrides.find((o) => o.questionId === a.questionId) ?? a)
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
describe('the question set itself', () => {
|
|
27
|
-
it('gives every option a unique id within its question', () => {
|
|
28
|
-
// A duplicate id would silently merge two distinct answers in the analytics.
|
|
29
|
-
for (const q of ANVIL_ONBOARDING_QUESTIONS) {
|
|
30
|
-
const ids = q.options.map((o) => o.id)
|
|
31
|
-
expect(new Set(ids).size).toBe(ids.length)
|
|
32
|
-
}
|
|
33
|
-
})
|
|
34
|
-
|
|
35
|
-
it('offers Other everywhere except the ordinal scale', () => {
|
|
36
|
-
// Free text on an ordered scale is unusable for the segmentation the
|
|
37
|
-
// question exists to support.
|
|
38
|
-
for (const q of ANVIL_ONBOARDING_QUESTIONS) {
|
|
39
|
-
const hasOther = q.options.some((o) => o.id === OTHER_OPTION_ID)
|
|
40
|
-
expect(hasOther).toBe(q.id !== 'proficiency')
|
|
41
|
-
}
|
|
42
|
-
})
|
|
43
|
-
|
|
44
|
-
it('maps every legacy option onto an option that still exists', () => {
|
|
45
|
-
// A successor that no longer exists silently drops the answers it was
|
|
46
|
-
// supposed to rescue — the exact failure the map exists to prevent.
|
|
47
|
-
for (const [questionId, map] of Object.entries(ONBOARDING_LEGACY_OPTION_IDS)) {
|
|
48
|
-
const question = ANVIL_ONBOARDING_QUESTIONS.find((q) => q.id === questionId)
|
|
49
|
-
expect(question).toBeDefined()
|
|
50
|
-
const live = new Set(question!.options.map((o) => o.id))
|
|
51
|
-
for (const [retired, successor] of Object.entries(map ?? {})) {
|
|
52
|
-
expect(live.has(retired)).toBe(false)
|
|
53
|
-
expect(live.has(successor)).toBe(true)
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
it('only marks options exclusive on multi-select questions', () => {
|
|
59
|
-
// On a single-choice question "exclusive" is meaningless, and reading as
|
|
60
|
-
// though it does something is worse than not having it.
|
|
61
|
-
for (const q of ANVIL_ONBOARDING_QUESTIONS) {
|
|
62
|
-
if (q.multi) continue
|
|
63
|
-
expect(q.options.some((o) => o.exclusive)).toBe(false)
|
|
64
|
-
}
|
|
65
|
-
})
|
|
66
|
-
})
|
|
67
|
-
|
|
68
|
-
describe('classifyOnboardingOtherText — write-ins folded into real options', () => {
|
|
69
|
-
it('counts every Instagram spelling as the Instagram / TikTok option', () => {
|
|
70
|
-
for (const text of ['insta', 'Instagram', 'instagram ads', 'IG', 'tik tok']) {
|
|
71
|
-
expect(classifyOnboardingOtherText('referral_source', text)).toBe('tiktok')
|
|
72
|
-
}
|
|
73
|
-
})
|
|
74
|
-
|
|
75
|
-
it('counts AI assistants as the Google / AI search option', () => {
|
|
76
|
-
for (const text of ['ChatGPT', 'chat gpt', 'AI', 'gemini', 'perplexity']) {
|
|
77
|
-
expect(classifyOnboardingOtherText('referral_source', text)).toBe('search')
|
|
78
|
-
}
|
|
79
|
-
})
|
|
80
|
-
|
|
81
|
-
it('prefers the more specific rule when a write-in matches both', () => {
|
|
82
|
-
expect(classifyOnboardingOtherText('referral_source', 'instagram AI page')).toBe(
|
|
83
|
-
'tiktok',
|
|
84
|
-
)
|
|
85
|
-
})
|
|
86
|
-
|
|
87
|
-
it('leaves genuinely other answers alone', () => {
|
|
88
|
-
// The AI rule is the dangerous one: a substring match would swallow
|
|
89
|
-
// "email", "said", "chair" and quietly inflate a channel that never
|
|
90
|
-
// referred anyone.
|
|
91
|
-
for (const text of ['forums', 'my brother', 'email newsletter', 'a fair']) {
|
|
92
|
-
expect(classifyOnboardingOtherText('referral_source', text)).toBeNull()
|
|
93
|
-
}
|
|
94
|
-
})
|
|
95
|
-
|
|
96
|
-
it('does nothing on questions with no rules', () => {
|
|
97
|
-
expect(classifyOnboardingOtherText('role', 'instagram')).toBeNull()
|
|
98
|
-
})
|
|
99
|
-
})
|
|
100
|
-
|
|
101
|
-
describe('validateOnboardingSubmission', () => {
|
|
102
|
-
it('accepts a complete submission', () => {
|
|
103
|
-
const result = validateOnboardingSubmission({ answers: fullAnswers() })
|
|
104
|
-
expect(result.ok).toBe(true)
|
|
105
|
-
})
|
|
106
|
-
|
|
107
|
-
it('requires every question', () => {
|
|
108
|
-
// The form has no skip, so a partial submission is a stale or hand-rolled
|
|
109
|
-
// client rather than a user choice — and half a response would sit in the
|
|
110
|
-
// analytics looking like a real one.
|
|
111
|
-
const result = validateOnboardingSubmission({
|
|
112
|
-
answers: fullAnswers().slice(0, 2),
|
|
113
|
-
})
|
|
114
|
-
expect(result.ok).toBe(false)
|
|
115
|
-
if (result.ok) throw new Error('unreachable')
|
|
116
|
-
expect(result.errors.length).toBeGreaterThan(0)
|
|
117
|
-
expect(result.errors.every((e) => e.questionId !== null)).toBe(true)
|
|
118
|
-
})
|
|
119
|
-
|
|
120
|
-
it('rejects an unknown option id', () => {
|
|
121
|
-
// Out-of-date client or a forged post. Accepting it would put a value in
|
|
122
|
-
// the analytics that no question can explain.
|
|
123
|
-
const result = validateOnboardingSubmission({
|
|
124
|
-
answers: fullAnswers([{ questionId: 'role', optionIds: ['ceo_of_mars'] }]),
|
|
125
|
-
})
|
|
126
|
-
expect(result.ok).toBe(false)
|
|
127
|
-
})
|
|
128
|
-
|
|
129
|
-
it('rejects multiple answers to a single-choice question', () => {
|
|
130
|
-
const result = validateOnboardingSubmission({
|
|
131
|
-
answers: fullAnswers([
|
|
132
|
-
{ questionId: 'role', optionIds: ['student', 'founder'] },
|
|
133
|
-
]),
|
|
134
|
-
})
|
|
135
|
-
expect(result.ok).toBe(false)
|
|
136
|
-
})
|
|
137
|
-
|
|
138
|
-
it('rejects an exclusive option combined with others', () => {
|
|
139
|
-
// "No subscriptions" and "Cursor" cannot both be true; a stored
|
|
140
|
-
// contradiction has no honest reading in the tally.
|
|
141
|
-
const result = validateOnboardingSubmission({
|
|
142
|
-
answers: fullAnswers([
|
|
143
|
-
{ questionId: 'subscriptions', optionIds: ['none', 'cursor'] },
|
|
144
|
-
]),
|
|
145
|
-
})
|
|
146
|
-
expect(result.ok).toBe(false)
|
|
147
|
-
})
|
|
148
|
-
|
|
149
|
-
it('accepts the exclusive option on its own', () => {
|
|
150
|
-
const result = validateOnboardingSubmission({
|
|
151
|
-
answers: fullAnswers([
|
|
152
|
-
{ questionId: 'subscriptions', optionIds: ['none'] },
|
|
153
|
-
]),
|
|
154
|
-
})
|
|
155
|
-
expect(result.ok).toBe(true)
|
|
156
|
-
})
|
|
157
|
-
|
|
158
|
-
it('accepts multiple answers where the question allows it', () => {
|
|
159
|
-
const result = validateOnboardingSubmission({
|
|
160
|
-
answers: fullAnswers([
|
|
161
|
-
{
|
|
162
|
-
questionId: 'intended_use',
|
|
163
|
-
optionIds: ['work', 'learning', 'automation'],
|
|
164
|
-
},
|
|
165
|
-
]),
|
|
166
|
-
})
|
|
167
|
-
expect(result.ok).toBe(true)
|
|
168
|
-
})
|
|
169
|
-
})
|
|
170
|
-
|
|
171
|
-
describe('the Other option', () => {
|
|
172
|
-
it('requires accompanying text', () => {
|
|
173
|
-
const result = validateOnboardingSubmission({
|
|
174
|
-
answers: fullAnswers([
|
|
175
|
-
{ questionId: 'role', optionIds: [OTHER_OPTION_ID] },
|
|
176
|
-
]),
|
|
177
|
-
})
|
|
178
|
-
expect(result.ok).toBe(false)
|
|
179
|
-
})
|
|
180
|
-
|
|
181
|
-
it('keeps the text when Other is chosen', () => {
|
|
182
|
-
const result = validateOnboardingSubmission({
|
|
183
|
-
answers: fullAnswers([
|
|
184
|
-
{
|
|
185
|
-
questionId: 'role',
|
|
186
|
-
optionIds: [OTHER_OPTION_ID],
|
|
187
|
-
otherText: ' technical writer ',
|
|
188
|
-
},
|
|
189
|
-
]),
|
|
190
|
-
})
|
|
191
|
-
expect(result.ok).toBe(true)
|
|
192
|
-
if (!result.ok) throw new Error('unreachable')
|
|
193
|
-
const role = result.answers.find((a) => a.questionId === 'role')
|
|
194
|
-
expect(role?.otherText).toBe('technical writer')
|
|
195
|
-
})
|
|
196
|
-
|
|
197
|
-
it('drops stray text when Other was NOT chosen', () => {
|
|
198
|
-
// Otherwise text rides along on an answer that has nowhere to display it,
|
|
199
|
-
// and the admin view shows a note against the wrong option.
|
|
200
|
-
const result = validateOnboardingSubmission({
|
|
201
|
-
answers: fullAnswers([
|
|
202
|
-
{ questionId: 'role', optionIds: ['student'], otherText: 'ignore me' },
|
|
203
|
-
]),
|
|
204
|
-
})
|
|
205
|
-
expect(result.ok).toBe(true)
|
|
206
|
-
if (!result.ok) throw new Error('unreachable')
|
|
207
|
-
expect(result.answers.find((a) => a.questionId === 'role')?.otherText).toBeUndefined()
|
|
208
|
-
})
|
|
209
|
-
|
|
210
|
-
it('truncates rather than rejecting a long note', () => {
|
|
211
|
-
const result = validateOnboardingSubmission({
|
|
212
|
-
answers: fullAnswers([
|
|
213
|
-
{
|
|
214
|
-
questionId: 'role',
|
|
215
|
-
optionIds: [OTHER_OPTION_ID],
|
|
216
|
-
otherText: 'x'.repeat(ONBOARDING_OTHER_TEXT_MAX + 500),
|
|
217
|
-
},
|
|
218
|
-
]),
|
|
219
|
-
})
|
|
220
|
-
expect(result.ok).toBe(true)
|
|
221
|
-
if (!result.ok) throw new Error('unreachable')
|
|
222
|
-
expect(result.answers.find((a) => a.questionId === 'role')?.otherText).toHaveLength(
|
|
223
|
-
ONBOARDING_OTHER_TEXT_MAX,
|
|
224
|
-
)
|
|
225
|
-
})
|
|
226
|
-
})
|
|
227
|
-
|
|
228
|
-
describe('isOnboardingComplete — the blocking gate reads this', () => {
|
|
229
|
-
it('is true only when every question has an answer', () => {
|
|
230
|
-
expect(isOnboardingComplete(fullAnswers())).toBe(true)
|
|
231
|
-
expect(isOnboardingComplete(fullAnswers().slice(0, 3))).toBe(false)
|
|
232
|
-
})
|
|
233
|
-
|
|
234
|
-
it('treats null, undefined and empty as incomplete', () => {
|
|
235
|
-
// A user with no record must be gated, not waved through — this is the
|
|
236
|
-
// difference between the gate working and being decorative.
|
|
237
|
-
expect(isOnboardingComplete(null)).toBe(false)
|
|
238
|
-
expect(isOnboardingComplete(undefined)).toBe(false)
|
|
239
|
-
expect(isOnboardingComplete([])).toBe(false)
|
|
240
|
-
})
|
|
241
|
-
|
|
242
|
-
it('does not count an answer with no options chosen', () => {
|
|
243
|
-
const hollow = fullAnswers([{ questionId: 'role', optionIds: [] }])
|
|
244
|
-
expect(isOnboardingComplete(hollow)).toBe(false)
|
|
245
|
-
})
|
|
246
|
-
})
|
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'bun:test'
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
detectCfWorker,
|
|
5
|
-
looksLikeProxyClientId,
|
|
6
|
-
parseAllowedWorkerZones,
|
|
7
|
-
} from '../cf-worker-signals'
|
|
8
|
-
|
|
9
|
-
const NO_ZONES = new Set<string>()
|
|
10
|
-
|
|
11
|
-
describe('detectCfWorker', () => {
|
|
12
|
-
it('detects a Worker subrequest that came through our edge', () => {
|
|
13
|
-
expect(
|
|
14
|
-
detectCfWorker({
|
|
15
|
-
cfWorkerHeader: 'anvil2api.workers.dev',
|
|
16
|
-
cfRayHeader: 'abc123-SJC',
|
|
17
|
-
allowedZones: NO_ZONES,
|
|
18
|
-
}),
|
|
19
|
-
).toEqual({ detected: true, zone: 'anvil2api.workers.dev' })
|
|
20
|
-
})
|
|
21
|
-
|
|
22
|
-
it('ignores ordinary traffic', () => {
|
|
23
|
-
expect(
|
|
24
|
-
detectCfWorker({
|
|
25
|
-
cfWorkerHeader: null,
|
|
26
|
-
cfRayHeader: 'abc123-SJC',
|
|
27
|
-
allowedZones: NO_ZONES,
|
|
28
|
-
}),
|
|
29
|
-
).toEqual({ detected: false, reason: 'no_header' })
|
|
30
|
-
})
|
|
31
|
-
|
|
32
|
-
it('refuses to act on cf-worker without edge corroboration', () => {
|
|
33
|
-
// This is the whole difference between this detector and the ones that were
|
|
34
|
-
// defeated: without `cf-ray` the header is caller-authored text on a request
|
|
35
|
-
// that never touched Cloudflare, which is exactly as forgeable as a
|
|
36
|
-
// self-reported fingerprint.
|
|
37
|
-
expect(
|
|
38
|
-
detectCfWorker({
|
|
39
|
-
cfWorkerHeader: 'anything.workers.dev',
|
|
40
|
-
cfRayHeader: null,
|
|
41
|
-
allowedZones: NO_ZONES,
|
|
42
|
-
}),
|
|
43
|
-
).toEqual({ detected: false, reason: 'not_edge_verified' })
|
|
44
|
-
})
|
|
45
|
-
|
|
46
|
-
it('never flags our own Workers', () => {
|
|
47
|
-
// app-preview-proxy is a Cloudflare Worker that talks to our API. Banning
|
|
48
|
-
// our own infrastructure is the most obvious way this could go wrong.
|
|
49
|
-
const allowed = parseAllowedWorkerZones('app-preview-proxy, vly-sh-router')
|
|
50
|
-
expect(
|
|
51
|
-
detectCfWorker({
|
|
52
|
-
cfWorkerHeader: 'app-preview-proxy',
|
|
53
|
-
cfRayHeader: 'abc-SJC',
|
|
54
|
-
allowedZones: allowed,
|
|
55
|
-
}),
|
|
56
|
-
).toEqual({ detected: false, reason: 'allowlisted' })
|
|
57
|
-
})
|
|
58
|
-
|
|
59
|
-
it('matches the allowlist case-insensitively and ignores padding', () => {
|
|
60
|
-
const allowed = parseAllowedWorkerZones(' App-Preview-Proxy ,, ')
|
|
61
|
-
expect(
|
|
62
|
-
detectCfWorker({
|
|
63
|
-
cfWorkerHeader: ' APP-PREVIEW-PROXY ',
|
|
64
|
-
cfRayHeader: 'abc-SJC',
|
|
65
|
-
allowedZones: allowed,
|
|
66
|
-
}).detected,
|
|
67
|
-
).toBe(false)
|
|
68
|
-
})
|
|
69
|
-
|
|
70
|
-
it('treats a whitespace-only header as absent', () => {
|
|
71
|
-
expect(
|
|
72
|
-
detectCfWorker({
|
|
73
|
-
cfWorkerHeader: ' ',
|
|
74
|
-
cfRayHeader: 'abc-SJC',
|
|
75
|
-
allowedZones: NO_ZONES,
|
|
76
|
-
}),
|
|
77
|
-
).toEqual({ detected: false, reason: 'no_header' })
|
|
78
|
-
})
|
|
79
|
-
})
|
|
80
|
-
|
|
81
|
-
describe('parseAllowedWorkerZones', () => {
|
|
82
|
-
it('is empty for unset config rather than throwing', () => {
|
|
83
|
-
expect(parseAllowedWorkerZones(undefined).size).toBe(0)
|
|
84
|
-
expect(parseAllowedWorkerZones('').size).toBe(0)
|
|
85
|
-
})
|
|
86
|
-
})
|
|
87
|
-
|
|
88
|
-
describe('looksLikeProxyClientId', () => {
|
|
89
|
-
it('matches the published proxy generator', () => {
|
|
90
|
-
expect(looksLikeProxyClientId('wf-a1b2c3d4')).toBe(true)
|
|
91
|
-
})
|
|
92
|
-
|
|
93
|
-
it('does not match our own clients or near-misses', () => {
|
|
94
|
-
for (const id of [
|
|
95
|
-
'enhanced-abc',
|
|
96
|
-
'wf-TOOLONGVALUE',
|
|
97
|
-
'wf-a1b2c3',
|
|
98
|
-
'wf-A1B2C3D4',
|
|
99
|
-
null,
|
|
100
|
-
undefined,
|
|
101
|
-
'',
|
|
102
|
-
]) {
|
|
103
|
-
expect(looksLikeProxyClientId(id as string)).toBe(false)
|
|
104
|
-
}
|
|
105
|
-
})
|
|
106
|
-
})
|