@workglow/test-contract 0.6.1
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 +232 -0
- package/dist/ai-provider/assertions/capabilityHonesty.d.ts +7 -0
- package/dist/ai-provider/assertions/dispose.d.ts +7 -0
- package/dist/ai-provider/assertions/inferAdvertisesRegistered.d.ts +15 -0
- package/dist/ai-provider/assertions/inferServesInferred.d.ts +20 -0
- package/dist/ai-provider/assertions/pricingMatchesModality.d.ts +36 -0
- package/dist/ai-provider/assertions/registryCoverage.d.ts +13 -0
- package/dist/ai-provider/assertions/sessionReuse.d.ts +7 -0
- package/dist/ai-provider/assertions/signalHonoring.d.ts +7 -0
- package/dist/ai-provider/assertions/structuredGeneration.d.ts +7 -0
- package/dist/ai-provider/assertions/textGenerationSmoke.d.ts +7 -0
- package/dist/ai-provider/assertions/toolCallAccumulator.d.ts +7 -0
- package/dist/ai-provider/assertions/toolCallMultiTurn.d.ts +7 -0
- package/dist/ai-provider/assertions/usageNormalization.d.ts +61 -0
- package/dist/ai-provider/fixtures.d.ts +8 -0
- package/dist/ai-provider/runAiProviderConformance.d.ts +7 -0
- package/dist/ai-provider/types.d.ts +75 -0
- package/dist/ai-provider.d.ts +25 -0
- package/dist/ai-provider.js +876 -0
- package/dist/ai-provider.js.map +26 -0
- package/dist/browser-context/ConformanceMockContext.d.ts +54 -0
- package/dist/browser-context/assertions/ariaRoundTrip.d.ts +7 -0
- package/dist/browser-context/assertions/capabilityHonesty.d.ts +7 -0
- package/dist/browser-context/assertions/itExpectFail.d.ts +8 -0
- package/dist/browser-context/assertions/networkIntrospection.d.ts +7 -0
- package/dist/browser-context/assertions/tabsLifecycle.d.ts +7 -0
- package/dist/browser-context/fixtures.d.ts +8 -0
- package/dist/browser-context/runIBrowserContextConformance.d.ts +7 -0
- package/dist/browser-context/types.d.ts +48 -0
- package/dist/browser-context.d.ts +18 -0
- package/dist/browser-context.js +567 -0
- package/dist/browser-context.js.map +17 -0
- package/dist/creditExhaustedSkip.d.ts +39 -0
- package/dist/creditExhaustedSkip.js +163 -0
- package/dist/creditExhaustedSkip.js.map +10 -0
- package/dist/entitlement-profile/assertions/denialShape.d.ts +13 -0
- package/dist/entitlement-profile/assertions/dispose.d.ts +16 -0
- package/dist/entitlement-profile/assertions/hierarchyHonoring.d.ts +11 -0
- package/dist/entitlement-profile/assertions/optionalNeverDenied.d.ts +11 -0
- package/dist/entitlement-profile/assertions/requestEntitlementShape.d.ts +12 -0
- package/dist/entitlement-profile/assertions/resourceScoping.d.ts +16 -0
- package/dist/entitlement-profile/assertions/subscribeGrant.d.ts +11 -0
- package/dist/entitlement-profile/assertions/subscribeReload.d.ts +11 -0
- package/dist/entitlement-profile/assertions/subscribeRevocation.d.ts +12 -0
- package/dist/entitlement-profile/assertions/surfaceCoverage.d.ts +7 -0
- package/dist/entitlement-profile/assertions/unsubscribeIdempotent.d.ts +7 -0
- package/dist/entitlement-profile/fixtures.d.ts +21 -0
- package/dist/entitlement-profile/runEntitlementProfileConformance.d.ts +7 -0
- package/dist/entitlement-profile/types.d.ts +45 -0
- package/dist/entitlement-profile.d.ts +23 -0
- package/dist/entitlement-profile.js +436 -0
- package/dist/entitlement-profile.js.map +22 -0
- package/dist/human-connector/MockHumanConnector.d.ts +29 -0
- package/dist/human-connector/assertions/abort.d.ts +7 -0
- package/dist/human-connector/assertions/capabilityHonesty.d.ts +7 -0
- package/dist/human-connector/assertions/concurrentIsolation.d.ts +7 -0
- package/dist/human-connector/assertions/multiTurnFollowUp.d.ts +7 -0
- package/dist/human-connector/assertions/notifyDisplayFastResolve.d.ts +7 -0
- package/dist/human-connector/assertions/roundtrip.d.ts +7 -0
- package/dist/human-connector/fixtures.d.ts +8 -0
- package/dist/human-connector/runHumanConnectorConformance.d.ts +9 -0
- package/dist/human-connector/types.d.ts +91 -0
- package/dist/human-connector.d.ts +19 -0
- package/dist/human-connector.js +843 -0
- package/dist/human-connector.js.map +20 -0
- package/dist/itExpectFail.d.ts +15 -0
- package/dist/itExpectFail.js +180 -0
- package/dist/itExpectFail.js.map +11 -0
- package/dist/storage-migrations/assertions/appliesAndRecords.d.ts +7 -0
- package/dist/storage-migrations/assertions/concurrentRunsSerialize.d.ts +18 -0
- package/dist/storage-migrations/assertions/ensureBookkeepingIdempotent.d.ts +7 -0
- package/dist/storage-migrations/assertions/failedMigrationLeavesNoPartialSchema.d.ts +14 -0
- package/dist/storage-migrations/assertions/failedMigrationNotRecorded.d.ts +7 -0
- package/dist/storage-migrations/assertions/idempotentRun.d.ts +7 -0
- package/dist/storage-migrations/assertions/incrementalApplication.d.ts +7 -0
- package/dist/storage-migrations/runMigrationRunnerContract.d.ts +9 -0
- package/dist/storage-migrations/types.d.ts +78 -0
- package/dist/storage-migrations.d.ts +18 -0
- package/dist/storage-migrations.js +473 -0
- package/dist/storage-migrations.js.map +20 -0
- package/dist/tabular-migrations/assertions/addAndDropIndex.d.ts +7 -0
- package/dist/tabular-migrations/assertions/addColumn.d.ts +7 -0
- package/dist/tabular-migrations/assertions/backfill.d.ts +7 -0
- package/dist/tabular-migrations/assertions/dropColumn.d.ts +7 -0
- package/dist/tabular-migrations/assertions/failedMigrationNotRecorded.d.ts +7 -0
- package/dist/tabular-migrations/assertions/freshDbFastPath.d.ts +7 -0
- package/dist/tabular-migrations/assertions/incrementalApplication.d.ts +7 -0
- package/dist/tabular-migrations/assertions/renameColumn.d.ts +7 -0
- package/dist/tabular-migrations/runTabularMigrationContract.d.ts +8 -0
- package/dist/tabular-migrations/types.d.ts +28 -0
- package/dist/tabular-migrations.d.ts +19 -0
- package/dist/tabular-migrations.js +257 -0
- package/dist/tabular-migrations.js.map +18 -0
- package/dist/tabular-storage/assertions/countMatchesQuery.d.ts +7 -0
- package/dist/tabular-storage/assertions/guardParity.d.ts +25 -0
- package/dist/tabular-storage/assertions/inListCriterion.d.ts +14 -0
- package/dist/tabular-storage/assertions/joinBoundedLeftRead.d.ts +29 -0
- package/dist/tabular-storage/assertions/joinPushdown.d.ts +13 -0
- package/dist/tabular-storage/assertions/notInListCriterion.d.ts +15 -0
- package/dist/tabular-storage/assertions/strategyParity.d.ts +31 -0
- package/dist/tabular-storage/assertions/subscribeToChanges.d.ts +21 -0
- package/dist/tabular-storage/assertions/vectorColumnFormat.d.ts +7 -0
- package/dist/tabular-storage/assertions/withConnectionTransaction.d.ts +7 -0
- package/dist/tabular-storage/assertions/withTransactionRollback.d.ts +7 -0
- package/dist/tabular-storage/fixtures.d.ts +33 -0
- package/dist/tabular-storage/joinFixtures.d.ts +66 -0
- package/dist/tabular-storage/runTabularJoinContract.d.ts +24 -0
- package/dist/tabular-storage/runTabularStorageContract.d.ts +9 -0
- package/dist/tabular-storage/types.d.ts +102 -0
- package/dist/tabular-storage.d.ts +25 -0
- package/dist/tabular-storage.js +1477 -0
- package/dist/tabular-storage.js.map +27 -0
- package/dist/vector-storage/assertions/dimensionValidation.d.ts +17 -0
- package/dist/vector-storage/assertions/legacyEncoding.d.ts +16 -0
- package/dist/vector-storage/assertions/metadataFilter.d.ts +16 -0
- package/dist/vector-storage/assertions/searchOptions.d.ts +16 -0
- package/dist/vector-storage/assertions/shared.d.ts +23 -0
- package/dist/vector-storage/assertions/similarityRanking.d.ts +18 -0
- package/dist/vector-storage/assertions/similaritySearchEvent.d.ts +16 -0
- package/dist/vector-storage/assertions/vectorRoundTrip.d.ts +17 -0
- package/dist/vector-storage/runVectorStorageContract.d.ts +20 -0
- package/dist/vector-storage/types.d.ts +87 -0
- package/dist/vector-storage.d.ts +19 -0
- package/dist/vector-storage.js +412 -0
- package/dist/vector-storage.js.map +21 -0
- package/dist/web-search/assertions/capabilityAgreement.d.ts +23 -0
- package/dist/web-search/assertions/credentialNaming.d.ts +21 -0
- package/dist/web-search/assertions/dateFiltering.d.ts +22 -0
- package/dist/web-search/assertions/domainNormalization.d.ts +24 -0
- package/dist/web-search/assertions/maxResultsCeiling.d.ts +22 -0
- package/dist/web-search/assertions/reportedQuery.d.ts +18 -0
- package/dist/web-search/assertions/signalThreading.d.ts +20 -0
- package/dist/web-search/fixtures.d.ts +40 -0
- package/dist/web-search/runWebSearchProviderConformance.d.ts +24 -0
- package/dist/web-search/types.d.ts +79 -0
- package/dist/web-search.d.ts +19 -0
- package/dist/web-search.js +560 -0
- package/dist/web-search.js.map +18 -0
- package/dist/worker-proxy/assertions/backlogOrdering.d.ts +7 -0
- package/dist/worker-proxy/assertions/disposeTerminatesWorker.d.ts +8 -0
- package/dist/worker-proxy/assertions/errorPropagation.d.ts +7 -0
- package/dist/worker-proxy/assertions/providerCallHelpers.d.ts +23 -0
- package/dist/worker-proxy/browserOnlyStub.d.ts +7 -0
- package/dist/worker-proxy/runWorkerProxyBoundary.d.ts +7 -0
- package/dist/worker-proxy/types.d.ts +47 -0
- package/dist/worker-proxy.d.ts +16 -0
- package/dist/worker-proxy.js +389 -0
- package/dist/worker-proxy.js.map +17 -0
- package/package.json +120 -0
|
@@ -0,0 +1,560 @@
|
|
|
1
|
+
// src/web-search/assertions/capabilityAgreement.ts
|
|
2
|
+
import { unhonorableOptions, WebSearchTask } from "@workglow/web-search";
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
4
|
+
|
|
5
|
+
// src/web-search/fixtures.ts
|
|
6
|
+
import { WebSearchProviderRegistry } from "@workglow/web-search";
|
|
7
|
+
var PROBE_QUERY = "transformer architecture";
|
|
8
|
+
function decoyProvider(name, onSearch) {
|
|
9
|
+
return {
|
|
10
|
+
name,
|
|
11
|
+
endpoint: `https://${name}.invalid`,
|
|
12
|
+
acceptsCredentialKey: true,
|
|
13
|
+
capabilities: {
|
|
14
|
+
answer: true,
|
|
15
|
+
content: true,
|
|
16
|
+
domainFilter: "native",
|
|
17
|
+
dateFilter: true,
|
|
18
|
+
maxResultsCap: undefined
|
|
19
|
+
},
|
|
20
|
+
search: async (request) => {
|
|
21
|
+
onSearch?.(request);
|
|
22
|
+
return {
|
|
23
|
+
results: [{ title: "decoy", url: `https://${name}.invalid/a` }],
|
|
24
|
+
query: request.query
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function registerOnly(harness) {
|
|
30
|
+
WebSearchProviderRegistry.clear();
|
|
31
|
+
WebSearchProviderRegistry.register(harness.provider);
|
|
32
|
+
}
|
|
33
|
+
function domainRoutes(provider) {
|
|
34
|
+
const capabilities = provider.capabilities;
|
|
35
|
+
return {
|
|
36
|
+
include: capabilities.domainFilter,
|
|
37
|
+
exclude: capabilities.excludeDomainFilter ?? capabilities.domainFilter
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function decodeLoosely(text) {
|
|
41
|
+
try {
|
|
42
|
+
return decodeURIComponent(text.replace(/\+/g, " "));
|
|
43
|
+
} catch {
|
|
44
|
+
return text;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function sentText(harness) {
|
|
48
|
+
const raw = harness.sent().join(`
|
|
49
|
+
`);
|
|
50
|
+
return `${raw}
|
|
51
|
+
${decodeLoosely(raw)}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// src/web-search/assertions/capabilityAgreement.ts
|
|
55
|
+
function probesFor(domain) {
|
|
56
|
+
return [
|
|
57
|
+
{ label: "includeDomains", request: { includeDomains: [domain] } },
|
|
58
|
+
{ label: "excludeDomains", request: { excludeDomains: [domain] } },
|
|
59
|
+
{
|
|
60
|
+
label: "includeDomains with excludeDomains",
|
|
61
|
+
request: { includeDomains: [domain], excludeDomains: ["elsewhere.example"] }
|
|
62
|
+
},
|
|
63
|
+
{ label: "dateRange", request: { dateRange: { start: "2024-01-01", end: "2024-12-31" } } },
|
|
64
|
+
{ label: "includeAnswer", request: { includeAnswer: true } },
|
|
65
|
+
{ label: "includeContent", request: { includeContent: true } }
|
|
66
|
+
];
|
|
67
|
+
}
|
|
68
|
+
function capabilityAgreementBlock(opts) {
|
|
69
|
+
const domain = opts.sampleDomain ?? "example.com";
|
|
70
|
+
const probes = probesFor(domain);
|
|
71
|
+
describe("capability agreement", () => {
|
|
72
|
+
let harness;
|
|
73
|
+
beforeEach(async () => {
|
|
74
|
+
harness = await opts.createHarness({ resultCount: 2 });
|
|
75
|
+
registerOnly(harness);
|
|
76
|
+
});
|
|
77
|
+
afterEach(() => harness.dispose());
|
|
78
|
+
it("probes every option the capability check can refuse", () => {
|
|
79
|
+
const everything = {
|
|
80
|
+
answer: false,
|
|
81
|
+
content: false,
|
|
82
|
+
domainFilter: false,
|
|
83
|
+
excludeDomainFilter: false,
|
|
84
|
+
exclusiveDomainDirections: true,
|
|
85
|
+
dateFilter: false,
|
|
86
|
+
maxResultsCap: undefined
|
|
87
|
+
};
|
|
88
|
+
const reachable = new Set;
|
|
89
|
+
for (const probe of probes) {
|
|
90
|
+
for (const gap of unhonorableOptions(everything, {
|
|
91
|
+
query: PROBE_QUERY,
|
|
92
|
+
...probe.request
|
|
93
|
+
})) {
|
|
94
|
+
reachable.add(gap);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
for (const gap of unhonorableOptions({ ...everything, domainFilter: "native", excludeDomainFilter: "native" }, { query: PROBE_QUERY, includeDomains: [domain], excludeDomains: ["elsewhere.example"] })) {
|
|
98
|
+
reachable.add(gap);
|
|
99
|
+
}
|
|
100
|
+
expect([...reachable].sort()).toEqual(probes.map((p) => p.label).sort());
|
|
101
|
+
}, opts.timeout);
|
|
102
|
+
for (const probe of probes) {
|
|
103
|
+
it(`agrees with its own record about ${probe.label}`, async () => {
|
|
104
|
+
const declared = unhonorableOptions(harness.provider.capabilities, {
|
|
105
|
+
query: PROBE_QUERY,
|
|
106
|
+
...probe.request
|
|
107
|
+
});
|
|
108
|
+
const run = new WebSearchTask().run({
|
|
109
|
+
query: PROBE_QUERY,
|
|
110
|
+
provider: harness.provider.name,
|
|
111
|
+
...probe.request
|
|
112
|
+
});
|
|
113
|
+
if (declared.length > 0) {
|
|
114
|
+
await expect(run).rejects.toThrow(new RegExp(declared.map((g) => g.replace(/\s/g, "\\s")).join("|")));
|
|
115
|
+
expect(harness.sent()).toEqual([]);
|
|
116
|
+
} else {
|
|
117
|
+
const output = await run;
|
|
118
|
+
expect(harness.sent().length).toBeGreaterThan(0);
|
|
119
|
+
if (probe.request.includeAnswer === true) {
|
|
120
|
+
expect(output.answer).toBeTruthy();
|
|
121
|
+
}
|
|
122
|
+
if (probe.request.includeContent === true) {
|
|
123
|
+
expect(output.results.some((result) => Boolean(result.content))).toBe(true);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}, opts.timeout);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
// src/web-search/assertions/credentialNaming.ts
|
|
131
|
+
import { WebSearchProviderRegistry as WebSearchProviderRegistry2, WebSearchTask as WebSearchTask2 } from "@workglow/web-search";
|
|
132
|
+
import { afterEach as afterEach2, beforeEach as beforeEach2, describe as describe2, expect as expect2, it as it2 } from "vitest";
|
|
133
|
+
var DECOY = "conformance-decoy";
|
|
134
|
+
var SECRET_KEY = "conformance-key-for-the-decoy";
|
|
135
|
+
function credentialNamingBlock(opts) {
|
|
136
|
+
describe2("credential naming", () => {
|
|
137
|
+
let harness;
|
|
138
|
+
beforeEach2(async () => {
|
|
139
|
+
harness = await opts.createHarness({ resultCount: 2 });
|
|
140
|
+
registerOnly(harness);
|
|
141
|
+
});
|
|
142
|
+
afterEach2(() => {
|
|
143
|
+
harness.dispose();
|
|
144
|
+
WebSearchProviderRegistry2.clear();
|
|
145
|
+
});
|
|
146
|
+
it2("refuses a bare credential_key under auto", async () => {
|
|
147
|
+
await expect2(new WebSearchTask2().run({
|
|
148
|
+
query: PROBE_QUERY,
|
|
149
|
+
provider: "auto",
|
|
150
|
+
credential_key: SECRET_KEY
|
|
151
|
+
})).rejects.toThrow(/credential_key.*auto|auto.*credential_key/s);
|
|
152
|
+
expect2(harness.sent()).toEqual([]);
|
|
153
|
+
}, opts.timeout);
|
|
154
|
+
it2("never sends a key named for another provider", async () => {
|
|
155
|
+
let decoyRan = false;
|
|
156
|
+
WebSearchProviderRegistry2.register(decoyProvider(DECOY, () => {
|
|
157
|
+
decoyRan = true;
|
|
158
|
+
}));
|
|
159
|
+
await new WebSearchTask2().run({
|
|
160
|
+
query: PROBE_QUERY,
|
|
161
|
+
provider: harness.provider.name,
|
|
162
|
+
credential_keys: { [DECOY]: SECRET_KEY }
|
|
163
|
+
});
|
|
164
|
+
expect2(decoyRan).toBe(false);
|
|
165
|
+
expect2(sentText(harness)).not.toContain(SECRET_KEY);
|
|
166
|
+
}, opts.timeout);
|
|
167
|
+
it2("refuses a key named for a provider that is not registered", async () => {
|
|
168
|
+
await expect2(new WebSearchTask2().run({
|
|
169
|
+
query: PROBE_QUERY,
|
|
170
|
+
provider: harness.provider.name,
|
|
171
|
+
credential_keys: { "not-a-provider": SECRET_KEY }
|
|
172
|
+
})).rejects.toThrow(/not a .*registered provider/s);
|
|
173
|
+
expect2(harness.sent()).toEqual([]);
|
|
174
|
+
}, opts.timeout);
|
|
175
|
+
it2("says so when this provider never receives a credential key", async () => {
|
|
176
|
+
const run = new WebSearchTask2().run({
|
|
177
|
+
query: PROBE_QUERY,
|
|
178
|
+
provider: harness.provider.name,
|
|
179
|
+
credential_keys: { [harness.provider.name]: SECRET_KEY }
|
|
180
|
+
});
|
|
181
|
+
if (harness.provider.acceptsCredentialKey) {
|
|
182
|
+
await expect2(run).resolves.toBeDefined();
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
await expect2(run).rejects.toThrow(/never receives a credential-store key/);
|
|
186
|
+
expect2(harness.sent()).toEqual([]);
|
|
187
|
+
}, opts.timeout);
|
|
188
|
+
it2("prefers a provider a key is named for when routing", async () => {
|
|
189
|
+
let decoyRan = false;
|
|
190
|
+
WebSearchProviderRegistry2.register(decoyProvider(DECOY, () => {
|
|
191
|
+
decoyRan = true;
|
|
192
|
+
}));
|
|
193
|
+
const out = await new WebSearchTask2().run({
|
|
194
|
+
query: PROBE_QUERY,
|
|
195
|
+
provider: "auto",
|
|
196
|
+
credential_keys: { [DECOY]: SECRET_KEY }
|
|
197
|
+
});
|
|
198
|
+
expect2(out.provider).toBe(DECOY);
|
|
199
|
+
expect2(decoyRan).toBe(true);
|
|
200
|
+
expect2(harness.sent()).toEqual([]);
|
|
201
|
+
}, opts.timeout);
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
// src/web-search/assertions/dateFiltering.ts
|
|
205
|
+
import { WebSearchTask as WebSearchTask3 } from "@workglow/web-search";
|
|
206
|
+
import { afterEach as afterEach3, describe as describe3, expect as expect3, it as it3 } from "vitest";
|
|
207
|
+
function datesIn(text) {
|
|
208
|
+
return [...new Set(text.match(/\d{4}-\d{2}-\d{2}/g) ?? [])];
|
|
209
|
+
}
|
|
210
|
+
var RANGES = [
|
|
211
|
+
{ label: "a closed range", value: { start: "2024-01-01", end: "2024-06-30" } },
|
|
212
|
+
{ label: "a range open at the start", value: { end: "2024-06-30" } },
|
|
213
|
+
{ label: "a range open at the end", value: { start: "2024-01-01" } }
|
|
214
|
+
];
|
|
215
|
+
function dateFilteringBlock(opts) {
|
|
216
|
+
describe3("date filtering", () => {
|
|
217
|
+
let harness;
|
|
218
|
+
afterEach3(() => harness?.dispose());
|
|
219
|
+
for (const range of RANGES) {
|
|
220
|
+
it3(`never emulates ${range.label}`, async () => {
|
|
221
|
+
harness = await opts.createHarness({ resultCount: 3 });
|
|
222
|
+
registerOnly(harness);
|
|
223
|
+
const run = new WebSearchTask3().run({
|
|
224
|
+
query: PROBE_QUERY,
|
|
225
|
+
provider: harness.provider.name,
|
|
226
|
+
dateRange: { ...range.value }
|
|
227
|
+
});
|
|
228
|
+
if (!harness.provider.capabilities.dateFilter) {
|
|
229
|
+
await expect3(run).rejects.toThrow(/dateRange/);
|
|
230
|
+
expect3(harness.sent()).toEqual([]);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
await expect3(run).resolves.toBeDefined();
|
|
234
|
+
const withRange = sentText(harness);
|
|
235
|
+
harness.dispose();
|
|
236
|
+
const plain = await opts.createHarness({ resultCount: 3 });
|
|
237
|
+
registerOnly(plain);
|
|
238
|
+
try {
|
|
239
|
+
await new WebSearchTask3().run({
|
|
240
|
+
query: PROBE_QUERY,
|
|
241
|
+
provider: plain.provider.name
|
|
242
|
+
});
|
|
243
|
+
expect3(withRange).not.toEqual(sentText(plain));
|
|
244
|
+
} finally {
|
|
245
|
+
plain.dispose();
|
|
246
|
+
}
|
|
247
|
+
}, opts.timeout);
|
|
248
|
+
}
|
|
249
|
+
it3("fills the open end of a half-open range rather than sending it half", async () => {
|
|
250
|
+
harness = await opts.createHarness({ resultCount: 3 });
|
|
251
|
+
registerOnly(harness);
|
|
252
|
+
if (!harness.provider.capabilities.dateFilter)
|
|
253
|
+
return;
|
|
254
|
+
await new WebSearchTask3().run({
|
|
255
|
+
query: PROBE_QUERY,
|
|
256
|
+
provider: harness.provider.name,
|
|
257
|
+
dateRange: { start: "2024-01-01" }
|
|
258
|
+
});
|
|
259
|
+
const openEnded = datesIn(sentText(harness));
|
|
260
|
+
expect3(openEnded).toContain("2024-01-01");
|
|
261
|
+
if (!opts.fillsOpenDateBounds) {
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
expect3(openEnded.length).toBeGreaterThanOrEqual(2);
|
|
265
|
+
}, opts.timeout);
|
|
266
|
+
it3("fills the open start of a half-open range too", async () => {
|
|
267
|
+
harness = await opts.createHarness({ resultCount: 3 });
|
|
268
|
+
registerOnly(harness);
|
|
269
|
+
if (!harness.provider.capabilities.dateFilter)
|
|
270
|
+
return;
|
|
271
|
+
await new WebSearchTask3().run({
|
|
272
|
+
query: PROBE_QUERY,
|
|
273
|
+
provider: harness.provider.name,
|
|
274
|
+
dateRange: { end: "2024-06-30" }
|
|
275
|
+
});
|
|
276
|
+
const openStart = datesIn(sentText(harness));
|
|
277
|
+
expect3(openStart).toContain("2024-06-30");
|
|
278
|
+
if (!opts.fillsOpenDateBounds)
|
|
279
|
+
return;
|
|
280
|
+
expect3(openStart.length).toBeGreaterThanOrEqual(2);
|
|
281
|
+
}, opts.timeout);
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
// src/web-search/assertions/domainNormalization.ts
|
|
285
|
+
import { WebSearchTask as WebSearchTask4 } from "@workglow/web-search";
|
|
286
|
+
import { afterEach as afterEach4, beforeEach as beforeEach3, describe as describe4, expect as expect4, it as it4 } from "vitest";
|
|
287
|
+
var SPELLINGS = ["https://Example.COM/", "www.example.com", "EXAMPLE.com", "example.com/"];
|
|
288
|
+
var UNUSABLE = ["", " ", "a.com b.com", "(example.com)", 'ex"ample.com', "it's.com"];
|
|
289
|
+
function domainNormalizationBlock(opts) {
|
|
290
|
+
describe4("domain normalization", () => {
|
|
291
|
+
let harness;
|
|
292
|
+
beforeEach3(async () => {
|
|
293
|
+
harness = await opts.createHarness({ resultCount: 2 });
|
|
294
|
+
registerOnly(harness);
|
|
295
|
+
});
|
|
296
|
+
afterEach4(() => harness.dispose());
|
|
297
|
+
const routes = () => domainRoutes(harness.provider);
|
|
298
|
+
it4("reduces every spelling of one host to the same request", async () => {
|
|
299
|
+
if (routes().include === false)
|
|
300
|
+
return;
|
|
301
|
+
const sentFor = async (domain) => {
|
|
302
|
+
const local = await opts.createHarness({ resultCount: 2 });
|
|
303
|
+
registerOnly(local);
|
|
304
|
+
try {
|
|
305
|
+
await new WebSearchTask4().run({
|
|
306
|
+
query: PROBE_QUERY,
|
|
307
|
+
provider: local.provider.name,
|
|
308
|
+
includeDomains: [domain]
|
|
309
|
+
});
|
|
310
|
+
return sentText(local);
|
|
311
|
+
} finally {
|
|
312
|
+
local.dispose();
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
const first = await sentFor(SPELLINGS[0]);
|
|
316
|
+
for (const spelling of SPELLINGS.slice(1)) {
|
|
317
|
+
expect4(await sentFor(spelling)).toEqual(first);
|
|
318
|
+
}
|
|
319
|
+
}, opts.timeout);
|
|
320
|
+
it4("carries the restriction to the wire rather than dropping it", async () => {
|
|
321
|
+
if (routes().include === false)
|
|
322
|
+
return;
|
|
323
|
+
const plain = await opts.createHarness({ resultCount: 2 });
|
|
324
|
+
registerOnly(plain);
|
|
325
|
+
await new WebSearchTask4().run({ query: PROBE_QUERY, provider: plain.provider.name });
|
|
326
|
+
const withoutFilter = sentText(plain);
|
|
327
|
+
plain.dispose();
|
|
328
|
+
registerOnly(harness);
|
|
329
|
+
await new WebSearchTask4().run({
|
|
330
|
+
query: PROBE_QUERY,
|
|
331
|
+
provider: harness.provider.name,
|
|
332
|
+
includeDomains: ["example.com"]
|
|
333
|
+
});
|
|
334
|
+
expect4(sentText(harness)).not.toEqual(withoutFilter);
|
|
335
|
+
expect4(sentText(harness)).toContain("example.com");
|
|
336
|
+
}, opts.timeout);
|
|
337
|
+
it4("carries an exclude restriction to the wire rather than dropping it", async () => {
|
|
338
|
+
if (routes().exclude === false)
|
|
339
|
+
return;
|
|
340
|
+
const plain = await opts.createHarness({ resultCount: 2 });
|
|
341
|
+
registerOnly(plain);
|
|
342
|
+
await new WebSearchTask4().run({ query: PROBE_QUERY, provider: plain.provider.name });
|
|
343
|
+
const withoutFilter = sentText(plain);
|
|
344
|
+
plain.dispose();
|
|
345
|
+
registerOnly(harness);
|
|
346
|
+
await new WebSearchTask4().run({
|
|
347
|
+
query: PROBE_QUERY,
|
|
348
|
+
provider: harness.provider.name,
|
|
349
|
+
excludeDomains: ["blocked.example"]
|
|
350
|
+
});
|
|
351
|
+
expect4(sentText(harness)).not.toEqual(withoutFilter);
|
|
352
|
+
expect4(sentText(harness)).toContain("blocked.example");
|
|
353
|
+
}, opts.timeout);
|
|
354
|
+
for (const entry of UNUSABLE) {
|
|
355
|
+
it4(`refuses includeDomains ${JSON.stringify(entry)} before the call`, async () => {
|
|
356
|
+
await expect4(new WebSearchTask4().run({
|
|
357
|
+
query: PROBE_QUERY,
|
|
358
|
+
provider: harness.provider.name,
|
|
359
|
+
includeDomains: [entry]
|
|
360
|
+
})).rejects.toThrow(/names no domain|name no domain/);
|
|
361
|
+
expect4(harness.sent()).toEqual([]);
|
|
362
|
+
}, opts.timeout);
|
|
363
|
+
it4(`refuses excludeDomains ${JSON.stringify(entry)} before the call`, async () => {
|
|
364
|
+
await expect4(new WebSearchTask4().run({
|
|
365
|
+
query: PROBE_QUERY,
|
|
366
|
+
provider: harness.provider.name,
|
|
367
|
+
excludeDomains: [entry]
|
|
368
|
+
})).rejects.toThrow(/names no domain|name no domain/);
|
|
369
|
+
expect4(harness.sent()).toEqual([]);
|
|
370
|
+
}, opts.timeout);
|
|
371
|
+
}
|
|
372
|
+
it4("refuses a bad entry even beside good ones", async () => {
|
|
373
|
+
await expect4(new WebSearchTask4().run({
|
|
374
|
+
query: PROBE_QUERY,
|
|
375
|
+
provider: harness.provider.name,
|
|
376
|
+
includeDomains: ["good.example", "a.com b.com", "also-good.example"]
|
|
377
|
+
})).rejects.toThrow(/names no domain/);
|
|
378
|
+
expect4(harness.sent()).toEqual([]);
|
|
379
|
+
}, opts.timeout);
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
// src/web-search/assertions/maxResultsCeiling.ts
|
|
383
|
+
import { WebSearchTask as WebSearchTask5 } from "@workglow/web-search";
|
|
384
|
+
import { afterEach as afterEach5, beforeEach as beforeEach4, describe as describe5, expect as expect5, it as it5 } from "vitest";
|
|
385
|
+
function maxResultsCeilingBlock(opts) {
|
|
386
|
+
describe5("maxResults ceiling", () => {
|
|
387
|
+
let harness;
|
|
388
|
+
afterEach5(() => harness?.dispose());
|
|
389
|
+
beforeEach4(() => {});
|
|
390
|
+
it5("never returns more results than asked for", async () => {
|
|
391
|
+
harness = await opts.createHarness({ resultCount: 9 });
|
|
392
|
+
registerOnly(harness);
|
|
393
|
+
const out = await new WebSearchTask5().run({
|
|
394
|
+
query: PROBE_QUERY,
|
|
395
|
+
provider: harness.provider.name,
|
|
396
|
+
maxResults: 3
|
|
397
|
+
});
|
|
398
|
+
expect5(out.results.length).toBeLessThanOrEqual(3);
|
|
399
|
+
expect5(out.count).toBe(out.results.length);
|
|
400
|
+
}, opts.timeout);
|
|
401
|
+
it5("clamps a value over the provider's cap rather than refusing it", async () => {
|
|
402
|
+
harness = await opts.createHarness({ resultCount: 4 });
|
|
403
|
+
registerOnly(harness);
|
|
404
|
+
const cap = harness.provider.capabilities.maxResultsCap;
|
|
405
|
+
const asked = (cap ?? 20) + 500;
|
|
406
|
+
const out = await new WebSearchTask5().run({
|
|
407
|
+
query: PROBE_QUERY,
|
|
408
|
+
provider: harness.provider.name,
|
|
409
|
+
maxResults: asked
|
|
410
|
+
});
|
|
411
|
+
expect5(out.results.length).toBeLessThanOrEqual(cap ?? asked);
|
|
412
|
+
}, opts.timeout);
|
|
413
|
+
it5("returns what it has when fewer results exist than asked for", async () => {
|
|
414
|
+
harness = await opts.createHarness({ resultCount: 2 });
|
|
415
|
+
registerOnly(harness);
|
|
416
|
+
const out = await new WebSearchTask5().run({
|
|
417
|
+
query: PROBE_QUERY,
|
|
418
|
+
provider: harness.provider.name,
|
|
419
|
+
maxResults: 8
|
|
420
|
+
});
|
|
421
|
+
expect5(out.results).toHaveLength(2);
|
|
422
|
+
}, opts.timeout);
|
|
423
|
+
it5("reports count as the number of results it actually returned", async () => {
|
|
424
|
+
harness = await opts.createHarness({ resultCount: 6 });
|
|
425
|
+
registerOnly(harness);
|
|
426
|
+
const out = await new WebSearchTask5().run({
|
|
427
|
+
query: PROBE_QUERY,
|
|
428
|
+
provider: harness.provider.name,
|
|
429
|
+
maxResults: 2
|
|
430
|
+
});
|
|
431
|
+
expect5(out.count).toBe(out.results.length);
|
|
432
|
+
}, opts.timeout);
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
// src/web-search/assertions/reportedQuery.ts
|
|
436
|
+
import { WebSearchTask as WebSearchTask6 } from "@workglow/web-search";
|
|
437
|
+
import { afterEach as afterEach6, beforeEach as beforeEach5, describe as describe6, expect as expect6, it as it6 } from "vitest";
|
|
438
|
+
function reportedQueryBlock(opts) {
|
|
439
|
+
describe6("reported query and provider", () => {
|
|
440
|
+
let harness;
|
|
441
|
+
beforeEach5(async () => {
|
|
442
|
+
harness = await opts.createHarness({ resultCount: 3 });
|
|
443
|
+
registerOnly(harness);
|
|
444
|
+
});
|
|
445
|
+
afterEach6(() => harness.dispose());
|
|
446
|
+
it6("reports the provider that ran when it was pinned", async () => {
|
|
447
|
+
const out = await new WebSearchTask6().run({
|
|
448
|
+
query: PROBE_QUERY,
|
|
449
|
+
provider: harness.provider.name
|
|
450
|
+
});
|
|
451
|
+
expect6(out.provider).toBe(harness.provider.name);
|
|
452
|
+
}, opts.timeout);
|
|
453
|
+
it6("reports the provider that ran when routing chose it", async () => {
|
|
454
|
+
const out = await new WebSearchTask6().run({ query: PROBE_QUERY, provider: "auto" });
|
|
455
|
+
expect6(out.provider).toBe(harness.provider.name);
|
|
456
|
+
}, opts.timeout);
|
|
457
|
+
it6("reports the unmodified query when nothing rewrote it", async () => {
|
|
458
|
+
const out = await new WebSearchTask6().run({
|
|
459
|
+
query: PROBE_QUERY,
|
|
460
|
+
provider: harness.provider.name
|
|
461
|
+
});
|
|
462
|
+
expect6(out.query).toBe(PROBE_QUERY);
|
|
463
|
+
}, opts.timeout);
|
|
464
|
+
it6("reports the rewritten query when a domain restriction moved into it", async () => {
|
|
465
|
+
const routes = domainRoutes(harness.provider);
|
|
466
|
+
if (routes.include !== "query-operator")
|
|
467
|
+
return;
|
|
468
|
+
const out = await new WebSearchTask6().run({
|
|
469
|
+
query: PROBE_QUERY,
|
|
470
|
+
provider: harness.provider.name,
|
|
471
|
+
includeDomains: ["example.com"]
|
|
472
|
+
});
|
|
473
|
+
expect6(out.query).toContain("site:example.com");
|
|
474
|
+
expect6(out.query).not.toBe(PROBE_QUERY);
|
|
475
|
+
expect6(sentText(harness)).toContain("site:example.com");
|
|
476
|
+
}, opts.timeout);
|
|
477
|
+
it6("leaves the query alone when the restriction travels as a list", async () => {
|
|
478
|
+
const routes = domainRoutes(harness.provider);
|
|
479
|
+
if (routes.include !== "native")
|
|
480
|
+
return;
|
|
481
|
+
const out = await new WebSearchTask6().run({
|
|
482
|
+
query: PROBE_QUERY,
|
|
483
|
+
provider: harness.provider.name,
|
|
484
|
+
includeDomains: ["example.com"]
|
|
485
|
+
});
|
|
486
|
+
expect6(out.query).toBe(PROBE_QUERY);
|
|
487
|
+
}, opts.timeout);
|
|
488
|
+
it6("always reports results and a count, even with nothing to report", async () => {
|
|
489
|
+
const empty = await opts.createHarness({ resultCount: 0 });
|
|
490
|
+
registerOnly(empty);
|
|
491
|
+
try {
|
|
492
|
+
const out = await new WebSearchTask6().run({
|
|
493
|
+
query: PROBE_QUERY,
|
|
494
|
+
provider: empty.provider.name
|
|
495
|
+
});
|
|
496
|
+
expect6(Array.isArray(out.results)).toBe(true);
|
|
497
|
+
expect6(out.count).toBe(0);
|
|
498
|
+
expect6(out.provider).toBe(empty.provider.name);
|
|
499
|
+
} finally {
|
|
500
|
+
empty.dispose();
|
|
501
|
+
}
|
|
502
|
+
}, opts.timeout);
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
// src/web-search/assertions/signalThreading.ts
|
|
506
|
+
import { afterEach as afterEach7, beforeEach as beforeEach6, describe as describe7, expect as expect7, it as it7 } from "vitest";
|
|
507
|
+
function signalThreadingBlock(opts) {
|
|
508
|
+
describe7.skipIf(!opts.signalReachesTransport)("signal threading", () => {
|
|
509
|
+
let harness;
|
|
510
|
+
beforeEach6(async () => {
|
|
511
|
+
harness = await opts.createHarness({ resultCount: 2 });
|
|
512
|
+
registerOnly(harness);
|
|
513
|
+
});
|
|
514
|
+
afterEach7(() => harness.dispose());
|
|
515
|
+
it7("hands the run's signal to the outbound call", async () => {
|
|
516
|
+
const controller = new AbortController;
|
|
517
|
+
await harness.search({ query: PROBE_QUERY }, controller.signal);
|
|
518
|
+
const signals = harness.signals();
|
|
519
|
+
expect7(signals.length).toBeGreaterThan(0);
|
|
520
|
+
for (const signal of signals) {
|
|
521
|
+
expect7(signal).toBeDefined();
|
|
522
|
+
expect7(signal?.aborted).toBe(false);
|
|
523
|
+
}
|
|
524
|
+
controller.abort();
|
|
525
|
+
for (const signal of signals) {
|
|
526
|
+
expect7(signal?.aborted).toBe(true);
|
|
527
|
+
}
|
|
528
|
+
}, opts.timeout);
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
// src/web-search/runWebSearchProviderConformance.ts
|
|
532
|
+
import { describe as describe8 } from "vitest";
|
|
533
|
+
function runWebSearchProviderConformance(opts) {
|
|
534
|
+
describe8.skipIf(opts.skip)(`Web search provider contract: ${opts.name}`, () => {
|
|
535
|
+
capabilityAgreementBlock(opts);
|
|
536
|
+
maxResultsCeilingBlock(opts);
|
|
537
|
+
domainNormalizationBlock(opts);
|
|
538
|
+
dateFilteringBlock(opts);
|
|
539
|
+
reportedQueryBlock(opts);
|
|
540
|
+
signalThreadingBlock(opts);
|
|
541
|
+
credentialNamingBlock(opts);
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
export {
|
|
545
|
+
PROBE_QUERY,
|
|
546
|
+
capabilityAgreementBlock,
|
|
547
|
+
credentialNamingBlock,
|
|
548
|
+
dateFilteringBlock,
|
|
549
|
+
decoyProvider,
|
|
550
|
+
domainNormalizationBlock,
|
|
551
|
+
domainRoutes,
|
|
552
|
+
maxResultsCeilingBlock,
|
|
553
|
+
registerOnly,
|
|
554
|
+
reportedQueryBlock,
|
|
555
|
+
runWebSearchProviderConformance,
|
|
556
|
+
sentText,
|
|
557
|
+
signalThreadingBlock
|
|
558
|
+
};
|
|
559
|
+
|
|
560
|
+
//# debugId=C4B5C15DB8F13A3964756E2164756E21
|