Skip to content

Commit 98c798c

Browse files
feat(vz): add runtime primitive conformance parity matrix
1 parent 5955256 commit 98c798c

File tree

9 files changed

+796
-8
lines changed

9 files changed

+796
-8
lines changed

.beads/issues.jsonl

Lines changed: 18 additions & 0 deletions
Large diffs are not rendered by default.

.github/workflows/vm-e2e.yml

Lines changed: 75 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,87 @@ on:
2222
default: false
2323
type: boolean
2424
schedule:
25-
- cron: "0 8 * * 1"
25+
- cron: "0 8 * * *"
2626

2727
env:
2828
CARGO_TERM_COLOR: always
2929
RUST_BACKTRACE: 1
3030

3131
jobs:
32-
vm-e2e:
33-
name: Real VM Sandbox E2E
32+
vm-e2e-smoke:
33+
name: VM E2E Smoke Lane
34+
if: github.event_name == 'schedule'
35+
runs-on: [self-hosted, macOS, ARM64]
36+
timeout-minutes: 240
37+
38+
steps:
39+
- uses: actions/checkout@v4
40+
41+
- uses: dtolnay/rust-toolchain@stable
42+
43+
- uses: Swatinem/rust-cache@v2
44+
with:
45+
workspaces: crates
46+
47+
- name: Run sandbox VM E2E harness
48+
run: |
49+
PROFILE="${{ github.event.inputs.profile }}"
50+
if [[ -z "$PROFILE" ]]; then
51+
PROFILE="debug"
52+
fi
53+
SUITES="sandbox"
54+
55+
./scripts/run-sandbox-vm-e2e.sh \
56+
--suite "$SUITES" \
57+
--profile "$PROFILE" \
58+
--output-dir "$RUNNER_TEMP/sandbox-vm-e2e-smoke"
59+
60+
- name: Upload VM E2E artifacts
61+
if: always()
62+
uses: actions/upload-artifact@v4
63+
with:
64+
name: sandbox-vm-e2e-smoke-artifacts
65+
path: ${{ runner.temp }}/sandbox-vm-e2e-smoke
66+
if-no-files-found: warn
67+
68+
vm-e2e-nightly-full:
69+
name: VM E2E Nightly Full Lane
70+
if: github.event_name == 'schedule'
71+
runs-on: [self-hosted, macOS, ARM64]
72+
timeout-minutes: 480
73+
needs: vm-e2e-smoke
74+
75+
steps:
76+
- uses: actions/checkout@v4
77+
78+
- uses: dtolnay/rust-toolchain@stable
79+
80+
- uses: Swatinem/rust-cache@v2
81+
with:
82+
workspaces: crates
83+
84+
- name: Run sandbox VM E2E full lane
85+
run: |
86+
PROFILE="${{ github.event.inputs.profile }}"
87+
if [[ -z "$PROFILE" ]]; then
88+
PROFILE="debug"
89+
fi
90+
./scripts/run-sandbox-vm-e2e.sh \
91+
--suite all \
92+
--profile "$PROFILE" \
93+
--output-dir "$RUNNER_TEMP/sandbox-vm-e2e-full"
94+
95+
- name: Upload VM E2E artifacts
96+
if: always()
97+
uses: actions/upload-artifact@v4
98+
with:
99+
name: sandbox-vm-e2e-nightly-full-artifacts
100+
path: ${{ runner.temp }}/sandbox-vm-e2e-full
101+
if-no-files-found: warn
102+
103+
vm-e2e-manual:
104+
name: VM E2E Manual
105+
if: github.event_name == 'workflow_dispatch'
34106
runs-on: [self-hosted, macOS, ARM64]
35107
timeout-minutes: 240
36108

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,10 @@ Full VM lanes (runtime + stack + buildkit):
160160

161161
See `docs/sandbox-vm-e2e.md` for reproducible debug workflow and artifact paths.
162162

163+
Conformance and parity coverage:
164+
165+
- [Runtime primitive conformance matrix](docs/runtime-primitive-conformance.md)
166+
163167
## License
164168

165169
[MIT](LICENSE.md)

crates/vz-api/src/lib.rs

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -392,6 +392,7 @@ mod tests {
392392
use super::*;
393393
use axum::body::{Body, to_bytes};
394394
use axum::http::Request;
395+
use std::collections::BTreeSet;
395396
use tempfile::tempdir;
396397
use tower::ServiceExt;
397398
use vz_stack::StackEvent;
@@ -409,6 +410,13 @@ mod tests {
409410
}
410411
}
411412

