@allbluecn/web-app 0.10.2 → 0.11.0

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 (169) hide show
  1. package/package.json +26 -11
  2. package/public/favicon.ico +0 -0
  3. package/public/logo/allblue-logo-app-icon.svg +7 -0
  4. package/public/logo/allblue-logo-dark-square.svg +4 -0
  5. package/public/logo/allblue-logo-dark.svg +4 -0
  6. package/public/logo/allblue-logo-light-square.svg +4 -0
  7. package/public/logo/allblue-logo-light.svg +4 -0
  8. package/src/__tests__/bench/baselines/format-timestamp.json +44 -0
  9. package/src/__tests__/bench/core-hot-path.bench.ts +71 -0
  10. package/src/__tests__/integration/db-crud.integration.test.ts +79 -0
  11. package/src/__tests__/rejects-tothrow-regression.test.ts +31 -0
  12. package/src/__tests__/security/ai/prompt-injection.test.ts +105 -0
  13. package/src/__tests__/security/ssrf-url-whitelist.test.ts +54 -0
  14. package/src/__tests__/security/xss-sanitize.test.ts +99 -0
  15. package/src/__tests__/setup.ts +9 -0
  16. package/src/__tests__/visual/__screenshots__/theme-surface.vrt.test.tsx/theme-surface-dark-baseline-1-chromium-darwin.png +0 -0
  17. package/src/__tests__/visual/__screenshots__/theme-surface.vrt.test.tsx/theme-surface-light-baseline-1-chromium-darwin.png +0 -0
  18. package/src/__tests__/visual/__screenshots__/user-avatar.vrt.test.tsx/user-avatar-initial-fallback-baseline-1-chromium-darwin.png +0 -0
  19. package/src/__tests__/visual/theme-surface.vrt.test.tsx +54 -0
  20. package/src/__tests__/visual/user-avatar.vrt.test.tsx +32 -0
  21. package/src/components/error/global-error-boundary.tsx +2 -2
  22. package/src/components/knowledge/general-groups.ts +15 -0
  23. package/src/components/knowledge/kb-filter-popover.tsx +143 -0
  24. package/src/components/knowledge-public/legal-compliance-editor.tsx +4 -2
  25. package/src/components/layout/sidebar-03/__tests__/nav-workspace.test.tsx +9 -6
  26. package/src/components/layout/sidebar-03/nav-knowledge.tsx +19 -17
  27. package/src/components/layout/sidebar-03/nav-secondary.tsx +57 -47
  28. package/src/components/layout/sidebar-03/nav-workspace.tsx +20 -22
  29. package/src/components/mdx.tsx +9 -0
  30. package/src/components/shared-kit/avatars/notion-avatar.tsx +2 -1
  31. package/src/components/shared-kit/icons/__tests__/lucide-curated-icons.test.ts +2 -2
  32. package/src/components/shared-kit/icons/__tests__/lucide-icon-picker.test.tsx +3 -2
  33. package/src/components/shared-kit/icons/lucide-icon-picker/curated-icons.ts +5 -10
  34. package/src/components/shared-kit/icons/lucide-icon-picker/lucide-icon-by-name.tsx +3 -3
  35. package/src/components/shared-kit/theme/theme-surface.tsx +52 -0
  36. package/src/components/story/relations/general-groups.ts +1 -15
  37. package/src/components/story/relations/knowledge-select-dialog.tsx +13 -87
  38. package/src/components/story/story-list-view.tsx +7 -1
  39. package/src/components/tiptap/node/table-node/ui/table-move-row-column-button/use-table-move-row-column.ts +0 -2
  40. package/src/components/workspace/__tests__/activity/workspace-activity-card.test.tsx +36 -0
  41. package/src/components/workspace/__tests__/contribution/workspace-contribution-card.test.tsx +17 -0
  42. package/src/components/workspace/__tests__/dialogs/create-workspace-dialog.test.tsx +63 -0
  43. package/src/components/workspace/__tests__/dialogs/delete-workspace-options.test.tsx +89 -0
  44. package/src/components/workspace/__tests__/dialogs/edit-workspace-dialog.test.tsx +68 -0
  45. package/src/components/workspace/__tests__/header/workspace-header.test.tsx +40 -0
  46. package/src/components/workspace/__tests__/kb/workspace-kb-card.test.tsx +154 -0
  47. package/src/components/workspace/__tests__/members/workspace-member-item.test.tsx +24 -0
  48. package/src/components/workspace/__tests__/members/workspace-members-manager.test.tsx +51 -0
  49. package/src/components/workspace/__tests__/projects/workspace-project-list.test.tsx +46 -0
  50. package/src/components/workspace/{create-workspace-dialog.tsx → dialogs/create-workspace-dialog.tsx} +1 -1
  51. package/src/components/workspace/{edit-workspace-dialog.tsx → dialogs/edit-workspace-dialog.tsx} +1 -1
  52. package/src/components/workspace/kb/workspace-kb-card.tsx +594 -0
  53. package/src/components/workspace/kb/workspace-kb-manage-dialog.tsx +174 -0
  54. package/src/components/workspace/kb/workspace-quick-doc-dialog.tsx +431 -0
  55. package/src/components/workspace/{workspace-members-manager.tsx → members/workspace-members-manager.tsx} +78 -40
  56. package/src/lib/__tests__/msw-sample.test.ts +68 -0
  57. package/src/lib/__tests__/pgvector.test.ts +2 -2
  58. package/src/lib/changelog/sdk-versions.ts +21 -21
  59. package/src/lib/collections/knowledge/knowledge-bases-collection.ts +3 -1
  60. package/src/lib/collections/reference/workspaces-collection.ts +3 -1
  61. package/src/lib/fetch-json.ts +23 -0
  62. package/src/lib/fumadocs-i18n.ts +7 -0
  63. package/src/{components/workspace/workspace-kb-card.tsx → lib/i18n/resolver.ts} +6 -12
  64. package/src/lib/layout.shared.tsx +40 -0
  65. package/src/lib/sanitize.ts +14 -1
  66. package/src/lib/session.ts +9 -1
  67. package/src/lib/source.ts +15 -0
  68. package/src/lib/vector-store/__tests__/factory.test.ts +1 -1
  69. package/src/lib/vector-store/__tests__/sqlite-vec-store.test.ts +2 -2
  70. package/src/plugins/core/tags/__tests__/tags.test.ts +10 -10
  71. package/src/routeTree.gen.ts +65 -0
  72. package/src/routes/$lang/docs/$/route.lazy.tsx +62 -0
  73. package/src/routes/$lang/docs/$/route.tsx +52 -0
  74. package/src/routes/-tests/docs-redirect.test.ts +45 -0
  75. package/src/routes/__root.tsx +13 -7
  76. package/src/routes/__tests__/-edition-routing.test.tsx +4 -4
  77. package/src/routes/__tests__/route-tree-structure.test.ts +65 -0
  78. package/src/routes/_nav/knowledge/compliance/index.lazy.tsx +5 -2
  79. package/src/routes/_nav/knowledge/index.lazy.tsx +1 -1
  80. package/src/routes/_nav/knowledge/laws/index.lazy.tsx +5 -2
  81. package/src/routes/_nav/route.tsx +21 -0
  82. package/src/routes/_nav/workspaces/$workspaceId/route.lazy.tsx +13 -11
  83. package/src/routes/_nav/workspaces/$workspaceId/route.tsx +4 -2
  84. package/src/routes/_nav/workspaces/$workspaceId/settings/route.lazy.tsx +1 -1
  85. package/src/routes/_nav/workspaces/index.lazy.tsx +3 -3
  86. package/src/routes/api/search.ts +13 -0
  87. package/src/routes/docs.tsx +9 -0
  88. package/src/server/__tests__/agent-crud.test.ts +4 -4
  89. package/src/server/__tests__/chat-conversations.test.ts +1 -3
  90. package/src/server/__tests__/fetch-guard.test.ts +99 -0
  91. package/src/server/__tests__/permissions.test.ts +1 -3
  92. package/src/server/__tests__/resources.team-seats.test.ts +2 -0
  93. package/src/server/__tests__/skill-crud.test.ts +3 -3
  94. package/src/server/ai-gateway/__tests__/ai-output-guard.test.ts +95 -0
  95. package/src/server/ai-gateway/__tests__/kb-context-guard.test.ts +136 -0
  96. package/src/server/ai-gateway/__tests__/knowledge-search-fence.test.ts +240 -0
  97. package/src/server/ai-gateway/__tests__/llm-fence.test.ts +125 -0
  98. package/src/server/ai-gateway/__tests__/run-store.test.ts +52 -0
  99. package/src/server/ai-gateway/ai-output-guard.ts +77 -0
  100. package/src/server/ai-gateway/index.ts +5 -0
  101. package/src/server/ai-gateway/kb-context-guard.ts +46 -0
  102. package/src/server/ai-gateway/llm-fence.ts +82 -0
  103. package/src/server/ai-gateway/persistence/run-store.ts +19 -0
  104. package/src/server/ai-gateway/story-prompt.ts +4 -2
  105. package/src/server/ai-gateway/tools/definitions.ts +12 -6
  106. package/src/server/ai-gateway/tools/server.ts +97 -54
  107. package/src/server/builtin-project.ts +21 -0
  108. package/src/server/email/sender.ts +8 -2
  109. package/src/server/fetch-guard.ts +170 -0
  110. package/src/server/middlewares/security-headers.ts +113 -0
  111. package/src/server/serverFns/__tests__/billing.test.ts +263 -0
  112. package/src/server/serverFns/__tests__/link-preview.test.ts +239 -0
  113. package/src/server/serverFns/__tests__/project-kb.test.ts +3 -3
  114. package/src/server/serverFns/__tests__/project-team-assign.test.ts +1 -1
  115. package/src/server/serverFns/__tests__/story-comments.test.ts +265 -0
  116. package/src/server/serverFns/__tests__/story-crud-archive-detail.test.ts +266 -0
  117. package/src/server/serverFns/__tests__/story-crud-labels-users.test.ts +227 -0
  118. package/src/server/serverFns/__tests__/story-crud.test.ts +526 -0
  119. package/src/server/serverFns/__tests__/story-links.test.ts +262 -0
  120. package/src/server/serverFns/__tests__/story-list-query.test.ts +259 -0
  121. package/src/server/serverFns/__tests__/story-reactions.test.ts +230 -0
  122. package/src/server/serverFns/__tests__/story-tree.test.ts +317 -0
  123. package/src/server/serverFns/__tests__/story-types.test.ts +294 -0
  124. package/src/server/serverFns/__tests__/team-accept.test.ts +6 -6
  125. package/src/server/serverFns/__tests__/team-invitation.test.ts +3 -3
  126. package/src/server/serverFns/__tests__/team-join-request.test.ts +8 -8
  127. package/src/server/serverFns/__tests__/update-seats.test.ts +4 -4
  128. package/src/server/serverFns/__tests__/workspace-kb-idor.test.ts +160 -0
  129. package/src/server/serverFns/__tests__/workspace-kb-utils.test.ts +54 -0
  130. package/src/server/serverFns/__tests__/workspace-kb.test.ts +348 -0
  131. package/src/server/serverFns/__tests__/workspace.test.ts +28 -8
  132. package/src/server/serverFns/iteration/__tests__/iteration-crud.test.ts +3 -5
  133. package/src/server/serverFns/iteration/__tests__/iteration-stories.test.ts +1 -1
  134. package/src/server/serverFns/iteration/__tests__/iteration-transfer.test.ts +2 -2
  135. package/src/server/serverFns/kb-doc-search.ts +111 -0
  136. package/src/server/serverFns/knowledge-public/compliance/create.ts +2 -1
  137. package/src/server/serverFns/knowledge-public/legal/create.ts +2 -1
  138. package/src/server/serverFns/story/__tests__/bulk.test.ts +1 -1
  139. package/src/server/serverFns/story/__tests__/label-suggest-fn.test.ts +2 -6
  140. package/src/server/serverFns/story/__tests__/story-assoc.test.ts +2 -4
  141. package/src/server/serverFns/story/__tests__/story-dependencies.test.ts +2 -2
  142. package/src/server/serverFns/story/__tests__/story-groups.test.ts +2 -2
  143. package/src/server/serverFns/story/kb-general-consts.ts +39 -0
  144. package/src/server/serverFns/story/kb-general.ts +30 -17
  145. package/src/server/serverFns/story/story-assoc.ts +13 -103
  146. package/src/server/serverFns/story/story-crud.ts +2 -5
  147. package/src/server/serverFns/story/story-groups.ts +4 -13
  148. package/src/server/serverFns/team.ts +1 -1
  149. package/src/server/serverFns/workspace-kb-utils.ts +138 -0
  150. package/src/server/serverFns/workspace-kb.ts +403 -0
  151. package/src/server/serverFns/workspace.ts +4 -1
  152. package/src/shared/ai-cli/__tests__/adapter.test.ts +67 -0
  153. package/src/shared/ai-cli/__tests__/cli-detector.test.ts +99 -0
  154. package/src/shared/ai-cli/adapter.ts +2 -2
  155. package/src/shared/ai-cli/cli-detector.ts +18 -9
  156. package/src/start.ts +2 -1
  157. package/src/styles/globals.css +116 -0
  158. package/src/types/jest-dom-vitest.d.ts +8 -7
  159. package/vite.shared.ts +18 -1
  160. package/public/file.svg +0 -1
  161. package/public/globe.svg +0 -1
  162. package/public/window.svg +0 -1
  163. package/src/lib/__tests__/cli-detector.test.ts +0 -80
  164. /package/src/components/workspace/{workspace-activity-card.tsx → activity/workspace-activity-card.tsx} +0 -0
  165. /package/src/components/workspace/{workspace-contribution-card.tsx → contribution/workspace-contribution-card.tsx} +0 -0
  166. /package/src/components/workspace/{delete-workspace-options.tsx → dialogs/delete-workspace-options.tsx} +0 -0
  167. /package/src/components/workspace/{workspace-header.tsx → header/workspace-header.tsx} +0 -0
  168. /package/src/components/workspace/{workspace-member-item.tsx → members/workspace-member-item.tsx} +0 -0
  169. /package/src/components/workspace/{workspace-project-list.tsx → projects/workspace-project-list.tsx} +0 -0
