sofia-cli 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. package/README.md +42 -20
  2. package/dist/infra/deploy.sh +193 -0
  3. package/dist/infra/gather-env.sh +211 -0
  4. package/dist/infra/infra/deploy.sh +193 -0
  5. package/dist/infra/infra/gather-env.sh +211 -0
  6. package/dist/infra/infra/main.bicep +90 -0
  7. package/dist/infra/infra/main.bicepparam +18 -0
  8. package/dist/infra/infra/resources.bicep +134 -0
  9. package/dist/infra/infra/teardown.sh +114 -0
  10. package/dist/infra/main.bicep +90 -0
  11. package/dist/infra/main.bicepparam +18 -0
  12. package/dist/infra/resources.bicep +134 -0
  13. package/dist/infra/teardown.sh +114 -0
  14. package/dist/src/cli/developCommand.js +0 -2
  15. package/dist/src/cli/index.js +8 -1
  16. package/dist/src/cli/workshopCommand.js +1 -1
  17. package/dist/src/develop/index.js +1 -1
  18. package/dist/src/develop/pocUtils.js +228 -0
  19. package/dist/src/develop/ralphLoop.js +8 -27
  20. package/dist/src/shared/data/cards.json +655 -670
  21. package/docs/architecture.md +2 -1
  22. package/package.json +5 -3
  23. package/src/cli/developCommand.ts +1 -3
  24. package/src/cli/index.ts +11 -1
  25. package/src/cli/workshopCommand.ts +21 -17
  26. package/src/develop/dynamicScaffolder.ts +36 -30
  27. package/src/develop/index.ts +13 -2
  28. package/src/develop/pocUtils.ts +296 -0
  29. package/src/develop/ralphLoop.ts +8 -28
  30. package/src/develop/templateRegistry.ts +19 -18
  31. package/src/shared/data/cards.json +655 -670
  32. package/tests/e2e/developE2e.spec.ts +3 -61
  33. package/tests/e2e/developFailureE2e.spec.ts +34 -38
  34. package/tests/integration/pocGithubMcp.spec.ts +29 -39
  35. package/tests/integration/pocLocalFallback.spec.ts +29 -39
  36. package/tests/integration/ralphLoopFlow.spec.ts +46 -66
  37. package/tests/integration/ralphLoopPartial.spec.ts +30 -37
  38. package/tests/unit/develop/githubMcpAdapter.spec.ts +0 -134
  39. package/tests/unit/develop/outputValidator.spec.ts +45 -21
  40. package/tests/unit/develop/ralphLoop.spec.ts +58 -94
  41. package/tsconfig.json +2 -1
  42. package/vitest.workspace.ts +5 -0
  43. package/dist/src/develop/pocScaffolder.js +0 -542
  44. package/dist/tests/e2e/developE2e.spec.js +0 -126
  45. package/dist/tests/e2e/developFailureE2e.spec.js +0 -247
  46. package/dist/tests/e2e/developPty.spec.js +0 -75
  47. package/dist/tests/e2e/discoveryWebSearchRelevance.spec.js +0 -84
  48. package/dist/tests/e2e/harness.spec.js +0 -83
  49. package/dist/tests/e2e/mcpLive.spec.js +0 -120
  50. package/dist/tests/e2e/newSession.e2e.spec.js +0 -177
  51. package/dist/tests/e2e/ralphLoopEnrichmentComparison.spec.js +0 -62
  52. package/dist/tests/e2e/workiqEnrichment.spec.js +0 -56
  53. package/dist/tests/e2e/zavaSimulation.spec.js +0 -452
  54. package/dist/tests/fixtures/test-fixture-project/src/add.js +0 -3
  55. package/dist/tests/fixtures/test-fixture-project/tests/failing.test.js +0 -6
  56. package/dist/tests/fixtures/test-fixture-project/tests/hanging.test.js +0 -8
  57. package/dist/tests/fixtures/test-fixture-project/tests/passing.test.js +0 -10
  58. package/dist/tests/fixtures/test-fixture-project/vitest.config.js +0 -6
  59. package/dist/tests/integration/autoStartConversation.spec.js +0 -138
  60. package/dist/tests/integration/defaultCommand.spec.js +0 -147
  61. package/dist/tests/integration/directCommandNonTty.spec.js +0 -224
  62. package/dist/tests/integration/directCommandTty.spec.js +0 -151
  63. package/dist/tests/integration/discoveryEnrichmentFlow.spec.js +0 -175
  64. package/dist/tests/integration/exportArtifacts.spec.js +0 -202
  65. package/dist/tests/integration/exportFallbackFlow.spec.js +0 -99
  66. package/dist/tests/integration/mcpDegradationFlow.spec.js +0 -190
  67. package/dist/tests/integration/mcpTransportFlow.spec.js +0 -139
  68. package/dist/tests/integration/newSessionFlow.spec.js +0 -343
  69. package/dist/tests/integration/pocGithubMcp.spec.js +0 -186
  70. package/dist/tests/integration/pocLocalFallback.spec.js +0 -171
  71. package/dist/tests/integration/pocScaffold.spec.js +0 -163
  72. package/dist/tests/integration/ralphLoopFlow.spec.js +0 -359
  73. package/dist/tests/integration/ralphLoopPartial.spec.js +0 -368
  74. package/dist/tests/integration/resumeAndBacktrack.spec.js +0 -247
  75. package/dist/tests/integration/spinnerLifecycle.spec.js +0 -220
  76. package/dist/tests/integration/summarizationFlow.spec.js +0 -115
  77. package/dist/tests/integration/testRunnerReal.spec.js +0 -52
  78. package/dist/tests/integration/webSearchAgent.spec.js +0 -128
  79. package/dist/tests/live/copilotSdkLive.spec.js +0 -107
  80. package/dist/tests/live/zavaFullWorkshop.spec.js +0 -392
  81. package/dist/tests/setup/loadEnv.js +0 -3
  82. package/dist/tests/unit/cli/developCommand.spec.js +0 -567
  83. package/dist/tests/unit/cli/directCommands.spec.js +0 -279
  84. package/dist/tests/unit/cli/envLoader.spec.js +0 -58
  85. package/dist/tests/unit/cli/ioContext.spec.js +0 -119
  86. package/dist/tests/unit/cli/preflight.spec.js +0 -108
  87. package/dist/tests/unit/cli/statusCommand.spec.js +0 -111
  88. package/dist/tests/unit/cli/workshopClientFallback.spec.js +0 -80
  89. package/dist/tests/unit/cli/workshopCommand.spec.js +0 -328
  90. package/dist/tests/unit/config/vitestEnvSetup.spec.js +0 -13
  91. package/dist/tests/unit/develop/checkpointState.spec.js +0 -315
  92. package/dist/tests/unit/develop/codeGenerator.spec.js +0 -355
  93. package/dist/tests/unit/develop/githubMcpAdapter.spec.js +0 -231
  94. package/dist/tests/unit/develop/mcpContextEnricher.spec.js +0 -433
  95. package/dist/tests/unit/develop/outputValidator.spec.js +0 -119
  96. package/dist/tests/unit/develop/pocScaffolder.spec.js +0 -353
  97. package/dist/tests/unit/develop/ralphLoop.spec.js +0 -1248
  98. package/dist/tests/unit/develop/templateRegistry.spec.js +0 -85
  99. package/dist/tests/unit/develop/testRunner.spec.js +0 -249
  100. package/dist/tests/unit/infraBicep.spec.js +0 -92
  101. package/dist/tests/unit/infraDeploy.spec.js +0 -82
  102. package/dist/tests/unit/infraTeardown.spec.js +0 -63
  103. package/dist/tests/unit/logging/logger.spec.js +0 -43
  104. package/dist/tests/unit/loop/conversationLoop.spec.js +0 -592
  105. package/dist/tests/unit/loop/phaseSummarizer.spec.js +0 -141
  106. package/dist/tests/unit/loop/streamingMarkdown.spec.js +0 -147
  107. package/dist/tests/unit/mcp/mcpManager.spec.js +0 -279
  108. package/dist/tests/unit/mcp/mcpTransport.spec.js +0 -529
  109. package/dist/tests/unit/mcp/retryPolicy.spec.js +0 -218
  110. package/dist/tests/unit/mcp/timeoutValidation.spec.js +0 -46
  111. package/dist/tests/unit/mcp/webSearch.spec.js +0 -567
  112. package/dist/tests/unit/phases/contextSummarizer.spec.js +0 -140
  113. package/dist/tests/unit/phases/discoveryEnricher.repeatCalls.spec.js +0 -93
  114. package/dist/tests/unit/phases/discoveryEnricher.spec.js +0 -411
  115. package/dist/tests/unit/phases/phaseExtractors.spec.js +0 -352
  116. package/dist/tests/unit/phases/phaseHandlers.spec.js +0 -425
  117. package/dist/tests/unit/prompts/promptLoader.spec.js +0 -118
  118. package/dist/tests/unit/schemas/pocSchemas.spec.js +0 -412
  119. package/dist/tests/unit/schemas/session.spec.js +0 -257
  120. package/dist/tests/unit/sessions/exportPaths.spec.js +0 -31
  121. package/dist/tests/unit/sessions/exportWriter.spec.js +0 -655
  122. package/dist/tests/unit/sessions/sessionManager.spec.js +0 -151
  123. package/dist/tests/unit/sessions/sessionStore.spec.js +0 -116
  124. package/dist/tests/unit/shared/activitySpinner.spec.js +0 -175
  125. package/dist/tests/unit/shared/cardsLoader.spec.js +0 -76
  126. package/dist/tests/unit/shared/copilotClient.spec.js +0 -155
  127. package/dist/tests/unit/shared/errorClassifier.spec.js +0 -131
  128. package/dist/tests/unit/shared/events.spec.js +0 -55
  129. package/dist/tests/unit/shared/markdownRenderer.spec.js +0 -35
  130. package/dist/tests/unit/shared/markdownRendererChunks.spec.js +0 -70
  131. package/dist/tests/unit/shared/tableRenderer.spec.js +0 -34
  132. package/dist/vitest.config.js +0 -14
  133. package/dist/vitest.live.config.js +0 -18
  134. package/src/develop/pocScaffolder.ts +0 -646
  135. package/tests/integration/pocScaffold.spec.ts +0 -220
  136. package/tests/unit/develop/pocScaffolder.spec.ts +0 -451
