outcome-cli 1.0.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 +261 -0
- package/package.json +95 -0
- package/src/agents/README.md +139 -0
- package/src/agents/adapters/anthropic.adapter.ts +166 -0
- package/src/agents/adapters/dalle.adapter.ts +145 -0
- package/src/agents/adapters/gemini.adapter.ts +134 -0
- package/src/agents/adapters/imagen.adapter.ts +106 -0
- package/src/agents/adapters/nano-banana.adapter.ts +129 -0
- package/src/agents/adapters/openai.adapter.ts +165 -0
- package/src/agents/adapters/veo.adapter.ts +130 -0
- package/src/agents/agent.schema.property.test.ts +379 -0
- package/src/agents/agent.schema.test.ts +148 -0
- package/src/agents/agent.schema.ts +263 -0
- package/src/agents/index.ts +60 -0
- package/src/agents/registered-agent.schema.ts +356 -0
- package/src/agents/registry.ts +97 -0
- package/src/agents/tournament-configs.property.test.ts +266 -0
- package/src/cli/README.md +145 -0
- package/src/cli/commands/define.ts +79 -0
- package/src/cli/commands/list.ts +46 -0
- package/src/cli/commands/logs.ts +83 -0
- package/src/cli/commands/run.ts +416 -0
- package/src/cli/commands/verify.ts +110 -0
- package/src/cli/index.ts +81 -0
- package/src/config/README.md +128 -0
- package/src/config/env.ts +262 -0
- package/src/config/index.ts +19 -0
- package/src/eval/README.md +318 -0
- package/src/eval/ai-judge.test.ts +435 -0
- package/src/eval/ai-judge.ts +368 -0
- package/src/eval/code-validators.ts +414 -0
- package/src/eval/evaluateOutcome.property.test.ts +1174 -0
- package/src/eval/evaluateOutcome.ts +591 -0
- package/src/eval/immigration-validators.ts +122 -0
- package/src/eval/index.ts +90 -0
- package/src/eval/judge-cache.ts +402 -0
- package/src/eval/tournament-validators.property.test.ts +439 -0
- package/src/eval/validators.property.test.ts +1118 -0
- package/src/eval/validators.ts +1199 -0
- package/src/eval/weighted-scorer.ts +285 -0
- package/src/index.ts +17 -0
- package/src/league/README.md +188 -0
- package/src/league/health-check.ts +353 -0
- package/src/league/index.ts +93 -0
- package/src/league/killAgent.ts +151 -0
- package/src/league/league.test.ts +1151 -0
- package/src/league/runLeague.ts +843 -0
- package/src/league/scoreAgent.ts +175 -0
- package/src/modules/omnibridge/__tests__/.gitkeep +1 -0
- package/src/modules/omnibridge/__tests__/auth-tunnel.property.test.ts +524 -0
- package/src/modules/omnibridge/__tests__/deterministic-logger.property.test.ts +965 -0
- package/src/modules/omnibridge/__tests__/ghost-api.property.test.ts +461 -0
- package/src/modules/omnibridge/__tests__/omnibridge-integration.test.ts +542 -0
- package/src/modules/omnibridge/__tests__/parallel-executor.property.test.ts +671 -0
- package/src/modules/omnibridge/__tests__/semantic-normalizer.property.test.ts +521 -0
- package/src/modules/omnibridge/__tests__/semantic-normalizer.test.ts +254 -0
- package/src/modules/omnibridge/__tests__/session-vault.property.test.ts +367 -0
- package/src/modules/omnibridge/__tests__/shadow-session.property.test.ts +523 -0
- package/src/modules/omnibridge/__tests__/triangulation-engine.property.test.ts +292 -0
- package/src/modules/omnibridge/__tests__/verification-engine.property.test.ts +769 -0
- package/src/modules/omnibridge/api/.gitkeep +1 -0
- package/src/modules/omnibridge/api/ghost-api.ts +1087 -0
- package/src/modules/omnibridge/auth/.gitkeep +1 -0
- package/src/modules/omnibridge/auth/auth-tunnel.ts +843 -0
- package/src/modules/omnibridge/auth/session-vault.ts +577 -0
- package/src/modules/omnibridge/core/.gitkeep +1 -0
- package/src/modules/omnibridge/core/semantic-normalizer.ts +702 -0
- package/src/modules/omnibridge/core/triangulation-engine.ts +530 -0
- package/src/modules/omnibridge/core/types.ts +610 -0
- package/src/modules/omnibridge/execution/.gitkeep +1 -0
- package/src/modules/omnibridge/execution/deterministic-logger.ts +629 -0
- package/src/modules/omnibridge/execution/parallel-executor.ts +542 -0
- package/src/modules/omnibridge/execution/shadow-session.ts +794 -0
- package/src/modules/omnibridge/index.ts +212 -0
- package/src/modules/omnibridge/omnibridge.ts +510 -0
- package/src/modules/omnibridge/verification/.gitkeep +1 -0
- package/src/modules/omnibridge/verification/verification-engine.ts +783 -0
- package/src/outcomes/README.md +75 -0
- package/src/outcomes/acquire-pilot-customer.ts +297 -0
- package/src/outcomes/code-delivery-outcomes.ts +89 -0
- package/src/outcomes/code-outcomes.ts +256 -0
- package/src/outcomes/code_review_battle.test.ts +135 -0
- package/src/outcomes/code_review_battle.ts +135 -0
- package/src/outcomes/cold_email_battle.ts +97 -0
- package/src/outcomes/content_creation_battle.ts +160 -0
- package/src/outcomes/f1_stem_opt_compliance.ts +61 -0
- package/src/outcomes/index.ts +107 -0
- package/src/outcomes/lead_gen_battle.test.ts +113 -0
- package/src/outcomes/lead_gen_battle.ts +99 -0
- package/src/outcomes/outcome.schema.property.test.ts +229 -0
- package/src/outcomes/outcome.schema.ts +187 -0
- package/src/outcomes/qualified_sales_interest.ts +118 -0
- package/src/outcomes/swarm_planner.property.test.ts +370 -0
- package/src/outcomes/swarm_planner.ts +96 -0
- package/src/outcomes/web_extraction.ts +234 -0
- package/src/runtime/README.md +220 -0
- package/src/runtime/agentRunner.test.ts +341 -0
- package/src/runtime/agentRunner.ts +746 -0
- package/src/runtime/claudeAdapter.ts +232 -0
- package/src/runtime/costTracker.ts +123 -0
- package/src/runtime/index.ts +34 -0
- package/src/runtime/modelAdapter.property.test.ts +305 -0
- package/src/runtime/modelAdapter.ts +144 -0
- package/src/runtime/openaiAdapter.ts +235 -0
- package/src/utils/README.md +122 -0
- package/src/utils/command-runner.ts +134 -0
- package/src/utils/cost-guard.ts +379 -0
- package/src/utils/errors.test.ts +290 -0
- package/src/utils/errors.ts +442 -0
- package/src/utils/index.ts +37 -0
- package/src/utils/logger.test.ts +361 -0
- package/src/utils/logger.ts +419 -0
- package/src/utils/output-parsers.ts +216 -0
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Property-Based Tests for Ghost-API
|
|
3
|
+
*
|
|
4
|
+
* These tests validate the correctness properties defined in the design document.
|
|
5
|
+
* Each property test runs minimum 100 iterations with randomly generated inputs.
|
|
6
|
+
*
|
|
7
|
+
* **Feature: omnibridge, Property 8: Ghost-API Response Conformance**
|
|
8
|
+
* **Validates: Requirements 3.4, 3.6**
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, test, expect, beforeEach } from 'vitest';
|
|
12
|
+
import * as fc from 'fast-check';
|
|
13
|
+
import {
|
|
14
|
+
GhostApi,
|
|
15
|
+
createGhostApi,
|
|
16
|
+
inferSchemaFromDocument,
|
|
17
|
+
generateEndpointPath,
|
|
18
|
+
estimateActions,
|
|
19
|
+
} from '../api/ghost-api.js';
|
|
20
|
+
import { createSemanticNormalizer } from '../core/semantic-normalizer.js';
|
|
21
|
+
import { createTriangulationEngine } from '../core/triangulation-engine.js';
|
|
22
|
+
import { createDeterministicLogger } from '../execution/deterministic-logger.js';
|
|
23
|
+
import type { GoalDefinition } from '../core/types.js';
|
|
24
|
+
|
|
25
|
+
// =============================================================================
|
|
26
|
+
// Test Setup
|
|
27
|
+
// =============================================================================
|
|
28
|
+
|
|
29
|
+
const normalizer = createSemanticNormalizer();
|
|
30
|
+
const triangulationEngine = createTriangulationEngine();
|
|
31
|
+
const logger = createDeterministicLogger();
|
|
32
|
+
|
|
33
|
+
let ghostApi: GhostApi;
|
|
34
|
+
|
|
35
|
+
beforeEach(() => {
|
|
36
|
+
logger.clearAll();
|
|
37
|
+
ghostApi = createGhostApi({
|
|
38
|
+
normalizer,
|
|
39
|
+
triangulationEngine,
|
|
40
|
+
logger,
|
|
41
|
+
basePath: '/omni-bridge',
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// =============================================================================
|
|
46
|
+
// Arbitraries (Test Data Generators)
|
|
47
|
+
// =============================================================================
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Generate valid goal definitions
|
|
51
|
+
*/
|
|
52
|
+
const goalDefinitionArb: fc.Arbitrary<GoalDefinition> = fc.record({
|
|
53
|
+
name: fc.stringMatching(/^[A-Z][a-zA-Z0-9_]{2,30}$/),
|
|
54
|
+
targetUrl: fc.webUrl(),
|
|
55
|
+
description: fc.lorem({ minCount: 3, maxCount: 20 }),
|
|
56
|
+
}).map(({ name, targetUrl, description }) => ({
|
|
57
|
+
name,
|
|
58
|
+
targetUrl,
|
|
59
|
+
description,
|
|
60
|
+
}));
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Generate simple HTML documents for testing
|
|
64
|
+
*/
|
|
65
|
+
const simpleHtmlArb = fc.record({
|
|
66
|
+
title: fc.lorem({ maxCount: 5 }),
|
|
67
|
+
buttonText: fc.lorem({ maxCount: 3 }),
|
|
68
|
+
inputLabel: fc.lorem({ maxCount: 3 }),
|
|
69
|
+
paragraphText: fc.lorem({ minCount: 5, maxCount: 30 }),
|
|
70
|
+
}).map(({ title, buttonText, inputLabel, paragraphText }) => `
|
|
71
|
+
<!DOCTYPE html>
|
|
72
|
+
<html>
|
|
73
|
+
<head>
|
|
74
|
+
<title>${title}</title>
|
|
75
|
+
</head>
|
|
76
|
+
<body>
|
|
77
|
+
<header>
|
|
78
|
+
<nav>
|
|
79
|
+
<a href="/">Home</a>
|
|
80
|
+
<a href="/about">About</a>
|
|
81
|
+
</nav>
|
|
82
|
+
</header>
|
|
83
|
+
<main>
|
|
84
|
+
<h1>${title}</h1>
|
|
85
|
+
<p>${paragraphText}</p>
|
|
86
|
+
<form action="/submit" method="POST">
|
|
87
|
+
<label for="input1">${inputLabel}</label>
|
|
88
|
+
<input type="text" id="input1" name="input1" required>
|
|
89
|
+
<button type="submit">${buttonText}</button>
|
|
90
|
+
</form>
|
|
91
|
+
</main>
|
|
92
|
+
<footer>
|
|
93
|
+
<p>Footer content</p>
|
|
94
|
+
</footer>
|
|
95
|
+
</body>
|
|
96
|
+
</html>
|
|
97
|
+
`);
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Generate HTML with forms for testing
|
|
101
|
+
*/
|
|
102
|
+
const htmlWithFormArb = fc.record({
|
|
103
|
+
formAction: fc.constantFrom('/login', '/signup', '/submit', '/search'),
|
|
104
|
+
formMethod: fc.constantFrom('GET', 'POST'),
|
|
105
|
+
fields: fc.array(
|
|
106
|
+
fc.record({
|
|
107
|
+
name: fc.stringMatching(/^[a-z][a-zA-Z0-9_]{2,15}$/),
|
|
108
|
+
type: fc.constantFrom('text', 'email', 'password', 'number', 'tel'),
|
|
109
|
+
label: fc.lorem({ maxCount: 3 }),
|
|
110
|
+
required: fc.boolean(),
|
|
111
|
+
}),
|
|
112
|
+
{ minLength: 1, maxLength: 5 }
|
|
113
|
+
),
|
|
114
|
+
submitText: fc.lorem({ maxCount: 2 }),
|
|
115
|
+
}).map(({ formAction, formMethod, fields, submitText }) => {
|
|
116
|
+
const fieldHtml = fields
|
|
117
|
+
.map(
|
|
118
|
+
(f) =>
|
|
119
|
+
`<label for="${f.name}">${f.label}</label>
|
|
120
|
+
<input type="${f.type}" id="${f.name}" name="${f.name}" ${f.required ? 'required' : ''}>`
|
|
121
|
+
)
|
|
122
|
+
.join('\n ');
|
|
123
|
+
|
|
124
|
+
return `
|
|
125
|
+
<!DOCTYPE html>
|
|
126
|
+
<html>
|
|
127
|
+
<head><title>Form Page</title></head>
|
|
128
|
+
<body>
|
|
129
|
+
<main>
|
|
130
|
+
<form action="${formAction}" method="${formMethod}">
|
|
131
|
+
${fieldHtml}
|
|
132
|
+
<button type="submit">${submitText}</button>
|
|
133
|
+
</form>
|
|
134
|
+
</main>
|
|
135
|
+
</body>
|
|
136
|
+
</html>
|
|
137
|
+
`;
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// =============================================================================
|
|
141
|
+
// Property Tests
|
|
142
|
+
// =============================================================================
|
|
143
|
+
|
|
144
|
+
describe('Ghost-API Property Tests', () => {
|
|
145
|
+
/**
|
|
146
|
+
* **Feature: omnibridge, Property 8: Ghost-API Response Conformance**
|
|
147
|
+
*
|
|
148
|
+
* *For any* Ghost-API response, the response SHALL include:
|
|
149
|
+
* (1) data conforming to the goal's schema,
|
|
150
|
+
* (2) metadata with confidence, executionTimeMs, and actionsPerformed,
|
|
151
|
+
* (3) a verificationHash.
|
|
152
|
+
*
|
|
153
|
+
* **Validates: Requirements 3.4, 3.6**
|
|
154
|
+
*/
|
|
155
|
+
test('Property 8: Ghost-API Response Conformance', async () => {
|
|
156
|
+
await fc.assert(
|
|
157
|
+
fc.asyncProperty(goalDefinitionArb, simpleHtmlArb, async (goal, html) => {
|
|
158
|
+
// Create fresh instance for each test
|
|
159
|
+
const testLogger = createDeterministicLogger();
|
|
160
|
+
const testApi = createGhostApi({
|
|
161
|
+
normalizer,
|
|
162
|
+
triangulationEngine,
|
|
163
|
+
logger: testLogger,
|
|
164
|
+
basePath: '/omni-bridge',
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// Define the goal and get schema mapping
|
|
168
|
+
const mapping = await testApi.defineGoal(goal, html);
|
|
169
|
+
|
|
170
|
+
// Execute the goal
|
|
171
|
+
const response = await testApi.execute(mapping.endpoint, {}, html);
|
|
172
|
+
|
|
173
|
+
// Verify response structure (Requirement 3.4, 3.6)
|
|
174
|
+
// 1. Response must have data property
|
|
175
|
+
expect(response).toHaveProperty('data');
|
|
176
|
+
expect(response.data).toBeDefined();
|
|
177
|
+
|
|
178
|
+
// 2. Response must have metadata with required fields
|
|
179
|
+
expect(response).toHaveProperty('metadata');
|
|
180
|
+
expect(response.metadata).toHaveProperty('confidence');
|
|
181
|
+
expect(response.metadata).toHaveProperty('executionTimeMs');
|
|
182
|
+
expect(response.metadata).toHaveProperty('actionsPerformed');
|
|
183
|
+
expect(response.metadata).toHaveProperty('triangulationHeals');
|
|
184
|
+
|
|
185
|
+
// 3. Metadata values must be valid
|
|
186
|
+
expect(typeof response.metadata.confidence).toBe('number');
|
|
187
|
+
expect(response.metadata.confidence).toBeGreaterThanOrEqual(0);
|
|
188
|
+
expect(response.metadata.confidence).toBeLessThanOrEqual(1);
|
|
189
|
+
|
|
190
|
+
expect(typeof response.metadata.executionTimeMs).toBe('number');
|
|
191
|
+
expect(response.metadata.executionTimeMs).toBeGreaterThanOrEqual(0);
|
|
192
|
+
|
|
193
|
+
expect(typeof response.metadata.actionsPerformed).toBe('number');
|
|
194
|
+
expect(response.metadata.actionsPerformed).toBeGreaterThanOrEqual(0);
|
|
195
|
+
|
|
196
|
+
expect(typeof response.metadata.triangulationHeals).toBe('number');
|
|
197
|
+
expect(response.metadata.triangulationHeals).toBeGreaterThanOrEqual(0);
|
|
198
|
+
|
|
199
|
+
// 4. Response must have verificationHash
|
|
200
|
+
expect(response).toHaveProperty('verificationHash');
|
|
201
|
+
expect(typeof response.verificationHash).toBe('string');
|
|
202
|
+
expect(response.verificationHash.length).toBeGreaterThan(0);
|
|
203
|
+
|
|
204
|
+
return true;
|
|
205
|
+
}),
|
|
206
|
+
{ numRuns: 100 }
|
|
207
|
+
);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Additional property: Schema mapping generates valid endpoints
|
|
212
|
+
*/
|
|
213
|
+
test('Schema mapping generates valid endpoint paths', () => {
|
|
214
|
+
fc.assert(
|
|
215
|
+
fc.property(goalDefinitionArb, (goal) => {
|
|
216
|
+
const endpoint = generateEndpointPath(goal, '/omni-bridge');
|
|
217
|
+
|
|
218
|
+
// Endpoint must start with base path
|
|
219
|
+
expect(endpoint.startsWith('/omni-bridge/')).toBe(true);
|
|
220
|
+
|
|
221
|
+
// Endpoint must be a valid URL path (no special characters except - and /)
|
|
222
|
+
expect(endpoint).toMatch(/^\/[a-z0-9\-\/]+$/);
|
|
223
|
+
|
|
224
|
+
// Endpoint must have at least 2 segments (base/domain/goal)
|
|
225
|
+
const segments = endpoint.split('/').filter(Boolean);
|
|
226
|
+
expect(segments.length).toBeGreaterThanOrEqual(2);
|
|
227
|
+
|
|
228
|
+
return true;
|
|
229
|
+
}),
|
|
230
|
+
{ numRuns: 100 }
|
|
231
|
+
);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Additional property: Schema inference produces valid JSON schema
|
|
236
|
+
*/
|
|
237
|
+
test('Schema inference produces valid JSON schema', () => {
|
|
238
|
+
fc.assert(
|
|
239
|
+
fc.property(htmlWithFormArb, goalDefinitionArb, (html, goal) => {
|
|
240
|
+
const document = normalizer.normalize(html, goal.targetUrl);
|
|
241
|
+
const schema = inferSchemaFromDocument(document, goal);
|
|
242
|
+
|
|
243
|
+
// Schema must have type property
|
|
244
|
+
expect(schema).toHaveProperty('type');
|
|
245
|
+
expect(schema.type).toBe('object');
|
|
246
|
+
|
|
247
|
+
// Schema must have properties object
|
|
248
|
+
expect(schema).toHaveProperty('properties');
|
|
249
|
+
expect(typeof schema.properties).toBe('object');
|
|
250
|
+
|
|
251
|
+
return true;
|
|
252
|
+
}),
|
|
253
|
+
{ numRuns: 100 }
|
|
254
|
+
);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Additional property: Action estimation is reasonable
|
|
259
|
+
*/
|
|
260
|
+
test('Action estimation is reasonable', () => {
|
|
261
|
+
fc.assert(
|
|
262
|
+
fc.property(htmlWithFormArb, goalDefinitionArb, (html, goal) => {
|
|
263
|
+
const document = normalizer.normalize(html, goal.targetUrl);
|
|
264
|
+
const estimate = estimateActions(document, goal);
|
|
265
|
+
|
|
266
|
+
// Estimate must be positive
|
|
267
|
+
expect(estimate).toBeGreaterThan(0);
|
|
268
|
+
|
|
269
|
+
// Estimate must be capped at 100
|
|
270
|
+
expect(estimate).toBeLessThanOrEqual(100);
|
|
271
|
+
|
|
272
|
+
// Estimate should be at least 1 (navigation)
|
|
273
|
+
expect(estimate).toBeGreaterThanOrEqual(1);
|
|
274
|
+
|
|
275
|
+
return true;
|
|
276
|
+
}),
|
|
277
|
+
{ numRuns: 100 }
|
|
278
|
+
);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Additional property: Response data type consistency
|
|
283
|
+
*/
|
|
284
|
+
test('Response data is object or array of objects', async () => {
|
|
285
|
+
await fc.assert(
|
|
286
|
+
fc.asyncProperty(goalDefinitionArb, simpleHtmlArb, async (goal, html) => {
|
|
287
|
+
// Create fresh instance for each test
|
|
288
|
+
const testLogger = createDeterministicLogger();
|
|
289
|
+
const testApi = createGhostApi({
|
|
290
|
+
normalizer,
|
|
291
|
+
triangulationEngine,
|
|
292
|
+
logger: testLogger,
|
|
293
|
+
basePath: '/omni-bridge',
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
const mapping = await testApi.defineGoal(goal, html);
|
|
297
|
+
const response = await testApi.execute(mapping.endpoint, {}, html);
|
|
298
|
+
|
|
299
|
+
// Data must be an object or array
|
|
300
|
+
const dataType = typeof response.data;
|
|
301
|
+
expect(dataType).toBe('object');
|
|
302
|
+
|
|
303
|
+
// If array, all items must be objects
|
|
304
|
+
if (Array.isArray(response.data)) {
|
|
305
|
+
for (const item of response.data) {
|
|
306
|
+
expect(typeof item).toBe('object');
|
|
307
|
+
expect(item).not.toBeNull();
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return true;
|
|
312
|
+
}),
|
|
313
|
+
{ numRuns: 100 }
|
|
314
|
+
);
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Additional property: Verification hash is non-empty hex string
|
|
319
|
+
*/
|
|
320
|
+
test('Verification hash is non-empty hex string', async () => {
|
|
321
|
+
await fc.assert(
|
|
322
|
+
fc.asyncProperty(goalDefinitionArb, simpleHtmlArb, async (goal, html) => {
|
|
323
|
+
// Create fresh instance for each test
|
|
324
|
+
const testLogger = createDeterministicLogger();
|
|
325
|
+
const testApi = createGhostApi({
|
|
326
|
+
normalizer,
|
|
327
|
+
triangulationEngine,
|
|
328
|
+
logger: testLogger,
|
|
329
|
+
basePath: '/omni-bridge',
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
const mapping = await testApi.defineGoal(goal, html);
|
|
333
|
+
const response = await testApi.execute(mapping.endpoint, {}, html);
|
|
334
|
+
|
|
335
|
+
// Hash must be a non-empty hex string
|
|
336
|
+
expect(response.verificationHash).toMatch(/^[a-f0-9]+$/);
|
|
337
|
+
expect(response.verificationHash.length).toBeGreaterThanOrEqual(32);
|
|
338
|
+
|
|
339
|
+
return true;
|
|
340
|
+
}),
|
|
341
|
+
{ numRuns: 50 }
|
|
342
|
+
);
|
|
343
|
+
});
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
// =============================================================================
|
|
347
|
+
// Unit Tests for Edge Cases
|
|
348
|
+
// =============================================================================
|
|
349
|
+
|
|
350
|
+
describe('Ghost-API Unit Tests', () => {
|
|
351
|
+
test('defineGoal creates valid schema mapping', async () => {
|
|
352
|
+
const goal: GoalDefinition = {
|
|
353
|
+
name: 'Fetch_Invoice_Data',
|
|
354
|
+
targetUrl: 'https://example.com/invoices',
|
|
355
|
+
description: 'Fetch invoice data from the portal',
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
const html = `
|
|
359
|
+
<html>
|
|
360
|
+
<body>
|
|
361
|
+
<h1>Invoices</h1>
|
|
362
|
+
<table>
|
|
363
|
+
<tr><td>Invoice #1</td><td>$100</td></tr>
|
|
364
|
+
</table>
|
|
365
|
+
</body>
|
|
366
|
+
</html>
|
|
367
|
+
`;
|
|
368
|
+
|
|
369
|
+
const mapping = await ghostApi.defineGoal(goal, html);
|
|
370
|
+
|
|
371
|
+
expect(mapping.endpoint).toBe('/omni-bridge/example/fetch-invoice-data');
|
|
372
|
+
expect(mapping.schema).toBeDefined();
|
|
373
|
+
expect(mapping.requiredAuth).toBe(true); // 'invoice' triggers auth detection
|
|
374
|
+
expect(mapping.estimatedActions).toBeGreaterThan(0);
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
test('execute returns valid response structure', async () => {
|
|
378
|
+
const goal: GoalDefinition = {
|
|
379
|
+
name: 'Simple_Test',
|
|
380
|
+
targetUrl: 'https://test.com/page',
|
|
381
|
+
description: 'Simple test goal',
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
const html = '<html><body><p>Test content</p></body></html>';
|
|
385
|
+
|
|
386
|
+
const mapping = await ghostApi.defineGoal(goal, html);
|
|
387
|
+
const response = await ghostApi.execute(mapping.endpoint, {}, html);
|
|
388
|
+
|
|
389
|
+
expect(response.data).toBeDefined();
|
|
390
|
+
expect(response.metadata.confidence).toBeGreaterThanOrEqual(0);
|
|
391
|
+
expect(response.metadata.confidence).toBeLessThanOrEqual(1);
|
|
392
|
+
expect(response.metadata.executionTimeMs).toBeGreaterThanOrEqual(0);
|
|
393
|
+
expect(response.metadata.actionsPerformed).toBeGreaterThanOrEqual(0);
|
|
394
|
+
expect(response.verificationHash).toBeTruthy();
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
test('execute throws for unknown endpoint', async () => {
|
|
398
|
+
await expect(
|
|
399
|
+
ghostApi.execute('/unknown/endpoint', {})
|
|
400
|
+
).rejects.toThrow('Endpoint not found');
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
test('detectPagination identifies pagination elements', () => {
|
|
404
|
+
const html = `
|
|
405
|
+
<html>
|
|
406
|
+
<body>
|
|
407
|
+
<div class="results">
|
|
408
|
+
<p>Result 1</p>
|
|
409
|
+
<p>Result 2</p>
|
|
410
|
+
</div>
|
|
411
|
+
<nav>
|
|
412
|
+
<a href="/page/1">1</a>
|
|
413
|
+
<a href="/page/2">2</a>
|
|
414
|
+
<a href="/page/2">Next »</a>
|
|
415
|
+
</nav>
|
|
416
|
+
</body>
|
|
417
|
+
</html>
|
|
418
|
+
`;
|
|
419
|
+
|
|
420
|
+
const document = normalizer.normalize(html, 'https://example.com');
|
|
421
|
+
const paginationInfo = ghostApi.detectPagination(document);
|
|
422
|
+
|
|
423
|
+
expect(paginationInfo.hasPagination).toBe(true);
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
test('listEndpoints returns registered endpoints', async () => {
|
|
427
|
+
const goal1: GoalDefinition = {
|
|
428
|
+
name: 'Goal_One',
|
|
429
|
+
targetUrl: 'https://example.com/one',
|
|
430
|
+
description: 'First goal',
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
const goal2: GoalDefinition = {
|
|
434
|
+
name: 'Goal_Two',
|
|
435
|
+
targetUrl: 'https://example.com/two',
|
|
436
|
+
description: 'Second goal',
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
await ghostApi.defineGoal(goal1);
|
|
440
|
+
await ghostApi.defineGoal(goal2);
|
|
441
|
+
|
|
442
|
+
const endpoints = ghostApi.listEndpoints();
|
|
443
|
+
expect(endpoints.length).toBe(2);
|
|
444
|
+
expect(endpoints).toContain('/omni-bridge/example/goal-one');
|
|
445
|
+
expect(endpoints).toContain('/omni-bridge/example/goal-two');
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
test('clearGoals removes all registered goals', async () => {
|
|
449
|
+
const goal: GoalDefinition = {
|
|
450
|
+
name: 'Test_Goal',
|
|
451
|
+
targetUrl: 'https://example.com/test',
|
|
452
|
+
description: 'Test goal',
|
|
453
|
+
};
|
|
454
|
+
|
|
455
|
+
await ghostApi.defineGoal(goal);
|
|
456
|
+
expect(ghostApi.getGoalCount()).toBe(1);
|
|
457
|
+
|
|
458
|
+
ghostApi.clearGoals();
|
|
459
|
+
expect(ghostApi.getGoalCount()).toBe(0);
|
|
460
|
+
});
|
|
461
|
+
});
|