@allbluecn/web-app 0.22.0 → 0.22.3
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 +6 -8
- package/public/logo/allblue-logo-app-icon.svg +1 -1
- package/src/components/changelog/changelog-page.tsx +24 -5
- package/src/components/desktop/cloud-login.tsx +91 -0
- package/src/components/desktop/conflict-center.tsx +148 -0
- package/src/components/desktop/renamed-toast.tsx +23 -0
- package/src/components/desktop/shell-widgets.tsx +19 -0
- package/src/components/desktop/sync-indicator.tsx +60 -0
- package/src/components/desktop/sync-status-card.tsx +60 -0
- package/src/components/layout/sidebar-03/app-sidebar.tsx +38 -3
- package/src/components/shared-kit/avatars/__tests__/avatar-shape.test.ts +17 -1
- package/src/components/shared-kit/avatars/avatar-shape.ts +10 -0
- package/src/components/shared-kit/avatars/user-avatar.tsx +2 -2
- package/src/lib/auth/auth.config.ts +4 -1
- package/src/lib/changelog/sdk-versions.ts +33 -2
- package/src/lib/desktop-bridge-types.ts +30 -0
- package/src/lib/desktop-detect.ts +2 -0
- package/src/lib/vector-store/__tests__/factory.test.ts +0 -7
- package/src/lib/vector-store/index.ts +2 -4
- package/src/paraglide/messages/_index.d.ts +1 -1
- package/src/paraglide/messages/_index.js +32 -16
- package/src/paraglide/messages/common_sidebar_usermenu_loginsaas3.d.ts +1 -1
- package/src/paraglide/messages/common_sidebar_usermenu_loginsaas3.js +3 -3
- package/src/paraglide/messages/en.js +11 -6
- package/src/paraglide/messages/{tasks_ai_desktop_notice.d.ts → settings_tabs_sync.d.ts} +5 -5
- package/src/paraglide/messages/settings_tabs_sync.js +29 -0
- package/src/paraglide/messages/zh.js +11 -6
- package/src/providers/index.tsx +6 -0
- package/src/routeTree.gen.ts +107 -49
- package/src/routes/_nav/route.lazy.tsx +12 -0
- package/src/routes/_nav/settings/-settings-tabs.tsx +1 -1
- package/src/routes/_nav/settings/sync/route.lazy.tsx +40 -0
- package/src/routes/_nav/settings/sync/route.tsx +15 -0
- package/src/routes/_nav/tasks/index.lazy.tsx +1 -11
- package/src/routes/api/billing-webhook.ts +6 -2
- package/src/routes/api/sync/bootstrap.ts +38 -0
- package/src/routes/api/sync/pull.ts +96 -0
- package/src/routes/api/sync/push.ts +169 -0
- package/src/routes/api/sync/snapshot.ts +104 -0
- package/src/server/__tests__/fetch-guard.test.ts +2 -2
- package/src/server/__tests__/resource-registry.test.ts +4 -2
- package/src/server/__tests__/sandbox-entitlement.test.ts +5 -0
- package/src/server/__tests__/security-headers.test.ts +25 -0
- package/src/server/__tests__/task-assigned-query.test.ts +0 -11
- package/src/server/__tests__/task-serverfn-crud.test.ts +1 -22
- package/src/server/__tests__/task-serverfn-queries.test.ts +1 -9
- package/src/server/ai-gateway/__tests__/chat-stream-builder-memory.test.ts +0 -11
- package/src/server/ai-gateway/__tests__/embedding-request.test.ts +1 -1
- package/src/server/ai-gateway/__tests__/provider-adapter.test.ts +2 -2
- package/src/server/ai-gateway/__tests__/run-store-durable.test.ts +1 -1
- package/src/server/ai-gateway/durability/__tests__/pg-stream.test.ts +1 -1
- package/src/server/ai-gateway/durability/__tests__/reaper-real-db.test.ts +1 -1
- package/src/server/ai-gateway/memory/index.ts +2 -2
- package/src/server/ai-gateway/sandbox/__tests__/instance-store.test.ts +1 -1
- package/src/server/ai-task/assigned-query.ts +4 -15
- package/src/server/desktop-guest.ts +12 -0
- package/src/server/fetch-guard.ts +3 -3
- package/src/server/middlewares/desktop-token.ts +23 -0
- package/src/server/remote-auth.ts +6 -0
- package/src/server/serverFns/__tests__/billing.test.ts +58 -1
- package/src/server/serverFns/__tests__/update-seats.test.ts +8 -8
- package/src/server/serverFns/agents/agent-crud.ts +3 -1
- package/src/server/serverFns/auth/session.ts +9 -0
- package/src/server/serverFns/billing.ts +10 -4
- package/src/server/serverFns/bug/bug-crud.ts +7 -2
- package/src/server/serverFns/story/__tests__/labels.test.ts +37 -15
- package/src/server/serverFns/story/story-comments.ts +2 -1
- package/src/server/serverFns/story/story-crud/activities.ts +1 -1
- package/src/server/serverFns/story/story-crud/user-brief.ts +1 -1
- package/src/server/serverFns/story/story-crud.ts +2 -1
- package/src/server/serverFns/story/story-transfer.ts +2 -1
- package/src/server/serverFns/tasks/task-crud.ts +0 -13
- package/src/server/serverFns/tasks/task-queries.ts +1 -12
- package/src/server/serverFns/testcase/plans.ts +1 -1
- package/src/server/serverFns/testcase/run-cases.ts +1 -1
- package/src/server/serverFns/testcase/runs.ts +1 -1
- package/src/server/serverFns/testcase/testcase-activities.ts +1 -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/workspace.ts +1 -1
- package/src/server/story/types.ts +7 -22
- package/src/server/story/user-brief.ts +48 -0
- package/src/server/sync/scopes.ts +29 -0
- package/src/start.ts +10 -2
- package/src/styles/globals.css +10 -0
- package/src/__tests__/desktop-connection-validate.test.ts +0 -66
- package/src/lib/vector-store/__tests__/sqlite-vec-store.test.ts +0 -143
- package/src/lib/vector-store/sqlite-vec-store.ts +0 -224
- package/src/paraglide/messages/tasks_ai_desktop_notice.js +0 -29
- package/src/routes/__desktop/onboarding/route.lazy.tsx +0 -175
- package/src/routes/__desktop/onboarding/route.tsx +0 -11
- package/src/routes/_nav/settings/connection/route.lazy.tsx +0 -64
- package/src/routes/_nav/settings/connection/route.tsx +0 -10
|
@@ -2,11 +2,6 @@ import { Prisma, prisma } from "@allbluecn/database"
|
|
|
2
2
|
import { mapToUnifiedPriority } from "./priority-map"
|
|
3
3
|
import type { AssignedItem, AssignedResult, TaskSource } from "./types"
|
|
4
4
|
|
|
5
|
-
// provider 探测与 packages/database/index.ts 同口径(process.env.DATABASE_PROVIDER ?? "postgresql")
|
|
6
|
-
export function getDatabaseProvider(): string {
|
|
7
|
-
return process.env.DATABASE_PROVIDER ?? "postgresql"
|
|
8
|
-
}
|
|
9
|
-
|
|
10
5
|
const storySelect = {
|
|
11
6
|
id: true,
|
|
12
7
|
identifier: true,
|
|
@@ -137,17 +132,11 @@ function toTestCaseItem(row: TestCaseRow): AssignedItem {
|
|
|
137
132
|
|
|
138
133
|
/**
|
|
139
134
|
* 「分配给我的」三表聚合(story → bug → testcase 组序,组内 updatedAt 倒序,不跨表重排)。
|
|
140
|
-
*
|
|
135
|
+
* 历史 sqlite 降级已随 Tauri 清退移除(迁移 P1):postgresql/pglite 均有全量表。
|
|
141
136
|
*/
|
|
142
137
|
export async function fetchAssignedToMe(userId: string): Promise<AssignedResult> {
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
const pendingBugs: Promise<BugRow[]> = isSqlite
|
|
146
|
-
? Promise.resolve([])
|
|
147
|
-
: queryAssignedBugs(userId)
|
|
148
|
-
const pendingTestCases: Promise<TestCaseRow[]> = isSqlite
|
|
149
|
-
? Promise.resolve([])
|
|
150
|
-
: queryAssignedTestCases(userId)
|
|
138
|
+
const pendingBugs: Promise<BugRow[]> = queryAssignedBugs(userId)
|
|
139
|
+
const pendingTestCases: Promise<TestCaseRow[]> = queryAssignedTestCases(userId)
|
|
151
140
|
|
|
152
141
|
const [storyRows, bugRows, testCaseRows] = await Promise.all([
|
|
153
142
|
queryAssignedStories(userId),
|
|
@@ -161,7 +150,7 @@ export async function fetchAssignedToMe(userId: string): Promise<AssignedResult>
|
|
|
161
150
|
...testCaseRows.map(toTestCaseItem),
|
|
162
151
|
]
|
|
163
152
|
|
|
164
|
-
const degraded: TaskSource[] =
|
|
153
|
+
const degraded: TaskSource[] = []
|
|
165
154
|
|
|
166
155
|
return { items, degraded }
|
|
167
156
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { DEFAULT_AI_MODEL, DEFAULT_AI_PROVIDER } from '@/lib/ai/default-model'
|
|
2
|
+
import { defaultAvatarConfig, presetBgColors } from '@/lib/avatar/avatar-config'
|
|
2
3
|
import type { Session } from '@/lib/session'
|
|
3
4
|
|
|
4
5
|
export const GUEST_USER_ID = 'local-guest'
|
|
@@ -9,6 +10,12 @@ export function isGuestSession(session: Session | null): boolean {
|
|
|
9
10
|
return !!(session?.user?.id === GUEST_USER_ID)
|
|
10
11
|
}
|
|
11
12
|
|
|
13
|
+
/** guest 无 DB 行:用户名散列取预设底色,保证跨请求稳定(不闪变) */
|
|
14
|
+
function guestBgColor(seed: string): string {
|
|
15
|
+
const sum = [...seed].reduce((acc, ch) => acc + ch.charCodeAt(0), 0)
|
|
16
|
+
return presetBgColors[sum % presetBgColors.length] as string
|
|
17
|
+
}
|
|
18
|
+
|
|
12
19
|
export function buildGuestSession(): Session {
|
|
13
20
|
return {
|
|
14
21
|
user: {
|
|
@@ -17,6 +24,11 @@ export function buildGuestSession(): Session {
|
|
|
17
24
|
email: GUEST_EMAIL,
|
|
18
25
|
role: 'USER',
|
|
19
26
|
isGuest: true,
|
|
27
|
+
// 本地 guest 无 avatarConfig(DB 无行),缺省会让 UserAvatar 退化成
|
|
28
|
+
// 首字母灰块——桌面离线首屏「头像不显示」来源之一,给稳定默认配置
|
|
29
|
+
avatarConfig: defaultAvatarConfig,
|
|
30
|
+
bgShape: 'square',
|
|
31
|
+
bgColor: guestBgColor(GUEST_USERNAME),
|
|
20
32
|
aiProvider: DEFAULT_AI_PROVIDER,
|
|
21
33
|
aiModel: DEFAULT_AI_MODEL,
|
|
22
34
|
hasAiKey: false,
|
|
@@ -34,14 +34,14 @@ export interface FetchGuardOptions {
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
/**
|
|
37
|
-
* 桌面端运行时判定:desktop
|
|
38
|
-
* DATABASE_PROVIDER=
|
|
37
|
+
* 桌面端运行时判定:desktop 端(Electron + PGlite,迁移设计 §3.8)单机部署,
|
|
38
|
+
* 数据库走 DATABASE_PROVIDER=pglite;此时 localhost/私网是合法服务地址(如本地
|
|
39
39
|
* Ollama http://localhost:11434),BYOK 请求的私网 baseUrl 应放行。
|
|
40
40
|
* 云端 SaaS 为 postgres,恒返回 false——SSRF 防护默认语义不变(未知取值
|
|
41
41
|
* 按云端处理,fail-closed)。
|
|
42
42
|
*/
|
|
43
43
|
export function isDesktopRuntime(): boolean {
|
|
44
|
-
return process.env.DATABASE_PROVIDER === "
|
|
44
|
+
return process.env.DATABASE_PROVIDER === "pglite"
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
/**
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// AllBlue - 理想之海
|
|
3
|
+
// Copyright (C) 2026 AllBlue Contributors
|
|
4
|
+
|
|
5
|
+
// 桌面端本地端口防直连(迁移设计 §2.1 D1 安全增强):main 进程为每会话生成
|
|
6
|
+
// X-Desktop-Token 并注入 renderer cookie 与 backend env;backend(DEPLOY_TARGET=desktop
|
|
7
|
+
// 且注入了 X_DESKTOP_TOKEN)校验请求携带的 cookie/头一致——堵「本机其他进程
|
|
8
|
+
// 直连随机端口拿到 guest 会话」。云端部署(无该 env)不受影响,恒放行。
|
|
9
|
+
import { createMiddleware } from "@tanstack/react-start"
|
|
10
|
+
|
|
11
|
+
export const desktopTokenMiddleware = createMiddleware().server(async ({ request, next }) => {
|
|
12
|
+
const expected = process.env.X_DESKTOP_TOKEN
|
|
13
|
+
if (process.env.DEPLOY_TARGET !== "desktop" || !expected) return next()
|
|
14
|
+
|
|
15
|
+
const cookieHeader = request.headers.get("cookie") ?? ""
|
|
16
|
+
const cookieToken = /(?:^|;\s*)X-Desktop-Token=([^;]+)/.exec(cookieHeader)?.[1]
|
|
17
|
+
const headerToken = request.headers.get("x-desktop-token")
|
|
18
|
+
const provided = cookieToken ?? headerToken ?? ""
|
|
19
|
+
if (provided !== expected) {
|
|
20
|
+
return new Response("Desktop token mismatch", { status: 403 })
|
|
21
|
+
}
|
|
22
|
+
return next()
|
|
23
|
+
})
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// DESKTOP-LEGACY(迁移 P1 标记,勿删勿改):Tauri 时代的桌面远端会话读取桥,
|
|
2
|
+
// Electron 壳(P2)将以 preload desktopBridge 等价实现替代后整链移除。
|
|
1
3
|
export interface RemoteAuthData {
|
|
2
4
|
userId: string
|
|
3
5
|
planName: string
|
|
@@ -8,6 +10,10 @@ export interface RemoteAuthData {
|
|
|
8
10
|
sessionToken?: string
|
|
9
11
|
tokenExpired?: boolean
|
|
10
12
|
inGracePeriod?: boolean
|
|
13
|
+
/** 云端用户头像(SaaS desktop-callback profile 下发;缺省时 UI 回落首字母) */
|
|
14
|
+
avatarConfig?: Record<string, number>
|
|
15
|
+
bgShape?: string
|
|
16
|
+
bgColor?: string
|
|
11
17
|
}
|
|
12
18
|
|
|
13
19
|
export async function readRemoteAuth(): Promise<RemoteAuthData | null> {
|
|
@@ -27,6 +27,8 @@ const M = vi.hoisted(() => ({
|
|
|
27
27
|
getEntitlements: vi.fn(),
|
|
28
28
|
getResourceSnapshots: vi.fn(),
|
|
29
29
|
userUpdate: vi.fn(),
|
|
30
|
+
subscriptionFindUnique: vi.fn(),
|
|
31
|
+
teamGroupBy: vi.fn(),
|
|
30
32
|
}));
|
|
31
33
|
|
|
32
34
|
const billingHolder = vi.hoisted(() => ({
|
|
@@ -36,6 +38,7 @@ const billingHolder = vi.hoisted(() => ({
|
|
|
36
38
|
createCheckoutSession: ReturnType<typeof vi.fn>;
|
|
37
39
|
createCustomerPortalSession: ReturnType<typeof vi.fn>;
|
|
38
40
|
listInvoices: ReturnType<typeof vi.fn>;
|
|
41
|
+
updateSeats: ReturnType<typeof vi.fn>;
|
|
39
42
|
},
|
|
40
43
|
}));
|
|
41
44
|
|
|
@@ -66,7 +69,11 @@ vi.mock("@tanstack/react-start", () => ({
|
|
|
66
69
|
vi.mock("@tanstack/react-start/server", () => ({ getRequest: () => ({}) }));
|
|
67
70
|
|
|
68
71
|
vi.mock("@allbluecn/database", () => ({
|
|
69
|
-
prisma: {
|
|
72
|
+
prisma: {
|
|
73
|
+
user: { update: M.userUpdate },
|
|
74
|
+
subscription: { findUnique: M.subscriptionFindUnique },
|
|
75
|
+
teamMembership: { groupBy: M.teamGroupBy },
|
|
76
|
+
},
|
|
70
77
|
Prisma: {},
|
|
71
78
|
}));
|
|
72
79
|
|
|
@@ -88,6 +95,7 @@ import {
|
|
|
88
95
|
createCustomerPortalSessionFn,
|
|
89
96
|
listInvoicesFn,
|
|
90
97
|
updatePreferredCurrencyFn,
|
|
98
|
+
updateSeatsFn,
|
|
91
99
|
} from "../billing";
|
|
92
100
|
import { BillingUnavailableError } from "@allbluecn/kernel";
|
|
93
101
|
|
|
@@ -103,6 +111,7 @@ beforeEach(() => {
|
|
|
103
111
|
createCheckoutSession: vi.fn().mockResolvedValue({ url: "https://checkout/x" }),
|
|
104
112
|
createCustomerPortalSession: vi.fn().mockResolvedValue({ url: "https://portal/x" }),
|
|
105
113
|
listInvoices: vi.fn().mockResolvedValue([{ id: "in-1" }]),
|
|
114
|
+
updateSeats: vi.fn().mockResolvedValue({ plan: "pro", addonSeats: 2 }),
|
|
106
115
|
};
|
|
107
116
|
});
|
|
108
117
|
|
|
@@ -261,3 +270,51 @@ describe("updatePreferredCurrencyFn", () => {
|
|
|
261
270
|
});
|
|
262
271
|
});
|
|
263
272
|
});
|
|
273
|
+
|
|
274
|
+
describe("updateSeatsFn(审计 M8:席位口径=团队成员数,与展示侧一致)", () => {
|
|
275
|
+
const subWith = (overrides: Record<string, unknown> = {}) => ({
|
|
276
|
+
status: "ACTIVE",
|
|
277
|
+
plan: {
|
|
278
|
+
maxSeats: 5,
|
|
279
|
+
providerSeatPriceIdUsd: "price_seat",
|
|
280
|
+
providerSeatPriceIdCny: null,
|
|
281
|
+
...overrides,
|
|
282
|
+
},
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it("未登录拒绝", async () => {
|
|
286
|
+
M.requireAuth.mockRejectedValue(new Error("UNAUTHORIZED"));
|
|
287
|
+
await expect(updateSeatsFn({ data: { addonSeats: 2 } })).rejects.toThrow("UNAUTHORIZED");
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it("无有效订阅拒绝", async () => {
|
|
291
|
+
M.subscriptionFindUnique.mockResolvedValue(null);
|
|
292
|
+
await expect(updateSeatsFn({ data: { addonSeats: 2 } })).rejects.toThrow(/无有效订阅/);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it("席位不限量(maxSeats=null)拒绝加购", async () => {
|
|
296
|
+
M.subscriptionFindUnique.mockResolvedValue(subWith({ maxSeats: null }));
|
|
297
|
+
await expect(updateSeatsFn({ data: { addonSeats: 2 } })).rejects.toThrow(/不限量/);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it("团队成员数在上限内:放行并调用计费模块加购", async () => {
|
|
301
|
+
M.subscriptionFindUnique.mockResolvedValue(subWith());
|
|
302
|
+
M.teamGroupBy.mockResolvedValue([{ ownerId: U, _count: { _all: 4 } }]);
|
|
303
|
+
await expect(updateSeatsFn({ data: { addonSeats: 2 } })).resolves.toEqual({ plan: "pro", addonSeats: 2 });
|
|
304
|
+
expect(M.teamGroupBy).toHaveBeenCalledWith({
|
|
305
|
+
by: ["ownerId"],
|
|
306
|
+
where: { owner: { id: U } },
|
|
307
|
+
_count: { _all: true },
|
|
308
|
+
});
|
|
309
|
+
expect(billingHolder.current!.updateSeats).toHaveBeenCalledWith(
|
|
310
|
+
expect.objectContaining({ userId: U, addonSeats: 2 }),
|
|
311
|
+
);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it("团队成员数超上限(5+2 < 10):拒绝,文案按团队成员口径", async () => {
|
|
315
|
+
M.subscriptionFindUnique.mockResolvedValue(subWith());
|
|
316
|
+
M.teamGroupBy.mockResolvedValue([{ ownerId: U, _count: { _all: 10 } }]);
|
|
317
|
+
await expect(updateSeatsFn({ data: { addonSeats: 2 } })).rejects.toThrow(/团队成员数(10)/);
|
|
318
|
+
expect(billingHolder.current!.updateSeats).not.toHaveBeenCalled();
|
|
319
|
+
});
|
|
320
|
+
});
|
|
@@ -46,8 +46,8 @@ vi.mock('@tanstack/react-start/server', () => ({
|
|
|
46
46
|
|
|
47
47
|
vi.mock('@allbluecn/database', () => ({
|
|
48
48
|
prisma: {
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
teamMembership: {
|
|
50
|
+
groupBy: vi.fn(),
|
|
51
51
|
},
|
|
52
52
|
subscription: {
|
|
53
53
|
findUnique: vi.fn(),
|
|
@@ -73,7 +73,7 @@ import { prisma } from '@allbluecn/database'
|
|
|
73
73
|
import { updateSeatsFn } from '../billing'
|
|
74
74
|
|
|
75
75
|
const mockedSubFindUnique = prisma.subscription.findUnique as ReturnType<typeof vi.fn>
|
|
76
|
-
const
|
|
76
|
+
const mockedTeamGroupBy = prisma.teamMembership.groupBy as ReturnType<typeof vi.fn>
|
|
77
77
|
|
|
78
78
|
const fakeBilling = billingHolder.current!
|
|
79
79
|
|
|
@@ -81,7 +81,7 @@ describe('updateSeatsFn', () => {
|
|
|
81
81
|
beforeEach(() => {
|
|
82
82
|
vi.clearAllMocks()
|
|
83
83
|
billingHolder.current = fakeBilling
|
|
84
|
-
|
|
84
|
+
mockedTeamGroupBy.mockResolvedValue([])
|
|
85
85
|
})
|
|
86
86
|
|
|
87
87
|
const proSubWithPlan = (over: Record<string, unknown> = {}) => ({
|
|
@@ -123,17 +123,17 @@ describe('updateSeatsFn', () => {
|
|
|
123
123
|
).rejects.toThrow('不限量')
|
|
124
124
|
})
|
|
125
125
|
|
|
126
|
-
it('
|
|
126
|
+
it('加购后仍低于团队成员数被 floor 拦截(审计 M8:口径=团队成员数)', async () => {
|
|
127
127
|
mockedSubFindUnique.mockResolvedValue(proSubWithPlan())
|
|
128
|
-
|
|
128
|
+
mockedTeamGroupBy.mockResolvedValue([{ ownerId: 'u1', _count: { _all: 7 } }])
|
|
129
129
|
await expect(
|
|
130
130
|
updateSeatsFn({ data: { addonSeats: 1 } }),
|
|
131
|
-
).rejects.toThrow('
|
|
131
|
+
).rejects.toThrow('团队成员数')
|
|
132
132
|
})
|
|
133
133
|
|
|
134
134
|
it('合法加购:透传 billing.updateSeats 并返回其结果', async () => {
|
|
135
135
|
mockedSubFindUnique.mockResolvedValue(proSubWithPlan())
|
|
136
|
-
|
|
136
|
+
mockedTeamGroupBy.mockResolvedValue([{ ownerId: 'u1', _count: { _all: 4 } }])
|
|
137
137
|
fakeBilling.updateSeats.mockResolvedValue({ planId: 'p_pro', addonSeats: 3 } as never)
|
|
138
138
|
const result = await updateSeatsFn({ data: { addonSeats: 3 } })
|
|
139
139
|
expect(fakeBilling.updateSeats).toHaveBeenCalledWith({
|
|
@@ -19,7 +19,9 @@ import { getPrisma } from "@allbluecn/database/get-prisma"
|
|
|
19
19
|
import { createServerFn } from "@tanstack/react-start"
|
|
20
20
|
import { getRequest } from "@tanstack/react-start/server"
|
|
21
21
|
import { z } from "zod"
|
|
22
|
-
|
|
22
|
+
// AGENT_TOOL_KEYS 在模块顶层(zod schema)被消费,会保留进客户端 split stub——
|
|
23
|
+
// 必须走纯净子路径 tools/select;tools index re-export 含 prisma 的 tools/server.ts
|
|
24
|
+
import { AGENT_TOOL_KEYS } from "@/server/ai-gateway/tools/select"
|
|
23
25
|
import { requireFeature } from "@/server/entitlements"
|
|
24
26
|
import { requireResource } from "@/server/resources"
|
|
25
27
|
import { requireUserId } from "@/server/auth-helpers"
|
|
@@ -37,6 +37,10 @@ const saasProfileSchema = z.object({
|
|
|
37
37
|
name: z.string().nullable().optional(),
|
|
38
38
|
email: z.string().nullable().optional(),
|
|
39
39
|
role: z.string().optional(),
|
|
40
|
+
// 头像字段可选下发(旧 SaaS 无此字段时 UI 回落首字母)
|
|
41
|
+
avatarConfig: z.record(z.string(), z.number()).optional(),
|
|
42
|
+
bgShape: z.string().optional(),
|
|
43
|
+
bgColor: z.string().optional(),
|
|
40
44
|
}),
|
|
41
45
|
planName: z.string().optional(),
|
|
42
46
|
features: z.record(z.string(), z.boolean()).optional(),
|
|
@@ -51,6 +55,11 @@ function buildRemoteSession(data: RemoteAuthData): Session {
|
|
|
51
55
|
email: data.email,
|
|
52
56
|
role: (data.role ?? 'USER') as 'USER' | 'ADMIN' | 'SUPER_ADMIN',
|
|
53
57
|
isGuest: false,
|
|
58
|
+
// 云端头像透传(SaaS desktop-callback 下发;缺省回落首字母,修复桌面端
|
|
59
|
+
// 当前用户头像不显示)
|
|
60
|
+
avatarConfig: data.avatarConfig,
|
|
61
|
+
bgShape: data.bgShape,
|
|
62
|
+
bgColor: data.bgColor,
|
|
54
63
|
aiProvider: DEFAULT_AI_PROVIDER,
|
|
55
64
|
aiModel: DEFAULT_AI_MODEL,
|
|
56
65
|
hasAiKey: false,
|
|
@@ -151,13 +151,19 @@ export const updateSeatsFn = createServerFn({ method: 'POST' })
|
|
|
151
151
|
throw new Error('当前计划未开通席位加购')
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
-
|
|
155
|
-
|
|
154
|
+
// 席位用量与展示侧(getSubscription.seats → maxActiveTeamMembers)同一口径:
|
|
155
|
+
// 账号名下团队席位总数。审计 M8 裁决(2026-10-29):此前误用 workspace.count
|
|
156
|
+
// 作校验基数,与客户看到的「席位」数字互相矛盾(席位≠工作空间,
|
|
157
|
+
// maxSeats 与 maxWorkspaces 是两项独立限制)。
|
|
158
|
+
const seatGroups = await prisma.teamMembership.groupBy({
|
|
159
|
+
by: ['ownerId'],
|
|
160
|
+
where: { owner: { id: userId } },
|
|
161
|
+
_count: { _all: true },
|
|
156
162
|
})
|
|
157
|
-
const maxMembers =
|
|
163
|
+
const maxMembers = seatGroups.reduce((m, g) => Math.max(m, g._count._all), 0)
|
|
158
164
|
if (subscription.plan.maxSeats + data.addonSeats < maxMembers) {
|
|
159
165
|
throw new Error(
|
|
160
|
-
`席位上限(${subscription.plan.maxSeats + data.addonSeats}
|
|
166
|
+
`席位上限(${subscription.plan.maxSeats + data.addonSeats})不能低于当前团队成员数(${maxMembers}),请先移除多余成员`,
|
|
161
167
|
)
|
|
162
168
|
}
|
|
163
169
|
|
|
@@ -41,14 +41,19 @@ import { touchCollabSnapshot } from "@/server/collab/touch-snapshot";
|
|
|
41
41
|
import { assertProjectMembership as assertProjectMembershipShared } from "@/server/lib/project-membership";
|
|
42
42
|
|
|
43
43
|
|
|
44
|
-
// S2
|
|
44
|
+
// S2:helper 迁兄弟文件(bug-crud-helpers),handler 内按需引用。禁止在此
|
|
45
|
+
// re-export:serverFn 的客户端 split stub 会保留模块顶层 import,而
|
|
46
|
+
// bug-crud-helpers 依赖 get-prisma/team-access(→ @allbluecn/database →
|
|
47
|
+
// dotenv/config),会把 server-only 链拉进浏览器令全站 hydration 崩溃
|
|
48
|
+
// (2026-10-30 实锤:routeTree → serverFns/bug/index → bug-crud stub →
|
|
49
|
+
// helpers → dotenv,浏览器端 process.argv.reduce 抛 TypeError,按钮全灭)。
|
|
50
|
+
// 消费方一律直接 import @/server/bug/bug-crud-helpers。
|
|
45
51
|
import {
|
|
46
52
|
validateBugAssignee,
|
|
47
53
|
assertBugsInMemberProjects,
|
|
48
54
|
toBugActivityItem,
|
|
49
55
|
bugDescriptionToContent,
|
|
50
56
|
} from "@/server/bug/bug-crud-helpers";
|
|
51
|
-
export { validateBugAssignee, assertBugsInMemberProjects, toBugActivityItem, bugDescriptionToContent };
|
|
52
57
|
// 白名单校验模式(与 story 同构):DB 中 state/severity/source 为 String,写入侧用 z.enum 收敛
|
|
53
58
|
const BUG_STATE_VALUES = BUG_STATES.map((s) => s.value) as [BugState, ...BugState[]];
|
|
54
59
|
const BUG_SEVERITY_VALUES = BUG_SEVERITIES.map((s) => s.value) as [BugSeverity, ...BugSeverity[]];
|
|
@@ -18,11 +18,14 @@
|
|
|
18
18
|
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
19
19
|
|
|
20
20
|
const mocks = vi.hoisted(() => ({
|
|
21
|
-
|
|
21
|
+
labelFindFirst: vi.fn(),
|
|
22
|
+
labelCreate: vi.fn(),
|
|
22
23
|
}));
|
|
23
24
|
|
|
24
25
|
vi.mock("@allbluecn/database", () => ({
|
|
25
|
-
prisma: {
|
|
26
|
+
prisma: {
|
|
27
|
+
label: { findFirst: mocks.labelFindFirst, create: mocks.labelCreate },
|
|
28
|
+
},
|
|
26
29
|
}));
|
|
27
30
|
|
|
28
31
|
import { upsertLabel } from "../../../story/story-crud/label-upsert";
|
|
@@ -45,23 +48,42 @@ describe("pickLabelColor", () => {
|
|
|
45
48
|
});
|
|
46
49
|
});
|
|
47
50
|
|
|
51
|
+
// S24 语义:项目归属标签 find→create(部分唯一索引兜底并发);
|
|
52
|
+
// 历史「全局 upsert」用例已随实现下线重写(迁移 P1 顺手修复的既有失败)
|
|
48
53
|
describe("upsertLabel", () => {
|
|
49
|
-
it("
|
|
50
|
-
mocks.
|
|
51
|
-
const r = await upsertLabel("性能", "u1");
|
|
52
|
-
expect(mocks.labelUpsert).toHaveBeenCalledTimes(1);
|
|
53
|
-
const arg = mocks.labelUpsert.mock.calls[0][0] as any;
|
|
54
|
-
expect(arg.where).toEqual({ name: "性能" });
|
|
55
|
-
expect(arg.update).toEqual({});
|
|
56
|
-
expect(LABEL_PALETTE).toContain(arg.create.color);
|
|
57
|
-
expect(arg.create.createdById).toBe("u1");
|
|
54
|
+
it("项目标签命中即直接返回,不创建", async () => {
|
|
55
|
+
mocks.labelFindFirst.mockResolvedValue({ id: "l1", name: "性能", color: "#ef4444" });
|
|
56
|
+
const r = await upsertLabel("性能", "u1", undefined, "p1");
|
|
58
57
|
expect(r).toEqual({ id: "l1", name: "性能", color: "#ef4444" });
|
|
58
|
+
expect(mocks.labelFindFirst).toHaveBeenCalledWith({
|
|
59
|
+
where: { name: "性能", projectId: "p1" },
|
|
60
|
+
select: { id: true, name: true, color: true },
|
|
61
|
+
});
|
|
62
|
+
expect(mocks.labelCreate).not.toHaveBeenCalled();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("未命中创建:create 分支使用色板颜色", async () => {
|
|
66
|
+
mocks.labelFindFirst.mockResolvedValue(null);
|
|
67
|
+
mocks.labelCreate.mockResolvedValue({ id: "l2", name: "性能", color: "#123456" });
|
|
68
|
+
const r = await upsertLabel("性能", "u1", undefined, "p1");
|
|
69
|
+
const arg = mocks.labelCreate.mock.calls[0][0] as any;
|
|
70
|
+
expect(LABEL_PALETTE).toContain(arg.data.color);
|
|
71
|
+
expect(arg.data.createdById).toBe("u1");
|
|
72
|
+
expect(arg.data.projectId).toBe("p1");
|
|
73
|
+
expect(r.id).toBe("l2");
|
|
59
74
|
});
|
|
60
75
|
|
|
61
76
|
it("传自定义颜色时优先使用自定义颜色", async () => {
|
|
62
|
-
mocks.
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
77
|
+
mocks.labelFindFirst.mockResolvedValue(null);
|
|
78
|
+
mocks.labelCreate.mockResolvedValue({ id: "l3", name: "性能", color: "#ff0000" });
|
|
79
|
+
await upsertLabel("性能", "u1", "#ff0000", "p1");
|
|
80
|
+
const arg = mocks.labelCreate.mock.calls[0][0] as any;
|
|
81
|
+
expect(arg.data.color).toBe("#ff0000");
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("兼容签名(无 projectId):仅匹配存量全局标签,找不到即抛不新建", async () => {
|
|
85
|
+
mocks.labelFindFirst.mockResolvedValue(null);
|
|
86
|
+
await expect(upsertLabel("旧标签", "u1")).rejects.toThrow("全局标签命名空间已关闭");
|
|
87
|
+
expect(mocks.labelCreate).not.toHaveBeenCalled();
|
|
66
88
|
});
|
|
67
89
|
});
|
|
@@ -21,7 +21,8 @@ import { getRequest } from "@tanstack/react-start/server";
|
|
|
21
21
|
import { z } from "zod";
|
|
22
22
|
import { requireAuth } from "@/server/auth-helpers";
|
|
23
23
|
import type { CommentWithReplies, ReactionGroup } from "@/shared/story/types";
|
|
24
|
-
import { USER_BRIEF_SELECT, castUserBrief
|
|
24
|
+
import { USER_BRIEF_SELECT, castUserBrief } from "@/server/story/user-brief";
|
|
25
|
+
import { aggregateReactions } from "@/server/story/types";
|
|
25
26
|
import { assertStoryProjectMember, assertStoryProjectMembership as assertProjectMembership } from "@/server/story/permissions";
|
|
26
27
|
|
|
27
28
|
|
|
@@ -21,7 +21,7 @@ import { getRequest } from "@tanstack/react-start/server";
|
|
|
21
21
|
import { z } from "zod";
|
|
22
22
|
import { requireAuth } from "@/server/auth-helpers";
|
|
23
23
|
import { assertStoryProjectMember } from "@/server/story/permissions";
|
|
24
|
-
import { USER_BRIEF_SELECT, castUserBrief } from "@/server/story/
|
|
24
|
+
import { USER_BRIEF_SELECT, castUserBrief } from "@/server/story/user-brief";
|
|
25
25
|
|
|
26
26
|
const listActivitiesSchema = z.object({
|
|
27
27
|
storyId: z.string(),
|
|
@@ -9,7 +9,7 @@ import { createServerFn } from "@tanstack/react-start";
|
|
|
9
9
|
import { getRequest } from "@tanstack/react-start/server";
|
|
10
10
|
import { requireAuth } from "@/server/auth-helpers";
|
|
11
11
|
import { assertStoryProjectMembership } from "@/server/story/permissions";
|
|
12
|
-
import { USER_BRIEF_SELECT } from "@/server/story/
|
|
12
|
+
import { USER_BRIEF_SELECT } from "@/server/story/user-brief";
|
|
13
13
|
|
|
14
14
|
export const listUsersBriefFn = createServerFn({ method: "GET" })
|
|
15
15
|
.validator((data?: { projectId?: string }) => data)
|
|
@@ -27,7 +27,8 @@ import { withCollabGuard } from "@/server/collab/write-guard";
|
|
|
27
27
|
import { touchCollabSnapshot } from "@/server/collab/touch-snapshot";
|
|
28
28
|
import type { ActivityItem } from "@/shared/story/types";
|
|
29
29
|
import { diffDescriptionBlocks } from "@/lib/story/description-diff";
|
|
30
|
-
import { USER_BRIEF_SELECT,
|
|
30
|
+
import { USER_BRIEF_SELECT, castUserBrief } from "@/server/story/user-brief";
|
|
31
|
+
import { STORY_LIST_SELECT, castStoryUsers } from "@/server/story/types";
|
|
31
32
|
import { toDateKeyUtc } from "@/server/story/date-key";
|
|
32
33
|
import { buildListWhere } from "./story-list-query";
|
|
33
34
|
import { createStoryInternal } from "./story-crud/create-story-internal";
|
|
@@ -25,7 +25,8 @@ import { requireAuth } from "@/server/auth-helpers";
|
|
|
25
25
|
import { BUILTIN_INDEPENDENT_DISPLAY_ID } from "@/lib/builtin-project";
|
|
26
26
|
import { resolveStoryScopeProjectId } from "@/server/builtin-project";
|
|
27
27
|
import { withCollabGuard } from "@/server/collab/write-guard";
|
|
28
|
-
import {
|
|
28
|
+
import { castUserBrief, USER_BRIEF_SELECT } from "@/server/story/user-brief";
|
|
29
|
+
import { STORY_LIST_SELECT, castStoryUsers, checkDescendant } from "@/server/story/types";
|
|
29
30
|
import { withSequenceNumberRetry } from "@/server/lib/sequence-number-retry";
|
|
30
31
|
import { assertProjectMembership as assertProjectMembershipShared } from "@/server/lib/project-membership";
|
|
31
32
|
|
|
@@ -9,8 +9,6 @@
|
|
|
9
9
|
// checkAiRateLimit(userId, "aiTask"),再做属主/成员校验。
|
|
10
10
|
// 并发裁决(双击防重入):updateMany 以 status:{not:"running"} 做条件更新占位,
|
|
11
11
|
// count===0 即已有在跑实例,抛「任务正在运行」,不建 run 不调执行体。
|
|
12
|
-
// sqlite 降级:aiTask 表 pg-only(DATABASE_PROVIDER=sqlite 无此表),三个变更入口
|
|
13
|
-
// 直接拒绝,不触 prisma(provider 探测复用任务 3 的 getDatabaseProvider())。
|
|
14
12
|
import { getPrisma } from "@allbluecn/database/get-prisma";
|
|
15
13
|
import { createServerFn } from "@tanstack/react-start";
|
|
16
14
|
import { getRequest } from "@tanstack/react-start/server";
|
|
@@ -19,7 +17,6 @@ import { requireAuth } from "@/server/auth-helpers";
|
|
|
19
17
|
import { assertProjectMembership } from "@/server/bug/bug-permissions";
|
|
20
18
|
import { checkAiRateLimit } from "@/server/lib/rate-limit";
|
|
21
19
|
import { executeAiTaskRun } from "@/server/ai-task/executor";
|
|
22
|
-
import { getDatabaseProvider } from "@/server/ai-task/assigned-query";
|
|
23
20
|
|
|
24
21
|
/** 源条目在创建流程中的投影(title 已归一化:story.name / bug.title / testCase.title) */
|
|
25
22
|
interface CreateSourceRef {
|
|
@@ -120,13 +117,6 @@ async function runTaskExecution(taskId: string, userId: string): Promise<string>
|
|
|
120
117
|
return run.id;
|
|
121
118
|
}
|
|
122
119
|
|
|
123
|
-
/** sqlite(aiTask pg-only)变更入口统一降级:明确报错而非裸 Prisma 崩溃 */
|
|
124
|
-
function assertPgOnlyMutation(): void {
|
|
125
|
-
if (getDatabaseProvider() === "sqlite") {
|
|
126
|
-
throw new Error("桌面端暂不支持 AI 任务");
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
120
|
const createAiTaskSchema = z
|
|
131
121
|
.object({
|
|
132
122
|
title: z.string().min(1).max(200).optional(),
|
|
@@ -158,7 +148,6 @@ export const createAiTaskFn = createServerFn({ method: "POST" })
|
|
|
158
148
|
const session = await requireAuth(request);
|
|
159
149
|
const userId = session.user.id;
|
|
160
150
|
|
|
161
|
-
assertPgOnlyMutation();
|
|
162
151
|
|
|
163
152
|
const prisma = await getPrisma();
|
|
164
153
|
|
|
@@ -230,7 +219,6 @@ export const runAiTaskFn = createServerFn({ method: "POST" })
|
|
|
230
219
|
const session = await requireAuth(request);
|
|
231
220
|
const userId = session.user.id;
|
|
232
221
|
|
|
233
|
-
assertPgOnlyMutation();
|
|
234
222
|
await checkAiRateLimit(userId, "aiTask");
|
|
235
223
|
|
|
236
224
|
const runId = await runTaskExecution(data.id, userId);
|
|
@@ -245,7 +233,6 @@ export const deleteAiTaskFn = createServerFn({ method: "POST" })
|
|
|
245
233
|
const session = await requireAuth(request);
|
|
246
234
|
const userId = session.user.id;
|
|
247
235
|
|
|
248
|
-
assertPgOnlyMutation();
|
|
249
236
|
|
|
250
237
|
const prisma = await getPrisma();
|
|
251
238
|
// 归属过滤放 where:非属主与不存在同样抛「任务不存在」,不泄露存在性
|
|
@@ -4,19 +4,13 @@
|
|
|
4
4
|
// 非导出成员。
|
|
5
5
|
// 鉴权惯例(同 testcase-crud):serverFn 是独立 RPC 入口,路由层守卫拦不到,handler
|
|
6
6
|
// 首行业务必须 requireAuth(getRequest()) 自查。
|
|
7
|
-
// sqlite 降级:aiTask 表 pg-only(DATABASE_PROVIDER=sqlite 无此表),listAiTasksFn
|
|
8
|
-
// 直接返回 { items: [], degraded: true } 不触 prisma.aiTask;provider 探测复用任务 3
|
|
9
|
-
// 的 getDatabaseProvider()(与 packages/database/index.ts 同口径),不自建探测。
|
|
10
7
|
import { getPrisma } from "@allbluecn/database/get-prisma";
|
|
11
8
|
import type { Prisma } from "@allbluecn/database";
|
|
12
9
|
import { createServerFn } from "@tanstack/react-start";
|
|
13
10
|
import { getRequest } from "@tanstack/react-start/server";
|
|
14
11
|
import { z } from "zod";
|
|
15
12
|
import { requireAuth } from "@/server/auth-helpers";
|
|
16
|
-
import {
|
|
17
|
-
fetchAssignedToMe,
|
|
18
|
-
getDatabaseProvider,
|
|
19
|
-
} from "@/server/ai-task/assigned-query";
|
|
13
|
+
import { fetchAssignedToMe } from "@/server/ai-task/assigned-query";
|
|
20
14
|
import type { AssignedResult } from "@/server/ai-task/types";
|
|
21
15
|
|
|
22
16
|
type AiTaskRow = Prisma.AiTaskModel;
|
|
@@ -82,11 +76,6 @@ export const listAiTasksFn = createServerFn({ method: "GET" }).handler(
|
|
|
82
76
|
const session = await requireAuth(request);
|
|
83
77
|
const userId = session.user.id;
|
|
84
78
|
|
|
85
|
-
// 桌面端 SQLite 无 aiTask 表(pg-only):降级空列表并声明 degraded,不触 DB
|
|
86
|
-
if (getDatabaseProvider() === "sqlite") {
|
|
87
|
-
return { items: [], degraded: true };
|
|
88
|
-
}
|
|
89
|
-
|
|
90
79
|
const prisma = await getPrisma();
|
|
91
80
|
const rows = await prisma.aiTask.findMany({
|
|
92
81
|
where: { createdById: userId },
|
|
@@ -32,7 +32,7 @@ import { z } from "zod";
|
|
|
32
32
|
import type { Prisma } from "@allbluecn/database";
|
|
33
33
|
import { requireAuth } from "@/server/auth-helpers";
|
|
34
34
|
import { resolveStoryScopeProjectId } from "@/server/builtin-project";
|
|
35
|
-
import { USER_BRIEF_SELECT, castUserBrief } from "@/server/story/
|
|
35
|
+
import { USER_BRIEF_SELECT, castUserBrief } from "@/server/story/user-brief";
|
|
36
36
|
import {
|
|
37
37
|
TEST_PLAN_STATE_VALUES,
|
|
38
38
|
type TestCaseState,
|
|
@@ -40,7 +40,7 @@ import {
|
|
|
40
40
|
finalizeRunIfComplete,
|
|
41
41
|
} from "@/server/testcase/run-finalize";
|
|
42
42
|
import { fireTestRunWebhook } from "@/server/testcase/run-webhook";
|
|
43
|
-
import { USER_BRIEF_SELECT, castUserBrief } from "@/server/story/
|
|
43
|
+
import { USER_BRIEF_SELECT, castUserBrief } from "@/server/story/user-brief";
|
|
44
44
|
import { RUN_STEP_RESULT_STATUS_VALUES } from "./constants";
|
|
45
45
|
import type { RunCaseStepResult, RunCaseExecutionDetail } from "./types";
|
|
46
46
|
import { assertRunCaseProjectMember, assertTestProjectMembership as assertProjectMembership } from "@/server/testcase/permissions";
|
|
@@ -37,7 +37,7 @@ import type { Prisma } from "@allbluecn/database";
|
|
|
37
37
|
import { requireAuth } from "@/server/auth-helpers";
|
|
38
38
|
import { resolveStoryScopeProjectId } from "@/server/builtin-project";
|
|
39
39
|
import { buildCaseSnapshot } from "@/server/testcase/snapshot";
|
|
40
|
-
import { USER_BRIEF_SELECT, castUserBrief } from "@/server/story/
|
|
40
|
+
import { USER_BRIEF_SELECT, castUserBrief } from "@/server/story/user-brief";
|
|
41
41
|
import {
|
|
42
42
|
RUN_EXECUTOR_TYPE_VALUES,
|
|
43
43
|
TEST_RUN_STATUS_VALUES,
|
|
@@ -26,7 +26,7 @@ import { getRequest } from "@tanstack/react-start/server";
|
|
|
26
26
|
import { z } from "zod";
|
|
27
27
|
import { requireAuth } from "@/server/auth-helpers";
|
|
28
28
|
import { assertTestCaseProjectMember } from "@/server/testcase/permissions";
|
|
29
|
-
import { USER_BRIEF_SELECT, castUserBrief } from "@/server/story/
|
|
29
|
+
import { USER_BRIEF_SELECT, castUserBrief } from "@/server/story/user-brief";
|
|
30
30
|
import type { TestCaseActivityItem } from "./types";
|
|
31
31
|
|
|
32
32
|
const listActivitiesSchema = z.object({
|
|
@@ -30,7 +30,7 @@ import type { Prisma } from "@allbluecn/database";
|
|
|
30
30
|
import { requireAuth } from "@/server/auth-helpers";
|
|
31
31
|
import { resolveStoryScopeProjectIds } from "@/server/builtin-project";
|
|
32
32
|
import { buildListWhere } from "@/server/testcase/list-query";
|
|
33
|
-
import { USER_BRIEF_SELECT, castUserBrief } from "@/server/story/
|
|
33
|
+
import { USER_BRIEF_SELECT, castUserBrief } from "@/server/story/user-brief";
|
|
34
34
|
import {
|
|
35
35
|
TESTCASE_ORDER_DIRECTIONS,
|
|
36
36
|
TESTCASE_ORDER_FIELD_MAP,
|
|
@@ -32,7 +32,7 @@ import { requireAuth } from "@/server/auth-helpers";
|
|
|
32
32
|
import { resolveStoryScopeProjectIds } from "@/server/builtin-project";
|
|
33
33
|
import { buildListWhere } from "@/server/testcase/list-query";
|
|
34
34
|
import { validateAssignee } from "@/server/lib/validate-assignee";
|
|
35
|
-
import { USER_BRIEF_SELECT, castUserBrief } from "@/server/story/
|
|
35
|
+
import { USER_BRIEF_SELECT, castUserBrief } from "@/server/story/user-brief";
|
|
36
36
|
import {
|
|
37
37
|
TESTCASE_ORDER_DIRECTIONS,
|
|
38
38
|
TESTCASE_ORDER_FIELD_MAP,
|