@vellumai/assistant 0.6.0 → 0.6.1
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/AGENTS.md +4 -0
- package/ARCHITECTURE.md +68 -15
- package/Dockerfile +2 -2
- package/bun.lock +6 -2
- package/docker-entrypoint.sh +32 -1
- package/docs/architecture/integrations.md +1 -1
- package/docs/architecture/memory.md +21 -24
- package/openapi.yaml +538 -3
- package/package.json +5 -1
- package/src/__tests__/anthropic-provider.test.ts +160 -95
- package/src/__tests__/app-dir-path-guard.test.ts +1 -0
- package/src/__tests__/app-executors.test.ts +47 -1
- package/src/__tests__/app-source-watcher.test.ts +159 -0
- package/src/__tests__/checker.test.ts +38 -6
- package/src/__tests__/config-schema.test.ts +5 -0
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +4 -6
- package/src/__tests__/conversation-agent-loop.test.ts +4 -51
- package/src/__tests__/conversation-history-web-search.test.ts +1 -1
- package/src/__tests__/conversation-runtime-assembly.test.ts +653 -832
- package/src/__tests__/conversation-runtime-workspace.test.ts +1 -93
- package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +17 -4
- package/src/__tests__/conversation-wipe.test.ts +2 -6
- package/src/__tests__/conversation-workspace-cache-state.test.ts +6 -12
- package/src/__tests__/conversation-workspace-injection.test.ts +25 -26
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +1 -1
- package/src/__tests__/copy-composer-tc-templates.test.ts +335 -0
- package/src/__tests__/date-context.test.ts +76 -210
- package/src/__tests__/db-schedule-syntax-migration.test.ts +16 -1
- package/src/__tests__/file-list-tool.test.ts +219 -0
- package/src/__tests__/first-greeting.test.ts +1 -1
- package/src/__tests__/heartbeat-service.test.ts +180 -3
- package/src/__tests__/identity-routes.test.ts +328 -0
- package/src/__tests__/injection-block.test.ts +24 -0
- package/src/__tests__/install-skill-routing.test.ts +7 -6
- package/src/__tests__/jobs-store-qdrant-breaker.test.ts +15 -14
- package/src/__tests__/list-messages-tool-merge.test.ts +300 -0
- package/src/__tests__/llm-context-normalization.test.ts +18 -18
- package/src/__tests__/llm-context-route-provider.test.ts +101 -0
- package/src/__tests__/llm-request-log-turn-query.test.ts +162 -0
- package/src/__tests__/log-export-workspace.test.ts +72 -105
- package/src/__tests__/mcp-abort-signal.test.ts +5 -0
- package/src/__tests__/mcp-client-auth.test.ts +5 -0
- package/src/__tests__/memory-recall-log-store.test.ts +132 -0
- package/src/__tests__/migration-export-streaming.test.ts +304 -0
- package/src/__tests__/migration-import-commit-http.test.ts +11 -10
- package/src/__tests__/mock-fetch.ts +87 -0
- package/src/__tests__/notification-decision-recipient-context.test.ts +282 -0
- package/src/__tests__/onboarding-template-contract.test.ts +62 -14
- package/src/__tests__/parser.test.ts +32 -0
- package/src/__tests__/permission-checker-host-gate.test.ts +452 -0
- package/src/__tests__/permission-controls-v2-flag.test.ts +55 -0
- package/src/__tests__/permission-mode-sse.test.ts +418 -0
- package/src/__tests__/permission-mode-store.test.ts +277 -0
- package/src/__tests__/permission-mode.test.ts +101 -0
- package/src/__tests__/platform-bash-auto-approve.test.ts +359 -0
- package/src/__tests__/profiler-routes.test.ts +502 -0
- package/src/__tests__/profiler-run-store.test.ts +441 -0
- package/src/__tests__/proxy-approval-callback.test.ts +4 -75
- package/src/__tests__/registry.test.ts +1 -1
- package/src/__tests__/sandbox-host-parity.test.ts +5 -4
- package/src/__tests__/scheduler-reuse-conversation.test.ts +368 -0
- package/src/__tests__/scrub-corrupted-image-attachments.test.ts +278 -0
- package/src/__tests__/search-skills-unified.test.ts +4 -3
- package/src/__tests__/send-endpoint-busy.test.ts +42 -3
- package/src/__tests__/set-permission-mode.test.ts +274 -0
- package/src/__tests__/skill-load-feature-flag.test.ts +12 -0
- package/src/__tests__/skill-memory.test.ts +2 -783
- package/src/__tests__/strip-memory-injections.test.ts +187 -0
- package/src/__tests__/subagent-detail.test.ts +84 -0
- package/src/__tests__/subagent-disposal.test.ts +308 -0
- package/src/__tests__/subagent-manager-notify.test.ts +19 -10
- package/src/__tests__/subagent-notify-parent.test.ts +390 -0
- package/src/__tests__/subagent-role-registry.test.ts +108 -0
- package/src/__tests__/subagent-tool-filtering.test.ts +71 -0
- package/src/__tests__/subagent-tools.test.ts +464 -4
- package/src/__tests__/system-prompt-ask-mode.test.ts +139 -0
- package/src/__tests__/task-memory-cleanup.test.ts +12 -12
- package/src/__tests__/terminal-tools.test.ts +17 -27
- package/src/__tests__/test-preload.ts +4 -0
- package/src/__tests__/tool-executor.test.ts +4 -26
- package/src/__tests__/tool-side-effects-slack-dm.test.ts +1 -0
- package/src/__tests__/top-level-renderer.test.ts +10 -13
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +116 -2
- package/src/__tests__/workspace-migration-028-recover-conversations-from-disk-view.test.ts +387 -0
- package/src/agent/loop.ts +6 -0
- package/src/approvals/guardian-request-resolvers.ts +24 -0
- package/src/avatar/traits-png-sync.ts +3 -3
- package/src/cli/__tests__/run-assistant-command.ts +29 -0
- package/src/cli/commands/__tests__/email-download.test.ts +245 -0
- package/src/cli/commands/__tests__/email-list.test.ts +192 -0
- package/src/cli/commands/__tests__/email-register.test.ts +186 -0
- package/src/cli/commands/__tests__/email-send.test.ts +291 -0
- package/src/cli/commands/__tests__/email-status.test.ts +181 -0
- package/src/cli/commands/__tests__/email-unregister.test.ts +139 -0
- package/src/cli/commands/__tests__/routes.test.ts +562 -0
- package/src/cli/commands/conversations.ts +1 -8
- package/src/cli/commands/email.ts +584 -835
- package/src/cli/commands/memory.ts +1 -34
- package/src/cli/commands/notifications.ts +7 -2
- package/src/cli/commands/oauth/connect.ts +14 -5
- package/src/cli/commands/routes.ts +396 -0
- package/src/cli/commands/skills.ts +130 -20
- package/src/cli/program.ts +2 -0
- package/src/cli.ts +1 -120
- package/src/config/bundled-skills/app-builder/SKILL.md +4 -1
- package/src/config/bundled-skills/gmail/SKILL.md +2 -2
- package/src/config/bundled-skills/messaging/SKILL.md +7 -0
- package/src/config/bundled-skills/schedule/SKILL.md +22 -2
- package/src/config/bundled-skills/schedule/TOOLS.json +8 -0
- package/src/config/bundled-skills/settings/tools/avatar-get.ts +3 -13
- package/src/config/bundled-skills/settings/tools/avatar-remove.ts +2 -4
- package/src/config/bundled-skills/settings/tools/avatar-update.ts +5 -2
- package/src/config/bundled-skills/slack/SKILL.md +2 -0
- package/src/config/bundled-skills/subagent/SKILL.md +43 -3
- package/src/config/bundled-skills/subagent/TOOLS.json +29 -4
- package/src/config/env-registry.ts +63 -0
- package/src/config/feature-flag-registry.json +17 -1
- package/src/config/schema.ts +8 -0
- package/src/config/schemas/filing.ts +51 -0
- package/src/config/schemas/heartbeat.ts +15 -12
- package/src/config/schemas/memory-lifecycle.ts +12 -0
- package/src/config/schemas/security.ts +14 -0
- package/src/daemon/app-source-watcher.ts +93 -0
- package/src/daemon/config-watcher.ts +79 -1
- package/src/daemon/conversation-agent-loop-handlers.ts +20 -0
- package/src/daemon/conversation-agent-loop.ts +158 -65
- package/src/daemon/conversation-history.ts +4 -19
- package/src/daemon/conversation-lifecycle.ts +8 -14
- package/src/daemon/conversation-process.ts +13 -7
- package/src/daemon/conversation-runtime-assembly.ts +300 -306
- package/src/daemon/conversation-tool-setup.ts +44 -14
- package/src/daemon/conversation-workspace.ts +1 -2
- package/src/daemon/conversation.ts +18 -0
- package/src/daemon/date-context.ts +26 -53
- package/src/daemon/first-greeting.ts +1 -1
- package/src/daemon/handlers/conversations.ts +4 -7
- package/src/daemon/handlers/shared.test.ts +143 -0
- package/src/daemon/handlers/shared.ts +63 -5
- package/src/daemon/handlers/skills.ts +11 -18
- package/src/daemon/lifecycle.ts +199 -157
- package/src/daemon/message-types/conversations.ts +25 -6
- package/src/daemon/message-types/messages.ts +9 -1
- package/src/daemon/message-types/schedules.ts +1 -0
- package/src/daemon/message-types/settings.ts +6 -0
- package/src/daemon/profiler-run-store.ts +557 -0
- package/src/daemon/server.ts +89 -9
- package/src/daemon/shutdown-handlers.ts +5 -0
- package/src/daemon/tool-side-effects.ts +23 -3
- package/src/export/transcript-formatter.ts +148 -0
- package/src/filing/filing-service.ts +228 -0
- package/src/heartbeat/heartbeat-service.ts +96 -7
- package/src/mcp/client.ts +6 -0
- package/src/mcp/mcp-oauth-provider.ts +149 -27
- package/src/memory/admin.ts +33 -32
- package/src/memory/app-store.ts +69 -0
- package/src/memory/conversation-bootstrap.ts +1 -1
- package/src/memory/conversation-crud.ts +136 -107
- package/src/memory/conversation-group-migration.ts +1 -1
- package/src/memory/conversation-queries.ts +58 -12
- package/src/memory/conversation-title-service.ts +1 -0
- package/src/memory/db-init.ts +182 -376
- package/src/memory/graph/bootstrap.ts +75 -66
- package/src/memory/graph/capability-seed.ts +167 -15
- package/src/memory/graph/consolidation.ts +38 -4
- package/src/memory/graph/conversation-graph-memory.ts +133 -104
- package/src/memory/graph/extraction-job.ts +9 -4
- package/src/memory/graph/extraction.ts +66 -23
- package/src/memory/graph/graph-memory-state-store.ts +37 -0
- package/src/memory/graph/graph-search.ts +29 -15
- package/src/memory/graph/injection.ts +38 -8
- package/src/memory/graph/inspect.ts +12 -3
- package/src/memory/graph/retriever.ts +365 -262
- package/src/memory/graph/store.test.ts +48 -0
- package/src/memory/graph/store.ts +150 -11
- package/src/memory/graph/tool-handlers.ts +84 -209
- package/src/memory/graph/tools.ts +8 -52
- package/src/memory/graph/types.ts +24 -0
- package/src/memory/job-handlers/cleanup.ts +44 -1
- package/src/memory/jobs-store.ts +70 -60
- package/src/memory/jobs-worker.ts +44 -28
- package/src/memory/llm-request-log-store.ts +96 -12
- package/src/memory/memory-recall-log-store.ts +49 -5
- package/src/memory/migrations/203-drop-memory-items-tables.ts +33 -1
- package/src/memory/migrations/206-memory-graph-node-edits.ts +19 -0
- package/src/memory/migrations/206-scrub-corrupted-image-attachments.ts +131 -0
- package/src/memory/migrations/207-conversation-graph-memory-state.ts +20 -0
- package/src/memory/migrations/208-conversations-last-message-at.ts +35 -0
- package/src/memory/migrations/209-strip-thinking-from-consolidated.ts +85 -0
- package/src/memory/migrations/210-schedule-reuse-conversation.ts +13 -0
- package/src/memory/migrations/211-memory-recall-logs-query-context.ts +21 -0
- package/src/memory/migrations/212-llm-request-logs-created-at-index.ts +19 -0
- package/src/memory/migrations/index.ts +8 -0
- package/src/memory/migrations/registry.ts +8 -0
- package/src/memory/schema/conversations.ts +14 -0
- package/src/memory/schema/infrastructure.ts +8 -1
- package/src/memory/schema/memory-core.ts +0 -51
- package/src/memory/schema/memory-graph.ts +15 -0
- package/src/memory/task-memory-cleanup.ts +30 -11
- package/src/notifications/copy-composer.ts +86 -0
- package/src/notifications/decision-engine.ts +35 -0
- package/src/permissions/checker.ts +12 -1
- package/src/permissions/permission-mode-store.ts +180 -0
- package/src/permissions/permission-mode.ts +31 -0
- package/src/permissions/workspace-policy.ts +9 -0
- package/src/prompts/system-prompt.ts +59 -7
- package/src/prompts/templates/BOOTSTRAP-REFERENCE.md +100 -0
- package/src/prompts/templates/BOOTSTRAP.md +70 -165
- package/src/prompts/templates/HEARTBEAT.md +3 -1
- package/src/prompts/templates/SOUL.md +25 -4
- package/src/prompts/templates/UPDATES.md +8 -0
- package/src/providers/anthropic/client.ts +107 -219
- package/src/runtime/auth/route-policy.ts +23 -0
- package/src/runtime/http-server.ts +32 -2
- package/src/runtime/http-types.ts +12 -1
- package/src/runtime/migrations/vbundle-builder.ts +389 -3
- package/src/runtime/migrations/vbundle-importer.ts +8 -6
- package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +378 -0
- package/src/runtime/routes/app-management-routes.ts +1 -11
- package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +26 -0
- package/src/runtime/routes/archive-utils.ts +29 -0
- package/src/runtime/routes/avatar-routes.ts +2 -9
- package/src/runtime/routes/btw-routes.ts +14 -1
- package/src/runtime/routes/conversation-analysis-routes.ts +173 -0
- package/src/runtime/routes/conversation-management-routes.ts +1 -14
- package/src/runtime/routes/conversation-query-routes.ts +49 -3
- package/src/runtime/routes/conversation-routes.ts +264 -44
- package/src/runtime/routes/heartbeat-routes.ts +4 -10
- package/src/runtime/routes/identity-routes.ts +53 -18
- package/src/runtime/routes/llm-context-normalization.ts +14 -10
- package/src/runtime/routes/log-export-routes.ts +23 -275
- package/src/runtime/routes/memory-item-routes.test.ts +168 -233
- package/src/runtime/routes/migration-routes.ts +18 -7
- package/src/runtime/routes/profiler-routes.ts +350 -0
- package/src/runtime/routes/schedule-routes.ts +27 -12
- package/src/runtime/routes/settings-routes.ts +95 -8
- package/src/runtime/routes/subagents-routes.ts +28 -7
- package/src/runtime/routes/user-route-dispatcher.ts +223 -0
- package/src/runtime/routes/user-routes.ts +41 -0
- package/src/runtime/routes/workspace-routes.ts +0 -1
- package/src/schedule/schedule-store.ts +30 -0
- package/src/schedule/scheduler.ts +45 -18
- package/src/skills/catalog-install.ts +10 -2
- package/src/skills/managed-store.ts +2 -2
- package/src/skills/skill-memory.ts +1 -293
- package/src/subagent/index.ts +13 -3
- package/src/subagent/manager.ts +308 -29
- package/src/subagent/types.ts +68 -0
- package/src/tasks/task-runner.ts +4 -4
- package/src/tools/apps/executors.ts +29 -4
- package/src/tools/filesystem/list.ts +93 -0
- package/src/tools/permission-checker.ts +78 -0
- package/src/tools/registry.ts +4 -0
- package/src/tools/schedule/create.ts +3 -0
- package/src/tools/schedule/list.ts +1 -0
- package/src/tools/schedule/update.ts +6 -0
- package/src/tools/shared/filesystem/errors.ts +5 -0
- package/src/tools/shared/filesystem/file-ops-service.ts +90 -2
- package/src/tools/shared/filesystem/types.ts +17 -0
- package/src/tools/shared/shell-output.ts +31 -2
- package/src/tools/subagent/abort.ts +12 -2
- package/src/tools/subagent/message.ts +9 -2
- package/src/tools/subagent/notify-parent.ts +79 -0
- package/src/tools/subagent/read.ts +29 -8
- package/src/tools/subagent/resolve.ts +21 -0
- package/src/tools/subagent/spawn.ts +2 -0
- package/src/tools/subagent/status.ts +11 -1
- package/src/tools/system/avatar-generator.ts +3 -3
- package/src/tools/system/register.ts +23 -0
- package/src/tools/system/set-permission-mode.ts +103 -0
- package/src/tools/terminal/parser.ts +30 -5
- package/src/tools/terminal/safe-env.ts +16 -1
- package/src/tools/tool-manifest.ts +6 -0
- package/src/tools/types.ts +2 -0
- package/src/util/logger.ts +1 -1
- package/src/util/platform.ts +50 -17
- package/src/workspace/migrations/023-move-config-files-to-workspace.ts +2 -2
- package/src/workspace/migrations/024-move-runtime-files-to-workspace.ts +2 -2
- package/src/workspace/migrations/028-recover-conversations-from-disk-view.ts +270 -0
- package/src/workspace/migrations/029-seed-pkb.ts +84 -0
- package/src/workspace/migrations/registry.ts +4 -0
- package/src/workspace/top-level-renderer.ts +5 -9
- package/src/__tests__/cli-memory.test.ts +0 -377
- package/src/__tests__/clipboard.test.ts +0 -88
- package/src/cli/cli-memory.ts +0 -179
- package/src/util/clipboard.ts +0 -34
|
@@ -187,6 +187,58 @@ describe("AnthropicProvider — Cache-Control Characterization", () => {
|
|
|
187
187
|
expect(system[1].cache_control).toEqual({ type: "ephemeral", ttl: "1h" });
|
|
188
188
|
});
|
|
189
189
|
|
|
190
|
+
test("drops static system block cache_control when total would exceed 4", async () => {
|
|
191
|
+
const staticBlock = "You are a helpful assistant.";
|
|
192
|
+
const dynamicBlock = "User workspace files here.";
|
|
193
|
+
const prompt = staticBlock + SYSTEM_PROMPT_CACHE_BOUNDARY + dynamicBlock;
|
|
194
|
+
|
|
195
|
+
// Boundary (2 system) + tools (1) + turn-start (1) + tail (1) = 5 → must cap at 4
|
|
196
|
+
const messages: Message[] = [
|
|
197
|
+
userMsg("Do something"),
|
|
198
|
+
toolUseMsg("tu_1", "bash"),
|
|
199
|
+
toolResultMsg("tu_1", "output"),
|
|
200
|
+
];
|
|
201
|
+
await provider.sendMessage(messages, sampleTools, prompt);
|
|
202
|
+
|
|
203
|
+
const system = lastStreamParams!.system as Array<{
|
|
204
|
+
type: string;
|
|
205
|
+
text: string;
|
|
206
|
+
cache_control?: { type: string; ttl?: string };
|
|
207
|
+
}>;
|
|
208
|
+
expect(system).toHaveLength(2);
|
|
209
|
+
// Static block's cache_control dropped (small, cheap to re-read)
|
|
210
|
+
expect(system[0].cache_control).toBeUndefined();
|
|
211
|
+
// Dynamic block keeps its cache_control
|
|
212
|
+
expect(system[1].cache_control).toEqual({ type: "ephemeral", ttl: "1h" });
|
|
213
|
+
|
|
214
|
+
// Tools breakpoint still present
|
|
215
|
+
const tools = lastStreamParams!.tools as Array<{
|
|
216
|
+
cache_control?: { type: string; ttl?: string };
|
|
217
|
+
}>;
|
|
218
|
+
expect(tools[tools.length - 1].cache_control).toEqual({
|
|
219
|
+
type: "ephemeral",
|
|
220
|
+
ttl: "1h",
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
// Turn-start + tail breakpoints still present
|
|
224
|
+
const sent = lastStreamParams!.messages as Array<{
|
|
225
|
+
role: string;
|
|
226
|
+
content: Array<{
|
|
227
|
+
type: string;
|
|
228
|
+
cache_control?: { type: string; ttl?: string };
|
|
229
|
+
}>;
|
|
230
|
+
}>;
|
|
231
|
+
const turnStart = sent[0];
|
|
232
|
+
expect(
|
|
233
|
+
turnStart.content[turnStart.content.length - 1].cache_control,
|
|
234
|
+
).toEqual({ type: "ephemeral", ttl: "1h" });
|
|
235
|
+
const lastMsg = sent[sent.length - 1];
|
|
236
|
+
expect(lastMsg.content[lastMsg.content.length - 1].cache_control).toEqual({
|
|
237
|
+
type: "ephemeral",
|
|
238
|
+
ttl: "5m",
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
|
|
190
242
|
// -----------------------------------------------------------------------
|
|
191
243
|
// Tool cache control
|
|
192
244
|
// -----------------------------------------------------------------------
|
|
@@ -225,33 +277,22 @@ describe("AnthropicProvider — Cache-Control Characterization", () => {
|
|
|
225
277
|
});
|
|
226
278
|
|
|
227
279
|
// -----------------------------------------------------------------------
|
|
228
|
-
//
|
|
280
|
+
// Advancing tail — 5m cache on last block after turn-starting message
|
|
229
281
|
// -----------------------------------------------------------------------
|
|
230
|
-
test("
|
|
282
|
+
test("no advancing tail cache when turn-starting user message is last", async () => {
|
|
231
283
|
await provider.sendMessage([userMsg("Hello")]);
|
|
232
284
|
|
|
233
|
-
|
|
234
|
-
role: string;
|
|
235
|
-
content: Array<{
|
|
236
|
-
type: string;
|
|
237
|
-
text: string;
|
|
238
|
-
cache_control?: { type: string; ttl?: string };
|
|
239
|
-
}>;
|
|
240
|
-
}>;
|
|
241
|
-
const lastUser = messages[messages.length - 1];
|
|
242
|
-
expect(lastUser.role).toBe("user");
|
|
285
|
+
// No top-level cache_control — would conflict with the 1h block breakpoint
|
|
243
286
|
expect(
|
|
244
|
-
|
|
287
|
+
(lastStreamParams as Record<string, unknown>).cache_control,
|
|
245
288
|
).toBeUndefined();
|
|
246
289
|
});
|
|
247
290
|
|
|
248
|
-
test("
|
|
291
|
+
test("advancing tail: 5m cache on last block when tool results follow turn-starting message", async () => {
|
|
249
292
|
const messages: Message[] = [
|
|
250
|
-
userMsg("
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
assistantMsg("Response 2"),
|
|
254
|
-
userMsg("Turn 3"), // user turn 2 — no cache (last)
|
|
293
|
+
userMsg("Do something"),
|
|
294
|
+
toolUseMsg("tu_1", "bash"),
|
|
295
|
+
toolResultMsg("tu_1", "output"),
|
|
255
296
|
];
|
|
256
297
|
await provider.sendMessage(messages);
|
|
257
298
|
|
|
@@ -259,33 +300,30 @@ describe("AnthropicProvider — Cache-Control Characterization", () => {
|
|
|
259
300
|
role: string;
|
|
260
301
|
content: Array<{
|
|
261
302
|
type: string;
|
|
262
|
-
text: string;
|
|
263
303
|
cache_control?: { type: string; ttl?: string };
|
|
264
304
|
}>;
|
|
265
305
|
}>;
|
|
266
306
|
|
|
267
|
-
//
|
|
268
|
-
const
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
// Second user turn (second-to-last): cache_control ephemeral
|
|
277
|
-
const secondUserLastBlock =
|
|
278
|
-
userMessages[1].content[userMessages[1].content.length - 1];
|
|
279
|
-
expect(secondUserLastBlock.cache_control).toEqual({ type: "ephemeral", ttl: "1h" });
|
|
280
|
-
|
|
281
|
-
// Third user turn (last): no cache_control
|
|
282
|
-
const thirdUserLastBlock =
|
|
283
|
-
userMessages[2].content[userMessages[2].content.length - 1];
|
|
284
|
-
expect(thirdUserLastBlock.cache_control).toBeUndefined();
|
|
307
|
+
// Turn-starting user message (first) keeps 1h
|
|
308
|
+
const turnStart = sent[0];
|
|
309
|
+
const turnStartLast = turnStart.content[turnStart.content.length - 1];
|
|
310
|
+
expect(turnStartLast.cache_control).toEqual({ type: "ephemeral", ttl: "1h" });
|
|
311
|
+
|
|
312
|
+
// Last message (tool_result) gets 5m advancing tail
|
|
313
|
+
const lastMessage = sent[sent.length - 1];
|
|
314
|
+
const lastBlock = lastMessage.content[lastMessage.content.length - 1];
|
|
315
|
+
expect(lastBlock.cache_control).toEqual({ type: "ephemeral", ttl: "5m" });
|
|
285
316
|
});
|
|
286
317
|
|
|
287
|
-
test("
|
|
288
|
-
|
|
318
|
+
test("turn-starting user message gets 1h cache on last block", async () => {
|
|
319
|
+
const messages: Message[] = [
|
|
320
|
+
userMsg("Turn 1"),
|
|
321
|
+
assistantMsg("Response 1"),
|
|
322
|
+
userMsg("Turn 2"),
|
|
323
|
+
assistantMsg("Response 2"),
|
|
324
|
+
userMsg("Turn 3"),
|
|
325
|
+
];
|
|
326
|
+
await provider.sendMessage(messages);
|
|
289
327
|
|
|
290
328
|
const sent = lastStreamParams!.messages as Array<{
|
|
291
329
|
role: string;
|
|
@@ -295,35 +333,17 @@ describe("AnthropicProvider — Cache-Control Characterization", () => {
|
|
|
295
333
|
cache_control?: { type: string; ttl?: string };
|
|
296
334
|
}>;
|
|
297
335
|
}>;
|
|
298
|
-
const userMessages = sent.filter((m) => m.role === "user");
|
|
299
|
-
expect(userMessages).toHaveLength(1);
|
|
300
|
-
expect(
|
|
301
|
-
userMessages[0].content[userMessages[0].content.length - 1].cache_control,
|
|
302
|
-
).toBeUndefined();
|
|
303
|
-
});
|
|
304
336
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
];
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
const sent = lastStreamParams!.messages as Array<{
|
|
317
|
-
role: string;
|
|
318
|
-
content: Array<{ type: string; cache_control?: { type: string; ttl?: string } }>;
|
|
319
|
-
}>;
|
|
320
|
-
const userMsgs = sent.filter((m) => m.role === "user");
|
|
321
|
-
// First user msg (second-to-last) should get cache
|
|
322
|
-
const firstLast = userMsgs[0].content[userMsgs[0].content.length - 1];
|
|
323
|
-
expect(firstLast.cache_control).toEqual({ type: "ephemeral", ttl: "1h" });
|
|
324
|
-
// tool_result msg (last) should NOT get cache
|
|
325
|
-
const secondLast = userMsgs[1].content[userMsgs[1].content.length - 1];
|
|
326
|
-
expect(secondLast.cache_control).toBeUndefined();
|
|
337
|
+
const userMessages = sent.filter((m) => m.role === "user");
|
|
338
|
+
// Only the last user message (turn-starting) gets cache_control
|
|
339
|
+
for (const user of userMessages.slice(0, -1)) {
|
|
340
|
+
for (const block of user.content) {
|
|
341
|
+
expect(block.cache_control).toBeUndefined();
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
const lastUser = userMessages[userMessages.length - 1];
|
|
345
|
+
const lastBlock = lastUser.content[lastUser.content.length - 1];
|
|
346
|
+
expect(lastBlock.cache_control).toEqual({ type: "ephemeral", ttl: "1h" });
|
|
327
347
|
});
|
|
328
348
|
|
|
329
349
|
// -----------------------------------------------------------------------
|
|
@@ -358,7 +378,7 @@ describe("AnthropicProvider — Cache-Control Characterization", () => {
|
|
|
358
378
|
// -----------------------------------------------------------------------
|
|
359
379
|
// Multi-block user message: cache lands on LAST block
|
|
360
380
|
// -----------------------------------------------------------------------
|
|
361
|
-
test("multi-block single user message
|
|
381
|
+
test("multi-block single user message gets cache on last block", async () => {
|
|
362
382
|
const multiBlockUser: Message = {
|
|
363
383
|
role: "user",
|
|
364
384
|
content: [
|
|
@@ -378,7 +398,7 @@ describe("AnthropicProvider — Cache-Control Characterization", () => {
|
|
|
378
398
|
}>;
|
|
379
399
|
const user = sent[0];
|
|
380
400
|
expect(user.content[0].cache_control).toBeUndefined();
|
|
381
|
-
expect(user.content[1].cache_control).
|
|
401
|
+
expect(user.content[1].cache_control).toEqual({ type: "ephemeral", ttl: "1h" });
|
|
382
402
|
});
|
|
383
403
|
|
|
384
404
|
// -----------------------------------------------------------------------
|
|
@@ -395,14 +415,14 @@ describe("AnthropicProvider — Cache-Control Characterization", () => {
|
|
|
395
415
|
// -----------------------------------------------------------------------
|
|
396
416
|
// Cache compatibility with workspace context injection
|
|
397
417
|
// -----------------------------------------------------------------------
|
|
398
|
-
test("workspace-prepended single user message
|
|
418
|
+
test("workspace-prepended single user message gets cache on last block", async () => {
|
|
399
419
|
// Simulates what applyRuntimeInjections does: prepend workspace block, keep user text as trailing
|
|
400
420
|
const workspaceInjectedUser: Message = {
|
|
401
421
|
role: "user",
|
|
402
422
|
content: [
|
|
403
423
|
{
|
|
404
424
|
type: "text",
|
|
405
|
-
text: "<
|
|
425
|
+
text: "<workspace>\nRoot: /sandbox\nDirectories: src, tests\n</workspace>",
|
|
406
426
|
},
|
|
407
427
|
{ type: "text", text: "What files are in src?" },
|
|
408
428
|
],
|
|
@@ -421,18 +441,18 @@ describe("AnthropicProvider — Cache-Control Characterization", () => {
|
|
|
421
441
|
expect(user.content).toHaveLength(2);
|
|
422
442
|
// Workspace block (first): no cache_control
|
|
423
443
|
expect(user.content[0].cache_control).toBeUndefined();
|
|
424
|
-
// User text (last):
|
|
425
|
-
expect(user.content[1].cache_control).
|
|
444
|
+
// User text (last): cache_control with 1h TTL
|
|
445
|
+
expect(user.content[1].cache_control).toEqual({ type: "ephemeral", ttl: "1h" });
|
|
426
446
|
});
|
|
427
447
|
|
|
428
|
-
test("workspace + multi-block single user message:
|
|
448
|
+
test("workspace + multi-block single user message: cache on last block only", async () => {
|
|
429
449
|
// Simulates workspace prepended + extra context block appended
|
|
430
450
|
const injectedUser: Message = {
|
|
431
451
|
role: "user",
|
|
432
452
|
content: [
|
|
433
453
|
{
|
|
434
454
|
type: "text",
|
|
435
|
-
text: "<
|
|
455
|
+
text: "<workspace>\nRoot: /sandbox\nDirectories: src, tests\n</workspace>",
|
|
436
456
|
},
|
|
437
457
|
{ type: "text", text: "Help me debug this" },
|
|
438
458
|
{
|
|
@@ -453,10 +473,10 @@ describe("AnthropicProvider — Cache-Control Characterization", () => {
|
|
|
453
473
|
}>;
|
|
454
474
|
const user = sent[0];
|
|
455
475
|
expect(user.content).toHaveLength(3);
|
|
456
|
-
//
|
|
476
|
+
// Only last block gets cache_control
|
|
457
477
|
expect(user.content[0].cache_control).toBeUndefined();
|
|
458
478
|
expect(user.content[1].cache_control).toBeUndefined();
|
|
459
|
-
expect(user.content[2].cache_control).
|
|
479
|
+
expect(user.content[2].cache_control).toEqual({ type: "ephemeral", ttl: "1h" });
|
|
460
480
|
});
|
|
461
481
|
|
|
462
482
|
// -----------------------------------------------------------------------
|
|
@@ -550,7 +570,7 @@ describe("AnthropicProvider — Cache-Control Characterization", () => {
|
|
|
550
570
|
content: [
|
|
551
571
|
{
|
|
552
572
|
type: "text",
|
|
553
|
-
text: "<
|
|
573
|
+
text: "<workspace>\nRoot: /sandbox\n</workspace>",
|
|
554
574
|
},
|
|
555
575
|
{
|
|
556
576
|
type: "tool_result",
|
|
@@ -1330,39 +1350,36 @@ describe("AnthropicProvider — Cache-Control Characterization", () => {
|
|
|
1330
1350
|
expect(sent[4].content[0].text).toBe("Follow-up question");
|
|
1331
1351
|
});
|
|
1332
1352
|
|
|
1333
|
-
test("multi-turn with workspace injection:
|
|
1353
|
+
test("multi-turn with workspace injection: only last user message gets 1h cache", async () => {
|
|
1334
1354
|
const messages: Message[] = [
|
|
1335
|
-
// Turn 1: workspace + user text (no cache - 3rd-to-last)
|
|
1336
1355
|
{
|
|
1337
1356
|
role: "user",
|
|
1338
1357
|
content: [
|
|
1339
1358
|
{
|
|
1340
1359
|
type: "text",
|
|
1341
|
-
text: "<
|
|
1360
|
+
text: "<workspace>\nRoot: /sandbox\nDirectories: src\n</workspace>",
|
|
1342
1361
|
},
|
|
1343
1362
|
{ type: "text", text: "Turn 1" },
|
|
1344
1363
|
],
|
|
1345
1364
|
},
|
|
1346
1365
|
assistantMsg("Response 1"),
|
|
1347
|
-
// Turn 2: workspace + user text (cache - second-to-last)
|
|
1348
1366
|
{
|
|
1349
1367
|
role: "user",
|
|
1350
1368
|
content: [
|
|
1351
1369
|
{
|
|
1352
1370
|
type: "text",
|
|
1353
|
-
text: "<
|
|
1371
|
+
text: "<workspace>\nRoot: /sandbox\nDirectories: src, lib\n</workspace>",
|
|
1354
1372
|
},
|
|
1355
1373
|
{ type: "text", text: "Turn 2" },
|
|
1356
1374
|
],
|
|
1357
1375
|
},
|
|
1358
1376
|
assistantMsg("Response 2"),
|
|
1359
|
-
// Turn 3: workspace + user text (no cache - last)
|
|
1360
1377
|
{
|
|
1361
1378
|
role: "user",
|
|
1362
1379
|
content: [
|
|
1363
1380
|
{
|
|
1364
1381
|
type: "text",
|
|
1365
|
-
text: "<
|
|
1382
|
+
text: "<workspace>\nRoot: /sandbox\nDirectories: src, lib, docs\n</workspace>",
|
|
1366
1383
|
},
|
|
1367
1384
|
{ type: "text", text: "Turn 3" },
|
|
1368
1385
|
],
|
|
@@ -1381,17 +1398,65 @@ describe("AnthropicProvider — Cache-Control Characterization", () => {
|
|
|
1381
1398
|
const userMsgs = sent.filter((m) => m.role === "user");
|
|
1382
1399
|
expect(userMsgs).toHaveLength(3);
|
|
1383
1400
|
|
|
1384
|
-
//
|
|
1385
|
-
|
|
1386
|
-
|
|
1401
|
+
// Earlier user messages: no cache_control
|
|
1402
|
+
for (const user of userMsgs.slice(0, -1)) {
|
|
1403
|
+
for (const block of user.content) {
|
|
1404
|
+
expect(block.cache_control).toBeUndefined();
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
// Last user message (turn 3): 1h cache on last block only
|
|
1409
|
+
const lastUser = userMsgs[userMsgs.length - 1];
|
|
1410
|
+
expect(lastUser.content[0].cache_control).toBeUndefined();
|
|
1411
|
+
expect(lastUser.content[1].cache_control).toEqual({ type: "ephemeral", ttl: "1h" });
|
|
1412
|
+
|
|
1413
|
+
// No top-level cache_control — breakpoints are set directly on blocks
|
|
1414
|
+
expect(
|
|
1415
|
+
(lastStreamParams as Record<string, unknown>).cache_control,
|
|
1416
|
+
).toBeUndefined();
|
|
1417
|
+
});
|
|
1418
|
+
|
|
1419
|
+
test("tool loop: turn-starting user message gets 1h cache, last tool_result gets 5m advancing tail", async () => {
|
|
1420
|
+
const messages: Message[] = [
|
|
1421
|
+
userMsg("Read the config file"),
|
|
1422
|
+
toolUseMsg("tu_1", "file_read"),
|
|
1423
|
+
toolResultMsg("tu_1", "config contents here"),
|
|
1424
|
+
toolUseMsg("tu_2", "file_read"),
|
|
1425
|
+
toolResultMsg("tu_2", "more contents"),
|
|
1426
|
+
];
|
|
1427
|
+
await provider.sendMessage(messages);
|
|
1387
1428
|
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1429
|
+
const sent = lastStreamParams!.messages as Array<{
|
|
1430
|
+
role: string;
|
|
1431
|
+
content: Array<{
|
|
1432
|
+
type: string;
|
|
1433
|
+
text?: string;
|
|
1434
|
+
cache_control?: { type: string; ttl?: string };
|
|
1435
|
+
}>;
|
|
1436
|
+
}>;
|
|
1437
|
+
|
|
1438
|
+
// First message is the turn-starting user text — gets 1h cache
|
|
1439
|
+
expect(sent[0].role).toBe("user");
|
|
1440
|
+
expect(sent[0].content[0].cache_control).toEqual({ type: "ephemeral", ttl: "1h" });
|
|
1441
|
+
|
|
1442
|
+
// Non-last tool result messages do NOT get cache_control
|
|
1443
|
+
const toolResultMsgs = sent.filter(
|
|
1444
|
+
(m) =>
|
|
1445
|
+
m.role === "user" &&
|
|
1446
|
+
Array.isArray(m.content) &&
|
|
1447
|
+
m.content.every((b) => typeof b !== "string" && b.type === "tool_result"),
|
|
1448
|
+
);
|
|
1449
|
+
expect(toolResultMsgs.length).toBeGreaterThan(0);
|
|
1450
|
+
for (const tr of toolResultMsgs.slice(0, -1)) {
|
|
1451
|
+
for (const block of tr.content) {
|
|
1452
|
+
expect(block.cache_control).toBeUndefined();
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1391
1455
|
|
|
1392
|
-
//
|
|
1393
|
-
|
|
1394
|
-
|
|
1456
|
+
// Last message gets 5m advancing tail cache on its last block
|
|
1457
|
+
const lastMsg = sent[sent.length - 1];
|
|
1458
|
+
const lastBlock = lastMsg.content[lastMsg.content.length - 1];
|
|
1459
|
+
expect(lastBlock.cache_control).toEqual({ type: "ephemeral", ttl: "5m" });
|
|
1395
1460
|
});
|
|
1396
1461
|
|
|
1397
1462
|
// -----------------------------------------------------------------------
|
|
@@ -18,6 +18,7 @@ import { describe, expect, test } from "bun:test";
|
|
|
18
18
|
const ALLOWLIST = new Set([
|
|
19
19
|
"assistant/src/memory/app-store.ts", // defines getAppsDir
|
|
20
20
|
"assistant/src/memory/app-git-service.ts", // uses getAppsDir for git repo root, not per-app paths
|
|
21
|
+
"assistant/src/daemon/app-source-watcher.ts", // uses getAppsDir for recursive fs.watch root, not per-app paths
|
|
21
22
|
]);
|
|
22
23
|
|
|
23
24
|
function isTestFile(filePath: string): boolean {
|
|
@@ -11,7 +11,10 @@ mock.module("../bundler/app-compiler.js", () => ({
|
|
|
11
11
|
|
|
12
12
|
import type { AppDefinition } from "../memory/app-store.js";
|
|
13
13
|
import type { AppStore } from "../tools/apps/executors.js";
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
executeAppCreate,
|
|
16
|
+
executeAppRefresh,
|
|
17
|
+
} from "../tools/apps/executors.js";
|
|
15
18
|
|
|
16
19
|
// ---------------------------------------------------------------------------
|
|
17
20
|
// Helpers
|
|
@@ -158,3 +161,46 @@ describe("executeAppCreate", () => {
|
|
|
158
161
|
expect(files["src/main.tsx"]).toBeDefined();
|
|
159
162
|
});
|
|
160
163
|
});
|
|
164
|
+
|
|
165
|
+
// ---------------------------------------------------------------------------
|
|
166
|
+
// executeAppRefresh
|
|
167
|
+
// ---------------------------------------------------------------------------
|
|
168
|
+
|
|
169
|
+
describe("executeAppRefresh", () => {
|
|
170
|
+
test("legacy app: bumps updatedAt without compiling", async () => {
|
|
171
|
+
const app = makeLegacyApp();
|
|
172
|
+
const store = mockStore(app);
|
|
173
|
+
const result = await executeAppRefresh({ app_id: app.id }, store);
|
|
174
|
+
|
|
175
|
+
expect(result.isError).toBe(false);
|
|
176
|
+
const parsed = JSON.parse(result.content);
|
|
177
|
+
expect(parsed.refreshed).toBe(true);
|
|
178
|
+
expect(parsed.appId).toBe(app.id);
|
|
179
|
+
// Legacy apps should not have compile-related fields
|
|
180
|
+
expect(parsed.compiled).toBeUndefined();
|
|
181
|
+
expect(parsed.compile_errors).toBeUndefined();
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test("multifile app: compiles src/ and returns result", async () => {
|
|
185
|
+
const app = makeMultifileApp();
|
|
186
|
+
const store = mockStore(app);
|
|
187
|
+
const result = await executeAppRefresh({ app_id: app.id }, store);
|
|
188
|
+
|
|
189
|
+
expect(result.isError).toBe(false);
|
|
190
|
+
const parsed = JSON.parse(result.content);
|
|
191
|
+
expect(parsed.refreshed).toBe(true);
|
|
192
|
+
expect(parsed.appId).toBe(app.id);
|
|
193
|
+
expect(parsed.compiled).toBe(true);
|
|
194
|
+
expect(parsed.compile_duration_ms).toBeDefined();
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test("returns error for unknown app", async () => {
|
|
198
|
+
const app = makeLegacyApp();
|
|
199
|
+
const store = mockStore(app);
|
|
200
|
+
const result = await executeAppRefresh({ app_id: "nonexistent" }, store);
|
|
201
|
+
|
|
202
|
+
expect(result.isError).toBe(true);
|
|
203
|
+
const parsed = JSON.parse(result.content);
|
|
204
|
+
expect(parsed.error).toContain("not found");
|
|
205
|
+
});
|
|
206
|
+
});
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for AppSourceWatcher — filesystem watcher that detects app source
|
|
3
|
+
* file changes and triggers debounced recompile + surface refresh.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
afterEach,
|
|
8
|
+
beforeEach,
|
|
9
|
+
describe,
|
|
10
|
+
expect,
|
|
11
|
+
mock,
|
|
12
|
+
test,
|
|
13
|
+
} from "bun:test";
|
|
14
|
+
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// Mocks — must be set up before importing the module under test
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
const TEST_APPS_DIR = "/tmp/test-apps";
|
|
20
|
+
const testDirNameMap = new Map<string, string>([["my-app", "app-id-1"]]);
|
|
21
|
+
|
|
22
|
+
let capturedWatchCallback: ((eventType: string, filename: string | null) => void) | null = null;
|
|
23
|
+
const mockWatcher = { close: mock(() => {}) };
|
|
24
|
+
|
|
25
|
+
mock.module("node:fs", () => {
|
|
26
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
27
|
+
const actualFs = require("node:fs");
|
|
28
|
+
return {
|
|
29
|
+
...actualFs,
|
|
30
|
+
existsSync: mock((p: string) => p === TEST_APPS_DIR),
|
|
31
|
+
watch: mock(
|
|
32
|
+
(
|
|
33
|
+
_path: string,
|
|
34
|
+
_opts: Record<string, unknown>,
|
|
35
|
+
callback: (eventType: string, filename: string | null) => void,
|
|
36
|
+
) => {
|
|
37
|
+
capturedWatchCallback = callback;
|
|
38
|
+
return mockWatcher;
|
|
39
|
+
},
|
|
40
|
+
),
|
|
41
|
+
};
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
mock.module("../memory/app-store.js", () => ({
|
|
45
|
+
getAppsDir: mock(() => TEST_APPS_DIR),
|
|
46
|
+
resolveAppIdByDirName: mock(
|
|
47
|
+
(dirName: string) => testDirNameMap.get(dirName) ?? null,
|
|
48
|
+
),
|
|
49
|
+
}));
|
|
50
|
+
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
// Import after mocks
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
|
|
55
|
+
import { AppSourceWatcher } from "../daemon/app-source-watcher.js";
|
|
56
|
+
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
// Tests
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
|
|
61
|
+
describe("AppSourceWatcher", () => {
|
|
62
|
+
let watcher: AppSourceWatcher;
|
|
63
|
+
let onChangeSpy: ReturnType<typeof mock>;
|
|
64
|
+
|
|
65
|
+
beforeEach(() => {
|
|
66
|
+
watcher = new AppSourceWatcher();
|
|
67
|
+
onChangeSpy = mock(() => {});
|
|
68
|
+
capturedWatchCallback = null;
|
|
69
|
+
mockWatcher.close.mockClear();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
afterEach(() => {
|
|
73
|
+
watcher.stop();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("start() creates a recursive watcher on the apps directory", () => {
|
|
77
|
+
watcher.start(onChangeSpy);
|
|
78
|
+
expect(capturedWatchCallback).not.toBeNull();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("source file change triggers callback with resolved appId", async () => {
|
|
82
|
+
watcher.start(onChangeSpy);
|
|
83
|
+
capturedWatchCallback!("change", "my-app/src/main.tsx");
|
|
84
|
+
|
|
85
|
+
// Wait for debounce (500ms + margin)
|
|
86
|
+
await new Promise((r) => setTimeout(r, 600));
|
|
87
|
+
expect(onChangeSpy).toHaveBeenCalledTimes(1);
|
|
88
|
+
expect(onChangeSpy).toHaveBeenCalledWith("app-id-1");
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("root-level app file triggers callback", async () => {
|
|
92
|
+
watcher.start(onChangeSpy);
|
|
93
|
+
capturedWatchCallback!("change", "my-app/index.html");
|
|
94
|
+
|
|
95
|
+
await new Promise((r) => setTimeout(r, 600));
|
|
96
|
+
expect(onChangeSpy).toHaveBeenCalledTimes(1);
|
|
97
|
+
expect(onChangeSpy).toHaveBeenCalledWith("app-id-1");
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("dist/ files are filtered out", async () => {
|
|
101
|
+
watcher.start(onChangeSpy);
|
|
102
|
+
capturedWatchCallback!("change", "my-app/dist/index.html");
|
|
103
|
+
|
|
104
|
+
await new Promise((r) => setTimeout(r, 600));
|
|
105
|
+
expect(onChangeSpy).not.toHaveBeenCalled();
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("records/ files are filtered out", async () => {
|
|
109
|
+
watcher.start(onChangeSpy);
|
|
110
|
+
capturedWatchCallback!("change", "my-app/records/rec-1.json");
|
|
111
|
+
|
|
112
|
+
await new Promise((r) => setTimeout(r, 600));
|
|
113
|
+
expect(onChangeSpy).not.toHaveBeenCalled();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("files directly in apps/ (no subdirectory) are filtered out", async () => {
|
|
117
|
+
watcher.start(onChangeSpy);
|
|
118
|
+
capturedWatchCallback!("change", "my-app.json");
|
|
119
|
+
|
|
120
|
+
await new Promise((r) => setTimeout(r, 600));
|
|
121
|
+
expect(onChangeSpy).not.toHaveBeenCalled();
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("unknown app directory is filtered out", async () => {
|
|
125
|
+
watcher.start(onChangeSpy);
|
|
126
|
+
capturedWatchCallback!("change", "unknown-app/src/main.tsx");
|
|
127
|
+
|
|
128
|
+
await new Promise((r) => setTimeout(r, 600));
|
|
129
|
+
expect(onChangeSpy).not.toHaveBeenCalled();
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test("null filename is ignored", async () => {
|
|
133
|
+
watcher.start(onChangeSpy);
|
|
134
|
+
capturedWatchCallback!("change", null);
|
|
135
|
+
|
|
136
|
+
await new Promise((r) => setTimeout(r, 600));
|
|
137
|
+
expect(onChangeSpy).not.toHaveBeenCalled();
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test("rapid changes to same app are debounced into single callback", async () => {
|
|
141
|
+
watcher.start(onChangeSpy);
|
|
142
|
+
|
|
143
|
+
capturedWatchCallback!("change", "my-app/src/main.tsx");
|
|
144
|
+
capturedWatchCallback!("change", "my-app/src/styles.css");
|
|
145
|
+
capturedWatchCallback!("change", "my-app/src/utils.ts");
|
|
146
|
+
|
|
147
|
+
await new Promise((r) => setTimeout(r, 600));
|
|
148
|
+
expect(onChangeSpy).toHaveBeenCalledTimes(1);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test("stop() closes watcher and cancels pending timers", () => {
|
|
152
|
+
watcher.start(onChangeSpy);
|
|
153
|
+
capturedWatchCallback!("change", "my-app/src/main.tsx");
|
|
154
|
+
|
|
155
|
+
watcher.stop();
|
|
156
|
+
|
|
157
|
+
expect(mockWatcher.close).toHaveBeenCalledTimes(1);
|
|
158
|
+
});
|
|
159
|
+
});
|