@allbluecn/web-app 0.10.3 → 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.
- package/package.json +21 -11
- package/public/favicon.ico +0 -0
- package/public/logo/allblue-logo-app-icon.svg +7 -0
- package/public/logo/allblue-logo-dark-square.svg +4 -0
- package/public/logo/allblue-logo-dark.svg +4 -0
- package/public/logo/allblue-logo-light-square.svg +4 -0
- package/public/logo/allblue-logo-light.svg +4 -0
- package/src/__tests__/bench/baselines/format-timestamp.json +44 -0
- package/src/__tests__/bench/core-hot-path.bench.ts +71 -0
- package/src/__tests__/integration/db-crud.integration.test.ts +79 -0
- package/src/__tests__/rejects-tothrow-regression.test.ts +31 -0
- package/src/__tests__/security/ai/prompt-injection.test.ts +105 -0
- package/src/__tests__/security/ssrf-url-whitelist.test.ts +54 -0
- package/src/__tests__/security/xss-sanitize.test.ts +99 -0
- package/src/__tests__/setup.ts +9 -0
- package/src/__tests__/visual/__screenshots__/theme-surface.vrt.test.tsx/theme-surface-dark-baseline-1-chromium-darwin.png +0 -0
- package/src/__tests__/visual/__screenshots__/theme-surface.vrt.test.tsx/theme-surface-light-baseline-1-chromium-darwin.png +0 -0
- package/src/__tests__/visual/__screenshots__/user-avatar.vrt.test.tsx/user-avatar-initial-fallback-baseline-1-chromium-darwin.png +0 -0
- package/src/__tests__/visual/theme-surface.vrt.test.tsx +54 -0
- package/src/__tests__/visual/user-avatar.vrt.test.tsx +32 -0
- package/src/components/error/global-error-boundary.tsx +2 -2
- package/src/components/knowledge/general-groups.ts +15 -0
- package/src/components/knowledge/kb-filter-popover.tsx +143 -0
- package/src/components/knowledge-public/legal-compliance-editor.tsx +4 -2
- package/src/components/layout/sidebar-03/__tests__/nav-workspace.test.tsx +9 -6
- package/src/components/layout/sidebar-03/nav-knowledge.tsx +19 -17
- package/src/components/layout/sidebar-03/nav-workspace.tsx +20 -22
- package/src/components/shared-kit/avatars/notion-avatar.tsx +2 -1
- package/src/components/shared-kit/icons/__tests__/lucide-curated-icons.test.ts +2 -2
- package/src/components/shared-kit/icons/__tests__/lucide-icon-picker.test.tsx +3 -2
- package/src/components/shared-kit/icons/lucide-icon-picker/curated-icons.ts +5 -10
- package/src/components/shared-kit/icons/lucide-icon-picker/lucide-icon-by-name.tsx +3 -3
- package/src/components/shared-kit/theme/theme-surface.tsx +52 -0
- package/src/components/story/relations/general-groups.ts +1 -15
- package/src/components/story/relations/knowledge-select-dialog.tsx +13 -87
- package/src/components/story/story-list-view.tsx +7 -1
- package/src/components/tiptap/node/table-node/ui/table-move-row-column-button/use-table-move-row-column.ts +0 -2
- package/src/components/workspace/__tests__/activity/workspace-activity-card.test.tsx +36 -0
- package/src/components/workspace/__tests__/contribution/workspace-contribution-card.test.tsx +17 -0
- package/src/components/workspace/__tests__/dialogs/create-workspace-dialog.test.tsx +63 -0
- package/src/components/workspace/__tests__/dialogs/delete-workspace-options.test.tsx +89 -0
- package/src/components/workspace/__tests__/dialogs/edit-workspace-dialog.test.tsx +68 -0
- package/src/components/workspace/__tests__/header/workspace-header.test.tsx +40 -0
- package/src/components/workspace/__tests__/kb/workspace-kb-card.test.tsx +154 -0
- package/src/components/workspace/__tests__/members/workspace-member-item.test.tsx +24 -0
- package/src/components/workspace/__tests__/members/workspace-members-manager.test.tsx +51 -0
- package/src/components/workspace/__tests__/projects/workspace-project-list.test.tsx +46 -0
- package/src/components/workspace/{create-workspace-dialog.tsx → dialogs/create-workspace-dialog.tsx} +1 -1
- package/src/components/workspace/{edit-workspace-dialog.tsx → dialogs/edit-workspace-dialog.tsx} +1 -1
- package/src/components/workspace/kb/workspace-kb-card.tsx +594 -0
- package/src/components/workspace/kb/workspace-kb-manage-dialog.tsx +174 -0
- package/src/components/workspace/kb/workspace-quick-doc-dialog.tsx +431 -0
- package/src/components/workspace/{workspace-members-manager.tsx → members/workspace-members-manager.tsx} +78 -40
- package/src/lib/__tests__/msw-sample.test.ts +68 -0
- package/src/lib/__tests__/pgvector.test.ts +2 -2
- package/src/lib/changelog/sdk-versions.ts +21 -21
- package/src/lib/collections/knowledge/knowledge-bases-collection.ts +3 -1
- package/src/lib/collections/reference/workspaces-collection.ts +3 -1
- package/src/lib/fetch-json.ts +23 -0
- package/src/{components/workspace/workspace-kb-card.tsx → lib/i18n/resolver.ts} +6 -12
- package/src/lib/sanitize.ts +14 -1
- package/src/lib/session.ts +9 -1
- package/src/lib/vector-store/__tests__/factory.test.ts +1 -1
- package/src/lib/vector-store/__tests__/sqlite-vec-store.test.ts +2 -2
- package/src/plugins/core/tags/__tests__/tags.test.ts +10 -10
- package/src/routes/__tests__/-edition-routing.test.tsx +4 -4
- package/src/routes/__tests__/route-tree-structure.test.ts +65 -0
- package/src/routes/_nav/knowledge/compliance/index.lazy.tsx +5 -2
- package/src/routes/_nav/knowledge/index.lazy.tsx +1 -1
- package/src/routes/_nav/knowledge/laws/index.lazy.tsx +5 -2
- package/src/routes/_nav/route.tsx +21 -0
- package/src/routes/_nav/workspaces/$workspaceId/route.lazy.tsx +13 -11
- package/src/routes/_nav/workspaces/$workspaceId/route.tsx +4 -2
- package/src/routes/_nav/workspaces/$workspaceId/settings/route.lazy.tsx +1 -1
- package/src/routes/_nav/workspaces/index.lazy.tsx +3 -3
- package/src/server/__tests__/agent-crud.test.ts +4 -4
- package/src/server/__tests__/chat-conversations.test.ts +1 -3
- package/src/server/__tests__/fetch-guard.test.ts +99 -0
- package/src/server/__tests__/permissions.test.ts +1 -3
- package/src/server/__tests__/resources.team-seats.test.ts +2 -0
- package/src/server/__tests__/skill-crud.test.ts +3 -3
- package/src/server/ai-gateway/__tests__/ai-output-guard.test.ts +95 -0
- package/src/server/ai-gateway/__tests__/kb-context-guard.test.ts +136 -0
- package/src/server/ai-gateway/__tests__/knowledge-search-fence.test.ts +240 -0
- package/src/server/ai-gateway/__tests__/llm-fence.test.ts +125 -0
- package/src/server/ai-gateway/__tests__/run-store.test.ts +52 -0
- package/src/server/ai-gateway/ai-output-guard.ts +77 -0
- package/src/server/ai-gateway/index.ts +5 -0
- package/src/server/ai-gateway/kb-context-guard.ts +46 -0
- package/src/server/ai-gateway/llm-fence.ts +82 -0
- package/src/server/ai-gateway/persistence/run-store.ts +19 -0
- package/src/server/ai-gateway/story-prompt.ts +4 -2
- package/src/server/ai-gateway/tools/definitions.ts +12 -6
- package/src/server/ai-gateway/tools/server.ts +97 -54
- package/src/server/builtin-project.ts +21 -0
- package/src/server/email/sender.ts +8 -2
- package/src/server/fetch-guard.ts +170 -0
- package/src/server/middlewares/security-headers.ts +113 -0
- package/src/server/serverFns/__tests__/billing.test.ts +263 -0
- package/src/server/serverFns/__tests__/link-preview.test.ts +239 -0
- package/src/server/serverFns/__tests__/project-kb.test.ts +3 -3
- package/src/server/serverFns/__tests__/project-team-assign.test.ts +1 -1
- package/src/server/serverFns/__tests__/story-comments.test.ts +265 -0
- package/src/server/serverFns/__tests__/story-crud-archive-detail.test.ts +266 -0
- package/src/server/serverFns/__tests__/story-crud-labels-users.test.ts +227 -0
- package/src/server/serverFns/__tests__/story-crud.test.ts +526 -0
- package/src/server/serverFns/__tests__/story-links.test.ts +262 -0
- package/src/server/serverFns/__tests__/story-list-query.test.ts +259 -0
- package/src/server/serverFns/__tests__/story-reactions.test.ts +230 -0
- package/src/server/serverFns/__tests__/story-tree.test.ts +317 -0
- package/src/server/serverFns/__tests__/story-types.test.ts +294 -0
- package/src/server/serverFns/__tests__/team-accept.test.ts +6 -6
- package/src/server/serverFns/__tests__/team-invitation.test.ts +3 -3
- package/src/server/serverFns/__tests__/team-join-request.test.ts +8 -8
- package/src/server/serverFns/__tests__/update-seats.test.ts +4 -4
- package/src/server/serverFns/__tests__/workspace-kb-idor.test.ts +160 -0
- package/src/server/serverFns/__tests__/workspace-kb-utils.test.ts +54 -0
- package/src/server/serverFns/__tests__/workspace-kb.test.ts +348 -0
- package/src/server/serverFns/__tests__/workspace.test.ts +28 -8
- package/src/server/serverFns/iteration/__tests__/iteration-crud.test.ts +3 -5
- package/src/server/serverFns/iteration/__tests__/iteration-stories.test.ts +1 -1
- package/src/server/serverFns/iteration/__tests__/iteration-transfer.test.ts +2 -2
- package/src/server/serverFns/kb-doc-search.ts +111 -0
- package/src/server/serverFns/knowledge-public/compliance/create.ts +2 -1
- package/src/server/serverFns/knowledge-public/legal/create.ts +2 -1
- package/src/server/serverFns/story/__tests__/bulk.test.ts +1 -1
- package/src/server/serverFns/story/__tests__/label-suggest-fn.test.ts +2 -6
- package/src/server/serverFns/story/__tests__/story-assoc.test.ts +2 -4
- package/src/server/serverFns/story/__tests__/story-dependencies.test.ts +2 -2
- package/src/server/serverFns/story/__tests__/story-groups.test.ts +2 -2
- package/src/server/serverFns/story/kb-general-consts.ts +39 -0
- package/src/server/serverFns/story/kb-general.ts +30 -17
- package/src/server/serverFns/story/story-assoc.ts +13 -103
- package/src/server/serverFns/story/story-crud.ts +2 -5
- package/src/server/serverFns/story/story-groups.ts +4 -13
- package/src/server/serverFns/team.ts +1 -1
- package/src/server/serverFns/workspace-kb-utils.ts +138 -0
- package/src/server/serverFns/workspace-kb.ts +403 -0
- package/src/server/serverFns/workspace.ts +4 -1
- package/src/shared/ai-cli/__tests__/adapter.test.ts +67 -0
- package/src/shared/ai-cli/__tests__/cli-detector.test.ts +99 -0
- package/src/shared/ai-cli/adapter.ts +2 -2
- package/src/shared/ai-cli/cli-detector.ts +18 -9
- package/src/start.ts +2 -1
- package/src/styles/globals.css +114 -0
- package/src/types/jest-dom-vitest.d.ts +8 -7
- package/vite.shared.ts +18 -1
- package/public/file.svg +0 -1
- package/public/globe.svg +0 -1
- package/public/window.svg +0 -1
- package/src/lib/__tests__/cli-detector.test.ts +0 -80
- /package/src/components/workspace/{workspace-activity-card.tsx → activity/workspace-activity-card.tsx} +0 -0
- /package/src/components/workspace/{workspace-contribution-card.tsx → contribution/workspace-contribution-card.tsx} +0 -0
- /package/src/components/workspace/{delete-workspace-options.tsx → dialogs/delete-workspace-options.tsx} +0 -0
- /package/src/components/workspace/{workspace-header.tsx → header/workspace-header.tsx} +0 -0
- /package/src/components/workspace/{workspace-member-item.tsx → members/workspace-member-item.tsx} +0 -0
- /package/src/components/workspace/{workspace-project-list.tsx → projects/workspace-project-list.tsx} +0 -0
|
@@ -0,0 +1,54 @@
|
|
|
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
|
+
import { describe, it, expect } from "vitest";
|
|
19
|
+
import { quickDocItemSchema, quickDocItemKey, generalDocHref } from "../workspace-kb-utils";
|
|
20
|
+
|
|
21
|
+
describe("quickDocItemSchema 互斥校验", () => {
|
|
22
|
+
it("docId 单独存在合法", () => {
|
|
23
|
+
expect(quickDocItemSchema.parse({ docId: "d1", displayTitle: "标题" })).toMatchObject({ docId: "d1" });
|
|
24
|
+
});
|
|
25
|
+
it("externalGroup+externalId 合法", () => {
|
|
26
|
+
expect(
|
|
27
|
+
quickDocItemSchema.parse({ externalGroup: "legal", externalId: "x1", displayTitle: "标题" }),
|
|
28
|
+
).toMatchObject({ externalGroup: "legal" });
|
|
29
|
+
});
|
|
30
|
+
it("docId 与 external 同时存在拒绝", () => {
|
|
31
|
+
expect(() =>
|
|
32
|
+
quickDocItemSchema.parse({ docId: "d1", externalGroup: "legal", externalId: "x1", displayTitle: "标题" }),
|
|
33
|
+
).toThrow();
|
|
34
|
+
});
|
|
35
|
+
it("两者皆无拒绝", () => {
|
|
36
|
+
expect(() => quickDocItemSchema.parse({ displayTitle: "标题" })).toThrow();
|
|
37
|
+
});
|
|
38
|
+
it("空标题拒绝", () => {
|
|
39
|
+
expect(() => quickDocItemSchema.parse({ docId: "d1", displayTitle: "" })).toThrow();
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe("quickDocItemKey / generalDocHref", () => {
|
|
44
|
+
it("doc 态返回 docId", () => {
|
|
45
|
+
expect(quickDocItemKey({ docId: "d1", externalGroup: undefined, externalId: undefined })).toBe("d1");
|
|
46
|
+
});
|
|
47
|
+
it("ext 态返回 ext: 前缀 id", () => {
|
|
48
|
+
expect(quickDocItemKey({ docId: undefined, externalGroup: "legal", externalId: "x1" })).toBe("ext:legal:x1");
|
|
49
|
+
});
|
|
50
|
+
it("href 映射", () => {
|
|
51
|
+
expect(generalDocHref("legal", "9")).toBe("/knowledge/laws/9");
|
|
52
|
+
expect(generalDocHref("summary", "9")).toBe("/knowledge/summary");
|
|
53
|
+
});
|
|
54
|
+
});
|
|
@@ -0,0 +1,348 @@
|
|
|
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
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
19
|
+
|
|
20
|
+
vi.mock("@tanstack/react-start", () => ({
|
|
21
|
+
createServerFn: (config: any) => {
|
|
22
|
+
const api: any = {
|
|
23
|
+
_validator: undefined,
|
|
24
|
+
validator(schema: any) {
|
|
25
|
+
this._validator = schema;
|
|
26
|
+
return this;
|
|
27
|
+
},
|
|
28
|
+
handler(fn: Function) {
|
|
29
|
+
return async (args: any) => {
|
|
30
|
+
let data = args?.data;
|
|
31
|
+
if (this._validator && data !== undefined) {
|
|
32
|
+
data = this._validator.parse ? this._validator.parse(data) : data;
|
|
33
|
+
}
|
|
34
|
+
return fn({ data });
|
|
35
|
+
};
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
return api;
|
|
39
|
+
},
|
|
40
|
+
}));
|
|
41
|
+
vi.mock("@tanstack/react-start/server", () => ({ getRequest: () => ({}) }));
|
|
42
|
+
|
|
43
|
+
const mocks = vi.hoisted(() => ({
|
|
44
|
+
currentSession: vi.fn(),
|
|
45
|
+
wsFindUnique: vi.fn(),
|
|
46
|
+
wkbFindMany: vi.fn(),
|
|
47
|
+
wkbDeleteMany: vi.fn(),
|
|
48
|
+
wkbCreateMany: vi.fn(),
|
|
49
|
+
wqdFindMany: vi.fn(),
|
|
50
|
+
wqdFindUnique: vi.fn(),
|
|
51
|
+
wqdUpdate: vi.fn(),
|
|
52
|
+
wqdDelete: vi.fn(),
|
|
53
|
+
wqdAggregate: vi.fn(),
|
|
54
|
+
wqdCreate: vi.fn(),
|
|
55
|
+
wqdFindFirst: vi.fn(),
|
|
56
|
+
legalFindMany: vi.fn(),
|
|
57
|
+
kbFindMany: vi.fn(),
|
|
58
|
+
kbFindFirst: vi.fn(),
|
|
59
|
+
transaction: vi.fn(),
|
|
60
|
+
docFindMany: vi.fn(),
|
|
61
|
+
docFindFirst: vi.fn(),
|
|
62
|
+
articleFavoriteFindMany: vi.fn(),
|
|
63
|
+
}));
|
|
64
|
+
|
|
65
|
+
vi.mock("@allbluecn/database", () => {
|
|
66
|
+
const prisma = {
|
|
67
|
+
workspace: { findUnique: mocks.wsFindUnique },
|
|
68
|
+
workspaceKnowledgeBase: { findMany: mocks.wkbFindMany, deleteMany: mocks.wkbDeleteMany, createMany: mocks.wkbCreateMany },
|
|
69
|
+
workspaceQuickDoc: {
|
|
70
|
+
findMany: mocks.wqdFindMany,
|
|
71
|
+
findUnique: mocks.wqdFindUnique,
|
|
72
|
+
update: mocks.wqdUpdate,
|
|
73
|
+
delete: mocks.wqdDelete,
|
|
74
|
+
aggregate: mocks.wqdAggregate,
|
|
75
|
+
create: mocks.wqdCreate,
|
|
76
|
+
findFirst: mocks.wqdFindFirst,
|
|
77
|
+
},
|
|
78
|
+
legalRegulation: { findMany: mocks.legalFindMany },
|
|
79
|
+
knowledgeBase: { findMany: mocks.kbFindMany, findFirst: mocks.kbFindFirst },
|
|
80
|
+
knowledgeDocument: { findMany: mocks.docFindMany, findFirst: mocks.docFindFirst },
|
|
81
|
+
articleFavorite: { findMany: mocks.articleFavoriteFindMany },
|
|
82
|
+
$transaction: mocks.transaction,
|
|
83
|
+
};
|
|
84
|
+
return { prisma, Prisma: {} };
|
|
85
|
+
});
|
|
86
|
+
vi.mock("@/server/auth-helpers", () => ({ requireAuth: mocks.currentSession }));
|
|
87
|
+
|
|
88
|
+
// 默认 transaction mock:调用回调并透传 tx 对象
|
|
89
|
+
beforeEach(() => {
|
|
90
|
+
mocks.transaction.mockImplementation(async (fn: (tx: any) => any) =>
|
|
91
|
+
fn({ workspaceKnowledgeBase: { deleteMany: mocks.wkbDeleteMany, createMany: mocks.wkbCreateMany } }),
|
|
92
|
+
);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
import {
|
|
96
|
+
getWorkspaceKbCardFn,
|
|
97
|
+
listWorkspaceAvailableKbsFn,
|
|
98
|
+
setWorkspaceKbsFn,
|
|
99
|
+
searchQuickDocCandidatesFn,
|
|
100
|
+
addQuickDocsFn,
|
|
101
|
+
renameQuickDocFn,
|
|
102
|
+
removeQuickDocFn,
|
|
103
|
+
reorderQuickDocsFn,
|
|
104
|
+
} from "../workspace-kb";
|
|
105
|
+
|
|
106
|
+
const MEMBER_WS = { ownerId: "u1", members: [] };
|
|
107
|
+
const MEMBER_AS_MEMBER = { ownerId: "other", members: [{ id: "m1" }] };
|
|
108
|
+
const NON_MEMBER = { ownerId: "other", members: [] };
|
|
109
|
+
|
|
110
|
+
beforeEach(() => {
|
|
111
|
+
vi.clearAllMocks();
|
|
112
|
+
mocks.currentSession.mockResolvedValue({ user: { id: "u1" } });
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
describe("getWorkspaceKbCardFn", () => {
|
|
116
|
+
it("非成员拒绝", async () => {
|
|
117
|
+
mocks.wsFindUnique.mockResolvedValue(NON_MEMBER);
|
|
118
|
+
await expect(
|
|
119
|
+
getWorkspaceKbCardFn({ data: { workspaceId: "ws1" } }),
|
|
120
|
+
).rejects.toThrow("成员");
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("返回 kbs + quickDocs(doc 态带 kb 信息、ext 态标题兜底)", async () => {
|
|
124
|
+
mocks.wsFindUnique.mockResolvedValue(MEMBER_WS);
|
|
125
|
+
mocks.wkbFindMany.mockResolvedValue([
|
|
126
|
+
{ kb: { id: "kb1", name: "团队库", icon: "notebook", visibility: "team" } },
|
|
127
|
+
]);
|
|
128
|
+
mocks.wqdFindMany.mockResolvedValue([
|
|
129
|
+
{
|
|
130
|
+
id: "q1", displayTitle: "接入指南", sortOrder: 0, docId: "d1",
|
|
131
|
+
externalGroup: null, externalId: null,
|
|
132
|
+
doc: { title: "接入指南", icon: "doc", isDeleted: false, kb: { id: "kb1", name: "团队库" } },
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
id: "q2", displayTitle: "数据安全法", sortOrder: 1, docId: null,
|
|
136
|
+
externalGroup: "legal", externalId: "x9",
|
|
137
|
+
doc: null,
|
|
138
|
+
},
|
|
139
|
+
]);
|
|
140
|
+
mocks.legalFindMany.mockResolvedValue([{ id: "x9", title: "数据安全法(现行)" }]);
|
|
141
|
+
const res = await getWorkspaceKbCardFn({ data: { workspaceId: "ws1" } });
|
|
142
|
+
expect(res.kbs).toHaveLength(1);
|
|
143
|
+
expect(res.quickDocs[0]).toMatchObject({ source: "doc", kbId: "kb1", stale: false });
|
|
144
|
+
expect(res.quickDocs[1]).toMatchObject({
|
|
145
|
+
source: "general", group: "legal", externalId: "x9",
|
|
146
|
+
sourceTitle: "数据安全法(现行)", stale: false,
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("外部条目源已删除 → stale: true 且仍返回", async () => {
|
|
151
|
+
mocks.wsFindUnique.mockResolvedValue(MEMBER_AS_MEMBER);
|
|
152
|
+
mocks.wkbFindMany.mockResolvedValue([]);
|
|
153
|
+
mocks.wqdFindMany.mockResolvedValue([
|
|
154
|
+
{ id: "q3", displayTitle: "旧法条", sortOrder: 0, docId: null, externalGroup: "legal", externalId: "gone", doc: null },
|
|
155
|
+
]);
|
|
156
|
+
mocks.legalFindMany.mockResolvedValue([]); // 源查不到
|
|
157
|
+
const res = await getWorkspaceKbCardFn({ data: { workspaceId: "ws1" } });
|
|
158
|
+
expect(res.quickDocs[0]).toMatchObject({ stale: true, displayTitle: "旧法条" });
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
describe("listWorkspaceAvailableKbsFn", () => {
|
|
163
|
+
it("返回已关联 ∪ 用户 team 库,带 linked 标记", async () => {
|
|
164
|
+
mocks.wsFindUnique.mockResolvedValue(MEMBER_WS);
|
|
165
|
+
mocks.wkbFindMany.mockResolvedValue([{ kbId: "kbLinked" }]);
|
|
166
|
+
mocks.kbFindMany.mockResolvedValue([
|
|
167
|
+
{ id: "kbLinked", name: "他人的库", icon: "notebook", visibility: "team" },
|
|
168
|
+
{ id: "kbMine", name: "我的库", icon: "notebook", visibility: "team" },
|
|
169
|
+
]);
|
|
170
|
+
const res = await listWorkspaceAvailableKbsFn({ data: { workspaceId: "ws1" } });
|
|
171
|
+
expect(res.find((k: any) => k.id === "kbLinked")?.linked).toBe(true);
|
|
172
|
+
expect(res.find((k: any) => k.id === "kbMine")?.linked).toBe(false);
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
describe("setWorkspaceKbsFn", () => {
|
|
177
|
+
it("非 team 库拒绝且不写库", async () => {
|
|
178
|
+
mocks.wsFindUnique.mockResolvedValue(MEMBER_WS);
|
|
179
|
+
mocks.kbFindMany.mockResolvedValue([{ id: "kb1" }]); // 请求 2 个只查回 1 个
|
|
180
|
+
await expect(
|
|
181
|
+
setWorkspaceKbsFn({ data: { workspaceId: "ws1", kbIds: ["kb1", "kb2"] } }),
|
|
182
|
+
).rejects.toThrow("团队可见");
|
|
183
|
+
expect(mocks.transaction).not.toHaveBeenCalled();
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("全量同步:删差集 + 增新增", async () => {
|
|
187
|
+
mocks.wsFindUnique.mockResolvedValue(MEMBER_WS);
|
|
188
|
+
mocks.kbFindMany.mockResolvedValue([{ id: "kb1" }, { id: "kb2" }]);
|
|
189
|
+
await setWorkspaceKbsFn({ data: { workspaceId: "ws1", kbIds: ["kb1", "kb2"] } });
|
|
190
|
+
expect(mocks.wkbDeleteMany).toHaveBeenCalledWith({
|
|
191
|
+
where: { workspaceId: "ws1", kbId: { notIn: ["kb1", "kb2"] } },
|
|
192
|
+
});
|
|
193
|
+
expect(mocks.wkbCreateMany).toHaveBeenCalledWith({
|
|
194
|
+
data: [
|
|
195
|
+
{ workspaceId: "ws1", kbId: "kb1" },
|
|
196
|
+
{ workspaceId: "ws1", kbId: "kb2" },
|
|
197
|
+
],
|
|
198
|
+
skipDuplicates: true,
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
describe("searchQuickDocCandidatesFn", () => {
|
|
204
|
+
it("kbId 态返回该库文档树(须属用户 team 库)", async () => {
|
|
205
|
+
mocks.wsFindUnique.mockResolvedValue(MEMBER_WS);
|
|
206
|
+
mocks.kbFindFirst.mockResolvedValue({ id: "kb1" });
|
|
207
|
+
mocks.docFindMany.mockResolvedValue([
|
|
208
|
+
{ id: "d1", title: "接入指南", parentId: null, icon: "doc", kb: { id: "kb1", name: "团队库", icon: "notebook" } },
|
|
209
|
+
]);
|
|
210
|
+
const res = await searchQuickDocCandidatesFn({
|
|
211
|
+
data: { workspaceId: "ws1", kbId: "kb1", query: "" },
|
|
212
|
+
});
|
|
213
|
+
expect(res.items[0]).toMatchObject({ id: "d1", kbId: "kb1", parentId: null });
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it("generalGroup 态走通用表(含 ext 前缀 id)", async () => {
|
|
217
|
+
mocks.wsFindUnique.mockResolvedValue(MEMBER_WS);
|
|
218
|
+
mocks.articleFavoriteFindMany.mockResolvedValue([]);
|
|
219
|
+
mocks.legalFindMany.mockResolvedValue([{ id: "x9", title: "数据安全法" }]);
|
|
220
|
+
const res = await searchQuickDocCandidatesFn({
|
|
221
|
+
data: { workspaceId: "ws1", generalGroup: "legal", query: "" },
|
|
222
|
+
});
|
|
223
|
+
expect(res.items[0]).toMatchObject({ id: "ext:legal:x9", title: "数据安全法" });
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
describe("addQuickDocsFn", () => {
|
|
228
|
+
it("互斥违规被 validator 拒绝", async () => {
|
|
229
|
+
await expect(
|
|
230
|
+
addQuickDocsFn({
|
|
231
|
+
data: {
|
|
232
|
+
workspaceId: "ws1",
|
|
233
|
+
items: [{ docId: "d1", externalGroup: "legal", externalId: "x1", displayTitle: "x" }],
|
|
234
|
+
},
|
|
235
|
+
}),
|
|
236
|
+
).rejects.toThrow("二选一");
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it("文档不存在 → failed outOfScope;存在 → 创建且落到最短列末尾", async () => {
|
|
240
|
+
mocks.wsFindUnique.mockResolvedValue(MEMBER_WS);
|
|
241
|
+
// 现有布局:col0 有 2 行(max sortOrder 1)、col1 有 1 行(max sortOrder 0)
|
|
242
|
+
mocks.wqdFindMany.mockResolvedValue([
|
|
243
|
+
{ columnIndex: 0, sortOrder: 0 },
|
|
244
|
+
{ columnIndex: 0, sortOrder: 1 },
|
|
245
|
+
{ columnIndex: 1, sortOrder: 0 },
|
|
246
|
+
]);
|
|
247
|
+
mocks.docFindFirst.mockResolvedValueOnce(null) // d1 不存在
|
|
248
|
+
.mockResolvedValueOnce({ id: "d2" }); // d2 存在
|
|
249
|
+
mocks.wqdFindFirst.mockResolvedValue(null); // 无重复
|
|
250
|
+
mocks.wqdCreate.mockImplementation(async (args: any) => ({ id: "new-" + args.data.docId }));
|
|
251
|
+
const res = await addQuickDocsFn({
|
|
252
|
+
data: {
|
|
253
|
+
workspaceId: "ws1",
|
|
254
|
+
items: [
|
|
255
|
+
{ docId: "d1", displayTitle: "失效文档" },
|
|
256
|
+
{ docId: "d2", displayTitle: "有效文档" },
|
|
257
|
+
],
|
|
258
|
+
},
|
|
259
|
+
});
|
|
260
|
+
expect(res.failed).toEqual([{ key: "d1", reason: "outOfScope" }]);
|
|
261
|
+
expect(res.created).toEqual(["d2"]);
|
|
262
|
+
// col2/3/4 为空列(0 行)比 col1(1 行)更短,新文档落到第一个空列 col2 末尾
|
|
263
|
+
expect(mocks.wqdCreate).toHaveBeenCalledWith(
|
|
264
|
+
expect.objectContaining({
|
|
265
|
+
data: expect.objectContaining({ docId: "d2", columnIndex: 2, sortOrder: 0 }),
|
|
266
|
+
}),
|
|
267
|
+
);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it("summary 组外部条目拒绝(无实体表)", async () => {
|
|
271
|
+
mocks.wsFindUnique.mockResolvedValue(MEMBER_WS);
|
|
272
|
+
const res = await addQuickDocsFn({
|
|
273
|
+
data: { workspaceId: "ws1", items: [{ externalGroup: "summary", externalId: "s1", displayTitle: "周报" }] },
|
|
274
|
+
});
|
|
275
|
+
expect(res.failed).toEqual([{ key: "ext:summary:s1", reason: "outOfScope" }]);
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
describe("renameQuickDocFn", () => {
|
|
280
|
+
it("非本工作空间成员拒绝", async () => {
|
|
281
|
+
mocks.wqdFindUnique.mockResolvedValue({ workspaceId: "ws1" });
|
|
282
|
+
mocks.wsFindUnique.mockResolvedValue(NON_MEMBER);
|
|
283
|
+
await expect(
|
|
284
|
+
renameQuickDocFn({ data: { quickDocId: "q1", displayTitle: "新标题" } }),
|
|
285
|
+
).rejects.toThrow("成员");
|
|
286
|
+
expect(mocks.wqdUpdate).not.toHaveBeenCalled();
|
|
287
|
+
});
|
|
288
|
+
it("成员重命名成功", async () => {
|
|
289
|
+
mocks.wqdFindUnique.mockResolvedValue({ workspaceId: "ws1" });
|
|
290
|
+
mocks.wsFindUnique.mockResolvedValue(MEMBER_WS);
|
|
291
|
+
await renameQuickDocFn({ data: { quickDocId: "q1", displayTitle: "新标题" } });
|
|
292
|
+
expect(mocks.wqdUpdate).toHaveBeenCalledWith({
|
|
293
|
+
where: { id: "q1" },
|
|
294
|
+
data: { displayTitle: "新标题" },
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
describe("removeQuickDocFn", () => {
|
|
300
|
+
it("成员删除成功", async () => {
|
|
301
|
+
mocks.wqdFindUnique.mockResolvedValue({ workspaceId: "ws1" });
|
|
302
|
+
mocks.wsFindUnique.mockResolvedValue(MEMBER_AS_MEMBER);
|
|
303
|
+
await removeQuickDocFn({ data: { quickDocId: "q1" } });
|
|
304
|
+
expect(mocks.wqdDelete).toHaveBeenCalledWith({ where: { id: "q1" } });
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
describe("reorderQuickDocsFn", () => {
|
|
309
|
+
it("包含非本工作空间 id 拒绝", async () => {
|
|
310
|
+
mocks.wsFindUnique.mockResolvedValue(MEMBER_WS);
|
|
311
|
+
mocks.wqdFindMany.mockResolvedValue([{ id: "q1" }]); // 请求 2 个只属 1 个
|
|
312
|
+
await expect(
|
|
313
|
+
reorderQuickDocsFn({
|
|
314
|
+
data: {
|
|
315
|
+
workspaceId: "ws1",
|
|
316
|
+
items: [
|
|
317
|
+
{ id: "q1", columnIndex: 0, sortOrder: 0 },
|
|
318
|
+
{ id: "qX", columnIndex: 1, sortOrder: 0 },
|
|
319
|
+
],
|
|
320
|
+
},
|
|
321
|
+
}),
|
|
322
|
+
).rejects.toThrow("不属于");
|
|
323
|
+
});
|
|
324
|
+
it("按传入的列归属与顺序重写 columnIndex/sortOrder", async () => {
|
|
325
|
+
mocks.wsFindUnique.mockResolvedValue(MEMBER_WS);
|
|
326
|
+
mocks.wqdFindMany.mockResolvedValue([{ id: "q1" }, { id: "q2" }]);
|
|
327
|
+
const updates: any[] = [];
|
|
328
|
+
mocks.wqdUpdate.mockImplementation(async (args: any) => { updates.push(args); return args; });
|
|
329
|
+
mocks.transaction.mockImplementation(async (ops: any[]) => Promise.all(ops.map((op) => op)));
|
|
330
|
+
await reorderQuickDocsFn({
|
|
331
|
+
data: {
|
|
332
|
+
workspaceId: "ws1",
|
|
333
|
+
items: [
|
|
334
|
+
{ id: "q2", columnIndex: 1, sortOrder: 0 },
|
|
335
|
+
{ id: "q1", columnIndex: 2, sortOrder: 3 },
|
|
336
|
+
],
|
|
337
|
+
},
|
|
338
|
+
});
|
|
339
|
+
expect(mocks.wqdUpdate).toHaveBeenNthCalledWith(1, {
|
|
340
|
+
where: { id: "q2" },
|
|
341
|
+
data: { columnIndex: 1, sortOrder: 0 },
|
|
342
|
+
});
|
|
343
|
+
expect(mocks.wqdUpdate).toHaveBeenNthCalledWith(2, {
|
|
344
|
+
where: { id: "q1" },
|
|
345
|
+
data: { columnIndex: 2, sortOrder: 3 },
|
|
346
|
+
});
|
|
347
|
+
});
|
|
348
|
+
});
|
|
@@ -68,6 +68,7 @@ const mocks = vi.hoisted(() => ({
|
|
|
68
68
|
knowledgeBaseDeleteMany: vi.fn(),
|
|
69
69
|
knowledgeBaseUpdateMany: vi.fn(),
|
|
70
70
|
knowledgeBaseCreate: vi.fn(),
|
|
71
|
+
wkbCreate: vi.fn(),
|
|
71
72
|
usageRecordDeleteMany: vi.fn(),
|
|
72
73
|
aiUsageLogDeleteMany: vi.fn(),
|
|
73
74
|
ssoProviderDeleteMany: vi.fn(),
|
|
@@ -132,6 +133,9 @@ vi.mock("@allbluecn/database", () => ({
|
|
|
132
133
|
deleteMany: mocks.knowledgeBaseDeleteMany,
|
|
133
134
|
updateMany: mocks.knowledgeBaseUpdateMany,
|
|
134
135
|
},
|
|
136
|
+
workspaceKnowledgeBase: {
|
|
137
|
+
create: mocks.wkbCreate,
|
|
138
|
+
},
|
|
135
139
|
usageRecord: {
|
|
136
140
|
deleteMany: mocks.usageRecordDeleteMany,
|
|
137
141
|
},
|
|
@@ -161,6 +165,9 @@ vi.mock("@allbluecn/database", () => ({
|
|
|
161
165
|
deleteMany: mocks.knowledgeBaseDeleteMany,
|
|
162
166
|
updateMany: mocks.knowledgeBaseUpdateMany,
|
|
163
167
|
},
|
|
168
|
+
workspaceKnowledgeBase: {
|
|
169
|
+
create: mocks.wkbCreate,
|
|
170
|
+
},
|
|
164
171
|
usageRecord: {
|
|
165
172
|
deleteMany: mocks.usageRecordDeleteMany,
|
|
166
173
|
},
|
|
@@ -215,6 +222,7 @@ describe("createWorkspaceFn", () => {
|
|
|
215
222
|
it("创建工作空间后 owner 即为所有者", async () => {
|
|
216
223
|
const ws = { id: "ws1", name: "测试空间", icon: "folder", ownerId: "u1" };
|
|
217
224
|
mocks.workspaceCreate.mockResolvedValue(ws);
|
|
225
|
+
mocks.knowledgeBaseCreate.mockResolvedValue({ id: "kb1" });
|
|
218
226
|
|
|
219
227
|
const result = await createWorkspaceFn({
|
|
220
228
|
data: { name: "测试空间", icon: "folder", description: "介绍" },
|
|
@@ -231,7 +239,7 @@ describe("createWorkspaceFn", () => {
|
|
|
231
239
|
|
|
232
240
|
await expect(
|
|
233
241
|
createWorkspaceFn({ data: { name: "测试" } }),
|
|
234
|
-
).rejects.
|
|
242
|
+
).rejects.toThrow("未认证");
|
|
235
243
|
});
|
|
236
244
|
|
|
237
245
|
it("名称为空时 zod validator 拦截", async () => {
|
|
@@ -239,6 +247,18 @@ describe("createWorkspaceFn", () => {
|
|
|
239
247
|
createWorkspaceFn({ data: { name: "", icon: "folder" } }),
|
|
240
248
|
).rejects.toThrow();
|
|
241
249
|
});
|
|
250
|
+
|
|
251
|
+
it("创建工作空间时事务内自动关联同步创建的知识库", async () => {
|
|
252
|
+
mocks.knowledgeBaseCreate.mockResolvedValue({ id: "kb1" });
|
|
253
|
+
const ws = { id: "ws1", name: "测试空间", icon: "folder", ownerId: "u1" };
|
|
254
|
+
mocks.workspaceCreate.mockResolvedValue(ws);
|
|
255
|
+
mocks.wkbCreate.mockResolvedValue({ id: "wkb1" });
|
|
256
|
+
|
|
257
|
+
await createWorkspaceFn({ data: { name: "测试空间" } });
|
|
258
|
+
expect(mocks.wkbCreate).toHaveBeenCalledWith({
|
|
259
|
+
data: { workspaceId: "ws1", kbId: "kb1" },
|
|
260
|
+
});
|
|
261
|
+
});
|
|
242
262
|
});
|
|
243
263
|
|
|
244
264
|
describe("listWorkspacesFn", () => {
|
|
@@ -312,7 +332,7 @@ describe("getWorkspaceDetailFn", () => {
|
|
|
312
332
|
|
|
313
333
|
await expect(
|
|
314
334
|
getWorkspaceDetailFn({ data: { workspaceId: "ws1" } }),
|
|
315
|
-
).rejects.
|
|
335
|
+
).rejects.toThrow("工作空间不存在");
|
|
316
336
|
expect(mocks.workspaceFindUnique).toHaveBeenCalledTimes(1);
|
|
317
337
|
});
|
|
318
338
|
|
|
@@ -321,7 +341,7 @@ describe("getWorkspaceDetailFn", () => {
|
|
|
321
341
|
|
|
322
342
|
await expect(
|
|
323
343
|
getWorkspaceDetailFn({ data: { workspaceId: "ws1" } }),
|
|
324
|
-
).rejects.
|
|
344
|
+
).rejects.toThrow("仅工作空间所有者可访问");
|
|
325
345
|
});
|
|
326
346
|
});
|
|
327
347
|
|
|
@@ -350,7 +370,7 @@ describe("listWorkspaceProjectsFn", () => {
|
|
|
350
370
|
|
|
351
371
|
await expect(
|
|
352
372
|
listWorkspaceProjectsFn({ data: { workspaceId: "ws1" } }),
|
|
353
|
-
).rejects.
|
|
373
|
+
).rejects.toThrow("仅工作空间所有者可访问");
|
|
354
374
|
});
|
|
355
375
|
});
|
|
356
376
|
|
|
@@ -443,7 +463,7 @@ describe("deleteWorkspaceFn", () => {
|
|
|
443
463
|
|
|
444
464
|
await expect(
|
|
445
465
|
deleteWorkspaceFn({ data: { workspaceId: "ws1", mode: "transfer" } }),
|
|
446
|
-
).rejects.
|
|
466
|
+
).rejects.toThrow("转移模式必须指定目标工作空间");
|
|
447
467
|
expect(mocks.workspaceDelete).not.toHaveBeenCalled();
|
|
448
468
|
});
|
|
449
469
|
|
|
@@ -455,7 +475,7 @@ describe("deleteWorkspaceFn", () => {
|
|
|
455
475
|
deleteWorkspaceFn({
|
|
456
476
|
data: { workspaceId: "ws1", mode: "transfer", targetWorkspaceId: "ws1" },
|
|
457
477
|
}),
|
|
458
|
-
).rejects.
|
|
478
|
+
).rejects.toThrow("不可转移到自身");
|
|
459
479
|
expect(mocks.workspaceDelete).not.toHaveBeenCalled();
|
|
460
480
|
});
|
|
461
481
|
|
|
@@ -482,7 +502,7 @@ describe("deleteWorkspaceFn", () => {
|
|
|
482
502
|
|
|
483
503
|
await expect(
|
|
484
504
|
deleteWorkspaceFn({ data: { workspaceId: "ws1" } }),
|
|
485
|
-
).rejects.
|
|
505
|
+
).rejects.toThrow("仅工作空间所有者可访问");
|
|
486
506
|
});
|
|
487
507
|
});
|
|
488
508
|
|
|
@@ -514,6 +534,6 @@ describe("updateWorkspaceFn", () => {
|
|
|
514
534
|
updateWorkspaceFn({
|
|
515
535
|
data: { workspaceId: "ws1", name: "新名称" },
|
|
516
536
|
}),
|
|
517
|
-
).rejects.
|
|
537
|
+
).rejects.toThrow("仅工作空间所有者可访问");
|
|
518
538
|
});
|
|
519
539
|
});
|
|
@@ -95,7 +95,7 @@ describe("createIterationsFn", () => {
|
|
|
95
95
|
]);
|
|
96
96
|
await expect(
|
|
97
97
|
createIterationsFn({ data: { projectId: "p1", name: "S2", startDate: "2026-09-10", endDate: "2026-09-23" } }),
|
|
98
|
-
).rejects.
|
|
98
|
+
).rejects.toThrow("重叠");
|
|
99
99
|
});
|
|
100
100
|
|
|
101
101
|
it("合法日期调用 create 且日期按 UTC 0 点入库", async () => {
|
|
@@ -130,9 +130,7 @@ describe("updateIterationsFn", () => {
|
|
|
130
130
|
archived: false,
|
|
131
131
|
projectId: "p1",
|
|
132
132
|
});
|
|
133
|
-
await expect(updateIterationsFn({ data: { id: "i1", name: "改名" } })).rejects.
|
|
134
|
-
message: expect.stringContaining("不可编辑"),
|
|
135
|
-
});
|
|
133
|
+
await expect(updateIterationsFn({ data: { id: "i1", name: "改名" } })).rejects.toThrow("不可编辑");
|
|
136
134
|
});
|
|
137
135
|
|
|
138
136
|
it("改期时校验与其他迭代不重叠", async () => {
|
|
@@ -148,7 +146,7 @@ describe("updateIterationsFn", () => {
|
|
|
148
146
|
]);
|
|
149
147
|
await expect(
|
|
150
148
|
updateIterationsFn({ data: { id: "i1", startDate: "2026-09-18", endDate: "2026-09-25" } }),
|
|
151
|
-
).rejects.
|
|
149
|
+
).rejects.toThrow("重叠");
|
|
152
150
|
});
|
|
153
151
|
});
|
|
154
152
|
|
|
@@ -83,7 +83,7 @@ describe("setStoryIterationFn", () => {
|
|
|
83
83
|
});
|
|
84
84
|
await expect(
|
|
85
85
|
setStoryIterationFn({ data: { storyId: "s1", iterationId: "i1" } }),
|
|
86
|
-
).rejects.
|
|
86
|
+
).rejects.toThrow("已完成");
|
|
87
87
|
});
|
|
88
88
|
|
|
89
89
|
it("合法目标更新 story 并记录活动", async () => {
|
|
@@ -85,7 +85,7 @@ describe("transferUnfinishedFn", () => {
|
|
|
85
85
|
});
|
|
86
86
|
await expect(
|
|
87
87
|
transferUnfinishedFn({ data: { iterationId: "src1", targetIterationId: "tgt1" } }),
|
|
88
|
-
).rejects.
|
|
88
|
+
).rejects.toThrow("已完成");
|
|
89
89
|
});
|
|
90
90
|
|
|
91
91
|
it("where 断言 state notIn 且 iterationId = 源迭代", async () => {
|
|
@@ -118,7 +118,7 @@ describe("transferUnfinishedFn", () => {
|
|
|
118
118
|
});
|
|
119
119
|
await expect(
|
|
120
120
|
transferUnfinishedFn({ data: { iterationId: "src1", targetIterationId: "tgt1" } }),
|
|
121
|
-
).rejects.
|
|
121
|
+
).rejects.toThrow("迭代不存在");
|
|
122
122
|
});
|
|
123
123
|
|
|
124
124
|
it("二次调用(无未完成)返回 { count: 0 },不抛错", async () => {
|