@dangao/bun-server 2.3.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (147) hide show
  1. package/README.md +81 -3
  2. package/dist/auth/jwt.d.ts.map +1 -1
  3. package/dist/config/service.d.ts +0 -1
  4. package/dist/config/service.d.ts.map +1 -1
  5. package/dist/core/application.d.ts +13 -0
  6. package/dist/core/application.d.ts.map +1 -1
  7. package/dist/core/cluster.d.ts.map +1 -1
  8. package/dist/core/server.d.ts +12 -9
  9. package/dist/core/server.d.ts.map +1 -1
  10. package/dist/dashboard/controller.d.ts.map +1 -1
  11. package/dist/database/connection-pool.d.ts +3 -3
  12. package/dist/database/connection-pool.d.ts.map +1 -1
  13. package/dist/database/sql-manager.d.ts +8 -4
  14. package/dist/database/sql-manager.d.ts.map +1 -1
  15. package/dist/database/sqlite-adapter.d.ts +7 -3
  16. package/dist/database/sqlite-adapter.d.ts.map +1 -1
  17. package/dist/debug/recorder.d.ts +0 -1
  18. package/dist/debug/recorder.d.ts.map +1 -1
  19. package/dist/files/static-middleware.d.ts.map +1 -1
  20. package/dist/files/storage.d.ts.map +1 -1
  21. package/dist/index.d.ts +2 -0
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +40264 -3542
  24. package/dist/index.node.mjs +17689 -0
  25. package/dist/middleware/builtin/static-file.d.ts +4 -2
  26. package/dist/middleware/builtin/static-file.d.ts.map +1 -1
  27. package/dist/platform/bun/crypto.d.ts +3 -0
  28. package/dist/platform/bun/crypto.d.ts.map +1 -0
  29. package/dist/platform/bun/fs.d.ts +3 -0
  30. package/dist/platform/bun/fs.d.ts.map +1 -0
  31. package/dist/platform/bun/http.d.ts +15 -0
  32. package/dist/platform/bun/http.d.ts.map +1 -0
  33. package/dist/platform/bun/index.d.ts +3 -0
  34. package/dist/platform/bun/index.d.ts.map +1 -0
  35. package/dist/platform/bun/parser.d.ts +3 -0
  36. package/dist/platform/bun/parser.d.ts.map +1 -0
  37. package/dist/platform/bun/process.d.ts +3 -0
  38. package/dist/platform/bun/process.d.ts.map +1 -0
  39. package/dist/platform/detector.d.ts +9 -0
  40. package/dist/platform/detector.d.ts.map +1 -0
  41. package/dist/platform/index.d.ts +4 -0
  42. package/dist/platform/index.d.ts.map +1 -0
  43. package/dist/platform/node/crypto.d.ts +3 -0
  44. package/dist/platform/node/crypto.d.ts.map +1 -0
  45. package/dist/platform/node/fs.d.ts +3 -0
  46. package/dist/platform/node/fs.d.ts.map +1 -0
  47. package/dist/platform/node/http.d.ts +3 -0
  48. package/dist/platform/node/http.d.ts.map +1 -0
  49. package/dist/platform/node/index.d.ts +3 -0
  50. package/dist/platform/node/index.d.ts.map +1 -0
  51. package/dist/platform/node/parser.d.ts +3 -0
  52. package/dist/platform/node/parser.d.ts.map +1 -0
  53. package/dist/platform/node/process.d.ts +3 -0
  54. package/dist/platform/node/process.d.ts.map +1 -0
  55. package/dist/platform/runtime.d.ts +14 -0
  56. package/dist/platform/runtime.d.ts.map +1 -0
  57. package/dist/platform/types.d.ts +139 -0
  58. package/dist/platform/types.d.ts.map +1 -0
  59. package/dist/prompt/stores/file-store.d.ts.map +1 -1
  60. package/dist/rag/service.d.ts.map +1 -1
  61. package/dist/request/response.d.ts +3 -1
  62. package/dist/request/response.d.ts.map +1 -1
  63. package/dist/security/guards/execution-context.d.ts +2 -2
  64. package/dist/security/guards/execution-context.d.ts.map +1 -1
  65. package/dist/security/guards/types.d.ts +2 -2
  66. package/dist/security/guards/types.d.ts.map +1 -1
  67. package/dist/swagger/generator.d.ts.map +1 -1
  68. package/dist/websocket/registry.d.ts +4 -4
  69. package/dist/websocket/registry.d.ts.map +1 -1
  70. package/docs/deployment.md +31 -7
  71. package/docs/design/query-interceptor-design.md +381 -0
  72. package/docs/idle-timeout.md +6 -4
  73. package/docs/migration.md +43 -0
  74. package/docs/platform.md +299 -0
  75. package/docs/testing.md +60 -0
  76. package/docs/zh/deployment.md +30 -7
  77. package/docs/zh/idle-timeout.md +6 -4
  78. package/docs/zh/migration.md +42 -0
  79. package/docs/zh/platform.md +299 -0
  80. package/docs/zh/testing.md +60 -0
  81. package/package.json +24 -6
  82. package/src/auth/jwt.ts +4 -3
  83. package/src/config/service.ts +7 -6
  84. package/src/core/application.ts +19 -1
  85. package/src/core/cluster.ts +16 -14
  86. package/src/core/server.ts +48 -35
  87. package/src/dashboard/controller.ts +3 -2
  88. package/src/database/connection-pool.ts +32 -20
  89. package/src/database/database-module.ts +1 -1
  90. package/src/database/db-proxy.ts +2 -2
  91. package/src/database/orm/transaction-manager.ts +1 -1
  92. package/src/database/sql-manager.ts +48 -13
  93. package/src/database/sqlite-adapter.ts +45 -12
  94. package/src/debug/recorder.ts +4 -3
  95. package/src/files/static-middleware.ts +3 -2
  96. package/src/files/storage.ts +2 -1
  97. package/src/index.ts +13 -0
  98. package/src/middleware/builtin/static-file.ts +8 -5
  99. package/src/platform/bun/crypto.ts +30 -0
  100. package/src/platform/bun/fs.ts +52 -0
  101. package/src/platform/bun/http.ts +106 -0
  102. package/src/platform/bun/index.ts +17 -0
  103. package/src/platform/bun/parser.ts +19 -0
  104. package/src/platform/bun/process.ts +37 -0
  105. package/src/platform/detector.ts +36 -0
  106. package/src/platform/index.ts +20 -0
  107. package/src/platform/node/crypto.ts +40 -0
  108. package/src/platform/node/fs.ts +115 -0
  109. package/src/platform/node/http.ts +196 -0
  110. package/src/platform/node/index.ts +17 -0
  111. package/src/platform/node/parser.ts +34 -0
  112. package/src/platform/node/process.ts +51 -0
  113. package/src/platform/runtime.ts +50 -0
  114. package/src/platform/types.ts +150 -0
  115. package/src/prompt/stores/file-store.ts +6 -5
  116. package/src/rag/service.ts +2 -1
  117. package/src/request/response.ts +7 -4
  118. package/src/security/guards/execution-context.ts +4 -4
  119. package/src/security/guards/types.ts +2 -2
  120. package/src/swagger/generator.ts +2 -1
  121. package/src/websocket/registry.ts +6 -7
  122. package/tests/controller/path-combination.test.ts +196 -2
  123. package/tests/files/static-middleware.test.ts +5 -2
  124. package/tests/middleware/static-file.test.ts +5 -2
  125. package/tests/platform/bun/crypto.test.ts +8 -0
  126. package/tests/platform/bun/database.test.ts +8 -0
  127. package/tests/platform/bun/fs.test.ts +8 -0
  128. package/tests/platform/bun/parser.test.ts +8 -0
  129. package/tests/platform/bun/process.test.ts +8 -0
  130. package/tests/platform/bun/websocket.test.ts +8 -0
  131. package/tests/platform/detector.test.ts +57 -0
  132. package/tests/platform/node/build-smoke.test.ts +92 -0
  133. package/tests/platform/node/crypto.test.ts +9 -0
  134. package/tests/platform/node/database.test.ts +9 -0
  135. package/tests/platform/node/fs.test.ts +9 -0
  136. package/tests/platform/node/parser.test.ts +9 -0
  137. package/tests/platform/node/process.test.ts +9 -0
  138. package/tests/platform/node/websocket.test.ts +9 -0
  139. package/tests/platform/shared/crypto.cases.ts +49 -0
  140. package/tests/platform/shared/database.cases.ts +43 -0
  141. package/tests/platform/shared/fs.cases.ts +82 -0
  142. package/tests/platform/shared/parser.cases.ts +55 -0
  143. package/tests/platform/shared/process.cases.ts +26 -0
  144. package/tests/platform/shared/suite.ts +33 -0
  145. package/tests/platform/shared/websocket.cases.ts +61 -0
  146. package/tests/request/response.test.ts +5 -2
  147. package/tests/router/router-extended.test.ts +53 -0
