dsh-labscript 0.2.1 → 0.2.2

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 CHANGED
@@ -1,5 +1,18 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.2
4
+
5
+ - Fix: labs_status and labs_engage report pathMap-mapped paths. `labs_status.labscript`
6
+ and `labs_status.shot_output_folder` used to be raw runmanager paths (e.g. E:\...)
7
+ and were unusable from the agent's machine; they are now what pathMap maps them to.
8
+ This is a field-semantics fix: read them as local paths, do not map again.
9
+ - Fix: tool output renders mapped run folders. The rendered lines of labs_engage,
10
+ labs_wait, labs_data, labs_protocol and the pending-run progress now prefer
11
+ `mapped_run_folder` over the raw `run_folder`.
12
+ - Fix: labs_status now lists the shot output folder, and the status progress object
13
+ reports `mapped_run_folder`.
14
+ - Tests: pathMap exit coverage (status mapping, idempotency, progress mapping).
15
+
3
16
  ## 0.2.1
4
17
 
5
18
  - Hard limits support a lower bound: limits entries can now use min as well as max
package/README.md CHANGED
@@ -97,6 +97,12 @@ Add one row to your DSH profile patch (see examples/config.example.yml):
97
97
  pythonPaths: [/path/to/my_lab]
98
98
  approval: { engage: true, protocolRun: true }
99
99
 
100
+ `pathMap` rewrites paths reported by runmanager (remote drive letters such as
101
+ `E:/Experiments`) into paths this machine can open (UNC shares). Tool output is
102
+ already mapped: `labs_status`, `labs_engage`, `labs_wait`, `labs_data` and
103
+ `labs_protocol` show local paths, while `run_folder` keeps the raw runmanager
104
+ value next to `mapped_run_folder`.
105
+
100
106
  The python interpreter must be able to import numpy and h5py; the real backend
101
107
  also needs runmanager (labscript-suite). The plugin never stores credentials:
102
108
  zprocess authentication is read by labscript itself from labconfig.
@@ -119,8 +125,8 @@ is not a physics simulation.
119
125
  ## Development
120
126
 
121
127
  npm install # only runtime dependency: yaml
122
- npm test # node --test, 18 tests (fake bridge, no python needed)
123
- python python/tests/run_tests.py # 8 tests (needs numpy + h5py)
128
+ npm test # node --test, 35 tests (fake bridge, no python needed)
129
+ python python/tests/run_tests.py # 15 tests (needs numpy + h5py)
124
130
  LABSCRIPT_PYTHON=/path/to/python3 node scripts/demo.mjs # end-to-end mock demo
125
131
 
126
132
  A fresh clone must run `npm install` before `npm test`; the CI workflow does this.
