@vellumai/assistant 0.5.1 → 0.5.2

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.
Files changed (338) hide show
  1. package/ARCHITECTURE.md +54 -54
  2. package/docs/architecture/integrations.md +62 -67
  3. package/docs/credential-execution-service.md +3 -3
  4. package/package.json +1 -1
  5. package/src/__tests__/agent-loop.test.ts +111 -0
  6. package/src/__tests__/always-loaded-tools-guard.test.ts +3 -4
  7. package/src/__tests__/app-builder-tool-scripts.test.ts +13 -151
  8. package/src/__tests__/app-dir-path-guard.test.ts +78 -0
  9. package/src/__tests__/app-executors.test.ts +1 -291
  10. package/src/__tests__/app-git-history.test.ts +4 -4
  11. package/src/__tests__/app-routes-csp.test.ts +1 -0
  12. package/src/__tests__/app-store-dir-names.test.ts +426 -0
  13. package/src/__tests__/attachments-store.test.ts +169 -21
  14. package/src/__tests__/attachments.test.ts +115 -1
  15. package/src/__tests__/btw-routes.test.ts +1 -0
  16. package/src/__tests__/canonical-guardian-store.test.ts +38 -0
  17. package/src/__tests__/channel-reply-delivery.test.ts +55 -0
  18. package/src/__tests__/checker.test.ts +54 -0
  19. package/src/__tests__/claude-code-skill-regression.test.ts +2 -0
  20. package/src/__tests__/claude-code-tool-profiles.test.ts +2 -0
  21. package/src/__tests__/compaction.benchmark.test.ts +2 -1
  22. package/src/__tests__/config-schema-cmd.test.ts +68 -21
  23. package/src/__tests__/config-schema.test.ts +1 -1
  24. package/src/__tests__/conversation-agent-loop-overflow.test.ts +149 -5
  25. package/src/__tests__/conversation-agent-loop.test.ts +290 -2
  26. package/src/__tests__/conversation-attachments.test.ts +17 -19
  27. package/src/__tests__/conversation-disk-view-integration.test.ts +277 -0
  28. package/src/__tests__/conversation-disk-view.test.ts +810 -0
  29. package/src/__tests__/conversation-error.test.ts +1 -1
  30. package/src/__tests__/conversation-fork-crud.test.ts +551 -0
  31. package/src/__tests__/conversation-fork-route.test.ts +386 -0
  32. package/src/__tests__/conversation-history-web-search.test.ts +1 -1
  33. package/src/__tests__/conversation-key-store-disk-view.test.ts +130 -0
  34. package/src/__tests__/conversation-media-retry.test.ts +8 -2
  35. package/src/__tests__/conversation-queue.test.ts +36 -1
  36. package/src/__tests__/conversation-routes-disk-view.test.ts +439 -0
  37. package/src/__tests__/conversation-routes-guardian-reply.test.ts +2 -2
  38. package/src/__tests__/conversation-routes-slash-commands.test.ts +2 -7
  39. package/src/__tests__/conversation-runtime-assembly.test.ts +17 -2
  40. package/src/__tests__/conversation-skill-tools.test.ts +4 -9
  41. package/src/__tests__/conversation-slash-commands.test.ts +149 -0
  42. package/src/__tests__/conversation-store.test.ts +24 -21
  43. package/src/__tests__/conversation-surfaces-state-update.test.ts +246 -0
  44. package/src/__tests__/conversation-surfaces-task-progress.test.ts +1 -0
  45. package/src/__tests__/conversation-title-service.test.ts +137 -0
  46. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +25 -315
  47. package/src/__tests__/conversation-tool-setup-memory-scope.test.ts +1 -0
  48. package/src/__tests__/conversation-tool-setup-side-effect-flag.test.ts +1 -0
  49. package/src/__tests__/conversation-workspace-cache-state.test.ts +44 -2
  50. package/src/__tests__/conversation-workspace-injection.test.ts +11 -0
  51. package/src/__tests__/credential-security-invariants.test.ts +3 -0
  52. package/src/__tests__/credential-vault-unit.test.ts +5 -10
  53. package/src/__tests__/cu-unified-flow.test.ts +1 -0
  54. package/src/__tests__/db-conversation-fork-lineage-migration.test.ts +241 -0
  55. package/src/__tests__/db-llm-request-log-provider-migration.test.ts +214 -0
  56. package/src/__tests__/diagnostics-export.test.ts +70 -1
  57. package/src/__tests__/first-greeting.test.ts +80 -0
  58. package/src/__tests__/gateway-only-guard.test.ts +1 -0
  59. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +3 -7
  60. package/src/__tests__/history-repair.test.ts +32 -10
  61. package/src/__tests__/http-conversation-lineage.test.ts +251 -0
  62. package/src/__tests__/image-source-path-reinject.test.ts +136 -0
  63. package/src/__tests__/llm-context-normalization.test.ts +1116 -0
  64. package/src/__tests__/llm-context-route-provider.test.ts +217 -0
  65. package/src/__tests__/llm-request-log-turn-query.test.ts +270 -0
  66. package/src/__tests__/media-generate-image.test.ts +47 -94
  67. package/src/__tests__/memory-lifecycle-e2e.test.ts +3 -1
  68. package/src/__tests__/memory-recall-quality.test.ts +5 -5
  69. package/src/__tests__/migration-cross-version-compatibility.test.ts +4 -1
  70. package/src/__tests__/migration-export-http.test.ts +3 -1
  71. package/src/__tests__/migration-import-commit-http.test.ts +18 -4
  72. package/src/__tests__/migration-import-preflight-http.test.ts +1 -3
  73. package/src/__tests__/mime-builder.test.ts +3 -2
  74. package/src/__tests__/non-member-access-request.test.ts +12 -1
  75. package/src/__tests__/notification-decision-identity.test.ts +52 -0
  76. package/src/__tests__/oauth-apps-routes.test.ts +103 -0
  77. package/src/__tests__/oauth-store.test.ts +115 -0
  78. package/src/__tests__/provider-error-scenarios.test.ts +1 -3
  79. package/src/__tests__/provider-failover-actual-provider.test.ts +66 -0
  80. package/src/__tests__/recording-handler.test.ts +17 -0
  81. package/src/__tests__/registry.test.ts +3 -8
  82. package/src/__tests__/relay-server.test.ts +1 -1
  83. package/src/__tests__/runtime-attachment-metadata.test.ts +7 -3
  84. package/src/__tests__/schema-transforms.test.ts +165 -5
  85. package/src/__tests__/server-history-render.test.ts +2 -2
  86. package/src/__tests__/slack-app-setup-skill-regression.test.ts +3 -1
  87. package/src/__tests__/slack-inbound-verification.test.ts +2 -2
  88. package/src/__tests__/starter-task-flow.test.ts +1 -0
  89. package/src/__tests__/suggestion-routes.test.ts +443 -0
  90. package/src/__tests__/swarm-conversation-integration.test.ts +1 -0
  91. package/src/__tests__/swarm-recursion.test.ts +1 -0
  92. package/src/__tests__/swarm-tool.test.ts +1 -0
  93. package/src/__tests__/tool-execution-abort-cleanup.test.ts +1 -0
  94. package/src/__tests__/tool-preview-lifecycle.test.ts +32 -5
  95. package/src/__tests__/top-level-renderer.test.ts +22 -0
  96. package/src/__tests__/turn-boundary-resolution.test.ts +243 -0
  97. package/src/__tests__/web-fetch.test.ts +6 -2
  98. package/src/__tests__/workspace-migration-006-services-config.test.ts +335 -0
  99. package/src/__tests__/workspace-migration-007-web-search-provider-rename.test.ts +312 -0
  100. package/src/__tests__/workspace-migration-009-backfill-conversation-disk-view.test.ts +278 -0
  101. package/src/__tests__/workspace-migration-010-app-dir-rename.test.ts +275 -0
  102. package/src/__tests__/workspace-migration-012-rename-conversation-disk-view-dirs.test.ts +77 -0
  103. package/src/__tests__/workspace-migration-013-repair-conversation-disk-view.test.ts +401 -0
  104. package/src/__tests__/workspace-migration-backfill-installation-id.test.ts +328 -0
  105. package/src/__tests__/workspace-migration-seed-device-id.test.ts +6 -10
  106. package/src/agent/attachments.ts +27 -1
  107. package/src/agent/loop.ts +29 -1
  108. package/src/avatar/traits-png-sync.ts +80 -25
  109. package/src/bundler/app-bundler.ts +4 -4
  110. package/src/calls/call-domain.ts +1 -0
  111. package/src/calls/voice-session-bridge.ts +1 -0
  112. package/src/cli/commands/auth.ts +92 -0
  113. package/src/cli/commands/avatar.ts +7 -6
  114. package/src/cli/commands/config.ts +2 -0
  115. package/src/cli/commands/oauth/providers.ts +29 -0
  116. package/src/cli/program.ts +12 -0
  117. package/src/cli.ts +15 -48
  118. package/src/config/bundled-skills/app-builder/SKILL.md +103 -28
  119. package/src/config/bundled-skills/app-builder/TOOLS.json +5 -199
  120. package/src/config/bundled-skills/app-builder/tools/{app-query.ts → app-refresh.ts} +2 -2
  121. package/src/config/bundled-skills/contacts/tools/google-contacts.ts +2 -3
  122. package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +6 -9
  123. package/src/config/bundled-skills/gmail/tools/gmail-attachments.ts +4 -6
  124. package/src/config/bundled-skills/gmail/tools/gmail-draft.ts +2 -3
  125. package/src/config/bundled-skills/gmail/tools/gmail-filters.ts +2 -3
  126. package/src/config/bundled-skills/gmail/tools/gmail-follow-up.ts +2 -3
  127. package/src/config/bundled-skills/gmail/tools/gmail-forward.ts +2 -3
  128. package/src/config/bundled-skills/gmail/tools/gmail-label.ts +4 -6
  129. package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +2 -3
  130. package/src/config/bundled-skills/gmail/tools/gmail-send-draft.ts +2 -3
  131. package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +2 -3
  132. package/src/config/bundled-skills/gmail/tools/gmail-trash.ts +2 -3
  133. package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +2 -3
  134. package/src/config/bundled-skills/gmail/tools/gmail-vacation.ts +2 -3
  135. package/src/config/bundled-skills/google-calendar/tools/shared.ts +1 -1
  136. package/src/config/bundled-skills/image-studio/SKILL.md +2 -2
  137. package/src/config/bundled-skills/image-studio/TOOLS.json +2 -2
  138. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +45 -72
  139. package/src/config/bundled-skills/media-processing/tools/extract-keyframes.ts +2 -2
  140. package/src/config/bundled-skills/messaging/tools/shared.ts +1 -1
  141. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +19 -3
  142. package/src/config/bundled-skills/skill-management/TOOLS.json +2 -2
  143. package/src/config/bundled-skills/slack/tools/shared.ts +19 -4
  144. package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +2 -3
  145. package/src/config/bundled-skills/transcribe/SKILL.md +1 -1
  146. package/src/config/bundled-skills/transcribe/TOOLS.json +2 -6
  147. package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +19 -83
  148. package/src/config/bundled-tool-registry.ts +2 -14
  149. package/src/config/feature-flag-registry.json +8 -0
  150. package/src/config/loader.ts +64 -0
  151. package/src/config/raw-config-utils.ts +30 -0
  152. package/src/config/schema-utils.ts +28 -7
  153. package/src/config/schema.ts +8 -0
  154. package/src/config/schemas/elevenlabs.ts +18 -0
  155. package/src/config/schemas/memory-lifecycle.ts +4 -2
  156. package/src/config/schemas/memory-storage.ts +1 -1
  157. package/src/config/schemas/services.ts +8 -6
  158. package/src/contacts/contact-store.ts +13 -6
  159. package/src/contacts/contacts-write.ts +0 -1
  160. package/src/context/window-manager.ts +13 -2
  161. package/src/daemon/conversation-agent-loop-handlers.ts +48 -7
  162. package/src/daemon/conversation-agent-loop.ts +56 -19
  163. package/src/daemon/conversation-attachments.ts +18 -36
  164. package/src/daemon/conversation-error.ts +2 -1
  165. package/src/daemon/conversation-history.ts +18 -4
  166. package/src/daemon/conversation-lifecycle.ts +39 -15
  167. package/src/daemon/conversation-messaging.ts +70 -26
  168. package/src/daemon/conversation-process.ts +58 -34
  169. package/src/daemon/conversation-runtime-assembly.ts +21 -38
  170. package/src/daemon/conversation-slash.ts +121 -256
  171. package/src/daemon/conversation-surfaces.ts +143 -20
  172. package/src/daemon/conversation-tool-setup.ts +0 -6
  173. package/src/daemon/conversation-workspace.ts +21 -1
  174. package/src/daemon/conversation.ts +51 -29
  175. package/src/daemon/first-greeting.ts +35 -0
  176. package/src/daemon/handlers/config-embeddings.ts +148 -0
  177. package/src/daemon/handlers/config-model.ts +71 -26
  178. package/src/daemon/handlers/conversations.ts +0 -23
  179. package/src/daemon/handlers/recording.ts +26 -21
  180. package/src/daemon/host-cu-proxy.ts +2 -2
  181. package/src/daemon/lifecycle.ts +106 -64
  182. package/src/daemon/message-protocol.ts +3 -0
  183. package/src/daemon/message-types/conversations.ts +19 -0
  184. package/src/daemon/message-types/messages.ts +1 -0
  185. package/src/daemon/message-types/shared.ts +2 -0
  186. package/src/daemon/message-types/surfaces.ts +2 -0
  187. package/src/daemon/message-types/upgrades.ts +23 -0
  188. package/src/daemon/server.ts +83 -12
  189. package/src/daemon/shutdown-handlers.ts +8 -5
  190. package/src/daemon/startup-error.ts +9 -0
  191. package/src/daemon/tool-side-effects.ts +11 -28
  192. package/src/events/tool-permission-telemetry-listener.ts +1 -3
  193. package/src/instrument.ts +0 -4
  194. package/src/media/app-icon-generator.ts +2 -2
  195. package/src/memory/app-git-service.ts +28 -16
  196. package/src/memory/app-store.ts +230 -41
  197. package/src/memory/attachments-store.ts +558 -130
  198. package/src/memory/conversation-attention-store.ts +70 -0
  199. package/src/memory/conversation-crud.ts +442 -3
  200. package/src/memory/conversation-directories.ts +125 -0
  201. package/src/memory/conversation-disk-view.ts +390 -0
  202. package/src/memory/conversation-key-store.ts +17 -5
  203. package/src/memory/conversation-queries.ts +5 -1
  204. package/src/memory/conversation-title-service.ts +21 -49
  205. package/src/memory/db-init.ts +28 -0
  206. package/src/memory/embedding-backend.ts +42 -53
  207. package/src/memory/embedding-gemini.test.ts +4 -4
  208. package/src/memory/embedding-local.ts +1 -3
  209. package/src/memory/embedding-ollama.ts +1 -3
  210. package/src/memory/embedding-openai.ts +1 -3
  211. package/src/memory/indexer.ts +9 -7
  212. package/src/memory/items-extractor.ts +42 -13
  213. package/src/memory/job-handlers/conversation-starters.ts +6 -1
  214. package/src/memory/job-handlers/embedding.test.ts +1 -4
  215. package/src/memory/llm-request-log-store.ts +100 -1
  216. package/src/memory/migrations/102-alter-table-columns.ts +5 -0
  217. package/src/memory/migrations/146-schedule-oneshot-routing.ts +3 -3
  218. package/src/memory/migrations/147-migrate-reminders-to-schedules.ts +66 -70
  219. package/src/memory/migrations/148-drop-reminders-table.ts +5 -9
  220. package/src/memory/migrations/160-drop-loopback-port-column.ts +1 -3
  221. package/src/memory/migrations/174-rename-thread-starters-table.ts +0 -7
  222. package/src/memory/migrations/178-oauth-providers-managed-service-config-key.ts +15 -0
  223. package/src/memory/migrations/179-llm-request-log-message-id.ts +16 -0
  224. package/src/memory/migrations/180-backfill-inline-attachments-to-disk.ts +66 -0
  225. package/src/memory/migrations/181-rename-thread-starters-checkpoints.ts +46 -0
  226. package/src/memory/migrations/182-oauth-providers-display-metadata.ts +20 -0
  227. package/src/memory/migrations/183-add-conversation-fork-lineage.ts +22 -0
  228. package/src/memory/migrations/184-llm-request-log-provider.ts +12 -0
  229. package/src/memory/migrations/index.ts +7 -0
  230. package/src/memory/migrations/registry.ts +13 -0
  231. package/src/memory/retriever.test.ts +601 -2
  232. package/src/memory/retriever.ts +85 -9
  233. package/src/memory/schema/conversations.ts +6 -0
  234. package/src/memory/schema/infrastructure.ts +13 -7
  235. package/src/memory/schema/oauth.ts +6 -0
  236. package/src/messaging/providers/gmail/mime-builder.ts +3 -1
  237. package/src/notifications/copy-composer.ts +26 -0
  238. package/src/notifications/decision-engine.ts +14 -1
  239. package/src/notifications/emit-signal.ts +1 -1
  240. package/src/notifications/signal.ts +36 -0
  241. package/src/oauth/byo-connection.test.ts +1 -45
  242. package/src/oauth/byo-connection.ts +2 -8
  243. package/src/oauth/connect-orchestrator.ts +15 -11
  244. package/src/oauth/connection-resolver.test.ts +191 -0
  245. package/src/oauth/connection-resolver.ts +66 -38
  246. package/src/oauth/connection.ts +0 -1
  247. package/src/oauth/oauth-store.ts +97 -47
  248. package/src/oauth/platform-connection.test.ts +0 -1
  249. package/src/oauth/platform-connection.ts +11 -3
  250. package/src/oauth/seed-providers.ts +78 -3
  251. package/src/oauth/token-persistence.ts +16 -10
  252. package/src/permissions/checker.ts +71 -8
  253. package/src/prompts/templates/BOOTSTRAP.md +2 -0
  254. package/src/providers/anthropic/client.ts +8 -1
  255. package/src/providers/failover.ts +4 -1
  256. package/src/providers/gemini/client.ts +50 -0
  257. package/src/providers/model-catalog.ts +92 -0
  258. package/src/providers/model-intents.ts +29 -20
  259. package/src/providers/openai/client.ts +49 -0
  260. package/src/providers/types.ts +2 -0
  261. package/src/runtime/access-request-helper.ts +16 -7
  262. package/src/runtime/auth/credential-service.ts +3 -1
  263. package/src/runtime/auth/route-policy.ts +14 -1
  264. package/src/runtime/btw-sidechain.ts +101 -0
  265. package/src/runtime/channel-reply-delivery.ts +17 -1
  266. package/src/runtime/http-router.ts +3 -1
  267. package/src/runtime/http-server.ts +196 -141
  268. package/src/runtime/http-types.ts +1 -0
  269. package/src/runtime/migrations/vbundle-builder.ts +5 -1
  270. package/src/runtime/routes/access-request-decision.ts +41 -0
  271. package/src/runtime/routes/app-management-routes.ts +6 -3
  272. package/src/runtime/routes/app-routes.ts +7 -3
  273. package/src/runtime/routes/approval-routes.ts +1 -0
  274. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +34 -2
  275. package/src/runtime/routes/attachment-routes.ts +45 -15
  276. package/src/runtime/routes/btw-routes.ts +21 -61
  277. package/src/runtime/routes/conversation-management-routes.ts +68 -0
  278. package/src/runtime/routes/conversation-query-routes.ts +180 -10
  279. package/src/runtime/routes/conversation-routes.ts +222 -28
  280. package/src/runtime/routes/conversation-starter-routes.ts +9 -11
  281. package/src/runtime/routes/diagnostics-routes.ts +1 -0
  282. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +2 -2
  283. package/src/runtime/routes/llm-context-normalization.ts +1199 -0
  284. package/src/runtime/routes/log-export-routes.ts +3 -0
  285. package/src/runtime/routes/memory-item-routes.test.ts +34 -0
  286. package/src/runtime/routes/memory-item-routes.ts +4 -0
  287. package/src/runtime/routes/migration-routes.ts +4 -1
  288. package/src/runtime/routes/oauth-apps.ts +291 -0
  289. package/src/runtime/routes/secret-routes.ts +28 -1
  290. package/src/runtime/routes/settings-routes.ts +14 -0
  291. package/src/runtime/routes/trace-event-routes.ts +4 -1
  292. package/src/schedule/schedule-store.ts +9 -21
  293. package/src/security/secure-keys.ts +21 -0
  294. package/src/signals/bash.ts +1 -1
  295. package/src/swarm/backend-claude-code.ts +3 -6
  296. package/src/telemetry/usage-telemetry-reporter.test.ts +3 -2
  297. package/src/telemetry/usage-telemetry-reporter.ts +3 -1
  298. package/src/tools/AGENTS.md +6 -10
  299. package/src/tools/apps/executors.ts +17 -232
  300. package/src/tools/claude-code/claude-code.ts +2 -3
  301. package/src/tools/credentials/vault.ts +7 -12
  302. package/src/tools/host-filesystem/read.ts +13 -10
  303. package/src/tools/network/__tests__/web-search.test.ts +4 -2
  304. package/src/tools/schedule/list.ts +2 -7
  305. package/src/tools/schema-transforms.ts +5 -0
  306. package/src/tools/shared/filesystem/format-diff.ts +2 -7
  307. package/src/tools/skills/execute.ts +1 -1
  308. package/src/tools/tool-manifest.ts +0 -6
  309. package/src/tools/ui-surface/definitions.ts +2 -2
  310. package/src/util/device-id.ts +28 -5
  311. package/src/util/platform.ts +6 -0
  312. package/src/util/pricing.ts +1 -0
  313. package/src/util/retry.ts +1 -3
  314. package/src/workspace/migrations/002-backfill-installation-id.ts +23 -12
  315. package/src/workspace/migrations/003-seed-device-id.ts +3 -4
  316. package/src/workspace/migrations/006-services-config.ts +5 -0
  317. package/src/workspace/migrations/008-voice-timeout-and-max-steps.ts +12 -0
  318. package/src/workspace/migrations/009-backfill-conversation-disk-view.ts +10 -0
  319. package/src/workspace/migrations/010-app-dir-rename.ts +223 -0
  320. package/src/workspace/migrations/012-rename-conversation-disk-view-dirs.ts +64 -0
  321. package/src/workspace/migrations/013-repair-conversation-disk-view.ts +11 -0
  322. package/src/workspace/migrations/rebuild-conversation-disk-view.ts +186 -0
  323. package/src/workspace/migrations/registry.ts +10 -0
  324. package/src/workspace/top-level-renderer.ts +12 -0
  325. package/src/__tests__/asset-materialize-tool.test.ts +0 -523
  326. package/src/__tests__/asset-search-tool.test.ts +0 -536
  327. package/src/__tests__/fixtures/media-reuse-fixtures.ts +0 -56
  328. package/src/__tests__/media-reuse-story.e2e.test.ts +0 -762
  329. package/src/__tests__/media-visibility-policy.test.ts +0 -190
  330. package/src/config/bundled-skills/app-builder/tools/app-file-edit.ts +0 -14
  331. package/src/config/bundled-skills/app-builder/tools/app-file-list.ts +0 -13
  332. package/src/config/bundled-skills/app-builder/tools/app-file-read.ts +0 -21
  333. package/src/config/bundled-skills/app-builder/tools/app-file-write.ts +0 -14
  334. package/src/config/bundled-skills/app-builder/tools/app-list.ts +0 -13
  335. package/src/config/bundled-skills/app-builder/tools/app-update.ts +0 -23
  336. package/src/daemon/media-visibility-policy.ts +0 -59
  337. package/src/tools/assets/materialize.ts +0 -248
  338. package/src/tools/assets/search.ts +0 -400