@@ -1,17 +1,20 @@
1
1
  import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
2
- import { mkdtemp, rm } from 'node:fs/promises';
2
+ import { mkdtemp, rm, writeFile } from 'node:fs/promises';
3
3
  import { tmpdir } from 'node:os';
4
4
  import { join } from 'node:path';
5
5
 
6
6
  import { createStaticFileMiddleware } from '../../src/files/static-middleware';
7
7
  import { Context } from '../../src/core/context';
8
+ import { initRuntime } from '../../src/platform/runtime';
9
+
10
+ initRuntime('bun');
8
11
 
9
12
  describe('Static File Middleware', () => {
10
13
  let root: string;
11
14
 
12
15
  beforeEach(async () => {
13
16
  root = await mkdtemp(join(tmpdir(), 'bun-static-'));
14
- await Bun.write(join(root, 'hello.txt'), 'hello world');
17
+ await writeFile(join(root, 'hello.txt'), 'hello world', 'utf-8');
15
18
  });
16
19
 
17
20
  afterEach(async () => {
@@ -1,14 +1,17 @@
1
1
  import { afterAll, beforeAll, describe, expect, test } from 'bun:test';
2
- import { mkdir, rm } from 'fs/promises';
2
+ import { mkdir, rm, writeFile as fsWriteFile } from 'fs/promises';
3
3
  import { join } from 'path';
4
4
 
5
5
  import { createStaticFileMiddleware } from '../../src/middleware/builtin/static-file';
6
6
  import { Context } from '../../src/core/context';
7
+ import { initRuntime } from '../../src/platform/runtime';
8
+
9
+ initRuntime('bun');
7
10
 
8
11
  const TMP_DIR = join(process.cwd(), 'tmp-static-test');
9
12
 
10
13
  async function writeFile(path: string, content: string): Promise<void> {
11
- await Bun.write(path, content);
14
+ await fsWriteFile(path, content, 'utf-8');
12
15
  }
13
16
 
14
17
  describe('StaticFileMiddleware', () => {
@@ -0,0 +1,8 @@
1
+ import { describe, test, expect } from 'bun:test';
2
+ import { initRuntime, getRuntime } from '../../../src/platform/runtime';
3
+ import { runCryptoCases } from '../shared/crypto.cases';
4
+
5
+ describe('BunCryptoAdapter', () => {
6
+ initRuntime('bun');
7
+ runCryptoCases({ test, expect: expect as any, beforeEach: () => {} }, () => getRuntime().crypto);
8
+ });
@@ -0,0 +1,8 @@
1
+ import { describe, test, expect } from 'bun:test';
2
+ import { initRuntime } from '../../../src/platform/runtime';
3
+ import { runDatabaseCases } from '../shared/database.cases';
4
+
5
+ describe('BunDatabase', () => {
6
+ initRuntime('bun');
7
+ runDatabaseCases({ test, expect: expect as any, beforeEach: () => {} }, 'bun');
8
+ });
@@ -0,0 +1,8 @@
1
+ import { describe, test, expect, beforeEach } from 'bun:test';
2
+ import { initRuntime, getRuntime } from '../../../src/platform/runtime';
3
+ import { runFsCases } from '../shared/fs.cases';
4
+
5
+ describe('BunFsAdapter', () => {
6
+ initRuntime('bun');
7
+ runFsCases({ test, expect: expect as any, beforeEach }, () => getRuntime().fs);
8
+ });
@@ -0,0 +1,8 @@
1
+ import { describe, test, expect } from 'bun:test';
2
+ import { initRuntime, getRuntime } from '../../../src/platform/runtime';
3
+ import { runParserCases } from '../shared/parser.cases';
4
+
5
+ describe('BunParserAdapter', () => {
6
+ initRuntime('bun');
7
+ runParserCases({ test, expect: expect as any, beforeEach: () => {} }, () => getRuntime().parser);
8
+ });
@@ -0,0 +1,8 @@
1
+ import { describe, test, expect } from 'bun:test';
2
+ import { initRuntime, getRuntime } from '../../../src/platform/runtime';
3
+ import { runProcessCases } from '../shared/process.cases';
4
+
5
+ describe('BunProcessAdapter', () => {
6
+ initRuntime('bun');
7
+ runProcessCases({ test, expect: expect as any, beforeEach: () => {} }, () => getRuntime().process);
8
+ });
@@ -0,0 +1,8 @@
1
+ import { describe, test, expect } from 'bun:test';
2
+ import { initRuntime, getRuntime } from '../../../src/platform/runtime';
3
+ import { runWebSocketCases } from '../shared/websocket.cases';
4
+
5
+ describe('BunWebSocket', () => {
6
+ initRuntime('bun');
7
+ runWebSocketCases({ test, expect: expect as any, beforeEach: () => {} }, () => getRuntime());
8
+ });
@@ -0,0 +1,57 @@
1
+ import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
2
+ import { resolvePlatform } from '../../src/platform/detector';
3
+
4
+ describe('resolvePlatform priority chain', () => {
5
+ const originalArgv = process.argv.slice();
6
+ const originalEnv = process.env['BUN_SERVER_PLATFORM'];
7
+
8
+ afterEach(() => {
9
+ process.argv.length = 0;
10
+ for (const arg of originalArgv) process.argv.push(arg);
11
+ if (originalEnv === undefined) {
12
+ delete process.env['BUN_SERVER_PLATFORM'];
13
+ } else {
14
+ process.env['BUN_SERVER_PLATFORM'] = originalEnv;
15
+ }
16
+ });
17
+
18
+ test('bootstrap config takes highest priority over CLI arg', () => {
19
+ process.argv.push('--platform=node');
20
+ expect(resolvePlatform('bun')).toBe('bun');
21
+ });
22
+
23
+ test('CLI arg --platform=node', () => {
24
+ delete process.env['BUN_SERVER_PLATFORM'];
25
+ process.argv.push('--platform=node');
26
+ expect(resolvePlatform()).toBe('node');
27
+ });
28
+
29
+ test('CLI arg --platform=bun', () => {
30
+ delete process.env['BUN_SERVER_PLATFORM'];
31
+ process.argv.push('--platform=bun');
32
+ expect(resolvePlatform()).toBe('bun');
33
+ });
34
+
35
+ test('env var BUN_SERVER_PLATFORM=node', () => {
36
+ process.env['BUN_SERVER_PLATFORM'] = 'node';
37
+ expect(resolvePlatform()).toBe('node');
38
+ });
39
+
40
+ test('env var BUN_SERVER_PLATFORM=bun', () => {
41
+ process.env['BUN_SERVER_PLATFORM'] = 'bun';
42
+ expect(resolvePlatform()).toBe('bun');
43
+ });
44
+
45
+ test('CLI arg takes priority over env var', () => {
46
+ process.env['BUN_SERVER_PLATFORM'] = 'node';
47
+ process.argv.push('--platform=bun');
48
+ expect(resolvePlatform()).toBe('bun');
49
+ });
50
+
51
+ test('auto-detect returns bun when running in Bun', () => {
52
+ delete process.env['BUN_SERVER_PLATFORM'];
53
+ // We're running in Bun (bun:test), so auto-detect should return 'bun'
54
+ const result = resolvePlatform();
55
+ expect(result).toBe('bun');
56
+ });
57
+ });
@@ -0,0 +1,92 @@
1
+ import { describe, test, expect } from 'vitest';
2
+ import { spawn } from 'node:child_process';
3
+ import { writeFileSync, mkdirSync, rmSync } from 'node:fs';
4
+ import { join, resolve } from 'node:path';
5
+ import { tmpdir } from 'node:os';
6
+
7
+ /**
8
+ * 烟雾测试:验证 bun build --target=node 的输出能被 Node.js 原生运行
9
+ *
10
+ * 测试流程:
11
+ * 1. 创建最小化 app fixture
12
+ * 2. 使用 bun build --target=node 编译
13
+ * 3. 用 node 运行编译产物
14
+ * 4. 验证应用启动成功
15
+ */
16
+ describe('Build Smoke Test (bun build --target=node)', () => {
17
+ test('compiled output runs on Node.js', async () => {
18
+ const tmpDir = join(tmpdir(), `build-smoke-${Date.now()}`);
19
+ mkdirSync(tmpDir, { recursive: true });
20
+
21
+ const outDir = join(tmpDir, 'out');
22
+ mkdirSync(outDir, { recursive: true });
23
+
24
+ // Create a minimal fixture app using the framework
25
+ const fixturePath = join(tmpDir, 'fixture.ts');
26
+ const pkgRoot = resolve(__dirname, '../../..');
27
+
28
+ writeFileSync(fixturePath, `
29
+ import { Application } from '${pkgRoot}/src/index.ts';
30
+ import { Module } from '${pkgRoot}/src/index.ts';
31
+
32
+ @Module({})
33
+ class AppModule {}
34
+
35
+ const app = new Application({ platform: 'node' });
36
+ app.registerModule(AppModule);
37
+ app.listen(0).then(() => {
38
+ const server = app.getServer()?.getServer();
39
+ console.log('SMOKE_OK port=' + (server?.port ?? 0));
40
+ process.exit(0);
41
+ }).catch((err) => {
42
+ console.error('SMOKE_FAIL', err.message);
43
+ process.exit(1);
44
+ });
45
+ `);
46
+
47
+ const outFile = join(outDir, 'app.cjs');
48
+
49
+ // Build with bun
50
+ const buildResult = await runCommand('bun', [
51
+ 'build',
52
+ fixturePath,
53
+ '--target=node',
54
+ '--outfile=' + outFile,
55
+ '--format=cjs',
56
+ ]);
57
+
58
+ if (buildResult.exitCode !== 0) {
59
+ // Build may fail in CI without Bun; skip gracefully
60
+ console.warn('[build-smoke] bun build failed, skipping test:', buildResult.stderr);
61
+ return;
62
+ }
63
+
64
+ // Run compiled file with node
65
+ const nodeResult = await runCommand('node', [outFile], 5000);
66
+
67
+ try {
68
+ rmSync(tmpDir, { recursive: true, force: true });
69
+ } catch {
70
+ // ignore cleanup errors
71
+ }
72
+
73
+ expect(nodeResult.exitCode).toBe(0);
74
+ expect(nodeResult.stdout).toContain('SMOKE_OK');
75
+ }, 30000);
76
+ });
77
+
78
+ function runCommand(
79
+ cmd: string,
80
+ args: string[],
81
+ timeout = 15000,
82
+ ): Promise<{ exitCode: number; stdout: string; stderr: string }> {
83
+ return new Promise((resolve) => {
84
+ const child = spawn(cmd, args, { timeout });
85
+ let stdout = '';
86
+ let stderr = '';
87
+ child.stdout?.on('data', (d) => { stdout += d.toString(); });
88
+ child.stderr?.on('data', (d) => { stderr += d.toString(); });
89
+ child.on('close', (code) => resolve({ exitCode: code ?? 1, stdout, stderr }));
90
+ child.on('error', (err) => resolve({ exitCode: 1, stdout, stderr: err.message }));
91
+ });
92
+ }
@@ -0,0 +1,9 @@
1
+ import { describe, test, expect } from 'vitest';
2
+ import { initRuntime, getRuntime, _resetRuntime } from '../../../src/platform/runtime';
3
+ import { runCryptoCases } from '../shared/crypto.cases';
4
+
5
+ describe('NodeCryptoAdapter', () => {
6
+ _resetRuntime();
7
+ initRuntime('node');
8
+ runCryptoCases({ test, expect: expect as any, beforeEach: () => {} }, () => getRuntime().crypto);
9
+ });
@@ -0,0 +1,9 @@
1
+ import { describe, test, expect } from 'vitest';
2
+ import { initRuntime, _resetRuntime } from '../../../src/platform/runtime';
3
+ import { runDatabaseCases } from '../shared/database.cases';
4
+
5
+ describe('NodeDatabase', () => {
6
+ _resetRuntime();
7
+ initRuntime('node');
8
+ runDatabaseCases({ test, expect: expect as any, beforeEach: () => {} }, 'node');
9
+ });
@@ -0,0 +1,9 @@
1
+ import { describe, test, expect, beforeEach } from 'vitest';
2
+ import { initRuntime, getRuntime, _resetRuntime } from '../../../src/platform/runtime';
3
+ import { runFsCases } from '../shared/fs.cases';
4
+
5
+ describe('NodeFsAdapter', () => {
6
+ _resetRuntime();
7
+ initRuntime('node');
8
+ runFsCases({ test, expect: expect as any, beforeEach }, () => getRuntime().fs);
9
+ });
@@ -0,0 +1,9 @@
1
+ import { describe, test, expect } from 'vitest';
2
+ import { initRuntime, getRuntime, _resetRuntime } from '../../../src/platform/runtime';
3
+ import { runParserCases } from '../shared/parser.cases';
4
+
5
+ describe('NodeParserAdapter', () => {
6
+ _resetRuntime();
7
+ initRuntime('node');
8
+ runParserCases({ test, expect: expect as any, beforeEach: () => {} }, () => getRuntime().parser);
9
+ });
@@ -0,0 +1,9 @@
1
+ import { describe, test, expect } from 'vitest';
2
+ import { initRuntime, getRuntime, _resetRuntime } from '../../../src/platform/runtime';
3
+ import { runProcessCases } from '../shared/process.cases';
4
+
5
+ describe('NodeProcessAdapter', () => {
6
+ _resetRuntime();
7
+ initRuntime('node');
8
+ runProcessCases({ test, expect: expect as any, beforeEach: () => {} }, () => getRuntime().process);
9
+ });
@@ -0,0 +1,9 @@
1
+ import { describe, test, expect } from 'vitest';
2
+ import { initRuntime, getRuntime, _resetRuntime } from '../../../src/platform/runtime';
3
+ import { runWebSocketCases } from '../shared/websocket.cases';
4
+
5
+ describe('NodeWebSocket', () => {
6
+ _resetRuntime();
7
+ initRuntime('node');
8
+ runWebSocketCases({ test, expect: expect as any, beforeEach: () => {} }, () => getRuntime());
9
+ });
@@ -0,0 +1,49 @@
1
+ import type { TestSuite } from './suite';
2
+ import type { ICryptoAdapter } from '../../../src/platform/types';
3
+
4
+ export function runCryptoCases(suite: TestSuite, getAdapter: () => ICryptoAdapter): void {
5
+ const { test, expect } = suite;
6
+
7
+ test('sha256 hex digest', () => {
8
+ const adapter = getAdapter();
9
+ const hasher = adapter.createHasher('sha256');
10
+ hasher.update('hello');
11
+ const hex = hasher.digest('hex');
12
+ // Known sha256('hello')
13
+ expect(hex).toBe('2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824');
14
+ });
15
+
16
+ test('sha256 base64 digest', () => {
17
+ const adapter = getAdapter();
18
+ const hasher = adapter.createHasher('sha256');
19
+ hasher.update('hello');
20
+ const b64 = hasher.digest('base64');
21
+ expect(typeof b64).toBe('string');
22
+ expect(b64.length).toBeGreaterThan(0);
23
+ });
24
+
25
+ test('sha256 arrayBuffer digest', () => {
26
+ const adapter = getAdapter();
27
+ const hasher = adapter.createHasher('sha256');
28
+ hasher.update('hello');
29
+ const buf = hasher.digest();
30
+ // Both Bun and Node return Buffer (extends Uint8Array), not a plain ArrayBuffer
31
+ expect(buf instanceof Uint8Array).toBe(true);
32
+ expect(buf.byteLength).toBe(32);
33
+ });
34
+
35
+ test('chaining update calls', () => {
36
+ const adapter = getAdapter();
37
+ // sha256('helloworld') should equal updating with 'hello' then 'world' only if same bytes
38
+ // We simply test that the result is consistent
39
+ const h1 = adapter.createHasher('sha256');
40
+ h1.update('hello').update('world');
41
+ const hex1 = h1.digest('hex');
42
+
43
+ const h2 = adapter.createHasher('sha256');
44
+ h2.update('helloworld');
45
+ const hex2 = h2.digest('hex');
46
+
47
+ expect(hex1).toBe(hex2);
48
+ });
49
+ }
@@ -0,0 +1,43 @@
1
+ import { tmpdir } from 'node:os';
2
+ import { join } from 'node:path';
3
+ import type { TestSuite } from './suite';
4
+ import type { PlatformEngine } from '../../../src/platform/types';
5
+ import { SqliteManager } from '../../../src/database/sqlite-adapter';
6
+
7
+ export function runDatabaseCases(suite: TestSuite, engine: PlatformEngine): void {
8
+ const { test, expect } = suite;
9
+
10
+ // better-sqlite3 is not supported when running Node-platform tests under Bun runtime
11
+ const skipBetterSqlite = engine === 'node' && typeof (globalThis as any).Bun !== 'undefined';
12
+
13
+ test('SqliteAdapter: create table, insert and query', () => {
14
+ if (skipBetterSqlite) {
15
+ // better-sqlite3 is not supported in Bun — this test must run under Node.js
16
+ console.log('[skip] better-sqlite3 not available in Bun runtime; run with vitest for Node platform');
17
+ return;
18
+ }
19
+ const dbPath = join(tmpdir(), `platform-db-test-${engine}-${Date.now()}.db`);
20
+ const manager = new SqliteManager();
21
+ const adapter = manager.getOrCreate('test', { database: dbPath, wal: false });
22
+
23
+ adapter.query('CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT)');
24
+ adapter.query("INSERT INTO users (name) VALUES ('Alice')");
25
+ const rows = adapter.query<{ id: number; name: string }>('SELECT * FROM users');
26
+
27
+ expect(rows.length).toBeGreaterThanOrEqual(1);
28
+ expect(rows[0]!.name).toBe('Alice');
29
+
30
+ manager.destroy('test');
31
+ });
32
+
33
+ test('SqliteManager.getDefault() throws when not initialized', () => {
34
+ const manager = new SqliteManager();
35
+ let threw = false;
36
+ try {
37
+ manager.getDefault();
38
+ } catch {
39
+ threw = true;
40
+ }
41
+ expect(threw).toBe(true);
42
+ });
43
+ }
@@ -0,0 +1,82 @@
1
+ import { tmpdir } from 'node:os';
2
+ import { join } from 'node:path';
3
+ import { mkdirSync, rmSync } from 'node:fs';
4
+ import type { TestSuite } from './suite';
5
+ import type { IFsAdapter } from '../../../src/platform/types';
6
+
7
+ export function runFsCases(suite: TestSuite, getAdapter: () => IFsAdapter): void {
8
+ const { test, expect, beforeEach } = suite;
9
+
10
+ let tmpDir: string;
11
+
12
+ beforeEach(() => {
13
+ tmpDir = join(tmpdir(), `platform-fs-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
14
+ mkdirSync(tmpDir, { recursive: true });
15
+ });
16
+
17
+ test('write and read text', async () => {
18
+ const adapter = getAdapter();
19
+ const filePath = join(tmpDir, 'hello.txt');
20
+ await adapter.write(filePath, 'hello world');
21
+ const content = await adapter.file(filePath).text();
22
+ expect(content).toBe('hello world');
23
+ });
24
+
25
+ test('write and read bytes', async () => {
26
+ const adapter = getAdapter();
27
+ const filePath = join(tmpDir, 'bytes.bin');
28
+ const data = new Uint8Array([1, 2, 3, 4, 5]);
29
+ await adapter.write(filePath, data);
30
+ const result = await adapter.file(filePath).bytes();
31
+ expect(result[0]).toBe(1);
32
+ expect(result[4]).toBe(5);
33
+ });
34
+
35
+ test('file.exists() returns true for existing file', async () => {
36
+ const adapter = getAdapter();
37
+ const filePath = join(tmpDir, 'exists.txt');
38
+ await adapter.write(filePath, 'content');
39
+ const exists = await adapter.file(filePath).exists();
40
+ expect(exists).toBe(true);
41
+ });
42
+
43
+ test('file.exists() returns false for missing file', async () => {
44
+ const adapter = getAdapter();
45
+ const filePath = join(tmpDir, 'does-not-exist.txt');
46
+ const exists = await adapter.file(filePath).exists();
47
+ expect(exists).toBe(false);
48
+ });
49
+
50
+ test('file.type returns MIME type', async () => {
51
+ const adapter = getAdapter();
52
+ const filePath = join(tmpDir, 'image.png');
53
+ await adapter.write(filePath, new Uint8Array([0x89, 0x50, 0x4e, 0x47]));
54
+ const mime = adapter.file(filePath).type;
55
+ expect(mime).toContain('png');
56
+ });
57
+
58
+ test('stream() produces readable stream with correct content', async () => {
59
+ const adapter = getAdapter();
60
+ const filePath = join(tmpDir, 'stream.txt');
61
+ await adapter.write(filePath, 'stream content');
62
+ const stream = adapter.file(filePath).stream();
63
+ const reader = stream.getReader();
64
+ const chunks: Uint8Array[] = [];
65
+ while (true) {
66
+ const { done, value } = await reader.read();
67
+ if (done) break;
68
+ chunks.push(value);
69
+ }
70
+ const combined = Buffer.concat(chunks).toString('utf-8');
71
+ expect(combined).toBe('stream content');
72
+ });
73
+
74
+ test('glob finds matching files', async () => {
75
+ const adapter = getAdapter();
76
+ await adapter.write(join(tmpDir, 'a.json'), '{}');
77
+ await adapter.write(join(tmpDir, 'b.json'), '{}');
78
+ await adapter.write(join(tmpDir, 'c.txt'), 'text');
79
+ const files = adapter.glob('*.json', tmpDir);
80
+ expect(files.length).toBeGreaterThanOrEqual(2);
81
+ });
82
+ }
@@ -0,0 +1,55 @@
1
+ import type { TestSuite } from './suite';
2
+ import type { IParserAdapter } from '../../../src/platform/types';
3
+
4
+ export function runParserCases(suite: TestSuite, getAdapter: () => IParserAdapter): void {
5
+ const { test, expect } = suite;
6
+
7
+ test('parseJSONC parses standard JSON', () => {
8
+ const adapter = getAdapter();
9
+ const result = adapter.parseJSONC('{"a": 1, "b": "hello"}') as Record<string, unknown>;
10
+ expect(result['a']).toBe(1);
11
+ expect(result['b']).toBe('hello');
12
+ });
13
+
14
+ test('parseJSONC ignores line comments', () => {
15
+ const adapter = getAdapter();
16
+ const result = adapter.parseJSONC('{\n// comment\n"a": 1}') as Record<string, unknown>;
17
+ expect(result['a']).toBe(1);
18
+ });
19
+
20
+ test('parseJSON5 parses JSON5 with trailing commas', () => {
21
+ const adapter = getAdapter();
22
+ const result = adapter.parseJSON5('{a: 1, b: "hello",}') as Record<string, unknown>;
23
+ expect(result['a']).toBe(1);
24
+ });
25
+
26
+ test('parseJSONL parses multiple JSON lines', () => {
27
+ const adapter = getAdapter();
28
+ const content = '{"id":1}\n{"id":2}\n{"id":3}';
29
+ const records = adapter.parseJSONL(content);
30
+ expect(records.length).toBe(3);
31
+ expect((records[0] as Record<string, unknown>)['id']).toBe(1);
32
+ expect((records[2] as Record<string, unknown>)['id']).toBe(3);
33
+ });
34
+
35
+ test('parseJSONL ignores empty lines', () => {
36
+ const adapter = getAdapter();
37
+ const content = '{"id":1}\n\n{"id":2}\n';
38
+ const records = adapter.parseJSONL(content);
39
+ expect(records.length).toBe(2);
40
+ });
41
+
42
+ test('renderMarkdown converts h1 heading', () => {
43
+ const adapter = getAdapter();
44
+ const html = adapter.renderMarkdown('# Hello World');
45
+ expect(html).toContain('<h1');
46
+ expect(html).toContain('Hello World');
47
+ });
48
+
49
+ test('renderMarkdown converts bold text', () => {
50
+ const adapter = getAdapter();
51
+ const html = adapter.renderMarkdown('**bold**');
52
+ expect(html).toContain('<strong>');
53
+ expect(html).toContain('bold');
54
+ });
55
+ }
@@ -0,0 +1,26 @@
1
+ import type { TestSuite } from './suite';
2
+ import type { IProcessAdapter } from '../../../src/platform/types';
3
+
4
+ export function runProcessCases(suite: TestSuite, getAdapter: () => IProcessAdapter): void {
5
+ const { test, expect } = suite;
6
+
7
+ test('sleep waits at least the specified time', async () => {
8
+ const adapter = getAdapter();
9
+ const start = Date.now();
10
+ await adapter.sleep(100);
11
+ const elapsed = Date.now() - start;
12
+ expect(elapsed).toBeGreaterThanOrEqual(90);
13
+ });
14
+
15
+ test('spawn runs a process and returns exit code 0', async () => {
16
+ const adapter = getAdapter();
17
+ const child = adapter.spawn({
18
+ cmd: ['node', '--version'],
19
+ stdout: 'pipe',
20
+ stderr: 'ignore',
21
+ });
22
+ expect(child.pid).toBeGreaterThan(0);
23
+ const exitCode = await child.exited;
24
+ expect(exitCode).toBe(0);
25
+ });
26
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * 框架无关的测试套件接口
3
+ * 让 shared cases 文件在 bun:test 和 vitest 中都能运行,无需任何适配层
4
+ */
5
+ export interface TestSuite {
6
+ test: (name: string, fn: () => void | Promise<void>) => void;
7
+ expect: (actual: unknown) => {
8
+ toBe(e: unknown): void;
9
+ toEqual(e: unknown): void;
10
+ toBeTruthy(): void;
11
+ toBeFalsy(): void;
12
+ toContain(e: unknown): void;
13
+ toBeGreaterThan(e: number): void;
14
+ toBeGreaterThanOrEqual(e: number): void;
15
+ toBeLessThan(e: number): void;
16
+ toBeLessThanOrEqual(e: number): void;
17
+ toBeInstanceOf(e: unknown): void;
18
+ toHaveLength(e: number): void;
19
+ not: {
20
+ toBe(e: unknown): void;
21
+ toBeNull(): void;
22
+ toBeUndefined(): void;
23
+ toThrow(): void;
24
+ };
25
+ resolves: {
26
+ toBeTruthy(): Promise<void>;
27
+ };
28
+ rejects: {
29
+ toThrow(): Promise<void>;
30
+ };
31
+ };
32
+ beforeEach: (fn: () => void | Promise<void>) => void;
33
+ }