@@ -140,7 +140,7 @@ describe('listTagsFn', () => {
140
140
  it('未登录时抛 UnauthorizedError', async () => {
141
141
  mocks.getCurrentSession.mockResolvedValue(null)
142
142
 
143
- await expect(listTagsFn()).rejects.toMatchObject({ message: expect.stringMatching(/UNAUTHORIZED/) })
143
+ await expect(listTagsFn()).rejects.toThrow(/UNAUTHORIZED/)
144
144
  })
145
145
  })
146
146
 
@@ -194,7 +194,7 @@ describe('createTagFn', () => {
194
194
  it('未登录时抛 UnauthorizedError', async () => {
195
195
  mocks.getCurrentSession.mockResolvedValue(null)
196
196
 
197
- await expect(createTagFn({ data: { name: 'AI' } })).rejects.toMatchObject({ message: expect.stringMatching(/UNAUTHORIZED/) })
197
+ await expect(createTagFn({ data: { name: 'AI' } })).rejects.toThrow(/UNAUTHORIZED/)
198
198
  })
199
199
  })
200
200
 
@@ -227,7 +227,7 @@ describe('updateTagFn', () => {
227
227
  mocks.getCurrentSession.mockResolvedValue({ user: { id: 'user-1' } })
228
228
  mocks.findFirst.mockResolvedValue(null)
229
229
 
230
- await expect(updateTagFn({ data: { id: 't-other', name: 'X' } })).rejects.toMatchObject({ message: expect.stringMatching(/FORBIDDEN/) })
230
+ await expect(updateTagFn({ data: { id: 't-other', name: 'X' } })).rejects.toThrow(/FORBIDDEN/)
231
231
  expect(mocks.update).not.toHaveBeenCalled()
232
232
  })
