Acceptance project (#27): examples/acceptance/sensor_enclosure.json, a sensor enclosure sized from user parameters (case with USB cut-out, patterned PCB standoffs and lid bosses, snap fit; lid with a lip from offsets of the case's rim and patterned screw holes). Scenario acceptance_sensor_enclosure edits it (USB moved, suppress and back, save and reopen, STEP out and back in); pending scenarios for what fails: #40 (lid lip after a resize), #41 (snap fit after a resize), #42 (pattern of a suppressed feature), #29 (Lip on the rounded rim). Harness: a script step runs a command script, $DIR in call parameters is the scenario's folder

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RjGw4gA5ZvKrU8MQP6sBgT
This commit is contained in:
bherbruckandClaude Opus 5.5 committed 2026-10-09 23:41:13 +00:00
1 parent 141f3acc85
commit abb9d7460f
7 files changed
+1079 -1

No files matched your search

+29 -1
View File
@@ -27,6 +27,8 @@
//! `{"dimension": "d1"}` or `{"handle": "arrow"}` (a manipulator handle: see [`publish_handle`])
//! - `{"autosave": true}`: autosave into the scenario's recovery folder; `{"restart": "crash" |
//! "close"}`: the app dies (or closes) and a new one starts on the same folders
//! - `{"script": "examples/…/x.json"}`: run a command script through the engine; `"$DIR"` in a
//! `call` step's string parameters is the scenario's folder
//! - `{"note": "…"}`: a comment; `{"pending": "why"}`: the scenario waits for a fix (its test
//! is ignored)
//! - `{"expect": {…}}`: checks, see [`check`]; `{"until": {…}}` waits (frames) until they pass
@@ -95,6 +97,16 @@ pub fn clear_handles() {
COUNTS.with(|h| h.borrow_mut().clear());
}
/// `v` with "$DIR" in its strings replaced by `dir`.
fn with_dir(v: Value, dir: &str) -> Value {
match v {
Value::String(t) => Value::String(t.replace("$DIR", dir)),
Value::Array(a) => Value::Array(a.into_iter().map(|x| with_dir(x, dir)).collect()),
Value::Object(o) => Value::Object(o.into_iter().map(|(k, x)| (k, with_dir(x, dir))).collect()),
other => other,
}
}
/// A fresh folder for one scenario's files.
fn scratch_dir() -> PathBuf {
use std::sync::atomic::{AtomicUsize, Ordering};
@@ -363,8 +375,24 @@ impl Harness {
return Err(format!("{m} was answered after {frames} frames: the agent cursor did not act it out ({min})"));
}
v
} else if let Some(f) = s.get("script").and_then(Value::as_str) {
// A command script (`{"commands": [{command, params}]}`, path from the repository
// root) run through the engine, as `solvecraft-cli run` does.
let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../..").join(f);
let text = std::fs::read_to_string(&path).map_err(|e| format!("{f}: {e}"))?;
let v: Value = serde_json::from_str(&text).map_err(|e| format!("{f}: {e}"))?;
for (i, c) in v["commands"].as_array().into_iter().flatten().enumerate() {
let r = self.call("engine.execute", json!({"command": c["command"], "params": c.get("params").cloned().unwrap_or(json!({}))}));
if r["ok"] != true {
return Err(format!("{f}: step {} ({}): {}", i + 1, c["command"], r["error"]));
}
}
self.frames(2);
return Ok(());
} else if let Some(m) = s.get("call").and_then(Value::as_str) {
self.call(m, s.get("params").cloned().unwrap_or(json!({})))
// "$DIR" in a string parameter is the scenario's folder.
let params = with_dir(s.get("params").cloned().unwrap_or(json!({})), &self.dir.to_string_lossy());
self.call(m, params)
} else if s.get("note").is_some() || s.get("pending").is_some() {
return Ok(());
} else if let Some(name) = s.get("shot").and_then(Value::as_str) {
@@ -0,0 +1,27 @@
[
{
"pending": "#40: offsets of projected geometry get new ids on recompute, the lid's lip fails"
},
{
"note": "Acceptance (#27): the board grows 60 \u2192 70 mm; the lid's lip (offsets of the case's projected rim) follows."
},
{
"script": "examples/acceptance/sensor_enclosure.json"
},
{
"call": "engine.execute",
"params": {
"command": "parameters.change",
"params": {
"name": "board_w",
"expression": "70 mm"
}
}
},
{
"expect": {
"errors": 0,
"bodies": 2
}
}
]
@@ -0,0 +1,33 @@
[
{
"pending": "#29: Plastic Lip fails on a rounded (filleted) shelled rim"
},
{
"note": "Acceptance (#27): a lip on the case's rounded rim with the Lip command."
},
{
"script": "examples/acceptance/sensor_enclosure.json"
},
{
"call": "engine.execute",
"params": {
"command": "plastic.lip",
"params": {
"face": [
0,
29,
25
],
"width": 1,
"height": 1.5,
"body": "Case"
}
}
},
{
"expect": {
"errors": 0,
"bodies": 2
}
}
]
@@ -0,0 +1,29 @@
[
{
"pending": "#42: suppressing a patterned feature keeps the pattern's copies"
},
{
"note": "Acceptance (#27): suppressing the PCB standoff takes the patterned copies with it."
},
{
"script": "examples/acceptance/sensor_enclosure.json"
},
{
"call": "engine.execute",
"params": {
"command": "timeline.suppress",
"params": {
"feature": "Standoff",
"suppressed": true
}
}
},
{
"expect": {
"body_volume": {
"body": "Case",
"volume": 24934.4
}
}
}
]
@@ -0,0 +1,145 @@
[
{
"note": "Acceptance (#27): the sensor enclosure built from examples/acceptance/sensor_enclosure.json, then edited: the USB cut-out moved, the height changed, features suppressed and back, saved and reopened, exported to STEP and read back."
},
{
"script": "examples/acceptance/sensor_enclosure.json"
},
{
"call": "ui.view",
"params": {
"view": "home"
}
},
{
"expect": {
"bodies": 2,
"errors": 0,
"body_volume": {
"body": "Case",
"volume": 25401.7
}
}
},
{
"expect": {
"body_volume": {
"body": "Lid",
"volume": 19629.7
}
}
},
{
"call": "engine.execute",
"params": {
"command": "parameters.change",
"params": {
"name": "usb_offset",
"expression": "15 mm"
}
}
},
{
"expect": {
"errors": 0,
"bodies": 2,
"body_volume": {
"body": "Case",
"volume": 25401.7
}
}
},
{
"call": "engine.execute",
"params": {
"command": "timeline.suppress",
"params": {
"feature": "SnapFit1",
"suppressed": true
}
}
},
{
"expect": {
"errors": 0,
"body_volume": {
"body": "Case",
"volume": 25340.5
}
}
},
{
"call": "engine.execute",
"params": {
"command": "timeline.suppress",
"params": {
"feature": "SnapFit1",
"suppressed": false
}
}
},
{
"expect": {
"errors": 0,
"body_volume": {
"body": "Case",
"volume": 25401.7
}
}
},
{
"call": "engine.execute",
"params": {
"command": "file.save_as",
"params": {
"path": "$DIR/enclosure.solvecraft"
}
}
},
{
"call": "engine.execute",
"params": {
"command": "doc.open",
"params": {
"path": "$DIR/enclosure.solvecraft"
}
}
},
{
"expect": {
"errors": 0,
"bodies": 2,
"body_volume": {
"body": "Case",
"volume": 25401.7
}
}
},
{
"call": "engine.execute",
"params": {
"command": "file.export",
"params": {
"path": "$DIR/enclosure.step"
}
}
},
{
"call": "engine.execute",
"params": {
"command": "doc.open",
"params": {
"path": "$DIR/enclosure.step"
}
}
},
{
"expect": {
"bodies": 2,
"body_volume": {
"body": "Case",
"volume": 25401.7
}
}
}
]
@@ -0,0 +1,44 @@
[
{
"pending": "#41: a snap fit stays at its old point when its wall moves (a floating body)"
},
{
"note": "Acceptance (#27): the board grows 40 \u2192 50 mm deep; the snap fit stays on the moved front wall."
},
{
"script": "examples/acceptance/sensor_enclosure.json"
},
{
"call": "engine.execute",
"params": {
"command": "parameters.change",
"params": {
"name": "board_d",
"expression": "50 mm"
}
}
},
{
"expect": {
"errors": 0,
"bodies": 2
}
},
{
"expect": {
"bbox": {
"body": "Case",
"min": [
-40,
-35,
0
],
"max": [
40,
35,
25
]
}
}
}
]