@openwop/openwop-conformance 1.6.0 → 1.10.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 +18 -0
- package/README.md +2 -2
- package/api/asyncapi.yaml +74 -1
- package/api/openapi.yaml +316 -0
- package/coverage.md +16 -0
- package/fixtures/conformance-run-duration-breach.json +33 -0
- package/fixtures.md +19 -0
- package/package.json +1 -1
- package/schemas/README.md +12 -0
- package/schemas/agent-inventory-response.schema.json +90 -0
- package/schemas/ai-envelope.schema.json +28 -0
- package/schemas/annotation-create.schema.json +37 -0
- package/schemas/annotation.schema.json +56 -0
- package/schemas/artifact-type-pack-manifest.schema.json +160 -0
- package/schemas/capabilities.schema.json +195 -4
- package/schemas/chat-card-pack-manifest.schema.json +158 -0
- package/schemas/envelopes/media.audio.schema.json +38 -0
- package/schemas/envelopes/media.file.schema.json +37 -0
- package/schemas/envelopes/media.image.schema.json +33 -0
- package/schemas/heartbeat-evaluated.schema.json +14 -0
- package/schemas/heartbeat-state-changed.schema.json +14 -0
- package/schemas/node-pack-manifest.schema.json +16 -1
- package/schemas/run-event-payloads.schema.json +96 -5
- package/schemas/run-event.schema.json +4 -0
- package/schemas/workflow-definition.schema.json +5 -0
- package/schemas/workspace-file-create.schema.json +20 -0
- package/schemas/workspace-file.schema.json +39 -0
- package/src/lib/agentLoop.ts +44 -0
- package/src/lib/agentRuntime.ts +45 -0
- package/src/lib/artifactTypes.ts +96 -0
- package/src/lib/cardPacks.ts +52 -0
- package/src/lib/discovery-capabilities.ts +50 -0
- package/src/lib/distillation.ts +38 -0
- package/src/lib/feedback.ts +31 -0
- package/src/lib/heartbeat.ts +31 -0
- package/src/lib/memoryAttribution.ts +48 -0
- package/src/lib/subRunAttestation.ts +35 -0
- package/src/lib/toolHooks.ts +33 -0
- package/src/scenarios/agent-loop-iteration-monotonic.test.ts +33 -0
- package/src/scenarios/agent-loop-stateful-resume.test.ts +28 -0
- package/src/scenarios/agent-loop-version5-shape.test.ts +41 -0
- package/src/scenarios/agent-loop-workspace-snapshot.test.ts +33 -0
- package/src/scenarios/agent-manifest-runtime.test.ts +85 -0
- package/src/scenarios/ai-envelope-shape.test.ts +14 -18
- package/src/scenarios/aiEnvelope.capBreached.test.ts +2 -1
- package/src/scenarios/aiEnvelope.schemaDrift.test.ts +2 -1
- package/src/scenarios/aiEnvelope.universalKinds.test.ts +2 -1
- package/src/scenarios/approval-gate-flow.test.ts +4 -6
- package/src/scenarios/artifact-schema-compile-bounded.test.ts +126 -0
- package/src/scenarios/artifact-type-pack-install.test.ts +78 -0
- package/src/scenarios/artifact-type-pack-manifest-validation.test.ts +140 -0
- package/src/scenarios/artifact-type-store-without-render.test.ts +54 -0
- package/src/scenarios/audit-log-integrity.test.ts +3 -2
- package/src/scenarios/auth-api-key-rotation.test.ts +2 -1
- package/src/scenarios/auth-mtls.test.ts +2 -1
- package/src/scenarios/auth-oauth2-client-credentials.test.ts +2 -1
- package/src/scenarios/auth-oidc-user-bearer.test.ts +2 -1
- package/src/scenarios/auth-saml-profile.test.ts +2 -1
- package/src/scenarios/auth-scim-profile.test.ts +2 -1
- package/src/scenarios/authorization-fail-closed.test.ts +2 -1
- package/src/scenarios/authorization-roles-shape.test.ts +2 -1
- package/src/scenarios/byok-auth-modes.test.ts +141 -0
- package/src/scenarios/chat-card-pack-execution.test.ts +56 -0
- package/src/scenarios/chat-card-pack-manifest-validation.test.ts +128 -0
- package/src/scenarios/commitment-fired.test.ts +83 -0
- package/src/scenarios/credential-payload-redaction.test.ts +2 -1
- package/src/scenarios/credentials-capability-shape.test.ts +2 -1
- package/src/scenarios/cross-engine-append-ordering.test.ts +2 -1
- package/src/scenarios/cross-host-ancestry-endpoint.test.ts +3 -2
- package/src/scenarios/cross-host-causation-shape.test.ts +3 -2
- package/src/scenarios/deadletter-capability-shape.test.ts +2 -1
- package/src/scenarios/deadletter-retry-exhaustion.test.ts +2 -1
- package/src/scenarios/distillation-index-roundtrip.test.ts +35 -0
- package/src/scenarios/distillation-secret-carryforward.test.ts +35 -0
- package/src/scenarios/distillation-shape.test.ts +41 -0
- package/src/scenarios/distillation-stable-archive.test.ts +37 -0
- package/src/scenarios/distillation-token-budget.test.ts +45 -0
- package/src/scenarios/envelope-completion-distinguishes-truncation.test.ts +4 -3
- package/src/scenarios/envelope-reasoning-secret-redaction.test.ts +5 -4
- package/src/scenarios/envelope-reasoning-shape.test.ts +3 -2
- package/src/scenarios/envelope-refusal-shape.test.ts +3 -2
- package/src/scenarios/envelope-rendering-hint.test.ts +95 -0
- package/src/scenarios/envelope-retry-attempted.test.ts +2 -1
- package/src/scenarios/envelope-tier-one-subset-static.test.ts +3 -2
- package/src/scenarios/exec-not-protocol-tier.test.ts +137 -0
- package/src/scenarios/experimental-tier-shape.test.ts +5 -4
- package/src/scenarios/feedback-capability-shape.test.ts +35 -0
- package/src/scenarios/feedback-correction-redaction.test.ts +35 -0
- package/src/scenarios/feedback-cross-tenant-isolation.test.ts +37 -0
- package/src/scenarios/feedback-fork-not-copied.test.ts +40 -0
- package/src/scenarios/feedback-on-terminal-run.test.ts +32 -0
- package/src/scenarios/feedback-record-and-list.test.ts +32 -0
- package/src/scenarios/feedback-unsupported-501.test.ts +32 -0
- package/src/scenarios/fs-path-traversal.test.ts +2 -1
- package/src/scenarios/heartbeat-capability-shape.test.ts +35 -0
- package/src/scenarios/heartbeat-fires-once-per-tick.test.ts +28 -0
- package/src/scenarios/heartbeat-idempotent-no-spam.test.ts +43 -0
- package/src/scenarios/heartbeat-runtime-bound.test.ts +30 -0
- package/src/scenarios/http-client-ssrf.test.ts +10 -13
- package/src/scenarios/mcp-toolcall-redaction.test.ts +3 -2
- package/src/scenarios/media-url-inline-cap.test.ts +167 -0
- package/src/scenarios/memory-attribution-emits-on-write.test.ts +54 -0
- package/src/scenarios/memory-attribution-no-content.test.ts +45 -0
- package/src/scenarios/memory-attribution-replay-stable.test.ts +60 -0
- package/src/scenarios/memory-attribution-shape.test.ts +28 -0
- package/src/scenarios/memory-attribution-tenant-scoped.test.ts +44 -0
- package/src/scenarios/memory-compaction-event-emitted.test.ts +2 -1
- package/src/scenarios/memory-compaction-provenance-tag.test.ts +2 -1
- package/src/scenarios/memory-compaction-sr1-carry-forward.test.ts +2 -1
- package/src/scenarios/memory-consolidation-idempotent.test.ts +77 -0
- package/src/scenarios/memory-consolidation-shape.test.ts +90 -0
- package/src/scenarios/model-capability-substituted.test.ts +2 -1
- package/src/scenarios/multi-agent-confidence-escalation.test.ts +5 -4
- package/src/scenarios/multi-agent-handoff-state-machine.test.ts +6 -5
- package/src/scenarios/multi-agent-memory-lifecycle.test.ts +4 -3
- package/src/scenarios/multi-region-idempotency.test.ts +10 -10
- package/src/scenarios/oauth-capability-shape.test.ts +2 -1
- package/src/scenarios/oauth-connector-redaction.test.ts +2 -1
- package/src/scenarios/pause-resume.test.ts +3 -3
- package/src/scenarios/production-backpressure.test.ts +2 -2
- package/src/scenarios/production-retention-expiry.test.ts +2 -2
- package/src/scenarios/prompt-all-four-kinds-events.test.ts +2 -1
- package/src/scenarios/prompt-composed-secret-redaction.test.ts +2 -1
- package/src/scenarios/prompt-composed-trust-marker.test.ts +2 -1
- package/src/scenarios/prompt-end-to-end-events.test.ts +2 -1
- package/src/scenarios/prompt-list-and-fetch.test.ts +2 -1
- package/src/scenarios/prompt-mutable-lifecycle.test.ts +2 -1
- package/src/scenarios/prompt-mutation-workspace-membership-enforced.test.ts +2 -1
- package/src/scenarios/prompt-pack-install.test.ts +2 -1
- package/src/scenarios/prompt-read-workspace-membership-enforced.test.ts +2 -1
- package/src/scenarios/prompt-render-deterministic.test.ts +2 -1
- package/src/scenarios/prompt-resolution-chain-agent-intrinsic.test.ts +2 -1
- package/src/scenarios/prompt-resolution-chain-fallback-cascade.test.ts +2 -1
- package/src/scenarios/prompt-resolution-chain-node-wins.test.ts +2 -1
- package/src/scenarios/prompt-template-shape.test.ts +2 -1
- package/src/scenarios/provider-usage.test.ts +2 -1
- package/src/scenarios/redaction.test.ts +4 -1
- package/src/scenarios/replay-divergence-at-refusal.test.ts +4 -3
- package/src/scenarios/replay-fork-arbitrary.test.ts +3 -1
- package/src/scenarios/replay-llm-cache-key-portable.test.ts +2 -1
- package/src/scenarios/replayDeterminism.test.ts +3 -1
- package/src/scenarios/run-execution-bounds-shape.test.ts +133 -0
- package/src/scenarios/sandbox-memory-cap.test.ts +2 -1
- package/src/scenarios/sandbox-mvp-behavior.test.ts +2 -1
- package/src/scenarios/sandbox-no-host-fs-escape.test.ts +2 -1
- package/src/scenarios/sandbox-timeout-cap.test.ts +2 -1
- package/src/scenarios/scheduling-capability-shape.test.ts +2 -1
- package/src/scenarios/scheduling-cron-fires-once.test.ts +2 -1
- package/src/scenarios/secret-leakage-otel-attribute.test.ts +7 -6
- package/src/scenarios/spec-corpus-validity.test.ts +4 -1
- package/src/scenarios/subrun-approval-fail-closed.test.ts +33 -0
- package/src/scenarios/subrun-approval-gate.test.ts +35 -0
- package/src/scenarios/subrun-attestation-shape.test.ts +30 -0
- package/src/scenarios/subrun-checksum-stable.test.ts +43 -0
- package/src/scenarios/tool-hooks-authorization-fail-closed.test.ts +39 -0
- package/src/scenarios/tool-hooks-content-free.test.ts +40 -0
- package/src/scenarios/tool-hooks-rate-limit.test.ts +32 -0
- package/src/scenarios/tool-hooks-secret-redaction.test.ts +34 -0
- package/src/scenarios/tool-hooks-shape.test.ts +34 -0
- package/src/scenarios/wasm-pack-abi-version-rejection.test.ts +3 -10
- package/src/scenarios/wasm-pack-invoke-completed.test.ts +2 -2
- package/src/scenarios/wasm-pack-invoke-suspended.test.ts +2 -2
- package/src/scenarios/wasm-pack-load.test.ts +2 -2
- package/src/scenarios/wasm-pack-memory-cap.test.ts +3 -6
- package/src/scenarios/wasm-pack-replay-determinism.test.ts +2 -2
- package/src/scenarios/workflow-primary-output-annotation.test.ts +142 -0
- package/src/scenarios/workspace-behavior.test.ts +134 -0
- package/src/scenarios/workspace-capability-shape.test.ts +73 -0
- package/src/scenarios/workspace-cross-tenant-isolation.test.ts +84 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* memory-attribution-replay-stable — RFC 0057 §D. `memory.written` is an
|
|
3
|
+
* immutable recorded fact: a `replay`-mode fork MUST NOT mint a new
|
|
4
|
+
* `memoryId` for a write the source run already recorded. This asserts the
|
|
5
|
+
* "MUST NOT regenerate" half — every `memory.written` on a replayed run
|
|
6
|
+
* reuses a `memoryId` the source run recorded (a compliant host that
|
|
7
|
+
* suppresses re-mint on replay satisfies this vacuously with zero events).
|
|
8
|
+
*
|
|
9
|
+
* Gated on `capabilities.memory.attribution.emitsWriteEvents`; soft-skips
|
|
10
|
+
* when unadvertised, when the seeded run wrote no memory, or when the host
|
|
11
|
+
* doesn't support `:fork` in `replay` mode.
|
|
12
|
+
*
|
|
13
|
+
* @see RFCS/0057-memory-write-attribution-event.md §D
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { describe, it, expect } from 'vitest';
|
|
17
|
+
import { driver } from '../lib/driver.js';
|
|
18
|
+
import { pollUntilTerminal } from '../lib/polling.js';
|
|
19
|
+
import { readMemoryAttributionCap, emitsWriteEvents, seedRun, memoryWrittenEvents } from '../lib/memoryAttribution.js';
|
|
20
|
+
|
|
21
|
+
function memoryIdOf(payload: Record<string, unknown> | undefined): string | null {
|
|
22
|
+
const id = (payload ?? {})['memoryId'];
|
|
23
|
+
return typeof id === 'string' ? id : null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
describe('memory-attribution-replay-stable (RFC 0057 §D)', () => {
|
|
27
|
+
it('a replay-mode fork introduces no memory.written with a new memoryId', async () => {
|
|
28
|
+
const cap = await readMemoryAttributionCap();
|
|
29
|
+
if (!emitsWriteEvents(cap)) return;
|
|
30
|
+
const runId = await seedRun('mem-attr-replay');
|
|
31
|
+
if (!runId) return;
|
|
32
|
+
try {
|
|
33
|
+
await pollUntilTerminal(runId, { timeoutMs: 10_000 });
|
|
34
|
+
} catch {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const original = await memoryWrittenEvents(runId);
|
|
38
|
+
if (original.length === 0) return; // run wrote no memory — nothing to test
|
|
39
|
+
const recordedIds = new Set(original.map((e) => memoryIdOf(e.payload)).filter((x): x is string => x !== null));
|
|
40
|
+
|
|
41
|
+
const fork = await driver.post(`/v1/runs/${runId}:fork`, { fromSeq: 0, mode: 'replay' });
|
|
42
|
+
if (fork.status !== 200 && fork.status !== 201) return; // replay fork unsupported — soft-skip
|
|
43
|
+
const forkId = (fork.json as { runId?: string } | undefined)?.runId;
|
|
44
|
+
if (!forkId) return;
|
|
45
|
+
try {
|
|
46
|
+
await pollUntilTerminal(forkId, { timeoutMs: 10_000 });
|
|
47
|
+
} catch {
|
|
48
|
+
/* still assert on whatever the replay emitted */
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const replayed = await memoryWrittenEvents(forkId);
|
|
52
|
+
for (const e of replayed) {
|
|
53
|
+
const id = memoryIdOf(e.payload);
|
|
54
|
+
expect(
|
|
55
|
+
id !== null && recordedIds.has(id),
|
|
56
|
+
driver.describe('RFC 0057 §D', 'a replay MUST NOT regenerate memoryId — every replayed memory.written reuses a recorded id'),
|
|
57
|
+
).toBe(true);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* memory-attribution-shape — RFC 0057 §A. The `capabilities.memory.attribution`
|
|
3
|
+
* advertisement block is either absent or a well-formed object.
|
|
4
|
+
*
|
|
5
|
+
* Status: ACTIVE (advertisement-shape; always runs). Behavioral coverage lives
|
|
6
|
+
* in the sibling memory-attribution-*.test.ts scenarios, gated on
|
|
7
|
+
* `capabilities.memory.attribution.emitsWriteEvents`.
|
|
8
|
+
*
|
|
9
|
+
* @see RFCS/0057-memory-write-attribution-event.md §A
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { describe, it, expect } from 'vitest';
|
|
13
|
+
import { driver } from '../lib/driver.js';
|
|
14
|
+
import { readMemoryAttributionCap } from '../lib/memoryAttribution.js';
|
|
15
|
+
|
|
16
|
+
describe('memory-attribution-shape: advertisement (RFC 0057 §A)', () => {
|
|
17
|
+
it('capabilities.memory.attribution is absent or a well-formed object', async () => {
|
|
18
|
+
const cap = await readMemoryAttributionCap();
|
|
19
|
+
if (cap === null) return; // not advertised — valid
|
|
20
|
+
expect(
|
|
21
|
+
cap.supported,
|
|
22
|
+
driver.describe('capabilities.schema.json §memory.attribution', 'memory.attribution.supported MUST be the literal true when the block is present'),
|
|
23
|
+
).toBe(true);
|
|
24
|
+
if (cap.emitsWriteEvents !== undefined) {
|
|
25
|
+
expect(typeof cap.emitsWriteEvents).toBe('boolean');
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* memory-attribution-tenant-scoped — RFC 0057 §C + SECURITY/invariants.yaml
|
|
3
|
+
* `memory-attribution-tenant-scoped`. A run's `memory.written` events appear
|
|
4
|
+
* only on that run's stream (mirrors CTI-1). The full cross-tenant proof
|
|
5
|
+
* (tenant B cannot read tenant A's run stream) needs a multi-tenant auth seam
|
|
6
|
+
* not standardized for this surface — that half soft-skips, mirroring
|
|
7
|
+
* `feedback-cross-tenant-isolation`.
|
|
8
|
+
*
|
|
9
|
+
* Gated on `capabilities.memory.attribution.emitsWriteEvents`.
|
|
10
|
+
*
|
|
11
|
+
* @see RFCS/0057-memory-write-attribution-event.md §C
|
|
12
|
+
* @see SECURITY/invariants.yaml — memory-attribution-tenant-scoped
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { describe, it, expect } from 'vitest';
|
|
16
|
+
import { driver } from '../lib/driver.js';
|
|
17
|
+
import { pollUntilTerminal } from '../lib/polling.js';
|
|
18
|
+
import { readMemoryAttributionCap, emitsWriteEvents, seedRun, memoryWrittenEvents } from '../lib/memoryAttribution.js';
|
|
19
|
+
|
|
20
|
+
describe('memory-attribution-tenant-scoped (RFC 0057 §C)', () => {
|
|
21
|
+
it("a run's memory.written events appear only on that run's stream", async () => {
|
|
22
|
+
const cap = await readMemoryAttributionCap();
|
|
23
|
+
if (!emitsWriteEvents(cap)) return;
|
|
24
|
+
const runId = await seedRun('mem-attr-cti');
|
|
25
|
+
if (!runId) return;
|
|
26
|
+
try {
|
|
27
|
+
await pollUntilTerminal(runId, { timeoutMs: 10_000 });
|
|
28
|
+
} catch {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const events = await memoryWrittenEvents(runId);
|
|
32
|
+
if (events.length === 0) return;
|
|
33
|
+
// Every memory.written we read came from THIS run's /events stream; if the
|
|
34
|
+
// host echoes a runId in the event it MUST be this run's (no cross-run leak).
|
|
35
|
+
for (const e of events) {
|
|
36
|
+
if (typeof e.runId === 'string') {
|
|
37
|
+
expect(
|
|
38
|
+
e.runId,
|
|
39
|
+
driver.describe('RFC 0057 §C', "a memory.written event MUST belong to its own run's stream (CTI-1)"),
|
|
40
|
+
).toBe(runId);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
});
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
|
|
27
27
|
import { describe, it, expect } from 'vitest';
|
|
28
28
|
import { driver } from '../lib/driver.js';
|
|
29
|
+
import { capabilityFamily } from '../lib/discovery-capabilities.js';
|
|
29
30
|
|
|
30
31
|
const MEMORY_REF = 'mem_tenant:default_agent:conformance-rfc0012-event_longTerm';
|
|
31
32
|
|
|
@@ -35,7 +36,7 @@ interface MemoryCaps {
|
|
|
35
36
|
|
|
36
37
|
async function isCompactionAdvertised(): Promise<boolean> {
|
|
37
38
|
const disco = await driver.get('/.well-known/openwop');
|
|
38
|
-
const memory = (disco.json
|
|
39
|
+
const memory = capabilityFamily<MemoryCaps>(disco.json, 'memory');
|
|
39
40
|
return memory?.compaction?.supported === true;
|
|
40
41
|
}
|
|
41
42
|
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
|
|
21
21
|
import { describe, it, expect } from 'vitest';
|
|
22
22
|
import { driver } from '../lib/driver.js';
|
|
23
|
+
import { capabilityFamily } from '../lib/discovery-capabilities.js';
|
|
23
24
|
|
|
24
25
|
const MEMORY_REF = 'mem_tenant:default_agent:conformance-rfc0012-tag_longTerm';
|
|
25
26
|
const COMPACTED_FROM_RE = /^compacted-from:[^\s:][^\s]*$/;
|
|
@@ -34,7 +35,7 @@ interface MemoryListResponse {
|
|
|
34
35
|
|
|
35
36
|
async function isCompactionAdvertised(): Promise<boolean> {
|
|
36
37
|
const disco = await driver.get('/.well-known/openwop');
|
|
37
|
-
const memory = (disco.json
|
|
38
|
+
const memory = capabilityFamily<MemoryCaps>(disco.json, 'memory');
|
|
38
39
|
return memory?.compaction?.supported === true;
|
|
39
40
|
}
|
|
40
41
|
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
|
|
29
29
|
import { describe, it, expect } from 'vitest';
|
|
30
30
|
import { driver } from '../lib/driver.js';
|
|
31
|
+
import { capabilityFamily } from '../lib/discovery-capabilities.js';
|
|
31
32
|
|
|
32
33
|
const MEMORY_REF = 'mem_tenant:default_agent:conformance-rfc0012-sr1_longTerm';
|
|
33
34
|
|
|
@@ -37,7 +38,7 @@ interface MemoryCaps {
|
|
|
37
38
|
|
|
38
39
|
async function isCompactionAdvertised(): Promise<boolean> {
|
|
39
40
|
const disco = await driver.get('/.well-known/openwop');
|
|
40
|
-
const memory = (disco.json
|
|
41
|
+
const memory = capabilityFamily<MemoryCaps>(disco.json, 'memory');
|
|
41
42
|
return memory?.compaction?.supported === true;
|
|
42
43
|
}
|
|
43
44
|
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Background memory consolidation — idempotence + SR-1 carry-forward
|
|
3
|
+
* (RFC 0068, `Draft`).
|
|
4
|
+
*
|
|
5
|
+
* Gated on `capabilities.agents.memoryConsolidation.supported`. Drives the
|
|
6
|
+
* documented host seam `POST /v1/host/sample/memory/consolidate` (staged
|
|
7
|
+
* per the RFC 0027 §G precedent — soft-skips on 404/501 until a reference
|
|
8
|
+
* host wires it). Asserts:
|
|
9
|
+
* - a consolidation pass emits `agent.memory.consolidated` with
|
|
10
|
+
* `outputCount <= inputCount` (RFC 0068 §D);
|
|
11
|
+
* - a second pass over the unchanged corpus is a no-op
|
|
12
|
+
* (`inputCount == outputCount`) — the idempotence MUST that bounds
|
|
13
|
+
* runaway consolidation;
|
|
14
|
+
* - SR-1 carry-forward — a redacted secret in a source entry stays
|
|
15
|
+
* redacted in a consolidated entry.
|
|
16
|
+
*
|
|
17
|
+
* Hosts that omit the capability skip cleanly.
|
|
18
|
+
*
|
|
19
|
+
* Spec references:
|
|
20
|
+
* - https://github.com/openwop/openwop/blob/main/spec/v1/agent-memory.md §"Background consolidation"
|
|
21
|
+
* - https://github.com/openwop/openwop/blob/main/RFCS/0068-memory-consolidation-and-standing-commitments.md
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { describe, it, expect } from 'vitest';
|
|
25
|
+
import { driver } from '../lib/driver.js';
|
|
26
|
+
|
|
27
|
+
interface ConsolidationCaps {
|
|
28
|
+
agents?: { memoryConsolidation?: { supported?: boolean } };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface ConsolidateResult {
|
|
32
|
+
event?: { inputCount?: number; outputCount?: number };
|
|
33
|
+
secretLeaked?: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function consolidationSupported(): Promise<boolean> {
|
|
37
|
+
const res = await driver.get('/.well-known/openwop', { authenticated: false });
|
|
38
|
+
if (res.status !== 200) return false;
|
|
39
|
+
return Boolean((res.json as ConsolidationCaps).agents?.memoryConsolidation?.supported);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
describe('memory-consolidation-idempotent: pass contract (RFC 0068 §D, capability-gated)', () => {
|
|
43
|
+
it('a consolidation pass reduces or holds entry count and is idempotent on a stable corpus', async () => {
|
|
44
|
+
if (!(await consolidationSupported())) return; // capability absent — gated skip
|
|
45
|
+
|
|
46
|
+
const first = await driver.post('/v1/host/sample/memory/consolidate', {
|
|
47
|
+
memoryRef: 'mem://conformance/consolidation',
|
|
48
|
+
includeSecretCanary: true,
|
|
49
|
+
});
|
|
50
|
+
if (first.status === 404 || first.status === 501) return; // seam not wired — soft-skip
|
|
51
|
+
|
|
52
|
+
expect(first.status, driver.describe('RFC 0068 §D', 'an advertised consolidation seam MUST succeed')).toBe(200);
|
|
53
|
+
const r1 = first.json as ConsolidateResult;
|
|
54
|
+
const in1 = r1.event?.inputCount ?? 0;
|
|
55
|
+
const out1 = r1.event?.outputCount ?? 0;
|
|
56
|
+
expect(out1, driver.describe('RFC 0068 §D.1', 'outputCount MUST be <= inputCount for a merge/dedup pass')).toBeLessThanOrEqual(in1);
|
|
57
|
+
|
|
58
|
+
// §D.2 — a second pass over the unchanged corpus is a no-op.
|
|
59
|
+
const second = await driver.post('/v1/host/sample/memory/consolidate', {
|
|
60
|
+
memoryRef: 'mem://conformance/consolidation',
|
|
61
|
+
});
|
|
62
|
+
if (second.status === 404 || second.status === 501) return;
|
|
63
|
+
const r2 = second.json as ConsolidateResult;
|
|
64
|
+
expect(
|
|
65
|
+
r2.event?.inputCount,
|
|
66
|
+
driver.describe('RFC 0068 §D.2', 'a second pass over an unchanged corpus MUST be a no-op (inputCount == outputCount)'),
|
|
67
|
+
).toBe(r2.event?.outputCount);
|
|
68
|
+
|
|
69
|
+
// §D.3 — SR-1 carry-forward: a redacted secret stays redacted in the consolidated entry.
|
|
70
|
+
if (typeof r1.secretLeaked === 'boolean') {
|
|
71
|
+
expect(
|
|
72
|
+
r1.secretLeaked,
|
|
73
|
+
driver.describe('RFC 0068 §D.3 / agent-memory.md §SR-1', 'a redacted secret MUST NOT re-appear in a consolidated entry'),
|
|
74
|
+
).toBe(false);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
});
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memory-consolidation + commitment event shapes (RFC 0068, `Draft`).
|
|
3
|
+
*
|
|
4
|
+
* Always-on, server-free schema-shape probe. Verifies that:
|
|
5
|
+
* - `capabilities.agents.memoryConsolidation` + `agents.commitments`
|
|
6
|
+
* sub-blocks are declared on the capabilities schema.
|
|
7
|
+
* - the `agent.memory.consolidated` + `commitment.fired` payload $defs
|
|
8
|
+
* validate conforming payloads and reject malformed ones (a
|
|
9
|
+
* `commitment.fired` missing `memoryRef` is rejected — a commitment
|
|
10
|
+
* with no memory provenance is not an *inferred* commitment).
|
|
11
|
+
* - both event names appear in the RunEventType enum.
|
|
12
|
+
*
|
|
13
|
+
* Distinct from RFC 0062 distillation (`memory.compacted`): consolidation
|
|
14
|
+
* reconciles long-term memory; this scenario asserts the new event
|
|
15
|
+
* contract, not host behavior.
|
|
16
|
+
*
|
|
17
|
+
* Spec references:
|
|
18
|
+
* - https://github.com/openwop/openwop/blob/main/spec/v1/agent-memory.md §"Background consolidation"
|
|
19
|
+
* - https://github.com/openwop/openwop/blob/main/spec/v1/agent-memory.md §"Inferred commitments"
|
|
20
|
+
* - https://github.com/openwop/openwop/blob/main/RFCS/0068-memory-consolidation-and-standing-commitments.md
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { describe, it, expect } from 'vitest';
|
|
24
|
+
import { readFileSync } from 'node:fs';
|
|
25
|
+
import { join } from 'node:path';
|
|
26
|
+
import Ajv2020 from 'ajv/dist/2020.js';
|
|
27
|
+
import addFormats from 'ajv-formats';
|
|
28
|
+
import { SCHEMAS_DIR } from '../lib/paths.js';
|
|
29
|
+
|
|
30
|
+
/** Server-free assertion-message helper (mirrors driver.describe's "spec — requirement" shape without requiring OPENWOP_BASE_URL). */
|
|
31
|
+
const why = (specRef: string, requirement: string): string => `${specRef} — ${requirement}`;
|
|
32
|
+
|
|
33
|
+
function loadSchema(name: string): Record<string, unknown> {
|
|
34
|
+
return JSON.parse(readFileSync(join(SCHEMAS_DIR, name), 'utf8')) as Record<string, unknown>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
describe('memory-consolidation-shape: capability advertisement (RFC 0068, server-free)', () => {
|
|
38
|
+
it('the capabilities schema declares agents.memoryConsolidation + agents.commitments', () => {
|
|
39
|
+
const caps = loadSchema('capabilities.schema.json');
|
|
40
|
+
const agents = (caps.properties as Record<string, { properties?: Record<string, unknown> }>).agents;
|
|
41
|
+
expect(
|
|
42
|
+
agents?.properties?.memoryConsolidation,
|
|
43
|
+
why('capabilities.md §agents', 'agents.memoryConsolidation MUST be declared'),
|
|
44
|
+
).toBeDefined();
|
|
45
|
+
expect(
|
|
46
|
+
agents?.properties?.commitments,
|
|
47
|
+
why('capabilities.md §agents', 'agents.commitments MUST be declared'),
|
|
48
|
+
).toBeDefined();
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe('memory-consolidation-shape: event payloads (RFC 0068, server-free)', () => {
|
|
53
|
+
const payloads = loadSchema('run-event-payloads.schema.json');
|
|
54
|
+
const ajv = new Ajv2020({ strict: false, allErrors: true });
|
|
55
|
+
addFormats(ajv);
|
|
56
|
+
ajv.addSchema(payloads, 'payloads');
|
|
57
|
+
|
|
58
|
+
const consolidated = ajv.getSchema('payloads#/$defs/agentMemoryConsolidated');
|
|
59
|
+
const fired = ajv.getSchema('payloads#/$defs/commitmentFired');
|
|
60
|
+
|
|
61
|
+
it('agent.memory.consolidated validates a content-free pass summary', () => {
|
|
62
|
+
expect(consolidated, 'the agentMemoryConsolidated $def MUST exist').toBeTruthy();
|
|
63
|
+
expect(
|
|
64
|
+
consolidated!({ memoryRef: 'mem://a/agent-1', inputCount: 240, outputCount: 201, trigger: 'host-managed' }),
|
|
65
|
+
why('RFC 0068 §B', 'a conforming agent.memory.consolidated payload MUST validate'),
|
|
66
|
+
).toBe(true);
|
|
67
|
+
// Negative: outputCount as string fails the integer type.
|
|
68
|
+
expect(consolidated!({ memoryRef: 'mem://a/agent-1', inputCount: 1, outputCount: 'x' })).toBe(false);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('commitment.fired validates a content-free fire record and requires memoryRef', () => {
|
|
72
|
+
expect(fired, 'the commitmentFired $def MUST exist').toBeTruthy();
|
|
73
|
+
expect(
|
|
74
|
+
fired!({ commitmentId: 'cmt-1', memoryRef: 'mem://a/agent-1', condition: 'predicate', enqueuedRunId: 'run-1' }),
|
|
75
|
+
why('RFC 0068 §C', 'a conforming commitment.fired payload MUST validate'),
|
|
76
|
+
).toBe(true);
|
|
77
|
+
// Negative: missing memoryRef — a commitment with no provenance breaks CTI-1 binding.
|
|
78
|
+
expect(
|
|
79
|
+
fired!({ commitmentId: 'cmt-1', condition: 'time' }),
|
|
80
|
+
why('RFC 0068 §C', 'commitment.fired without memoryRef MUST be rejected'),
|
|
81
|
+
).toBe(false);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('both event names appear in the RunEventType enum', () => {
|
|
85
|
+
const runEvent = loadSchema('run-event.schema.json');
|
|
86
|
+
const enumVals = (runEvent.$defs as Record<string, { enum?: string[] }>).RunEventType?.enum ?? [];
|
|
87
|
+
expect(enumVals).toContain('agent.memory.consolidated');
|
|
88
|
+
expect(enumVals).toContain('commitment.fired');
|
|
89
|
+
});
|
|
90
|
+
});
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
|
|
18
18
|
import { describe, it, expect } from 'vitest';
|
|
19
19
|
import { driver } from '../lib/driver.js';
|
|
20
|
+
import { capabilityFamily } from '../lib/discovery-capabilities.js';
|
|
20
21
|
|
|
21
22
|
const HTTP_SKIP = !process.env.OPENWOP_BASE_URL;
|
|
22
23
|
|
|
@@ -64,7 +65,7 @@ describe.skipIf(HTTP_SKIP)('model-capability-substituted: advertisement shape (R
|
|
|
64
65
|
it('capabilities.modelCapabilities (when present) conforms to RFC 0031 §E', async () => {
|
|
65
66
|
const d = await readDiscovery();
|
|
66
67
|
if (d === null) return;
|
|
67
|
-
const mc = d
|
|
68
|
+
const mc = capabilityFamily(d, 'modelCapabilities');
|
|
68
69
|
if (mc === undefined) return;
|
|
69
70
|
expect(
|
|
70
71
|
typeof mc.supported,
|
|
@@ -50,6 +50,7 @@ import { describe, it, expect } from 'vitest';
|
|
|
50
50
|
import { driver } from '../lib/driver.js';
|
|
51
51
|
import { isFixtureAdvertised } from '../lib/fixtures.js';
|
|
52
52
|
import { pollUntilTerminal } from '../lib/polling.js';
|
|
53
|
+
import { capabilityFamily } from '../lib/discovery-capabilities.js';
|
|
53
54
|
|
|
54
55
|
const HTTP_SKIP = !process.env.OPENWOP_BASE_URL;
|
|
55
56
|
const FIXTURE = 'conformance-multi-agent-confidence-escalation';
|
|
@@ -84,7 +85,7 @@ describe.skipIf(HTTP_SKIP)('multi-agent-confidence-escalation: capability shape
|
|
|
84
85
|
it('confidenceEscalationFloor (when advertised) MUST be in [0.5, 1.0]', async () => {
|
|
85
86
|
const d = await readDiscovery();
|
|
86
87
|
if (d === null) return;
|
|
87
|
-
const em = d
|
|
88
|
+
const em = capabilityFamily<{ executionModel?: { [k: string]: unknown; crossHostCausation?: Record<string, unknown>; replayDeterminism?: Record<string, unknown> } }>(d, 'multiAgent')?.executionModel;
|
|
88
89
|
if (em === undefined) return;
|
|
89
90
|
const floor = em.confidenceEscalationFloor;
|
|
90
91
|
if (floor === undefined) return;
|
|
@@ -101,8 +102,8 @@ describe.skipIf(HTTP_SKIP)('multi-agent-confidence-escalation: capability shape
|
|
|
101
102
|
describe.skipIf(BEHAVIORAL_SKIP)('multi-agent-confidence-escalation: behavioral (RFC 0039 §A)', () => {
|
|
102
103
|
it('happy-path: low-confidence decision → confidence-escalated event + clarification interrupt + zero dispatch events', async () => {
|
|
103
104
|
const d = await readDiscovery();
|
|
104
|
-
const supported = d
|
|
105
|
-
const versionRaw = d
|
|
105
|
+
const supported = capabilityFamily<{ executionModel?: { [k: string]: unknown; crossHostCausation?: Record<string, unknown>; replayDeterminism?: Record<string, unknown> } }>(d, 'multiAgent')?.executionModel?.supported === true;
|
|
106
|
+
const versionRaw = capabilityFamily<{ executionModel?: { [k: string]: unknown; crossHostCausation?: Record<string, unknown>; replayDeterminism?: Record<string, unknown> } }>(d, 'multiAgent')?.executionModel?.version;
|
|
106
107
|
const version = typeof versionRaw === 'number' ? versionRaw : 0;
|
|
107
108
|
if (!supported || version < 2) return; // soft-skip — `version: 1` hosts pass via this absence
|
|
108
109
|
|
|
@@ -125,7 +126,7 @@ describe.skipIf(BEHAVIORAL_SKIP)('multi-agent-confidence-escalation: behavioral
|
|
|
125
126
|
// status — the host's own interrupt.md mapping determines the suffix).
|
|
126
127
|
// When the host does NOT advertise the field, fall back to the canonical
|
|
127
128
|
// either-status check.
|
|
128
|
-
const advertisedKind = d
|
|
129
|
+
const advertisedKind = capabilityFamily<{ executionModel?: { [k: string]: unknown; crossHostCausation?: Record<string, unknown>; replayDeterminism?: Record<string, unknown> } }>(d, 'multiAgent')?.executionModel?.confidenceEscalationInterruptKind;
|
|
129
130
|
const isVendorKind = typeof advertisedKind === 'string' && /^x-host-[a-z][a-z0-9-]*-[a-z][a-z0-9-]*$/.test(advertisedKind);
|
|
130
131
|
const isCanonicalKind = advertisedKind === 'clarification' || advertisedKind === 'approval';
|
|
131
132
|
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* Asserts (Phase 1 — execution-loop + handoff state machine per spec/v1/multi-agent-execution.md):
|
|
11
11
|
*
|
|
12
12
|
* 1. Advertisement shape: when capabilities.multiAgent.executionModel.supported
|
|
13
|
-
* is present, version MUST be integer in [1,
|
|
13
|
+
* is present, version MUST be integer in [1, 5]; supported MUST be boolean.
|
|
14
14
|
*
|
|
15
15
|
* 2. Behavioral (gated on supported: true + fixture availability): a
|
|
16
16
|
* supervisor → next-worker → child-completed run emits the 4 expected
|
|
@@ -34,6 +34,7 @@ import { describe, it, expect } from 'vitest';
|
|
|
34
34
|
import { driver } from '../lib/driver.js';
|
|
35
35
|
import { isFixtureAdvertised } from '../lib/fixtures.js';
|
|
36
36
|
import { pollUntilTerminal } from '../lib/polling.js';
|
|
37
|
+
import { capabilityFamily } from '../lib/discovery-capabilities.js';
|
|
37
38
|
|
|
38
39
|
const HTTP_SKIP = !process.env.OPENWOP_BASE_URL;
|
|
39
40
|
|
|
@@ -62,7 +63,7 @@ describe.skipIf(HTTP_SKIP)('multi-agent-handoff-state-machine: advertisement sha
|
|
|
62
63
|
it('capabilities.multiAgent.executionModel (when present) conforms to RFC 0037 §C', async () => {
|
|
63
64
|
const d = await readDiscovery();
|
|
64
65
|
if (d === null) return; // discovery unavailable — skip
|
|
65
|
-
const executionModel = d
|
|
66
|
+
const executionModel = capabilityFamily<{ executionModel?: { [k: string]: unknown; crossHostCausation?: Record<string, unknown>; replayDeterminism?: Record<string, unknown> } }>(d, 'multiAgent')?.executionModel;
|
|
66
67
|
if (executionModel === undefined) return; // host doesn't advertise — soft-skip
|
|
67
68
|
expect(
|
|
68
69
|
typeof executionModel.supported,
|
|
@@ -80,10 +81,10 @@ describe.skipIf(HTTP_SKIP)('multi-agent-handoff-state-machine: advertisement sha
|
|
|
80
81
|
).toBe('number');
|
|
81
82
|
const v = executionModel.version as number;
|
|
82
83
|
expect(
|
|
83
|
-
Number.isInteger(v) && v >= 1 && v <=
|
|
84
|
+
Number.isInteger(v) && v >= 1 && v <= 5,
|
|
84
85
|
driver.describe(
|
|
85
86
|
'RFCS/0037-multi-agent-execution-model.md §C',
|
|
86
|
-
'version MUST be an integer in [1,
|
|
87
|
+
'version MUST be an integer in [1, 5] (1 = Phase 1 only; Phases 2-5 lift the ceiling additively — Phase 5 = RFC 0061 stateful agent-loop lifecycle, matching `capabilities.schema.json` §multiAgent.executionModel.version maximum)',
|
|
87
88
|
),
|
|
88
89
|
).toBe(true);
|
|
89
90
|
});
|
|
@@ -104,7 +105,7 @@ const BEHAVIORAL_SKIP = HTTP_SKIP || !isFixtureAdvertised(PARENT_FIXTURE) || !is
|
|
|
104
105
|
describe.skipIf(BEHAVIORAL_SKIP)('multi-agent-handoff-state-machine: behavioral 4-event causation chain (RFC 0037 §"Handoff state machine")', () => {
|
|
105
106
|
it('happy-path: dispatch.began → dispatch.succeeded → child.completed → output.harvested fire in causation order', async () => {
|
|
106
107
|
const d = await readDiscovery();
|
|
107
|
-
const advertised = d
|
|
108
|
+
const advertised = capabilityFamily<{ executionModel?: { [k: string]: unknown; crossHostCausation?: Record<string, unknown>; replayDeterminism?: Record<string, unknown> } }>(d, 'multiAgent')?.executionModel?.supported === true;
|
|
108
109
|
if (!advertised) return; // soft-skip — host honest about not implementing
|
|
109
110
|
|
|
110
111
|
const create = await driver.post('/v1/runs', { workflowId: PARENT_FIXTURE });
|
|
@@ -48,6 +48,7 @@
|
|
|
48
48
|
|
|
49
49
|
import { describe, it, expect } from 'vitest';
|
|
50
50
|
import { driver } from '../lib/driver.js';
|
|
51
|
+
import { capabilityFamily } from '../lib/discovery-capabilities.js';
|
|
51
52
|
|
|
52
53
|
const HTTP_SKIP = !process.env.OPENWOP_BASE_URL;
|
|
53
54
|
|
|
@@ -81,7 +82,7 @@ describe.skipIf(HTTP_SKIP)('multi-agent-memory-lifecycle: advertisement shape (R
|
|
|
81
82
|
ctx.skip();
|
|
82
83
|
return;
|
|
83
84
|
}
|
|
84
|
-
const ccmc = d
|
|
85
|
+
const ccmc = capabilityFamily<{ executionModel?: { [k: string]: unknown; crossHostCausation?: Record<string, unknown>; replayDeterminism?: Record<string, unknown> } }>(d, 'multiAgent')?.executionModel?.crossChildMemoryConcurrency;
|
|
85
86
|
if (ccmc === undefined) {
|
|
86
87
|
ctx.skip(); // optional advertisement — host hasn't opted in
|
|
87
88
|
return;
|
|
@@ -135,8 +136,8 @@ describe.skipIf(HTTP_SKIP)('multi-agent-memory-lifecycle: behavioral (RFC 0039
|
|
|
135
136
|
ctx.skip();
|
|
136
137
|
return;
|
|
137
138
|
}
|
|
138
|
-
const v = d
|
|
139
|
-
const memorySupported = d
|
|
139
|
+
const v = capabilityFamily<{ executionModel?: { [k: string]: unknown; crossHostCausation?: Record<string, unknown>; replayDeterminism?: Record<string, unknown> } }>(d, 'multiAgent')?.executionModel?.version;
|
|
140
|
+
const memorySupported = capabilityFamily<{ supported?: unknown }>(d, 'memory')?.supported;
|
|
140
141
|
const phase2OrLater = typeof v === 'number' && v >= 2;
|
|
141
142
|
const expiredRunId = process.env.OPENWOP_TEST_EXPIRED_REPLAY_RUN_ID;
|
|
142
143
|
if (!phase2OrLater || memorySupported !== true || !expiredRunId) {
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
|
|
21
21
|
import { describe, it, expect } from 'vitest';
|
|
22
22
|
import { driver } from '../lib/driver.js';
|
|
23
|
+
import { capabilityFamily } from '../lib/discovery-capabilities.js';
|
|
23
24
|
|
|
24
25
|
const ALLOWED = new Set(['single-region', 'best-effort', 'strict']);
|
|
25
26
|
const REQUIRED_METRICS_WHEN_MULTI_REGION = [
|
|
@@ -40,9 +41,7 @@ interface ObservabilityCaps {
|
|
|
40
41
|
describe('multi-region-idempotency: capability shape', () => {
|
|
41
42
|
it('idempotency.crossRegion (when advertised) MUST be one of the closed enum', async () => {
|
|
42
43
|
const disco = await driver.get('/.well-known/openwop');
|
|
43
|
-
const idem =
|
|
44
|
-
(disco.json as { capabilities?: { idempotency?: IdempotencyCaps } }).capabilities
|
|
45
|
-
?.idempotency;
|
|
44
|
+
const idem = capabilityFamily<IdempotencyCaps>(disco.json, 'idempotency');
|
|
46
45
|
|
|
47
46
|
if (!idem || idem.crossRegion === undefined) {
|
|
48
47
|
// eslint-disable-next-line no-console
|
|
@@ -67,16 +66,16 @@ describe('multi-region-idempotency: capability shape', () => {
|
|
|
67
66
|
|
|
68
67
|
it('multi-region hosts SHOULD expose the cross-region conflict counter per §"Operator surface"', async () => {
|
|
69
68
|
const disco = await driver.get('/.well-known/openwop');
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
const crossRegion =
|
|
69
|
+
const idem = capabilityFamily<IdempotencyCaps>(disco.json, 'idempotency');
|
|
70
|
+
const observability = capabilityFamily<ObservabilityCaps>(disco.json, 'observability');
|
|
71
|
+
const crossRegion = idem?.crossRegion;
|
|
73
72
|
|
|
74
73
|
if (crossRegion !== 'best-effort' && crossRegion !== 'strict') {
|
|
75
74
|
// Single-region hosts have no conflicts to count — skip.
|
|
76
75
|
return;
|
|
77
76
|
}
|
|
78
77
|
|
|
79
|
-
const advertised = new Set(
|
|
78
|
+
const advertised = new Set(observability?.metrics?.names ?? []);
|
|
80
79
|
for (const name of REQUIRED_METRICS_WHEN_MULTI_REGION) {
|
|
81
80
|
expect(advertised.has(name), driver.describe(
|
|
82
81
|
'idempotency.md §"Operator surface"',
|
|
@@ -103,9 +102,10 @@ interface MultiRegionCaps {
|
|
|
103
102
|
describe('multi-region-idempotency: granular multiRegion advertisement shape (RFC 0036 §A)', () => {
|
|
104
103
|
it('capabilities.idempotency.multiRegion (when present) conforms to RFC 0036 §A', async () => {
|
|
105
104
|
const disco = await driver.get('/.well-known/openwop');
|
|
106
|
-
const idem =
|
|
107
|
-
|
|
108
|
-
|
|
105
|
+
const idem = capabilityFamily<IdempotencyCaps & { multiRegion?: MultiRegionCaps }>(
|
|
106
|
+
disco.json,
|
|
107
|
+
'idempotency',
|
|
108
|
+
);
|
|
109
109
|
const mr = idem?.multiRegion;
|
|
110
110
|
if (mr === undefined) return; // host doesn't advertise the granular block — soft-skip
|
|
111
111
|
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
|
|
19
19
|
import { describe, it, expect } from 'vitest';
|
|
20
20
|
import { driver } from '../lib/driver.js';
|
|
21
|
+
import { capabilityFamily } from '../lib/discovery-capabilities.js';
|
|
21
22
|
|
|
22
23
|
interface DiscoveryOAuthProvider {
|
|
23
24
|
id?: string;
|
|
@@ -47,7 +48,7 @@ const VALID_GRANTS: ReadonlySet<string> = new Set([
|
|
|
47
48
|
async function readOAuth(): Promise<DiscoveryOAuth | null> {
|
|
48
49
|
const res = await driver.get('/.well-known/openwop');
|
|
49
50
|
const body = res.json as DiscoveryDoc | undefined;
|
|
50
|
-
return body
|
|
51
|
+
return capabilityFamily(body, 'oauth') ?? null;
|
|
51
52
|
}
|
|
52
53
|
|
|
53
54
|
describe('oauth-capability-shape: advertisement shape (RFC 0047 §A)', () => {
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
|
|
27
27
|
import { describe, it, expect } from 'vitest';
|
|
28
28
|
import { driver } from '../lib/driver.js';
|
|
29
|
+
import { capabilityFamily } from '../lib/discovery-capabilities.js';
|
|
29
30
|
|
|
30
31
|
interface DiscoveryOAuth {
|
|
31
32
|
supported?: boolean;
|
|
@@ -42,7 +43,7 @@ const TOKEN_CANARY = 'OPENWOP_OAUTH_CANARY_b7d3e1a9c2';
|
|
|
42
43
|
async function readOAuth(): Promise<DiscoveryOAuth | null> {
|
|
43
44
|
const res = await driver.get('/.well-known/openwop');
|
|
44
45
|
const body = res.json as DiscoveryDoc | undefined;
|
|
45
|
-
return body
|
|
46
|
+
return capabilityFamily(body, 'oauth') ?? null;
|
|
46
47
|
}
|
|
47
48
|
|
|
48
49
|
describe('oauth-connector-redaction: advertisement shape (RFC 0047 §A)', () => {
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
|
|
20
20
|
import { describe, it, expect } from 'vitest';
|
|
21
21
|
import { driver } from '../lib/driver.js';
|
|
22
|
+
import { capabilityFamily } from '../lib/discovery-capabilities.js';
|
|
22
23
|
import { pollUntilStatus, pollUntilTerminal } from '../lib/polling.js';
|
|
23
24
|
import { isFixtureAdvertised } from '../lib/fixtures.js';
|
|
24
25
|
|
|
@@ -235,9 +236,8 @@ describe.skipIf(SKIP)('pause/resume: drainPolicy discrimination per capabilities
|
|
|
235
236
|
it('every drainPolicy advertised by the host is accepted on :pause', async () => {
|
|
236
237
|
const disco = await driver.get('/.well-known/openwop');
|
|
237
238
|
const drainPolicies =
|
|
238
|
-
(disco.json
|
|
239
|
-
|
|
240
|
-
}).capabilities?.runs?.pauseResume?.drainPolicies ?? [];
|
|
239
|
+
capabilityFamily<{ pauseResume?: { drainPolicies?: string[] } }>(disco.json, 'runs')
|
|
240
|
+
?.pauseResume?.drainPolicies ?? [];
|
|
241
241
|
if (drainPolicies.length === 0) {
|
|
242
242
|
// eslint-disable-next-line no-console
|
|
243
243
|
console.warn('[pause-resume] host advertises no drainPolicies; skipping policy-discrimination subtest');
|
|
@@ -40,6 +40,7 @@ import { driver } from '../lib/driver.js';
|
|
|
40
40
|
import { loadEnv } from '../lib/env.js';
|
|
41
41
|
import { behaviorGate } from '../lib/behavior-gate.js';
|
|
42
42
|
import { isFixtureAdvertised } from '../lib/fixtures.js';
|
|
43
|
+
import { capabilityFamily } from '../lib/discovery-capabilities.js';
|
|
43
44
|
|
|
44
45
|
interface BackpressureCaps {
|
|
45
46
|
supported?: boolean;
|
|
@@ -54,8 +55,7 @@ interface ProductionCaps {
|
|
|
54
55
|
|
|
55
56
|
async function readProductionCaps(): Promise<ProductionCaps | undefined> {
|
|
56
57
|
const disco = await driver.get('/.well-known/openwop');
|
|
57
|
-
return (disco.json
|
|
58
|
-
.capabilities?.production;
|
|
58
|
+
return capabilityFamily<ProductionCaps>(disco.json, 'production');
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
function isProfileAdvertised(prod: ProductionCaps | undefined): boolean {
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
import { describe, it, expect } from 'vitest';
|
|
32
32
|
import { driver } from '../lib/driver.js';
|
|
33
33
|
import { behaviorGate } from '../lib/behavior-gate.js';
|
|
34
|
+
import { capabilityFamily } from '../lib/discovery-capabilities.js';
|
|
34
35
|
|
|
35
36
|
interface RetentionCaps {
|
|
36
37
|
supported?: boolean;
|
|
@@ -45,8 +46,7 @@ interface ProductionCaps {
|
|
|
45
46
|
|
|
46
47
|
async function readProductionCaps(): Promise<ProductionCaps | undefined> {
|
|
47
48
|
const disco = await driver.get('/.well-known/openwop');
|
|
48
|
-
return (disco.json
|
|
49
|
-
.capabilities?.production;
|
|
49
|
+
return capabilityFamily<ProductionCaps>(disco.json, 'production');
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
function isProfileAdvertised(prod: ProductionCaps | undefined): boolean {
|