Skip to content

Commit a88405c

Browse files
authored
fix(core): fall back to packaged worker registry (#697)
* fix(core): fall back to packaged worker registry * test(cli): allow benign registry warnings
1 parent 6604086 commit a88405c

4 files changed

Lines changed: 121 additions & 8 deletions

File tree

.aiox-core/core/registry/registry-loader.js

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,36 @@ const path = require('path');
1414

1515
// Cache configuration
1616
const CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
17+
const REGISTRY_RELATIVE_PATH = path.join('.aiox-core', 'core', 'registry', 'service-registry.json');
18+
const PACKAGE_REGISTRY_PATH = path.join(__dirname, 'service-registry.json');
19+
20+
function uniquePaths(paths) {
21+
return Array.from(new Set(paths));
22+
}
23+
24+
function getDefaultRegistryPaths() {
25+
return uniquePaths([
26+
path.join(process.cwd(), REGISTRY_RELATIVE_PATH),
27+
PACKAGE_REGISTRY_PATH,
28+
]);
29+
}
30+
31+
async function readRegistryFile(paths) {
32+
const errors = [];
33+
34+
for (const registryPath of paths) {
35+
try {
36+
return {
37+
content: await fs.readFile(registryPath, 'utf8'),
38+
registryPath,
39+
};
40+
} catch (error) {
41+
errors.push(`${registryPath}: ${error.message}`);
42+
}
43+
}
44+
45+
throw new Error(`Failed to load registry: ${errors.join('; ')}`);
46+
}
1747

1848
/**
1949
* Service Registry class with caching
@@ -24,6 +54,7 @@ class ServiceRegistry {
2454
this.cache = null;
2555
this.cacheTimestamp = 0;
2656
this.cacheTTL = options.cacheTTL || CACHE_TTL_MS;
57+
this.resolvedRegistryPath = null;
2758

2859
// Indexed lookups (built on load)
2960
this._byId = new Map();
@@ -45,16 +76,15 @@ class ServiceRegistry {
4576
return this.cache;
4677
}
4778

48-
// Determine registry path
49-
const registryPath = this.registryPath ||
50-
path.join(process.cwd(), '.aiox-core/core/registry/service-registry.json');
79+
const registryPaths = this.registryPath ? [this.registryPath] : getDefaultRegistryPaths();
5180

5281
const startTime = Date.now();
5382

5483
try {
55-
const content = await fs.readFile(registryPath, 'utf8');
84+
const { content, registryPath } = await readRegistryFile(registryPaths);
5685
this.cache = JSON.parse(content);
5786
this.cacheTimestamp = now;
87+
this.resolvedRegistryPath = registryPath;
5888

5989
// Build indexes
6090
this._buildIndexes();
@@ -66,6 +96,9 @@ class ServiceRegistry {
6696

6797
return this.cache;
6898
} catch (error) {
99+
if (error.message.startsWith('Failed to load registry:')) {
100+
throw error;
101+
}
69102
throw new Error(`Failed to load registry: ${error.message}`);
70103
}
71104
}
@@ -276,6 +309,7 @@ class ServiceRegistry {
276309
clearCache() {
277310
this.cache = null;
278311
this.cacheTimestamp = 0;
312+
this.resolvedRegistryPath = null;
279313
this._byId.clear();
280314
this._byCategory.clear();
281315
this._byTag.clear();
@@ -290,6 +324,7 @@ class ServiceRegistry {
290324
return {
291325
cached: this.cache !== null,
292326
cacheAge: this.cache ? Date.now() - this.cacheTimestamp : null,
327+
registryPath: this.resolvedRegistryPath,
293328
workerCount: this._byId.size,
294329
categoryCount: this._byCategory.size,
295330
tagCount: this._byTag.size,
@@ -326,5 +361,6 @@ async function loadRegistry(registryPath = null) {
326361
module.exports = {
327362
ServiceRegistry,
328363
getRegistry,
364+
getDefaultRegistryPaths,
329365
loadRegistry,
330366
};

.aiox-core/install-manifest.yaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
# - File types for categorization
99
#
1010
version: 5.1.15
11-
generated_at: "2026-05-08T05:14:31.637Z"
11+
generated_at: "2026-05-08T05:30:43.651Z"
1212
generator: scripts/generate-install-manifest.js
1313
file_count: 1111
1414
files:
@@ -1041,9 +1041,9 @@ files:
10411041
type: core
10421042
size: 4786
10431043
- path: core/registry/registry-loader.js
1044-
hash: sha256:40310943d29e4ecffe8dab1f7eb7388c73d959b7176d5277b066b97ef556a85a
1044+
hash: sha256:0cf9fa2ca39f7c4ca20043f3c4d7742e40dec7e94f81b706cf9318ebee199975
10451045
type: core
1046-
size: 7988
1046+
size: 9006
10471047
- path: core/registry/registry-schema.json
10481048
hash: sha256:f7947c4c4eff858857934f2f17a8f7778f0be25a93e27dac3509f29099115573
10491049
type: core

tests/core/registry-loader.test.js

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,11 @@ jest.mock('fs', () => ({
2020
}));
2121

2222
const fs = require('fs').promises;
23-
const { ServiceRegistry, getRegistry } = require('../../.aiox-core/core/registry/registry-loader');
23+
const {
24+
ServiceRegistry,
25+
getDefaultRegistryPaths,
26+
getRegistry,
27+
} = require('../../.aiox-core/core/registry/registry-loader');
2428

2529
// Set timeout for all tests
2630
jest.setTimeout(30000);
@@ -94,6 +98,7 @@ describe('ServiceRegistry', () => {
9498

9599
expect(reg).toBeDefined();
96100
expect(reg.registryPath).toBeNull();
101+
expect(reg.resolvedRegistryPath).toBeNull();
97102
expect(reg.cache).toBeNull();
98103
expect(reg.cacheTimestamp).toBe(0);
99104
});
@@ -156,6 +161,41 @@ describe('ServiceRegistry', () => {
156161

157162
await expect(registry.load()).rejects.toThrow('Failed to load registry');
158163
});
164+
165+
it('should fall back to the package registry when cwd registry is missing', async () => {
166+
const cwdSpy = jest
167+
.spyOn(process, 'cwd')
168+
.mockReturnValue(path.join(path.sep, 'tmp', 'project-without-registry'));
169+
const defaultPaths = getDefaultRegistryPaths();
170+
const fallbackRegistry = new ServiceRegistry();
171+
172+
fs.readFile.mockImplementation(async (registryPath) => {
173+
if (registryPath === defaultPaths[0]) {
174+
throw new Error('File not found');
175+
}
176+
return JSON.stringify(mockData);
177+
});
178+
179+
try {
180+
const data = await fallbackRegistry.load();
181+
182+
expect(data).toEqual(mockData);
183+
expect(fs.readFile).toHaveBeenNthCalledWith(1, defaultPaths[0], 'utf8');
184+
expect(fs.readFile).toHaveBeenNthCalledWith(2, defaultPaths[1], 'utf8');
185+
expect(fallbackRegistry.resolvedRegistryPath).toBe(defaultPaths[1]);
186+
} finally {
187+
cwdSpy.mockRestore();
188+
}
189+
});
190+
191+
it('should not fall back when an explicit registry path is provided', async () => {
192+
fs.readFile.mockRejectedValue(new Error('File not found'));
193+
194+
await expect(registry.load()).rejects.toThrow('Failed to load registry');
195+
196+
expect(fs.readFile).toHaveBeenCalledTimes(1);
197+
expect(fs.readFile).toHaveBeenCalledWith('/mock/path.json', 'utf8');
198+
});
159199
});
160200

161201
describe('getById', () => {
@@ -354,6 +394,7 @@ describe('ServiceRegistry', () => {
354394

355395
expect(registry.cache).toBeNull();
356396
expect(registry.cacheTimestamp).toBe(0);
397+
expect(registry.resolvedRegistryPath).toBeNull();
357398
expect(registry._byId.size).toBe(0);
358399
expect(registry._byCategory.size).toBe(0);
359400
});

tests/unit/cli.test.js

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
const fs = require('fs');
77
const path = require('path');
8+
const os = require('os');
89
const { spawn } = require('child_process');
910

1011
describe('CLI Entry Point', () => {
@@ -99,6 +100,41 @@ describe('CLI Entry Point', () => {
99100
});
100101
});
101102

103+
it('should load packaged worker registry outside a project cwd', async () => {
104+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'aiox-cli-registry-'));
105+
try {
106+
const { code, output, errors } = await new Promise((resolve) => {
107+
const child = spawn('node', [cliPath, 'workers', 'list', '--count'], {
108+
cwd: tempDir,
109+
env: process.env,
110+
});
111+
let output = '';
112+
let errors = '';
113+
114+
child.stdout.on('data', (data) => {
115+
output += data.toString();
116+
});
117+
118+
child.stderr.on('data', (data) => {
119+
errors += data.toString();
120+
});
121+
122+
child.on('close', (code) => {
123+
resolve({ code, output, errors });
124+
});
125+
});
126+
127+
expect({ code, errors }).toEqual({
128+
code: 0,
129+
errors: expect.not.stringMatching(/\b(?:Error|ERROR)\b/),
130+
});
131+
expect(output).toContain('Total:');
132+
expect(output).toContain('workers');
133+
} finally {
134+
fs.rmSync(tempDir, { recursive: true, force: true });
135+
}
136+
}, 15000);
137+
102138
it('should error on unknown command', (done) => {
103139
const child = spawn('node', [cliPath, 'unknown-command']);
104140
let errors = '';

0 commit comments

Comments
 (0)