@@ -2,7 +2,6 @@ import { describe, expect, mock, test } from "bun:test";
2
2
 
3
3
  import type { AppDefinition } from "../memory/app-store.js";
4
4
  import type { AppStore } from "../tools/apps/executors.js";
5
- import type { EditEngineResult } from "../tools/shared/filesystem/edit-engine.js";
6
5
  import type { ToolContext } from "../tools/types.js";
7
6
 
8
7
  // ---------------------------------------------------------------------------
@@ -26,24 +25,11 @@ function makeMockStore(overrides: Partial<AppStore> = {}): AppStore {
26
25
  return {
27
26
  getApp: () => makeApp(),
28
27
  listApps: () => [makeApp()],
29
- queryAppRecords: () => [],
30
- listAppFiles: () => ["index.html"],
31
- readAppFile: () => "<h1>Hi</h1>",
32
28
  createApp: (params) =>
33
29
  makeApp({ name: params.name, description: params.description }),
34
30
  updateApp: (id, updates) => makeApp({ id, ...updates }),
35
31
  deleteApp: () => {},
36
32
  writeAppFile: () => {},
37
- editAppFile: () =>
38
- ({
39
- ok: true,
40
- updatedContent: "new",
41
- matchCount: 1,
42
- matchMethod: "exact",
43
- similarity: 1,
44
- actualOld: "old",
45
- actualNew: "new",
46
- }) as EditEngineResult,
47
33
  ...overrides,
48
34
  };
49
35
  }
