dazscript-framework 1.0.31 → 1.0.33

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/README.md CHANGED
@@ -11,6 +11,7 @@
11
11
  - [Installation & Setup](#installation--setup)
12
12
  - [Unit Tests](#unit-tests)
13
13
  - [DAZ Studio Integration Tests](#daz-studio-integration-tests)
14
+ - [DAZ Studio Headless Probes](#daz-studio-headless-probes)
14
15
  - [Project Configuration](#project-configuration)
15
16
  - [The `action(...)` Entrypoint](#the-action-entrypoint)
16
17
  - [Build Output: Launcher Shims](#build-output-launcher-shims)
@@ -62,6 +63,7 @@ To include optional test scaffolds, run one or both:
62
63
  ```bash
63
64
  npx dazscript init --unit-tests
64
65
  npx dazscript init --integration-tests
66
+ npx dazscript init --probes
65
67
  ```
66
68
 
67
69
  ### 3. Write the script
@@ -177,6 +179,7 @@ Available `init` flags:
177
179
  npx dazscript init --menu-path /MyScripts --scripts-path ./src --out-dir ./out --app-data-path YourName/my-project
178
180
  npx dazscript init --unit-tests --app-data-path YourName/my-project
179
181
  npx dazscript init --integration-tests --app-data-path YourName/my-project
182
+ npx dazscript init --probes --app-data-path YourName/my-project
180
183
  ```
181
184
 
182
185
  | Flag | Description |
@@ -222,6 +225,22 @@ npm run test:integration
222
225
 
223
226
  The generated script calls `dazscript integration --fixture ./test/integration/fixtures/<project>-smoke.dsa.ts`. Configure local machine paths in an ignored `.env.integration.local`; `DAZ_STUDIO_EXE` is always required, and `DAZ_TEST_CONTENT_DUF` is required only for tests that use `--require-content`. See `test/integration/README.md` in this repository for maintainer and consuming-project details.
224
227
 
228
+ ### DAZ Studio headless probes
229
+
230
+ Projects can bootstrap non-asserting DAZ Studio probes with:
231
+
232
+ ```bash
233
+ npx dazscript init --probes
234
+ ```
235
+
236
+ Run a probe with:
237
+
238
+ ```bash
239
+ npm run probe
240
+ ```
241
+
242
+ Probes use the same headless DAZ build and launch infrastructure as integration tests, but they do not require `ok: true` assertions. A probe succeeds when DAZ completes and writes readable JSON. Use `dazscript probe` for exploratory runtime observations such as available globals, installed plugins, scene state, signal behavior, or content loading details.
243
+
225
244
  ---
226
245
 
227
246
  ### Project Configuration
@@ -592,7 +611,7 @@ my-daz-scripts/
592
611
 
593
612
  Files ending in `.dsa.ts` are treated as runnable entry points and compiled to `.dsa`. Plain `.ts` files are modules — imported by entry points but not compiled independently.
594
613
 
595
- The `test/unit/` files are generated only when you run `dazscript init --unit-tests`. The `test/integration/` files and env examples are generated only when you run `dazscript init --integration-tests`.
614
+ The `test/unit/` files are generated only when you run `dazscript init --unit-tests`. The `test/integration/` files and env examples are generated only when you run `dazscript init --integration-tests`. The `probes/` files and probe env examples are generated only when you run `dazscript init --probes`.
596
615
 
597
616
  **Common commands**
598
617
 
@@ -9,7 +9,7 @@ const { copyIcons } = require('./icons');
9
9
  const { runEncrypt } = require('./encrypt');
10
10
  const { generateInstallerFiles } = require('./install-generator');
11
11
  const { initProject } = require('./init');
12
- const { runIntegration } = require('./integration');
12
+ const { runIntegration, runProbe } = require('./integration');
13
13
 
14
14
  function printHelp() {
15
15
  console.log(`dazscript <command> [options]
@@ -22,6 +22,7 @@ Commands:
22
22
  icons Copy png assets into the output directory
23
23
  installer Generate Install.dsa.ts and Uninstall.dsa.ts
24
24
  integration Run a DAZ Studio headless integration fixture
25
+ probe Run a non-asserting DAZ Studio headless probe fixture
25
26
 
26
27
  Options:
27
28
  --menu-path <path> Default menu path. Default: /MyScripts
@@ -32,11 +33,12 @@ Options:
32
33
  --daz-studio <path> Daz Studio executable for encrypt
33
34
  --keep-source Keep source script.dsa files after encrypting
34
35
  --timeout-ms <value> Daz Studio encrypt timeout. Default: 300000
35
- --fixture <path> Integration fixture .dsa.ts file
36
- --env-file <path> Integration env file. Default: .env.integration.local
37
- --require-content Require DAZ_TEST_CONTENT_DUF for integration tests
36
+ --fixture <path> Integration or probe fixture .dsa.ts file
37
+ --env-file <path> Integration/probe env file. Defaults: .env.integration.local or .env.probe.local
38
+ --require-content Require DAZ_TEST_CONTENT_DUF for integration tests or probes
38
39
  --unit-tests Add Vitest unit-test scaffold during init
39
40
  --integration-tests Add integration-test scaffold during init
41
+ --probes Add DAZ headless probe scaffold during init
40
42
  --force Overwrite generated files
41
43
  --help Show this message
42
44
  `);
@@ -109,6 +111,11 @@ function parseOptions(args, defaults) {
109
111
  continue;
110
112
  }
111
113
 
114
+ if (arg === '--probes') {
115
+ options.probes = true;
116
+ continue;
117
+ }
118
+
112
119
  if (arg === '--menu-path') {
113
120
  options.menuPath = args[index + 1];
114
121
  index += 1;
@@ -230,6 +237,7 @@ async function main(argv) {
230
237
  requireContent: false,
231
238
  unitTests: false,
232
239
  integrationTests: false,
240
+ probes: false,
233
241
  });
234
242
 
235
243
  if (options.help) {
@@ -242,6 +250,11 @@ async function main(argv) {
242
250
  return;
243
251
  }
244
252
 
253
+ if (command === 'probe') {
254
+ await runProbe(options, process.env, workdir);
255
+ return;
256
+ }
257
+
245
258
  const commandOptions =
246
259
  command === 'init'
247
260
  ? await resolveInitOptions(workdir, options)
@@ -147,6 +147,23 @@ function updatePackageJsonForIntegration(workdir, fixturePath) {
147
147
  }
148
148
  }
149
149
 
150
+ function updatePackageJsonForProbe(workdir, fixturePath) {
151
+ const packageJsonPath = path.join(workdir, 'package.json');
152
+ const packageJson = fs.existsSync(packageJsonPath)
153
+ ? readJson(packageJsonPath)
154
+ : { private: true };
155
+
156
+ packageJson.scripts = packageJson.scripts || {};
157
+ if (packageJson.scripts.probe) {
158
+ console.log('skip package.json probe');
159
+ }
160
+ else {
161
+ packageJson.scripts.probe = `dazscript probe --fixture ${fixturePath}`;
162
+ writeJson(packageJsonPath, packageJson);
163
+ console.log('update package.json');
164
+ }
165
+ }
166
+
150
167
  function isNpmDefaultTestScript(scriptName, command) {
151
168
  return scriptName === 'test' && command === 'echo "Error: no test specified" && exit 1';
152
169
  }
@@ -423,6 +440,122 @@ function initIntegrationTests(workdir, options) {
423
440
  ensureLine(path.join(workdir, '.gitignore'), '.env.integration.local');
424
441
  }
425
442
 
443
+ function buildProbeFixtureContent() {
444
+ return `import { action } from '@dsf/core/action'
445
+ import { saveToFile } from '@dsf/helpers/file-helper'
446
+ import { getStringScriptArguments } from '@dsf/helpers/script-helper'
447
+
448
+ type ProbeResult = {
449
+ kind: string
450
+ status: string
451
+ observations: Record<string, unknown>
452
+ }
453
+
454
+ const countSceneNodes = (): number => {
455
+ return Scene.getNumNodes()
456
+ }
457
+
458
+ action({ text: 'Scene Probe', menuPath: false }, () => {
459
+ const args = getStringScriptArguments()
460
+ const resultPath = args.length > 0 ? args[0] : ''
461
+ const result: ProbeResult = {
462
+ kind: 'daz-headless-probe',
463
+ status: 'observed',
464
+ observations: {
465
+ graphicsMode: App.getGraphicsMode(),
466
+ interfaceAvailable: !!App.getInterface(),
467
+ nodeCount: countSceneNodes(),
468
+ time: String(Scene.getTime()),
469
+ frame: Scene.getFrame()
470
+ }
471
+ }
472
+
473
+ if (resultPath) {
474
+ saveToFile(resultPath, JSON.stringify(result, null, 2))
475
+ }
476
+ })
477
+ `;
478
+ }
479
+
480
+ function buildProbeReadmeContent(fixturePath) {
481
+ return `# DAZ Headless Probes
482
+
483
+ Probes are non-asserting DAZ Studio headless scripts for runtime exploration. They are useful for AI-agent investigations where the desired output is structured observations, not pass/fail test assertions.
484
+
485
+ Create a local env file:
486
+
487
+ \`\`\`bash
488
+ cp .env.probe.linux.example .env.probe.local
489
+ \`\`\`
490
+
491
+ \`\`\`powershell
492
+ Copy-Item .env.probe.windows.example .env.probe.local
493
+ \`\`\`
494
+
495
+ Then run:
496
+
497
+ \`\`\`bash
498
+ npm run probe
499
+ \`\`\`
500
+
501
+ The default scene probe is:
502
+
503
+ \`\`\`text
504
+ ${fixturePath}
505
+ \`\`\`
506
+
507
+ Generated output is written to \`probes/out/\` and ignored by git.
508
+ `;
509
+ }
510
+
511
+ function buildProbeLinuxEnvExample() {
512
+ return `# Copy this file to .env.probe.local and edit paths for your machine.
513
+ # Shell environment variables override values in .env.probe.local.
514
+ # DAZ_STUDIO_EXE is required for probes.
515
+
516
+ WINEPREFIX=/home/your-user/.local/share/daz-wine/prefix
517
+ DAZ_STUDIO_EXE=/home/your-user/.local/share/daz-wine/prefix/drive_c/Program Files/DAZ 3D/DAZStudio4/DAZStudio.exe
518
+ DAZ_PROBE_TIMEOUT_MS=300000
519
+ `;
520
+ }
521
+
522
+ function buildProbeWindowsEnvExample() {
523
+ return `# Copy this file to .env.probe.local and edit paths for your machine.
524
+ # Shell environment variables override values in .env.probe.local.
525
+ # Forward slashes avoid escaping issues in env files.
526
+ # DAZ_STUDIO_EXE is required for probes.
527
+
528
+ DAZ_STUDIO_EXE=C:/Program Files/DAZ 3D/DAZStudio4/DAZStudio.exe
529
+ DAZ_PROBE_TIMEOUT_MS=300000
530
+ `;
531
+ }
532
+
533
+ function initProbes(workdir, options) {
534
+ const fixtureBaseName = `${toFixtureName(path.basename(workdir))}-scene.dsa.ts`;
535
+ const fixturePath = `./probes/fixtures/${fixtureBaseName}`;
536
+ const localFixturePath = path.join(workdir, fixturePath);
537
+
538
+ writeFileIfNeeded(localFixturePath, buildProbeFixtureContent(), options.force);
539
+ writeFileIfNeeded(
540
+ path.join(workdir, 'probes/README.md'),
541
+ buildProbeReadmeContent(fixturePath),
542
+ options.force
543
+ );
544
+ writeFileIfNeeded(
545
+ path.join(workdir, '.env.probe.linux.example'),
546
+ buildProbeLinuxEnvExample(),
547
+ options.force
548
+ );
549
+ writeFileIfNeeded(
550
+ path.join(workdir, '.env.probe.windows.example'),
551
+ buildProbeWindowsEnvExample(),
552
+ options.force
553
+ );
554
+ updatePackageJsonForProbe(workdir, fixturePath);
555
+ ensureLine(path.join(workdir, '.gitignore'), 'probes/out/');
556
+ ensureLine(path.join(workdir, '.gitignore'), '.env.probe.local');
557
+ }
558
+
426
559
  function initProject(workdir, rawOptions) {
427
560
  const projectName = path.basename(workdir);
428
561
  const options = {
@@ -434,6 +567,7 @@ function initProject(workdir, rawOptions) {
434
567
  bundleName: toBundleName(projectName),
435
568
  unitTests: Boolean(rawOptions.unitTests),
436
569
  integrationTests: Boolean(rawOptions.integrationTests),
570
+ probes: Boolean(rawOptions.probes),
437
571
  };
438
572
 
439
573
  writeFileIfNeeded(
@@ -456,10 +590,15 @@ function initProject(workdir, rawOptions) {
456
590
  if (options.integrationTests) {
457
591
  initIntegrationTests(workdir, options);
458
592
  }
593
+
594
+ if (options.probes) {
595
+ initProbes(workdir, options);
596
+ }
459
597
  }
460
598
 
461
599
  module.exports = {
462
600
  initIntegrationTests,
601
+ initProbes,
463
602
  initUnitTests,
464
603
  initProject,
465
604
  };
@@ -71,10 +71,10 @@ function assertRequiredFile(label, filePath, allowDazPath) {
71
71
  return resolvedPath;
72
72
  }
73
73
 
74
- function resolveIntegrationOptions(cwd, rawOptions, env) {
74
+ function resolveHeadlessOptions(cwd, rawOptions, env, defaults) {
75
75
  const options = rawOptions || {};
76
76
  const projectRoot = path.resolve(cwd || process.cwd());
77
- const envFile = path.resolve(projectRoot, options.envFile || '.env.integration.local');
77
+ const envFile = path.resolve(projectRoot, options.envFile || defaults.envFile);
78
78
  const fixturePath = options.fixture
79
79
  ? path.resolve(projectRoot, options.fixture)
80
80
  : '';
@@ -100,12 +100,12 @@ function resolveIntegrationOptions(cwd, rawOptions, env) {
100
100
  throw new Error(`DAZ_TEST_CONTENT_DUF does not exist: ${path.resolve(rawContentPath)}`);
101
101
  }
102
102
 
103
- const timeoutMs = Number(options.timeoutMs || env.DAZ_TEST_TIMEOUT_MS || defaultTimeoutMs);
103
+ const timeoutMs = Number(options.timeoutMs || env[defaults.timeoutEnv] || env.DAZ_TEST_TIMEOUT_MS || defaultTimeoutMs);
104
104
  if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
105
- throw new Error(`invalid timeout: ${options.timeoutMs || env.DAZ_TEST_TIMEOUT_MS}`);
105
+ throw new Error(`invalid timeout: ${options.timeoutMs || env[defaults.timeoutEnv] || env.DAZ_TEST_TIMEOUT_MS}`);
106
106
  }
107
107
 
108
- const outDir = path.resolve(projectRoot, options.outDir || './test/integration/out');
108
+ const outDir = path.resolve(projectRoot, options.outDir || defaults.outDir);
109
109
  const fixtureName = path.basename(fixturePath).replace(/\.dsa\.ts$/i, '').replace(/\.ts$/i, '');
110
110
  const fixtureRoot = path.join(outDir, 'fixture');
111
111
  const resultPath = path.join(fixtureRoot, 'result.json');
@@ -124,12 +124,35 @@ function resolveIntegrationOptions(cwd, rawOptions, env) {
124
124
  contentPath: rawContentPath ? normalizeForDaz(rawContentPath) : '',
125
125
  requireContent: Boolean(options.requireContent),
126
126
  timeoutMs,
127
- appDataPath: options.appDataPath || 'DazScriptFramework/integration-tests',
128
- bundleName: options.bundleName || 'Integration Test Fixture',
127
+ appDataPath: options.appDataPath || defaults.appDataPath,
128
+ bundleName: options.bundleName || defaults.bundleName,
129
+ commandName: defaults.commandName,
129
130
  env,
130
131
  };
131
132
  }
132
133
 
134
+ function resolveIntegrationOptions(cwd, rawOptions, env) {
135
+ return resolveHeadlessOptions(cwd, rawOptions, env, {
136
+ commandName: 'integration',
137
+ envFile: '.env.integration.local',
138
+ timeoutEnv: 'DAZ_TEST_TIMEOUT_MS',
139
+ outDir: './test/integration/out',
140
+ appDataPath: 'DazScriptFramework/integration-tests',
141
+ bundleName: 'Integration Test Fixture',
142
+ });
143
+ }
144
+
145
+ function resolveProbeOptions(cwd, rawOptions, env) {
146
+ return resolveHeadlessOptions(cwd, rawOptions, env, {
147
+ commandName: 'probe',
148
+ envFile: '.env.probe.local',
149
+ timeoutEnv: 'DAZ_PROBE_TIMEOUT_MS',
150
+ outDir: './probes/out',
151
+ appDataPath: 'DazScriptFramework/probes',
152
+ bundleName: 'Probe Fixture',
153
+ });
154
+ }
155
+
133
156
  function run(command, args, options) {
134
157
  const result = spawnSync(command, args, {
135
158
  cwd: options.cwd,
@@ -153,11 +176,65 @@ function run(command, args, options) {
153
176
  return result;
154
177
  }
155
178
 
179
+ function getNpmInvocation(args, platform) {
180
+ const npmArgs = args || [];
181
+ if ((platform || process.platform) !== 'win32') {
182
+ return { command: 'npm', args: npmArgs };
183
+ }
184
+
185
+ const npmCliPath = path.join(path.dirname(process.execPath), 'node_modules', 'npm', 'bin', 'npm-cli.js');
186
+ if (fs.existsSync(npmCliPath)) {
187
+ return { command: process.execPath, args: [npmCliPath].concat(npmArgs) };
188
+ }
189
+
190
+ return { command: 'npm.cmd', args: npmArgs };
191
+ }
192
+
156
193
  function writeFile(filePath, content) {
157
194
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
158
195
  fs.writeFileSync(filePath, content, 'utf8');
159
196
  }
160
197
 
198
+ const fixtureBuildDependencies = [
199
+ '@babel/core',
200
+ '@babel/plugin-proposal-class-properties',
201
+ '@babel/plugin-proposal-decorators',
202
+ '@babel/plugin-transform-arrow-functions',
203
+ '@babel/plugin-transform-block-scoping',
204
+ '@babel/plugin-transform-class-properties',
205
+ '@babel/plugin-transform-private-methods',
206
+ '@babel/plugin-transform-private-property-in-object',
207
+ '@babel/preset-env',
208
+ '@babel/preset-typescript',
209
+ 'babel-core',
210
+ 'babel-loader',
211
+ 'babel-plugin-transform-class-properties',
212
+ 'babel-plugin-transform-typescript-metadata',
213
+ 'glob',
214
+ 'ts-loader',
215
+ 'tsconfig-paths-webpack-plugin',
216
+ 'typescript',
217
+ 'webpack',
218
+ ];
219
+
220
+ function getFixtureBuildDependencies(frameworkRoot) {
221
+ const frameworkPackageJson = JSON.parse(
222
+ fs.readFileSync(path.join(frameworkRoot, 'package.json'), 'utf8')
223
+ );
224
+ const availableDependencies = {
225
+ ...(frameworkPackageJson.dependencies || {}),
226
+ ...(frameworkPackageJson.devDependencies || {}),
227
+ };
228
+
229
+ return fixtureBuildDependencies.reduce((result, dependencyName) => {
230
+ const version = availableDependencies[dependencyName];
231
+ if (version) {
232
+ result[dependencyName] = version;
233
+ }
234
+ return result;
235
+ }, {});
236
+ }
237
+
161
238
  function buildFixtureProject(options) {
162
239
  fs.rmSync(options.fixtureRoot, { recursive: true, force: true });
163
240
  fs.mkdirSync(path.join(options.fixtureRoot, 'src'), { recursive: true });
@@ -171,7 +248,7 @@ function buildFixtureProject(options) {
171
248
  'dazscript-framework': `file:${options.frameworkRoot}`,
172
249
  'dazscript-types': '^1.0.1',
173
250
  },
174
- devDependencies: {},
251
+ devDependencies: getFixtureBuildDependencies(options.frameworkRoot),
175
252
  }, null, 2));
176
253
 
177
254
  writeFile(path.join(options.fixtureRoot, 'dazscript.config.ts'), [
@@ -223,8 +300,10 @@ function buildFixtureProject(options) {
223
300
  }, null, 2));
224
301
 
225
302
  fs.copyFileSync(options.fixturePath, path.join(options.fixtureRoot, 'src', `${options.fixtureName}.dsa.ts`));
226
- run('npm', ['install', '--ignore-scripts'], { cwd: options.fixtureRoot });
227
- run('npm', ['run', 'build'], { cwd: options.fixtureRoot });
303
+ const npmInstall = getNpmInvocation(['install', '--ignore-scripts']);
304
+ run(npmInstall.command, npmInstall.args, { cwd: options.fixtureRoot });
305
+ const npmBuild = getNpmInvocation(['run', 'build']);
306
+ run(npmBuild.command, npmBuild.args, { cwd: options.fixtureRoot });
228
307
  }
229
308
 
230
309
  function runDazFixture(options) {
@@ -250,7 +329,7 @@ function runDazFixture(options) {
250
329
  const command = useWine ? 'wine' : options.dazStudioExe;
251
330
  const commandArgs = useWine ? [options.dazStudioExe].concat(dazArgs) : dazArgs;
252
331
 
253
- console.log(`[dazscript integration] launching DAZ: ${useWine ? `${command} ${options.dazStudioExe}` : command}`);
332
+ console.log(`[dazscript ${options.commandName || 'integration'}] launching DAZ: ${useWine ? `${command} ${options.dazStudioExe}` : command}`);
254
333
  run(command, commandArgs, {
255
334
  cwd: options.fixtureRoot,
256
335
  env: options.env,
@@ -279,6 +358,19 @@ function readIntegrationResult(resultPath) {
279
358
  return result;
280
359
  }
281
360
 
361
+ function readProbeResult(resultPath) {
362
+ if (!fs.existsSync(resultPath)) {
363
+ throw new Error(`DAZ did not write result JSON: ${resultPath}`);
364
+ }
365
+
366
+ try {
367
+ return JSON.parse(fs.readFileSync(resultPath, 'utf8'));
368
+ }
369
+ catch (error) {
370
+ throw new Error(`could not parse result JSON: ${error}`);
371
+ }
372
+ }
373
+
282
374
  async function runIntegration(rawOptions, injectedEnv, cwd) {
283
375
  const env = { ...(injectedEnv || process.env) };
284
376
  const projectRoot = path.resolve(cwd || process.cwd());
@@ -293,12 +385,31 @@ async function runIntegration(rawOptions, injectedEnv, cwd) {
293
385
  return result;
294
386
  }
295
387
 
388
+ async function runProbe(rawOptions, injectedEnv, cwd) {
389
+ const env = { ...(injectedEnv || process.env) };
390
+ const projectRoot = path.resolve(cwd || process.cwd());
391
+ const envFile = path.resolve(projectRoot, (rawOptions && rawOptions.envFile) || '.env.probe.local');
392
+ loadEnvFile(envFile, env);
393
+
394
+ const options = resolveProbeOptions(projectRoot, rawOptions, env);
395
+ buildFixtureProject(options);
396
+ runDazFixture(options);
397
+ const result = readProbeResult(options.resultPath);
398
+ console.log(`[dazscript probe] result: ${options.resultPath}`);
399
+ return result;
400
+ }
401
+
296
402
  module.exports = {
297
403
  loadEnvFile,
298
404
  normalizeForDaz,
405
+ getNpmInvocation,
406
+ getFixtureBuildDependencies,
299
407
  readIntegrationResult,
408
+ readProbeResult,
300
409
  resolveIntegrationOptions,
410
+ resolveProbeOptions,
301
411
  buildFixtureProject,
302
412
  runDazFixture,
303
413
  runIntegration,
414
+ runProbe,
304
415
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dazscript-framework",
3
- "version": "1.0.31",
3
+ "version": "1.0.33",
4
4
  "author": "Freddy Diaz",
5
5
  "license": "MPL-2.0",
6
6
  "description": "",
@@ -3,7 +3,7 @@ import os from 'node:os'
3
3
  import path from 'node:path'
4
4
  import { afterEach, describe, expect, it } from 'vitest'
5
5
 
6
- const { initIntegrationTests, initUnitTests } = require('../../dist/scripts/init')
6
+ const { initIntegrationTests, initProbes, initUnitTests } = require('../../dist/scripts/init')
7
7
 
8
8
  const tempDirs: string[] = []
9
9
 
@@ -74,6 +74,48 @@ describe('init integration tests', () => {
74
74
  })
75
75
  })
76
76
 
77
+ describe('init probes', () => {
78
+ it('creates the probe fixture, docs, env examples, npm script, and ignore entries', () => {
79
+ const projectDir = makeProject('probe-test')
80
+ const fixtureBaseName = toFixtureName(projectDir)
81
+
82
+ initProbes(projectDir, { force: false })
83
+
84
+ const fixturePath = path.join(projectDir, `probes/fixtures/${fixtureBaseName}-scene.dsa.ts`)
85
+ const packageJson = JSON.parse(fs.readFileSync(path.join(projectDir, 'package.json'), 'utf8'))
86
+ const gitignore = fs.readFileSync(path.join(projectDir, '.gitignore'), 'utf8')
87
+
88
+ expect(fs.existsSync(fixturePath)).toBe(true)
89
+ expect(fs.existsSync(path.join(projectDir, 'probes/README.md'))).toBe(true)
90
+ expect(fs.existsSync(path.join(projectDir, '.env.probe.linux.example'))).toBe(true)
91
+ expect(fs.existsSync(path.join(projectDir, '.env.probe.windows.example'))).toBe(true)
92
+ expect(packageJson.scripts.probe).toBe(
93
+ `dazscript probe --fixture ./probes/fixtures/${fixtureBaseName}-scene.dsa.ts`
94
+ )
95
+ expect(gitignore).toContain('probes/out/')
96
+ expect(gitignore).toContain('.env.probe.local')
97
+ })
98
+
99
+ it('does not replace an existing probe script or duplicate ignore entries', () => {
100
+ const projectDir = makeProject('existing-probe')
101
+ const packageJsonPath = path.join(projectDir, 'package.json')
102
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'))
103
+ packageJson.scripts.probe = 'custom probe'
104
+ fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2))
105
+ fs.writeFileSync(path.join(projectDir, '.gitignore'), 'probes/out/\n')
106
+
107
+ initProbes(projectDir, { force: false })
108
+ initProbes(projectDir, { force: false })
109
+
110
+ const updatedPackageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'))
111
+ const gitignore = fs.readFileSync(path.join(projectDir, '.gitignore'), 'utf8')
112
+
113
+ expect(updatedPackageJson.scripts.probe).toBe('custom probe')
114
+ expect(gitignore.match(/probes\/out\//g)?.length).toBe(1)
115
+ expect(gitignore.match(/\.env\.probe\.local/g)?.length).toBe(1)
116
+ })
117
+ })
118
+
77
119
  describe('init unit tests', () => {
78
120
  it('creates Vitest config, sample test, docs, npm scripts, and dev dependency', () => {
79
121
  const projectDir = makeProject('unit-test')
@@ -5,8 +5,12 @@ import { afterEach, describe, expect, it } from 'vitest'
5
5
 
6
6
  const {
7
7
  loadEnvFile,
8
+ getFixtureBuildDependencies,
9
+ getNpmInvocation,
8
10
  readIntegrationResult,
11
+ readProbeResult,
9
12
  resolveIntegrationOptions,
13
+ resolveProbeOptions,
10
14
  } = require('../../dist/scripts/integration')
11
15
 
12
16
  const tempDirs: string[] = []
@@ -78,6 +82,56 @@ describe('integration option resolution', () => {
78
82
  })
79
83
  })
80
84
 
85
+ describe('probe option resolution', () => {
86
+ it('uses probe-specific env and output defaults', () => {
87
+ const projectDir = makeProject()
88
+ fs.mkdirSync(path.join(projectDir, 'probes/fixtures'), { recursive: true })
89
+ fs.writeFileSync(path.join(projectDir, 'probes/fixtures/scene.dsa.ts'), 'action({}, function() {})\n')
90
+
91
+ const options = resolveProbeOptions(projectDir, {
92
+ fixture: './probes/fixtures/scene.dsa.ts',
93
+ }, {
94
+ DAZ_STUDIO_EXE: path.join(projectDir, 'DAZStudio.exe'),
95
+ DAZ_PROBE_TIMEOUT_MS: '123456',
96
+ })
97
+
98
+ expect(options.envFile).toBe(path.join(projectDir, '.env.probe.local'))
99
+ expect(options.outDir).toBe(path.join(projectDir, 'probes/out'))
100
+ expect(options.fixtureRoot).toBe(path.join(projectDir, 'probes/out/fixture'))
101
+ expect(options.resultPath).toBe(path.join(projectDir, 'probes/out/fixture/result.json'))
102
+ expect(options.timeoutMs).toBe(123456)
103
+ expect(options.commandName).toBe('probe')
104
+ })
105
+ })
106
+
107
+ describe('integration command resolution', () => {
108
+ it('uses node plus npm-cli on Windows so child_process can spawn without a shell', () => {
109
+ const invocation = getNpmInvocation(['--version'], 'win32')
110
+
111
+ expect(invocation.command).toMatch(/node(\.exe)?$/i)
112
+ expect(invocation.args[0]).toMatch(/npm-cli\.js$/)
113
+ expect(invocation.args[1]).toBe('--version')
114
+ })
115
+
116
+ it('uses npm on non-Windows platforms', () => {
117
+ expect(getNpmInvocation(['--version'], 'linux')).toEqual({
118
+ command: 'npm',
119
+ args: ['--version']
120
+ })
121
+ })
122
+ })
123
+
124
+ describe('integration fixture build dependencies', () => {
125
+ it('includes webpack loader dependencies needed by generated fixture projects', () => {
126
+ const dependencies = getFixtureBuildDependencies(path.resolve(__dirname, '../..'))
127
+
128
+ expect(dependencies['babel-loader']).toBeTruthy()
129
+ expect(dependencies['ts-loader']).toBeTruthy()
130
+ expect(dependencies['webpack']).toBeTruthy()
131
+ expect(dependencies['typescript']).toBeTruthy()
132
+ })
133
+ })
134
+
81
135
  describe('integration result reader', () => {
82
136
  it('accepts successful result JSON', () => {
83
137
  const projectDir = makeProject()
@@ -95,3 +149,21 @@ describe('integration result reader', () => {
95
149
  expect(() => readIntegrationResult(resultPath)).toThrow(/bad frame/)
96
150
  })
97
151
  })
152
+
153
+ describe('probe result reader', () => {
154
+ it('accepts arbitrary readable probe JSON without ok assertions', () => {
155
+ const projectDir = makeProject()
156
+ const resultPath = path.join(projectDir, 'result.json')
157
+ fs.writeFileSync(resultPath, JSON.stringify({
158
+ kind: 'daz-headless-probe',
159
+ status: 'inconclusive',
160
+ observations: { interfaceAvailable: false }
161
+ }))
162
+
163
+ expect(readProbeResult(resultPath)).toEqual({
164
+ kind: 'daz-headless-probe',
165
+ status: 'inconclusive',
166
+ observations: { interfaceAvailable: false }
167
+ })
168
+ })
169
+ })