@allbluecn/web-app 0.19.5 → 0.21.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.
- package/package.json +3 -3
- package/src/components/bug/__tests__/board-subgroups.test.tsx +1 -1
- package/src/components/bug/__tests__/board-view.test.tsx +1 -1
- package/src/components/bug/__tests__/comment-reactions.test.tsx +1 -1
- package/src/components/bug/__tests__/create-bug-dialog.test.tsx +2 -2
- package/src/components/bug/__tests__/filters-compat.test.tsx +1 -1
- package/src/components/bug/__tests__/fix-request-race.test.tsx +1 -1
- package/src/components/bug/__tests__/peek-modes.test.tsx +1 -1
- package/src/components/bug/__tests__/use-paged-bug-groups.test.tsx +1 -1
- package/src/components/bug/__tests__/view-type.test.tsx +1 -1
- package/src/components/bug/bug-list-view.tsx +2 -2
- package/src/components/bug/bug-toolbar.tsx +1 -1
- package/src/components/bug/create-bug-dialog.tsx +3 -3
- package/src/components/bug/detail/__tests__/activity-timeline.test.tsx +1 -1
- package/src/components/bug/detail/__tests__/bug-detail-collab.test.tsx +1 -1
- package/src/components/bug/detail/__tests__/repro-report-card.test.tsx +1 -1
- package/src/components/bug/detail/activity-tabs.tsx +34 -3
- package/src/components/bug/detail/activity-timeline.tsx +1 -1
- package/src/components/bug/detail/bug-detail-body.tsx +3 -3
- package/src/components/bug/detail/comment-input.tsx +1 -1
- package/src/components/bug/detail/comment-item.tsx +1 -1
- package/src/components/bug/detail/comment-reactions.tsx +1 -1
- package/src/components/bug/detail/comment-thread.tsx +1 -1
- package/src/components/bug/detail/props-section.tsx +2 -2
- package/src/components/bug/detail/repro-report-card.tsx +1 -1
- package/src/components/bug/detail/reproduce-steps-view.tsx +1 -1
- package/src/components/bug/display/use-paged-bug-groups.ts +2 -2
- package/src/components/bug/filters/field-configs.tsx +1 -1
- package/src/components/bug/inline-editors/assignee-editor.tsx +1 -1
- package/src/components/bug/peek-panel.tsx +2 -2
- package/src/components/bug/reproduce-steps-editor.tsx +1 -1
- package/src/components/bug/story-bugs-panel.tsx +10 -6
- package/src/components/bug/views/board-view.tsx +2 -2
- package/src/components/bug/views/bug-list/batch-toolbar.tsx +1 -1
- package/src/components/bug/views/bug-list/group.tsx +1 -1
- package/src/components/bug/views/bug-list/index.tsx +1 -1
- package/src/components/bug/views/bug-list/inline-bug-creator.tsx +1 -1
- package/src/components/bug/views/bug-list/row.tsx +2 -2
- package/src/components/bug/views/create-view-dialog.tsx +1 -1
- package/src/components/design/editor/versions/hooks/use-design-versions.ts +11 -4
- package/src/components/design/editor/versions/page-version-panel.tsx +30 -11
- package/src/components/harness/code/workspace-context.tsx +5 -1
- package/src/components/harness/code/workspace-picker.tsx +19 -6
- package/src/components/harness/composer/harness-composer.tsx +25 -3
- package/src/components/harness/composer/mention/mention-resolvers.ts +2 -0
- package/src/components/harness/composer/mention/mention-suggestion-menu.tsx +6 -3
- package/src/components/harness/composer/mention/use-mention-sources.ts +1 -1
- package/src/components/iteration/iteration-progress-bar.tsx +2 -2
- package/src/components/knowledge/public/legal-compliance-editor.tsx +1 -1
- package/src/components/layout/sidebar-03/nav-knowledge.tsx +1 -1
- package/src/components/layout/sidebar-03/nav-workspace.tsx +1 -1
- package/src/components/project/invite-member-dialog.tsx +2 -2
- package/src/components/project/project-member-manager.tsx +0 -3
- package/src/components/shared-kit/list-kit/kanban/__tests__/instance-key.test.ts +1 -1
- package/src/components/story/__tests__/constants.test.ts +1 -1
- package/src/components/story/__tests__/dependency-type-badge.test.ts +1 -1
- package/src/components/story/__tests__/peek-panel-patch-sync.test.tsx +1 -1
- package/src/components/story/__tests__/story-list-view-peek-sync.test.tsx +1 -1
- package/src/components/story/ai/story-ai-panel.tsx +1 -1
- package/src/components/story/ai/story-ai-preview-list.tsx +2 -2
- package/src/components/story/ai/use-story-ai-model.ts +17 -4
- package/src/components/story/columns.tsx +2 -2
- package/src/components/story/create-story-dialog.tsx +4 -4
- package/src/components/story/detail/__tests__/activity-timeline.test.tsx +1 -1
- package/src/components/story/detail/__tests__/associated-attachments-panel.test.tsx +1 -1
- package/src/components/story/detail/__tests__/story-collab.test.tsx +1 -1
- package/src/components/story/detail/activity-tabs.tsx +1 -1
- package/src/components/story/detail/activity-timeline.tsx +36 -17
- package/src/components/story/detail/comment-input.tsx +1 -1
- package/src/components/story/detail/comment-item.tsx +1 -1
- package/src/components/story/detail/comment-thread.tsx +1 -1
- package/src/components/story/detail/reaction-bar.tsx +1 -1
- package/src/components/story/detail/story-detail-body.tsx +4 -4
- package/src/components/story/detail/story-detail-toolbar/__tests__/attachment-cache.test.ts +83 -0
- package/src/components/story/detail/story-detail-toolbar/associated-attachments-panel.tsx +50 -9
- package/src/components/story/detail/story-detail-toolbar/children.tsx +3 -3
- package/src/components/story/detail/story-detail-toolbar/dependency-panel.tsx +9 -8
- package/src/components/story/detail/story-detail-toolbar/index.tsx +8 -8
- package/src/components/story/detail/story-detail-toolbar/related-test-cases.tsx +2 -2
- package/src/components/story/detail/story-properties.tsx +2 -2
- package/src/components/story/display/__tests__/flatten-story-groups.test.ts +1 -1
- package/src/components/story/display/__tests__/group-fields.test.ts +1 -1
- package/src/components/story/display/__tests__/group-resolver.test.ts +2 -2
- package/src/components/story/display/__tests__/order-resolver.test.ts +1 -1
- package/src/components/story/display/__tests__/use-grouped-stories.test.ts +1 -1
- package/src/components/story/display/__tests__/use-paged-story-groups.test.ts +1 -1
- package/src/components/story/display/flatten-story-groups.ts +1 -1
- package/src/components/story/display/group-fields.ts +2 -2
- package/src/components/story/display/group-resolver.tsx +4 -4
- package/src/components/story/display/order-resolver.ts +3 -3
- package/src/components/story/display/types.ts +5 -4
- package/src/components/story/display/use-grouped-stories.ts +5 -5
- package/src/components/story/display/use-paged-story-groups.ts +11 -4
- package/src/components/story/entity-overlay.ts +24 -0
- package/src/components/story/filters/field-configs.tsx +2 -2
- package/src/components/story/inline-editors/assignee-editor.tsx +1 -1
- package/src/components/story/inline-editors/dependency-type-select.tsx +1 -1
- package/src/components/story/inline-editors/parent-story-editor.tsx +2 -2
- package/src/components/story/inline-editors/priority-editor.tsx +2 -2
- package/src/components/story/inline-editors/state-editor.tsx +2 -2
- package/src/components/story/inline-editors/story-select-dialog.tsx +2 -2
- package/src/components/story/peek-panel.tsx +6 -5
- package/src/components/story/priority-icon.tsx +2 -2
- package/src/components/story/relations/__tests__/attachment-manage-dialog.test.tsx +1 -1
- package/src/components/story/relations/__tests__/design-select-dialog.test.tsx +1 -1
- package/src/components/story/relations/__tests__/knowledge-select-dialog.test.tsx +1 -1
- package/src/components/story/relations/__tests__/link-manage-dialog.test.tsx +1 -1
- package/src/components/story/relations/attachment-manage-dialog.tsx +4 -4
- package/src/components/story/relations/design-select-dialog.tsx +2 -2
- package/src/components/story/relations/knowledge-select-dialog.tsx +2 -2
- package/src/components/story/relations/link-manage-dialog.tsx +2 -2
- package/src/components/story/relations/story-ref-row.tsx +3 -3
- package/src/components/story/story-list-table.tsx +1 -1
- package/src/components/story/story-list-view.tsx +1 -1
- package/src/components/story/story-toolbar.tsx +1 -1
- package/src/components/story/transfer-story-popover.tsx +3 -2
- package/src/components/story/views/__tests__/calendar-view.test.tsx +1 -1
- package/src/components/story/views/__tests__/kanban-drop-subgroup.test.ts +1 -1
- package/src/components/story/views/__tests__/kanban-peek-sync.test.tsx +2 -2
- package/src/components/story/views/__tests__/kanban-view-membership-sync.test.tsx +1 -1
- package/src/components/story/views/__tests__/timeline-view.test.tsx +1 -1
- package/src/components/story/views/calendar/__tests__/mappers.test.ts +1 -1
- package/src/components/story/views/calendar/mappers.ts +1 -1
- package/src/components/story/views/calendar-view.tsx +1 -1
- package/src/components/story/views/create-view-dialog.tsx +1 -1
- package/src/components/story/views/kanban-view.tsx +2 -2
- package/src/components/story/views/story-list/batch-preview-dialog.tsx +2 -2
- package/src/components/story/views/story-list/batch-toolbar.tsx +3 -3
- package/src/components/story/views/story-list/group.tsx +18 -5
- package/src/components/story/views/story-list/index.tsx +4 -2
- package/src/components/story/views/story-list/inline-story-creator.tsx +2 -2
- package/src/components/story/views/story-list/row.tsx +2 -2
- package/src/components/story/views/story-list/use-flat-rows.ts +1 -1
- package/src/components/story/views/timeline/__tests__/mappers.test.ts +2 -2
- package/src/components/story/views/timeline/mappers.ts +2 -2
- package/src/components/story/views/timeline/use-date-commit.ts +1 -1
- package/src/components/story/views/timeline-view.tsx +2 -2
- package/src/components/testcase/__tests__/case-row.test.tsx +1 -1
- package/src/components/testcase/__tests__/create-dialog-scope.test.tsx +1 -1
- package/src/components/testcase/__tests__/display-options.test.tsx +1 -1
- package/src/components/testcase/__tests__/export-entry.test.tsx +1 -1
- package/src/components/testcase/__tests__/list-view.test.tsx +1 -1
- package/src/components/testcase/__tests__/step-editor.test.tsx +4 -2
- package/src/components/testcase/__tests__/toolbar.test.tsx +1 -1
- package/src/components/testcase/create-testcase-dialog.tsx +6 -5
- package/src/components/testcase/detail/case-detail-body.tsx +1 -1
- package/src/components/testcase/detail/properties.tsx +1 -1
- package/src/components/testcase/display/display-dropdown.tsx +2 -2
- package/src/components/testcase/display/use-testcase-display-options.ts +1 -1
- package/src/components/testcase/filters/field-configs.tsx +1 -1
- package/src/components/testcase/generation/draft-card.tsx +2 -2
- package/src/components/testcase/inline-editors/assignee-editor.tsx +1 -1
- package/src/components/testcase/list/case-row.tsx +1 -1
- package/src/components/testcase/list/grouped-list.tsx +1 -1
- package/src/components/testcase/list-view.tsx +2 -2
- package/src/components/testcase/runs/convert-bug-button.tsx +2 -2
- package/src/components/testcase/step-editor.tsx +13 -2
- package/src/components/testcase/testcase-toolbar.tsx +2 -2
- package/src/components/testcase/views/batch-toolbar.tsx +1 -1
- package/src/components/testcase/views/board-view.tsx +2 -2
- package/src/lib/changelog/sdk-versions.ts +2 -2
- package/src/lib/collections/knowledge/knowledge-bases-collection.ts +2 -1
- package/src/lib/collections/query-keys.ts +18 -0
- package/src/lib/collections/reference/users-brief-collection.ts +2 -1
- package/src/lib/collections/reference/workspaces-collection.ts +2 -1
- package/src/lib/story/__tests__/activity-grouper.test.ts +1 -1
- package/src/lib/story/activity-grouper.ts +1 -1
- package/src/paraglide/messages/_index.d.ts +2 -0
- package/src/paraglide/messages/_index.js +88269 -5642
- package/src/paraglide/messages/en.js +28255 -0
- package/src/paraglide/messages/story_group_loadfailed1.d.ts +17 -0
- package/src/paraglide/messages/story_group_loadfailed1.js +30 -0
- package/src/paraglide/messages/story_group_retry.d.ts +16 -0
- package/src/paraglide/messages/story_group_retry.js +29 -0
- package/src/paraglide/messages/zh.js +28253 -0
- package/src/routes/_nav/bugs/$bugId/route.lazy.tsx +2 -2
- package/src/routes/_nav/projects/$projectId/iterations/$iterationId/route.lazy.tsx +1 -1
- package/src/routes/_nav/route.tsx +1 -2
- package/src/routes/_nav/stories/$storyId/route.lazy.tsx +2 -2
- package/src/routes/_nav/stories/$storyId/route.tsx +1 -1
- package/src/routes/api/design/prototype/__tests__/route.test.ts +4 -1
- package/src/server/__tests__/task-executor.test.ts +7 -0
- package/src/server/ai-gateway/__tests__/ai-output-guard.test.ts +4 -4
- package/src/server/ai-gateway/__tests__/tools-authorization.test.ts +25 -0
- package/src/server/ai-gateway/tools/server.ts +24 -15
- package/src/server/knowledge/reindex-guard.ts +35 -0
- package/src/server/lib/__tests__/cron-auth.test.ts +1 -1
- package/src/server/serverFns/__tests__/legal-code-canonical.test.ts +169 -0
- package/src/server/serverFns/__tests__/story-tree.test.ts +0 -1
- package/src/server/serverFns/__tests__/story-types.test.ts +2 -1
- package/src/server/serverFns/__tests__/update-bug-guard.test.ts +1 -1
- package/src/server/serverFns/bug/__tests__/bug-comments.test.ts +95 -44
- package/src/server/serverFns/bug/__tests__/bug-crud.test.ts +46 -18
- package/src/server/serverFns/bug/__tests__/bug-groups.test.ts +1 -1
- package/src/server/serverFns/bug/bug-activities.ts +2 -2
- package/src/server/serverFns/bug/bug-comments.ts +44 -19
- package/src/server/serverFns/bug/bug-crud/detail-select.ts +1 -1
- package/src/server/serverFns/bug/bug-crud/detail.ts +2 -2
- package/src/server/serverFns/bug/bug-crud.ts +142 -110
- package/src/server/serverFns/bug/bug-group-query.ts +1 -1
- package/src/server/serverFns/bug/bug-groups.ts +2 -2
- package/src/server/serverFns/bug/bug-story-utils.ts +1 -1
- package/src/server/serverFns/chat/conversations.ts +27 -13
- package/src/server/serverFns/harness/github-workspace.ts +32 -25
- package/src/server/serverFns/harness/search-mention-candidates.ts +3 -1
- package/src/server/serverFns/iteration/iteration-crud.ts +1 -1
- package/src/server/serverFns/knowledge/public/legal/create.ts +3 -0
- package/src/server/serverFns/knowledge/public/legal/update.ts +8 -8
- package/src/server/serverFns/knowledge-index.ts +12 -5
- package/src/server/serverFns/project.ts +5 -11
- package/src/server/serverFns/story/__tests__/story-dependencies.test.ts +15 -4
- package/src/server/serverFns/story/__tests__/story-transfer.test.ts +20 -0
- package/src/server/serverFns/story/link-preview.ts +3 -2
- package/src/server/serverFns/story/story-children.ts +1 -1
- package/src/server/serverFns/story/story-comments.ts +1 -1
- package/src/server/serverFns/story/story-crud/bulk.ts +1 -1
- package/src/server/serverFns/story/story-crud.ts +1 -1
- package/src/server/serverFns/story/story-dependencies.ts +61 -69
- package/src/server/serverFns/story/story-groups.ts +1 -1
- package/src/server/serverFns/story/story-transfer.ts +8 -20
- package/src/server/serverFns/testcase/__tests__/automation.test.ts +21 -0
- package/src/server/serverFns/testcase/__tests__/testcase-groups.test.ts +1 -1
- package/src/server/serverFns/testcase/automation.ts +24 -1
- package/src/server/serverFns/testcase/testcase-board.ts +1 -1
- package/src/server/serverFns/testcase/testcase-crud.ts +1 -1
- package/src/server/serverFns/testcase/testcase-display-options.ts +1 -1
- package/src/server/serverFns/testcase/testcase-filter-views.ts +1 -1
- package/src/server/serverFns/testcase/types.ts +1 -1
- package/src/server/story/types.ts +7 -3
- package/src/shared/bug/__tests__/constants.test.ts +1 -1
- package/src/{components → shared}/bug/types.ts +4 -3
- package/src/{components → shared}/story/constants.ts +1 -1
- package/src/{components → shared}/story/types.ts +1 -7
- package/src/types/project.ts +1 -2
- /package/src/{components/testcase/display → shared/testcase}/field-configs.ts +0 -0
|
@@ -17,8 +17,8 @@
|
|
|
17
17
|
|
|
18
18
|
import { createLazyFileRoute, useNavigate } from "@tanstack/react-router";
|
|
19
19
|
import { useState } from "react";
|
|
20
|
-
import type { BugDetail } from "@/
|
|
21
|
-
import type { UserBrief } from "@/
|
|
20
|
+
import type { BugDetail } from "@/shared/bug/types";
|
|
21
|
+
import type { UserBrief } from "@/shared/story/types";
|
|
22
22
|
import { BugDetailBody } from "@/components/bug/detail/bug-detail-body";
|
|
23
23
|
import { BugPanelHeader } from "@/components/bug/bug-panel-header";
|
|
24
24
|
|
|
@@ -32,7 +32,7 @@ import { listProjectsBriefFn } from "@/server/serverFns/project";
|
|
|
32
32
|
import { toast } from "@/components/ui/toast";
|
|
33
33
|
import { useQuery } from "@tanstack/react-query";
|
|
34
34
|
import type { IterationSummary } from "@/server/serverFns/iteration/schemas";
|
|
35
|
-
import type { StoryState } from "@/
|
|
35
|
+
import type { StoryState } from "@/shared/story/constants";
|
|
36
36
|
import type { AvatarConfig } from "@allbluecn/kernel";
|
|
37
37
|
import { m } from "@/paraglide/messages.js";
|
|
38
38
|
import { toastError } from "@/lib/toast-error";
|
|
@@ -23,8 +23,7 @@ import { listPinnedWorkspacesBriefFn } from '@/server/serverFns/workspace'
|
|
|
23
23
|
import { listKnowledgeFn } from '@/server/serverFns/knowledge'
|
|
24
24
|
import { ensureVerifyReminderFn } from '@/server/serverFns/auth/verify-reminder'
|
|
25
25
|
import { getSidebarStateFn } from '@/server/serverFns/sidebar-state'
|
|
26
|
-
import { WORKSPACES_QUERY_KEY } from '@/lib/collections/
|
|
27
|
-
import { KNOWLEDGE_BASES_QUERY_KEY } from '@/lib/collections/knowledge/knowledge-bases-collection'
|
|
26
|
+
import { WORKSPACES_QUERY_KEY, KNOWLEDGE_BASES_QUERY_KEY } from '@/lib/collections/query-keys'
|
|
28
27
|
import { GlobalErrorBoundary } from '@/components/error/global-error-boundary'
|
|
29
28
|
import { NavNotFound } from './-components/nav-not-found'
|
|
30
29
|
|
|
@@ -17,9 +17,9 @@
|
|
|
17
17
|
|
|
18
18
|
import { createLazyFileRoute, useNavigate, useRouter } from "@tanstack/react-router";
|
|
19
19
|
import { useCallback, useState, type UIEvent } from "react";
|
|
20
|
-
import type { StoryDetail, UserBrief, CommentWithReplies, ActivityItem, RelationsData } from "@/
|
|
20
|
+
import type { StoryDetail, UserBrief, CommentWithReplies, ActivityItem, RelationsData } from "@/shared/story/types";
|
|
21
21
|
import type { IterationSummary } from "@/server/serverFns/iteration/schemas";
|
|
22
|
-
import type { StoryState } from "@/
|
|
22
|
+
import type { StoryState } from "@/shared/story/constants";
|
|
23
23
|
import { StoryDetailBody } from "@/components/story/detail/story-detail-body";
|
|
24
24
|
import { StoryPanelHeader } from "@/components/story/story-panel-header";
|
|
25
25
|
|
|
@@ -35,7 +35,7 @@ import { listProjectsBriefFn } from "@/server/serverFns/project";
|
|
|
35
35
|
import { listIterationsFn } from "@/server/serverFns/iteration";
|
|
36
36
|
import { getCollabSnapshotFn } from "@/server/serverFns/collab-snapshot";
|
|
37
37
|
import { isCollabEnabled } from "@/lib/collab/env";
|
|
38
|
-
import type { RelationsData } from "@/
|
|
38
|
+
import type { RelationsData } from "@/shared/story/types";
|
|
39
39
|
import { StoryDetailSkeleton } from "../-components/story-detail-skeleton";
|
|
40
40
|
|
|
41
41
|
// 「关联需求」选择源拉取上限:超过该数的需求会被静默截断(组件端无搜索补拉),
|
|
@@ -450,6 +450,9 @@ describe('/api/design/prototype 流式生成', () => {
|
|
|
450
450
|
expect(eventsOf(events, 'canvas/nodes')).toHaveLength(1)
|
|
451
451
|
const dones = eventsOf(events, 'canvas/done')
|
|
452
452
|
expect(dones).toHaveLength(1)
|
|
453
|
-
|
|
453
|
+
// 文案协议化(2026-10-28):服务端只回 status,完成文案由客户端 m.canvas_ai_doneFallback()
|
|
454
|
+
// 渲染;无 LLM 总结时 message 缺省(不再由服务端塞中文)
|
|
455
|
+
expect((dones[0].value as { status: string }).status).toBe('completed')
|
|
456
|
+
expect((dones[0].value as { message?: string }).message).toBeUndefined()
|
|
454
457
|
})
|
|
455
458
|
})
|
|
@@ -20,6 +20,10 @@ vi.mock('@allbluecn/database', () => ({
|
|
|
20
20
|
testCase: { findUnique: vi.fn() },
|
|
21
21
|
},
|
|
22
22
|
}))
|
|
23
|
+
// 源条目属主校验(2026-10-28 审计 A4x-1):测试默认按「是项目成员」放行,
|
|
24
|
+
// 需要验证拒绝路径时在用例内改 mock
|
|
25
|
+
vi.mock('@/server/lib/project-membership', () => ({ isProjectMember: vi.fn(async () => true) }))
|
|
26
|
+
|
|
23
27
|
vi.mock('@/server/ai-task/context-builder', async (importOriginal) => {
|
|
24
28
|
const actual = await importOriginal<typeof import('@/server/ai-task/context-builder')>()
|
|
25
29
|
return { ...actual, buildSourceContextBlock: vi.fn(() => '## 需求上下文(mock)') }
|
|
@@ -257,6 +261,7 @@ describe('executeAiTaskRun', () => {
|
|
|
257
261
|
|
|
258
262
|
it('源型 story:查源表并注入 buildSourceContextBlock 输出到 systemPrompts', async () => {
|
|
259
263
|
storyFindUnique.mockResolvedValue({
|
|
264
|
+
projectId: 'p1',
|
|
260
265
|
identifier: 'ALB-S12',
|
|
261
266
|
name: '登录页重构',
|
|
262
267
|
description: null,
|
|
@@ -272,6 +277,7 @@ describe('executeAiTaskRun', () => {
|
|
|
272
277
|
expect(storyFindUnique).toHaveBeenCalledWith({
|
|
273
278
|
where: { id: 'story-1' },
|
|
274
279
|
select: expect.objectContaining({
|
|
280
|
+
projectId: true,
|
|
275
281
|
identifier: true,
|
|
276
282
|
name: true,
|
|
277
283
|
description: true,
|
|
@@ -308,6 +314,7 @@ describe('executeAiTaskRun', () => {
|
|
|
308
314
|
|
|
309
315
|
it('源型分发:bug / testcase 各查对应表', async () => {
|
|
310
316
|
bugFindUnique.mockResolvedValue({
|
|
317
|
+
projectId: 'p1',
|
|
311
318
|
identifier: 'ALB-B3',
|
|
312
319
|
title: '首页白屏',
|
|
313
320
|
severity: 'critical',
|
|
@@ -27,7 +27,7 @@ describe("scanAiOutputForSecrets 输出泄露检测", () => {
|
|
|
27
27
|
})
|
|
28
28
|
|
|
29
29
|
it.each([
|
|
30
|
-
["OpenAI key 回显", "配置的密钥是 sk-proj-
|
|
30
|
+
["OpenAI key 回显", "配置的密钥是 sk-proj-abcdefghijkmnopqrstuv"],
|
|
31
31
|
["Bearer token 回显", "token 为 Bearer eyJhbGciOiJIUzI1NiJ9.payload"],
|
|
32
32
|
["anthropic key 回显", "sk-ant-api03-x7y8z9w0v1u2t3s4r5q6p7o8"],
|
|
33
33
|
["系统 prompt 回显", "我的系统提示词是:你是一个敏捷开发中的需求分析师。"],
|
|
@@ -41,7 +41,7 @@ describe("scanAiOutputForSecrets 输出泄露检测", () => {
|
|
|
41
41
|
})
|
|
42
42
|
|
|
43
43
|
it.each([
|
|
44
|
-
["openai-key", "密钥 sk-proj-
|
|
44
|
+
["openai-key", "密钥 sk-proj-abcdefghijkmnopqrstuv"],
|
|
45
45
|
["anthropic-key", "密钥 sk-ant-api03-x7y8z9w0v1u2t3s4r5q6p7o8"],
|
|
46
46
|
["bearer-token", "token 为 Bearer eyJhbGciOiJIUzI1NiJ9.payload"],
|
|
47
47
|
["jwt", "令牌 eyJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOiIxMjMifQ 已生成"],
|
|
@@ -60,13 +60,13 @@ describe("scanAiOutputForSecrets 输出泄露检测", () => {
|
|
|
60
60
|
})
|
|
61
61
|
|
|
62
62
|
it("命中清单不含原文泄露(防扫描器自身成为泄露面)", () => {
|
|
63
|
-
const secret = "sk-proj-
|
|
63
|
+
const secret = "sk-proj-abcdefghijkmnopqrstuv"
|
|
64
64
|
const r = scanAiOutputForSecrets(`密钥 ${secret} 已泄露`)
|
|
65
65
|
expect(JSON.stringify(r)).not.toContain(secret)
|
|
66
66
|
})
|
|
67
67
|
|
|
68
68
|
it("同一文本重复调用两次结果一致(/g 正则 lastIndex 复位)", () => {
|
|
69
|
-
const text = "配置 sk-proj-
|
|
69
|
+
const text = "配置 sk-proj-abcdefghijkmnopqrstuv 并部署到 192.168.1.100"
|
|
70
70
|
const a = scanAiOutputForSecrets(text)
|
|
71
71
|
const b = scanAiOutputForSecrets(text)
|
|
72
72
|
expect(b).toEqual(a)
|
|
@@ -208,4 +208,29 @@ describe("reindex_knowledge_base 归属", () => {
|
|
|
208
208
|
expect(out).toEqual({ indexed: 1, documentCount: 1 })
|
|
209
209
|
expect(mocks.deleteEmbeddings).toHaveBeenCalledWith("knowledge_embeddings", "kbId", "kb-1")
|
|
210
210
|
})
|
|
211
|
+
|
|
212
|
+
it("S26:同 KB 重建进行中拒绝并发触发,完成后可再次重建", async () => {
|
|
213
|
+
mocks.kb.findFirst.mockResolvedValue({ id: "kb-2" })
|
|
214
|
+
// 用受控 promise 拉长首例重建,制造 in-flight 窗口
|
|
215
|
+
let release!: () => void
|
|
216
|
+
const gate = new Promise<void>((r) => { release = r })
|
|
217
|
+
mocks.doc.findMany.mockImplementation(async () => {
|
|
218
|
+
await gate
|
|
219
|
+
return [{ id: "d-1", title: "t", content: "c", kbId: "kb-2" }]
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
const first = run({ knowledgeBaseId: "kb-2" }, { context: ctxWith() })
|
|
223
|
+
await expect(
|
|
224
|
+
run({ knowledgeBaseId: "kb-2" }, { context: ctxWith() }),
|
|
225
|
+
).rejects.toThrow("正在重建索引")
|
|
226
|
+
release()
|
|
227
|
+
await expect(first).resolves.toEqual({ indexed: 1, documentCount: 1 })
|
|
228
|
+
|
|
229
|
+
// 释放后可再次重建
|
|
230
|
+
mocks.doc.findMany.mockResolvedValue([{ id: "d-1", title: "t", content: "c", kbId: "kb-2" }])
|
|
231
|
+
await expect(run({ knowledgeBaseId: "kb-2" }, { context: ctxWith() })).resolves.toEqual({
|
|
232
|
+
indexed: 1,
|
|
233
|
+
documentCount: 1,
|
|
234
|
+
})
|
|
235
|
+
})
|
|
211
236
|
})
|
|
@@ -24,6 +24,7 @@ import { constrainKnowledgeBaseId, mergeKnowledgeResults, type KnowledgeSearchIn
|
|
|
24
24
|
import { todoWriteTool } from "./todo-write"
|
|
25
25
|
import { planSubmitTool } from "./plan-submit"
|
|
26
26
|
import { ToolValidationError, toToolErrorMessage } from "./tool-errors"
|
|
27
|
+
import { tryAcquireReindex, releaseReindex } from "@/server/knowledge/reindex-guard"
|
|
27
28
|
|
|
28
29
|
/**
|
|
29
30
|
* knowledge_search 的执行逻辑(从 tool 定义中抽出,使返回边界可被直接单测)。
|
|
@@ -288,25 +289,33 @@ const reindexKnowledgeBaseTool = reindexKnowledgeBaseDef.server<AppContext>(
|
|
|
288
289
|
})
|
|
289
290
|
if (!kb) throw new ToolValidationError("knowledgeBaseId 不存在或无权限")
|
|
290
291
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
}
|
|
292
|
+
// S26:同 KB 重建进行中拒绝重复触发
|
|
293
|
+
if (!tryAcquireReindex(knowledgeBaseId)) {
|
|
294
|
+
throw new ToolValidationError("该知识库正在重建索引,请稍后再试")
|
|
295
|
+
}
|
|
296
|
+
try {
|
|
297
|
+
const docs = await prisma.knowledgeDocument.findMany({
|
|
298
|
+
where: { kbId: knowledgeBaseId, isDeleted: false },
|
|
299
|
+
select: { id: true, title: true, content: true, kbId: true },
|
|
300
|
+
})
|
|
295
301
|
|
|
296
|
-
|
|
302
|
+
const aiConfig = await getPorts().infra.resolveKnowledgeAiConfig(knowledgeBaseId)
|
|
297
303
|
|
|
298
|
-
|
|
304
|
+
await getPorts().infra.deleteEmbeddings("knowledge_embeddings", "kbId", knowledgeBaseId)
|
|
299
305
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
306
|
+
let total = 0
|
|
307
|
+
for (const doc of docs) {
|
|
308
|
+
const indexed = await getPorts().infra.indexDocumentEmbeddings(
|
|
309
|
+
{ id: doc.id, kbId: doc.kbId, title: doc.title, content: doc.content as string },
|
|
310
|
+
aiConfig
|
|
311
|
+
)
|
|
312
|
+
total += indexed
|
|
313
|
+
}
|
|
308
314
|
|
|
309
|
-
|
|
315
|
+
return { indexed: total, documentCount: docs.length }
|
|
316
|
+
} finally {
|
|
317
|
+
releaseReindex(knowledgeBaseId)
|
|
318
|
+
}
|
|
310
319
|
},
|
|
311
320
|
)
|
|
312
321
|
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// AllBlue - 理想之海
|
|
3
|
+
// Copyright (C) 2026 AllBlue Contributors
|
|
4
|
+
//
|
|
5
|
+
// This program is free software: you can redistribute it and/or modify
|
|
6
|
+
// it under the terms of the GNU Affero General Public License as published by
|
|
7
|
+
// the Free Software Foundation, either version 3 of the License, or
|
|
8
|
+
// (at your option) any later version.
|
|
9
|
+
//
|
|
10
|
+
// This program is distributed in the hope that it will be useful,
|
|
11
|
+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
12
|
+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
13
|
+
// GNU Affero General Public License for more details.
|
|
14
|
+
//
|
|
15
|
+
// You should have received a copy of the GNU Affero General Public License
|
|
16
|
+
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
|
+
|
|
18
|
+
// S26(审计 A3x ⚪):reindex 并发去抖——同 KB 重建进行中拒绝重复触发,
|
|
19
|
+
// 防 embedding 配额双倍消耗与「先删后建」窗口交叠。进程内存态(与
|
|
20
|
+
// rate-limit.ts 同边界声明):多实例部署不共享,各实例至多一个重建在途;
|
|
21
|
+
// 分布式锁(DB 标记/Redis)属独立架构任务,落地前不得把本守卫视为唯一防线。
|
|
22
|
+
|
|
23
|
+
const inFlightReindex = new Set<string>();
|
|
24
|
+
|
|
25
|
+
/** 尝试占用 kbId 的重建锁;false = 已有重建在途(调用方应拒绝请求) */
|
|
26
|
+
export function tryAcquireReindex(kbId: string): boolean {
|
|
27
|
+
if (inFlightReindex.has(kbId)) return false;
|
|
28
|
+
inFlightReindex.add(kbId);
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** 释放重建锁(调用方 finally 中必须调用) */
|
|
33
|
+
export function releaseReindex(kbId: string): void {
|
|
34
|
+
inFlightReindex.delete(kbId);
|
|
35
|
+
}
|
|
@@ -21,7 +21,7 @@ import { afterEach, describe, expect, it, vi } from "vitest";
|
|
|
21
21
|
|
|
22
22
|
import { isCronRequestAuthorized, verifyCronSecret } from "../cron-auth";
|
|
23
23
|
|
|
24
|
-
const SECRET = "cron-secret-
|
|
24
|
+
const SECRET = "cron-secret-fakefakefakefakefake";
|
|
25
25
|
|
|
26
26
|
const reqWith = (authorization: string | null): Request => {
|
|
27
27
|
const h = new Headers();
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// AllBlue - 理想之海
|
|
3
|
+
// Copyright (C) 2026 AllBlue Contributors
|
|
4
|
+
//
|
|
5
|
+
// This program is free software: you can redistribute it and/or modify
|
|
6
|
+
// it under the terms of the GNU Affero General Public License as published by
|
|
7
|
+
// the Free Software Foundation, either version 3 of the License, or
|
|
8
|
+
// (at your option) any later version.
|
|
9
|
+
//
|
|
10
|
+
// This program is distributed in the hope that it will be useful,
|
|
11
|
+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
12
|
+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
13
|
+
// GNU Affero General Public License for more details.
|
|
14
|
+
//
|
|
15
|
+
// You should have received a copy of the GNU Affero General Public License
|
|
16
|
+
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
|
+
|
|
18
|
+
// 审计 S7(2026-10-28)LegalRegulation.code 持久化回归:
|
|
19
|
+
// - update:canonicalId 仅在显式传 code 时重算(compliance 同款裁定),
|
|
20
|
+
// 仅改 title 一律不漂移(有 code 行身份=code;无 code 存量行保持创建时 key);
|
|
21
|
+
// - code 落库 + 空串归一 null;
|
|
22
|
+
// - create:code 落库 + canonicalId = code || title。
|
|
23
|
+
|
|
24
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
25
|
+
|
|
26
|
+
vi.mock('@tanstack/react-start', () => ({
|
|
27
|
+
createServerFn: (_config: unknown) => {
|
|
28
|
+
const api = {
|
|
29
|
+
_validator: undefined as unknown,
|
|
30
|
+
validator(schema: unknown) {
|
|
31
|
+
this._validator = schema
|
|
32
|
+
return this
|
|
33
|
+
},
|
|
34
|
+
handler(fn: (args: { data: unknown }) => Promise<unknown>) {
|
|
35
|
+
const wrapped = async (args: unknown) => {
|
|
36
|
+
let data = (args as { data?: unknown } | null | undefined)?.data
|
|
37
|
+
if (this._validator && data !== undefined) {
|
|
38
|
+
const v = this._validator as { parse?: (d: unknown) => unknown }
|
|
39
|
+
data = v.parse ? v.parse(data) : data
|
|
40
|
+
}
|
|
41
|
+
return fn({ data })
|
|
42
|
+
}
|
|
43
|
+
return wrapped
|
|
44
|
+
},
|
|
45
|
+
}
|
|
46
|
+
return api
|
|
47
|
+
},
|
|
48
|
+
}))
|
|
49
|
+
|
|
50
|
+
vi.mock('@tanstack/react-start/server', () => ({
|
|
51
|
+
getRequest: vi.fn(),
|
|
52
|
+
}))
|
|
53
|
+
|
|
54
|
+
const m = vi.hoisted(() => ({
|
|
55
|
+
legalRegulation: {
|
|
56
|
+
findUnique: vi.fn(),
|
|
57
|
+
create: vi.fn(),
|
|
58
|
+
update: vi.fn(),
|
|
59
|
+
},
|
|
60
|
+
}))
|
|
61
|
+
|
|
62
|
+
vi.mock('@allbluecn/database/get-prisma', () => ({ getPrisma: async () => m }))
|
|
63
|
+
vi.mock('@allbluecn/database', () => ({ Prisma: {} }))
|
|
64
|
+
|
|
65
|
+
vi.mock('@/server/auth-helpers', () => ({ requireAuth: vi.fn() }))
|
|
66
|
+
|
|
67
|
+
import { requireAuth } from '@/server/auth-helpers'
|
|
68
|
+
import * as updateMod from '../knowledge/public/legal/update'
|
|
69
|
+
import * as createMod from '../knowledge/public/legal/create'
|
|
70
|
+
|
|
71
|
+
const mockRequireAuth = vi.mocked(requireAuth)
|
|
72
|
+
|
|
73
|
+
describe('updateLegalFn:code 持久化 + canonicalId 重算口径(S7)', () => {
|
|
74
|
+
beforeEach(() => {
|
|
75
|
+
vi.clearAllMocks()
|
|
76
|
+
mockRequireAuth.mockResolvedValue({ user: { id: 'u1', role: 'USER' } } as never)
|
|
77
|
+
m.legalRegulation.update.mockResolvedValue({ id: 'row-1' })
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('仅改 title(有 code 行)→ canonicalId 保持 code 维度不漂移,code 不重写', async () => {
|
|
81
|
+
m.legalRegulation.findUnique.mockResolvedValue({
|
|
82
|
+
id: 'row-1', userId: 'u1', title: '旧标题', code: 'GB 4943', canonicalId: 'GB 4943',
|
|
83
|
+
})
|
|
84
|
+
await updateMod.updateLegalFn({ data: { id: 'row-1', title: '新标题' } })
|
|
85
|
+
expect(m.legalRegulation.update).toHaveBeenCalledWith(
|
|
86
|
+
expect.objectContaining({
|
|
87
|
+
data: expect.not.objectContaining({ canonicalId: expect.anything(), code: expect.anything() }),
|
|
88
|
+
}),
|
|
89
|
+
)
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('仅改 title(无 code 存量行)→ canonicalId 保持创建时 key 不失联', async () => {
|
|
93
|
+
m.legalRegulation.findUnique.mockResolvedValue({
|
|
94
|
+
id: 'row-1', userId: 'u1', title: '旧标题', code: null, canonicalId: '旧标题',
|
|
95
|
+
})
|
|
96
|
+
await updateMod.updateLegalFn({ data: { id: 'row-1', title: '新标题' } })
|
|
97
|
+
expect(m.legalRegulation.update).toHaveBeenCalledWith(
|
|
98
|
+
expect.objectContaining({
|
|
99
|
+
data: expect.not.objectContaining({ canonicalId: expect.anything() }),
|
|
100
|
+
}),
|
|
101
|
+
)
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('显式传 code → code 落库且 canonicalId 以 code 重算', async () => {
|
|
105
|
+
m.legalRegulation.findUnique.mockResolvedValue({
|
|
106
|
+
id: 'row-1', userId: 'u1', title: '旧标题', code: null, canonicalId: '旧标题',
|
|
107
|
+
})
|
|
108
|
+
await updateMod.updateLegalFn({ data: { id: 'row-1', code: 'GB 4943' } })
|
|
109
|
+
expect(m.legalRegulation.update).toHaveBeenCalledWith(
|
|
110
|
+
expect.objectContaining({
|
|
111
|
+
data: expect.objectContaining({ code: 'GB 4943', canonicalId: 'GB 4943' }),
|
|
112
|
+
}),
|
|
113
|
+
)
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
it('code 传空串 → 归一为 null 落库,canonicalId 回落 title 维度', async () => {
|
|
117
|
+
m.legalRegulation.findUnique.mockResolvedValue({
|
|
118
|
+
id: 'row-1', userId: 'u1', title: '旧标题', code: 'GB 4943', canonicalId: 'GB 4943',
|
|
119
|
+
})
|
|
120
|
+
await updateMod.updateLegalFn({ data: { id: 'row-1', code: '' } })
|
|
121
|
+
expect(m.legalRegulation.update).toHaveBeenCalledWith(
|
|
122
|
+
expect.objectContaining({
|
|
123
|
+
data: expect.objectContaining({ code: null, canonicalId: '旧标题' }),
|
|
124
|
+
}),
|
|
125
|
+
)
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it('code + title 同传 → code 维度优先', async () => {
|
|
129
|
+
m.legalRegulation.findUnique.mockResolvedValue({
|
|
130
|
+
id: 'row-1', userId: 'u1', title: '旧标题', code: null, canonicalId: '旧标题',
|
|
131
|
+
})
|
|
132
|
+
await updateMod.updateLegalFn({ data: { id: 'row-1', title: '新标题', code: 'GB 4943-2024' } })
|
|
133
|
+
expect(m.legalRegulation.update).toHaveBeenCalledWith(
|
|
134
|
+
expect.objectContaining({
|
|
135
|
+
data: expect.objectContaining({ code: 'GB 4943-2024', canonicalId: 'GB 4943-2024' }),
|
|
136
|
+
}),
|
|
137
|
+
)
|
|
138
|
+
})
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
describe('createLegalFn:code 落库(S7)', () => {
|
|
142
|
+
beforeEach(() => {
|
|
143
|
+
vi.clearAllMocks()
|
|
144
|
+
mockRequireAuth.mockResolvedValue({ user: { id: 'u1', role: 'USER' } } as never)
|
|
145
|
+
m.legalRegulation.create.mockResolvedValue({ id: 'new-1' })
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
it('传 code → 落库且 canonicalId = code', async () => {
|
|
149
|
+
await createMod.createLegalFn({
|
|
150
|
+
data: { title: '某标准', code: 'GB 4943', jurisdiction: 'CN_NATIONAL', category: '安全', tags: [] },
|
|
151
|
+
})
|
|
152
|
+
expect(m.legalRegulation.create).toHaveBeenCalledWith(
|
|
153
|
+
expect.objectContaining({
|
|
154
|
+
data: expect.objectContaining({ code: 'GB 4943', canonicalId: 'GB 4943' }),
|
|
155
|
+
}),
|
|
156
|
+
)
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
it('不传 code → code 落 null,canonicalId = title', async () => {
|
|
160
|
+
await createMod.createLegalFn({
|
|
161
|
+
data: { title: '某办法', jurisdiction: 'CN_LOCAL', category: '管理', tags: [] },
|
|
162
|
+
})
|
|
163
|
+
expect(m.legalRegulation.create).toHaveBeenCalledWith(
|
|
164
|
+
expect.objectContaining({
|
|
165
|
+
data: expect.objectContaining({ code: null, canonicalId: '某办法' }),
|
|
166
|
+
}),
|
|
167
|
+
)
|
|
168
|
+
})
|
|
169
|
+
})
|
|
@@ -123,7 +123,8 @@ describe("castStoryUsers", () => {
|
|
|
123
123
|
it("头像配置保留非空 JSON", () => {
|
|
124
124
|
const cfg = { bgShape: "blob", bgColor: "#0000ff", parts: [] };
|
|
125
125
|
const res = castStoryUsers(storyFull({ assignee: userBrief({ avatarConfig: cfg }) }));
|
|
126
|
-
|
|
126
|
+
// 测试构造的入参 assignee 非空,按行类型声明收窄
|
|
127
|
+
expect(res.assignee!.avatarConfig).toEqual(cfg);
|
|
127
128
|
});
|
|
128
129
|
|
|
129
130
|
it("主列表 select 携带 iteration(甘特迭代分组数据源,缺失即退化为单一无迭代组)", () => {
|
|
@@ -92,7 +92,7 @@ vi.mock("@/server/collab/write-guard", () => ({
|
|
|
92
92
|
}));
|
|
93
93
|
|
|
94
94
|
import { updateBugFn } from "../bug/bug-crud";
|
|
95
|
-
import { BUG_LIST_SELECT } from "@/
|
|
95
|
+
import { BUG_LIST_SELECT } from "@/shared/bug/types";
|
|
96
96
|
|
|
97
97
|
const U = "u-1";
|
|
98
98
|
const P = "pr-1";
|
|
@@ -93,7 +93,7 @@ import {
|
|
|
93
93
|
listCommentsFn,
|
|
94
94
|
updateCommentFn,
|
|
95
95
|
} from "../bug-comments";
|
|
96
|
-
import { BUG_USER_BRIEF_SELECT } from "@/
|
|
96
|
+
import { BUG_USER_BRIEF_SELECT } from "@/shared/bug/types";
|
|
97
97
|
|
|
98
98
|
const USER = "u-current";
|
|
99
99
|
const OTHER = "u-other";
|
|
@@ -133,18 +133,29 @@ beforeEach(() => {
|
|
|
133
133
|
});
|
|
134
134
|
|
|
135
135
|
describe("listCommentsFn", () => {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
136
|
+
// 新契约(S39):根评论分页查询 + 回复整树查询,两次 findMany 分别 mock
|
|
137
|
+
const mockRootsAndReplies = (roots: unknown[], replies: unknown[] = []) => {
|
|
138
|
+
mocks.bugCommentFindMany.mockResolvedValueOnce(roots).mockResolvedValueOnce(replies);
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
it("首页取根评论(take+1 探测)+ 回复整树随根,组装多级回复树", async () => {
|
|
142
|
+
mockRootsAndReplies(
|
|
143
|
+
[
|
|
144
|
+
commentRow({ id: "c1", parentId: null }),
|
|
145
|
+
commentRow({ id: "c2", parentId: null }),
|
|
146
|
+
],
|
|
147
|
+
[
|
|
148
|
+
commentRow({ id: "r1", parentId: "c1" }),
|
|
149
|
+
commentRow({ id: "r2", parentId: "r1" }),
|
|
150
|
+
],
|
|
151
|
+
);
|
|
142
152
|
|
|
143
153
|
const res = await listCommentsFn({ data: { bugId: BUG_ID } });
|
|
144
154
|
|
|
145
|
-
expect(mocks.bugCommentFindMany).
|
|
146
|
-
where: { bugId: BUG_ID },
|
|
147
|
-
orderBy: { createdAt: "asc" },
|
|
155
|
+
expect(mocks.bugCommentFindMany).toHaveBeenNthCalledWith(1, {
|
|
156
|
+
where: { bugId: BUG_ID, parentId: null },
|
|
157
|
+
orderBy: [{ createdAt: "asc" }, { id: "asc" }],
|
|
158
|
+
take: 51,
|
|
148
159
|
select: {
|
|
149
160
|
id: true,
|
|
150
161
|
content: true,
|
|
@@ -155,57 +166,97 @@ describe("listCommentsFn", () => {
|
|
|
155
166
|
reactions: { select: { emoji: true, userId: true } },
|
|
156
167
|
},
|
|
157
168
|
});
|
|
158
|
-
expect(
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
expect(res
|
|
163
|
-
expect(res
|
|
164
|
-
expect(res[0].
|
|
165
|
-
expect(res
|
|
166
|
-
expect(res
|
|
167
|
-
expect(res[0].replies
|
|
168
|
-
expect(res[0].replies[0].
|
|
169
|
-
expect(res[0].replies[0].replies[0].id).toBe("
|
|
170
|
-
expect(res
|
|
169
|
+
expect(mocks.bugCommentFindMany).toHaveBeenNthCalledWith(
|
|
170
|
+
2,
|
|
171
|
+
expect.objectContaining({ where: { bugId: BUG_ID, parentId: { in: ["c1", "c2"] } } }),
|
|
172
|
+
);
|
|
173
|
+
expect(res.nextCursor).toBeNull();
|
|
174
|
+
expect(res.items).toHaveLength(2);
|
|
175
|
+
expect(res.items[0].id).toBe("c1");
|
|
176
|
+
expect(res.items[0].createdAt).toBe(T1.toISOString());
|
|
177
|
+
expect(res.items[0].user.id).toBe(USER);
|
|
178
|
+
expect(res.items[0].replies).toHaveLength(1);
|
|
179
|
+
expect(res.items[0].replies[0].id).toBe("r1");
|
|
180
|
+
expect(res.items[0].replies[0].replies[0].id).toBe("r2");
|
|
181
|
+
expect(res.items[1].id).toBe("c2");
|
|
182
|
+
expect(res.items[1].replies).toEqual([]);
|
|
171
183
|
});
|
|
172
184
|
|
|
173
|
-
it("
|
|
174
|
-
|
|
175
|
-
commentRow({
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
{ emoji: "👍", userId: USER },
|
|
179
|
-
{ emoji: "👍", userId: OTHER },
|
|
180
|
-
{ emoji: "🎉", userId: OTHER },
|
|
181
|
-
],
|
|
182
|
-
}),
|
|
185
|
+
it("超过 take 时 nextCursor 指向下一页锚点,items 截到 take", async () => {
|
|
186
|
+
mockRootsAndReplies([
|
|
187
|
+
commentRow({ id: "c1", parentId: null }),
|
|
188
|
+
commentRow({ id: "c2", parentId: null }),
|
|
189
|
+
commentRow({ id: "c3", parentId: null, createdAt: T2 }), // take+1 探测行
|
|
183
190
|
]);
|
|
184
191
|
|
|
192
|
+
const res = await listCommentsFn({ data: { bugId: BUG_ID, take: 2 } });
|
|
193
|
+
|
|
194
|
+
expect(mocks.bugCommentFindMany).toHaveBeenNthCalledWith(
|
|
195
|
+
1,
|
|
196
|
+
expect.objectContaining({ take: 3 }),
|
|
197
|
+
);
|
|
198
|
+
expect(res.items.map((c) => c.id)).toEqual(["c1", "c2"]);
|
|
199
|
+
expect(res.nextCursor).toBe("c3");
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it("cursor 续拉:skip 1 跳过锚行,回复只查当前页根", async () => {
|
|
203
|
+
mockRootsAndReplies([commentRow({ id: "c9", parentId: null, createdAt: T2 })]);
|
|
204
|
+
|
|
205
|
+
const res = await listCommentsFn({ data: { bugId: BUG_ID, cursor: "c2" } });
|
|
206
|
+
|
|
207
|
+
expect(mocks.bugCommentFindMany).toHaveBeenNthCalledWith(
|
|
208
|
+
1,
|
|
209
|
+
expect.objectContaining({ cursor: { id: "c2" }, skip: 1, take: 51 }),
|
|
210
|
+
);
|
|
211
|
+
expect(mocks.bugCommentFindMany).toHaveBeenNthCalledWith(
|
|
212
|
+
2,
|
|
213
|
+
expect.objectContaining({ where: { bugId: BUG_ID, parentId: { in: ["c9"] } } }),
|
|
214
|
+
);
|
|
215
|
+
expect(res.items).toHaveLength(1);
|
|
216
|
+
expect(res.nextCursor).toBeNull();
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it("评论 reaction 计数聚合并入评论查询(当前用户已表态 reacted=true)", async () => {
|
|
220
|
+
mockRootsAndReplies(
|
|
221
|
+
[
|
|
222
|
+
commentRow({
|
|
223
|
+
id: "c1",
|
|
224
|
+
reactions: [
|
|
225
|
+
{ emoji: "👍", userId: USER },
|
|
226
|
+
{ emoji: "👍", userId: OTHER },
|
|
227
|
+
{ emoji: "🎉", userId: OTHER },
|
|
228
|
+
],
|
|
229
|
+
}),
|
|
230
|
+
],
|
|
231
|
+
[],
|
|
232
|
+
);
|
|
233
|
+
|
|
185
234
|
const res = await listCommentsFn({ data: { bugId: BUG_ID } });
|
|
186
235
|
|
|
187
|
-
expect(res[0].reactions).toEqual([
|
|
236
|
+
expect(res.items[0].reactions).toEqual([
|
|
188
237
|
{ emoji: "👍", count: 2, userIds: [USER, OTHER], reacted: true },
|
|
189
238
|
{ emoji: "🎉", count: 1, userIds: [OTHER], reacted: false },
|
|
190
239
|
]);
|
|
191
240
|
});
|
|
192
241
|
|
|
193
242
|
it("parentId 指向不存在评论的孤儿回复被丢弃(照 story 实际行为)", async () => {
|
|
194
|
-
|
|
195
|
-
commentRow({ id: "c1", parentId: null }),
|
|
196
|
-
commentRow({ id: "c-orphan", parentId: "c-missing" }),
|
|
197
|
-
|
|
243
|
+
mockRootsAndReplies(
|
|
244
|
+
[commentRow({ id: "c1", parentId: null })],
|
|
245
|
+
[commentRow({ id: "c-orphan", parentId: "c-missing" })],
|
|
246
|
+
);
|
|
198
247
|
|
|
199
248
|
const res = await listCommentsFn({ data: { bugId: BUG_ID } });
|
|
200
249
|
|
|
201
|
-
expect(res).toHaveLength(1);
|
|
202
|
-
expect(res[0].id).toBe("c1");
|
|
203
|
-
expect(res[0].replies).toEqual([]);
|
|
250
|
+
expect(res.items).toHaveLength(1);
|
|
251
|
+
expect(res.items[0].id).toBe("c1");
|
|
252
|
+
expect(res.items[0].replies).toEqual([]);
|
|
204
253
|
});
|
|
205
254
|
|
|
206
|
-
it("
|
|
207
|
-
|
|
208
|
-
await
|
|
255
|
+
it("无评论时返回空 items 且不发起回复查询", async () => {
|
|
256
|
+
mockRootsAndReplies([]);
|
|
257
|
+
const res = await listCommentsFn({ data: { bugId: BUG_ID } });
|
|
258
|
+
expect(res).toEqual({ items: [], nextCursor: null });
|
|
259
|
+
expect(mocks.bugCommentFindMany).toHaveBeenCalledTimes(1);
|
|
209
260
|
});
|
|
210
261
|
|
|
211
262
|
it("项目非成员拒绝且不查询评论", async () => {
|