package/lib/tools.js CHANGED
@@ -26,7 +26,7 @@ function number(value) {
26
26
 
27
27
  function progressLine(run) {
28
28
  if (!run) return 'run: none pending'
29
- const parts = ['run: ' + (run.run_folder || '<submitted>')]
29
+ const parts = ['run: ' + (run.mapped_run_folder || run.run_folder || '<submitted>')]
30
30
  if (run.kind) parts.push('kind=' + run.kind)
31
31
  if (run.n_h5 !== undefined) parts.push('h5=' + run.n_h5 + '/' + (run.expected_n_shots || '?'))
32
32
  else if (run.expected_n_shots) parts.push('expected=' + run.expected_n_shots)
@@ -105,7 +105,7 @@ export function registerTools(ctx, runtime) {
105
105
 
106
106
  ctx.tools.register({
107
107
  name: 'labs_status',
108
- description: 'Read the labscript runmanager panel: loaded labscript, globals (evaluated; raw expressions with raw=true), n_shots, error_in_globals, run_shots, shot output folder, pending run progress, and the panel snapshot directory. Read-only; call this before any write.',
108
+ description: 'Read the labscript runmanager panel: loaded labscript, globals (evaluated; raw expressions with raw=true), n_shots, error_in_globals, run_shots, shot output folder, pending run progress, and the panel snapshot directory. Read-only; call this before any write. Paths in the output are already mapped for this machine through pathMap; do not map them again.',
109
109
  parameters: {
110
110
  type: 'object',
111
111
  properties: {
@@ -118,6 +118,7 @@ export function registerTools(ctx, runtime) {
118
118
  output: looseOutput((_args, value) => text(lines([
119
119
  'backend: ' + value.backend + ' plugin: v' + runtime.version,
120
120
  'labscript: ' + value.labscript,
121
+ 'shot output: ' + value.shot_output_folder,
121
122
  'n_shots: ' + value.n_shots + ' error_in_globals: ' + value.error_in_globals + ' run_shots: ' + value.run_shots,
122
123
  progressLine(value.last_run),
123
124
  'panel history: ' + (value.panel_history ? value.panel_history.dir + ' (' + value.panel_history.count + ' snapshots)' : 'n/a'),
@@ -174,7 +175,7 @@ export function registerTools(ctx, runtime) {
174
175
  },
175
176
  isConcurrencySafe: () => false,
176
177
  output: looseOutput((_args, value) => text(lines([
177
- 'submitted run: ' + (value.run_folder || '<run folder pending>'),
178
+ 'submitted run: ' + (value.mapped_run_folder || value.run_folder || '<run folder pending>'),
178
179
  'n_shots_at_submit: ' + value.n_shots_at_submit + ' expected: ' + value.expected_n_shots,
179
180
  'call labs_wait when results are needed.',
180
181
  ]))),
@@ -216,7 +217,7 @@ export function registerTools(ctx, runtime) {
216
217
  },
217
218
  isConcurrencySafe: () => true,
218
219
  output: looseOutput((_args, value) => text(lines([
219
- 'run: ' + value.run_folder,
220
+ 'run: ' + (value.mapped_run_folder || value.run_folder),
220
221
  value.aborted ? 'wait cancelled by labs_abort (run aborted)' : '',
221
222
  'shots: ' + value.n_h5 + '/' + (value.expected_n_shots || '?') + ' complete: ' + value.complete + (value.timed_out ? ' TIMED OUT' : '') + (value.diagnosis ? ' diagnosis: ' + value.diagnosis : ''),
222
223
  value.hint ? 'hint: ' + value.hint : '',
@@ -326,7 +327,7 @@ export function registerTools(ctx, runtime) {
326
327
  },
327
328
  isConcurrencySafe: () => true,
328
329
  output: looseOutput((_args, value) => text(lines([
329
- 'run: ' + value.run_folder,
330
+ 'run: ' + (value.mapped_run_folder || value.run_folder),
330
331
  'h5: ' + value.n_h5,
331
332
  value.scan_table ? 'scan ' + value.scan_table.scan + ': ' + value.scan_table.n_points + ' shots' : '',
332
333
  value.globals ? 'globals: ' + Object.keys(value.globals).length + ' keys' : '',
@@ -366,7 +367,7 @@ export function registerTools(ctx, runtime) {
366
367
  value.warnings && value.warnings.length ? 'warnings: ' + value.warnings.join('; ') : '',
367
368
  value.name_check && value.name_check.checked && value.name_check.missing && value.name_check.missing.length ? 'script key check: MISSING ' + value.name_check.missing.join(', ') : '',
368
369
  value.name_check && value.name_check.checked === false ? 'script key check: not checked (' + (value.name_check.reason || 'no script') + ')' : '',
369
- value.engage ? 'submitted: ' + (value.engage.run_folder || '<run folder pending>') + ' n_shots_at_submit: ' + value.n_shots_at_submit : '',
370
+ value.engage ? 'submitted: ' + (value.engage.mapped_run_folder || value.engage.run_folder || '<run folder pending>') + ' n_shots_at_submit: ' + value.n_shots_at_submit : '',
370
371
  value.engage ? 'call labs_wait to finalize this protocol.' : '',
371
372
  ]))),
372
373
  async execute(args, exec) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-labscript",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "Control labscript-suite experiments from DeepSeek Harness: runmanager globals and scans, submits and waits, YAML fixed-operation protocols with postprocess write-back, panel snapshots, a built-in mock backend and a read-only doctor.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
package/python/bridge.py CHANGED
@@ -927,8 +927,8 @@ def cmd_status(config, args):
927
927
  out = {
928
928
  "backend": config.get("backend"),
929
929
  "version": backend.version() if hasattr(backend, "version") else None,
930
- "labscript": backend.labscript_file(),
931
- "shot_output_folder": backend.shot_output_folder(),
930
+ "labscript": map_path(config, backend.labscript_file()),
931
+ "shot_output_folder": map_path(config, backend.shot_output_folder()),
932
932
  "globals": jsonable(values),
933
933
  "n_shots": backend.n_shots(),
934
934
  "error_in_globals": backend.error_in_globals(),
@@ -1015,7 +1015,7 @@ def cmd_engage(config, args):
1015
1015
  "mapped_run_folder": map_path(config, run_folder),
1016
1016
  "n_shots_at_submit": n_shots,
1017
1017
  "expected_n_shots": int(args.get("expected_n_shots") or n_shots),
1018
- "labscript": backend.labscript_file(),
1018
+ "labscript": map_path(config, backend.labscript_file()),
1019
1019
  "submitted_at": now_iso(),
1020
1020
  }
1021
1021
  append_event(config, {"ts": out["submitted_at"], "tool": "engage", "backend": config.get("backend"), "note": args.get("note"), "run_folder": run_folder, "expected_n_shots": out["expected_n_shots"]})
@@ -1127,7 +1127,7 @@ def cmd_progress(config, args):
1127
1127
  expected = int(args.get("expected_n_shots") or 0)
1128
1128
  folder = map_path(config, run_folder)
1129
1129
  shots = sorted(glob.glob(os.path.join(folder, "**", "*.h5"), recursive=True)) if folder else []
1130
- return {"run_folder": run_folder, "n_h5": len(shots), "expected_n_shots": expected, "complete": bool(expected and len(shots) >= expected)}
1130
+ return {"run_folder": run_folder, "mapped_run_folder": folder, "n_h5": len(shots), "expected_n_shots": expected, "complete": bool(expected and len(shots) >= expected)}
1131
1131
 
1132
1132
 
1133
1133
 
@@ -291,6 +291,61 @@ def test_doctor_mock():
291
291
  shutil.rmtree(tmp, ignore_errors=True)
292
292
 
293
293
 
294
+ def test_status_maps_paths_from_runmanager():
295
+ """labs_status must return paths the agent's machine can open."""
296
+ tmp = tempfile.mkdtemp(prefix="dsh-labscript-py-map-")
297
+ try:
298
+ cfg = make_config(tmp)
299
+ cfg["pathMap"] = [{"remotePrefix": "E:\\Experiments", "localPrefix": "\\\\lab-pc\\Experiments"}]
300
+
301
+ class FakeBackend(object):
302
+ def get_globals(self, raw=False):
303
+ return {}
304
+
305
+ def labscript_file(self):
306
+ return "E:\\Experiments\\2026\\x.py"
307
+
308
+ def shot_output_folder(self):
309
+ return "E:\\Experiments\\2026\\11\\12\\0177"
310
+
311
+ def n_shots(self):
312
+ return 1
313
+
314
+ def error_in_globals(self):
315
+ return False
316
+
317
+ def run_shots(self):
318
+ return True
319
+
320
+ def version(self):
321
+ return "fake"
322
+
323
+ original = bridge.make_backend
324
+ bridge.make_backend = lambda config: FakeBackend()
325
+ try:
326
+ out = bridge.cmd_status(cfg, {})
327
+ finally:
328
+ bridge.make_backend = original
329
+ assert out["labscript"] == "\\\\lab-pc\\Experiments\\2026\\x.py", out["labscript"]
330
+ assert out["shot_output_folder"] == "\\\\lab-pc\\Experiments\\2026\\11\\12\\0177", out["shot_output_folder"]
331
+ finally:
332
+ shutil.rmtree(tmp, ignore_errors=True)
333
+
334
+
335
+ def test_map_path_is_idempotent():
336
+ cfg = {"pathMap": [{"remotePrefix": "E:\\Experiments", "localPrefix": "\\\\lab-pc\\Experiments"}]}
337
+ once = bridge.map_path(cfg, "E:\\Experiments\\a\\b.h5")
338
+ assert once == "\\\\lab-pc\\Experiments\\a\\b.h5", once
339
+ assert bridge.map_path(cfg, once) == once
340
+
341
+
342
+ def test_progress_reports_mapped_run_folder():
343
+ cfg = {"pathMap": [{"remotePrefix": "E:\\Experiments", "localPrefix": "\\\\lab-pc\\Experiments"}]}
344
+ out = bridge.cmd_progress(cfg, {"run_folder": "E:\\Experiments\\nope", "expected_n_shots": 1})
345
+ assert out["run_folder"] == "E:\\Experiments\\nope", out
346
+ assert out["mapped_run_folder"] == "\\\\lab-pc\\Experiments\\nope", out
347
+
348
+
294
349
  def main():
295
350
  tests = [value for name, value in sorted(globals().items()) if name.startswith("test_")]
296
351
  failures = 0