sofia-cli 0.1.1 → 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.
- package/README.md +42 -20
- package/dist/infra/deploy.sh +193 -0
- package/dist/infra/gather-env.sh +211 -0
- package/dist/infra/infra/deploy.sh +193 -0
- package/dist/infra/infra/gather-env.sh +211 -0
- package/dist/infra/infra/main.bicep +90 -0
- package/dist/infra/infra/main.bicepparam +18 -0
- package/dist/infra/infra/resources.bicep +134 -0
- package/dist/infra/infra/teardown.sh +114 -0
- package/dist/infra/main.bicep +90 -0
- package/dist/infra/main.bicepparam +18 -0
- package/dist/infra/resources.bicep +134 -0
- package/dist/infra/teardown.sh +114 -0
- package/dist/src/cli/developCommand.js +0 -2
- package/dist/src/cli/index.js +8 -1
- package/dist/src/cli/workshopCommand.js +1 -1
- package/dist/src/develop/index.js +1 -1
- package/dist/src/develop/pocUtils.js +228 -0
- package/dist/src/develop/ralphLoop.js +3 -3
- package/dist/src/shared/data/cards.json +655 -670
- package/docs/architecture.md +2 -1
- package/package.json +5 -3
- package/src/cli/developCommand.ts +1 -3
- package/src/cli/index.ts +11 -1
- package/src/cli/workshopCommand.ts +21 -17
- package/src/develop/dynamicScaffolder.ts +36 -30
- package/src/develop/index.ts +13 -2
- package/src/develop/pocUtils.ts +296 -0
- package/src/develop/ralphLoop.ts +8 -28
- package/src/develop/templateRegistry.ts +19 -18
- package/src/shared/data/cards.json +655 -670
- package/tests/e2e/developE2e.spec.ts +3 -61
- package/tests/e2e/developFailureE2e.spec.ts +34 -38
- package/tests/integration/pocGithubMcp.spec.ts +29 -39
- package/tests/integration/pocLocalFallback.spec.ts +29 -39
- package/tests/integration/ralphLoopFlow.spec.ts +46 -66
- package/tests/integration/ralphLoopPartial.spec.ts +30 -37
- package/tests/unit/develop/githubMcpAdapter.spec.ts +0 -134
- package/tests/unit/develop/outputValidator.spec.ts +45 -21
- package/tests/unit/develop/ralphLoop.spec.ts +58 -94
- package/tsconfig.json +2 -1
- package/vitest.workspace.ts +5 -0
- package/dist/src/develop/pocScaffolder.js +0 -542
- package/dist/tests/e2e/developE2e.spec.js +0 -126
- package/dist/tests/e2e/developFailureE2e.spec.js +0 -247
- package/dist/tests/e2e/developPty.spec.js +0 -75
- package/dist/tests/e2e/discoveryWebSearchRelevance.spec.js +0 -84
- package/dist/tests/e2e/harness.spec.js +0 -83
- package/dist/tests/e2e/mcpLive.spec.js +0 -120
- package/dist/tests/e2e/newSession.e2e.spec.js +0 -177
- package/dist/tests/e2e/ralphLoopEnrichmentComparison.spec.js +0 -62
- package/dist/tests/e2e/workiqEnrichment.spec.js +0 -56
- package/dist/tests/e2e/zavaSimulation.spec.js +0 -452
- package/dist/tests/fixtures/test-fixture-project/src/add.js +0 -3
- package/dist/tests/fixtures/test-fixture-project/tests/failing.test.js +0 -6
- package/dist/tests/fixtures/test-fixture-project/tests/hanging.test.js +0 -8
- package/dist/tests/fixtures/test-fixture-project/tests/passing.test.js +0 -10
- package/dist/tests/fixtures/test-fixture-project/vitest.config.js +0 -6
- package/dist/tests/integration/autoStartConversation.spec.js +0 -138
- package/dist/tests/integration/defaultCommand.spec.js +0 -147
- package/dist/tests/integration/directCommandNonTty.spec.js +0 -224
- package/dist/tests/integration/directCommandTty.spec.js +0 -151
- package/dist/tests/integration/discoveryEnrichmentFlow.spec.js +0 -175
- package/dist/tests/integration/exportArtifacts.spec.js +0 -202
- package/dist/tests/integration/exportFallbackFlow.spec.js +0 -99
- package/dist/tests/integration/mcpDegradationFlow.spec.js +0 -190
- package/dist/tests/integration/mcpTransportFlow.spec.js +0 -139
- package/dist/tests/integration/newSessionFlow.spec.js +0 -343
- package/dist/tests/integration/pocGithubMcp.spec.js +0 -186
- package/dist/tests/integration/pocLocalFallback.spec.js +0 -171
- package/dist/tests/integration/pocScaffold.spec.js +0 -163
- package/dist/tests/integration/ralphLoopFlow.spec.js +0 -359
- package/dist/tests/integration/ralphLoopPartial.spec.js +0 -368
- package/dist/tests/integration/resumeAndBacktrack.spec.js +0 -247
- package/dist/tests/integration/spinnerLifecycle.spec.js +0 -220
- package/dist/tests/integration/summarizationFlow.spec.js +0 -115
- package/dist/tests/integration/testRunnerReal.spec.js +0 -52
- package/dist/tests/integration/webSearchAgent.spec.js +0 -128
- package/dist/tests/live/copilotSdkLive.spec.js +0 -107
- package/dist/tests/live/zavaFullWorkshop.spec.js +0 -392
- package/dist/tests/setup/loadEnv.js +0 -3
- package/dist/tests/unit/cli/developCommand.spec.js +0 -567
- package/dist/tests/unit/cli/directCommands.spec.js +0 -279
- package/dist/tests/unit/cli/envLoader.spec.js +0 -58
- package/dist/tests/unit/cli/ioContext.spec.js +0 -119
- package/dist/tests/unit/cli/preflight.spec.js +0 -108
- package/dist/tests/unit/cli/statusCommand.spec.js +0 -111
- package/dist/tests/unit/cli/workshopClientFallback.spec.js +0 -80
- package/dist/tests/unit/cli/workshopCommand.spec.js +0 -329
- package/dist/tests/unit/config/vitestEnvSetup.spec.js +0 -13
- package/dist/tests/unit/develop/checkpointState.spec.js +0 -315
- package/dist/tests/unit/develop/codeGenerator.spec.js +0 -355
- package/dist/tests/unit/develop/githubMcpAdapter.spec.js +0 -231
- package/dist/tests/unit/develop/mcpContextEnricher.spec.js +0 -433
- package/dist/tests/unit/develop/outputValidator.spec.js +0 -119
- package/dist/tests/unit/develop/pocScaffolder.spec.js +0 -353
- package/dist/tests/unit/develop/ralphLoop.spec.js +0 -1248
- package/dist/tests/unit/develop/templateRegistry.spec.js +0 -85
- package/dist/tests/unit/develop/testRunner.spec.js +0 -249
- package/dist/tests/unit/infraBicep.spec.js +0 -92
- package/dist/tests/unit/infraDeploy.spec.js +0 -82
- package/dist/tests/unit/infraTeardown.spec.js +0 -63
- package/dist/tests/unit/logging/logger.spec.js +0 -43
- package/dist/tests/unit/loop/conversationLoop.spec.js +0 -592
- package/dist/tests/unit/loop/phaseSummarizer.spec.js +0 -141
- package/dist/tests/unit/loop/streamingMarkdown.spec.js +0 -147
- package/dist/tests/unit/mcp/mcpManager.spec.js +0 -279
- package/dist/tests/unit/mcp/mcpTransport.spec.js +0 -529
- package/dist/tests/unit/mcp/retryPolicy.spec.js +0 -218
- package/dist/tests/unit/mcp/timeoutValidation.spec.js +0 -46
- package/dist/tests/unit/mcp/webSearch.spec.js +0 -567
- package/dist/tests/unit/phases/contextSummarizer.spec.js +0 -140
- package/dist/tests/unit/phases/discoveryEnricher.repeatCalls.spec.js +0 -93
- package/dist/tests/unit/phases/discoveryEnricher.spec.js +0 -411
- package/dist/tests/unit/phases/phaseExtractors.spec.js +0 -352
- package/dist/tests/unit/phases/phaseHandlers.spec.js +0 -425
- package/dist/tests/unit/prompts/promptLoader.spec.js +0 -118
- package/dist/tests/unit/schemas/pocSchemas.spec.js +0 -412
- package/dist/tests/unit/schemas/session.spec.js +0 -257
- package/dist/tests/unit/sessions/exportPaths.spec.js +0 -31
- package/dist/tests/unit/sessions/exportWriter.spec.js +0 -655
- package/dist/tests/unit/sessions/sessionManager.spec.js +0 -151
- package/dist/tests/unit/sessions/sessionStore.spec.js +0 -116
- package/dist/tests/unit/shared/activitySpinner.spec.js +0 -175
- package/dist/tests/unit/shared/cardsLoader.spec.js +0 -76
- package/dist/tests/unit/shared/copilotClient.spec.js +0 -155
- package/dist/tests/unit/shared/errorClassifier.spec.js +0 -131
- package/dist/tests/unit/shared/events.spec.js +0 -55
- package/dist/tests/unit/shared/markdownRenderer.spec.js +0 -35
- package/dist/tests/unit/shared/markdownRendererChunks.spec.js +0 -70
- package/dist/tests/unit/shared/tableRenderer.spec.js +0 -34
- package/dist/vitest.config.js +0 -14
- package/dist/vitest.live.config.js +0 -18
- package/src/develop/pocScaffolder.ts +0 -646
- package/tests/integration/pocScaffold.spec.ts +0 -220
- package/tests/unit/develop/pocScaffolder.spec.ts +0 -451
|
@@ -1,352 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Phase extraction logic tests.
|
|
3
|
-
*
|
|
4
|
-
* Tests for extractResult() implementations that parse structured JSON
|
|
5
|
-
* blocks from LLM responses and map them to WorkshopSession fields.
|
|
6
|
-
*/
|
|
7
|
-
import { describe, it, expect } from 'vitest';
|
|
8
|
-
import { extractJsonBlock, extractBusinessContext, extractWorkflow, extractIdeas, extractEvaluation, extractSelection, extractPlan, extractPocState, extractSessionName, extractAllJsonBlocks, extractJsonBlockForSchema, } from '../../../src/phases/phaseExtractors.js';
|
|
9
|
-
import { z } from '../../../src/vendor/zod.js';
|
|
10
|
-
function _emptySession() {
|
|
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
|
-
};
|
|
22
|
-
}
|
|
23
|
-
describe('extractJsonBlock', () => {
|
|
24
|
-
it('extracts JSON from markdown code fence', () => {
|
|
25
|
-
const response = `Here are the results:\n\`\`\`json\n{"key": "value"}\n\`\`\`\nDone.`;
|
|
26
|
-
const result = extractJsonBlock(response);
|
|
27
|
-
expect(result).toEqual({ key: 'value' });
|
|
28
|
-
});
|
|
29
|
-
it('extracts JSON from unfenced block', () => {
|
|
30
|
-
const response = `Result: {"businessDescription": "A company"}`;
|
|
31
|
-
const result = extractJsonBlock(response);
|
|
32
|
-
expect(result).toEqual({ businessDescription: 'A company' });
|
|
33
|
-
});
|
|
34
|
-
it('returns null when no JSON found', () => {
|
|
35
|
-
const response = 'This is a plain text response with no JSON.';
|
|
36
|
-
const result = extractJsonBlock(response);
|
|
37
|
-
expect(result).toBeNull();
|
|
38
|
-
});
|
|
39
|
-
it('handles malformed JSON gracefully', () => {
|
|
40
|
-
const response = '```json\n{broken: json}\n```';
|
|
41
|
-
const result = extractJsonBlock(response);
|
|
42
|
-
expect(result).toBeNull();
|
|
43
|
-
});
|
|
44
|
-
it('extracts JSON array from code fence', () => {
|
|
45
|
-
const response = '```json\n[{"id": "1"}, {"id": "2"}]\n```';
|
|
46
|
-
const result = extractJsonBlock(response);
|
|
47
|
-
expect(result).toEqual([{ id: '1' }, { id: '2' }]);
|
|
48
|
-
});
|
|
49
|
-
});
|
|
50
|
-
describe('extractBusinessContext', () => {
|
|
51
|
-
it('extracts valid business context from response', () => {
|
|
52
|
-
const response = `Here's what I gathered:
|
|
53
|
-
|
|
54
|
-
\`\`\`json
|
|
55
|
-
{
|
|
56
|
-
"businessDescription": "A logistics company",
|
|
57
|
-
"challenges": ["Slow routing", "High costs"]
|
|
58
|
-
}
|
|
59
|
-
\`\`\``;
|
|
60
|
-
const result = extractBusinessContext(response);
|
|
61
|
-
expect(result).not.toBeNull();
|
|
62
|
-
expect(result.businessDescription).toBe('A logistics company');
|
|
63
|
-
expect(result.challenges).toEqual(['Slow routing', 'High costs']);
|
|
64
|
-
});
|
|
65
|
-
it('returns null for non-matching JSON', () => {
|
|
66
|
-
const response = '```json\n{"unrelated": "data"}\n```';
|
|
67
|
-
const result = extractBusinessContext(response);
|
|
68
|
-
expect(result).toBeNull();
|
|
69
|
-
});
|
|
70
|
-
it('returns null when no JSON present', () => {
|
|
71
|
-
const result = extractBusinessContext('No structured data here.');
|
|
72
|
-
expect(result).toBeNull();
|
|
73
|
-
});
|
|
74
|
-
it('includes optional fields when present', () => {
|
|
75
|
-
const response = `\`\`\`json
|
|
76
|
-
{
|
|
77
|
-
"businessDescription": "SaaS platform",
|
|
78
|
-
"challenges": ["Scale"],
|
|
79
|
-
"constraints": ["Budget limited"],
|
|
80
|
-
"successMetrics": [{"name": "Revenue", "value": "10M", "unit": "USD"}]
|
|
81
|
-
}
|
|
82
|
-
\`\`\``;
|
|
83
|
-
const result = extractBusinessContext(response);
|
|
84
|
-
expect(result.constraints).toEqual(['Budget limited']);
|
|
85
|
-
expect(result.successMetrics).toHaveLength(1);
|
|
86
|
-
});
|
|
87
|
-
});
|
|
88
|
-
describe('extractWorkflow', () => {
|
|
89
|
-
it('extracts workflow map with activities and edges', () => {
|
|
90
|
-
const response = `\`\`\`json
|
|
91
|
-
{
|
|
92
|
-
"activities": [
|
|
93
|
-
{"id": "s1", "name": "Receive Order"},
|
|
94
|
-
{"id": "s2", "name": "Process Order"}
|
|
95
|
-
],
|
|
96
|
-
"edges": [
|
|
97
|
-
{"fromStepId": "s1", "toStepId": "s2"}
|
|
98
|
-
]
|
|
99
|
-
}
|
|
100
|
-
\`\`\``;
|
|
101
|
-
const result = extractWorkflow(response);
|
|
102
|
-
expect(result).not.toBeNull();
|
|
103
|
-
expect(result.activities).toHaveLength(2);
|
|
104
|
-
expect(result.edges).toHaveLength(1);
|
|
105
|
-
});
|
|
106
|
-
it('returns null for invalid workflow', () => {
|
|
107
|
-
const response = '```json\n{"name": "not a workflow"}\n```';
|
|
108
|
-
const result = extractWorkflow(response);
|
|
109
|
-
expect(result).toBeNull();
|
|
110
|
-
});
|
|
111
|
-
});
|
|
112
|
-
describe('extractIdeas', () => {
|
|
113
|
-
it('extracts array of idea cards', () => {
|
|
114
|
-
const response = `\`\`\`json
|
|
115
|
-
[
|
|
116
|
-
{
|
|
117
|
-
"id": "idea-1",
|
|
118
|
-
"title": "AI Routing",
|
|
119
|
-
"description": "Use AI to optimize delivery routes",
|
|
120
|
-
"workflowStepIds": ["s1", "s2"]
|
|
121
|
-
}
|
|
122
|
-
]
|
|
123
|
-
\`\`\``;
|
|
124
|
-
const result = extractIdeas(response);
|
|
125
|
-
expect(result).toHaveLength(1);
|
|
126
|
-
expect(result[0].title).toBe('AI Routing');
|
|
127
|
-
});
|
|
128
|
-
it('extracts ideas from object wrapper', () => {
|
|
129
|
-
const response = `\`\`\`json
|
|
130
|
-
{
|
|
131
|
-
"ideas": [
|
|
132
|
-
{
|
|
133
|
-
"id": "idea-1",
|
|
134
|
-
"title": "AI Routing",
|
|
135
|
-
"description": "Optimize routes",
|
|
136
|
-
"workflowStepIds": ["s1"]
|
|
137
|
-
}
|
|
138
|
-
]
|
|
139
|
-
}
|
|
140
|
-
\`\`\``;
|
|
141
|
-
const result = extractIdeas(response);
|
|
142
|
-
expect(result).toHaveLength(1);
|
|
143
|
-
});
|
|
144
|
-
it('returns null when no ideas found', () => {
|
|
145
|
-
const result = extractIdeas('No ideas in this response');
|
|
146
|
-
expect(result).toBeNull();
|
|
147
|
-
});
|
|
148
|
-
});
|
|
149
|
-
describe('extractEvaluation', () => {
|
|
150
|
-
it('extracts evaluation with method', () => {
|
|
151
|
-
const response = `\`\`\`json
|
|
152
|
-
{
|
|
153
|
-
"method": "feasibility-value-matrix",
|
|
154
|
-
"ideas": [
|
|
155
|
-
{
|
|
156
|
-
"ideaId": "idea-1",
|
|
157
|
-
"feasibility": 4,
|
|
158
|
-
"value": 5,
|
|
159
|
-
"risks": ["Data quality"]
|
|
160
|
-
}
|
|
161
|
-
]
|
|
162
|
-
}
|
|
163
|
-
\`\`\``;
|
|
164
|
-
const result = extractEvaluation(response);
|
|
165
|
-
expect(result).not.toBeNull();
|
|
166
|
-
expect(result.method).toBe('feasibility-value-matrix');
|
|
167
|
-
expect(result.ideas).toHaveLength(1);
|
|
168
|
-
});
|
|
169
|
-
it('returns null for non-evaluation JSON', () => {
|
|
170
|
-
const result = extractEvaluation('```json\n{"foo": "bar"}\n```');
|
|
171
|
-
expect(result).toBeNull();
|
|
172
|
-
});
|
|
173
|
-
});
|
|
174
|
-
describe('extractSelection', () => {
|
|
175
|
-
it('extracts selected idea', () => {
|
|
176
|
-
const response = `\`\`\`json
|
|
177
|
-
{
|
|
178
|
-
"ideaId": "idea-1",
|
|
179
|
-
"selectionRationale": "Highest combined score",
|
|
180
|
-
"confirmedByUser": false
|
|
181
|
-
}
|
|
182
|
-
\`\`\``;
|
|
183
|
-
const result = extractSelection(response);
|
|
184
|
-
expect(result).not.toBeNull();
|
|
185
|
-
expect(result.ideaId).toBe('idea-1');
|
|
186
|
-
expect(result.confirmedByUser).toBe(false);
|
|
187
|
-
});
|
|
188
|
-
});
|
|
189
|
-
describe('extractPlan', () => {
|
|
190
|
-
it('extracts implementation plan with milestones', () => {
|
|
191
|
-
const response = `\`\`\`json
|
|
192
|
-
{
|
|
193
|
-
"milestones": [
|
|
194
|
-
{
|
|
195
|
-
"id": "m1",
|
|
196
|
-
"title": "Setup",
|
|
197
|
-
"items": ["Initialize project", "Configure CI"]
|
|
198
|
-
},
|
|
199
|
-
{
|
|
200
|
-
"id": "m2",
|
|
201
|
-
"title": "Core Features",
|
|
202
|
-
"items": ["Implement routing", "Add monitoring"]
|
|
203
|
-
}
|
|
204
|
-
],
|
|
205
|
-
"architectureNotes": "Microservices architecture",
|
|
206
|
-
"dependencies": ["Azure Maps API"]
|
|
207
|
-
}
|
|
208
|
-
\`\`\``;
|
|
209
|
-
const result = extractPlan(response);
|
|
210
|
-
expect(result).not.toBeNull();
|
|
211
|
-
expect(result.milestones).toHaveLength(2);
|
|
212
|
-
expect(result.architectureNotes).toBe('Microservices architecture');
|
|
213
|
-
});
|
|
214
|
-
});
|
|
215
|
-
describe('extractPocState', () => {
|
|
216
|
-
it('extracts PoC development state', () => {
|
|
217
|
-
const response = `\`\`\`json
|
|
218
|
-
{
|
|
219
|
-
"repoSource": "local",
|
|
220
|
-
"iterations": [
|
|
221
|
-
{
|
|
222
|
-
"iteration": 1,
|
|
223
|
-
"startedAt": "2025-01-01T00:00:00Z",
|
|
224
|
-
"outcome": "scaffold",
|
|
225
|
-
"filesChanged": ["package.json", "src/index.ts"],
|
|
226
|
-
"changesSummary": "Initial scaffold"
|
|
227
|
-
}
|
|
228
|
-
]
|
|
229
|
-
}
|
|
230
|
-
\`\`\``;
|
|
231
|
-
const result = extractPocState(response);
|
|
232
|
-
expect(result).not.toBeNull();
|
|
233
|
-
expect(result.iterations).toHaveLength(1);
|
|
234
|
-
});
|
|
235
|
-
it('returns null when no PoC data', () => {
|
|
236
|
-
const result = extractPocState('Just text here');
|
|
237
|
-
expect(result).toBeNull();
|
|
238
|
-
});
|
|
239
|
-
});
|
|
240
|
-
// ── T062: extractSessionName ─────────────────────────────────────────────────
|
|
241
|
-
describe('extractSessionName', () => {
|
|
242
|
-
it('extracts sessionName from JSON block in response', () => {
|
|
243
|
-
const response = `Here's the summary:
|
|
244
|
-
|
|
245
|
-
\`\`\`json
|
|
246
|
-
{
|
|
247
|
-
"businessDescription": "A logistics company",
|
|
248
|
-
"challenges": ["Slow routing"],
|
|
249
|
-
"sessionName": "Logistics AI Routing"
|
|
250
|
-
}
|
|
251
|
-
\`\`\``;
|
|
252
|
-
const result = extractSessionName(response);
|
|
253
|
-
expect(result).toBe('Logistics AI Routing');
|
|
254
|
-
});
|
|
255
|
-
it('returns null when sessionName is missing from JSON block', () => {
|
|
256
|
-
const response = `\`\`\`json
|
|
257
|
-
{
|
|
258
|
-
"businessDescription": "A company",
|
|
259
|
-
"challenges": ["Growth"]
|
|
260
|
-
}
|
|
261
|
-
\`\`\``;
|
|
262
|
-
const result = extractSessionName(response);
|
|
263
|
-
expect(result).toBeNull();
|
|
264
|
-
});
|
|
265
|
-
it('returns null when no JSON block is present', () => {
|
|
266
|
-
const result = extractSessionName('Just a plain text response with no JSON.');
|
|
267
|
-
expect(result).toBeNull();
|
|
268
|
-
});
|
|
269
|
-
it('returns null when sessionName is not a string', () => {
|
|
270
|
-
const response = `\`\`\`json
|
|
271
|
-
{ "sessionName": 42 }
|
|
272
|
-
\`\`\``;
|
|
273
|
-
const result = extractSessionName(response);
|
|
274
|
-
expect(result).toBeNull();
|
|
275
|
-
});
|
|
276
|
-
it('trims whitespace from sessionName', () => {
|
|
277
|
-
const response = `\`\`\`json
|
|
278
|
-
{ "sessionName": " Retail AI Onboarding " }
|
|
279
|
-
\`\`\``;
|
|
280
|
-
const result = extractSessionName(response);
|
|
281
|
-
expect(result).toBe('Retail AI Onboarding');
|
|
282
|
-
});
|
|
283
|
-
it('returns null for empty string sessionName', () => {
|
|
284
|
-
const response = `\`\`\`json
|
|
285
|
-
{ "sessionName": "" }
|
|
286
|
-
\`\`\``;
|
|
287
|
-
const result = extractSessionName(response);
|
|
288
|
-
expect(result).toBeNull();
|
|
289
|
-
});
|
|
290
|
-
});
|
|
291
|
-
// ── Multi-block extraction tests (T006, T007) ──────────────────────────────
|
|
292
|
-
describe('extractAllJsonBlocks', () => {
|
|
293
|
-
it('returns empty array when no JSON blocks found', () => {
|
|
294
|
-
const response = 'This is a plain text response with no JSON.';
|
|
295
|
-
expect(extractAllJsonBlocks(response)).toEqual([]);
|
|
296
|
-
});
|
|
297
|
-
it('extracts a single fenced JSON block', () => {
|
|
298
|
-
const response = `Here are the results:\n\`\`\`json\n{"key": "value"}\n\`\`\`\nDone.`;
|
|
299
|
-
const results = extractAllJsonBlocks(response);
|
|
300
|
-
expect(results).toEqual([{ key: 'value' }]);
|
|
301
|
-
});
|
|
302
|
-
it('extracts two fenced JSON blocks', () => {
|
|
303
|
-
const response = `First:\n\`\`\`json\n{"a": 1}\n\`\`\`\nSecond:\n\`\`\`json\n{"b": 2}\n\`\`\``;
|
|
304
|
-
const results = extractAllJsonBlocks(response);
|
|
305
|
-
expect(results).toHaveLength(2);
|
|
306
|
-
expect(results[0]).toEqual({ a: 1 });
|
|
307
|
-
expect(results[1]).toEqual({ b: 2 });
|
|
308
|
-
});
|
|
309
|
-
it('extracts three fenced JSON blocks', () => {
|
|
310
|
-
const response = `\`\`\`json\n[1]\n\`\`\`\n\`\`\`json\n[2]\n\`\`\`\n\`\`\`json\n[3]\n\`\`\``;
|
|
311
|
-
const results = extractAllJsonBlocks(response);
|
|
312
|
-
expect(results).toHaveLength(3);
|
|
313
|
-
});
|
|
314
|
-
it('falls back to bracket-depth counting when no fenced blocks', () => {
|
|
315
|
-
const response = 'Result: {"key": "value"} and also [1, 2, 3]';
|
|
316
|
-
const results = extractAllJsonBlocks(response);
|
|
317
|
-
expect(results).toHaveLength(2);
|
|
318
|
-
expect(results[0]).toEqual({ key: 'value' });
|
|
319
|
-
expect(results[1]).toEqual([1, 2, 3]);
|
|
320
|
-
});
|
|
321
|
-
it('skips invalid JSON in fenced blocks', () => {
|
|
322
|
-
const response = `\`\`\`json\n{invalid}\n\`\`\`\n\`\`\`json\n{"valid": true}\n\`\`\``;
|
|
323
|
-
const results = extractAllJsonBlocks(response);
|
|
324
|
-
expect(results).toEqual([{ valid: true }]);
|
|
325
|
-
});
|
|
326
|
-
});
|
|
327
|
-
describe('extractJsonBlockForSchema', () => {
|
|
328
|
-
const simpleSchema = z.object({
|
|
329
|
-
name: z.string(),
|
|
330
|
-
age: z.number(),
|
|
331
|
-
});
|
|
332
|
-
it('returns null when no blocks match schema', () => {
|
|
333
|
-
const response = `\`\`\`json\n{"unrelated": true}\n\`\`\``;
|
|
334
|
-
expect(extractJsonBlockForSchema(response, simpleSchema)).toBeNull();
|
|
335
|
-
});
|
|
336
|
-
it('returns the first matching block when multiple blocks present', () => {
|
|
337
|
-
const response = `\`\`\`json\n{"unrelated": true}\n\`\`\`\n\`\`\`json\n{"name": "Alice", "age": 30}\n\`\`\``;
|
|
338
|
-
const result = extractJsonBlockForSchema(response, simpleSchema);
|
|
339
|
-
expect(result).toEqual({ name: 'Alice', age: 30 });
|
|
340
|
-
});
|
|
341
|
-
it('skips first block that does not match and returns second that does', () => {
|
|
342
|
-
const response = `\`\`\`json\n{"name": 123}\n\`\`\`\n\`\`\`json\n{"name": "Bob", "age": 25}\n\`\`\``;
|
|
343
|
-
const result = extractJsonBlockForSchema(response, simpleSchema);
|
|
344
|
-
expect(result).toEqual({ name: 'Bob', age: 25 });
|
|
345
|
-
});
|
|
346
|
-
it('works with array schemas', () => {
|
|
347
|
-
const arraySchema = z.array(z.object({ id: z.string() }));
|
|
348
|
-
const response = `\`\`\`json\n{"not": "array"}\n\`\`\`\n\`\`\`json\n[{"id": "a"}]\n\`\`\``;
|
|
349
|
-
const result = extractJsonBlockForSchema(response, arraySchema);
|
|
350
|
-
expect(result).toEqual([{ id: 'a' }]);
|
|
351
|
-
});
|
|
352
|
-
});
|