413+
fn sample_openapi_path(path: &str) -> String {
414+
match path {
415+
"/v1/events/{stack_name}" => "/v1/events/runtime-conformance-stack".to_string(),
416+
_ => path.to_string(),
417+
}
418+
}
419+
412420
#[test]
413421
fn openapi_document_contains_required_paths() {
414422
let document = openapi_document();
@@ -598,4 +606,80 @@ mod tests {
598606
assert!(paths.contains_key("/v1/events/{stack_name}/stream"));
599607
assert!(paths.contains_key("/v1/events/{stack_name}/ws"));
600608
}
609+
610+
#[tokio::test]
611+
async fn transport_parity_openapi_matrix_paths_match_contract() {
612+
let document = openapi_document();
613+
let paths = document["paths"].as_object().unwrap();
614+
let mut matrix_paths = BTreeSet::new();
615+
616+
for entry in vz_runtime_contract::PRIMITIVE_CONFORMANCE_MATRIX {
617+
if let Some(surface) = entry.openapi {
618+
assert!(!surface.path.is_empty());
619+
assert!(surface.path.starts_with('/'));
620+
assert!(!surface.surface.is_empty());
621+
assert!(
622+
paths.contains_key(surface.path),
623+
"missing OpenAPI path `{}` for `{}`",
624+
surface.path,
625+
entry.operation.as_str()
626+
);
627+
matrix_paths.insert(surface.path);
628+
}
629+
}
630+
631+
assert!(!matrix_paths.is_empty());
632+
}
633+
634+
#[tokio::test]
635+
async fn transport_parity_openapi_surface_errors_match_runtime_operation_labels() {
636+
let temp_dir = tempdir().unwrap();
637+
let state_path = temp_dir.path().join("state.db");
638+
StateStore::open(&state_path).unwrap();
639+
640+
let app = router(test_config(state_path));
641+
642+
for entry in vz_runtime_contract::PRIMITIVE_CONFORMANCE_MATRIX {
643+
let Some(surface) = entry.openapi else {
644+
continue;
645+
};
646+
647+
let request = Request::builder()
648+
.uri(sample_openapi_path(surface.path))
649+
.body(Body::empty())
650+
.unwrap();
651+
let response = app.clone().oneshot(request).await.unwrap();
652+
let status = response.status();
653+
654+
if status == StatusCode::NOT_IMPLEMENTED {
655+
let body = to_bytes(response.into_body(), usize::MAX).await.unwrap();
656+
let envelope: MachineErrorEnvelope = serde_json::from_slice(&body).unwrap();
657+
658+
assert_eq!(
659+
envelope.error.code,
660+
vz_runtime_contract::MachineErrorCode::UnsupportedOperation
661+
);
662+
assert_eq!(
663+
envelope.error.details.get("operation").map(String::as_str),
664+
Some(surface.surface),
665+
"matrix operation mismatch for `{}` at `{}`",
666+
entry.operation.as_str(),
667+
surface.path
668+
);
669+
continue;
670+
}
671+
672+
if status == StatusCode::OK
673+
&& matches!(surface.path, "/v1/capabilities" | "/v1/events/{stack_name}")
674+
{
675+
continue;
676+
}
677+
678+
panic!(
679+
"unexpected matrix API status for `{}` at `{}`: {status}",
680+
entry.operation.as_str(),
681+
surface.path
682+
);
683+
}
684+
}
601685
}

crates/vz-cli/src/commands/stack.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2810,6 +2810,29 @@ mod tests {
28102810
assert!(no_key.is_none());
28112811
}
28122812

2813+
#[test]
2814+
fn cli_transport_parity_matrix_is_claimed_for_idempotency_behavior() {
2815+
let components = ["stack-a", "web-service", "echo_hello"];
2816+
for entry in vz_runtime_contract::PRIMITIVE_CONFORMANCE_MATRIX {
2817+
if !entry.cli {
2818+
continue;
2819+
}
2820+
2821+
let key = runtime_idempotency_key(entry.operation, &components);
2822+
let expected = entry
2823+
.operation
2824+
.idempotency_key_prefix()
2825+
.map(|prefix| format!("{prefix}:stack-a:web-service:echo_hello"));
2826+
2827+
assert_eq!(
2828+
key,
2829+
expected,
2830+
"CLI parity key mismatch for {}",
2831+
entry.operation.as_str()
2832+
);
2833+
}
2834+
}
2835+
28132836
#[test]
28142837
fn control_request_idempotency_key_is_deterministic() {
28152838
let key = control_request_idempotency_key(

crates/vz-linux/src/grpc_client.rs

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -157,10 +157,10 @@ impl GrpcAgentClient {
157157
operation: Option<RuntimeOperation>,
158158
) -> ProtoTransportMetadata {
159159
self.next_request_sequence = self.next_request_sequence.saturating_add(1);
160-
let request_id = format!("req_{:016x}", self.next_request_sequence);
161-
let idempotency_key = operation
162-
.and_then(RuntimeOperation::idempotency_key_prefix)
163-
.map(|prefix| format!("{prefix}:{request_id}"));
160+
let (request_id, idempotency_key) = vz_runtime_contract::transport_metadata_for_sequence(
161+
self.next_request_sequence,
162+
operation,
163+
);
164164
let normalized = ContractRequestMetadata::new(Some(request_id), idempotency_key);
165165

166166
ProtoTransportMetadata {
@@ -843,4 +843,36 @@ mod tests {
843843
.unwrap_err();
844844
assert!(err.to_string().contains("request_id mismatch"));
845845
}
846+
847+
#[test]
848+
fn transport_parity_grpc_metadata_generation_is_stable_for_matrixed_operations() {
849+
let mut expected_sequence = 0u64;
850+
for entry in vz_runtime_contract::PRIMITIVE_CONFORMANCE_MATRIX {
851+
if !entry.grpc_metadata {
852+
continue;
853+
}
854+
855+
let (expected_request_id, expected_key) =
856+
vz_runtime_contract::transport_metadata_for_sequence(
857+
expected_sequence,
858+
Some(entry.operation),
859+
);
860+
expected_sequence = expected_sequence.saturating_add(1);
861+
862+
let expected_prefix = entry
863+
.operation
864+
.idempotency_key_prefix()
865+
.map(|prefix| format!("{prefix}:{expected_request_id}"));
866+
assert_eq!(expected_key, expected_prefix);
867+
868+
assert_eq!(
869+
expected_request_id,
870+
format!("req_{:016x}", expected_sequence),
871+
"request id sequence mismatch for {}",
872+
entry.operation.as_str()
873+
);
874+
}
875+
876+
assert!(expected_sequence > 0);
877+
}
846878
}

0 commit comments

Comments
 (0)