@@ -1,141 +0,0 @@
1
- /**
2
- * Phase summarizer tests.
3
- *
4
- * Tests for the post-phase summarization fallback that extracts
5
- * structured data from conversation transcripts when the conversation
6
- * loop's inline extraction fails.
7
- */
8
- import { describe, it, expect, vi } from 'vitest';
9
- import { needsSummarization, buildPhaseTranscript, phaseSummarize, } from '../../../src/loop/phaseSummarizer.js';
10
- function emptySession(overrides) {
11
- return {
12
- sessionId: 'test-1',
13
- schemaVersion: '1.0.0',
14
- createdAt: '2025-01-01T00:00:00Z',
15
- updatedAt: '2025-01-01T00:00:00Z',
16
- phase: 'Discover',
17
- status: 'Active',
18
- participants: [],
19
- artifacts: { generatedFiles: [] },
20
- turns: [],
21
- ...overrides,
22
- };
23
- }
24
- describe('needsSummarization', () => {
25
- it('returns true when Ideate session field is null', () => {
26
- const session = emptySession();
27
- expect(needsSummarization('Ideate', session)).toBe(true);
28
- });
29
- it('returns false when Ideate session field is populated', () => {
30
- const session = emptySession({
31
- ideas: [{ id: 'idea-1', title: 'Test', description: 'Desc', workflowStepIds: [] }],
32
- });
33
- expect(needsSummarization('Ideate', session)).toBe(false);
34
- });
35
- it('returns true when Design session field is null', () => {
36
- expect(needsSummarization('Design', emptySession())).toBe(true);
37
- });
38
- it('returns false when Design session field is populated', () => {
39
- const session = emptySession({
40
- evaluation: { method: 'feasibility-value-matrix', ideas: [] },
41
- });
42
- expect(needsSummarization('Design', session)).toBe(false);
43
- });
44
- it('returns false for unknown phase', () => {
45
- expect(needsSummarization('Complete', emptySession())).toBe(false);
46
- });
47
- });
48
- describe('buildPhaseTranscript', () => {
49
- it('returns empty string when no turns for phase', () => {
50
- const session = emptySession();
51
- expect(buildPhaseTranscript('Ideate', session)).toBe('');
52
- });
53
- it('concatenates turns for the specified phase', () => {
54
- const session = emptySession({
55
- turns: [
56
- { phase: 'Ideate', sequence: 1, role: 'user', content: 'Hello', timestamp: '2025-01-01T00:00:00Z' },
57
- { phase: 'Ideate', sequence: 2, role: 'assistant', content: 'Hi there', timestamp: '2025-01-01T00:00:00Z' },
58
- { phase: 'Design', sequence: 3, role: 'user', content: 'Other phase', timestamp: '2025-01-01T00:00:00Z' },
59
- ],
60
- });
61
- const transcript = buildPhaseTranscript('Ideate', session);
62
- expect(transcript).toContain('[user]: Hello');
63
- expect(transcript).toContain('[assistant]: Hi there');
64
- expect(transcript).not.toContain('Other phase');
65
- });
66
- });
67
- describe('phaseSummarize', () => {
68
- function createFakeClient(responseText) {
69
- return {
70
- createSession: vi.fn().mockResolvedValue({
71
- send: vi.fn().mockImplementation(async function* () {
72
- yield { type: 'TextDelta', text: responseText };
73
- }),
74
- }),
75
- };
76
- }
77
- function createHandler(extractReturn = {}) {
78
- return {
79
- phase: 'Ideate',
80
- buildSystemPrompt: () => 'test prompt',
81
- extractResult: vi.fn().mockReturnValue(extractReturn),
82
- };
83
- }
84
- it('returns empty object when session field already populated (no-op)', async () => {
85
- const session = emptySession({
86
- ideas: [{ id: 'idea-1', title: 'Test', description: 'Desc', workflowStepIds: [] }],
87
- });
88
- const client = createFakeClient('');
89
- const handler = createHandler();
90
- const result = await phaseSummarize(client, 'Ideate', session, handler);
91
- expect(result).toEqual({});
92
- expect(client.createSession).not.toHaveBeenCalled();
93
- });
94
- it('returns empty object when no transcript turns exist', async () => {
95
- const session = emptySession();
96
- const client = createFakeClient('');
97
- const handler = createHandler();
98
- const result = await phaseSummarize(client, 'Ideate', session, handler);
99
- expect(result).toEqual({});
100
- });
101
- it('extracts IdeaCard[] from LLM summary response', async () => {
102
- const ideas = [{ id: 'idea-1', title: 'AI Assistant', description: 'Automate tasks', workflowStepIds: ['s1'] }];
103
- const responseJson = '```json\n' + JSON.stringify(ideas) + '\n```';
104
- const client = createFakeClient(responseJson);
105
- const handler = createHandler({ ideas });
106
- const session = emptySession({
107
- turns: [
108
- { phase: 'Ideate', sequence: 1, role: 'user', content: 'Give me ideas', timestamp: '2025-01-01T00:00:00Z' },
109
- { phase: 'Ideate', sequence: 2, role: 'assistant', content: 'Here are ideas', timestamp: '2025-01-01T00:00:00Z' },
110
- ],
111
- });
112
- const result = await phaseSummarize(client, 'Ideate', session, handler);
113
- expect(result).toEqual({ ideas });
114
- expect(handler.extractResult).toHaveBeenCalledWith(session, responseJson);
115
- });
116
- it('returns empty object when LLM returns invalid response (no crash)', async () => {
117
- const client = createFakeClient('This is not JSON at all');
118
- const handler = createHandler({});
119
- const session = emptySession({
120
- turns: [
121
- { phase: 'Ideate', sequence: 1, role: 'user', content: 'Give me ideas', timestamp: '2025-01-01T00:00:00Z' },
122
- { phase: 'Ideate', sequence: 2, role: 'assistant', content: 'Here are ideas', timestamp: '2025-01-01T00:00:00Z' },
123
- ],
124
- });
125
- const result = await phaseSummarize(client, 'Ideate', session, handler);
126
- expect(result).toEqual({});
127
- });
128
- it('does not throw when client throws', async () => {
129
- const client = {
130
- createSession: vi.fn().mockRejectedValue(new Error('Network error')),
131
- };
132
- const handler = createHandler();
133
- const session = emptySession({
134
- turns: [
135
- { phase: 'Ideate', sequence: 1, role: 'user', content: 'Hello', timestamp: '2025-01-01T00:00:00Z' },
136
- ],
137
- });
138
- const result = await phaseSummarize(client, 'Ideate', session, handler);
139
- expect(result).toEqual({});
140
- });
141
- });
@@ -1,147 +0,0 @@
1
- /**
2
- * Tests for incremental markdown rendering during streaming (T080).
3
- *
4
- * Verifies that TextDelta chunks are rendered through markdownRenderer
5
- * in TTY mode, raw markdown in non-TTY/JSON mode, and that turn history
6
- * stores raw markdown (not ANSI).
7
- */
8
- import { describe, it, expect, vi } from 'vitest';
9
- import { ConversationLoop, } from '../../../src/loop/conversationLoop.js';
10
- import { createFakeCopilotClient } from '../../../src/shared/copilotClient.js';
11
- import * as markdownRenderer from '../../../src/shared/markdownRenderer.js';
12
- import { createNoOpSpinner } from '../../../src/shared/activitySpinner.js';
13
- // ── Helpers ─────────────────────────────────────────────────────────────────
14
- function makeSession(overrides) {
15
- return {
16
- sessionId: 'test-md-stream',
17
- schemaVersion: '1.0.0',
18
- createdAt: '2025-01-01T00:00:00Z',
19
- updatedAt: '2025-01-01T00:00:00Z',
20
- phase: 'Discover',
21
- status: 'Active',
22
- participants: [],
23
- artifacts: { generatedFiles: [] },
24
- ...overrides,
25
- };
26
- }
27
- function makeIO(inputs, opts) {
28
- let inputIndex = 0;
29
- const written = [];
30
- const activities = [];
31
- return {
32
- write(text) {
33
- written.push(text);
34
- },
35
- writeActivity(text) {
36
- activities.push(text);
37
- },
38
- writeToolSummary(_toolName, _summary) {
39
- // no-op for these tests
40
- },
41
- async readInput() {
42
- if (inputIndex >= inputs.length)
43
- return null;
44
- return inputs[inputIndex++] ?? null;
45
- },
46
- async showDecisionGate() {
47
- return { choice: 'continue' };
48
- },
49
- isJsonMode: opts?.json ?? false,
50
- isTTY: opts?.tty ?? true,
51
- get _written() {
52
- return written;
53
- },
54
- get _activities() {
55
- return activities;
56
- },
57
- };
58
- }
59
- function makePhaseHandler(overrides) {
60
- return {
61
- phase: 'Discover',
62
- buildSystemPrompt: () => 'System prompt',
63
- extractResult: () => ({}),
64
- ...overrides,
65
- };
66
- }
67
- // ── Tests ────────────────────────────────────────────────────────────────────
68
- describe('Incremental streaming markdown rendering (T080)', () => {
69
- it('renders TextDelta chunks through renderMarkdown in TTY mode', async () => {
70
- const renderSpy = vi.spyOn(markdownRenderer, 'renderMarkdown');
71
- const client = createFakeCopilotClient([
72
- { role: 'assistant', content: '## Hello World\n\nSome **bold** text.' },
73
- ]);
74
- const io = makeIO(['test input'], { tty: true, json: false });
75
- const loop = new ConversationLoop({
76
- client,
77
- io,
78
- session: makeSession(),
79
- phaseHandler: makePhaseHandler(),
80
- spinner: createNoOpSpinner(),
81
- });
82
- await loop.run();
83
- // renderMarkdown should have been called for the TextDelta chunk
84
- expect(renderSpy).toHaveBeenCalled();
85
- const callArgs = renderSpy.mock.calls;
86
- // At least one call should have the LLM content
87
- const contentCalls = callArgs.filter(([text]) => text.includes('Hello World'));
88
- expect(contentCalls.length).toBeGreaterThanOrEqual(1);
89
- renderSpy.mockRestore();
90
- });
91
- it('writes raw text in non-TTY mode without markdown rendering', async () => {
92
- const client = createFakeCopilotClient([
93
- { role: 'assistant', content: '## Raw heading' },
94
- ]);
95
- const io = makeIO(['test'], { tty: false, json: false });
96
- const loop = new ConversationLoop({
97
- client,
98
- io,
99
- session: makeSession(),
100
- phaseHandler: makePhaseHandler(),
101
- spinner: createNoOpSpinner(),
102
- });
103
- await loop.run();
104
- // In non-TTY mode, raw text should be written
105
- const allOutput = io._written.join('');
106
- expect(allOutput).toContain('## Raw heading');
107
- });
108
- it('preserves raw markdown in JSON mode output', async () => {
109
- const client = createFakeCopilotClient([
110
- { role: 'assistant', content: '**Bold** text' },
111
- ]);
112
- const io = makeIO(['test'], { tty: false, json: true });
113
- const loop = new ConversationLoop({
114
- client,
115
- io,
116
- session: makeSession(),
117
- phaseHandler: makePhaseHandler(),
118
- spinner: createNoOpSpinner(),
119
- });
120
- await loop.run();
121
- const jsonOutputs = io._written.filter((w) => w.startsWith('{'));
122
- expect(jsonOutputs.length).toBeGreaterThanOrEqual(1);
123
- const parsed = JSON.parse(jsonOutputs[0]);
124
- expect(parsed.content).toBe('**Bold** text');
125
- });
126
- it('stores raw markdown in turn history (not ANSI)', async () => {
127
- const client = createFakeCopilotClient([
128
- { role: 'assistant', content: '## Phase Output\n\nContent here.' },
129
- ]);
130
- const io = makeIO(['input'], { tty: true, json: false });
131
- const loop = new ConversationLoop({
132
- client,
133
- io,
134
- session: makeSession(),
135
- phaseHandler: makePhaseHandler(),
136
- spinner: createNoOpSpinner(),
137
- });
138
- const result = await loop.run();
139
- // Turn history should contain raw markdown, not ANSI escape codes
140
- const assistantTurn = result.turns?.find(t => t.role === 'assistant');
141
- expect(assistantTurn).toBeDefined();
142
- expect(assistantTurn.content).toBe('## Phase Output\n\nContent here.');
143
- // Should NOT contain ANSI escape sequences
144
- // eslint-disable-next-line no-control-regex
145
- expect(assistantTurn.content).not.toMatch(/\u001b\[/);
146
- });
147
- });
@@ -1,279 +0,0 @@
1
- /**
2
- * MCP Manager tests.
3
- *
4
- * The McpManager loads .vscode/mcp.json, manages connections to MCP servers,
5
- * lists available tools, and classifies errors.
6
- *
7
- * T007: Tests for callTool() real dispatch (lazy transport, retry, normalization)
8
- * T049: Tests for toSdkMcpServers() conversion
9
- */
10
- import { describe, it, expect } from 'vitest';
11
- import { McpManager, loadMcpConfig, classifyMcpError, toSdkMcpServers, } from '../../../src/mcp/mcpManager.js';
12
- // ── Tests ────────────────────────────────────────────────────────────────────
13
- describe('McpManager', () => {
14
- describe('loadMcpConfig', () => {
15
- it('loads and parses .vscode/mcp.json from given path', async () => {
16
- const config = await loadMcpConfig(new URL('../../../.vscode/mcp.json', import.meta.url).pathname);
17
- expect(config).toBeDefined();
18
- expect(config.servers).toBeDefined();
19
- expect(Object.keys(config.servers).length).toBeGreaterThan(0);
20
- });
21
- it('returns empty servers when file does not exist', async () => {
22
- const config = await loadMcpConfig('/nonexistent/mcp.json');
23
- expect(config.servers).toEqual({});
24
- });
25
- it('identifies server types correctly (stdio vs http)', async () => {
26
- const config = await loadMcpConfig(new URL('../../../.vscode/mcp.json', import.meta.url).pathname);
27
- // workiq has command → stdio type
28
- const workiq = config.servers['workiq'];
29
- expect(workiq).toBeDefined();
30
- expect(workiq.type).toBe('stdio');
31
- // github has url → http type
32
- const github = config.servers['github'];
33
- expect(github).toBeDefined();
34
- expect(github.type).toBe('http');
35
- });
36
- });
37
- describe('McpManager instance', () => {
38
- it('can be created with a config', () => {
39
- const config = {
40
- servers: {
41
- testServer: {
42
- name: 'testServer',
43
- type: 'stdio',
44
- command: 'echo',
45
- args: ['hello'],
46
- },
47
- },
48
- };
49
- const manager = new McpManager(config);
50
- expect(manager).toBeDefined();
51
- });
52
- it('listServers returns configured server names', () => {
53
- const config = {
54
- servers: {
55
- s1: { name: 's1', type: 'stdio', command: 'echo', args: [] },
56
- s2: { name: 's2', type: 'http', url: 'http://example.com' },
57
- },
58
- };
59
- const manager = new McpManager(config);
60
- const names = manager.listServers();
61
- expect(names).toEqual(['s1', 's2']);
62
- });
63
- it('getServerConfig returns config for a known server', () => {
64
- const config = {
65
- servers: {
66
- myServer: {
67
- name: 'myServer',
68
- type: 'stdio',
69
- command: 'npx',
70
- args: ['-y', 'my-tool'],
71
- },
72
- },
73
- };
74
- const manager = new McpManager(config);
75
- const sc = manager.getServerConfig('myServer');
76
- expect(sc).toBeDefined();
77
- expect(sc.type).toBe('stdio');
78
- expect(sc.command).toBe('npx');
79
- });
80
- it('getServerConfig returns undefined for unknown server', () => {
81
- const manager = new McpManager({ servers: {} });
82
- expect(manager.getServerConfig('nope')).toBeUndefined();
83
- });
84
- it('isAvailable returns false when not connected', () => {
85
- const config = {
86
- servers: {
87
- s1: { name: 's1', type: 'stdio', command: 'echo', args: [] },
88
- },
89
- };
90
- const manager = new McpManager(config);
91
- expect(manager.isAvailable('s1')).toBe(false);
92
- });
93
- });
94
- describe('classifyMcpError', () => {
95
- it('classifies ECONNREFUSED as connection-refused', () => {
96
- const err = new Error('connect ECONNREFUSED 127.0.0.1:3000');
97
- err.code = 'ECONNREFUSED';
98
- expect(classifyMcpError(err)).toBe('connection-refused');
99
- });
100
- it('classifies ENOTFOUND as dns-failure', () => {
101
- const err = new Error('getaddrinfo ENOTFOUND example.com');
102
- err.code = 'ENOTFOUND';
103
- expect(classifyMcpError(err)).toBe('dns-failure');
104
- });
105
- it('classifies ETIMEDOUT as timeout', () => {
106
- const err = new Error('connect ETIMEDOUT');
107
- err.code = 'ETIMEDOUT';
108
- expect(classifyMcpError(err)).toBe('timeout');
109
- });
110
- it('classifies unknown errors as unknown', () => {
111
- expect(classifyMcpError(new Error('something weird'))).toBe('unknown');
112
- });
113
- it('classifies non-Error values as unknown', () => {
114
- expect(classifyMcpError('string error')).toBe('unknown');
115
- });
116
- });
117
- // ── T007: callTool() real dispatch tests ─────────────────────────────────
118
- describe('callTool() real dispatch', () => {
119
- it('throws when server is not in config', async () => {
120
- const manager = new McpManager({ servers: {} });
121
- await expect(manager.callTool('nonexistent', 'tool', {})).rejects.toThrow(/[Uu]nknown.*nonexistent|not available/);
122
- });
123
- it('returns unwrapped content from ToolCallResponse', async () => {
124
- // This test will fail until T016 implements real dispatch.
125
- // Once implemented, McpManager.callTool should return the
126
- // unwrapped content from the transport's ToolCallResponse.
127
- const config = {
128
- servers: {
129
- testserver: {
130
- name: 'testserver',
131
- type: 'http',
132
- url: 'https://test.example.com/mcp',
133
- },
134
- },
135
- };
136
- const manager = new McpManager(config);
137
- manager.markConnected('testserver');
138
- // After T016: This should dispatch to transport and return parsed content.
139
- // For now, we just verify the method exists and can be called
140
- // (it currently throws "not yet wired to transport").
141
- try {
142
- await manager.callTool('testserver', 'search', { query: 'test' });
143
- }
144
- catch (err) {
145
- // Expected to throw "not yet wired" until T016
146
- expect(err).toBeInstanceOf(Error);
147
- }
148
- });
149
- it('throws when calling unavailable server', async () => {
150
- const config = {
151
- servers: {
152
- myserver: {
153
- name: 'myserver',
154
- type: 'stdio',
155
- command: 'echo',
156
- args: [],
157
- },
158
- },
159
- };
160
- const manager = new McpManager(config);
161
- // Don't mark connected
162
- await expect(manager.callTool('myserver', 'tool', {})).rejects.toThrow(/not available/);
163
- });
164
- });
165
- // ── T049: toSdkMcpServers() tests ───────────────────────────────────────
166
- describe('toSdkMcpServers()', () => {
167
- it('converts StdioServerConfig to SDK format', () => {
168
- const config = {
169
- servers: {
170
- context7: {
171
- name: 'context7',
172
- type: 'stdio',
173
- command: 'npx',
174
- args: ['-y', '@upstash/context7-mcp'],
175
- env: { NODE_ENV: 'production' },
176
- cwd: '/tmp',
177
- tools: ['resolve-library-id', 'query-docs'],
178
- timeout: 15000,
179
- },
180
- },
181
- };
182
- const result = toSdkMcpServers(config);
183
- expect(result).toEqual({
184
- context7: {
185
- type: 'stdio',
186
- command: 'npx',
187
- args: ['-y', '@upstash/context7-mcp'],
188
- tools: ['resolve-library-id', 'query-docs'],
189
- env: { NODE_ENV: 'production' },
190
- cwd: '/tmp',
191
- timeout: 15000,
192
- },
193
- });
194
- });
195
- it('converts HttpServerConfig to SDK format', () => {
196
- const config = {
197
- servers: {
198
- github: {
199
- name: 'github',
200
- type: 'http',
201
- url: 'https://api.githubcopilot.com/mcp/',
202
- headers: { 'X-Custom': 'value' },
203
- tools: ['create_repository'],
204
- timeout: 60000,
205
- },
206
- },
207
- };
208
- const result = toSdkMcpServers(config);
209
- expect(result).toEqual({
210
- github: {
211
- type: 'http',
212
- url: 'https://api.githubcopilot.com/mcp/',
213
- tools: ['create_repository'],
214
- headers: { 'X-Custom': 'value' },
215
- timeout: 60000,
216
- },
217
- });
218
- });
219
- it('returns empty object for empty servers', () => {
220
- const config = { servers: {} };
221
- const result = toSdkMcpServers(config);
222
- expect(result).toEqual({});
223
- });
224
- it('defaults tools to ["*"] when not specified', () => {
225
- const config = {
226
- servers: {
227
- simple: {
228
- name: 'simple',
229
- type: 'stdio',
230
- command: 'echo',
231
- args: [],
232
- },
233
- },
234
- };
235
- const result = toSdkMcpServers(config);
236
- expect(result.simple.tools).toEqual(['*']);
237
- });
238
- it('omits optional fields when not in source config', () => {
239
- const config = {
240
- servers: {
241
- minimal: {
242
- name: 'minimal',
243
- type: 'http',
244
- url: 'https://example.com/mcp',
245
- },
246
- },
247
- };
248
- const result = toSdkMcpServers(config);
249
- expect(result.minimal).toEqual({
250
- type: 'http',
251
- url: 'https://example.com/mcp',
252
- tools: ['*'],
253
- });
254
- expect(result.minimal).not.toHaveProperty('headers');
255
- expect(result.minimal).not.toHaveProperty('timeout');
256
- });
257
- it('converts mixed stdio and http configs', () => {
258
- const config = {
259
- servers: {
260
- local: {
261
- name: 'local',
262
- type: 'stdio',
263
- command: 'node',
264
- args: ['server.js'],
265
- },
266
- remote: {
267
- name: 'remote',
268
- type: 'http',
269
- url: 'https://api.example.com',
270
- },
271
- },
272
- };
273
- const result = toSdkMcpServers(config);
274
- expect(Object.keys(result)).toEqual(['local', 'remote']);
275
- expect(result.local.type).toBe('stdio');
276
- expect(result.remote.type).toBe('http');
277
- });
278
- });
279
- });