@openwop/openwop-conformance 1.37.0 → 1.43.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/CHANGELOG.md +32 -0
- package/README.md +2 -2
- package/api/openapi.yaml +62 -5
- package/fixtures/conformance-agent-memory-injection-budget.json +44 -0
- package/fixtures/conformance-context-budget-multiturn.json +50 -0
- package/fixtures.md +2 -0
- package/package.json +1 -1
- package/schemas/README.md +3 -0
- package/schemas/a2ui-surface-delta-frame.schema.json +48 -0
- package/schemas/capabilities.schema.json +128 -1
- package/schemas/channel-presence-payload.schema.json +41 -0
- package/schemas/compact-tool-descriptor.schema.json +51 -0
- package/schemas/conversation-turn.schema.json +10 -0
- package/schemas/memory-list-options.schema.json +16 -0
- package/schemas/run-event-payloads.schema.json +25 -2
- package/schemas/run-event.schema.json +2 -0
- package/src/lib/toolCatalog.ts +89 -0
- package/src/scenarios/a2ui-surface-delta-transport.test.ts +600 -0
- package/src/scenarios/channel-presence-behavioral.test.ts +83 -0
- package/src/scenarios/channel-presence-shape.test.ts +93 -0
- package/src/scenarios/context-budget-transcript-bound.test.ts +253 -0
- package/src/scenarios/context-summarization-replay.test.ts +155 -0
- package/src/scenarios/conversation-turn-model-provenance-shape.test.ts +120 -0
- package/src/scenarios/memory-injection-budget.test.ts +188 -0
- package/src/scenarios/prompt-prefix-cache.test.ts +200 -0
- package/src/scenarios/run-transport-economy.test.ts +236 -0
- package/src/scenarios/tool-catalog-compact-projection.test.ts +149 -0
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compact tool projection — `GET /v1/tools?view=compact` (RFC 0112) — behavioral.
|
|
3
|
+
*
|
|
4
|
+
* Capability-gated on `capabilities.toolCatalog.compactView === true` (root-first
|
|
5
|
+
* per RFC 0073). Soft-skips when unadvertised (default) / hard-fails under
|
|
6
|
+
* `OPENWOP_REQUIRE_BEHAVIOR=true`. The standard projection coverage lives in
|
|
7
|
+
* `tool-catalog-projection.test.ts`; this asserts the OPT-IN compact view per
|
|
8
|
+
* `spec/v1/tool-catalog.md` §compact (and the new
|
|
9
|
+
* `compact-tool-descriptor.schema.json`):
|
|
10
|
+
*
|
|
11
|
+
* 1. ENVELOPE (§compact) — `GET /v1/tools?view=compact` returns the
|
|
12
|
+
* `{ tools: CompactToolDescriptor[] }` envelope (NOT a bare array).
|
|
13
|
+
* 2. SCHEMA — every compact descriptor validates against
|
|
14
|
+
* `compact-tool-descriptor.schema.json` (closed field set; the heavy
|
|
15
|
+
* `ToolDescriptor` fields — `outputSchema`/`auth`/`egress`/`approval`/
|
|
16
|
+
* `replayPolicy`/`costHint`/`latencyHint` — are ABSENT).
|
|
17
|
+
* 3. STRUCTURAL SUBSET — every present `inputSchema` satisfies the compact
|
|
18
|
+
* structural subset: top-level `type: "object"` with `properties`, and
|
|
19
|
+
* none of `$ref`/`oneOf`/`allOf`/`anyOf`/`not`/`patternProperties`/
|
|
20
|
+
* `dependentSchemas`. Validated against the schema (no dereference of the
|
|
21
|
+
* informative RFC 0030 Tier-1 table).
|
|
22
|
+
* 4. PROJECTION COMPLETENESS — the compact `tools[]` `toolId` set EQUALS the
|
|
23
|
+
* standard `tools[]` `toolId` set for the same principal (a compact catalog
|
|
24
|
+
* that drops a tool the standard view shows is non-conformant;
|
|
25
|
+
* authorization-scoping preserved).
|
|
26
|
+
* 5. BY-ID — `GET /v1/tools/{toolId}?view=compact` returns one schema-valid
|
|
27
|
+
* CompactToolDescriptor.
|
|
28
|
+
*
|
|
29
|
+
* Spec references:
|
|
30
|
+
* - https://github.com/openwop/openwop/blob/main/spec/v1/tool-catalog.md (§compact)
|
|
31
|
+
* - https://github.com/openwop/openwop/blob/main/RFCS/0112-compact-tool-projection.md
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import { describe, it, expect } from 'vitest';
|
|
35
|
+
import { readFileSync } from 'node:fs';
|
|
36
|
+
import { join } from 'node:path';
|
|
37
|
+
import Ajv2020 from 'ajv/dist/2020.js';
|
|
38
|
+
import addFormats from 'ajv-formats';
|
|
39
|
+
import { driver } from '../lib/driver.js';
|
|
40
|
+
import { behaviorGate } from '../lib/behavior-gate.js';
|
|
41
|
+
import { SCHEMAS_DIR } from '../lib/paths.js';
|
|
42
|
+
import {
|
|
43
|
+
readToolCatalogCap,
|
|
44
|
+
listToolsCompact,
|
|
45
|
+
COMPACT_DROPPED_FIELDS,
|
|
46
|
+
findBannedInputSchemaKeyword,
|
|
47
|
+
type CompactToolDescriptor,
|
|
48
|
+
} from '../lib/toolCatalog.js';
|
|
49
|
+
|
|
50
|
+
function loadSchema(name: string): Record<string, unknown> {
|
|
51
|
+
return JSON.parse(readFileSync(join(SCHEMAS_DIR, name), 'utf8')) as Record<string, unknown>;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Extract the `toolId` set from a `GET /v1/tools` body, tolerating both the
|
|
55
|
+
* bare-array and `{ tools: [] }` envelope standard shapes (cast-free). */
|
|
56
|
+
function toolIdSet(body: unknown): Set<string> {
|
|
57
|
+
const ids = new Set<string>();
|
|
58
|
+
const arr: unknown[] = Array.isArray(body)
|
|
59
|
+
? body
|
|
60
|
+
: body && typeof body === 'object' && Array.isArray((body as { tools?: unknown }).tools)
|
|
61
|
+
? ((body as { tools: unknown[] }).tools)
|
|
62
|
+
: [];
|
|
63
|
+
for (const t of arr) {
|
|
64
|
+
if (t && typeof t === 'object') {
|
|
65
|
+
const id = (t as { toolId?: unknown }).toolId;
|
|
66
|
+
if (typeof id === 'string') ids.add(id);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return ids;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
describe('tool-catalog-compact-projection (RFC 0112 §compact)', () => {
|
|
73
|
+
it('serves the { tools: CompactToolDescriptor[] } projection — closed shape, bounded inputSchema, same toolId set as standard', async () => {
|
|
74
|
+
const cap = await readToolCatalogCap();
|
|
75
|
+
if (!behaviorGate('openwop-tool-catalog-compact', cap?.compactView === true)) return;
|
|
76
|
+
|
|
77
|
+
const ajv = new Ajv2020({ strict: false, allErrors: true });
|
|
78
|
+
addFormats(ajv);
|
|
79
|
+
const validate = ajv.compile(loadSchema('compact-tool-descriptor.schema.json'));
|
|
80
|
+
|
|
81
|
+
// ---- Leg 1: the compact envelope (§compact) -------------------------
|
|
82
|
+
const compact = await listToolsCompact();
|
|
83
|
+
if (compact === null) return; // advertises the cap but doesn't serve the read — soft-skip the rest
|
|
84
|
+
|
|
85
|
+
for (const t of compact) {
|
|
86
|
+
// ---- Leg 2: schema validity + heavy fields dropped ----------------
|
|
87
|
+
expect(
|
|
88
|
+
validate(t),
|
|
89
|
+
driver.describe('compact-tool-descriptor.schema.json', `each CompactToolDescriptor MUST validate (${ajv.errorsText(validate.errors)})`),
|
|
90
|
+
).toBe(true);
|
|
91
|
+
for (const f of COMPACT_DROPPED_FIELDS) {
|
|
92
|
+
expect(
|
|
93
|
+
!(f in t),
|
|
94
|
+
driver.describe('tool-catalog.md §compact', `CompactToolDescriptor MUST drop the heavy field "${f}"`),
|
|
95
|
+
).toBe(true);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ---- Leg 3: the compact structural subset on inputSchema ----------
|
|
99
|
+
const input = (t as CompactToolDescriptor).inputSchema;
|
|
100
|
+
if (input !== undefined) {
|
|
101
|
+
expect(
|
|
102
|
+
input.type === 'object' && typeof input.properties === 'object' && input.properties !== null,
|
|
103
|
+
driver.describe('tool-catalog.md §compact', 'compact inputSchema MUST be top-level type:"object" with a properties map'),
|
|
104
|
+
).toBe(true);
|
|
105
|
+
// Total (any-depth), schema-aware: a nested oneOf/$ref under a property
|
|
106
|
+
// schema is exactly the verbosity the compact view exists to drop.
|
|
107
|
+
const banned = findBannedInputSchemaKeyword(input);
|
|
108
|
+
expect(
|
|
109
|
+
banned,
|
|
110
|
+
driver.describe('tool-catalog.md §compact', `compact inputSchema MUST NOT use $ref/oneOf/allOf/anyOf/not/patternProperties/dependentSchemas at any nesting depth (found "${banned ?? 'none'}")`),
|
|
111
|
+
).toBe(null);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ---- Leg 4: projection completeness vs the standard view -------------
|
|
116
|
+
const standardRes = await driver.get('/v1/tools');
|
|
117
|
+
const standardIds = toolIdSet(standardRes.json);
|
|
118
|
+
const compactIds = new Set<string>();
|
|
119
|
+
for (const t of compact) {
|
|
120
|
+
if (typeof t.toolId === 'string') compactIds.add(t.toolId);
|
|
121
|
+
}
|
|
122
|
+
const sameSet =
|
|
123
|
+
standardIds.size === compactIds.size && [...standardIds].every((id) => compactIds.has(id));
|
|
124
|
+
expect(
|
|
125
|
+
sameSet,
|
|
126
|
+
driver.describe(
|
|
127
|
+
'tool-catalog.md §compact',
|
|
128
|
+
`compact tools[] MUST carry the same toolId set as the standard view (standard=${[...standardIds].sort().join(',')} compact=${[...compactIds].sort().join(',')})`,
|
|
129
|
+
),
|
|
130
|
+
).toBe(true);
|
|
131
|
+
|
|
132
|
+
// ---- Leg 5: by-id compact round-trip --------------------------------
|
|
133
|
+
if (compact.length > 0 && typeof compact[0]!.toolId === 'string') {
|
|
134
|
+
const id = compact[0]!.toolId;
|
|
135
|
+
const one = await driver.get(`/v1/tools/${encodeURIComponent(id)}?view=compact`);
|
|
136
|
+
if (one.status === 200) {
|
|
137
|
+
expect(
|
|
138
|
+
validate(one.json),
|
|
139
|
+
driver.describe('compact-tool-descriptor.schema.json', `GET /v1/tools/{toolId}?view=compact MUST return a valid CompactToolDescriptor (${ajv.errorsText(validate.errors)})`),
|
|
140
|
+
).toBe(true);
|
|
141
|
+
const got = one.json;
|
|
142
|
+
expect(
|
|
143
|
+
got && typeof got === 'object' && (got as { toolId?: unknown }).toolId === id,
|
|
144
|
+
driver.describe('tool-catalog.md §compact', 'GET /v1/tools/{toolId}?view=compact MUST return the requested descriptor'),
|
|
145
|
+
).toBe(true);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
});
|