@alumbwe/anvil 1.0.42 → 1.0.49
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,919 +0,0 @@
|
|
|
1
|
-
import { describe, test, expect, afterAll, beforeEach, afterEach, mock } from 'bun:test'
|
|
2
|
-
import * as fs from 'fs'
|
|
3
|
-
import * as path from 'path'
|
|
4
|
-
import * as os from 'os'
|
|
5
|
-
|
|
6
|
-
import {
|
|
7
|
-
getAllToggleIdsFromMessages,
|
|
8
|
-
getRunStatePath,
|
|
9
|
-
getChatMessagesPath,
|
|
10
|
-
saveChatState,
|
|
11
|
-
loadMostRecentChatState,
|
|
12
|
-
clearChatState,
|
|
13
|
-
setChatDirOverrideForTesting,
|
|
14
|
-
setLiveChatStateProvider,
|
|
15
|
-
clearLiveChatStateProvider,
|
|
16
|
-
flushLiveChatState,
|
|
17
|
-
scheduleCheckpointSave,
|
|
18
|
-
settleCheckpointSave,
|
|
19
|
-
} from '../run-state-storage'
|
|
20
|
-
import type { ChatMessage, ContentBlock } from '../../types/chat'
|
|
21
|
-
import type { RunState } from '@anvil/sdk'
|
|
22
|
-
|
|
23
|
-
/** Every directory below hangs off a root this run owns. They were fixed
|
|
24
|
-
* `os.tmpdir()/anvil-test-*` paths that `beforeEach` deletes recursively,
|
|
25
|
-
* so an overlapping run wiped the chat state this one was still writing (4 of
|
|
26
|
-
* 10 overlapped runs failed; solo always passed). Only the parent is unique —
|
|
27
|
-
* the names below stay stable, which is what the assertions read. See
|
|
28
|
-
* docs/testing.md. */
|
|
29
|
-
const TEST_ROOT = fs.mkdtempSync(path.join(os.tmpdir(), 'cli-run-state-'))
|
|
30
|
-
|
|
31
|
-
afterAll(() => {
|
|
32
|
-
fs.rmSync(TEST_ROOT, { recursive: true, force: true })
|
|
33
|
-
})
|
|
34
|
-
|
|
35
|
-
// Mock the project-files module
|
|
36
|
-
const mockProjectDataDir = path.join(TEST_ROOT, 'anvil-test-project')
|
|
37
|
-
const mockCurrentChatDir = path.join(
|
|
38
|
-
mockProjectDataDir,
|
|
39
|
-
'chats',
|
|
40
|
-
'test-chat-123',
|
|
41
|
-
)
|
|
42
|
-
|
|
43
|
-
// Mock the module before importing
|
|
44
|
-
const originalGetProjectDataDir = () => mockProjectDataDir
|
|
45
|
-
const originalGetCurrentChatDir = () => mockCurrentChatDir
|
|
46
|
-
|
|
47
|
-
describe('run-state-storage', () => {
|
|
48
|
-
beforeEach(() => {
|
|
49
|
-
// Create test directories
|
|
50
|
-
if (fs.existsSync(mockProjectDataDir)) {
|
|
51
|
-
fs.rmSync(mockProjectDataDir, { recursive: true })
|
|
52
|
-
}
|
|
53
|
-
fs.mkdirSync(mockCurrentChatDir, { recursive: true })
|
|
54
|
-
})
|
|
55
|
-
|
|
56
|
-
afterEach(() => {
|
|
57
|
-
// Clean up test directories
|
|
58
|
-
if (fs.existsSync(mockProjectDataDir)) {
|
|
59
|
-
fs.rmSync(mockProjectDataDir, { recursive: true })
|
|
60
|
-
}
|
|
61
|
-
})
|
|
62
|
-
|
|
63
|
-
describe('getAllToggleIdsFromMessages', () => {
|
|
64
|
-
test('extracts agent IDs from messages', () => {
|
|
65
|
-
const messages: ChatMessage[] = [
|
|
66
|
-
{
|
|
67
|
-
id: 'msg-1',
|
|
68
|
-
variant: 'agent',
|
|
69
|
-
content: '',
|
|
70
|
-
timestamp: new Date().toISOString(),
|
|
71
|
-
blocks: [
|
|
72
|
-
{
|
|
73
|
-
type: 'agent',
|
|
74
|
-
agentId: 'agent-1',
|
|
75
|
-
agentName: 'TestAgent',
|
|
76
|
-
agentType: 'inline',
|
|
77
|
-
content: '',
|
|
78
|
-
status: 'complete',
|
|
79
|
-
blocks: [],
|
|
80
|
-
},
|
|
81
|
-
],
|
|
82
|
-
},
|
|
83
|
-
]
|
|
84
|
-
|
|
85
|
-
const ids = getAllToggleIdsFromMessages(messages)
|
|
86
|
-
|
|
87
|
-
expect(ids).toContain('agent-1')
|
|
88
|
-
})
|
|
89
|
-
|
|
90
|
-
test('extracts tool call IDs from messages', () => {
|
|
91
|
-
const messages: ChatMessage[] = [
|
|
92
|
-
{
|
|
93
|
-
id: 'msg-1',
|
|
94
|
-
variant: 'agent',
|
|
95
|
-
content: '',
|
|
96
|
-
timestamp: new Date().toISOString(),
|
|
97
|
-
blocks: [
|
|
98
|
-
{
|
|
99
|
-
type: 'tool',
|
|
100
|
-
toolCallId: 'tool-1',
|
|
101
|
-
toolName: 'glob',
|
|
102
|
-
input: {},
|
|
103
|
-
output: '',
|
|
104
|
-
},
|
|
105
|
-
],
|
|
106
|
-
},
|
|
107
|
-
]
|
|
108
|
-
|
|
109
|
-
const ids = getAllToggleIdsFromMessages(messages)
|
|
110
|
-
|
|
111
|
-
expect(ids).toContain('tool-1')
|
|
112
|
-
})
|
|
113
|
-
|
|
114
|
-
test('recursively extracts IDs from nested agent blocks', () => {
|
|
115
|
-
const messages: ChatMessage[] = [
|
|
116
|
-
{
|
|
117
|
-
id: 'msg-1',
|
|
118
|
-
variant: 'agent',
|
|
119
|
-
content: '',
|
|
120
|
-
timestamp: new Date().toISOString(),
|
|
121
|
-
blocks: [
|
|
122
|
-
{
|
|
123
|
-
type: 'agent',
|
|
124
|
-
agentId: 'parent-agent',
|
|
125
|
-
agentName: 'ParentAgent',
|
|
126
|
-
agentType: 'inline',
|
|
127
|
-
content: '',
|
|
128
|
-
status: 'complete',
|
|
129
|
-
blocks: [
|
|
130
|
-
{
|
|
131
|
-
type: 'tool',
|
|
132
|
-
toolCallId: 'nested-tool',
|
|
133
|
-
toolName: 'glob',
|
|
134
|
-
input: {},
|
|
135
|
-
output: '',
|
|
136
|
-
},
|
|
137
|
-
{
|
|
138
|
-
type: 'agent',
|
|
139
|
-
agentId: 'child-agent',
|
|
140
|
-
agentName: 'ChildAgent',
|
|
141
|
-
agentType: 'inline',
|
|
142
|
-
content: '',
|
|
143
|
-
status: 'complete',
|
|
144
|
-
blocks: [
|
|
145
|
-
{
|
|
146
|
-
type: 'tool',
|
|
147
|
-
toolCallId: 'deep-tool',
|
|
148
|
-
toolName: 'glob',
|
|
149
|
-
input: {},
|
|
150
|
-
output: '',
|
|
151
|
-
},
|
|
152
|
-
],
|
|
153
|
-
},
|
|
154
|
-
],
|
|
155
|
-
},
|
|
156
|
-
],
|
|
157
|
-
},
|
|
158
|
-
]
|
|
159
|
-
|
|
160
|
-
const ids = getAllToggleIdsFromMessages(messages)
|
|
161
|
-
|
|
162
|
-
expect(ids).toContain('parent-agent')
|
|
163
|
-
expect(ids).toContain('nested-tool')
|
|
164
|
-
expect(ids).toContain('child-agent')
|
|
165
|
-
expect(ids).toContain('deep-tool')
|
|
166
|
-
})
|
|
167
|
-
|
|
168
|
-
test('handles messages with no blocks', () => {
|
|
169
|
-
const messages: ChatMessage[] = [
|
|
170
|
-
{
|
|
171
|
-
id: 'msg-1',
|
|
172
|
-
variant: 'user',
|
|
173
|
-
content: '',
|
|
174
|
-
timestamp: new Date().toISOString(),
|
|
175
|
-
blocks: [],
|
|
176
|
-
},
|
|
177
|
-
]
|
|
178
|
-
|
|
179
|
-
const ids = getAllToggleIdsFromMessages(messages)
|
|
180
|
-
|
|
181
|
-
expect(ids).toHaveLength(0)
|
|
182
|
-
})
|
|
183
|
-
|
|
184
|
-
test('handles empty messages array', () => {
|
|
185
|
-
const ids = getAllToggleIdsFromMessages([])
|
|
186
|
-
expect(ids).toHaveLength(0)
|
|
187
|
-
})
|
|
188
|
-
|
|
189
|
-
test('handles mixed block types in single message', () => {
|
|
190
|
-
const messages: ChatMessage[] = [
|
|
191
|
-
{
|
|
192
|
-
id: 'msg-1',
|
|
193
|
-
variant: 'agent',
|
|
194
|
-
content: '',
|
|
195
|
-
timestamp: new Date().toISOString(),
|
|
196
|
-
blocks: [
|
|
197
|
-
{ type: 'text', content: 'Some text' },
|
|
198
|
-
{
|
|
199
|
-
type: 'agent',
|
|
200
|
-
agentId: 'agent-1',
|
|
201
|
-
agentName: 'TestAgent',
|
|
202
|
-
agentType: 'inline',
|
|
203
|
-
content: '',
|
|
204
|
-
status: 'complete',
|
|
205
|
-
blocks: [],
|
|
206
|
-
},
|
|
207
|
-
{
|
|
208
|
-
type: 'tool',
|
|
209
|
-
toolCallId: 'tool-1',
|
|
210
|
-
toolName: 'glob',
|
|
211
|
-
input: {},
|
|
212
|
-
output: '',
|
|
213
|
-
},
|
|
214
|
-
],
|
|
215
|
-
},
|
|
216
|
-
]
|
|
217
|
-
|
|
218
|
-
const ids = getAllToggleIdsFromMessages(messages)
|
|
219
|
-
|
|
220
|
-
expect(ids).toContain('agent-1')
|
|
221
|
-
expect(ids).toContain('tool-1')
|
|
222
|
-
expect(ids).toHaveLength(2)
|
|
223
|
-
})
|
|
224
|
-
|
|
225
|
-
test('does not deduplicate IDs (returns all occurrences)', () => {
|
|
226
|
-
const messages: ChatMessage[] = [
|
|
227
|
-
{
|
|
228
|
-
id: 'msg-1',
|
|
229
|
-
variant: 'agent',
|
|
230
|
-
content: '',
|
|
231
|
-
timestamp: new Date().toISOString(),
|
|
232
|
-
blocks: [
|
|
233
|
-
{
|
|
234
|
-
type: 'agent',
|
|
235
|
-
agentId: 'shared-id',
|
|
236
|
-
agentName: 'TestAgent',
|
|
237
|
-
agentType: 'inline',
|
|
238
|
-
content: '',
|
|
239
|
-
status: 'complete',
|
|
240
|
-
blocks: [],
|
|
241
|
-
},
|
|
242
|
-
],
|
|
243
|
-
},
|
|
244
|
-
{
|
|
245
|
-
id: 'msg-2',
|
|
246
|
-
variant: 'agent',
|
|
247
|
-
content: '',
|
|
248
|
-
timestamp: new Date().toISOString(),
|
|
249
|
-
blocks: [
|
|
250
|
-
{
|
|
251
|
-
type: 'tool',
|
|
252
|
-
toolCallId: 'shared-id',
|
|
253
|
-
toolName: 'glob',
|
|
254
|
-
input: {},
|
|
255
|
-
output: '',
|
|
256
|
-
},
|
|
257
|
-
],
|
|
258
|
-
},
|
|
259
|
-
]
|
|
260
|
-
|
|
261
|
-
const ids = getAllToggleIdsFromMessages(messages)
|
|
262
|
-
|
|
263
|
-
// Current implementation returns all occurrences without deduplication
|
|
264
|
-
expect(ids.filter((id) => id === 'shared-id')).toHaveLength(2)
|
|
265
|
-
})
|
|
266
|
-
})
|
|
267
|
-
|
|
268
|
-
describe('getRunStatePath', () => {
|
|
269
|
-
test('returns path with correct filename', () => {
|
|
270
|
-
// We need to mock the internal functions
|
|
271
|
-
// This is a simplified test - in reality we'd need to mock the module
|
|
272
|
-
const testPath = path.join(mockCurrentChatDir, 'run-state.json')
|
|
273
|
-
expect(testPath).toContain('run-state.json')
|
|
274
|
-
})
|
|
275
|
-
})
|
|
276
|
-
|
|
277
|
-
describe('getChatMessagesPath', () => {
|
|
278
|
-
test('returns path with correct filename', () => {
|
|
279
|
-
const testPath = path.join(mockCurrentChatDir, 'chat-messages.json')
|
|
280
|
-
expect(testPath).toContain('chat-messages.json')
|
|
281
|
-
})
|
|
282
|
-
})
|
|
283
|
-
|
|
284
|
-
describe('file serialization format', () => {
|
|
285
|
-
test('run state JSON structure is preserved through serialization', () => {
|
|
286
|
-
const runState: RunState = {
|
|
287
|
-
output: {
|
|
288
|
-
type: 'error',
|
|
289
|
-
message: 'Test output',
|
|
290
|
-
},
|
|
291
|
-
} as unknown as RunState
|
|
292
|
-
|
|
293
|
-
const runStatePath = path.join(mockCurrentChatDir, 'run-state.json')
|
|
294
|
-
fs.writeFileSync(runStatePath, JSON.stringify(runState, null, 2))
|
|
295
|
-
|
|
296
|
-
const savedRunState = JSON.parse(fs.readFileSync(runStatePath, 'utf8'))
|
|
297
|
-
expect(savedRunState.output.type).toBe('error')
|
|
298
|
-
expect(savedRunState.output.message).toBe('Test output')
|
|
299
|
-
})
|
|
300
|
-
|
|
301
|
-
test('messages JSON structure is preserved through serialization', () => {
|
|
302
|
-
const messages: ChatMessage[] = [
|
|
303
|
-
{
|
|
304
|
-
id: 'msg-1',
|
|
305
|
-
variant: 'user',
|
|
306
|
-
content: 'Hello',
|
|
307
|
-
timestamp: new Date().toISOString(),
|
|
308
|
-
blocks: [{ type: 'text', content: 'Hello' }],
|
|
309
|
-
},
|
|
310
|
-
]
|
|
311
|
-
|
|
312
|
-
const messagesPath = path.join(mockCurrentChatDir, 'chat-messages.json')
|
|
313
|
-
fs.writeFileSync(messagesPath, JSON.stringify(messages, null, 2))
|
|
314
|
-
|
|
315
|
-
const savedMessages = JSON.parse(fs.readFileSync(messagesPath, 'utf8'))
|
|
316
|
-
expect(savedMessages).toHaveLength(1)
|
|
317
|
-
expect(savedMessages[0].variant).toBe('user')
|
|
318
|
-
})
|
|
319
|
-
|
|
320
|
-
test('nested message structure is preserved through serialization', () => {
|
|
321
|
-
const messages: ChatMessage[] = [
|
|
322
|
-
{
|
|
323
|
-
id: 'msg-1',
|
|
324
|
-
variant: 'agent',
|
|
325
|
-
content: '',
|
|
326
|
-
timestamp: new Date().toISOString(),
|
|
327
|
-
blocks: [
|
|
328
|
-
{
|
|
329
|
-
type: 'agent',
|
|
330
|
-
agentId: 'nested-agent',
|
|
331
|
-
agentName: 'NestedAgent',
|
|
332
|
-
agentType: 'inline',
|
|
333
|
-
content: '',
|
|
334
|
-
status: 'complete',
|
|
335
|
-
blocks: [
|
|
336
|
-
{ type: 'text', content: 'Nested content' },
|
|
337
|
-
{
|
|
338
|
-
type: 'tool',
|
|
339
|
-
toolCallId: 'tool-xyz',
|
|
340
|
-
toolName: 'glob',
|
|
341
|
-
input: {},
|
|
342
|
-
output: '',
|
|
343
|
-
},
|
|
344
|
-
],
|
|
345
|
-
},
|
|
346
|
-
],
|
|
347
|
-
},
|
|
348
|
-
]
|
|
349
|
-
|
|
350
|
-
const messagesPath = path.join(mockCurrentChatDir, 'chat-messages.json')
|
|
351
|
-
fs.writeFileSync(messagesPath, JSON.stringify(messages, null, 2))
|
|
352
|
-
|
|
353
|
-
const savedMessages = JSON.parse(fs.readFileSync(messagesPath, 'utf8'))
|
|
354
|
-
expect(savedMessages[0].blocks[0].type).toBe('agent')
|
|
355
|
-
expect(savedMessages[0].blocks[0].blocks).toHaveLength(2)
|
|
356
|
-
})
|
|
357
|
-
})
|
|
358
|
-
|
|
359
|
-
describe('edge cases', () => {
|
|
360
|
-
test('handles empty blocks array', () => {
|
|
361
|
-
const messages: ChatMessage[] = [
|
|
362
|
-
{
|
|
363
|
-
id: 'msg-1',
|
|
364
|
-
variant: 'agent',
|
|
365
|
-
content: '',
|
|
366
|
-
timestamp: new Date().toISOString(),
|
|
367
|
-
blocks: [],
|
|
368
|
-
},
|
|
369
|
-
]
|
|
370
|
-
|
|
371
|
-
const ids = getAllToggleIdsFromMessages(messages)
|
|
372
|
-
expect(ids).toHaveLength(0)
|
|
373
|
-
})
|
|
374
|
-
|
|
375
|
-
test('handles deeply nested structure', () => {
|
|
376
|
-
const deepBlock: ContentBlock = {
|
|
377
|
-
type: 'agent',
|
|
378
|
-
agentId: 'level-0',
|
|
379
|
-
agentName: 'Level0Agent',
|
|
380
|
-
agentType: 'inline',
|
|
381
|
-
content: '',
|
|
382
|
-
status: 'complete',
|
|
383
|
-
blocks: [
|
|
384
|
-
{
|
|
385
|
-
type: 'agent',
|
|
386
|
-
agentId: 'level-1',
|
|
387
|
-
agentName: 'Level1Agent',
|
|
388
|
-
agentType: 'inline',
|
|
389
|
-
content: '',
|
|
390
|
-
status: 'complete',
|
|
391
|
-
blocks: [
|
|
392
|
-
{
|
|
393
|
-
type: 'agent',
|
|
394
|
-
agentId: 'level-2',
|
|
395
|
-
agentName: 'Level2Agent',
|
|
396
|
-
agentType: 'inline',
|
|
397
|
-
content: '',
|
|
398
|
-
status: 'complete',
|
|
399
|
-
blocks: [
|
|
400
|
-
{
|
|
401
|
-
type: 'tool',
|
|
402
|
-
toolCallId: 'deep-tool',
|
|
403
|
-
toolName: 'glob',
|
|
404
|
-
input: {},
|
|
405
|
-
output: '',
|
|
406
|
-
},
|
|
407
|
-
],
|
|
408
|
-
},
|
|
409
|
-
],
|
|
410
|
-
},
|
|
411
|
-
],
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
const messages: ChatMessage[] = [
|
|
415
|
-
{
|
|
416
|
-
id: 'msg-1',
|
|
417
|
-
variant: 'agent',
|
|
418
|
-
content: '',
|
|
419
|
-
timestamp: new Date().toISOString(),
|
|
420
|
-
blocks: [deepBlock],
|
|
421
|
-
},
|
|
422
|
-
]
|
|
423
|
-
|
|
424
|
-
const ids = getAllToggleIdsFromMessages(messages)
|
|
425
|
-
|
|
426
|
-
expect(ids).toContain('level-0')
|
|
427
|
-
expect(ids).toContain('level-1')
|
|
428
|
-
expect(ids).toContain('level-2')
|
|
429
|
-
expect(ids).toContain('deep-tool')
|
|
430
|
-
})
|
|
431
|
-
|
|
432
|
-
test('preserves order of IDs as encountered', () => {
|
|
433
|
-
const messages: ChatMessage[] = [
|
|
434
|
-
{
|
|
435
|
-
id: 'msg-1',
|
|
436
|
-
variant: 'agent',
|
|
437
|
-
content: '',
|
|
438
|
-
timestamp: new Date().toISOString(),
|
|
439
|
-
blocks: [
|
|
440
|
-
{
|
|
441
|
-
type: 'agent',
|
|
442
|
-
agentId: 'first',
|
|
443
|
-
agentName: 'FirstAgent',
|
|
444
|
-
agentType: 'inline',
|
|
445
|
-
content: '',
|
|
446
|
-
status: 'complete',
|
|
447
|
-
blocks: [],
|
|
448
|
-
},
|
|
449
|
-
{
|
|
450
|
-
type: 'tool',
|
|
451
|
-
toolCallId: 'second',
|
|
452
|
-
toolName: 'glob',
|
|
453
|
-
input: {},
|
|
454
|
-
output: '',
|
|
455
|
-
},
|
|
456
|
-
{
|
|
457
|
-
type: 'agent',
|
|
458
|
-
agentId: 'third',
|
|
459
|
-
agentName: 'ThirdAgent',
|
|
460
|
-
agentType: 'inline',
|
|
461
|
-
content: '',
|
|
462
|
-
status: 'complete',
|
|
463
|
-
blocks: [],
|
|
464
|
-
},
|
|
465
|
-
],
|
|
466
|
-
},
|
|
467
|
-
]
|
|
468
|
-
|
|
469
|
-
const ids = getAllToggleIdsFromMessages(messages)
|
|
470
|
-
|
|
471
|
-
expect(ids[0]).toBe('first')
|
|
472
|
-
expect(ids[1]).toBe('second')
|
|
473
|
-
expect(ids[2]).toBe('third')
|
|
474
|
-
})
|
|
475
|
-
})
|
|
476
|
-
})
|
|
477
|
-
|
|
478
|
-
describe('live chat state provider', () => {
|
|
479
|
-
// Point persistence at a temp dir via the explicit test override — module
|
|
480
|
-
// seams (mock.module, HOME, spyOn on auth) are unreliable across bun test
|
|
481
|
-
// files and platforms.
|
|
482
|
-
const chatDir = path.join(TEST_ROOT, 'anvil-test-live-chatdir')
|
|
483
|
-
|
|
484
|
-
const testRunState = (marker: string): RunState =>
|
|
485
|
-
({
|
|
486
|
-
output: { type: 'error', message: marker },
|
|
487
|
-
}) as unknown as RunState
|
|
488
|
-
|
|
489
|
-
const testMessages = (marker: string): ChatMessage[] => [
|
|
490
|
-
{
|
|
491
|
-
id: 'msg-1',
|
|
492
|
-
variant: 'user',
|
|
493
|
-
content: marker,
|
|
494
|
-
timestamp: new Date().toISOString(),
|
|
495
|
-
},
|
|
496
|
-
]
|
|
497
|
-
|
|
498
|
-
const readSavedMessages = () =>
|
|
499
|
-
JSON.parse(
|
|
500
|
-
fs.readFileSync(path.join(chatDir, 'chat-messages.json'), 'utf8'),
|
|
501
|
-
) as ChatMessage[]
|
|
502
|
-
|
|
503
|
-
beforeEach(() => {
|
|
504
|
-
fs.rmSync(chatDir, { recursive: true, force: true })
|
|
505
|
-
setChatDirOverrideForTesting(chatDir)
|
|
506
|
-
})
|
|
507
|
-
|
|
508
|
-
afterEach(() => {
|
|
509
|
-
clearLiveChatStateProvider('run-a')
|
|
510
|
-
clearLiveChatStateProvider('run-b')
|
|
511
|
-
setChatDirOverrideForTesting(undefined)
|
|
512
|
-
fs.rmSync(chatDir, { recursive: true, force: true })
|
|
513
|
-
})
|
|
514
|
-
|
|
515
|
-
test('flushLiveChatState persists the provided state', () => {
|
|
516
|
-
setLiveChatStateProvider('run-a', () => ({
|
|
517
|
-
runState: testRunState('checkpoint'),
|
|
518
|
-
messages: testMessages('in-flight prompt'),
|
|
519
|
-
}))
|
|
520
|
-
|
|
521
|
-
flushLiveChatState()
|
|
522
|
-
|
|
523
|
-
expect(readSavedMessages()[0].content).toBe('in-flight prompt')
|
|
524
|
-
})
|
|
525
|
-
|
|
526
|
-
test('flushLiveChatState is a no-op with no provider registered', () => {
|
|
527
|
-
flushLiveChatState()
|
|
528
|
-
|
|
529
|
-
expect(fs.existsSync(path.join(chatDir, 'chat-messages.json'))).toBe(false)
|
|
530
|
-
})
|
|
531
|
-
|
|
532
|
-
test('clearLiveChatStateProvider stops flushing', () => {
|
|
533
|
-
setLiveChatStateProvider('run-a', () => ({
|
|
534
|
-
runState: testRunState('checkpoint'),
|
|
535
|
-
messages: testMessages('in-flight prompt'),
|
|
536
|
-
}))
|
|
537
|
-
clearLiveChatStateProvider('run-a')
|
|
538
|
-
|
|
539
|
-
flushLiveChatState()
|
|
540
|
-
|
|
541
|
-
expect(fs.existsSync(path.join(chatDir, 'chat-messages.json'))).toBe(false)
|
|
542
|
-
})
|
|
543
|
-
|
|
544
|
-
test('a stale run cannot clear a newer run provider', () => {
|
|
545
|
-
setLiveChatStateProvider('run-a', () => ({
|
|
546
|
-
runState: testRunState('old'),
|
|
547
|
-
messages: testMessages('old prompt'),
|
|
548
|
-
}))
|
|
549
|
-
setLiveChatStateProvider('run-b', () => ({
|
|
550
|
-
runState: testRunState('new'),
|
|
551
|
-
messages: testMessages('new prompt'),
|
|
552
|
-
}))
|
|
553
|
-
|
|
554
|
-
// The old run settling late must not remove the new run's provider.
|
|
555
|
-
clearLiveChatStateProvider('run-a')
|
|
556
|
-
flushLiveChatState()
|
|
557
|
-
|
|
558
|
-
expect(readSavedMessages()[0].content).toBe('new prompt')
|
|
559
|
-
})
|
|
560
|
-
|
|
561
|
-
test('flushLiveChatState swallows provider errors', () => {
|
|
562
|
-
setLiveChatStateProvider('run-a', () => {
|
|
563
|
-
throw new Error('boom')
|
|
564
|
-
})
|
|
565
|
-
|
|
566
|
-
expect(() => flushLiveChatState()).not.toThrow()
|
|
567
|
-
})
|
|
568
|
-
})
|
|
569
|
-
|
|
570
|
-
describe('atomic save and resilient load', () => {
|
|
571
|
-
const chatDir = path.join(TEST_ROOT, 'anvil-test-resilient-chatdir')
|
|
572
|
-
|
|
573
|
-
const runState = { output: { type: 'error', message: 'x' } } as RunState
|
|
574
|
-
const messages: ChatMessage[] = [
|
|
575
|
-
{
|
|
576
|
-
id: 'msg-1',
|
|
577
|
-
variant: 'user',
|
|
578
|
-
content: 'the prompt',
|
|
579
|
-
timestamp: new Date().toISOString(),
|
|
580
|
-
},
|
|
581
|
-
]
|
|
582
|
-
|
|
583
|
-
beforeEach(() => {
|
|
584
|
-
fs.rmSync(chatDir, { recursive: true, force: true })
|
|
585
|
-
setChatDirOverrideForTesting(chatDir)
|
|
586
|
-
})
|
|
587
|
-
|
|
588
|
-
afterEach(() => {
|
|
589
|
-
setChatDirOverrideForTesting(undefined)
|
|
590
|
-
fs.rmSync(chatDir, { recursive: true, force: true })
|
|
591
|
-
})
|
|
592
|
-
|
|
593
|
-
test('saveChatState leaves no .tmp files behind', () => {
|
|
594
|
-
saveChatState(runState, messages)
|
|
595
|
-
|
|
596
|
-
const leftovers = fs
|
|
597
|
-
.readdirSync(chatDir)
|
|
598
|
-
.filter((name) => name.endsWith('.tmp'))
|
|
599
|
-
expect(leftovers).toHaveLength(0)
|
|
600
|
-
expect(
|
|
601
|
-
JSON.parse(
|
|
602
|
-
fs.readFileSync(path.join(chatDir, 'chat-messages.json'), 'utf8'),
|
|
603
|
-
),
|
|
604
|
-
).toHaveLength(1)
|
|
605
|
-
})
|
|
606
|
-
|
|
607
|
-
test('torn run-state.json still restores the transcript', () => {
|
|
608
|
-
saveChatState(runState, messages)
|
|
609
|
-
fs.writeFileSync(
|
|
610
|
-
path.join(chatDir, 'run-state.json'),
|
|
611
|
-
'{"sessionState": {"trunc',
|
|
612
|
-
)
|
|
613
|
-
|
|
614
|
-
const loaded = loadMostRecentChatState()
|
|
615
|
-
|
|
616
|
-
expect(loaded).not.toBeNull()
|
|
617
|
-
expect(loaded!.messages[0].content).toBe('the prompt')
|
|
618
|
-
expect(loaded!.runState.output.type).toBe('error')
|
|
619
|
-
})
|
|
620
|
-
|
|
621
|
-
test('torn chat-messages.json still restores the run state', () => {
|
|
622
|
-
saveChatState(runState, messages)
|
|
623
|
-
fs.writeFileSync(path.join(chatDir, 'chat-messages.json'), '[{"id":')
|
|
624
|
-
|
|
625
|
-
const loaded = loadMostRecentChatState()
|
|
626
|
-
|
|
627
|
-
expect(loaded).not.toBeNull()
|
|
628
|
-
expect(loaded!.messages).toHaveLength(0)
|
|
629
|
-
expect((loaded!.runState.output as any).message).toBe('x')
|
|
630
|
-
})
|
|
631
|
-
|
|
632
|
-
test('returns null when both files are unreadable', () => {
|
|
633
|
-
saveChatState(runState, messages)
|
|
634
|
-
fs.writeFileSync(path.join(chatDir, 'run-state.json'), '{')
|
|
635
|
-
fs.writeFileSync(path.join(chatDir, 'chat-messages.json'), '[')
|
|
636
|
-
|
|
637
|
-
expect(loadMostRecentChatState()).toBeNull()
|
|
638
|
-
})
|
|
639
|
-
})
|
|
640
|
-
|
|
641
|
-
describe('scheduleCheckpointSave (async, coalescing)', () => {
|
|
642
|
-
const chatDir = path.join(TEST_ROOT, 'anvil-test-checkpoint-chatdir')
|
|
643
|
-
|
|
644
|
-
const runState = (marker: string) =>
|
|
645
|
-
({ output: { type: 'error', message: marker } }) as unknown as RunState
|
|
646
|
-
|
|
647
|
-
const messages = (marker: string): ChatMessage[] => [
|
|
648
|
-
{
|
|
649
|
-
id: 'msg-1',
|
|
650
|
-
variant: 'user',
|
|
651
|
-
content: marker,
|
|
652
|
-
timestamp: new Date().toISOString(),
|
|
653
|
-
},
|
|
654
|
-
]
|
|
655
|
-
|
|
656
|
-
const readSavedMessages = () =>
|
|
657
|
-
JSON.parse(
|
|
658
|
-
fs.readFileSync(path.join(chatDir, 'chat-messages.json'), 'utf8'),
|
|
659
|
-
) as ChatMessage[]
|
|
660
|
-
|
|
661
|
-
beforeEach(() => {
|
|
662
|
-
fs.rmSync(chatDir, { recursive: true, force: true })
|
|
663
|
-
setChatDirOverrideForTesting(chatDir)
|
|
664
|
-
})
|
|
665
|
-
|
|
666
|
-
afterEach(async () => {
|
|
667
|
-
await settleCheckpointSave()
|
|
668
|
-
setChatDirOverrideForTesting(undefined)
|
|
669
|
-
fs.rmSync(chatDir, { recursive: true, force: true })
|
|
670
|
-
})
|
|
671
|
-
|
|
672
|
-
test('persists the scheduled state after settling', async () => {
|
|
673
|
-
scheduleCheckpointSave(runState('a'), messages('first'))
|
|
674
|
-
|
|
675
|
-
await settleCheckpointSave()
|
|
676
|
-
|
|
677
|
-
expect(readSavedMessages()[0].content).toBe('first')
|
|
678
|
-
})
|
|
679
|
-
|
|
680
|
-
test('does not write synchronously (deferred off the calling tick)', () => {
|
|
681
|
-
scheduleCheckpointSave(runState('a'), messages('deferred'))
|
|
682
|
-
|
|
683
|
-
// Nothing on disk yet: the write is scheduled for a later tick.
|
|
684
|
-
expect(fs.existsSync(path.join(chatDir, 'chat-messages.json'))).toBe(false)
|
|
685
|
-
})
|
|
686
|
-
|
|
687
|
-
test('coalesces a burst to the latest state', async () => {
|
|
688
|
-
scheduleCheckpointSave(runState('a'), messages('one'))
|
|
689
|
-
scheduleCheckpointSave(runState('b'), messages('two'))
|
|
690
|
-
scheduleCheckpointSave(runState('c'), messages('three'))
|
|
691
|
-
|
|
692
|
-
await settleCheckpointSave()
|
|
693
|
-
|
|
694
|
-
// Whatever intermediate states were dropped, the newest wins.
|
|
695
|
-
expect(readSavedMessages()[0].content).toBe('three')
|
|
696
|
-
})
|
|
697
|
-
|
|
698
|
-
test('an authoritative save after settling is the last write (no clobber)', async () => {
|
|
699
|
-
scheduleCheckpointSave(runState('a'), messages('checkpoint'))
|
|
700
|
-
// settle waits for the queued async write to flush, so the synchronous
|
|
701
|
-
// final save below is guaranteed to land last.
|
|
702
|
-
await settleCheckpointSave()
|
|
703
|
-
|
|
704
|
-
saveChatState(runState('final'), messages('authoritative'))
|
|
705
|
-
// Give any lingering async write a chance to (incorrectly) land on top.
|
|
706
|
-
await new Promise((r) => setImmediate(r))
|
|
707
|
-
await settleCheckpointSave()
|
|
708
|
-
|
|
709
|
-
expect(readSavedMessages()[0].content).toBe('authoritative')
|
|
710
|
-
})
|
|
711
|
-
|
|
712
|
-
test('settleCheckpointSave is safe with nothing scheduled', async () => {
|
|
713
|
-
await expect(settleCheckpointSave()).resolves.toBeUndefined()
|
|
714
|
-
})
|
|
715
|
-
})
|
|
716
|
-
|
|
717
|
-
describe('chat switches while saves are pending', () => {
|
|
718
|
-
// Regression tests for /history threads being clobbered: save paths used to
|
|
719
|
-
// be resolved at write time from the mutable current chat id, so a pending
|
|
720
|
-
// write from chat A could land in chat B's directory after a /new or
|
|
721
|
-
// /history resume rotated the id in between.
|
|
722
|
-
const chatDirA = path.join(TEST_ROOT, 'anvil-test-switch-chat-a')
|
|
723
|
-
const chatDirB = path.join(TEST_ROOT, 'anvil-test-switch-chat-b')
|
|
724
|
-
|
|
725
|
-
const runState = (marker: string) =>
|
|
726
|
-
({ output: { type: 'error', message: marker } }) as unknown as RunState
|
|
727
|
-
|
|
728
|
-
const messages = (marker: string): ChatMessage[] => [
|
|
729
|
-
{
|
|
730
|
-
id: 'msg-1',
|
|
731
|
-
variant: 'user',
|
|
732
|
-
content: marker,
|
|
733
|
-
timestamp: new Date().toISOString(),
|
|
734
|
-
},
|
|
735
|
-
]
|
|
736
|
-
|
|
737
|
-
const messagesFileIn = (dir: string) => path.join(dir, 'chat-messages.json')
|
|
738
|
-
|
|
739
|
-
beforeEach(() => {
|
|
740
|
-
fs.rmSync(chatDirA, { recursive: true, force: true })
|
|
741
|
-
fs.rmSync(chatDirB, { recursive: true, force: true })
|
|
742
|
-
setChatDirOverrideForTesting(chatDirA)
|
|
743
|
-
})
|
|
744
|
-
|
|
745
|
-
afterEach(async () => {
|
|
746
|
-
await settleCheckpointSave()
|
|
747
|
-
clearLiveChatStateProvider('run-a')
|
|
748
|
-
setChatDirOverrideForTesting(undefined)
|
|
749
|
-
fs.rmSync(chatDirA, { recursive: true, force: true })
|
|
750
|
-
fs.rmSync(chatDirB, { recursive: true, force: true })
|
|
751
|
-
})
|
|
752
|
-
|
|
753
|
-
test('a checkpoint scheduled before a chat switch writes to the original chat dir', async () => {
|
|
754
|
-
scheduleCheckpointSave(runState('a'), messages('chat A transcript'))
|
|
755
|
-
|
|
756
|
-
// Simulate /new or a /history resume rotating the current chat while the
|
|
757
|
-
// async checkpoint write is still queued.
|
|
758
|
-
setChatDirOverrideForTesting(chatDirB)
|
|
759
|
-
await settleCheckpointSave()
|
|
760
|
-
|
|
761
|
-
const saved = JSON.parse(
|
|
762
|
-
fs.readFileSync(messagesFileIn(chatDirA), 'utf8'),
|
|
763
|
-
) as ChatMessage[]
|
|
764
|
-
expect(saved[0].content).toBe('chat A transcript')
|
|
765
|
-
expect(fs.existsSync(messagesFileIn(chatDirB))).toBe(false)
|
|
766
|
-
})
|
|
767
|
-
|
|
768
|
-
test('checkpoints for different chats do not displace each other', async () => {
|
|
769
|
-
// Chat A's final flush is queued, then the newly-active chat B schedules
|
|
770
|
-
// its own checkpoint before the queue drains. Both must be written —
|
|
771
|
-
// coalescing is per chat, not global.
|
|
772
|
-
scheduleCheckpointSave(runState('a'), messages('chat A final'), chatDirA)
|
|
773
|
-
scheduleCheckpointSave(runState('b'), messages('chat B first'), chatDirB)
|
|
774
|
-
|
|
775
|
-
await settleCheckpointSave()
|
|
776
|
-
|
|
777
|
-
const savedA = JSON.parse(
|
|
778
|
-
fs.readFileSync(messagesFileIn(chatDirA), 'utf8'),
|
|
779
|
-
) as ChatMessage[]
|
|
780
|
-
const savedB = JSON.parse(
|
|
781
|
-
fs.readFileSync(messagesFileIn(chatDirB), 'utf8'),
|
|
782
|
-
) as ChatMessage[]
|
|
783
|
-
expect(savedA[0].content).toBe('chat A final')
|
|
784
|
-
expect(savedB[0].content).toBe('chat B first')
|
|
785
|
-
})
|
|
786
|
-
|
|
787
|
-
test('saveChatState with an explicit chatDir ignores a later chat switch', () => {
|
|
788
|
-
setChatDirOverrideForTesting(chatDirB)
|
|
789
|
-
|
|
790
|
-
// chatDirA was captured while chat A was current (run start).
|
|
791
|
-
saveChatState(runState('a'), messages('chat A final'), chatDirA)
|
|
792
|
-
|
|
793
|
-
const saved = JSON.parse(
|
|
794
|
-
fs.readFileSync(messagesFileIn(chatDirA), 'utf8'),
|
|
795
|
-
) as ChatMessage[]
|
|
796
|
-
expect(saved[0].content).toBe('chat A final')
|
|
797
|
-
expect(fs.existsSync(messagesFileIn(chatDirB))).toBe(false)
|
|
798
|
-
})
|
|
799
|
-
|
|
800
|
-
test('flushLiveChatState after a chat switch writes nothing', () => {
|
|
801
|
-
setLiveChatStateProvider('run-a', () => ({
|
|
802
|
-
runState: runState('a'),
|
|
803
|
-
// After the switch the store holds the NEW chat's messages; flushing
|
|
804
|
-
// them into chat A's directory would replace A's transcript.
|
|
805
|
-
messages: messages('chat B messages'),
|
|
806
|
-
}))
|
|
807
|
-
|
|
808
|
-
setChatDirOverrideForTesting(chatDirB)
|
|
809
|
-
flushLiveChatState()
|
|
810
|
-
|
|
811
|
-
expect(fs.existsSync(messagesFileIn(chatDirA))).toBe(false)
|
|
812
|
-
expect(fs.existsSync(messagesFileIn(chatDirB))).toBe(false)
|
|
813
|
-
})
|
|
814
|
-
|
|
815
|
-
test('flushLiveChatState drains queued checkpoints synchronously on exit', () => {
|
|
816
|
-
// A chat switch aborted the run and queued its final checkpoint, then the
|
|
817
|
-
// process exits before the async drain runs. The exit flush must write
|
|
818
|
-
// the queued checkpoint (to chat A) even though the current chat is B.
|
|
819
|
-
scheduleCheckpointSave(runState('a'), messages('aborted turn'), chatDirA)
|
|
820
|
-
setChatDirOverrideForTesting(chatDirB)
|
|
821
|
-
|
|
822
|
-
flushLiveChatState()
|
|
823
|
-
|
|
824
|
-
const saved = JSON.parse(
|
|
825
|
-
fs.readFileSync(messagesFileIn(chatDirA), 'utf8'),
|
|
826
|
-
) as ChatMessage[]
|
|
827
|
-
expect(saved[0].content).toBe('aborted turn')
|
|
828
|
-
expect(fs.existsSync(messagesFileIn(chatDirB))).toBe(false)
|
|
829
|
-
})
|
|
830
|
-
|
|
831
|
-
test('flushLiveChatState still writes to the run chat dir when unswitched', () => {
|
|
832
|
-
setLiveChatStateProvider('run-a', () => ({
|
|
833
|
-
runState: runState('a'),
|
|
834
|
-
messages: messages('in-flight prompt'),
|
|
835
|
-
}))
|
|
836
|
-
|
|
837
|
-
flushLiveChatState()
|
|
838
|
-
|
|
839
|
-
const saved = JSON.parse(
|
|
840
|
-
fs.readFileSync(messagesFileIn(chatDirA), 'utf8'),
|
|
841
|
-
) as ChatMessage[]
|
|
842
|
-
expect(saved[0].content).toBe('in-flight prompt')
|
|
843
|
-
})
|
|
844
|
-
})
|
|
845
|
-
|
|
846
|
-
describe('poisoned payload persistence', () => {
|
|
847
|
-
const chatDir = path.join(TEST_ROOT, 'anvil-test-poisoned-chatdir')
|
|
848
|
-
|
|
849
|
-
const messages: ChatMessage[] = [
|
|
850
|
-
{
|
|
851
|
-
id: 'msg-1',
|
|
852
|
-
variant: 'user',
|
|
853
|
-
content: 'the prompt',
|
|
854
|
-
timestamp: new Date().toISOString(),
|
|
855
|
-
},
|
|
856
|
-
]
|
|
857
|
-
|
|
858
|
-
beforeEach(() => {
|
|
859
|
-
fs.rmSync(chatDir, { recursive: true, force: true })
|
|
860
|
-
fs.mkdirSync(chatDir, { recursive: true })
|
|
861
|
-
setChatDirOverrideForTesting(chatDir)
|
|
862
|
-
})
|
|
863
|
-
|
|
864
|
-
afterEach(() => {
|
|
865
|
-
setChatDirOverrideForTesting(undefined)
|
|
866
|
-
fs.rmSync(chatDir, { recursive: true, force: true })
|
|
867
|
-
})
|
|
868
|
-
|
|
869
|
-
test('cyclic run state still persists (cycles broken) alongside messages', () => {
|
|
870
|
-
const cyclicRunState: any = { output: { type: 'error', message: 'x' } }
|
|
871
|
-
cyclicRunState.self = cyclicRunState
|
|
872
|
-
|
|
873
|
-
saveChatState(cyclicRunState as RunState, messages)
|
|
874
|
-
|
|
875
|
-
const savedRunState = JSON.parse(
|
|
876
|
-
fs.readFileSync(path.join(chatDir, 'run-state.json'), 'utf8'),
|
|
877
|
-
)
|
|
878
|
-
expect(savedRunState.self).toBe('[Circular]')
|
|
879
|
-
expect(savedRunState.output.message).toBe('x')
|
|
880
|
-
|
|
881
|
-
const savedMessages = JSON.parse(
|
|
882
|
-
fs.readFileSync(path.join(chatDir, 'chat-messages.json'), 'utf8'),
|
|
883
|
-
) as ChatMessage[]
|
|
884
|
-
expect(savedMessages[0].content).toBe('the prompt')
|
|
885
|
-
})
|
|
886
|
-
|
|
887
|
-
test('cyclic tool output in messages does not block the transcript save', () => {
|
|
888
|
-
const cyclicOutput: any = { status: 'ok' }
|
|
889
|
-
cyclicOutput.self = cyclicOutput
|
|
890
|
-
const poisonedMessages: ChatMessage[] = [
|
|
891
|
-
{
|
|
892
|
-
id: 'msg-1',
|
|
893
|
-
variant: 'agent',
|
|
894
|
-
content: '',
|
|
895
|
-
timestamp: new Date().toISOString(),
|
|
896
|
-
blocks: [
|
|
897
|
-
{
|
|
898
|
-
type: 'tool',
|
|
899
|
-
toolCallId: 'tc-1',
|
|
900
|
-
toolName: 'run_terminal_command' as any,
|
|
901
|
-
input: {},
|
|
902
|
-
outputRaw: cyclicOutput,
|
|
903
|
-
},
|
|
904
|
-
],
|
|
905
|
-
},
|
|
906
|
-
]
|
|
907
|
-
|
|
908
|
-
saveChatState(
|
|
909
|
-
{ output: { type: 'error', message: 'x' } } as RunState,
|
|
910
|
-
poisonedMessages,
|
|
911
|
-
)
|
|
912
|
-
|
|
913
|
-
const savedMessages = JSON.parse(
|
|
914
|
-
fs.readFileSync(path.join(chatDir, 'chat-messages.json'), 'utf8'),
|
|
915
|
-
) as ChatMessage[]
|
|
916
|
-
const block = savedMessages[0].blocks?.[0] as any
|
|
917
|
-
expect(block.outputRaw.self).toBe('[Circular]')
|
|
918
|
-
})
|
|
919
|
-
})
|