@@ -66,6 +52,11 @@ const mockStore = makeMockStore();
66
52
  mock.module("../memory/app-store.js", () => ({
67
53
  ...mockStore,
68
54
  getAppsDir: () => "/tmp/test-apps",
55
+ getAppDirPath: (appId: string) => `/tmp/test-apps/${appId}`,
56
+ resolveAppDir: (id: string) => ({
57
+ dirName: id,
58
+ appDir: `/tmp/test-apps/${id}`,
59
+ }),
69
60
  isMultifileApp: (app: AppDefinition) => app.formatVersion === 2,
70
61
  }));
71
62
 
@@ -85,13 +76,7 @@ mock.module("../bundler/app-compiler.js", () => ({
85
76
 
86
77
  import * as appCreateScript from "../config/bundled-skills/app-builder/tools/app-create.js";
87
78
  import * as appDeleteScript from "../config/bundled-skills/app-builder/tools/app-delete.js";
88
- import * as appFileEditScript from "../config/bundled-skills/app-builder/tools/app-file-edit.js";
89
- import * as appFileListScript from "../config/bundled-skills/app-builder/tools/app-file-list.js";
90
- import * as appFileReadScript from "../config/bundled-skills/app-builder/tools/app-file-read.js";
91
- import * as appFileWriteScript from "../config/bundled-skills/app-builder/tools/app-file-write.js";
92
- import * as appListScript from "../config/bundled-skills/app-builder/tools/app-list.js";
93
- import * as appQueryScript from "../config/bundled-skills/app-builder/tools/app-query.js";
94
- import * as appUpdateScript from "../config/bundled-skills/app-builder/tools/app-update.js";
79
+ import * as appRefreshScript from "../config/bundled-skills/app-builder/tools/app-refresh.js";
95
80
 
96
81
  // ---------------------------------------------------------------------------
97
82
  // Tests
@@ -142,58 +127,6 @@ describe("app-builder skill tool scripts", () => {
142
127
  });
143
128
  });
144
129
 
145
- // ---- app-list ----------------------------------------------------------
146
-
147
- describe("app-list", () => {
148
- test("exports a run function", () => {
149
- expect(typeof appListScript.run).toBe("function");
150
- });
151
-
152
- test("delegates to executeAppList and returns result", async () => {
153
- const result = await appListScript.run({}, makeContext());
154
- expect(result.isError).toBe(false);
155
- const parsed = JSON.parse(result.content);
156
- expect(Array.isArray(parsed)).toBe(true);
157
- expect(parsed.length).toBeGreaterThan(0);
158
- expect(parsed[0].id).toBe("app-1");
159
- });
160
- });
161
-
162
- // ---- app-query ---------------------------------------------------------
163
-
164
- describe("app-query", () => {
165
- test("exports a run function", () => {
166
- expect(typeof appQueryScript.run).toBe("function");
167
- });
168
-
169
- test("delegates to executeAppQuery and returns result", async () => {
170
- const result = await appQueryScript.run(
171
- { app_id: "app-1" },
172
- makeContext(),
173
- );
174
- expect(result.isError).toBe(false);
175
- expect(JSON.parse(result.content)).toEqual([]);
176
- });
177
- });
178
-
179
- // ---- app-update --------------------------------------------------------
180
-
181
- describe("app-update", () => {
182
- test("exports a run function", () => {
183
- expect(typeof appUpdateScript.run).toBe("function");
184
- });
185
-
186
- test("delegates to executeAppUpdate and returns result", async () => {
187
- const result = await appUpdateScript.run(
188
- { app_id: "app-1", name: "Updated" },
189
- makeContext(),
190
- );
191
- expect(result.isError).toBe(false);
192
- const parsed = JSON.parse(result.content);
193
- expect(parsed.name).toBe("Updated");
194
- });
195
- });
196
-
197
130
  // ---- app-delete --------------------------------------------------------
198
131
 
199
132
  describe("app-delete", () => {
@@ -213,93 +146,22 @@ describe("app-builder skill tool scripts", () => {
213
146
  });
214
147
  });
215
148
 
216
- // ---- app-file-list -----------------------------------------------------
149
+ // ---- app-refresh -------------------------------------------------------
217
150
 
218
- describe("app-file-list", () => {
151
+ describe("app-refresh", () => {
219
152
  test("exports a run function", () => {
220
- expect(typeof appFileListScript.run).toBe("function");
153
+ expect(typeof appRefreshScript.run).toBe("function");
221
154
  });
222
155
 
223
- test("delegates to executeAppFileList and returns result", async () => {
224
- const result = await appFileListScript.run(
156
+ test("delegates to executeAppRefresh and returns result", async () => {
157
+ const result = await appRefreshScript.run(
225
158
  { app_id: "app-1" },
226
159
  makeContext(),
227
160
  );
228
161
  expect(result.isError).toBe(false);
229
- expect(JSON.parse(result.content)).toEqual(["index.html"]);
230
- });
231
- });
232
-
233
- // ---- app-file-read -----------------------------------------------------
234
-
235
- describe("app-file-read", () => {
236
- test("exports a run function", () => {
237
- expect(typeof appFileReadScript.run).toBe("function");
238
- });
239
-
240
- test("delegates to executeAppFileRead and returns formatted content", async () => {
241
- const result = await appFileReadScript.run(
242
- { app_id: "app-1", path: "index.html" },
243
- makeContext(),
244
- );
245
- expect(result.isError).toBe(false);
246
- // Content should include line numbers
247
- expect(result.content).toContain("1\t");
248
- });
249
- });
250
-
251
- // ---- app-file-edit -----------------------------------------------------
252
-
253
- describe("app-file-edit", () => {
254
- test("exports a run function", () => {
255
- expect(typeof appFileEditScript.run).toBe("function");
256
- });
257
-
258
- test("delegates to executeAppFileEdit and returns result", async () => {
259
- const result = await appFileEditScript.run(
260
- {
261
- app_id: "app-1",
262
- path: "index.html",
263
- old_string: "old",
264
- new_string: "new",
265
- },
266
- makeContext(),
267
- );
268
- expect(result.isError).toBe(false);
269
162
  const parsed = JSON.parse(result.content);
270
- expect(parsed.ok).toBe(true);
271
- });
272
-
273
- test("returns error when old_string is empty", async () => {
274
- const result = await appFileEditScript.run(
275
- {
276
- app_id: "app-1",
277
- path: "index.html",
278
- old_string: "",
279
- new_string: "new",
280
- },
281
- makeContext(),
282
- );
283
- expect(result.isError).toBe(true);
284
- });
285
- });
286
-
287
- // ---- app-file-write ----------------------------------------------------
288
-
289
- describe("app-file-write", () => {
290
- test("exports a run function", () => {
291
- expect(typeof appFileWriteScript.run).toBe("function");
292
- });
293
-
294
- test("delegates to executeAppFileWrite and returns result", async () => {
295
- const result = await appFileWriteScript.run(
296
- { app_id: "app-1", path: "new.html", content: "<div/>" },
297
- makeContext(),
298
- );
299
- expect(result.isError).toBe(false);
300
- const parsed = JSON.parse(result.content);
301
- expect(parsed.written).toBe(true);
302
- expect(parsed.path).toBe("new.html");
163
+ expect(parsed.refreshed).toBe(true);
164
+ expect(parsed.appId).toBe("app-1");
303
165
  });
304
166
  });
305
167
  });
@@ -0,0 +1,78 @@
1
+ import { execSync } from "node:child_process";
2
+ import { describe, expect, test } from "bun:test";
3
+
4
+ /**
5
+ * Guard test: files outside app-store.ts must not import getAppsDir and use
6
+ * it to construct paths with an app ID. All app path construction must go
7
+ * through getAppDirPath() or resolveAppDir() from app-store.ts.
8
+ *
9
+ * This prevents regressions where new code bypasses the dirName-based path
10
+ * resolution and constructs UUID-based paths directly.
11
+ *
12
+ * Allowlist: only app-store.ts itself, app-git-service.ts (uses getAppsDir
13
+ * for the git repo root, not for per-app paths), and workspace migrations
14
+ * (self-contained, don't import from app-store).
15
+ */
16
+
17
+ /** Files that are permitted to import getAppsDir. */
18
+ const ALLOWLIST = new Set([
19
+ "assistant/src/memory/app-store.ts", // defines getAppsDir
20
+ "assistant/src/memory/app-git-service.ts", // uses getAppsDir for git repo root, not per-app paths
21
+ ]);
22
+
23
+ function isTestFile(filePath: string): boolean {
24
+ return (
25
+ filePath.includes("/__tests__/") ||
26
+ filePath.endsWith(".test.ts") ||
27
+ filePath.endsWith(".test.js") ||
28
+ filePath.endsWith(".spec.ts")
29
+ );
30
+ }
31
+
32
+ function isMigrationFile(filePath: string): boolean {
33
+ return filePath.includes("/workspace/migrations/");
34
+ }
35
+
36
+ describe("app directory path construction guard", () => {
37
+ test("no non-allowlisted production files import getAppsDir", () => {
38
+ // Search for files that import getAppsDir (not just mention it in comments)
39
+ const pattern = "import.*getAppsDir.*from|getAppsDir\\(\\)";
40
+
41
+ let grepOutput = "";
42
+ try {
43
+ grepOutput = execSync(`git grep -lE '${pattern}' -- '*.ts'`, {
44
+ encoding: "utf-8",
45
+ cwd: process.cwd() + "/..",
46
+ }).trim();
47
+ } catch (err) {
48
+ // Exit code 1 means no matches
49
+ if ((err as { status?: number }).status === 1) {
50
+ return;
51
+ }
52
+ throw err;
53
+ }
54
+
55
+ const files = grepOutput.split("\n").filter((f) => f.length > 0);
56
+ const violations = files.filter((f) => {
57
+ if (isTestFile(f)) return false;
58
+ if (isMigrationFile(f)) return false;
59
+ if (ALLOWLIST.has(f)) return false;
60
+ return true;
61
+ });
62
+
63
+ if (violations.length > 0) {
64
+ const message = [
65
+ "Found non-allowlisted production files importing or using getAppsDir().",
66
+ "Use getAppDirPath(appId) or resolveAppDir(appId) from app-store.ts instead.",
67
+ "",
68
+ "Violations:",
69
+ ...violations.map((f) => ` - ${f}`),
70
+ "",
71
+ "To fix: replace getAppsDir() + appId path construction with getAppDirPath(appId).",
72
+ "If this is an intentional exception, add it to the ALLOWLIST in app-dir-path-guard.test.ts.",
73
+ ].join("\n");
74
+
75
+ expect(violations, message).toEqual([]);
76
+ }
77
+ });
78
+ });
@@ -11,14 +11,7 @@ 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 {
15
- executeAppCreate,
16
- executeAppFileEdit,
17
- executeAppFileList,
18
- executeAppFileRead,
19
- executeAppFileWrite,
20
- resolveAppFilePath,
21
- } from "../tools/apps/executors.js";
14
+ import { executeAppCreate } from "../tools/apps/executors.js";
22
15
 
23
16
  // ---------------------------------------------------------------------------
24
17
  // Helpers
@@ -60,298 +53,15 @@ function mockStore(
60
53
  return {
61
54
  getApp: (id: string) => (id === app.id ? app : null),
62
55
  listApps: () => [app],
63
- queryAppRecords: () => [],
64
- listAppFiles: () => Object.keys(files).sort(),
65
- readAppFile: (_appId: string, path: string) => {
66
- if (!(path in files)) throw new Error(`File not found: ${path}`);
67
- return files[path];
68
- },
69
56
  createApp: () => app,
70
57
  updateApp: () => app,
71
58
  deleteApp: () => {},
72
59
  writeAppFile: (_appId: string, path: string, content: string) => {
73
60
  files[path] = content;
74
61
  },
75
- editAppFile: (
76
- _appId: string,
77
- path: string,
78
- oldStr: string,
79
- newStr: string,
80
- _replaceAll?: boolean,
81
- ) => {
82
- if (!(path in files)) throw new Error(`File not found: ${path}`);
83
- const content = files[path];
84
- if (!content.includes(oldStr)) {
85
- return { ok: false as const, reason: "not_found" as const };
86
- }
87
- const updated = content.replace(oldStr, newStr);
88
- files[path] = updated;
89
- return {
90
- ok: true as const,
91
- updatedContent: updated,
92
- matchCount: 1,
93
- matchMethod: "exact" as const,
94
- similarity: 1,
95
- actualOld: oldStr,
96
- actualNew: newStr,
97
- };
98
- },
99
62
  };
100
63
  }
101
64
 
102
- // ---------------------------------------------------------------------------
103
- // resolveAppFilePath
104
- // ---------------------------------------------------------------------------
105
-
106
- describe("resolveAppFilePath", () => {
107
- test("prepends src/ for multifile app with plain path", () => {
108
- const app = makeMultifileApp();
109
- expect(resolveAppFilePath(app, "main.tsx")).toBe("src/main.tsx");
110
- });
111
-
112
- test("prepends src/ for nested path in multifile app", () => {
113
- const app = makeMultifileApp();
114
- expect(resolveAppFilePath(app, "components/Header.tsx")).toBe(
115
- "src/components/Header.tsx",
116
- );
117
- });
118
-
119
- test("passes through src/ prefix unchanged for multifile app", () => {
120
- const app = makeMultifileApp();
121
- expect(resolveAppFilePath(app, "src/main.tsx")).toBe("src/main.tsx");
122
- });
123
-
124
- test("passes through dist/ prefix unchanged for multifile app", () => {
125
- const app = makeMultifileApp();
126
- expect(resolveAppFilePath(app, "dist/bundle.js")).toBe("dist/bundle.js");
127
- });
128
-
129
- test("passes through records/ prefix unchanged for multifile app", () => {
130
- const app = makeMultifileApp();
131
- expect(resolveAppFilePath(app, "records/data.json")).toBe(
132
- "records/data.json",
133
- );
134
- });
135
-
136
- test("does not modify path for legacy app", () => {
137
- const app = makeLegacyApp();
138
- expect(resolveAppFilePath(app, "main.tsx")).toBe("main.tsx");
139
- });
140
-
141
- test("does not modify path for legacy app (formatVersion undefined)", () => {
142
- const app = makeLegacyApp({ formatVersion: undefined });
143
- expect(resolveAppFilePath(app, "styles.css")).toBe("styles.css");
144
- });
145
-
146
- test("does not modify path for legacy app (formatVersion 1)", () => {
147
- const app = makeLegacyApp({ formatVersion: 1 });
148
- expect(resolveAppFilePath(app, "index.html")).toBe("index.html");
149
- });
150
-
151
- test("strips ./ prefix and prepends src/ for multifile app", () => {
152
- const app = makeMultifileApp();
153
- expect(resolveAppFilePath(app, "./main.tsx")).toBe("src/main.tsx");
154
- });
155
-
156
- test("strips ./ prefix for known top-level dir in multifile app", () => {
157
- const app = makeMultifileApp();
158
- expect(resolveAppFilePath(app, "./src/main.tsx")).toBe("src/main.tsx");
159
- expect(resolveAppFilePath(app, "./dist/bundle.js")).toBe("dist/bundle.js");
160
- expect(resolveAppFilePath(app, "./records/data.json")).toBe(
161
- "records/data.json",
162
- );
163
- });
164
- });
165
-
166
- // ---------------------------------------------------------------------------
167
- // executeAppFileWrite
168
- // ---------------------------------------------------------------------------
169
-
170
- describe("executeAppFileWrite", () => {
171
- test("resolves plain path to src/ for multifile app", () => {
172
- const files: Record<string, string> = {};
173
- const app = makeMultifileApp();
174
- const store = mockStore(app, files);
175
-
176
- const result = executeAppFileWrite(
177
- { app_id: app.id, path: "main.tsx", content: "export default 1;" },
178
- store,
179
- );
180
-
181
- expect(result.isError).toBe(false);
182
- expect(JSON.parse(result.content).path).toBe("src/main.tsx");
183
- expect(files["src/main.tsx"]).toBe("export default 1;");
184
- });
185
-
186
- test("passes through src/ path unchanged for multifile app", () => {
187
- const files: Record<string, string> = {};
188
- const app = makeMultifileApp();
189
- const store = mockStore(app, files);
190
-
191
- const result = executeAppFileWrite(
192
- { app_id: app.id, path: "src/main.tsx", content: "export default 2;" },
193
- store,
194
- );
195
-
196
- expect(result.isError).toBe(false);
197
- expect(JSON.parse(result.content).path).toBe("src/main.tsx");
198
- expect(files["src/main.tsx"]).toBe("export default 2;");
199
- });
200
-
201
- test("does not modify path for legacy app", () => {
202
- const files: Record<string, string> = {};
203
- const app = makeLegacyApp();
204
- const store = mockStore(app, files);
205
-
206
- const result = executeAppFileWrite(
207
- { app_id: app.id, path: "index.html", content: "<html></html>" },
208
- store,
209
- );
210
-
211
- expect(result.isError).toBe(false);
212
- expect(JSON.parse(result.content).path).toBe("index.html");
213
- expect(files["index.html"]).toBe("<html></html>");
214
- });
215
- });
216
-
217
- // ---------------------------------------------------------------------------
218
- // executeAppFileRead
219
- // ---------------------------------------------------------------------------
220
-
221
- describe("executeAppFileRead", () => {
222
- test("resolves plain path to src/ for multifile app", () => {
223
- const app = makeMultifileApp();
224
- const store = mockStore(app, { "src/main.tsx": "line1\nline2" });
225
-
226
- const result = executeAppFileRead(
227
- { app_id: app.id, path: "main.tsx" },
228
- store,
229
- );
230
-
231
- expect(result.isError).toBe(false);
232
- expect(result.content).toContain("line1");
233
- });
234
-
235
- test("can read dist/ files explicitly for multifile app", () => {
236
- const app = makeMultifileApp();
237
- const store = mockStore(app, { "dist/bundle.js": "bundled code" });
238
-
239
- const result = executeAppFileRead(
240
- { app_id: app.id, path: "dist/bundle.js" },
241
- store,
242
- );
243
-
244
- expect(result.isError).toBe(false);
245
- expect(result.content).toContain("bundled code");
246
- });
247
-
248
- test("does not modify path for legacy app", () => {
249
- const app = makeLegacyApp();
250
- const store = mockStore(app, { "index.html": "<html>hello</html>" });
251
-
252
- const result = executeAppFileRead(
253
- { app_id: app.id, path: "index.html" },
254
- store,
255
- );
256
-
257
- expect(result.isError).toBe(false);
258
- expect(result.content).toContain("<html>hello</html>");
259
- });
260
- });
261
-
262
- // ---------------------------------------------------------------------------
263
- // executeAppFileEdit
264
- // ---------------------------------------------------------------------------
265
-
266
- describe("executeAppFileEdit", () => {
267
- test("resolves plain path to src/ for multifile app", () => {
268
- const files = { "src/main.tsx": "const x = 1;" };
269
- const app = makeMultifileApp();
270
- const store = mockStore(app, files);
271
-
272
- const result = executeAppFileEdit(
273
- {
274
- app_id: app.id,
275
- path: "main.tsx",
276
- old_string: "const x = 1;",
277
- new_string: "const x = 2;",
278
- },
279
- store,
280
- );
281
-
282
- expect(result.isError).toBe(false);
283
- expect(files["src/main.tsx"]).toBe("const x = 2;");
284
- });
285
-
286
- test("does not modify path for legacy app", () => {
287
- const files = { "index.html": "<p>old</p>" };
288
- const app = makeLegacyApp();
289
- const store = mockStore(app, files);
290
-
291
- const result = executeAppFileEdit(
292
- {
293
- app_id: app.id,
294
- path: "index.html",
295
- old_string: "<p>old</p>",
296
- new_string: "<p>new</p>",
297
- },
298
- store,
299
- );
300
-
301
- expect(result.isError).toBe(false);
302
- expect(files["index.html"]).toBe("<p>new</p>");
303
- });
304
- });
305
-
306
- // ---------------------------------------------------------------------------
307
- // executeAppFileList
308
- // ---------------------------------------------------------------------------
309
-
310
- describe("executeAppFileList", () => {
311
- test("returns clean file paths and separate buildOutput for multifile app", () => {
312
- const app = makeMultifileApp();
313
- const store = mockStore(app, {
314
- "src/main.tsx": "",
315
- "src/components/Header.tsx": "",
316
- "dist/index.html": "",
317
- });
318
-
319
- const result = executeAppFileList({ app_id: app.id }, store);
320
- const parsed = JSON.parse(result.content) as {
321
- files: string[];
322
- buildOutput: string[];
323
- };
324
-
325
- // File paths must be clean — no annotations appended
326
- expect(parsed.files).toContain("src/main.tsx");
327
- expect(parsed.files).toContain("src/components/Header.tsx");
328
- expect(parsed.files).toContain("dist/index.html");
329
- expect(
330
- parsed.files.every((f: string) => !f.includes("[build output]")),
331
- ).toBe(true);
332
-
333
- // Build output files listed separately
334
- expect(parsed.buildOutput).toEqual(["dist/index.html"]);
335
- });
336
-
337
- test("does not annotate files for legacy app", () => {
338
- const app = makeLegacyApp();
339
- const store = mockStore(app, {
340
- "index.html": "",
341
- "styles.css": "",
342
- });
343
-
344
- const result = executeAppFileList({ app_id: app.id }, store);
345
- const parsed = JSON.parse(result.content) as string[];
346
-
347
- expect(parsed).toContain("index.html");
348
- expect(parsed).toContain("styles.css");
349
- expect(parsed.every((f: string) => !f.includes("[build output]"))).toBe(
350
- true,
351
- );
352
- });
353
- });
354
-
355
65
  // ---------------------------------------------------------------------------
356
66
  // executeAppCreate
357
67
  // ---------------------------------------------------------------------------
@@ -14,7 +14,7 @@ mock.module("../util/platform.js", () => ({
14
14
  }));
15
15
 
16
16
  // Re-import after mocking so modules use our temp dir
17
- const { createApp, updateApp, getAppsDir } =
17
+ const { createApp, updateApp, getAppDirPath } =
18
18
  await import("../memory/app-store.js");
19
19
  const {
20
20
  getAppHistory,
@@ -147,7 +147,7 @@ describe("App Git History", () => {
147
147
 
148
148
  // Current file should show new content
149
149
  const currentContent = readFileSync(
150
- join(getAppsDir(), app.id, "index.html"),
150
+ join(getAppDirPath(app.id), "index.html"),
151
151
  "utf-8",
152
152
  );
153
153
  expect(currentContent).toContain("version two");
@@ -169,7 +169,7 @@ describe("App Git History", () => {
169
169
 
170
170
  // Verify current content is "new content"
171
171
  let current = readFileSync(
172
- join(getAppsDir(), app.id, "index.html"),
172
+ join(getAppDirPath(app.id), "index.html"),
173
173
  "utf-8",
174
174
  );
175
175
  expect(current).toContain("new content");
@@ -178,7 +178,7 @@ describe("App Git History", () => {
178
178
  await restoreAppVersion(app.id, originalHash);
179
179
 
180
180
  // Verify content is restored
181
- current = readFileSync(join(getAppsDir(), app.id, "index.html"), "utf-8");
181
+ current = readFileSync(join(getAppDirPath(app.id), "index.html"), "utf-8");
182
182
  expect(current).toContain("original content");
183
183
 
184
184
  // Verify a restore commit was created
@@ -37,6 +37,7 @@ const apps = new Map<string, typeof legacyApp | typeof multifileApp>([
37
37
  mock.module("../memory/app-store.js", () => ({
38
38
  getApp: (id: string) => apps.get(id) ?? null,
39
39
  getAppsDir: () => "/fake/apps",
40
+ getAppDirPath: (appId: string) => `/fake/apps/${appId}`,
40
41
  isMultifileApp: (app: Record<string, unknown>) => app.formatVersion === 2,
41
42
  }));
42
43