@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,202 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from 'bun:test'
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
addDaysToDateKey,
|
|
5
|
-
calculateAnvilStreak,
|
|
6
|
-
getAnvilDailyStreakRewardPool,
|
|
7
|
-
getAnvilStreakGlmBonusUnits,
|
|
8
|
-
getAnvilUsageDateKey,
|
|
9
|
-
} from '../anvil-streak'
|
|
10
|
-
|
|
11
|
-
function usageDatesFrom(startDateKey: string, count: number): string[] {
|
|
12
|
-
return Array.from({ length: count }, (_, index) =>
|
|
13
|
-
addDaysToDateKey(startDateKey, index),
|
|
14
|
-
)
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
describe('anvil streak helpers', () => {
|
|
18
|
-
test('formats usage dates in the Anvil reset timezone', () => {
|
|
19
|
-
expect(getAnvilUsageDateKey(new Date('2026-05-27T06:30:00.000Z'))).toBe(
|
|
20
|
-
'2026-05-26',
|
|
21
|
-
)
|
|
22
|
-
expect(getAnvilUsageDateKey(new Date('2026-05-27T08:30:00.000Z'))).toBe(
|
|
23
|
-
'2026-05-27',
|
|
24
|
-
)
|
|
25
|
-
})
|
|
26
|
-
|
|
27
|
-
test('adds days across month boundaries', () => {
|
|
28
|
-
expect(addDaysToDateKey('2026-03-01', -1)).toBe('2026-02-28')
|
|
29
|
-
expect(addDaysToDateKey('2024-03-01', -1)).toBe('2024-02-29')
|
|
30
|
-
expect(addDaysToDateKey('2026-12-31', 1)).toBe('2027-01-01')
|
|
31
|
-
})
|
|
32
|
-
|
|
33
|
-
test('counts a streak that includes today', () => {
|
|
34
|
-
expect(
|
|
35
|
-
calculateAnvilStreak({
|
|
36
|
-
todayDateKey: '2026-05-27',
|
|
37
|
-
usageDates: ['2026-05-25', '2026-05-23', '2026-05-27', '2026-05-26'],
|
|
38
|
-
}),
|
|
39
|
-
).toEqual({
|
|
40
|
-
streak: 3,
|
|
41
|
-
todayUsed: true,
|
|
42
|
-
lastUsageDate: '2026-05-27',
|
|
43
|
-
})
|
|
44
|
-
})
|
|
45
|
-
|
|
46
|
-
test('keeps yesterday-anchored streaks alive before today is used', () => {
|
|
47
|
-
expect(
|
|
48
|
-
calculateAnvilStreak({
|
|
49
|
-
todayDateKey: '2026-05-27',
|
|
50
|
-
usageDates: ['2026-05-26', '2026-05-25', '2026-05-24'],
|
|
51
|
-
}),
|
|
52
|
-
).toEqual({
|
|
53
|
-
streak: 3,
|
|
54
|
-
todayUsed: false,
|
|
55
|
-
lastUsageDate: '2026-05-26',
|
|
56
|
-
})
|
|
57
|
-
})
|
|
58
|
-
|
|
59
|
-
test('returns zero after a missed full day', () => {
|
|
60
|
-
expect(
|
|
61
|
-
calculateAnvilStreak({
|
|
62
|
-
todayDateKey: '2026-05-27',
|
|
63
|
-
usageDates: ['2026-05-25', '2026-05-24'],
|
|
64
|
-
}),
|
|
65
|
-
).toEqual({
|
|
66
|
-
streak: 0,
|
|
67
|
-
todayUsed: false,
|
|
68
|
-
lastUsageDate: '2026-05-25',
|
|
69
|
-
})
|
|
70
|
-
})
|
|
71
|
-
})
|
|
72
|
-
|
|
73
|
-
describe('anvil streak rewards', () => {
|
|
74
|
-
test('grants one weekly GLM session per completed 7 days, capped at 4', () => {
|
|
75
|
-
const expected: Array<[streak: number, units: number]> = [
|
|
76
|
-
[7, 1],
|
|
77
|
-
[8, 1],
|
|
78
|
-
[13, 1],
|
|
79
|
-
[14, 2],
|
|
80
|
-
[21, 3],
|
|
81
|
-
[28, 4],
|
|
82
|
-
[42, 4],
|
|
83
|
-
]
|
|
84
|
-
for (const [streak, units] of expected) {
|
|
85
|
-
expect(
|
|
86
|
-
getAnvilStreakGlmBonusUnits({
|
|
87
|
-
todayDateKey: addDaysToDateKey('2026-07-01', streak - 1),
|
|
88
|
-
usageDates: usageDatesFrom('2026-07-01', streak),
|
|
89
|
-
}),
|
|
90
|
-
).toBe(units)
|
|
91
|
-
}
|
|
92
|
-
})
|
|
93
|
-
|
|
94
|
-
test('returns zero for empty and six-day usage histories', () => {
|
|
95
|
-
expect(
|
|
96
|
-
getAnvilStreakGlmBonusUnits({
|
|
97
|
-
todayDateKey: '2026-07-13',
|
|
98
|
-
usageDates: [],
|
|
99
|
-
}),
|
|
100
|
-
).toBe(0)
|
|
101
|
-
expect(
|
|
102
|
-
getAnvilStreakGlmBonusUnits({
|
|
103
|
-
todayDateKey: '2026-07-13',
|
|
104
|
-
usageDates: usageDatesFrom('2026-07-07', 6),
|
|
105
|
-
}),
|
|
106
|
-
).toBe(0)
|
|
107
|
-
})
|
|
108
|
-
|
|
109
|
-
test('a yesterday-anchored 28-day streak earns the max through the bounded window', () => {
|
|
110
|
-
// Mirrors the web quota query, which fetches only 28 days back from today:
|
|
111
|
-
// the oldest still-active max-tier streak (today unused) must be fully
|
|
112
|
-
// visible inside that window.
|
|
113
|
-
expect(
|
|
114
|
-
getAnvilStreakGlmBonusUnits({
|
|
115
|
-
todayDateKey: addDaysToDateKey('2026-07-01', 28),
|
|
116
|
-
usageDates: usageDatesFrom('2026-07-01', 28),
|
|
117
|
-
}),
|
|
118
|
-
).toBe(4)
|
|
119
|
-
})
|
|
120
|
-
|
|
121
|
-
test('refills GLM before the first use of a new week while the streak is alive', () => {
|
|
122
|
-
expect(
|
|
123
|
-
getAnvilStreakGlmBonusUnits({
|
|
124
|
-
todayDateKey: '2026-07-13',
|
|
125
|
-
usageDates: usageDatesFrom('2026-07-06', 7),
|
|
126
|
-
}),
|
|
127
|
-
).toBe(1)
|
|
128
|
-
})
|
|
129
|
-
|
|
130
|
-
test('removes the live GLM entitlement after a missed full day', () => {
|
|
131
|
-
expect(
|
|
132
|
-
getAnvilStreakGlmBonusUnits({
|
|
133
|
-
todayDateKey: '2026-07-14',
|
|
134
|
-
usageDates: usageDatesFrom('2026-07-06', 7),
|
|
135
|
-
}),
|
|
136
|
-
).toBe(0)
|
|
137
|
-
})
|
|
138
|
-
|
|
139
|
-
test('full access persists only the daily premium bonus', () => {
|
|
140
|
-
expect(
|
|
141
|
-
getAnvilDailyStreakRewardPool({
|
|
142
|
-
streak: 7,
|
|
143
|
-
todayUsed: true,
|
|
144
|
-
accessTier: 'full',
|
|
145
|
-
}),
|
|
146
|
-
).toBe('premium')
|
|
147
|
-
})
|
|
148
|
-
|
|
149
|
-
test('full access grants the daily premium bonus throughout the streak', () => {
|
|
150
|
-
expect(
|
|
151
|
-
getAnvilDailyStreakRewardPool({
|
|
152
|
-
streak: 8,
|
|
153
|
-
todayUsed: true,
|
|
154
|
-
accessTier: 'full',
|
|
155
|
-
}),
|
|
156
|
-
).toBe('premium')
|
|
157
|
-
expect(
|
|
158
|
-
getAnvilDailyStreakRewardPool({
|
|
159
|
-
streak: 13,
|
|
160
|
-
todayUsed: true,
|
|
161
|
-
accessTier: 'full',
|
|
162
|
-
}),
|
|
163
|
-
).toBe('premium')
|
|
164
|
-
})
|
|
165
|
-
|
|
166
|
-
test('limited access grants the limited bonus every day at streak >= 7', () => {
|
|
167
|
-
// The threshold day and later days both grant the bonus.
|
|
168
|
-
expect(
|
|
169
|
-
getAnvilDailyStreakRewardPool({
|
|
170
|
-
streak: 14,
|
|
171
|
-
todayUsed: true,
|
|
172
|
-
accessTier: 'limited',
|
|
173
|
-
}),
|
|
174
|
-
).toBe('limited')
|
|
175
|
-
expect(
|
|
176
|
-
getAnvilDailyStreakRewardPool({
|
|
177
|
-
streak: 9,
|
|
178
|
-
todayUsed: true,
|
|
179
|
-
accessTier: 'limited',
|
|
180
|
-
}),
|
|
181
|
-
).toBe('limited')
|
|
182
|
-
})
|
|
183
|
-
|
|
184
|
-
test('no daily reward below seven days or before today is used', () => {
|
|
185
|
-
expect(
|
|
186
|
-
getAnvilDailyStreakRewardPool({
|
|
187
|
-
streak: 6,
|
|
188
|
-
todayUsed: true,
|
|
189
|
-
accessTier: 'full',
|
|
190
|
-
}),
|
|
191
|
-
).toBeNull()
|
|
192
|
-
// Streak is at 7 only because yesterday anchored it; the user hasn't used
|
|
193
|
-
// Anvil today, so no bonus is earned yet.
|
|
194
|
-
expect(
|
|
195
|
-
getAnvilDailyStreakRewardPool({
|
|
196
|
-
streak: 7,
|
|
197
|
-
todayUsed: false,
|
|
198
|
-
accessTier: 'full',
|
|
199
|
-
}),
|
|
200
|
-
).toBeNull()
|
|
201
|
-
})
|
|
202
|
-
})
|
|
@@ -1,193 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'bun:test'
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
buildAnvilUsageGrid,
|
|
5
|
-
buildAnvilUsageSummary,
|
|
6
|
-
calculateLongestAnvilStreak,
|
|
7
|
-
} from '../anvil-usage-summary'
|
|
8
|
-
|
|
9
|
-
describe('calculateLongestAnvilStreak', () => {
|
|
10
|
-
it('finds the longest run rather than the most recent one', () => {
|
|
11
|
-
expect(
|
|
12
|
-
calculateLongestAnvilStreak([
|
|
13
|
-
'2026-08-01',
|
|
14
|
-
'2026-08-02',
|
|
15
|
-
'2026-08-03',
|
|
16
|
-
'2026-08-04',
|
|
17
|
-
// gap
|
|
18
|
-
'2026-08-10',
|
|
19
|
-
'2026-08-11',
|
|
20
|
-
]),
|
|
21
|
-
).toBe(4)
|
|
22
|
-
})
|
|
23
|
-
|
|
24
|
-
it('does not extend a run across a gap, unlike the current streak', () => {
|
|
25
|
-
// The current streak deliberately survives "used yesterday, not yet today"
|
|
26
|
-
// because the day is not over. A historical run has no such grace.
|
|
27
|
-
expect(calculateLongestAnvilStreak(['2026-08-01', '2026-08-03'])).toBe(1)
|
|
28
|
-
})
|
|
29
|
-
|
|
30
|
-
it('counts each day once when the input repeats or arrives unsorted', () => {
|
|
31
|
-
expect(
|
|
32
|
-
calculateLongestAnvilStreak([
|
|
33
|
-
'2026-08-03',
|
|
34
|
-
'2026-08-01',
|
|
35
|
-
'2026-08-02',
|
|
36
|
-
'2026-08-02',
|
|
37
|
-
]),
|
|
38
|
-
).toBe(3)
|
|
39
|
-
})
|
|
40
|
-
|
|
41
|
-
it('is zero with no history', () => {
|
|
42
|
-
expect(calculateLongestAnvilStreak([])).toBe(0)
|
|
43
|
-
})
|
|
44
|
-
|
|
45
|
-
it('handles a run across a month boundary', () => {
|
|
46
|
-
expect(
|
|
47
|
-
calculateLongestAnvilStreak([
|
|
48
|
-
'2026-07-30',
|
|
49
|
-
'2026-07-31',
|
|
50
|
-
'2026-08-01',
|
|
51
|
-
]),
|
|
52
|
-
).toBe(3)
|
|
53
|
-
})
|
|
54
|
-
})
|
|
55
|
-
|
|
56
|
-
describe('buildAnvilUsageSummary', () => {
|
|
57
|
-
const activeDates = ['2026-08-10', '2026-08-11', '2026-08-12']
|
|
58
|
-
|
|
59
|
-
it('reports the current and longest streak together', () => {
|
|
60
|
-
const summary = buildAnvilUsageSummary({
|
|
61
|
-
activeDates: [
|
|
62
|
-
// A four-day run that has since lapsed...
|
|
63
|
-
'2026-07-01',
|
|
64
|
-
'2026-07-02',
|
|
65
|
-
'2026-07-03',
|
|
66
|
-
'2026-07-04',
|
|
67
|
-
// ...and a shorter one running right now.
|
|
68
|
-
'2026-08-11',
|
|
69
|
-
'2026-08-12',
|
|
70
|
-
],
|
|
71
|
-
todayDateKey: '2026-08-12',
|
|
72
|
-
})
|
|
73
|
-
|
|
74
|
-
expect(summary.streak.current).toBe(2)
|
|
75
|
-
expect(summary.streak.longest).toBe(4)
|
|
76
|
-
expect(summary.streak.todayUsed).toBe(true)
|
|
77
|
-
expect(summary.streak.lastUsageDate).toBe('2026-08-12')
|
|
78
|
-
})
|
|
79
|
-
|
|
80
|
-
it('counts all-time active days regardless of the map window', () => {
|
|
81
|
-
const summary = buildAnvilUsageSummary({
|
|
82
|
-
activeDates,
|
|
83
|
-
todayDateKey: '2026-08-12',
|
|
84
|
-
windowDays: 2,
|
|
85
|
-
})
|
|
86
|
-
|
|
87
|
-
// The window covers 08-11 and 08-12; 08-10 falls outside it but still
|
|
88
|
-
// counts toward the account's history.
|
|
89
|
-
expect(summary.activeDates).toEqual(['2026-08-11', '2026-08-12'])
|
|
90
|
-
expect(summary.allTimeActiveDays).toBe(3)
|
|
91
|
-
})
|
|
92
|
-
|
|
93
|
-
it('drops days after today rather than letting a fast clock inflate totals', () => {
|
|
94
|
-
const summary = buildAnvilUsageSummary({
|
|
95
|
-
activeDates: [...activeDates, '2026-08-13'],
|
|
96
|
-
todayDateKey: '2026-08-12',
|
|
97
|
-
})
|
|
98
|
-
|
|
99
|
-
expect(summary.allTimeActiveDays).toBe(3)
|
|
100
|
-
expect(summary.streak.lastUsageDate).toBe('2026-08-12')
|
|
101
|
-
})
|
|
102
|
-
|
|
103
|
-
it('sorts and dedupes unordered input before walking the streak', () => {
|
|
104
|
-
const summary = buildAnvilUsageSummary({
|
|
105
|
-
activeDates: ['2026-08-12', '2026-08-10', '2026-08-11', '2026-08-11'],
|
|
106
|
-
todayDateKey: '2026-08-12',
|
|
107
|
-
})
|
|
108
|
-
|
|
109
|
-
expect(summary.streak.current).toBe(3)
|
|
110
|
-
expect(summary.activeDates).toEqual(activeDates)
|
|
111
|
-
expect(summary.allTimeActiveDays).toBe(3)
|
|
112
|
-
})
|
|
113
|
-
|
|
114
|
-
it('carries a null recent window through rather than inventing zeros', () => {
|
|
115
|
-
// The token aggregate caps itself with a statement timeout; a timeout must
|
|
116
|
-
// read as "unknown" downstream, never as "you sent nothing".
|
|
117
|
-
const summary = buildAnvilUsageSummary({
|
|
118
|
-
activeDates,
|
|
119
|
-
todayDateKey: '2026-08-12',
|
|
120
|
-
recent: null,
|
|
121
|
-
})
|
|
122
|
-
|
|
123
|
-
expect(summary.recent).toBeNull()
|
|
124
|
-
})
|
|
125
|
-
|
|
126
|
-
it('passes the recent window through when it resolved', () => {
|
|
127
|
-
const summary = buildAnvilUsageSummary({
|
|
128
|
-
activeDates,
|
|
129
|
-
todayDateKey: '2026-08-12',
|
|
130
|
-
recent: {
|
|
131
|
-
days: 7,
|
|
132
|
-
messages: 12,
|
|
133
|
-
inputTokens: 300,
|
|
134
|
-
cacheReadTokens: 50,
|
|
135
|
-
outputTokens: 50,
|
|
136
|
-
totalTokens: 400,
|
|
137
|
-
},
|
|
138
|
-
})
|
|
139
|
-
|
|
140
|
-
expect(summary.recent?.messages).toBe(12)
|
|
141
|
-
expect(summary.recent?.totalTokens).toBe(400)
|
|
142
|
-
})
|
|
143
|
-
})
|
|
144
|
-
|
|
145
|
-
describe('buildAnvilUsageGrid', () => {
|
|
146
|
-
// 2026-08-12 is a Wednesday (weekday 3).
|
|
147
|
-
const todayDateKey = '2026-08-12'
|
|
148
|
-
const todayWeekday = 3
|
|
149
|
-
|
|
150
|
-
it('pads to whole weeks so every row is one weekday', () => {
|
|
151
|
-
const grid = buildAnvilUsageGrid({
|
|
152
|
-
activeDates: [],
|
|
153
|
-
todayDateKey,
|
|
154
|
-
windowDays: 365,
|
|
155
|
-
todayWeekday,
|
|
156
|
-
})
|
|
157
|
-
|
|
158
|
-
expect(grid.length % 7).toBe(0)
|
|
159
|
-
})
|
|
160
|
-
|
|
161
|
-
it('places today at its own weekday in the final column', () => {
|
|
162
|
-
const grid = buildAnvilUsageGrid({
|
|
163
|
-
activeDates: [],
|
|
164
|
-
todayDateKey,
|
|
165
|
-
windowDays: 365,
|
|
166
|
-
todayWeekday,
|
|
167
|
-
})
|
|
168
|
-
|
|
169
|
-
const lastColumn = grid.slice(grid.length - 7)
|
|
170
|
-
expect(lastColumn[todayWeekday]).toMatchObject({ date: todayDateKey })
|
|
171
|
-
// Everything after today in the final column is padding, not a quiet day —
|
|
172
|
-
// those dates have not happened yet.
|
|
173
|
-
expect(lastColumn.slice(todayWeekday + 1).every((cell) => cell === null)).toBe(
|
|
174
|
-
true,
|
|
175
|
-
)
|
|
176
|
-
})
|
|
177
|
-
|
|
178
|
-
it('marks active days and fills the rest as quiet', () => {
|
|
179
|
-
const grid = buildAnvilUsageGrid({
|
|
180
|
-
activeDates: ['2026-08-11'],
|
|
181
|
-
todayDateKey,
|
|
182
|
-
windowDays: 3,
|
|
183
|
-
todayWeekday,
|
|
184
|
-
})
|
|
185
|
-
|
|
186
|
-
const cells = grid.filter((cell) => cell !== null)
|
|
187
|
-
expect(cells).toEqual([
|
|
188
|
-
{ date: '2026-08-10', active: false },
|
|
189
|
-
{ date: '2026-08-11', active: true },
|
|
190
|
-
{ date: '2026-08-12', active: false },
|
|
191
|
-
])
|
|
192
|
-
})
|
|
193
|
-
})
|
|
@@ -1,140 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from 'bun:test'
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
CONTEXT_PRUNING_COMPLETED_EVENT,
|
|
5
|
-
getAxiomOnlyLogEvent,
|
|
6
|
-
STREAM_RECOVERY_EVENT,
|
|
7
|
-
} from '../axiom-only-log'
|
|
8
|
-
|
|
9
|
-
describe('getAxiomOnlyLogEvent', () => {
|
|
10
|
-
test('sanitizes context-pruning metadata', () => {
|
|
11
|
-
expect(
|
|
12
|
-
getAxiomOnlyLogEvent({
|
|
13
|
-
axiomEvent: CONTEXT_PRUNING_COMPLETED_EVENT,
|
|
14
|
-
trigger_reason: 'context_limit',
|
|
15
|
-
client_session_id: 'turn-123',
|
|
16
|
-
dropped_user_entry_count: 2,
|
|
17
|
-
live_user_prompt_text_preserved: true,
|
|
18
|
-
prompt: 'must not leave the client',
|
|
19
|
-
nested: { secret: true },
|
|
20
|
-
context_token_count: Number.POSITIVE_INFINITY,
|
|
21
|
-
}),
|
|
22
|
-
).toEqual({
|
|
23
|
-
event: CONTEXT_PRUNING_COMPLETED_EVENT,
|
|
24
|
-
data: {
|
|
25
|
-
trigger_reason: 'context_limit',
|
|
26
|
-
client_session_id: 'turn-123',
|
|
27
|
-
dropped_user_entry_count: 2,
|
|
28
|
-
live_user_prompt_text_preserved: true,
|
|
29
|
-
},
|
|
30
|
-
})
|
|
31
|
-
})
|
|
32
|
-
|
|
33
|
-
test('does not treat arbitrary events as Axiom-only', () => {
|
|
34
|
-
expect(
|
|
35
|
-
getAxiomOnlyLogEvent({
|
|
36
|
-
axiomEvent: 'untrusted.event',
|
|
37
|
-
prompt: 'secret',
|
|
38
|
-
}),
|
|
39
|
-
).toBeNull()
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
test('does not treat an Object.prototype property name as a registered event', () => {
|
|
43
|
-
// The event name is caller-supplied (any logger.*(data, msg) call sets
|
|
44
|
-
// `data.axiomEvent`). Guards against ever matching it with a lookup keyed
|
|
45
|
-
// on that name (e.g. a plain-object registry), where 'constructor' would
|
|
46
|
-
// resolve through the prototype chain and get treated as registered —
|
|
47
|
-
// shipping `{}` in place of the log's real payload. Must be rejected like
|
|
48
|
-
// any other unknown event, via both the data-field and event-param path.
|
|
49
|
-
for (const poisonEvent of [
|
|
50
|
-
'constructor',
|
|
51
|
-
'toString',
|
|
52
|
-
'hasOwnProperty',
|
|
53
|
-
'valueOf',
|
|
54
|
-
'__proto__',
|
|
55
|
-
]) {
|
|
56
|
-
expect(
|
|
57
|
-
getAxiomOnlyLogEvent({
|
|
58
|
-
axiomEvent: poisonEvent,
|
|
59
|
-
prompt: 'must not be silently dropped',
|
|
60
|
-
}),
|
|
61
|
-
).toBeNull()
|
|
62
|
-
expect(
|
|
63
|
-
getAxiomOnlyLogEvent({ prompt: 'must not be silently dropped' }, poisonEvent),
|
|
64
|
-
).toBeNull()
|
|
65
|
-
}
|
|
66
|
-
})
|
|
67
|
-
|
|
68
|
-
test('sanitizes the client wire format identified by its top-level event', () => {
|
|
69
|
-
expect(
|
|
70
|
-
getAxiomOnlyLogEvent(
|
|
71
|
-
{
|
|
72
|
-
dropped_user_entry_count: 2,
|
|
73
|
-
prompt: 'must not reach Axiom',
|
|
74
|
-
},
|
|
75
|
-
CONTEXT_PRUNING_COMPLETED_EVENT,
|
|
76
|
-
),
|
|
77
|
-
).toEqual({
|
|
78
|
-
event: CONTEXT_PRUNING_COMPLETED_EVENT,
|
|
79
|
-
data: { dropped_user_entry_count: 2 },
|
|
80
|
-
})
|
|
81
|
-
})
|
|
82
|
-
|
|
83
|
-
test('accepts an allowlisted top-level event with empty data', () => {
|
|
84
|
-
expect(getAxiomOnlyLogEvent(null, CONTEXT_PRUNING_COMPLETED_EVENT)).toEqual(
|
|
85
|
-
{
|
|
86
|
-
event: CONTEXT_PRUNING_COMPLETED_EVENT,
|
|
87
|
-
data: {},
|
|
88
|
-
},
|
|
89
|
-
)
|
|
90
|
-
})
|
|
91
|
-
|
|
92
|
-
test('sanitizes stream-recovery metadata', () => {
|
|
93
|
-
expect(
|
|
94
|
-
getAxiomOnlyLogEvent({
|
|
95
|
-
axiomEvent: STREAM_RECOVERY_EVENT,
|
|
96
|
-
metric: 'stream_recovery_detected',
|
|
97
|
-
source: 'stream-interrupted',
|
|
98
|
-
model: 'openrouter/anthropic/claude-sonnet-4.5',
|
|
99
|
-
agentId: 'base2',
|
|
100
|
-
runId: 'run-123',
|
|
101
|
-
userInputId: 'input-456',
|
|
102
|
-
finishReason: 'unknown',
|
|
103
|
-
hasYieldedContent: true,
|
|
104
|
-
consecutive: 2,
|
|
105
|
-
// Not in the allowlist: must not leak through.
|
|
106
|
-
userId: 'user-789',
|
|
107
|
-
message: 'must not leave the client',
|
|
108
|
-
messageHistory: [{ role: 'user', content: 'secret' }],
|
|
109
|
-
}),
|
|
110
|
-
).toEqual({
|
|
111
|
-
event: STREAM_RECOVERY_EVENT,
|
|
112
|
-
data: {
|
|
113
|
-
metric: 'stream_recovery_detected',
|
|
114
|
-
source: 'stream-interrupted',
|
|
115
|
-
model: 'openrouter/anthropic/claude-sonnet-4.5',
|
|
116
|
-
agentId: 'base2',
|
|
117
|
-
runId: 'run-123',
|
|
118
|
-
userInputId: 'input-456',
|
|
119
|
-
finishReason: 'unknown',
|
|
120
|
-
hasYieldedContent: true,
|
|
121
|
-
consecutive: 2,
|
|
122
|
-
},
|
|
123
|
-
})
|
|
124
|
-
})
|
|
125
|
-
|
|
126
|
-
test('drops a stream-recovery field with the wrong value type', () => {
|
|
127
|
-
// consecutive must be a number; a string value for it (or any other
|
|
128
|
-
// type mismatch) is dropped rather than coerced.
|
|
129
|
-
expect(
|
|
130
|
-
getAxiomOnlyLogEvent({
|
|
131
|
-
axiomEvent: STREAM_RECOVERY_EVENT,
|
|
132
|
-
metric: 'stream_recovery_rescued',
|
|
133
|
-
consecutive: '2',
|
|
134
|
-
}),
|
|
135
|
-
).toEqual({
|
|
136
|
-
event: STREAM_RECOVERY_EVENT,
|
|
137
|
-
data: { metric: 'stream_recovery_rescued' },
|
|
138
|
-
})
|
|
139
|
-
})
|
|
140
|
-
})
|
|
@@ -1,148 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'bun:test'
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
UNRECOGNIZED_CLIENT,
|
|
5
|
-
clientUserAgentFields,
|
|
6
|
-
normalizeClientUserAgent,
|
|
7
|
-
} from '../client-user-agent'
|
|
8
|
-
|
|
9
|
-
describe('normalizeClientUserAgent', () => {
|
|
10
|
-
it('parses the official CLI agent', () => {
|
|
11
|
-
// What getCliAdRequestUserAgent() emits.
|
|
12
|
-
expect(normalizeClientUserAgent('Anvil-CLI/0.0.138')).toEqual({
|
|
13
|
-
product: 'anvil-cli',
|
|
14
|
-
version: '0.0.138',
|
|
15
|
-
})
|
|
16
|
-
expect(normalizeClientUserAgent('Anvil-CLI/1.0.685')).toEqual({
|
|
17
|
-
product: 'anvil-cli',
|
|
18
|
-
version: '1.0.685',
|
|
19
|
-
})
|
|
20
|
-
})
|
|
21
|
-
|
|
22
|
-
it('parses runtime and SDK agents a non-official client would send', () => {
|
|
23
|
-
expect(normalizeClientUserAgent('Go-http-client/2.0')).toEqual({
|
|
24
|
-
product: 'go-http-client',
|
|
25
|
-
version: '2.0',
|
|
26
|
-
})
|
|
27
|
-
expect(normalizeClientUserAgent('Bun/1.3.11')).toEqual({
|
|
28
|
-
product: 'bun',
|
|
29
|
-
version: '1.3.11',
|
|
30
|
-
})
|
|
31
|
-
// The UA a published Anvil proxy sends. The second segment is not a
|
|
32
|
-
// version, so it is dropped — `ai-sdk` alone still separates it from
|
|
33
|
-
// `anvil-cli`, which is the whole point.
|
|
34
|
-
expect(
|
|
35
|
-
normalizeClientUserAgent('ai-sdk/openai-compatible/1.0.25/anvil'),
|
|
36
|
-
).toEqual({ product: 'ai-sdk' })
|
|
37
|
-
})
|
|
38
|
-
|
|
39
|
-
it('keeps only the leading product token, dropping platform detail', () => {
|
|
40
|
-
// The PII guard: everything after the first whitespace is never read, so
|
|
41
|
-
// OS/device strings cannot reach an event payload.
|
|
42
|
-
const browser =
|
|
43
|
-
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36'
|
|
44
|
-
expect(normalizeClientUserAgent(browser)).toEqual({
|
|
45
|
-
product: 'mozilla',
|
|
46
|
-
version: '5.0',
|
|
47
|
-
})
|
|
48
|
-
})
|
|
49
|
-
|
|
50
|
-
it('returns undefined for an absent or blank header', () => {
|
|
51
|
-
expect(normalizeClientUserAgent(undefined)).toBeUndefined()
|
|
52
|
-
expect(normalizeClientUserAgent(null)).toBeUndefined()
|
|
53
|
-
expect(normalizeClientUserAgent(' ')).toBeUndefined()
|
|
54
|
-
expect(clientUserAgentFields(null)).toEqual({})
|
|
55
|
-
})
|
|
56
|
-
|
|
57
|
-
it('collapses an over-long product instead of truncating it', () => {
|
|
58
|
-
// Truncating would both fabricate a name and leave long random strings as
|
|
59
|
-
// a cheap way to mint new values.
|
|
60
|
-
expect(normalizeClientUserAgent('A'.repeat(200) + '/1.0')).toEqual({
|
|
61
|
-
product: UNRECOGNIZED_CLIENT,
|
|
62
|
-
})
|
|
63
|
-
// Exactly at the cap is still kept.
|
|
64
|
-
const atCap = 'a'.repeat(32)
|
|
65
|
-
expect(normalizeClientUserAgent(`${atCap}/1.0`)).toEqual({
|
|
66
|
-
product: atCap,
|
|
67
|
-
version: '1.0',
|
|
68
|
-
})
|
|
69
|
-
// One past it collapses.
|
|
70
|
-
expect(normalizeClientUserAgent(`${'a'.repeat(33)}/1.0`)).toEqual({
|
|
71
|
-
product: UNRECOGNIZED_CLIENT,
|
|
72
|
-
})
|
|
73
|
-
})
|
|
74
|
-
|
|
75
|
-
it('collapses junk to a single constant rather than minting a value', () => {
|
|
76
|
-
// The cardinality guard: a caller varying unparseable bytes cannot create
|
|
77
|
-
// new distinct values.
|
|
78
|
-
expect(normalizeClientUserAgent('!!!@@@###')).toEqual({
|
|
79
|
-
product: UNRECOGNIZED_CLIENT,
|
|
80
|
-
})
|
|
81
|
-
expect(normalizeClientUserAgent('***/1.0')).toEqual({
|
|
82
|
-
product: UNRECOGNIZED_CLIENT,
|
|
83
|
-
})
|
|
84
|
-
})
|
|
85
|
-
|
|
86
|
-
it('rejects an address-shaped product rather than mangling it', () => {
|
|
87
|
-
// Stripping `@` would record `userexample.com`, which is still identifying.
|
|
88
|
-
expect(normalizeClientUserAgent('user@example.com/1.0')).toEqual({
|
|
89
|
-
product: UNRECOGNIZED_CLIENT,
|
|
90
|
-
})
|
|
91
|
-
// A contact address in the conventional comment position is already gone:
|
|
92
|
-
// everything after the first whitespace is never read.
|
|
93
|
-
expect(
|
|
94
|
-
normalizeClientUserAgent('MyBot/1.0 (+contact@example.com)'),
|
|
95
|
-
).toEqual({ product: 'mybot', version: '1.0' })
|
|
96
|
-
})
|
|
97
|
-
|
|
98
|
-
it('cannot inject fields into a log line', () => {
|
|
99
|
-
// Newlines and CR terminate the token before any charset stripping.
|
|
100
|
-
expect(normalizeClientUserAgent('evil\nFAKE-FIELD: 1/1.0')).toEqual({
|
|
101
|
-
product: 'evil',
|
|
102
|
-
})
|
|
103
|
-
expect(normalizeClientUserAgent('evil\r\ninjected/1.0')).toEqual({
|
|
104
|
-
product: 'evil',
|
|
105
|
-
})
|
|
106
|
-
expect(normalizeClientUserAgent('{"json":"inject"}/1.0')).toEqual({
|
|
107
|
-
product: 'jsoninject',
|
|
108
|
-
version: '1.0',
|
|
109
|
-
})
|
|
110
|
-
})
|
|
111
|
-
|
|
112
|
-
it('drops a version that does not look like a version', () => {
|
|
113
|
-
// Keeps free-form text (a plausible PII carrier) out of the version field.
|
|
114
|
-
expect(normalizeClientUserAgent('curl/not-a-version')).toEqual({
|
|
115
|
-
product: 'curl',
|
|
116
|
-
})
|
|
117
|
-
expect(normalizeClientUserAgent('someclient')).toEqual({
|
|
118
|
-
product: 'someclient',
|
|
119
|
-
})
|
|
120
|
-
})
|
|
121
|
-
|
|
122
|
-
it('drops an over-long version rather than fabricating a truncated one', () => {
|
|
123
|
-
expect(normalizeClientUserAgent('x/1' + '2'.repeat(100))).toEqual({
|
|
124
|
-
product: 'x',
|
|
125
|
-
})
|
|
126
|
-
// The case that makes truncation actively wrong: a real, legitimate semver
|
|
127
|
-
// longer than the cap. Reporting `1.0.0-beta+build` would name a version
|
|
128
|
-
// that does not exist and would collide with `...build.2`.
|
|
129
|
-
expect(normalizeClientUserAgent('x/1.0.0-beta+build.1')).toEqual({
|
|
130
|
-
product: 'x',
|
|
131
|
-
})
|
|
132
|
-
// Exactly at the cap is kept.
|
|
133
|
-
expect(normalizeClientUserAgent('x/1234567890123456')).toEqual({
|
|
134
|
-
product: 'x',
|
|
135
|
-
version: '1234567890123456',
|
|
136
|
-
})
|
|
137
|
-
})
|
|
138
|
-
|
|
139
|
-
it('omits the version key entirely when absent', () => {
|
|
140
|
-
expect(clientUserAgentFields('someclient')).toEqual({
|
|
141
|
-
client_ua_product: 'someclient',
|
|
142
|
-
})
|
|
143
|
-
expect(clientUserAgentFields('Anvil-CLI/0.0.138')).toEqual({
|
|
144
|
-
client_ua_product: 'anvil-cli',
|
|
145
|
-
client_ua_version: '0.0.138',
|
|
146
|
-
})
|
|
147
|
-
})
|
|
148
|
-
})
|