@mrpatronz/nexusflow 2.2.0 → 2.3.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.
- package/README.md +80 -17
- package/dist/commands/ui.d.ts +1 -0
- package/dist/commands/ui.d.ts.map +1 -1
- package/dist/commands/ui.js +24 -13
- package/dist/commands/ui.js.map +1 -1
- package/dist/commands/ui.test.d.ts +2 -0
- package/dist/commands/ui.test.d.ts.map +1 -0
- package/dist/commands/ui.test.js +58 -0
- package/dist/commands/ui.test.js.map +1 -0
- package/dist/core/workspace-skills-workflow.integration.test.d.ts +2 -0
- package/dist/core/workspace-skills-workflow.integration.test.d.ts.map +1 -0
- package/dist/core/workspace-skills-workflow.integration.test.js +162 -0
- package/dist/core/workspace-skills-workflow.integration.test.js.map +1 -0
- package/dist/core/worktree.test.js +1 -1
- package/dist/core/worktree.test.js.map +1 -1
- package/dist/generators/index.d.ts.map +1 -1
- package/dist/generators/index.js +4 -8
- package/dist/generators/index.js.map +1 -1
- package/dist/generators/skills-generator.d.ts +2 -2
- package/dist/generators/skills-generator.d.ts.map +1 -1
- package/dist/generators/skills-generator.js +84 -89
- package/dist/generators/skills-generator.js.map +1 -1
- package/dist/generators/skills-generator.test.d.ts +2 -0
- package/dist/generators/skills-generator.test.d.ts.map +1 -0
- package/dist/generators/skills-generator.test.js +79 -0
- package/dist/generators/skills-generator.test.js.map +1 -0
- package/dist/gui/assets/AgentsPage-BtrTgIyU.js +3 -0
- package/dist/gui/assets/{DashboardPage-CUspfCap.js → DashboardPage-CeUkMdxc.js} +1 -1
- package/dist/gui/assets/{GettingStartedPage-BqtgARZW.js → GettingStartedPage-YwJXpsc6.js} +1 -1
- package/dist/gui/assets/ProjectsPage-D8kEHb-m.js +1 -0
- package/dist/gui/assets/ScaffoldRepoInline-ij7JLzmx.js +1 -0
- package/dist/gui/assets/SettingsPage-BQhAP4go.js +1 -0
- package/dist/gui/assets/SkillsPage-DJ-dXfcC.js +6 -0
- package/dist/gui/assets/StartWorkPage-DGWZzF4H.js +1 -0
- package/dist/gui/assets/{StrategiesPage-D0Y6A4Tw.js → StrategiesPage-CLOjDRbX.js} +2 -2
- package/dist/gui/assets/WorkspaceLauncher-CjNI4vqn.js +1 -0
- package/dist/gui/assets/WorkspacesPage-MDzm2JUh.js +6 -0
- package/dist/gui/assets/alert-dialog-DjwFcE6b.js +1 -0
- package/dist/gui/assets/bot-wH0FKlic.js +1 -0
- package/dist/gui/assets/chevron-down-DDmhvRLr.js +1 -0
- package/dist/gui/assets/code-xml-Chu1p81J.js +1 -0
- package/dist/gui/assets/index-CfQEnn52.js +52 -0
- package/dist/gui/assets/index-k7inKaui.css +2 -0
- package/dist/gui/assets/{spinner-DXt6mnJk.js → spinner-BCEaGbib.js} +1 -1
- package/dist/gui/assets/terminal-gK6BMl3E.js +1 -0
- package/dist/gui/assets/{textarea-BLU5HXmZ.js → textarea-D3-elOAa.js} +1 -1
- package/dist/gui/assets/{useAriaLabelledBy-CjNlctb9.js → useAriaLabelledBy-DmwmZIqb.js} +1 -1
- package/dist/gui/assets/useCompositeListItem-CZnlZ-eF.js +1 -0
- package/dist/gui/assets/useValueChanged-BfI2s7NX.js +1 -0
- package/dist/gui/assets/zap-FQrDnrgb.js +1 -0
- package/dist/gui/index.html +6 -4
- package/dist/resources/agents-catalog.d.ts +7 -0
- package/dist/resources/agents-catalog.d.ts.map +1 -0
- package/dist/resources/agents-catalog.js +141 -0
- package/dist/resources/agents-catalog.js.map +1 -0
- package/dist/resources/agents-catalog.test.d.ts +2 -0
- package/dist/resources/agents-catalog.test.d.ts.map +1 -0
- package/dist/resources/agents-catalog.test.js +50 -0
- package/dist/resources/agents-catalog.test.js.map +1 -0
- package/dist/resources/codex-agent.d.ts +6 -0
- package/dist/resources/codex-agent.d.ts.map +1 -0
- package/dist/resources/codex-agent.js +55 -0
- package/dist/resources/codex-agent.js.map +1 -0
- package/dist/resources/codex-agent.test.d.ts +2 -0
- package/dist/resources/codex-agent.test.d.ts.map +1 -0
- package/dist/resources/codex-agent.test.js +27 -0
- package/dist/resources/codex-agent.test.js.map +1 -0
- package/dist/resources/contracts.d.ts +116 -0
- package/dist/resources/contracts.d.ts.map +1 -0
- package/dist/resources/contracts.js +103 -0
- package/dist/resources/contracts.js.map +1 -0
- package/dist/resources/fs-safety.d.ts +11 -0
- package/dist/resources/fs-safety.d.ts.map +1 -0
- package/dist/resources/fs-safety.js +77 -0
- package/dist/resources/fs-safety.js.map +1 -0
- package/dist/resources/materializer.d.ts +17 -0
- package/dist/resources/materializer.d.ts.map +1 -0
- package/dist/resources/materializer.js +439 -0
- package/dist/resources/materializer.js.map +1 -0
- package/dist/resources/materializer.test.d.ts +2 -0
- package/dist/resources/materializer.test.d.ts.map +1 -0
- package/dist/resources/materializer.test.js +149 -0
- package/dist/resources/materializer.test.js.map +1 -0
- package/dist/resources/service.d.ts +8 -0
- package/dist/resources/service.d.ts.map +1 -0
- package/dist/resources/service.js +45 -0
- package/dist/resources/service.js.map +1 -0
- package/dist/resources/service.test.d.ts +2 -0
- package/dist/resources/service.test.d.ts.map +1 -0
- package/dist/resources/service.test.js +58 -0
- package/dist/resources/service.test.js.map +1 -0
- package/dist/server.d.ts +5 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +310 -4
- package/dist/server.js.map +1 -1
- package/dist/server.test.js +365 -5
- package/dist/server.test.js.map +1 -1
- package/dist/types.d.ts +107 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/resources.d.ts +2 -1
- package/dist/utils/resources.d.ts.map +1 -1
- package/dist/utils/resources.js +4 -3
- package/dist/utils/resources.js.map +1 -1
- package/dist/utils/session-finder.d.ts +6 -0
- package/dist/utils/session-finder.d.ts.map +1 -1
- package/dist/utils/session-finder.js +56 -0
- package/dist/utils/session-finder.js.map +1 -1
- package/dist/utils/session-finder.test.js +34 -1
- package/dist/utils/session-finder.test.js.map +1 -1
- package/dist/utils/skills-catalog.d.ts +77 -0
- package/dist/utils/skills-catalog.d.ts.map +1 -0
- package/dist/utils/skills-catalog.js +871 -0
- package/dist/utils/skills-catalog.js.map +1 -0
- package/dist/utils/skills-catalog.test.d.ts +2 -0
- package/dist/utils/skills-catalog.test.d.ts.map +1 -0
- package/dist/utils/skills-catalog.test.js +281 -0
- package/dist/utils/skills-catalog.test.js.map +1 -0
- package/dist/utils/workspace-launch.d.ts.map +1 -1
- package/dist/utils/workspace-launch.js +22 -1
- package/dist/utils/workspace-launch.js.map +1 -1
- package/dist/utils/workspace-launch.test.js +36 -2
- package/dist/utils/workspace-launch.test.js.map +1 -1
- package/package.json +4 -1
- package/dist/gui/assets/ProjectsPage-UBoQxI2G.js +0 -1
- package/dist/gui/assets/ScaffoldRepoInline-DqoourJe.js +0 -1
- package/dist/gui/assets/SettingsPage-BZl24tqD.js +0 -1
- package/dist/gui/assets/StartWorkPage-BGNFdBfj.js +0 -1
- package/dist/gui/assets/WorkspaceLauncher-Cxv0O_wt.js +0 -1
- package/dist/gui/assets/WorkspacesPage-Cvb1rOeh.js +0 -6
- package/dist/gui/assets/index-CllPiiFL.js +0 -52
- package/dist/gui/assets/index-DGCBhMAn.css +0 -2
- package/dist/gui/assets/useCompositeListItem-DWGFLKqA.js +0 -1
- /package/dist/{resources → static-resources}/workflows/plan-implement-review.md +0 -0
- /package/dist/{resources → static-resources}/workflows/research-verify.md +0 -0
- /package/dist/{resources → static-resources}/workflows/solo-developer.md +0 -0
package/dist/server.test.js
CHANGED
|
@@ -2,12 +2,15 @@ import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
|
2
2
|
import * as fs from 'node:fs/promises';
|
|
3
3
|
import * as path from 'node:path';
|
|
4
4
|
import { execa } from 'execa';
|
|
5
|
-
import { AgentTurnGate, app, dispatchAgentInput, isAllowedUpdateUrl } from './server.js';
|
|
5
|
+
import { AgentTurnGate, app, canOfferClaudeDesktopTransfer, dispatchAgentInput, isAllowedUpdateUrl } from './server.js';
|
|
6
6
|
import * as workspace from './core/workspace.js';
|
|
7
7
|
import * as config from './core/config.js';
|
|
8
8
|
import * as analyzers from './analyzers/index.js';
|
|
9
9
|
import * as generators from './generators/index.js';
|
|
10
10
|
import * as workflows from './utils/workflows.js';
|
|
11
|
+
import * as skillsCatalog from './utils/skills-catalog.js';
|
|
12
|
+
import * as agentsCatalog from './resources/agents-catalog.js';
|
|
13
|
+
import * as resourceService from './resources/service.js';
|
|
11
14
|
import * as detectAi from './utils/detect-ai.js';
|
|
12
15
|
import * as newRepo from './core/new-repo.js';
|
|
13
16
|
import * as orchestration from './orchestration/index.js';
|
|
@@ -23,6 +26,16 @@ vi.mock('./utils/update-check.js');
|
|
|
23
26
|
vi.mock('./analyzers/index.js');
|
|
24
27
|
vi.mock('./generators/index.js');
|
|
25
28
|
vi.mock('./utils/workflows.js');
|
|
29
|
+
vi.mock('./utils/skills-catalog.js');
|
|
30
|
+
vi.mock('./resources/agents-catalog.js');
|
|
31
|
+
vi.mock('./resources/service.js', async (importOriginal) => {
|
|
32
|
+
const actual = await importOriginal();
|
|
33
|
+
return {
|
|
34
|
+
...actual,
|
|
35
|
+
withResourceAdministrationLock: vi.fn((operation) => operation()),
|
|
36
|
+
validateResourceSelections: vi.fn().mockResolvedValue(undefined),
|
|
37
|
+
};
|
|
38
|
+
});
|
|
26
39
|
vi.mock('./utils/detect-ai.js', () => ({
|
|
27
40
|
detectAIAssistants: vi.fn().mockResolvedValue([])
|
|
28
41
|
}));
|
|
@@ -329,7 +342,7 @@ describe('Server API Endpoints Unit Tests', () => {
|
|
|
329
342
|
it('launches Codex with a once-encoded validated workspace path', async () => {
|
|
330
343
|
const platform = vi.spyOn(process, 'platform', 'get').mockReturnValue('win32');
|
|
331
344
|
vi.mocked(execa).mockImplementation((async (command) => ({
|
|
332
|
-
exitCode: command === 'reg.exe' || command === '
|
|
345
|
+
exitCode: command === 'reg.exe' || command === 'cmd.exe' ? 0 : 1,
|
|
333
346
|
})));
|
|
334
347
|
try {
|
|
335
348
|
const response = await app.request('/api/workspace/safe-workspace/launch', {
|
|
@@ -339,7 +352,14 @@ describe('Server API Endpoints Unit Tests', () => {
|
|
|
339
352
|
});
|
|
340
353
|
expect(response.status).toBe(200);
|
|
341
354
|
const prompt = 'Read the workspace instructions and implementation plan, inspect the repository state, then begin the task described for this workspace. Ask before making a decision that materially changes scope.\n\nWorkspace task: Improve the local Desktop handoff';
|
|
342
|
-
expect(execa).toHaveBeenCalledWith('
|
|
355
|
+
expect(execa).toHaveBeenCalledWith('cmd.exe', ['/d', '/v:off', '/s', '/c', 'start "" "%NEXUSFLOW_DESKTOP_URI%"'], {
|
|
356
|
+
env: {
|
|
357
|
+
NEXUSFLOW_DESKTOP_URI: `codex://threads/new?path=${encodeURIComponent(workspacePath)}&prompt=${encodeURIComponent(prompt)}`,
|
|
358
|
+
},
|
|
359
|
+
shell: false,
|
|
360
|
+
windowsHide: true,
|
|
361
|
+
windowsVerbatimArguments: true,
|
|
362
|
+
});
|
|
343
363
|
}
|
|
344
364
|
finally {
|
|
345
365
|
platform.mockRestore();
|
|
@@ -358,7 +378,12 @@ describe('Server API Endpoints Unit Tests', () => {
|
|
|
358
378
|
body: JSON.stringify({ targetId: 'codex-desktop', action: 'resume', sessionId }),
|
|
359
379
|
});
|
|
360
380
|
expect(response.status).toBe(200);
|
|
361
|
-
expect(execa).toHaveBeenCalledWith('
|
|
381
|
+
expect(execa).toHaveBeenCalledWith('cmd.exe', ['/d', '/v:off', '/s', '/c', 'start "" "%NEXUSFLOW_DESKTOP_URI%"'], {
|
|
382
|
+
env: { NEXUSFLOW_DESKTOP_URI: `codex://threads/${sessionId}` },
|
|
383
|
+
shell: false,
|
|
384
|
+
windowsHide: true,
|
|
385
|
+
windowsVerbatimArguments: true,
|
|
386
|
+
});
|
|
362
387
|
}
|
|
363
388
|
finally {
|
|
364
389
|
authorizeSession.mockRestore();
|
|
@@ -384,7 +409,7 @@ describe('Server API Endpoints Unit Tests', () => {
|
|
|
384
409
|
await expect(response.json()).resolves.toEqual({
|
|
385
410
|
error: 'Codex session not found in this workspace.',
|
|
386
411
|
});
|
|
387
|
-
expect(execa).not.toHaveBeenCalledWith('
|
|
412
|
+
expect(execa).not.toHaveBeenCalledWith('cmd.exe', expect.anything(), expect.anything());
|
|
388
413
|
}
|
|
389
414
|
finally {
|
|
390
415
|
authorizeSession.mockRestore();
|
|
@@ -392,6 +417,117 @@ describe('Server API Endpoints Unit Tests', () => {
|
|
|
392
417
|
}
|
|
393
418
|
});
|
|
394
419
|
});
|
|
420
|
+
describe('workspace recent session handoffs', () => {
|
|
421
|
+
const workspacesDir = path.resolve('session-workspaces');
|
|
422
|
+
const workspacePath = path.join(workspacesDir, 'safe-workspace');
|
|
423
|
+
const codexId = '0199a213-81c0-7800-8aa1-bbab2a035a53';
|
|
424
|
+
const claudeId = '0199a213-81c0-7800-8aa1-bbab2a035a54';
|
|
425
|
+
const unauthorizedCodexId = '0199a213-81c0-7800-8aa1-bbab2a035a55';
|
|
426
|
+
it('returns only server-authorized Desktop handoff capabilities', async () => {
|
|
427
|
+
vi.spyOn(config, 'loadConfig').mockResolvedValue({ workspacesDir });
|
|
428
|
+
vi.mocked(fs.realpath).mockImplementation(async (candidate) => path.resolve(String(candidate)));
|
|
429
|
+
vi.mocked(workspace.loadWorkspaceManifest).mockResolvedValue({
|
|
430
|
+
id: 'safe-workspace',
|
|
431
|
+
repos: [path.join(workspacePath, 'repo')],
|
|
432
|
+
workspacePath,
|
|
433
|
+
});
|
|
434
|
+
const find = vi.spyOn(sessionFinder, 'findSessions').mockResolvedValue([
|
|
435
|
+
{
|
|
436
|
+
id: 'copilot-session', assistant: 'copilot', title: 'Newer Copilot task',
|
|
437
|
+
createdAt: '2026-08-16T03:00:00.000Z', updatedAt: '2026-08-16T03:00:00.000Z',
|
|
438
|
+
messageCount: 2, workspacePath,
|
|
439
|
+
},
|
|
440
|
+
{
|
|
441
|
+
id: 'antigravity-session', assistant: 'antigravity', title: 'Newer Antigravity task',
|
|
442
|
+
createdAt: '2026-08-16T02:00:00.000Z', updatedAt: '2026-08-16T02:00:00.000Z',
|
|
443
|
+
messageCount: 2, workspacePath,
|
|
444
|
+
},
|
|
445
|
+
{
|
|
446
|
+
id: unauthorizedCodexId, assistant: 'codex', title: 'Fuzzy Codex task',
|
|
447
|
+
createdAt: '2026-08-16T01:00:00.000Z', updatedAt: '2026-08-16T01:00:00.000Z',
|
|
448
|
+
messageCount: 2, workspacePath,
|
|
449
|
+
},
|
|
450
|
+
{
|
|
451
|
+
id: codexId, assistant: 'codex', title: 'Codex task',
|
|
452
|
+
createdAt: '2026-08-15T00:00:00.000Z', updatedAt: '2026-08-16T00:00:00.000Z',
|
|
453
|
+
messageCount: 4, workspacePath,
|
|
454
|
+
},
|
|
455
|
+
{
|
|
456
|
+
id: claudeId, assistant: 'claude', title: 'Claude task',
|
|
457
|
+
createdAt: '2026-08-14T00:00:00.000Z', updatedAt: '2026-08-15T00:00:00.000Z',
|
|
458
|
+
messageCount: 2, workspacePath,
|
|
459
|
+
},
|
|
460
|
+
]);
|
|
461
|
+
const codexAuth = vi.spyOn(sessionFinder, 'canOpenCodexSessionInWorkspace')
|
|
462
|
+
.mockImplementation(async (_workspace, _repos, sessionId) => sessionId === codexId);
|
|
463
|
+
const claudeAuth = vi.spyOn(sessionFinder, 'canTransferClaudeSessionInWorkspace').mockResolvedValue(true);
|
|
464
|
+
const platform = vi.spyOn(process, 'platform', 'get').mockReturnValue('win32');
|
|
465
|
+
const architecture = vi.spyOn(process, 'arch', 'get').mockReturnValue('x64');
|
|
466
|
+
vi.stubEnv('ANTHROPIC_API_KEY', '');
|
|
467
|
+
vi.stubEnv('ANTHROPIC_AUTH_TOKEN', '');
|
|
468
|
+
vi.stubEnv('CLAUDE_CODE_USE_BEDROCK', '');
|
|
469
|
+
vi.stubEnv('CLAUDE_CODE_USE_VERTEX', '');
|
|
470
|
+
vi.stubEnv('CLAUDE_CODE_USE_FOUNDRY', '');
|
|
471
|
+
const provider = vi.spyOn(ProviderRegistry, 'getProvider').mockReturnValue({
|
|
472
|
+
isConfigured: () => true,
|
|
473
|
+
});
|
|
474
|
+
try {
|
|
475
|
+
const response = await app.request('/api/workspace/safe-workspace/sessions?limit=2&desktopHandoffOnly=true');
|
|
476
|
+
const body = await response.json();
|
|
477
|
+
expect(response.status).toBe(200);
|
|
478
|
+
expect(body.sessions).toHaveLength(2);
|
|
479
|
+
expect(body.sessions[0].desktopHandoff).toEqual({ targetId: 'codex-desktop', method: 'direct' });
|
|
480
|
+
expect(body.sessions[1].desktopHandoff).toEqual({ targetId: 'claude-desktop', method: 'guided' });
|
|
481
|
+
expect(codexAuth).toHaveBeenCalledWith(workspacePath, [path.join(workspacePath, 'repo')], codexId);
|
|
482
|
+
expect(claudeAuth).toHaveBeenCalledWith(workspacePath, [path.join(workspacePath, 'repo')], claudeId);
|
|
483
|
+
}
|
|
484
|
+
finally {
|
|
485
|
+
find.mockRestore();
|
|
486
|
+
codexAuth.mockRestore();
|
|
487
|
+
claudeAuth.mockRestore();
|
|
488
|
+
provider.mockRestore();
|
|
489
|
+
platform.mockRestore();
|
|
490
|
+
architecture.mockRestore();
|
|
491
|
+
vi.unstubAllEnvs();
|
|
492
|
+
}
|
|
493
|
+
});
|
|
494
|
+
it('omits a discovered session when strong authorization fails', async () => {
|
|
495
|
+
vi.spyOn(config, 'loadConfig').mockResolvedValue({ workspacesDir });
|
|
496
|
+
vi.mocked(fs.realpath).mockImplementation(async (candidate) => path.resolve(String(candidate)));
|
|
497
|
+
vi.mocked(workspace.loadWorkspaceManifest).mockResolvedValue({
|
|
498
|
+
id: 'safe-workspace', repos: [], workspacePath,
|
|
499
|
+
});
|
|
500
|
+
const find = vi.spyOn(sessionFinder, 'findSessions').mockResolvedValue([{
|
|
501
|
+
id: codexId, assistant: 'codex', title: 'Fuzzy match only',
|
|
502
|
+
createdAt: '2026-08-15T00:00:00.000Z', updatedAt: '2026-08-16T00:00:00.000Z',
|
|
503
|
+
messageCount: 2, workspacePath,
|
|
504
|
+
}]);
|
|
505
|
+
const codexAuth = vi.spyOn(sessionFinder, 'canOpenCodexSessionInWorkspace').mockResolvedValue(false);
|
|
506
|
+
try {
|
|
507
|
+
const response = await app.request('/api/workspace/safe-workspace/sessions?limit=3&desktopHandoffOnly=true');
|
|
508
|
+
expect(response.status).toBe(200);
|
|
509
|
+
await expect(response.json()).resolves.toEqual({ sessions: [] });
|
|
510
|
+
}
|
|
511
|
+
finally {
|
|
512
|
+
find.mockRestore();
|
|
513
|
+
codexAuth.mockRestore();
|
|
514
|
+
}
|
|
515
|
+
});
|
|
516
|
+
it('rejects an excessive recent-session limit', async () => {
|
|
517
|
+
const response = await app.request('/api/workspace/safe-workspace/sessions?limit=200');
|
|
518
|
+
expect(response.status).toBe(400);
|
|
519
|
+
await expect(response.json()).resolves.toEqual({ error: 'Session limit must be an integer from 1 to 20.' });
|
|
520
|
+
});
|
|
521
|
+
it('gates Claude Desktop transfer to supported subscription CLI environments', () => {
|
|
522
|
+
const configured = () => true;
|
|
523
|
+
expect(canOfferClaudeDesktopTransfer('win32', 'x64', {}, configured)).toBe(true);
|
|
524
|
+
expect(canOfferClaudeDesktopTransfer('darwin', 'arm64', {}, configured)).toBe(true);
|
|
525
|
+
expect(canOfferClaudeDesktopTransfer('linux', 'x64', {}, configured)).toBe(false);
|
|
526
|
+
expect(canOfferClaudeDesktopTransfer('win32', 'arm64', {}, configured)).toBe(false);
|
|
527
|
+
expect(canOfferClaudeDesktopTransfer('win32', 'x64', { ANTHROPIC_API_KEY: 'configured' }, configured)).toBe(false);
|
|
528
|
+
expect(canOfferClaudeDesktopTransfer('win32', 'x64', {}, () => false)).toBe(false);
|
|
529
|
+
});
|
|
530
|
+
});
|
|
395
531
|
describe('POST /api/workspace/:id/resume', () => {
|
|
396
532
|
it('should fail with 400 if command is forbidden', async () => {
|
|
397
533
|
vi.spyOn(config, 'loadConfig').mockResolvedValue({
|
|
@@ -1030,5 +1166,229 @@ describe('Server API Endpoints Unit Tests', () => {
|
|
|
1030
1166
|
expect(response.status).toBe(400);
|
|
1031
1167
|
});
|
|
1032
1168
|
});
|
|
1169
|
+
describe('Skills & Categories Endpoints', () => {
|
|
1170
|
+
it('GET /api/skills/categories returns default categories', async () => {
|
|
1171
|
+
vi.mocked(skillsCatalog.getSkillCategories).mockResolvedValue([
|
|
1172
|
+
{
|
|
1173
|
+
id: 'pull-requests',
|
|
1174
|
+
name: 'Pull Requests & Review',
|
|
1175
|
+
description: 'PR review skills',
|
|
1176
|
+
icon: 'git-pull-request',
|
|
1177
|
+
color: '#3b82f6',
|
|
1178
|
+
custom: false,
|
|
1179
|
+
isTemplate: true,
|
|
1180
|
+
},
|
|
1181
|
+
]);
|
|
1182
|
+
const response = await app.request('/api/skills/categories');
|
|
1183
|
+
expect(response.status).toBe(200);
|
|
1184
|
+
const data = await response.json();
|
|
1185
|
+
expect(data.categories).toBeDefined();
|
|
1186
|
+
expect(data.categories.length).toBe(1);
|
|
1187
|
+
expect(data.categories[0].id).toBe('pull-requests');
|
|
1188
|
+
expect(data.categories[0].isTemplate).toBe(true);
|
|
1189
|
+
});
|
|
1190
|
+
it('POST /api/skills/categories creates a custom category', async () => {
|
|
1191
|
+
vi.mocked(skillsCatalog.saveSkillCategory).mockResolvedValue({
|
|
1192
|
+
id: 'serverless-workflows',
|
|
1193
|
+
name: 'Serverless Workflows',
|
|
1194
|
+
description: 'AWS Lambda and Cloudflare workers',
|
|
1195
|
+
icon: 'zap',
|
|
1196
|
+
color: '#ec4899',
|
|
1197
|
+
custom: true,
|
|
1198
|
+
isTemplate: false,
|
|
1199
|
+
});
|
|
1200
|
+
const response = await app.request('/api/skills/categories', {
|
|
1201
|
+
method: 'POST',
|
|
1202
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1203
|
+
body: JSON.stringify({
|
|
1204
|
+
name: 'Serverless Workflows',
|
|
1205
|
+
description: 'AWS Lambda and Cloudflare workers',
|
|
1206
|
+
icon: 'zap',
|
|
1207
|
+
color: '#ec4899',
|
|
1208
|
+
}),
|
|
1209
|
+
});
|
|
1210
|
+
expect(response.status).toBe(200);
|
|
1211
|
+
const data = await response.json();
|
|
1212
|
+
expect(data.success).toBe(true);
|
|
1213
|
+
expect(data.category.id).toBe('serverless-workflows');
|
|
1214
|
+
expect(data.category.custom).toBe(true);
|
|
1215
|
+
});
|
|
1216
|
+
it('GET /api/skills returns all skills', async () => {
|
|
1217
|
+
vi.mocked(skillsCatalog.getAllSkills).mockResolvedValue([
|
|
1218
|
+
{
|
|
1219
|
+
id: 'pr-review-toolkit',
|
|
1220
|
+
name: 'pr-review-toolkit',
|
|
1221
|
+
title: 'PR Review Toolkit',
|
|
1222
|
+
category: 'pull-requests',
|
|
1223
|
+
description: 'Review pull requests',
|
|
1224
|
+
custom: false,
|
|
1225
|
+
content: '# PR Review',
|
|
1226
|
+
},
|
|
1227
|
+
]);
|
|
1228
|
+
const response = await app.request('/api/skills');
|
|
1229
|
+
expect(response.status).toBe(200);
|
|
1230
|
+
const data = await response.json();
|
|
1231
|
+
expect(data.skills).toBeDefined();
|
|
1232
|
+
expect(data.skills.length).toBe(1);
|
|
1233
|
+
expect(data.skills[0].id).toBe('pr-review-toolkit');
|
|
1234
|
+
});
|
|
1235
|
+
it('POST /api/skills creates a new skill package', async () => {
|
|
1236
|
+
vi.mocked(skillsCatalog.saveSkill).mockResolvedValue({
|
|
1237
|
+
id: 'graphql-linter',
|
|
1238
|
+
name: 'graphql-linter',
|
|
1239
|
+
title: 'GraphQL Schema Linter',
|
|
1240
|
+
category: 'database-migrations',
|
|
1241
|
+
description: 'Validates GraphQL schemas for deprecated fields',
|
|
1242
|
+
custom: true,
|
|
1243
|
+
content: '# GraphQL Schema Linter\n\nRun graphql-inspector validate.',
|
|
1244
|
+
});
|
|
1245
|
+
const response = await app.request('/api/skills', {
|
|
1246
|
+
method: 'POST',
|
|
1247
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1248
|
+
body: JSON.stringify({
|
|
1249
|
+
name: 'graphql-linter',
|
|
1250
|
+
title: 'GraphQL Schema Linter',
|
|
1251
|
+
category: 'database-migrations',
|
|
1252
|
+
description: 'Validates GraphQL schemas for deprecated fields',
|
|
1253
|
+
content: '# GraphQL Schema Linter\n\nRun graphql-inspector validate.',
|
|
1254
|
+
}),
|
|
1255
|
+
});
|
|
1256
|
+
expect(response.status).toBe(200);
|
|
1257
|
+
const data = await response.json();
|
|
1258
|
+
expect(data.success).toBe(true);
|
|
1259
|
+
expect(data.skill.id).toBe('graphql-linter');
|
|
1260
|
+
expect(data.skill.custom).toBe(true);
|
|
1261
|
+
});
|
|
1262
|
+
it('GET and POST /api/agents administer Codex-native agents', async () => {
|
|
1263
|
+
const agent = {
|
|
1264
|
+
id: 'reviewer',
|
|
1265
|
+
name: 'reviewer',
|
|
1266
|
+
category: 'general',
|
|
1267
|
+
description: 'Review a fixed diff.',
|
|
1268
|
+
developerInstructions: 'Review correctness and security.',
|
|
1269
|
+
sandboxMode: 'read-only',
|
|
1270
|
+
custom: true,
|
|
1271
|
+
};
|
|
1272
|
+
vi.mocked(agentsCatalog.getAllAgents).mockResolvedValue([agent]);
|
|
1273
|
+
vi.mocked(agentsCatalog.saveAgent).mockResolvedValue(agent);
|
|
1274
|
+
const listResponse = await app.request('/api/agents');
|
|
1275
|
+
expect(listResponse.status).toBe(200);
|
|
1276
|
+
await expect(listResponse.json()).resolves.toEqual({ agents: [agent] });
|
|
1277
|
+
const saveResponse = await app.request('/api/agents', {
|
|
1278
|
+
method: 'POST',
|
|
1279
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1280
|
+
body: JSON.stringify(agent),
|
|
1281
|
+
});
|
|
1282
|
+
expect(saveResponse.status).toBe(200);
|
|
1283
|
+
expect(agentsCatalog.saveAgent).toHaveBeenCalledWith(agent);
|
|
1284
|
+
});
|
|
1285
|
+
it('blocks deleting resources that are still assigned to a workspace', async () => {
|
|
1286
|
+
const workspacePath = path.resolve('/workspaces/example');
|
|
1287
|
+
vi.mocked(config.loadConfig).mockResolvedValue({ workspacesDir: path.resolve('/workspaces') });
|
|
1288
|
+
vi.mocked(workspace.listWorkspaces).mockResolvedValue([{ id: 'example', workspacePath }]);
|
|
1289
|
+
vi.mocked(fs.realpath).mockImplementation(async (candidate) => path.resolve(String(candidate)));
|
|
1290
|
+
vi.mocked(workspace.loadWorkspaceManifest).mockResolvedValue({ workspacePath });
|
|
1291
|
+
vi.mocked(skillsCatalog.getWorkspaceSkillsConfig).mockResolvedValue({
|
|
1292
|
+
schemaVersion: 1,
|
|
1293
|
+
revision: 1,
|
|
1294
|
+
enabledSkills: ['pr-review-toolkit'],
|
|
1295
|
+
enabledAgents: ['reviewer'],
|
|
1296
|
+
enabledCategories: [],
|
|
1297
|
+
});
|
|
1298
|
+
const skillResponse = await app.request('/api/skills/pr-review-toolkit', { method: 'DELETE' });
|
|
1299
|
+
expect(skillResponse.status).toBe(409);
|
|
1300
|
+
await expect(skillResponse.json()).resolves.toMatchObject({ workspaces: ['example'] });
|
|
1301
|
+
expect(skillsCatalog.deleteSkill).not.toHaveBeenCalled();
|
|
1302
|
+
const agentResponse = await app.request('/api/agents/reviewer', { method: 'DELETE' });
|
|
1303
|
+
expect(agentResponse.status).toBe(409);
|
|
1304
|
+
await expect(agentResponse.json()).resolves.toMatchObject({ workspaces: ['example'] });
|
|
1305
|
+
expect(agentsCatalog.deleteAgent).not.toHaveBeenCalled();
|
|
1306
|
+
});
|
|
1307
|
+
it('requires an exact existing workspace before reading resource assignments', async () => {
|
|
1308
|
+
vi.mocked(config.loadConfig).mockResolvedValue({ workspacesDir: '/workspaces' });
|
|
1309
|
+
vi.mocked(fs.realpath).mockImplementation(async (candidate) => {
|
|
1310
|
+
if (String(candidate).endsWith('missing')) {
|
|
1311
|
+
throw Object.assign(new Error('missing'), { code: 'ENOENT' });
|
|
1312
|
+
}
|
|
1313
|
+
return String(candidate);
|
|
1314
|
+
});
|
|
1315
|
+
const response = await app.request('/api/skills/workspace/missing');
|
|
1316
|
+
expect(response.status).toBe(404);
|
|
1317
|
+
expect(skillsCatalog.getWorkspaceSkillsConfig).not.toHaveBeenCalled();
|
|
1318
|
+
});
|
|
1319
|
+
it('saves versioned skill and agent assignments for an exact workspace', async () => {
|
|
1320
|
+
const workspacePath = path.resolve('/workspaces/example');
|
|
1321
|
+
vi.mocked(config.loadConfig).mockResolvedValue({ workspacesDir: path.resolve('/workspaces') });
|
|
1322
|
+
vi.mocked(fs.realpath).mockImplementation(async (candidate) => path.resolve(String(candidate)));
|
|
1323
|
+
vi.mocked(workspace.loadWorkspaceManifest).mockResolvedValue({ workspacePath });
|
|
1324
|
+
vi.mocked(skillsCatalog.saveWorkspaceSkillsConfig).mockResolvedValue({
|
|
1325
|
+
schemaVersion: 1,
|
|
1326
|
+
revision: 4,
|
|
1327
|
+
enabledSkills: ['pr-review-toolkit'],
|
|
1328
|
+
enabledAgents: ['reviewer'],
|
|
1329
|
+
enabledCategories: [],
|
|
1330
|
+
});
|
|
1331
|
+
const response = await app.request('/api/skills/workspace/example/assign', {
|
|
1332
|
+
method: 'POST',
|
|
1333
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1334
|
+
body: JSON.stringify({
|
|
1335
|
+
expectedRevision: 3,
|
|
1336
|
+
enabledSkills: ['pr-review-toolkit'],
|
|
1337
|
+
enabledAgents: ['reviewer'],
|
|
1338
|
+
enabledCategories: [],
|
|
1339
|
+
}),
|
|
1340
|
+
});
|
|
1341
|
+
expect(response.status).toBe(200);
|
|
1342
|
+
expect(skillsCatalog.saveWorkspaceSkillsConfig).toHaveBeenCalledWith(workspacePath, expect.objectContaining({ enabledSkills: ['pr-review-toolkit'], enabledAgents: ['reviewer'] }), 3);
|
|
1343
|
+
});
|
|
1344
|
+
it('rejects workspace assignments that reference missing catalog resources', async () => {
|
|
1345
|
+
const workspacePath = path.resolve('/workspaces/example');
|
|
1346
|
+
vi.mocked(config.loadConfig).mockResolvedValue({ workspacesDir: path.resolve('/workspaces') });
|
|
1347
|
+
vi.mocked(fs.realpath).mockImplementation(async (candidate) => path.resolve(String(candidate)));
|
|
1348
|
+
vi.mocked(workspace.loadWorkspaceManifest).mockResolvedValue({ workspacePath });
|
|
1349
|
+
vi.mocked(resourceService.validateResourceSelections).mockRejectedValueOnce(new resourceService.ResourceSelectionError(['missing-skill'], ['missing_agent']));
|
|
1350
|
+
const response = await app.request('/api/skills/workspace/example/assign', {
|
|
1351
|
+
method: 'POST',
|
|
1352
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1353
|
+
body: JSON.stringify({
|
|
1354
|
+
expectedRevision: 0,
|
|
1355
|
+
enabledSkills: ['missing-skill'],
|
|
1356
|
+
enabledAgents: ['missing_agent'],
|
|
1357
|
+
enabledCategories: [],
|
|
1358
|
+
}),
|
|
1359
|
+
});
|
|
1360
|
+
expect(response.status).toBe(400);
|
|
1361
|
+
await expect(response.json()).resolves.toMatchObject({
|
|
1362
|
+
missingSkills: ['missing-skill'],
|
|
1363
|
+
missingAgents: ['missing_agent'],
|
|
1364
|
+
});
|
|
1365
|
+
expect(skillsCatalog.saveWorkspaceSkillsConfig).not.toHaveBeenCalled();
|
|
1366
|
+
});
|
|
1367
|
+
it('requires an assignment revision and maps stale revisions to a conflict', async () => {
|
|
1368
|
+
const missingRevision = await app.request('/api/skills/workspace/example/assign', {
|
|
1369
|
+
method: 'POST',
|
|
1370
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1371
|
+
body: JSON.stringify({ enabledSkills: [], enabledAgents: [] }),
|
|
1372
|
+
});
|
|
1373
|
+
expect(missingRevision.status).toBe(400);
|
|
1374
|
+
expect(config.loadConfig).not.toHaveBeenCalled();
|
|
1375
|
+
const workspacePath = path.resolve('/workspaces/example');
|
|
1376
|
+
vi.mocked(config.loadConfig).mockResolvedValue({ workspacesDir: path.resolve('/workspaces') });
|
|
1377
|
+
vi.mocked(fs.realpath).mockImplementation(async (candidate) => path.resolve(String(candidate)));
|
|
1378
|
+
vi.mocked(workspace.loadWorkspaceManifest).mockResolvedValue({ workspacePath });
|
|
1379
|
+
const revisionError = new skillsCatalog.WorkspaceResourceRevisionError(2, 3);
|
|
1380
|
+
revisionError.message = 'Workspace resource configuration changed (expected revision 2, current 3).';
|
|
1381
|
+
vi.mocked(skillsCatalog.saveWorkspaceSkillsConfig).mockRejectedValueOnce(revisionError);
|
|
1382
|
+
const staleRevision = await app.request('/api/skills/workspace/example/assign', {
|
|
1383
|
+
method: 'POST',
|
|
1384
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1385
|
+
body: JSON.stringify({ expectedRevision: 2, enabledSkills: [], enabledAgents: [] }),
|
|
1386
|
+
});
|
|
1387
|
+
expect(staleRevision.status).toBe(409);
|
|
1388
|
+
await expect(staleRevision.json()).resolves.toMatchObject({
|
|
1389
|
+
error: expect.stringContaining('expected revision 2, current 3'),
|
|
1390
|
+
});
|
|
1391
|
+
});
|
|
1392
|
+
});
|
|
1033
1393
|
});
|
|
1034
1394
|
//# sourceMappingURL=server.test.js.map
|