@svadmin/core 0.25.2 → 0.25.4

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@svadmin/core",
3
- "version": "0.25.2",
3
+ "version": "0.25.4",
4
4
  "author": "zuohuadong",
5
5
  "repository": {
6
6
  "type": "git",
@@ -22,7 +22,7 @@
22
22
  },
23
23
  "peerDependencies": {
24
24
  "svelte": "^5.0.0",
25
- "@tanstack/svelte-query": "^6.0.0"
25
+ "@tanstack/svelte-query": "^6.1.29"
26
26
  },
27
27
  "exports": {
28
28
  ".": {
@@ -63,7 +63,11 @@
63
63
  },
64
64
  "description": "Core SDK — hooks, types, context, i18n, permissions, router",
65
65
  "files": [
66
- "src/**/*"
66
+ "src/**/*.ts",
67
+ "!src/**/*.test.*",
68
+ "!src/**/*.spec.*",
69
+ "!src/**/vitest.config.*",
70
+ "!src/**/setupTest.*"
67
71
  ],
68
72
  "homepage": "https://github.com/zuohuadong/svadmin#readme",
69
73
  "license": "MIT",
package/src/README.md DELETED
@@ -1,18 +0,0 @@
1
- # Name
2
- ### {{module_name}}
3
-
4
- # Synopsis
5
- {{synopsis}}
6
-
7
- # Description
8
-
9
- # Example
10
-
11
- # Install:
12
- `npm install {{module_name}}`
13
-
14
- # Test:
15
- `npm test`
16
-
17
- #License:
18
- {{license}}
@@ -1,205 +0,0 @@
1
- /**
2
- * Auth Hooks — Unit Tests
3
- *
4
- * These test the auth hook *logic* by mocking the Svelte context module
5
- * so `getAuthProvider()` returns a mock provider without needing a real component tree.
6
- */
7
- import { describe, test, expect, mock, beforeEach } from 'bun:test';
8
- import type { AuthProvider, AuthActionResult, CheckResult, Identity } from './types';
9
-
10
- // Build a mock AuthProvider factory
11
- function createMockAuthProvider(overrides: Partial<AuthProvider> = {}): AuthProvider {
12
- return {
13
- login: mock(async () => ({ success: true, redirectTo: '/' } as AuthActionResult)),
14
- logout: mock(async () => ({ success: true, redirectTo: '/login' } as AuthActionResult)),
15
- check: mock(async () => ({ authenticated: true } as CheckResult)),
16
- getIdentity: mock(async () => ({ id: '1', name: 'Admin', email: 'admin@test.com' } as Identity)),
17
- register: mock(async () => ({ success: true } as AuthActionResult)),
18
- forgotPassword: mock(async () => ({ success: true } as AuthActionResult)),
19
- updatePassword: mock(async () => ({ success: true } as AuthActionResult)),
20
- onError: mock(async () => ({ logout: false })),
21
- getPermissions: mock(async () => ['admin', 'editor']),
22
- ...overrides,
23
- };
24
- }
25
-
26
- // ─── Tests for AuthProvider mock interface ───────────────────
27
-
28
- describe('AuthProvider mock interface', () => {
29
- test('login returns success', async () => {
30
- const provider = createMockAuthProvider();
31
- const result = await provider.login({ email: 'test@test.com', password: '123' });
32
- expect(result.success).toBe(true);
33
- expect(result.redirectTo).toBe('/');
34
- expect(provider.login).toHaveBeenCalledTimes(1);
35
- });
36
-
37
- test('login returns failure', async () => {
38
- const provider = createMockAuthProvider({
39
- login: mock(async () => ({ success: false, error: { message: 'Invalid credentials' } })),
40
- });
41
- const result = await provider.login({ email: 'bad', password: 'bad' });
42
- expect(result.success).toBe(false);
43
- expect(result.error?.message).toBe('Invalid credentials');
44
- });
45
-
46
- test('logout returns success with redirect', async () => {
47
- const provider = createMockAuthProvider();
48
- const result = await provider.logout!();
49
- expect(result.success).toBe(true);
50
- expect(result.redirectTo).toBe('/login');
51
- });
52
-
53
- test('check returns authenticated', async () => {
54
- const provider = createMockAuthProvider();
55
- const result = await provider.check();
56
- expect(result.authenticated).toBe(true);
57
- });
58
-
59
- test('check returns not authenticated', async () => {
60
- const provider = createMockAuthProvider({
61
- check: mock(async () => ({ authenticated: false, redirectTo: '/login' })),
62
- });
63
- const result = await provider.check();
64
- expect(result.authenticated).toBe(false);
65
- expect(result.redirectTo).toBe('/login');
66
- });
67
-
68
- test('getIdentity returns user info', async () => {
69
- const provider = createMockAuthProvider();
70
- const identity = await provider.getIdentity();
71
- expect(identity).not.toBeNull();
72
- expect(identity!.name).toBe('Admin');
73
- expect(identity!.email).toBe('admin@test.com');
74
- });
75
-
76
- test('getIdentity returns null when not logged in', async () => {
77
- const provider = createMockAuthProvider({
78
- getIdentity: mock(async () => null),
79
- });
80
- const identity = await provider.getIdentity();
81
- expect(identity).toBeNull();
82
- });
83
-
84
- test('register returns success', async () => {
85
- const provider = createMockAuthProvider();
86
- const result = await provider.register!({ email: 'new@test.com', password: '123' });
87
- expect(result.success).toBe(true);
88
- });
89
-
90
- test('forgotPassword returns success', async () => {
91
- const provider = createMockAuthProvider();
92
- const result = await provider.forgotPassword!({ email: 'test@test.com' });
93
- expect(result.success).toBe(true);
94
- });
95
-
96
- test('updatePassword returns success', async () => {
97
- const provider = createMockAuthProvider();
98
- const result = await provider.updatePassword!({ password: 'newpass', confirmPassword: 'newpass' });
99
- expect(result.success).toBe(true);
100
- });
101
-
102
- test('onError with logout flag', async () => {
103
- const provider = createMockAuthProvider({
104
- onError: mock(async () => ({ logout: true, redirectTo: '/login' })),
105
- });
106
- const result = await provider.onError!(new Error('401'));
107
- expect(result.logout).toBe(true);
108
- expect(result.redirectTo).toBe('/login');
109
- });
110
-
111
- test('onError without logout', async () => {
112
- const provider = createMockAuthProvider({
113
- onError: mock(async () => ({ logout: false, redirectTo: '/error' })),
114
- });
115
- const result = await provider.onError!(new Error('500'));
116
- expect(result.logout).toBe(false);
117
- expect(result.redirectTo).toBe('/error');
118
- });
119
-
120
- test('getPermissions returns permissions array', async () => {
121
- const provider = createMockAuthProvider();
122
- const permissions = await provider.getPermissions!();
123
- expect(permissions).toEqual(['admin', 'editor']);
124
- });
125
-
126
- test('getPermissions can return custom structure', async () => {
127
- const provider = createMockAuthProvider({
128
- getPermissions: mock(async () => ({ role: 'admin', canDelete: true })),
129
- });
130
- const permissions = await provider.getPermissions!() as { role: string; canDelete: boolean };
131
- expect(permissions.role).toBe('admin');
132
- expect(permissions.canDelete).toBe(true);
133
- });
134
- });
135
-
136
- // ─── Tests for auth logic patterns ──────────────────────────
137
-
138
- describe('auth hook patterns', () => {
139
- test('login flow: success triggers redirect', async () => {
140
- const provider = createMockAuthProvider();
141
- const result = await provider.login({ email: 'admin@test.com', password: 'pass' });
142
- expect(result.success).toBe(true);
143
- // In real hook: navigate(result.redirectTo)
144
- expect(result.redirectTo).toBeDefined();
145
- });
146
-
147
- test('login flow: failure shows error', async () => {
148
- const provider = createMockAuthProvider({
149
- login: mock(async () => ({ success: false, error: { message: 'Bad password' } })),
150
- });
151
- const result = await provider.login({ email: 'admin@test.com', password: 'wrong' });
152
- expect(result.success).toBe(false);
153
- // In real hook: toast.error(result.error.message)
154
- expect(result.error?.message).toBe('Bad password');
155
- });
156
-
157
- test('login flow: exception produces error result', async () => {
158
- const provider = createMockAuthProvider({
159
- login: mock(async () => { throw new Error('Network Error'); }),
160
- });
161
- try {
162
- await provider.login({ email: 'test', password: 'test' });
163
- expect(true).toBe(false); // should not reach
164
- } catch (err) {
165
- expect((err as Error).message).toBe('Network Error');
166
- }
167
- });
168
-
169
- test('onError flow: 401 triggers logout + redirect', async () => {
170
- const provider = createMockAuthProvider({
171
- onError: mock(async (err: unknown) => {
172
- if (err instanceof Error && err.message.includes('401')) {
173
- return { logout: true, redirectTo: '/login' };
174
- }
175
- return { logout: false };
176
- }),
177
- });
178
- const result = await provider.onError!(new Error('401 Unauthorized'));
179
- expect(result.logout).toBe(true);
180
- // In real hook: provider.logout() then navigate(result.redirectTo)
181
- });
182
-
183
- test('onError flow: 500 does not trigger logout', async () => {
184
- const provider = createMockAuthProvider({
185
- onError: mock(async (err: unknown) => {
186
- if (err instanceof Error && err.message.includes('401')) {
187
- return { logout: true, redirectTo: '/login' };
188
- }
189
- return { logout: false };
190
- }),
191
- });
192
- const result = await provider.onError!(new Error('500 Internal Server Error'));
193
- expect(result.logout).toBe(false);
194
- });
195
-
196
- test('check → getIdentity flow', async () => {
197
- const provider = createMockAuthProvider();
198
- const checkResult = await provider.check();
199
- expect(checkResult.authenticated).toBe(true);
200
- if (checkResult.authenticated) {
201
- const identity = await provider.getIdentity();
202
- expect(identity!.name).toBe('Admin');
203
- }
204
- });
205
- });
@@ -1,388 +0,0 @@
1
- // Unit tests for ChatProvider types and ChatContext logic
2
- // Since chatProvider.svelte.ts uses $state runes (requires Svelte compiler),
3
- // we test the pure type shapes and serialization here — matching the project's
4
- // pattern in i18n.test.ts of testing extracted logic.
5
-
6
- import { describe, test, expect } from 'bun:test';
7
-
8
- /** Mirror of ChatAction from chatProvider.svelte.ts */
9
- interface ChatAction {
10
- label: string;
11
- variant?: 'default' | 'destructive' | 'outline';
12
- payload?: Record<string, unknown>;
13
- }
14
-
15
- /** Mirror of ChatMessage from chatProvider.svelte.ts */
16
- interface ChatMessage {
17
- id: string;
18
- role: 'user' | 'assistant' | 'system';
19
- content: string;
20
- timestamp: number;
21
- actions?: ChatAction[];
22
- }
23
-
24
- /** Mirror of ChatContext from chatProvider.svelte.ts */
25
- interface ChatContext {
26
- currentResource?: string;
27
- selectedRecordId?: string;
28
- currentView?: 'list' | 'edit' | 'create' | 'show';
29
- pathname?: string;
30
- }
31
-
32
- /** Build context system message (mirrors logic in ChatDialog.svelte) */
33
- function buildContextSystemMessage(ctx: ChatContext): ChatMessage | null {
34
- if (!ctx.currentResource) return null;
35
-
36
- const parts: string[] = [];
37
- parts.push(`Resource: ${ctx.currentResource}`);
38
- if (ctx.currentView) parts.push(`View: ${ctx.currentView}`);
39
- if (ctx.selectedRecordId) parts.push(`Selected Record ID: ${ctx.selectedRecordId}`);
40
- if (ctx.pathname) parts.push(`Path: ${ctx.pathname}`);
41
-
42
- return {
43
- id: 'ctx-system',
44
- role: 'system',
45
- content: `[Admin Context] ${parts.join(' | ')}`,
46
- timestamp: Date.now(),
47
- };
48
- }
49
-
50
- describe('ChatMessage with actions', () => {
51
- test('message with actions serializes to JSON and back', () => {
52
- const actions: ChatAction[] = [
53
- { label: 'Delete Record', variant: 'destructive', payload: { id: '42' } },
54
- { label: 'View Details' },
55
- ];
56
- const msg: ChatMessage = {
57
- id: 'msg-1',
58
- role: 'assistant',
59
- content: 'I found a record to delete.',
60
- timestamp: 1711500000000,
61
- actions,
62
- };
63
-
64
- const serialized = JSON.stringify(msg);
65
- const deserialized = JSON.parse(serialized) as ChatMessage;
66
-
67
- expect(deserialized.actions).toHaveLength(2);
68
- expect(deserialized.actions![0].label).toBe('Delete Record');
69
- expect(deserialized.actions![0].variant).toBe('destructive');
70
- expect(deserialized.actions![0].payload).toEqual({ id: '42' });
71
- expect(deserialized.actions![1].variant).toBeUndefined();
72
- });
73
-
74
- test('message without actions has undefined actions', () => {
75
- const msg: ChatMessage = {
76
- id: 'msg-2',
77
- role: 'user',
78
- content: 'Hello',
79
- timestamp: 1711500000000,
80
- };
81
- expect(msg.actions).toBeUndefined();
82
- });
83
-
84
- test('system message persists correctly', () => {
85
- const msg: ChatMessage = {
86
- id: 'ctx-1',
87
- role: 'system',
88
- content: '[Admin Context] Resource: posts | View: edit',
89
- timestamp: 1711500000000,
90
- };
91
- const parsed = JSON.parse(JSON.stringify(msg)) as ChatMessage;
92
- expect(parsed.role).toBe('system');
93
- expect(parsed.content).toContain('posts');
94
- });
95
- });
96
-
97
- describe('buildContextSystemMessage', () => {
98
- test('returns null when no resource', () => {
99
- expect(buildContextSystemMessage({})).toBeNull();
100
- });
101
-
102
- test('builds message with resource only', () => {
103
- const msg = buildContextSystemMessage({ currentResource: 'users' });
104
- expect(msg).not.toBeNull();
105
- expect(msg!.role).toBe('system');
106
- expect(msg!.content).toContain('Resource: users');
107
- });
108
-
109
- test('includes all context fields', () => {
110
- const msg = buildContextSystemMessage({
111
- currentResource: 'posts',
112
- currentView: 'edit',
113
- selectedRecordId: '42',
114
- pathname: '/posts/edit/42',
115
- });
116
- expect(msg!.content).toContain('Resource: posts');
117
- expect(msg!.content).toContain('View: edit');
118
- expect(msg!.content).toContain('Selected Record ID: 42');
119
- expect(msg!.content).toContain('Path: /posts/edit/42');
120
- });
121
-
122
- test('omits undefined fields', () => {
123
- const msg = buildContextSystemMessage({
124
- currentResource: 'users',
125
- currentView: 'list',
126
- });
127
- expect(msg!.content).not.toContain('Selected Record ID');
128
- expect(msg!.content).not.toContain('Path');
129
- });
130
- });
131
-
132
- describe('ChatContext shape', () => {
133
- test('empty context is valid', () => {
134
- const ctx: ChatContext = {};
135
- expect(ctx.currentResource).toBeUndefined();
136
- });
137
-
138
- test('full context preserves all fields', () => {
139
- const ctx: ChatContext = {
140
- currentResource: 'orders',
141
- selectedRecordId: '99',
142
- currentView: 'show',
143
- pathname: '/orders/show/99',
144
- };
145
- expect(ctx.currentResource).toBe('orders');
146
- expect(ctx.selectedRecordId).toBe('99');
147
- expect(ctx.currentView).toBe('show');
148
- expect(ctx.pathname).toBe('/orders/show/99');
149
- });
150
- });
151
-
152
- describe('localStorage persistence simulation', () => {
153
- test('messages array round-trips through JSON', () => {
154
- const messages: ChatMessage[] = [
155
- { id: '1', role: 'user', content: 'Hello', timestamp: 1000 },
156
- {
157
- id: '2', role: 'assistant', content: 'Hi!', timestamp: 1001,
158
- actions: [{ label: 'Create Post', variant: 'default', payload: { resource: 'posts' } }],
159
- },
160
- ];
161
- const json = JSON.stringify(messages);
162
- const restored = JSON.parse(json) as ChatMessage[];
163
-
164
- expect(restored).toHaveLength(2);
165
- expect(restored[0].role).toBe('user');
166
- expect(restored[1].actions![0].label).toBe('Create Post');
167
- });
168
- });
169
-
170
- // ─── AgentProvider Types ───────────────────────────────────────
171
-
172
- /** Mirror of AdminToolParameter from chatProvider.svelte.ts */
173
- interface AdminToolParameter {
174
- type: 'string' | 'number' | 'boolean' | 'object' | 'array';
175
- description?: string;
176
- enum?: string[];
177
- required?: boolean;
178
- }
179
-
180
- /** Mirror of AdminTool from chatProvider.svelte.ts */
181
- interface AdminTool {
182
- name: string;
183
- description: string;
184
- parameters: Record<string, AdminToolParameter>;
185
- needsApproval?: boolean;
186
- execute(args: Record<string, unknown>): Promise<{ success: boolean; data?: unknown; error?: string }>;
187
- }
188
-
189
- /** Mirror of AgentEvent from chatProvider.svelte.ts */
190
- type AgentEvent =
191
- | { type: 'text'; content: string }
192
- | { type: 'tool_call'; tool: string; args: Record<string, unknown> }
193
- | { type: 'approval_request'; id: string; tool: string; args: Record<string, unknown>; description: string }
194
- | { type: 'tool_result'; tool: string; result: { success: boolean; data?: unknown; error?: string } }
195
- | { type: 'component'; name: string; props: Record<string, unknown> }
196
- | { type: 'done' };
197
-
198
- describe('AdminTool shape', () => {
199
- test('tool with needsApproval serializes correctly', () => {
200
- const tool: AdminTool = {
201
- name: 'deletePosts',
202
- description: 'Delete posts matching a filter',
203
- parameters: {
204
- status: { type: 'string', description: 'Filter by status', enum: ['draft', 'archived'] },
205
- },
206
- needsApproval: true,
207
- execute: async (args) => ({ success: true, data: { deleted: args.ids } }),
208
- };
209
-
210
- expect(tool.name).toBe('deletePosts');
211
- expect(tool.needsApproval).toBe(true);
212
- expect(tool.parameters.status.enum).toEqual(['draft', 'archived']);
213
- });
214
-
215
- test('tool without approval defaults to undefined', () => {
216
- const tool: AdminTool = {
217
- name: 'getStats',
218
- description: 'Fetch dashboard statistics',
219
- parameters: {},
220
- execute: async () => ({ success: true, data: { users: 100 } }),
221
- };
222
-
223
- expect(tool.needsApproval).toBeUndefined();
224
- });
225
-
226
- test('tool execute returns ToolResult', async () => {
227
- const tool: AdminTool = {
228
- name: 'createPost',
229
- description: 'Create a new post',
230
- parameters: {
231
- title: { type: 'string', required: true },
232
- },
233
- execute: async (args) => ({
234
- success: true,
235
- data: { id: '1', title: args.title },
236
- }),
237
- };
238
-
239
- const result = await tool.execute({ title: 'Hello World' });
240
- expect(result.success).toBe(true);
241
- expect(result.data).toEqual({ id: '1', title: 'Hello World' });
242
- });
243
-
244
- test('tool execute can return error', async () => {
245
- const tool: AdminTool = {
246
- name: 'failingTool',
247
- description: 'A tool that fails',
248
- parameters: {},
249
- execute: async () => ({
250
- success: false,
251
- error: 'Permission denied',
252
- }),
253
- };
254
-
255
- const result = await tool.execute({});
256
- expect(result.success).toBe(false);
257
- expect(result.error).toBe('Permission denied');
258
- });
259
- });
260
-
261
- describe('AgentEvent stream', () => {
262
- test('events can be collected from async generator', async () => {
263
- async function* mockAgent(): AsyncGenerator<AgentEvent, void, unknown> {
264
- yield { type: 'text', content: 'Searching...' };
265
- yield { type: 'tool_call', tool: 'getList', args: { resource: 'posts' } };
266
- yield { type: 'tool_result', tool: 'getList', result: { success: true, data: [{ id: 1 }] } };
267
- yield { type: 'text', content: 'Found 1 post.' };
268
- yield { type: 'done' };
269
- }
270
-
271
- const events: AgentEvent[] = [];
272
- for await (const event of mockAgent()) {
273
- events.push(event);
274
- }
275
-
276
- expect(events).toHaveLength(5);
277
- expect(events[0].type).toBe('text');
278
- expect(events[1].type).toBe('tool_call');
279
- expect(events[2].type).toBe('tool_result');
280
- expect(events[4].type).toBe('done');
281
- });
282
-
283
- test('approval_request event serializes correctly', () => {
284
- const event: AgentEvent = {
285
- type: 'approval_request',
286
- id: 'req-42',
287
- tool: 'deletePosts',
288
- args: { ids: ['1', '2', '3'] },
289
- description: 'Delete 3 draft posts',
290
- };
291
-
292
- const serialized = JSON.stringify(event);
293
- const parsed = JSON.parse(serialized) as AgentEvent;
294
-
295
- expect(parsed.type).toBe('approval_request');
296
- if (parsed.type === 'approval_request') {
297
- expect(parsed.id).toBe('req-42');
298
- expect(parsed.tool).toBe('deletePosts');
299
- expect(parsed.description).toBe('Delete 3 draft posts');
300
- }
301
- });
302
-
303
- test('component event carries name and props', () => {
304
- const event: AgentEvent = {
305
- type: 'component',
306
- name: 'SalesChart',
307
- props: { period: 'monthly', data: [100, 200, 150] },
308
- };
309
-
310
- expect(event.type).toBe('component');
311
- if (event.type === 'component') {
312
- expect(event.name).toBe('SalesChart');
313
- expect(event.props.period).toBe('monthly');
314
- }
315
- });
316
- });
317
-
318
- describe('Approval flow', () => {
319
- test('registerApproval and resolveApproval work together', () => {
320
- // Simulate the approval flow without importing rune-based module
321
- const pending = new Map<string, (approved: boolean) => void>();
322
-
323
- function register(id: string, cb: (approved: boolean) => void) {
324
- pending.set(id, cb);
325
- }
326
-
327
- function resolve(id: string, approved: boolean): boolean {
328
- const cb = pending.get(id);
329
- if (!cb) return false;
330
- cb(approved);
331
- pending.delete(id);
332
- return true;
333
- }
334
-
335
- let approvalResult = false;
336
-
337
- register('req-1', (approved) => {
338
- approvalResult = approved;
339
- });
340
-
341
- expect(pending.size).toBe(1);
342
-
343
- // Resolve with approval
344
- const resolved = resolve('req-1', true);
345
- expect(resolved).toBe(true);
346
- expect(approvalResult).toEqual(true);
347
- expect(pending.size).toBe(0);
348
- });
349
-
350
- test('resolveApproval returns false for unknown id', () => {
351
- const pending = new Map<string, (approved: boolean) => void>();
352
-
353
- function resolve(id: string, approved: boolean): boolean {
354
- const cb = pending.get(id);
355
- if (!cb) return false;
356
- cb(approved);
357
- pending.delete(id);
358
- return true;
359
- }
360
-
361
- expect(resolve('nonexistent', true)).toBe(false);
362
- });
363
-
364
- test('rejection flow works', () => {
365
- const pending = new Map<string, (approved: boolean) => void>();
366
-
367
- function register(id: string, cb: (approved: boolean) => void) {
368
- pending.set(id, cb);
369
- }
370
-
371
- function resolve(id: string, approved: boolean): boolean {
372
- const cb = pending.get(id);
373
- if (!cb) return false;
374
- cb(approved);
375
- pending.delete(id);
376
- return true;
377
- }
378
-
379
- let wasApproved = true;
380
-
381
- register('req-2', (approved) => {
382
- wasApproved = approved;
383
- });
384
-
385
- resolve('req-2', false);
386
- expect(wasApproved).toEqual(false);
387
- });
388
- });