233
233
 
@@ -237,13 +237,13 @@ describe('updateTagFn', () => {
237
237
  .mockResolvedValueOnce({ id: 't1', name: 'AI', userId: 'user-1' })
238
238
  .mockResolvedValueOnce({ id: 't2', name: 'AI-New', userId: 'user-1' })
239
239
 
240
- await expect(updateTagFn({ data: { id: 't1', name: 'AI-New' } })).rejects.toMatchObject({ message: expect.stringMatching(/TAG_NAME_CONFLICT/) })
240
+ await expect(updateTagFn({ data: { id: 't1', name: 'AI-New' } })).rejects.toThrow(/TAG_NAME_CONFLICT/)
241
241
  })
242
242
 
243
243
  it('未登录时抛 UnauthorizedError', async () => {
244
244
  mocks.getCurrentSession.mockResolvedValue(null)
245
245
 
246
- await expect(updateTagFn({ data: { id: 't1', name: 'X' } })).rejects.toMatchObject({ message: expect.stringMatching(/UNAUTHORIZED/) })
246
+ await expect(updateTagFn({ data: { id: 't1', name: 'X' } })).rejects.toThrow(/UNAUTHORIZED/)
247
247
  })
248
248
  })
249
249
 
@@ -258,7 +258,7 @@ describe('deleteTagFn', () => {
258
258
  mocks.getCurrentSession.mockResolvedValue({ user: { id: 'user-1' } })
259
259
  mocks.findFirst.mockResolvedValue(null)
260
260
 
261
- await expect(deleteTagFn({ data: { id: 't-other' } })).rejects.toMatchObject({ message: expect.stringMatching(/FORBIDDEN/) })
261
+ await expect(deleteTagFn({ data: { id: 't-other' } })).rejects.toThrow(/FORBIDDEN/)
262
262
  expect(mocks.update).not.toHaveBeenCalled()
263
263
  })
264
264
 
@@ -279,7 +279,7 @@ describe('deleteTagFn', () => {
279
279
  it('未登录时抛 UnauthorizedError', async () => {
280
280
  mocks.getCurrentSession.mockResolvedValue(null)
281
281
 
282
- await expect(deleteTagFn({ data: { id: 't1' } })).rejects.toMatchObject({ message: expect.stringMatching(/UNAUTHORIZED/) })
282
+ await expect(deleteTagFn({ data: { id: 't1' } })).rejects.toThrow(/UNAUTHORIZED/)
283
283
  })
284
284
  })
285
285
 
@@ -329,7 +329,7 @@ describe('reorderTagsFn', () => {
329
329
  it('未登录时抛 UnauthorizedError', async () => {
330
330
  mocks.getCurrentSession.mockResolvedValue(null)
331
331
 
332
- await expect(reorderTagsFn({ data: { orderedIds: ['t1'] } })).rejects.toMatchObject({ message: expect.stringMatching(/UNAUTHORIZED/) })
332
+ await expect(reorderTagsFn({ data: { orderedIds: ['t1'] } })).rejects.toThrow(/UNAUTHORIZED/)
333
333
  })
334
334
  })
335
335
 
