bluera-knowledge 0.9.32 → 0.9.36
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/.claude/hooks/post-edit-check.sh +5 -3
- package/.claude/skills/atomic-commits/SKILL.md +3 -1
- package/.husky/pre-commit +3 -2
- package/.prettierrc +9 -0
- package/.versionrc.json +1 -1
- package/CHANGELOG.md +70 -0
- package/CLAUDE.md +6 -0
- package/README.md +25 -13
- package/bun.lock +277 -33
- package/dist/{chunk-L2YVNC63.js → chunk-6FHWC36B.js} +9 -1
- package/dist/chunk-6FHWC36B.js.map +1 -0
- package/dist/{chunk-RST4XGRL.js → chunk-DC7CGSGT.js} +288 -241
- package/dist/chunk-DC7CGSGT.js.map +1 -0
- package/dist/{chunk-6PBP5DVD.js → chunk-WFNPNAAP.js} +3212 -3054
- package/dist/chunk-WFNPNAAP.js.map +1 -0
- package/dist/{chunk-WT2DAEO7.js → chunk-Z2KKVH45.js} +548 -482
- package/dist/chunk-Z2KKVH45.js.map +1 -0
- package/dist/index.js +871 -758
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.js +3 -3
- package/dist/watch.service-BJV3TI3F.js +7 -0
- package/dist/workers/background-worker-cli.js +97 -71
- package/dist/workers/background-worker-cli.js.map +1 -1
- package/eslint.config.js +43 -1
- package/package.json +18 -11
- package/plugin.json +8 -0
- package/python/requirements.txt +1 -1
- package/src/analysis/ast-parser.test.ts +12 -11
- package/src/analysis/ast-parser.ts +28 -22
- package/src/analysis/code-graph.test.ts +52 -62
- package/src/analysis/code-graph.ts +9 -13
- package/src/analysis/dependency-usage-analyzer.test.ts +91 -271
- package/src/analysis/dependency-usage-analyzer.ts +52 -24
- package/src/analysis/go-ast-parser.test.ts +22 -22
- package/src/analysis/go-ast-parser.ts +18 -25
- package/src/analysis/parser-factory.test.ts +9 -9
- package/src/analysis/parser-factory.ts +3 -3
- package/src/analysis/python-ast-parser.test.ts +27 -27
- package/src/analysis/python-ast-parser.ts +2 -2
- package/src/analysis/repo-url-resolver.test.ts +82 -82
- package/src/analysis/rust-ast-parser.test.ts +19 -19
- package/src/analysis/rust-ast-parser.ts +17 -27
- package/src/analysis/tree-sitter-parser.test.ts +3 -3
- package/src/analysis/tree-sitter-parser.ts +10 -16
- package/src/cli/commands/crawl.test.ts +40 -24
- package/src/cli/commands/crawl.ts +186 -166
- package/src/cli/commands/index-cmd.test.ts +90 -90
- package/src/cli/commands/index-cmd.ts +52 -36
- package/src/cli/commands/mcp.test.ts +6 -6
- package/src/cli/commands/mcp.ts +2 -2
- package/src/cli/commands/plugin-api.test.ts +16 -18
- package/src/cli/commands/plugin-api.ts +9 -6
- package/src/cli/commands/search.test.ts +16 -7
- package/src/cli/commands/search.ts +124 -87
- package/src/cli/commands/serve.test.ts +67 -25
- package/src/cli/commands/serve.ts +18 -3
- package/src/cli/commands/setup.test.ts +176 -101
- package/src/cli/commands/setup.ts +140 -117
- package/src/cli/commands/store.test.ts +82 -53
- package/src/cli/commands/store.ts +56 -37
- package/src/cli/program.ts +2 -2
- package/src/crawl/article-converter.test.ts +4 -1
- package/src/crawl/article-converter.ts +46 -31
- package/src/crawl/bridge.test.ts +240 -132
- package/src/crawl/bridge.ts +87 -30
- package/src/crawl/claude-client.test.ts +124 -56
- package/src/crawl/claude-client.ts +7 -15
- package/src/crawl/intelligent-crawler.test.ts +65 -22
- package/src/crawl/intelligent-crawler.ts +86 -53
- package/src/crawl/markdown-utils.ts +1 -4
- package/src/db/embeddings.ts +4 -6
- package/src/db/lance.test.ts +4 -4
- package/src/db/lance.ts +16 -12
- package/src/index.ts +26 -17
- package/src/logging/index.ts +1 -5
- package/src/logging/logger.ts +3 -5
- package/src/logging/payload.test.ts +1 -1
- package/src/logging/payload.ts +3 -5
- package/src/mcp/commands/index.ts +2 -2
- package/src/mcp/commands/job.commands.ts +12 -18
- package/src/mcp/commands/meta.commands.ts +13 -13
- package/src/mcp/commands/registry.ts +5 -8
- package/src/mcp/commands/store.commands.ts +19 -19
- package/src/mcp/handlers/execute.handler.test.ts +10 -10
- package/src/mcp/handlers/execute.handler.ts +4 -5
- package/src/mcp/handlers/index.ts +10 -14
- package/src/mcp/handlers/job.handler.test.ts +10 -10
- package/src/mcp/handlers/job.handler.ts +22 -25
- package/src/mcp/handlers/search.handler.test.ts +36 -65
- package/src/mcp/handlers/search.handler.ts +135 -104
- package/src/mcp/handlers/store.handler.test.ts +41 -52
- package/src/mcp/handlers/store.handler.ts +108 -88
- package/src/mcp/schemas/index.test.ts +73 -68
- package/src/mcp/schemas/index.ts +18 -12
- package/src/mcp/server.test.ts +1 -1
- package/src/mcp/server.ts +59 -46
- package/src/plugin/commands.test.ts +230 -95
- package/src/plugin/commands.ts +24 -25
- package/src/plugin/dependency-analyzer.test.ts +52 -52
- package/src/plugin/dependency-analyzer.ts +85 -22
- package/src/plugin/git-clone.test.ts +24 -13
- package/src/plugin/git-clone.ts +3 -7
- package/src/server/app.test.ts +109 -109
- package/src/server/app.ts +32 -23
- package/src/server/index.test.ts +64 -66
- package/src/services/chunking.service.test.ts +32 -32
- package/src/services/chunking.service.ts +16 -9
- package/src/services/code-graph.service.test.ts +30 -36
- package/src/services/code-graph.service.ts +24 -10
- package/src/services/code-unit.service.test.ts +55 -11
- package/src/services/code-unit.service.ts +85 -11
- package/src/services/config.service.test.ts +37 -18
- package/src/services/config.service.ts +30 -7
- package/src/services/index.service.test.ts +49 -18
- package/src/services/index.service.ts +98 -48
- package/src/services/index.ts +6 -9
- package/src/services/job.service.test.ts +22 -22
- package/src/services/job.service.ts +18 -18
- package/src/services/project-root.service.test.ts +1 -3
- package/src/services/search.service.test.ts +248 -120
- package/src/services/search.service.ts +286 -156
- package/src/services/services.test.ts +1 -1
- package/src/services/snippet.service.test.ts +14 -6
- package/src/services/snippet.service.ts +7 -5
- package/src/services/store.service.test.ts +68 -29
- package/src/services/store.service.ts +41 -12
- package/src/services/watch.service.test.ts +34 -14
- package/src/services/watch.service.ts +11 -1
- package/src/types/brands.test.ts +3 -1
- package/src/types/index.ts +2 -13
- package/src/types/search.ts +10 -8
- package/src/utils/type-guards.test.ts +20 -15
- package/src/utils/type-guards.ts +1 -1
- package/src/workers/background-worker-cli.ts +28 -30
- package/src/workers/background-worker.test.ts +54 -40
- package/src/workers/background-worker.ts +76 -60
- package/src/workers/pid-file.test.ts +167 -0
- package/src/workers/pid-file.ts +82 -0
- package/src/workers/spawn-worker.test.ts +22 -10
- package/src/workers/spawn-worker.ts +6 -6
- package/tests/analysis/ast-parser.test.ts +3 -3
- package/tests/analysis/code-graph.test.ts +5 -5
- package/tests/fixtures/code-snippets/api/error-handling.ts +4 -15
- package/tests/fixtures/code-snippets/api/rest-controller.ts +3 -9
- package/tests/fixtures/code-snippets/auth/jwt-auth.ts +5 -21
- package/tests/fixtures/code-snippets/auth/oauth-flow.ts +4 -4
- package/tests/fixtures/code-snippets/database/repository-pattern.ts +11 -3
- package/tests/fixtures/corpus/oss-repos/hono/src/adapter/aws-lambda/handler.ts +2 -2
- package/tests/fixtures/corpus/oss-repos/hono/src/adapter/cloudflare-pages/handler.ts +1 -1
- package/tests/fixtures/corpus/oss-repos/hono/src/adapter/cloudflare-workers/serve-static.ts +2 -2
- package/tests/fixtures/corpus/oss-repos/hono/src/client/client.ts +2 -2
- package/tests/fixtures/corpus/oss-repos/hono/src/client/types.ts +22 -20
- package/tests/fixtures/corpus/oss-repos/hono/src/context.ts +13 -10
- package/tests/fixtures/corpus/oss-repos/hono/src/helper/accepts/accepts.ts +10 -7
- package/tests/fixtures/corpus/oss-repos/hono/src/helper/adapter/index.ts +2 -2
- package/tests/fixtures/corpus/oss-repos/hono/src/helper/css/index.ts +1 -1
- package/tests/fixtures/corpus/oss-repos/hono/src/helper/factory/index.ts +16 -16
- package/tests/fixtures/corpus/oss-repos/hono/src/helper/ssg/ssg.ts +2 -2
- package/tests/fixtures/corpus/oss-repos/hono/src/hono-base.ts +3 -3
- package/tests/fixtures/corpus/oss-repos/hono/src/hono.ts +1 -1
- package/tests/fixtures/corpus/oss-repos/hono/src/jsx/dom/css.ts +2 -2
- package/tests/fixtures/corpus/oss-repos/hono/src/jsx/dom/intrinsic-element/components.ts +1 -1
- package/tests/fixtures/corpus/oss-repos/hono/src/jsx/dom/render.ts +7 -7
- package/tests/fixtures/corpus/oss-repos/hono/src/jsx/hooks/index.ts +3 -3
- package/tests/fixtures/corpus/oss-repos/hono/src/jsx/intrinsic-element/components.ts +1 -1
- package/tests/fixtures/corpus/oss-repos/hono/src/jsx/utils.ts +6 -6
- package/tests/fixtures/corpus/oss-repos/hono/src/middleware/jsx-renderer/index.ts +3 -3
- package/tests/fixtures/corpus/oss-repos/hono/src/middleware/serve-static/index.ts +1 -1
- package/tests/fixtures/corpus/oss-repos/hono/src/preset/quick.ts +1 -1
- package/tests/fixtures/corpus/oss-repos/hono/src/preset/tiny.ts +1 -1
- package/tests/fixtures/corpus/oss-repos/hono/src/router/pattern-router/router.ts +2 -2
- package/tests/fixtures/corpus/oss-repos/hono/src/router/reg-exp-router/node.ts +4 -4
- package/tests/fixtures/corpus/oss-repos/hono/src/router/reg-exp-router/router.ts +1 -1
- package/tests/fixtures/corpus/oss-repos/hono/src/router/trie-router/node.ts +1 -1
- package/tests/fixtures/corpus/oss-repos/hono/src/types.ts +166 -169
- package/tests/fixtures/corpus/oss-repos/hono/src/utils/body.ts +8 -8
- package/tests/fixtures/corpus/oss-repos/hono/src/utils/color.ts +3 -3
- package/tests/fixtures/corpus/oss-repos/hono/src/utils/cookie.ts +2 -2
- package/tests/fixtures/corpus/oss-repos/hono/src/utils/encode.ts +2 -2
- package/tests/fixtures/corpus/oss-repos/hono/src/utils/types.ts +30 -33
- package/tests/fixtures/corpus/oss-repos/hono/src/validator/validator.ts +2 -2
- package/tests/fixtures/test-server.ts +3 -2
- package/tests/helpers/performance-metrics.ts +8 -25
- package/tests/helpers/search-relevance.ts +14 -69
- package/tests/integration/cli-consistency.test.ts +6 -5
- package/tests/integration/python-bridge.test.ts +13 -3
- package/tests/mcp/server.test.ts +1 -1
- package/tests/services/code-unit.service.test.ts +48 -0
- package/tests/services/job.service.test.ts +124 -0
- package/tests/services/search.progressive-context.test.ts +2 -2
- package/.claude-plugin/plugin.json +0 -13
- package/dist/chunk-6PBP5DVD.js.map +0 -1
- package/dist/chunk-L2YVNC63.js.map +0 -1
- package/dist/chunk-RST4XGRL.js.map +0 -1
- package/dist/chunk-WT2DAEO7.js.map +0 -1
- package/dist/watch.service-YAIKKDCF.js +0 -7
- package/skills/atomic-commits/SKILL.md +0 -77
- /package/dist/{watch.service-YAIKKDCF.js.map → watch.service-BJV3TI3F.js.map} +0 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { mkdtempSync, rmSync, existsSync, chmodSync, writeFileSync, readFileSync } from 'fs';
|
|
3
|
+
import { tmpdir } from 'os';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
import { writePidFile, deletePidFile, buildPidFilePath } from './pid-file.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* PID File Operations Tests
|
|
9
|
+
*
|
|
10
|
+
* SAFETY: All tests use fake PID 999999999 - never real PIDs.
|
|
11
|
+
* This prevents accidentally killing VSCode, terminals, or other processes.
|
|
12
|
+
*/
|
|
13
|
+
describe('PID File Operations', () => {
|
|
14
|
+
let tempDir: string;
|
|
15
|
+
let pidFile: string;
|
|
16
|
+
|
|
17
|
+
// Fake PID - guaranteed not to be a real process
|
|
18
|
+
const FAKE_PID = 999999999;
|
|
19
|
+
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
tempDir = mkdtempSync(join(tmpdir(), 'pid-file-test-'));
|
|
22
|
+
pidFile = join(tempDir, 'test_job.pid');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
afterEach(() => {
|
|
26
|
+
if (existsSync(tempDir)) {
|
|
27
|
+
// Restore permissions before cleanup (in case test made it read-only)
|
|
28
|
+
try {
|
|
29
|
+
chmodSync(tempDir, 0o755);
|
|
30
|
+
} catch {
|
|
31
|
+
// Ignore - might not exist
|
|
32
|
+
}
|
|
33
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe('writePidFile', () => {
|
|
38
|
+
it('should write PID to file successfully', () => {
|
|
39
|
+
writePidFile(pidFile, FAKE_PID);
|
|
40
|
+
|
|
41
|
+
expect(existsSync(pidFile)).toBe(true);
|
|
42
|
+
const content = readFileSync(pidFile, 'utf-8');
|
|
43
|
+
expect(content).toBe('999999999');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('should overwrite existing PID file', () => {
|
|
47
|
+
writeFileSync(pidFile, '123456', 'utf-8');
|
|
48
|
+
|
|
49
|
+
writePidFile(pidFile, FAKE_PID);
|
|
50
|
+
|
|
51
|
+
const content = readFileSync(pidFile, 'utf-8');
|
|
52
|
+
expect(content).toBe('999999999');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should throw with CRITICAL message when write fails (permission denied)', () => {
|
|
56
|
+
// Make directory read-only to prevent file creation
|
|
57
|
+
chmodSync(tempDir, 0o444);
|
|
58
|
+
|
|
59
|
+
expect(() => writePidFile(pidFile, FAKE_PID)).toThrow(/CRITICAL/);
|
|
60
|
+
expect(() => writePidFile(pidFile, FAKE_PID)).toThrow(/Failed to write PID file/);
|
|
61
|
+
expect(() => writePidFile(pidFile, FAKE_PID)).toThrow(/Job cannot be cancelled/);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('should include file path in error message', () => {
|
|
65
|
+
chmodSync(tempDir, 0o444);
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
writePidFile(pidFile, FAKE_PID);
|
|
69
|
+
expect.fail('Should have thrown');
|
|
70
|
+
} catch (error) {
|
|
71
|
+
expect(error).toBeInstanceOf(Error);
|
|
72
|
+
expect((error as Error).message).toContain(pidFile);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('should throw when path directory does not exist', () => {
|
|
77
|
+
const invalidPath = '/nonexistent/directory/test.pid';
|
|
78
|
+
|
|
79
|
+
expect(() => writePidFile(invalidPath, FAKE_PID)).toThrow(/CRITICAL/);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe('deletePidFile', () => {
|
|
84
|
+
it('should delete PID file successfully', () => {
|
|
85
|
+
writeFileSync(pidFile, FAKE_PID.toString(), 'utf-8');
|
|
86
|
+
|
|
87
|
+
const result = deletePidFile(pidFile, 'success');
|
|
88
|
+
|
|
89
|
+
expect(result.success).toBe(true);
|
|
90
|
+
expect(result.error).toBeUndefined();
|
|
91
|
+
expect(existsSync(pidFile)).toBe(false);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('should return success when PID file does not exist', () => {
|
|
95
|
+
// File doesn't exist
|
|
96
|
+
expect(existsSync(pidFile)).toBe(false);
|
|
97
|
+
|
|
98
|
+
const result = deletePidFile(pidFile, 'success');
|
|
99
|
+
|
|
100
|
+
expect(result.success).toBe(true);
|
|
101
|
+
expect(result.error).toBeUndefined();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('should return failure (NOT throw) when delete fails', () => {
|
|
105
|
+
writeFileSync(pidFile, FAKE_PID.toString(), 'utf-8');
|
|
106
|
+
// Make directory read-only to prevent deletion
|
|
107
|
+
chmodSync(tempDir, 0o444);
|
|
108
|
+
|
|
109
|
+
// Should NOT throw
|
|
110
|
+
const result = deletePidFile(pidFile, 'success');
|
|
111
|
+
|
|
112
|
+
expect(result.success).toBe(false);
|
|
113
|
+
expect(result.error).toBeInstanceOf(Error);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('should never throw on delete failure - returns result instead', () => {
|
|
117
|
+
writeFileSync(pidFile, FAKE_PID.toString(), 'utf-8');
|
|
118
|
+
chmodSync(tempDir, 0o444);
|
|
119
|
+
|
|
120
|
+
// Must not throw - this is best-effort cleanup
|
|
121
|
+
expect(() => deletePidFile(pidFile, 'failure')).not.toThrow();
|
|
122
|
+
expect(() => deletePidFile(pidFile, 'sigterm')).not.toThrow();
|
|
123
|
+
expect(() => deletePidFile(pidFile, 'success')).not.toThrow();
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('should handle sigterm context', () => {
|
|
127
|
+
writeFileSync(pidFile, FAKE_PID.toString(), 'utf-8');
|
|
128
|
+
|
|
129
|
+
const result = deletePidFile(pidFile, 'sigterm');
|
|
130
|
+
|
|
131
|
+
expect(result.success).toBe(true);
|
|
132
|
+
expect(existsSync(pidFile)).toBe(false);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('should handle failure context', () => {
|
|
136
|
+
writeFileSync(pidFile, FAKE_PID.toString(), 'utf-8');
|
|
137
|
+
|
|
138
|
+
const result = deletePidFile(pidFile, 'failure');
|
|
139
|
+
|
|
140
|
+
expect(result.success).toBe(true);
|
|
141
|
+
expect(existsSync(pidFile)).toBe(false);
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
describe('buildPidFilePath', () => {
|
|
146
|
+
it('should build correct PID file path', () => {
|
|
147
|
+
const result = buildPidFilePath('/data/jobs', 'job_123');
|
|
148
|
+
|
|
149
|
+
expect(result).toBe('/data/jobs/job_123.pid');
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('should handle job IDs with various formats', () => {
|
|
153
|
+
expect(buildPidFilePath('/jobs', 'abc123def')).toBe('/jobs/abc123def.pid');
|
|
154
|
+
expect(buildPidFilePath('/jobs', 'test-job')).toBe('/jobs/test-job.pid');
|
|
155
|
+
expect(buildPidFilePath('/jobs', 'job_with_underscore')).toBe(
|
|
156
|
+
'/jobs/job_with_underscore.pid'
|
|
157
|
+
);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('should handle paths with trailing slash', () => {
|
|
161
|
+
// path.join normalizes this
|
|
162
|
+
const result = buildPidFilePath('/data/jobs/', 'job_123');
|
|
163
|
+
|
|
164
|
+
expect(result).toBe('/data/jobs/job_123.pid');
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
});
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Result of a PID file delete operation.
|
|
6
|
+
* Delete operations are best-effort and should not throw.
|
|
7
|
+
*/
|
|
8
|
+
export interface PidFileResult {
|
|
9
|
+
success: boolean;
|
|
10
|
+
error?: Error;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Context for PID file deletion - indicates when the delete is happening.
|
|
15
|
+
* Used for logging/debugging purposes.
|
|
16
|
+
*/
|
|
17
|
+
export type PidFileDeleteContext = 'sigterm' | 'success' | 'failure';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Write PID file - CRITICAL operation that must succeed.
|
|
21
|
+
*
|
|
22
|
+
* If the PID file cannot be written, the job cannot be cancelled through
|
|
23
|
+
* the job management system. This is a critical failure and the job
|
|
24
|
+
* should not proceed.
|
|
25
|
+
*
|
|
26
|
+
* @param pidFile - Absolute path to the PID file
|
|
27
|
+
* @param pid - Process ID to write
|
|
28
|
+
* @throws Error if PID file cannot be written
|
|
29
|
+
*/
|
|
30
|
+
export function writePidFile(pidFile: string, pid: number): void {
|
|
31
|
+
try {
|
|
32
|
+
fs.writeFileSync(pidFile, pid.toString(), 'utf-8');
|
|
33
|
+
} catch (error) {
|
|
34
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
35
|
+
throw new Error(
|
|
36
|
+
`CRITICAL: Failed to write PID file ${pidFile}. ` +
|
|
37
|
+
`Job cannot be cancelled without PID file. ` +
|
|
38
|
+
`Original error: ${message}`
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Delete PID file - best-effort cleanup during shutdown.
|
|
45
|
+
*
|
|
46
|
+
* This operation should NEVER throw. During process shutdown (SIGTERM,
|
|
47
|
+
* job success, job failure), failing to delete a PID file should not
|
|
48
|
+
* prevent the process from exiting cleanly.
|
|
49
|
+
*
|
|
50
|
+
* Stale PID files are cleaned up by JobService.cleanupOldJobs().
|
|
51
|
+
*
|
|
52
|
+
* @param pidFile - Absolute path to the PID file
|
|
53
|
+
* @param _context - Context indicating when the delete is happening (for future logging)
|
|
54
|
+
* @returns Result indicating success or failure with error details
|
|
55
|
+
*/
|
|
56
|
+
export function deletePidFile(pidFile: string, _context: PidFileDeleteContext): PidFileResult {
|
|
57
|
+
try {
|
|
58
|
+
fs.unlinkSync(pidFile);
|
|
59
|
+
return { success: true };
|
|
60
|
+
} catch (error) {
|
|
61
|
+
// ENOENT = file doesn't exist - that's success (nothing to delete)
|
|
62
|
+
if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {
|
|
63
|
+
return { success: true };
|
|
64
|
+
}
|
|
65
|
+
// Any other error = failure (permission denied, etc.)
|
|
66
|
+
return {
|
|
67
|
+
success: false,
|
|
68
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Build the path to a PID file for a given job.
|
|
75
|
+
*
|
|
76
|
+
* @param jobsDir - Directory where job files are stored
|
|
77
|
+
* @param jobId - Job identifier
|
|
78
|
+
* @returns Absolute path to the PID file
|
|
79
|
+
*/
|
|
80
|
+
export function buildPidFilePath(jobsDir: string, jobId: string): string {
|
|
81
|
+
return path.join(jobsDir, `${jobId}.pid`);
|
|
82
|
+
}
|
|
@@ -3,11 +3,11 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
|
3
3
|
// Mock child_process before importing spawn-worker
|
|
4
4
|
const mockUnref = vi.fn();
|
|
5
5
|
const mockSpawn = vi.fn(() => ({
|
|
6
|
-
unref: mockUnref
|
|
6
|
+
unref: mockUnref,
|
|
7
7
|
}));
|
|
8
8
|
|
|
9
9
|
vi.mock('child_process', () => ({
|
|
10
|
-
spawn: mockSpawn
|
|
10
|
+
spawn: mockSpawn,
|
|
11
11
|
}));
|
|
12
12
|
|
|
13
13
|
// Import after mocking
|
|
@@ -50,11 +50,15 @@ describe('spawnBackgroundWorker', () => {
|
|
|
50
50
|
spawnBackgroundWorker('test-job', '/test/data/dir');
|
|
51
51
|
|
|
52
52
|
expect(mockSpawn).toHaveBeenCalledTimes(1);
|
|
53
|
-
const [, , options] = mockSpawn.mock.calls[0] as [
|
|
53
|
+
const [, , options] = mockSpawn.mock.calls[0] as [
|
|
54
|
+
string,
|
|
55
|
+
string[],
|
|
56
|
+
{ detached: boolean; stdio: string },
|
|
57
|
+
];
|
|
54
58
|
|
|
55
59
|
expect(options).toMatchObject({
|
|
56
60
|
detached: true,
|
|
57
|
-
stdio: 'ignore'
|
|
61
|
+
stdio: 'ignore',
|
|
58
62
|
});
|
|
59
63
|
});
|
|
60
64
|
|
|
@@ -63,11 +67,15 @@ describe('spawnBackgroundWorker', () => {
|
|
|
63
67
|
spawnBackgroundWorker('test-job', dataDir);
|
|
64
68
|
|
|
65
69
|
expect(mockSpawn).toHaveBeenCalledTimes(1);
|
|
66
|
-
const [, , options] = mockSpawn.mock.calls[0] as [
|
|
70
|
+
const [, , options] = mockSpawn.mock.calls[0] as [
|
|
71
|
+
string,
|
|
72
|
+
string[],
|
|
73
|
+
{ env: Record<string, string> },
|
|
74
|
+
];
|
|
67
75
|
|
|
68
76
|
expect(options.env).toMatchObject({
|
|
69
77
|
...process.env,
|
|
70
|
-
BLUERA_DATA_DIR: dataDir
|
|
78
|
+
BLUERA_DATA_DIR: dataDir,
|
|
71
79
|
});
|
|
72
80
|
});
|
|
73
81
|
|
|
@@ -76,7 +84,11 @@ describe('spawnBackgroundWorker', () => {
|
|
|
76
84
|
spawnBackgroundWorker('job-456', testDataDir);
|
|
77
85
|
|
|
78
86
|
expect(mockSpawn).toHaveBeenCalledTimes(1);
|
|
79
|
-
const [, , options] = mockSpawn.mock.calls[0] as [
|
|
87
|
+
const [, , options] = mockSpawn.mock.calls[0] as [
|
|
88
|
+
string,
|
|
89
|
+
string[],
|
|
90
|
+
{ env: Record<string, string> },
|
|
91
|
+
];
|
|
80
92
|
|
|
81
93
|
expect(options.env.BLUERA_DATA_DIR).toBe(testDataDir);
|
|
82
94
|
});
|
|
@@ -86,7 +98,7 @@ describe('spawnBackgroundWorker', () => {
|
|
|
86
98
|
describe('spawnBackgroundWorker (production mode)', () => {
|
|
87
99
|
const mockUnrefProd = vi.fn();
|
|
88
100
|
const mockSpawnProd = vi.fn(() => ({
|
|
89
|
-
unref: mockUnrefProd
|
|
101
|
+
unref: mockUnrefProd,
|
|
90
102
|
}));
|
|
91
103
|
|
|
92
104
|
beforeEach(() => {
|
|
@@ -102,12 +114,12 @@ describe('spawnBackgroundWorker (production mode)', () => {
|
|
|
102
114
|
it('should use Node.js directly in production mode (dist folder)', async () => {
|
|
103
115
|
// Mock child_process
|
|
104
116
|
vi.doMock('child_process', () => ({
|
|
105
|
-
spawn: mockSpawnProd
|
|
117
|
+
spawn: mockSpawnProd,
|
|
106
118
|
}));
|
|
107
119
|
|
|
108
120
|
// Mock url module to return a path containing /dist/
|
|
109
121
|
vi.doMock('url', () => ({
|
|
110
|
-
fileURLToPath: () => '/app/dist/workers/spawn-worker.js'
|
|
122
|
+
fileURLToPath: () => '/app/dist/workers/spawn-worker.js',
|
|
111
123
|
}));
|
|
112
124
|
|
|
113
125
|
// Import fresh module with production path
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { spawn } from 'child_process';
|
|
2
|
-
import { fileURLToPath } from 'url';
|
|
3
2
|
import path from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Spawn a background worker process to execute a job
|
|
@@ -36,12 +36,12 @@ export function spawnBackgroundWorker(jobId: string, dataDir: string): void {
|
|
|
36
36
|
|
|
37
37
|
// Spawn the worker process
|
|
38
38
|
const worker = spawn(command, args, {
|
|
39
|
-
detached: true,
|
|
40
|
-
stdio: 'ignore',
|
|
39
|
+
detached: true, // Detach from parent process
|
|
40
|
+
stdio: 'ignore', // Don't pipe stdio (fully independent)
|
|
41
41
|
env: {
|
|
42
|
-
...process.env,
|
|
43
|
-
BLUERA_DATA_DIR: dataDir
|
|
44
|
-
}
|
|
42
|
+
...process.env, // Inherit environment variables
|
|
43
|
+
BLUERA_DATA_DIR: dataDir, // Pass dataDir to worker
|
|
44
|
+
},
|
|
45
45
|
});
|
|
46
46
|
|
|
47
47
|
// Unref the worker so the parent can exit
|
|
@@ -43,7 +43,7 @@ export class UserService {
|
|
|
43
43
|
const parser = new ASTParser();
|
|
44
44
|
const nodes = parser.parse(code, 'typescript');
|
|
45
45
|
|
|
46
|
-
const classNode = nodes.find(n => n.type === 'class');
|
|
46
|
+
const classNode = nodes.find((n) => n.type === 'class');
|
|
47
47
|
expect(classNode).toBeDefined();
|
|
48
48
|
expect(classNode?.name).toBe('UserService');
|
|
49
49
|
expect(classNode?.methods).toHaveLength(3); // constructor + create + delete
|
|
@@ -78,11 +78,11 @@ class Bar {
|
|
|
78
78
|
const parser = new ASTParser();
|
|
79
79
|
const nodes = parser.parse(code, 'typescript');
|
|
80
80
|
|
|
81
|
-
const fooNode = nodes.find(n => n.name === 'foo');
|
|
81
|
+
const fooNode = nodes.find((n) => n.name === 'foo');
|
|
82
82
|
expect(fooNode?.startLine).toBe(2);
|
|
83
83
|
expect(fooNode?.endLine).toBe(4);
|
|
84
84
|
|
|
85
|
-
const barNode = nodes.find(n => n.name === 'Bar');
|
|
85
|
+
const barNode = nodes.find((n) => n.name === 'Bar');
|
|
86
86
|
expect(barNode?.startLine).toBe(6);
|
|
87
87
|
expect(barNode?.endLine).toBe(8);
|
|
88
88
|
});
|
|
@@ -10,15 +10,15 @@ describe('CodeGraph', () => {
|
|
|
10
10
|
name: 'validateToken',
|
|
11
11
|
exported: true,
|
|
12
12
|
startLine: 1,
|
|
13
|
-
endLine: 5
|
|
13
|
+
endLine: 5,
|
|
14
14
|
},
|
|
15
15
|
{
|
|
16
16
|
type: 'function',
|
|
17
17
|
name: 'parseToken',
|
|
18
18
|
exported: false,
|
|
19
19
|
startLine: 7,
|
|
20
|
-
endLine: 10
|
|
21
|
-
}
|
|
20
|
+
endLine: 10,
|
|
21
|
+
},
|
|
22
22
|
];
|
|
23
23
|
|
|
24
24
|
const graph = new CodeGraph();
|
|
@@ -52,9 +52,9 @@ export function handler(req) {
|
|
|
52
52
|
graph.analyzeCallRelationships(code, 'src/handler.ts', 'handler');
|
|
53
53
|
|
|
54
54
|
const edges = graph.getEdges('src/handler.ts:handler');
|
|
55
|
-
const callEdges = edges.filter(e => e.type === 'calls');
|
|
55
|
+
const callEdges = edges.filter((e) => e.type === 'calls');
|
|
56
56
|
|
|
57
57
|
expect(callEdges.length).toBeGreaterThan(0);
|
|
58
|
-
expect(callEdges.some(e => e.to.includes('validateToken'))).toBe(true);
|
|
58
|
+
expect(callEdges.some((e) => e.to.includes('validateToken'))).toBe(true);
|
|
59
59
|
});
|
|
60
60
|
});
|
|
@@ -17,12 +17,7 @@ export class ApiError extends Error {
|
|
|
17
17
|
public readonly isOperational: boolean;
|
|
18
18
|
public readonly timestamp: Date;
|
|
19
19
|
|
|
20
|
-
constructor(
|
|
21
|
-
statusCode: number,
|
|
22
|
-
message: string,
|
|
23
|
-
code?: string,
|
|
24
|
-
isOperational = true
|
|
25
|
-
) {
|
|
20
|
+
constructor(statusCode: number, message: string, code?: string, isOperational = true) {
|
|
26
21
|
super(message);
|
|
27
22
|
this.statusCode = statusCode;
|
|
28
23
|
this.code = code || 'API_ERROR';
|
|
@@ -77,9 +72,7 @@ export class NotFoundError extends ApiError {
|
|
|
77
72
|
public readonly resource: string;
|
|
78
73
|
|
|
79
74
|
constructor(resource: string, id?: string) {
|
|
80
|
-
const message = id
|
|
81
|
-
? `${resource} with ID '${id}' not found`
|
|
82
|
-
: `${resource} not found`;
|
|
75
|
+
const message = id ? `${resource} with ID '${id}' not found` : `${resource} not found`;
|
|
83
76
|
super(404, message, 'NOT_FOUND');
|
|
84
77
|
this.resource = resource;
|
|
85
78
|
Object.setPrototypeOf(this, NotFoundError.prototype);
|
|
@@ -106,9 +99,7 @@ export class ValidationError extends ApiError {
|
|
|
106
99
|
value?: unknown;
|
|
107
100
|
}>;
|
|
108
101
|
|
|
109
|
-
constructor(
|
|
110
|
-
errors: Array<{ field: string; message: string; value?: unknown }>
|
|
111
|
-
) {
|
|
102
|
+
constructor(errors: Array<{ field: string; message: string; value?: unknown }>) {
|
|
112
103
|
super(422, 'Validation failed', 'VALIDATION_ERROR');
|
|
113
104
|
this.errors = errors;
|
|
114
105
|
Object.setPrototypeOf(this, ValidationError.prototype);
|
|
@@ -239,9 +230,7 @@ export const errorHandler: ErrorRequestHandler = (
|
|
|
239
230
|
|
|
240
231
|
// Handle unknown errors
|
|
241
232
|
const internalError = new InternalError(
|
|
242
|
-
process.env.NODE_ENV === 'production'
|
|
243
|
-
? 'An unexpected error occurred'
|
|
244
|
-
: err.message
|
|
233
|
+
process.env.NODE_ENV === 'production' ? 'An unexpected error occurred' : err.message
|
|
245
234
|
);
|
|
246
235
|
|
|
247
236
|
const response = formatErrorResponse(internalError, req, requestId);
|
|
@@ -92,7 +92,7 @@ function validateBody<T>(schema: z.ZodSchema<T>, data: unknown): T {
|
|
|
92
92
|
const result = schema.safeParse(data);
|
|
93
93
|
|
|
94
94
|
if (!result.success) {
|
|
95
|
-
const errors = result.error.errors.map(err => ({
|
|
95
|
+
const errors = result.error.errors.map((err) => ({
|
|
96
96
|
field: err.path.join('.'),
|
|
97
97
|
message: err.message,
|
|
98
98
|
}));
|
|
@@ -132,11 +132,7 @@ export class UserController {
|
|
|
132
132
|
asyncHandler(this.createUser.bind(this))
|
|
133
133
|
);
|
|
134
134
|
|
|
135
|
-
this.router.put(
|
|
136
|
-
'/:id',
|
|
137
|
-
authenticateToken,
|
|
138
|
-
asyncHandler(this.updateUser.bind(this))
|
|
139
|
-
);
|
|
135
|
+
this.router.put('/:id', authenticateToken, asyncHandler(this.updateUser.bind(this)));
|
|
140
136
|
|
|
141
137
|
this.router.delete(
|
|
142
138
|
'/:id',
|
|
@@ -217,9 +213,7 @@ export class UserController {
|
|
|
217
213
|
const data = validateBody(createUserSchema, req.body);
|
|
218
214
|
|
|
219
215
|
// Check for existing user
|
|
220
|
-
const existingUser = Array.from(users.values()).find(
|
|
221
|
-
u => u.email === data.email
|
|
222
|
-
);
|
|
216
|
+
const existingUser = Array.from(users.values()).find((u) => u.email === data.email);
|
|
223
217
|
|
|
224
218
|
if (existingUser) {
|
|
225
219
|
throw new BadRequestError('User with this email already exists', {
|
|
@@ -51,11 +51,7 @@ declare global {
|
|
|
51
51
|
* @param user - User object containing id, email, and roles
|
|
52
52
|
* @returns JWT access token
|
|
53
53
|
*/
|
|
54
|
-
export function generateAccessToken(user: {
|
|
55
|
-
id: string;
|
|
56
|
-
email: string;
|
|
57
|
-
roles: string[];
|
|
58
|
-
}): string {
|
|
54
|
+
export function generateAccessToken(user: { id: string; email: string; roles: string[] }): string {
|
|
59
55
|
const payload: JwtPayload = {
|
|
60
56
|
userId: user.id,
|
|
61
57
|
email: user.email,
|
|
@@ -76,11 +72,7 @@ export function generateAccessToken(user: {
|
|
|
76
72
|
export function generateRefreshToken(userId: string): string {
|
|
77
73
|
const tokenId = randomBytes(16).toString('hex');
|
|
78
74
|
|
|
79
|
-
return jwt.sign(
|
|
80
|
-
{ userId, tokenId },
|
|
81
|
-
REFRESH_SECRET,
|
|
82
|
-
{ expiresIn: REFRESH_TOKEN_EXPIRY }
|
|
83
|
-
);
|
|
75
|
+
return jwt.sign({ userId, tokenId }, REFRESH_SECRET, { expiresIn: REFRESH_TOKEN_EXPIRY });
|
|
84
76
|
}
|
|
85
77
|
|
|
86
78
|
/**
|
|
@@ -88,11 +80,7 @@ export function generateRefreshToken(userId: string): string {
|
|
|
88
80
|
* @param user - User object
|
|
89
81
|
* @returns Token pair with access token, refresh token, and expiry time
|
|
90
82
|
*/
|
|
91
|
-
export function generateTokenPair(user: {
|
|
92
|
-
id: string;
|
|
93
|
-
email: string;
|
|
94
|
-
roles: string[];
|
|
95
|
-
}): TokenPair {
|
|
83
|
+
export function generateTokenPair(user: { id: string; email: string; roles: string[] }): TokenPair {
|
|
96
84
|
return {
|
|
97
85
|
accessToken: generateAccessToken(user),
|
|
98
86
|
refreshToken: generateRefreshToken(user.id),
|
|
@@ -137,11 +125,7 @@ export function verifyRefreshToken(token: string): { userId: string; tokenId: st
|
|
|
137
125
|
* Express middleware to authenticate requests using JWT
|
|
138
126
|
* Extracts the token from the Authorization header (Bearer scheme)
|
|
139
127
|
*/
|
|
140
|
-
export function authenticateToken(
|
|
141
|
-
req: Request,
|
|
142
|
-
res: Response,
|
|
143
|
-
next: NextFunction
|
|
144
|
-
): void {
|
|
128
|
+
export function authenticateToken(req: Request, res: Response, next: NextFunction): void {
|
|
145
129
|
const authHeader = req.headers['authorization'];
|
|
146
130
|
const token = authHeader && authHeader.split(' ')[1];
|
|
147
131
|
|
|
@@ -179,7 +163,7 @@ export function requireRoles(...requiredRoles: string[]) {
|
|
|
179
163
|
return;
|
|
180
164
|
}
|
|
181
165
|
|
|
182
|
-
const hasRole = req.user.roles.some(role => requiredRoles.includes(role));
|
|
166
|
+
const hasRole = req.user.roles.some((role) => requiredRoles.includes(role));
|
|
183
167
|
|
|
184
168
|
if (!hasRole) {
|
|
185
169
|
res.status(403).json({
|
|
@@ -180,7 +180,7 @@ export async function exchangeCodeForTokens(
|
|
|
180
180
|
method: 'POST',
|
|
181
181
|
headers: {
|
|
182
182
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
183
|
-
|
|
183
|
+
Accept: 'application/json',
|
|
184
184
|
},
|
|
185
185
|
body: body.toString(),
|
|
186
186
|
});
|
|
@@ -202,8 +202,8 @@ export async function fetchUserInfo(
|
|
|
202
202
|
): Promise<Record<string, unknown>> {
|
|
203
203
|
const response = await fetch(config.userInfoUrl, {
|
|
204
204
|
headers: {
|
|
205
|
-
|
|
206
|
-
|
|
205
|
+
Authorization: `Bearer ${accessToken}`,
|
|
206
|
+
Accept: 'application/json',
|
|
207
207
|
},
|
|
208
208
|
});
|
|
209
209
|
|
|
@@ -232,7 +232,7 @@ export async function refreshAccessToken(
|
|
|
232
232
|
method: 'POST',
|
|
233
233
|
headers: {
|
|
234
234
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
235
|
-
|
|
235
|
+
Accept: 'application/json',
|
|
236
236
|
},
|
|
237
237
|
body: body.toString(),
|
|
238
238
|
});
|
|
@@ -45,7 +45,11 @@ export interface IRepository<T extends Entity> {
|
|
|
45
45
|
findById(id: string): Promise<T | null>;
|
|
46
46
|
findOne(options: QueryOptions<T>): Promise<T | null>;
|
|
47
47
|
findMany(options?: QueryOptions<T>): Promise<T[]>;
|
|
48
|
-
findPaginated(
|
|
48
|
+
findPaginated(
|
|
49
|
+
page: number,
|
|
50
|
+
pageSize: number,
|
|
51
|
+
options?: QueryOptions<T>
|
|
52
|
+
): Promise<PaginatedResult<T>>;
|
|
49
53
|
create(data: Omit<T, 'id'>): Promise<T>;
|
|
50
54
|
update(id: string, data: Partial<T>): Promise<T>;
|
|
51
55
|
delete(id: string): Promise<void>;
|
|
@@ -75,7 +79,7 @@ export abstract class BaseRepository<T extends Entity> implements IRepository<T>
|
|
|
75
79
|
|
|
76
80
|
// Apply where filters
|
|
77
81
|
if (options?.where) {
|
|
78
|
-
result = result.filter(item => {
|
|
82
|
+
result = result.filter((item) => {
|
|
79
83
|
return Object.entries(options.where!).every(([key, value]) => {
|
|
80
84
|
return item[key as keyof T] === value;
|
|
81
85
|
});
|
|
@@ -126,7 +130,11 @@ export abstract class BaseRepository<T extends Entity> implements IRepository<T>
|
|
|
126
130
|
options?: QueryOptions<T>
|
|
127
131
|
): Promise<PaginatedResult<T>> {
|
|
128
132
|
const allItems = Array.from(this.items.values());
|
|
129
|
-
const filtered = this.applyFilters(allItems, {
|
|
133
|
+
const filtered = this.applyFilters(allItems, {
|
|
134
|
+
...options,
|
|
135
|
+
limit: undefined,
|
|
136
|
+
offset: undefined,
|
|
137
|
+
});
|
|
130
138
|
const total = filtered.length;
|
|
131
139
|
|
|
132
140
|
const offset = (page - 1) * pageSize;
|
|
@@ -108,7 +108,7 @@ const streamToNodeStream = async (
|
|
|
108
108
|
export const streamHandle = <
|
|
109
109
|
E extends Env = Env,
|
|
110
110
|
S extends Schema = {},
|
|
111
|
-
BasePath extends string = '/'
|
|
111
|
+
BasePath extends string = '/',
|
|
112
112
|
>(
|
|
113
113
|
app: Hono<E, S, BasePath>
|
|
114
114
|
): Handler => {
|
|
@@ -206,7 +206,7 @@ abstract class EventProcessor<E extends LambdaEvent> {
|
|
|
206
206
|
const domainName =
|
|
207
207
|
event.requestContext && 'domainName' in event.requestContext
|
|
208
208
|
? event.requestContext.domainName
|
|
209
|
-
: event.headers?.['host'] ?? event.multiValueHeaders?.['host']?.[0]
|
|
209
|
+
: (event.headers?.['host'] ?? event.multiValueHeaders?.['host']?.[0])
|
|
210
210
|
const path = this.getPath(event)
|
|
211
211
|
const urlPath = `https://${domainName}${path}`
|
|
212
212
|
const url = queryString ? `${urlPath}?${queryString}` : urlPath
|
|
@@ -24,7 +24,7 @@ declare type PagesFunction<
|
|
|
24
24
|
Env = unknown,
|
|
25
25
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
26
26
|
Params extends string = any,
|
|
27
|
-
Data extends Record<string, unknown> = Record<string, unknown
|
|
27
|
+
Data extends Record<string, unknown> = Record<string, unknown>,
|
|
28
28
|
> = (context: EventContext<Env, Params, Data>) => Response | Promise<Response>
|
|
29
29
|
|
|
30
30
|
export const handle =
|