@@ -342,7 +342,7 @@ describe('assignTagsToPrdFn', () => {
342
342
  mocks.getCurrentSession.mockResolvedValue({ user: { id: 'user-1' } })
343
343
  mocks.prdFindFirst.mockResolvedValue(null)
344
344
 
345
- await expect(assignTagsToPrdFn({ data: { prdId: 'p1', tagIds: ['t1'] } })).rejects.toMatchObject({ message: expect.stringMatching(/FORBIDDEN/) })
345
+ await expect(assignTagsToPrdFn({ data: { prdId: 'p1', tagIds: ['t1'] } })).rejects.toThrow(/FORBIDDEN/)
346
346
  })
347
347
 
348
348
  it('校验 tagId 不属于自己时抛 FORBIDDEN', async () => {
@@ -350,7 +350,7 @@ describe('assignTagsToPrdFn', () => {
350
350
  mocks.prdFindFirst.mockResolvedValue({ id: 'p1' })
351
351
  mocks.findFirst.mockResolvedValue(null)
352
352
 
353
- await expect(assignTagsToPrdFn({ data: { prdId: 'p1', tagIds: ['t-other'] } })).rejects.toMatchObject({ message: expect.stringMatching(/FORBIDDEN/) })
353
+ await expect(assignTagsToPrdFn({ data: { prdId: 'p1', tagIds: ['t-other'] } })).rejects.toThrow(/FORBIDDEN/)
354
354
  })
355
355
 
356
356
  it('事务内 diff 增删', async () => {
@@ -14,6 +14,7 @@ import { Route as rootRouteImport } from './routes/__root'
14
14
  import { Route as LoginRouteRouteImport } from './routes/_login/route'
15
15
  import { Route as NavRouteRouteImport } from './routes/_nav/route'
16
16
  import { Route as PublicRouteRouteImport } from './routes/_public/route'
17
+ import { Route as DocsRouteImport } from './routes/docs'
17
18
  import { Route as ShareRouteRouteImport } from './routes/share/route'
18
19
  import { Route as WorkspaceRouteImport } from './routes/workspace'
19
20
  import { Route as _desktopOnboardingRouteRouteImport } from './routes/__desktop/onboarding/route'
@@ -46,11 +47,13 @@ import { Route as PublicPricingRouteRouteImport } from './routes/_public/pricing
46
47
  import { Route as ApiBillingWebhookRouteImport } from './routes/api/billing-webhook'
47
48
  import { Route as ApiChatRouteImport } from './routes/api/chat'
48
49
  import { Route as ApiHealthRouteImport } from './routes/api/health'
50
+ import { Route as ApiSearchRouteImport } from './routes/api/search'
49
51
  import { Route as ApiStoryAiRouteImport } from './routes/api/story-ai'
50
52
  import { Route as InviteTokenRouteRouteImport } from './routes/invite/$token/route'
51
53
  import { Route as ServerAiStreamRouteImport } from './routes/server.ai-stream'
52
54
  import { Route as ServerWsRouteImport } from './routes/server.ws'
53
55
  import { Route as ShareVersionIdRouteRouteImport } from './routes/share/$versionId/route'
56
+ import { Route as LangDocsSplatRouteRouteImport } from './routes/$lang/docs/$/route'
54
57
  import { Route as NavAppsAnyhubRouteRouteImport } from './routes/_nav/apps/anyhub/route'
55
58
  import { Route as NavChatThreadIdRouteImport } from './routes/_nav/chat/$threadId'
56
59
  import { Route as NavDesignIndexRouteImport } from './routes/_nav/design/index'
@@ -160,6 +163,11 @@ const PublicRouteRoute = PublicRouteRouteImport.update({
160
163
  id: '/_public',
161
164
  getParentRoute: () => rootRouteImport,
162
165
  } as any)
166
+ const DocsRoute = DocsRouteImport.update({
167
+ id: '/docs',
168
+ path: '/docs',
169
+ getParentRoute: () => rootRouteImport,
170
+ } as any)
163
171
  const ShareRouteRoute = ShareRouteRouteImport.update({
164
172
  id: '/share',
165
173
  path: '/share',
@@ -346,6 +354,11 @@ const ApiHealthRoute = ApiHealthRouteImport.update({
346
354
  path: '/api/health',
347
355
  getParentRoute: () => rootRouteImport,
348
356
  } as any)
357
+ const ApiSearchRoute = ApiSearchRouteImport.update({
358
+ id: '/api/search',
359
+ path: '/api/search',
360
+ getParentRoute: () => rootRouteImport,
361
+ } as any)
349
362
  const ApiStoryAiRoute = ApiStoryAiRouteImport.update({
350
363
  id: '/api/story-ai',
351
364
  path: '/api/story-ai',
@@ -375,6 +388,13 @@ const ShareVersionIdRouteRoute = ShareVersionIdRouteRouteImport.update({
375
388
  } as any).lazy(() =>
376
389
  import('./routes/share/$versionId/route.lazy').then((d) => d.Route),
377
390
  )
391
+ const LangDocsSplatRouteRoute = LangDocsSplatRouteRouteImport.update({
392
+ id: '/$lang/docs/$',
393
+ path: '/$lang/docs/$',
394
+ getParentRoute: () => rootRouteImport,
395
+ } as any).lazy(() =>
396
+ import('./routes/$lang/docs/$/route.lazy').then((d) => d.Route),
397
+ )
378
398
  const NavAppsAnyhubRouteRoute = NavAppsAnyhubRouteRouteImport.update({
379
399
  id: '/anyhub',
380
400
  path: '/anyhub',
@@ -878,6 +898,7 @@ const ApiSharePrdPrdIdEventsRoute = ApiSharePrdPrdIdEventsRouteImport.update({
878
898
  export interface FileRoutesByFullPath {
879
899
  '/': typeof PublicIndexRouteRoute
880
900
  '/share': typeof ShareRouteRouteWithChildren
901
+ '/docs': typeof DocsRoute
881
902
  '/workspace': typeof WorkspaceRoute
882
903
  '/onboarding': typeof _desktopOnboardingRouteRoute
883
904
  '/forgot-password': typeof LoginForgotPasswordRouteRoute
@@ -910,9 +931,11 @@ export interface FileRoutesByFullPath {
910
931
  '/api/billing-webhook': typeof ApiBillingWebhookRoute
911
932
  '/api/chat': typeof ApiChatRoute
912
933
  '/api/health': typeof ApiHealthRoute
934
+ '/api/search': typeof ApiSearchRoute
913
935
  '/api/story-ai': typeof ApiStoryAiRoute
914
936
  '/server/ai-stream': typeof ServerAiStreamRoute
915
937
  '/server/ws': typeof ServerWsRoute
938
+ '/$lang/docs/$': typeof LangDocsSplatRouteRoute
916
939
  '/apps/anyhub': typeof NavAppsAnyhubRouteRouteWithChildren
917
940
  '/design/$projectId': typeof NavDesignProjectIdRouteRouteWithChildren
918
941
  '/knowledge/$kbId': typeof NavKnowledgeKbIdRouteRouteWithChildren
@@ -987,6 +1010,7 @@ export interface FileRoutesByFullPath {
987
1010
  export interface FileRoutesByTo {
988
1011
  '/': typeof PublicIndexRouteRoute
989
1012
  '/share': typeof ShareRouteRouteWithChildren
1013
+ '/docs': typeof DocsRoute
990
1014
  '/workspace': typeof WorkspaceRoute
991
1015
  '/onboarding': typeof _desktopOnboardingRouteRoute
992
1016
  '/forgot-password': typeof LoginForgotPasswordRouteRoute
@@ -1013,9 +1037,11 @@ export interface FileRoutesByTo {
1013
1037
  '/api/billing-webhook': typeof ApiBillingWebhookRoute
1014
1038
  '/api/chat': typeof ApiChatRoute
1015
1039
  '/api/health': typeof ApiHealthRoute
1040
+ '/api/search': typeof ApiSearchRoute
1016
1041
  '/api/story-ai': typeof ApiStoryAiRoute
1017
1042
  '/server/ai-stream': typeof ServerAiStreamRoute
1018
1043
  '/server/ws': typeof ServerWsRoute
1044
+ '/$lang/docs/$': typeof LangDocsSplatRouteRoute
1019
1045
  '/apps/anyhub': typeof NavAppsAnyhubRouteRouteWithChildren
1020
1046
  '/design/$projectId': typeof NavDesignProjectIdRouteRouteWithChildren
1021
1047
  '/prd/$id': typeof NavPrdIdRouteRoute
@@ -1085,6 +1111,7 @@ export interface FileRoutesById {
1085
1111
  '/_nav': typeof NavRouteRouteWithChildren
1086
1112
  '/_public': typeof PublicRouteRouteWithChildren
1087
1113
  '/share': typeof ShareRouteRouteWithChildren
1114
+ '/docs': typeof DocsRoute
1088
1115
  '/workspace': typeof WorkspaceRoute
1089
1116
  '/_public/': typeof PublicIndexRouteRoute
1090
1117
  '/__desktop/onboarding': typeof _desktopOnboardingRouteRoute
@@ -1118,9 +1145,11 @@ export interface FileRoutesById {
1118
1145
  '/api/billing-webhook': typeof ApiBillingWebhookRoute
1119
1146
  '/api/chat': typeof ApiChatRoute
1120
1147
  '/api/health': typeof ApiHealthRoute
1148
+ '/api/search': typeof ApiSearchRoute
1121
1149
  '/api/story-ai': typeof ApiStoryAiRoute
1122
1150
  '/server/ai-stream': typeof ServerAiStreamRoute
1123
1151
  '/server/ws': typeof ServerWsRoute
1152
+ '/$lang/docs/$': typeof LangDocsSplatRouteRoute
1124
1153
  '/_nav/apps/anyhub': typeof NavAppsAnyhubRouteRouteWithChildren
1125
1154
  '/_nav/design/$projectId': typeof NavDesignProjectIdRouteRouteWithChildren
1126
1155
  '/_nav/knowledge/$kbId': typeof NavKnowledgeKbIdRouteRouteWithChildren
@@ -1197,6 +1226,7 @@ export interface FileRouteTypes {
1197
1226
  fullPaths:
1198
1227
  | '/'
1199
1228
  | '/share'
1229
+ | '/docs'
1200
1230
  | '/workspace'
1201
1231
  | '/onboarding'
1202
1232
  | '/forgot-password'
@@ -1229,9 +1259,11 @@ export interface FileRouteTypes {
1229
1259
  | '/api/billing-webhook'
1230
1260
  | '/api/chat'
1231
1261
  | '/api/health'
1262
+ | '/api/search'
1232
1263
  | '/api/story-ai'
1233
1264
  | '/server/ai-stream'
1234
1265
  | '/server/ws'
1266
+ | '/$lang/docs/$'
1235
1267
  | '/apps/anyhub'
1236
1268
  | '/design/$projectId'
1237
1269
  | '/knowledge/$kbId'
@@ -1306,6 +1338,7 @@ export interface FileRouteTypes {
1306
1338
  to:
1307
1339
  | '/'
1308
1340
  | '/share'
1341
+ | '/docs'
1309
1342
  | '/workspace'
1310
1343
  | '/onboarding'
1311
1344
  | '/forgot-password'
@@ -1332,9 +1365,11 @@ export interface FileRouteTypes {
1332
1365
  | '/api/billing-webhook'
1333
1366
  | '/api/chat'
1334
1367
  | '/api/health'
1368
+ | '/api/search'
1335
1369
  | '/api/story-ai'
1336
1370
  | '/server/ai-stream'
1337
1371
  | '/server/ws'
1372
+ | '/$lang/docs/$'
1338
1373
  | '/apps/anyhub'
1339
1374
  | '/design/$projectId'
1340
1375
  | '/prd/$id'
@@ -1403,6 +1438,7 @@ export interface FileRouteTypes {
1403
1438
  | '/_nav'
1404
1439
  | '/_public'
1405
1440
  | '/share'
1441
+ | '/docs'
1406
1442
  | '/workspace'
1407
1443
  | '/_public/'
1408
1444
  | '/__desktop/onboarding'
@@ -1436,9 +1472,11 @@ export interface FileRouteTypes {
1436
1472
  | '/api/billing-webhook'
1437
1473
  | '/api/chat'
1438
1474
  | '/api/health'
1475
+ | '/api/search'
1439
1476
  | '/api/story-ai'
1440
1477
  | '/server/ai-stream'
1441
1478
  | '/server/ws'
1479
+ | '/$lang/docs/$'
1442
1480
  | '/_nav/apps/anyhub'
1443
1481
  | '/_nav/design/$projectId'
1444
1482
  | '/_nav/knowledge/$kbId'
@@ -1516,15 +1554,18 @@ export interface RootRouteChildren {
1516
1554
  NavRouteRoute: typeof NavRouteRouteWithChildren
1517
1555
  PublicRouteRoute: typeof PublicRouteRouteWithChildren
1518
1556
  ShareRouteRoute: typeof ShareRouteRouteWithChildren
1557
+ DocsRoute: typeof DocsRoute
1519
1558
  WorkspaceRoute: typeof WorkspaceRoute
1520
1559
  _desktopOnboardingRouteRoute: typeof _desktopOnboardingRouteRoute
1521
1560
  InviteTokenRouteRoute: typeof InviteTokenRouteRoute
1522
1561
  ApiBillingWebhookRoute: typeof ApiBillingWebhookRoute
1523
1562
  ApiChatRoute: typeof ApiChatRoute
1524
1563
  ApiHealthRoute: typeof ApiHealthRoute
1564
+ ApiSearchRoute: typeof ApiSearchRoute
1525
1565
  ApiStoryAiRoute: typeof ApiStoryAiRoute
1526
1566
  ServerAiStreamRoute: typeof ServerAiStreamRoute
1527
1567
  ServerWsRoute: typeof ServerWsRoute
1568
+ LangDocsSplatRouteRoute: typeof LangDocsSplatRouteRoute
1528
1569
  ApiLicenseActivateRouteRoute: typeof ApiLicenseActivateRouteRoute
1529
1570
  ApiLicenseHeartbeatRouteRoute: typeof ApiLicenseHeartbeatRouteRoute
1530
1571
  ApiAttachmentsUploadRoute: typeof ApiAttachmentsUploadRoute
@@ -1562,6 +1603,13 @@ declare module '@tanstack/react-router' {
1562
1603
  preLoaderRoute: typeof PublicRouteRouteImport
1563
1604
  parentRoute: typeof rootRouteImport
1564
1605
  }
1606
+ '/docs': {
1607
+ id: '/docs'
1608
+ path: '/docs'
1609
+ fullPath: '/docs'
1610
+ preLoaderRoute: typeof DocsRouteImport
1611
+ parentRoute: typeof rootRouteImport
1612
+ }
1565
1613
  '/share': {
1566
1614
  id: '/share'
1567
1615
  path: '/share'
@@ -1786,6 +1834,13 @@ declare module '@tanstack/react-router' {
1786
1834
  preLoaderRoute: typeof ApiHealthRouteImport
1787
1835
  parentRoute: typeof rootRouteImport
1788
1836
  }
1837
+ '/api/search': {
1838
+ id: '/api/search'
1839
+ path: '/api/search'
1840
+ fullPath: '/api/search'
1841
+ preLoaderRoute: typeof ApiSearchRouteImport
1842
+ parentRoute: typeof rootRouteImport
1843
+ }
1789
1844
  '/api/story-ai': {
1790
1845
  id: '/api/story-ai'
1791
1846
  path: '/api/story-ai'
@@ -1821,6 +1876,13 @@ declare module '@tanstack/react-router' {
1821
1876
  preLoaderRoute: typeof ShareVersionIdRouteRouteImport
1822
1877
  parentRoute: typeof ShareRouteRoute
1823
1878
  }
1879
+ '/$lang/docs/$': {
1880
+ id: '/$lang/docs/$'
1881
+ path: '/$lang/docs/$'
1882
+ fullPath: '/$lang/docs/$'
1883
+ preLoaderRoute: typeof LangDocsSplatRouteRouteImport
1884
+ parentRoute: typeof rootRouteImport
1885
+ }
1824
1886
  '/_nav/apps/anyhub': {
1825
1887
  id: '/_nav/apps/anyhub'
1826
1888
  path: '/anyhub'
@@ -2761,15 +2823,18 @@ const rootRouteChildren: RootRouteChildren = {
2761
2823
  NavRouteRoute: NavRouteRouteWithChildren,
2762
2824
  PublicRouteRoute: PublicRouteRouteWithChildren,
2763
2825
  ShareRouteRoute: ShareRouteRouteWithChildren,
2826
+ DocsRoute: DocsRoute,
2764
2827
  WorkspaceRoute: WorkspaceRoute,
2765
2828
  _desktopOnboardingRouteRoute: _desktopOnboardingRouteRoute,
2766
2829
  InviteTokenRouteRoute: InviteTokenRouteRoute,
2767
2830
  ApiBillingWebhookRoute: ApiBillingWebhookRoute,
2768
2831
  ApiChatRoute: ApiChatRoute,
2769
2832
  ApiHealthRoute: ApiHealthRoute,
2833
+ ApiSearchRoute: ApiSearchRoute,
2770
2834
  ApiStoryAiRoute: ApiStoryAiRoute,
2771
2835
  ServerAiStreamRoute: ServerAiStreamRoute,
2772
2836
  ServerWsRoute: ServerWsRoute,
2837
+ LangDocsSplatRouteRoute: LangDocsSplatRouteRoute,
2773
2838
  ApiLicenseActivateRouteRoute: ApiLicenseActivateRouteRoute,
2774
2839
  ApiLicenseHeartbeatRouteRoute: ApiLicenseHeartbeatRouteRoute,
2775
2840
  ApiAttachmentsUploadRoute: ApiAttachmentsUploadRoute,
@@ -0,0 +1,62 @@
1
+ import { createLazyFileRoute, Link } from '@tanstack/react-router'
2
+ import { Suspense, useEffect } from 'react'
3
+ import { DocsLayout } from 'fumadocs-ui/layouts/docs'
4
+ import {
5
+ DocsBody,
6
+ DocsDescription,
7
+ DocsPage,
8
+ DocsTitle,
9
+ } from 'fumadocs-ui/layouts/docs/page'
10
+ import { useFumadocsLoader } from 'fumadocs-core/source/client'
11
+ import { baseOptions, isDocsLang, type DocsLang } from '@/lib/layout.shared'
12
+ import { getMDXComponents } from '@/components/mdx'
13
+ import { docs } from '@/lib/source'
14
+ import { setLocale } from '@/paraglide/runtime.js'
15
+
16
+ export const Route = createLazyFileRoute('/$lang/docs/$')({
17
+ component: DocsRouteComponent,
18
+ })
19
+
20
+ function Content({ path, lang }: { path: string; lang: DocsLang }) {
21
+ const page = docs.getPage(path)
22
+ if (!page) throw new Error(`unknown page: ${path}`)
23
+
24
+ const { toc, body: MdxContent } = page
25
+
26
+ return (
27
+ <DocsPage toc={toc} className="pt-8">
28
+ <Link
29
+ to="/dashboard"
30
+ className="text-fd-muted-foreground hover:text-fd-foreground mb-2 inline-flex items-center gap-1 text-sm"
31
+ >
32
+ ← {lang === 'en' ? 'Back to App' : '返回应用'}
33
+ </Link>
34
+ <DocsTitle>{page.title}</DocsTitle>
35
+ <DocsDescription>{page.description}</DocsDescription>
36
+ <DocsBody>
37
+ <MdxContent components={getMDXComponents()} />
38
+ </DocsBody>
39
+ </DocsPage>
40
+ )
41
+ }
42
+
43
+ function DocsRouteComponent() {
44
+ const { lang: rawLang } = Route.useParams()
45
+ const data = useFumadocsLoader(Route.useLoaderData())
46
+
47
+ useEffect(() => {
48
+ if (isDocsLang(rawLang)) setLocale(rawLang)
49
+ }, [rawLang])
50
+
51
+ if (!isDocsLang(rawLang)) return null
52
+
53
+ const lang: DocsLang = rawLang
54
+
55
+ return (
56
+ <DocsLayout {...baseOptions(lang)} tree={data.pageTree}>
57
+ <Suspense>
58
+ <Content path={data.path} lang={lang} />
59
+ </Suspense>
60
+ </DocsLayout>
61
+ )
62
+ }
@@ -0,0 +1,52 @@
1
+ import { createFileRoute, notFound } from '@tanstack/react-router'
2
+ import { createServerFn } from '@tanstack/react-start'
3
+ import { docs, source } from '@/lib/source'
4
+
5
+ // fumadocs-ui 16.15.8 的 getLayoutTabs 遍历 root/folder 的 children 时
6
+ // 会 `for of child` 递归调用 next,但 page 节点(叶子)无 children 字段导致
7
+ // `node.children is not iterable`。给每个无 children 的节点补 `children: []`。
8
+ function fixPageTreeChildren<T extends { data: unknown }>(tree: T): T {
9
+ const patch = (node: { type?: string; children?: unknown[] }): void => {
10
+ if (!node || typeof node !== 'object') return
11
+ if (Array.isArray(node.children)) {
12
+ for (const child of node.children) patch(child as Parameters<typeof patch>[0])
13
+ } else {
14
+ ;(node as { children: unknown[] }).children = []
15
+ }
16
+ }
17
+ patch(tree.data as { children?: unknown[] })
18
+ return tree
19
+ }
20
+
21
+ const loadDocsPage = createServerFn({ method: 'GET' })
22
+ .validator((data: { slugs: string[]; lang: string }) => data)
23
+ .handler(async ({ data: { slugs, lang } }) => {
24
+ const page = source.getPage(slugs, lang)
25
+ if (!page) return null
26
+
27
+ const pageTree = fixPageTreeChildren(await source.serializePageTree(source.getPageTree(lang)))
28
+ return {
29
+ path: page.path,
30
+ title: page.data.title,
31
+ description: page.data.description,
32
+ pageTree,
33
+ }
34
+ })
35
+
36
+ export const Route = createFileRoute('/$lang/docs/$')({
37
+ loader: async ({ params }) => {
38
+ const slugs = params._splat?.split('/') ?? []
39
+ const data = await loadDocsPage({ data: { slugs, lang: params.lang } })
40
+ if (!data) throw notFound()
41
+ await docs.getPage(data.path)?.preload()
42
+ return data
43
+ },
44
+ head: ({ loaderData }) => ({
45
+ meta: [
46
+ { title: loaderData ? `${loaderData.title} - AllBlue` : 'AllBlue' },
47
+ ...(loaderData?.description
48
+ ? [{ name: 'description', content: loaderData.description }]
49
+ : []),
50
+ ],
51
+ }),
52
+ })
@@ -0,0 +1,45 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest'
2
+
3
+ vi.mock('@/paraglide/runtime.js', () => ({
4
+ getLocale: vi.fn(() => 'zh'),
5
+ }))
6
+
7
+ import { getLocale } from '@/paraglide/runtime.js'
8
+ import { Route } from '../docs'
9
+
10
+ type BeforeLoadFn = (ctx: never) => unknown
11
+ const beforeLoad = Route.options.beforeLoad as unknown as BeforeLoadFn
12
+
13
+ async function catchRedirectTarget(fn: () => unknown): Promise<string> {
14
+ try {
15
+ await fn()
16
+ return ''
17
+ } catch (e) {
18
+ const err = e as {
19
+ href?: string
20
+ options?: { to?: string; params?: Record<string, string> }
21
+ }
22
+ if (err.href) return err.href
23
+ if (err.options?.to) {
24
+ const p = err.options.params ?? {}
25
+ return err.options.to.replace(/\$(\w+)/g, (_, name: string) => p[name] ?? `$${name}`)
26
+ }
27
+ return JSON.stringify(e)
28
+ }
29
+ }
30
+
31
+ describe('/docs redirect', () => {
32
+ beforeEach(() => {
33
+ vi.clearAllMocks()
34
+ })
35
+
36
+ it('zh locale 重定向到 /zh/docs', async () => {
37
+ vi.mocked(getLocale).mockReturnValue('zh')
38
+ await expect(catchRedirectTarget(() => beforeLoad({} as never))).resolves.toContain('/zh/docs')
39
+ })
40
+
41
+ it('en locale 重定向到 /en/docs', async () => {
42
+ vi.mocked(getLocale).mockReturnValue('en')
43
+ await expect(catchRedirectTarget(() => beforeLoad({} as never))).resolves.toContain('/en/docs')
44
+ })
45
+ })
@@ -15,7 +15,7 @@
15
15
  // You should have received a copy of the GNU Affero General Public License
16
16
  // along with this program. If not, see <https://www.gnu.org/licenses/>.
17
17
 
18
- import { Link, Outlet, createRootRouteWithContext, HeadContent, Scripts } from '@tanstack/react-router'
18
+ import { Link, Outlet, createRootRouteWithContext, HeadContent, Scripts, useParams } from '@tanstack/react-router'
19
19
  import { HotkeysProvider } from '@tanstack/react-hotkeys'
20
20
  import { ShortcutsPanel } from '@/components/hotkeys/shortcuts-panel'
21
21
  import { Analytics } from '@vercel/analytics/react'
@@ -23,6 +23,9 @@ import { SpeedInsights } from '@vercel/speed-insights/react'
23
23
  import globalsCss from '@/styles/globals.css?url'
24
24
  import type { RouterContext } from '@/lib/router-context'
25
25
  import { Providers } from '@/providers'
26
+ import { RootProvider } from 'fumadocs-ui/provider/tanstack'
27
+ import { i18nProvider } from 'fumadocs-ui/i18n'
28
+ import { translations } from '@/lib/layout.shared'
26
29
  import { Toaster } from '@/components/ui/sonner'
27
30
  import { DevtoolsPanel } from '@/components/devtools'
28
31
  import { getSessionFn } from '@/server/serverFns/auth/session'
@@ -62,6 +65,7 @@ export const Route = createRootRouteWithContext<RouterContext>()({
62
65
 
63
66
  function RootComponent() {
64
67
  const session = Route.useRouteContext({ select: (c) => c.session }) as Session | null
68
+ const { lang } = useParams({ strict: false }) as { lang?: string | undefined }
65
69
  return (
66
70
  <html lang={getLocale()} suppressHydrationWarning>
67
71
  <head>
@@ -69,12 +73,14 @@ function RootComponent() {
69
73
  </head>
70
74
  <body className="w-full h-full min-h-screen antialiased">
71
75
  <Providers session={session}>
72
- <HotkeysProvider>
73
- <Outlet />
74
- <ShortcutsPanel />
75
- <Toaster position="top-center" />
76
- {import.meta.env.DEV && <DevtoolsPanel />}
77
- </HotkeysProvider>
76
+ <RootProvider i18n={i18nProvider(translations, lang)}>
77
+ <HotkeysProvider>
78
+ <Outlet />
79
+ <ShortcutsPanel />
80
+ <Toaster position="top-center" />
81
+ {import.meta.env.DEV && <DevtoolsPanel />}
82
+ </HotkeysProvider>
83
+ </RootProvider>
78
84
  </Providers>
79
85
  <Scripts />
80
86
  <Analytics />
@@ -67,7 +67,7 @@ describe('_nav beforeLoad', () => {
67
67
  )
68
68
  expect(r?.to).toBe('/login')
69
69
  expect(r?.search?.redirect).toBe('/prd?x=1')
70
- })
70
+ }, 20000)
71
71
 
72
72
  it('SaaS 已登录:放行', async () => {
73
73
  const { Route } = await importNavRoute({ profileId: 'saas', desktop: false })
@@ -76,21 +76,21 @@ describe('_nav beforeLoad', () => {
76
76
  Route.options.beforeLoad!({ context: { session: { user: { id: 'u1' } } }, location } as never),
77
77
  ),
78
78
  ).toBeNull()
79
- })
79
+ }, 20000)
80
80
 
81
81
  it('OSS 未登录:放行(登录后置)', async () => {
82
82
  const { Route } = await importNavRoute({ profileId: 'oss', desktop: false })
83
83
  expect(
84
84
  catchRedirect(() => Route.options.beforeLoad!({ context: { session: null }, location } as never)),
85
85
  ).toBeNull()
86
- })
86
+ }, 20000)
87
87
 
88
88
  it('desktop:无条件放行', async () => {
89
89
  const { Route } = await importNavRoute({ profileId: 'saas', desktop: true })
90
90
  expect(
91
91
  catchRedirect(() => Route.options.beforeLoad!({ context: { session: null }, location } as never)),
92
92
  ).toBeNull()
93
- })
93
+ }, 20000)
94
94
  })
95
95
 
96
96
  describe('GlobalErrorBoundary', () => {
@@ -0,0 +1,65 @@
1
+ // @vitest-environment node
2
+ import { readFileSync } from "node:fs"
3
+ import { resolve } from "node:path"
4
+ import { describe, expect, it } from "vitest"
5
+
6
+ // routeTree.gen.ts 由 TanStack Router 插件生成,是路由结构的单一事实源。
7
+ // 本测试锁定关键路由路径集合:误删路由文件 / 改名 path 未同步调用方时立即红灯。
8
+ // 清单项均为已用 grep 校准的真实生成字面量(单引号 + 完整路径串)。
9
+ const content = readFileSync(
10
+ resolve(import.meta.dirname, "../../routeTree.gen.ts"),
11
+ "utf-8"
12
+ )
13
+
14
+ const MUST_EXIST_PATHS: string[] = [
15
+ // 认证区(_login pathless layout)
16
+ "'/login'",
17
+ "'/register'",
18
+ "'/forgot-password'",
19
+ "'/reset-password'",
20
+ // 导航区(_nav pathless layout,登录后落地页)
21
+ "'/dashboard'",
22
+ "'/workspaces'",
23
+ "'/prd'",
24
+ "'/settings'",
25
+ "'/knowledge'",
26
+ "'/apps'",
27
+ // 公开 / 分享(_public layout + share 公开页)
28
+ "'/share/$versionId'",
29
+ "'/share/prd/$prdId'",
30
+ // API
31
+ "'/api/auth/$'",
32
+ "'/api/health'",
33
+ "'/api/chat'",
34
+ ]
35
+
36
+ const DYNAMIC_PATH_PATTERNS: RegExp[] = [
37
+ /prd\/\$id/,
38
+ /workspaces\/\$workspaceId/,
39
+ /settings\/ai\/\$providerId/,
40
+ ]
41
+
42
+ describe("routeTree 结构完整性", () => {
43
+ it.each(MUST_EXIST_PATHS)("关键路由 %s 存在", (p) => {
44
+ expect(content).toContain(p)
45
+ })
46
+
47
+ for (const re of DYNAMIC_PATH_PATTERNS) {
48
+ it(`动态路由 ${re} 存在`, () => {
49
+ expect(re.test(content)).toBe(true)
50
+ })
51
+ }
52
+
53
+ it("pathless 布局路由 _nav/_login/_public 存在", () => {
54
+ expect(content).toContain("_nav")
55
+ expect(content).toContain("_login")
56
+ expect(content).toContain("_public")
57
+ })
58
+
59
+ // 反向守卫(面向未来):routeTree.gen.ts 由路由插件扫描 src/routes/** 生成,
60
+ // 若测试文件被误放进 routes 目录会被一并收录为路由。当前恒真,防未来回归。
61
+ it("测试文件本身未被路由插件收录(防误扫为路由)", () => {
62
+ expect(content).not.toContain("route-tree-structure")
63
+ expect(content).not.toContain("__tests__")
64
+ })
65
+ })