cdp-skill 1.0.7 → 1.0.14

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.
Files changed (47) hide show
  1. package/README.md +80 -35
  2. package/SKILL.md +198 -1344
  3. package/install.js +1 -0
  4. package/package.json +1 -1
  5. package/src/aria/index.js +8 -0
  6. package/src/aria/output-processor.js +173 -0
  7. package/src/aria/role-query.js +1229 -0
  8. package/src/aria/snapshot.js +459 -0
  9. package/src/aria.js +237 -43
  10. package/src/cdp/browser.js +22 -4
  11. package/src/cdp-skill.js +268 -68
  12. package/src/dom/click-executor.js +240 -76
  13. package/src/dom/element-locator.js +34 -25
  14. package/src/dom/fill-executor.js +55 -27
  15. package/src/page/dialog-handler.js +119 -0
  16. package/src/page/page-controller.js +190 -3
  17. package/src/runner/context-helpers.js +33 -55
  18. package/src/runner/execute-dynamic.js +34 -143
  19. package/src/runner/execute-form.js +11 -11
  20. package/src/runner/execute-input.js +2 -2
  21. package/src/runner/execute-interaction.js +99 -120
  22. package/src/runner/execute-navigation.js +11 -26
  23. package/src/runner/execute-query.js +8 -5
  24. package/src/runner/step-executors.js +256 -95
  25. package/src/runner/step-registry.js +1064 -0
  26. package/src/runner/step-validator.js +16 -740
  27. package/src/tests/Aria.test.js +1025 -0
  28. package/src/tests/ContextHelpers.test.js +39 -28
  29. package/src/tests/ExecuteBrowser.test.js +572 -0
  30. package/src/tests/ExecuteDynamic.test.js +34 -736
  31. package/src/tests/ExecuteForm.test.js +700 -0
  32. package/src/tests/ExecuteInput.test.js +540 -0
  33. package/src/tests/ExecuteInteraction.test.js +319 -0
  34. package/src/tests/ExecuteQuery.test.js +820 -0
  35. package/src/tests/FillExecutor.test.js +2 -2
  36. package/src/tests/StepValidator.test.js +222 -76
  37. package/src/tests/TestRunner.test.js +36 -25
  38. package/src/tests/integration.test.js +2 -1
  39. package/src/types.js +9 -9
  40. package/src/utils/backoff.js +118 -0
  41. package/src/utils/cdp-helpers.js +130 -0
  42. package/src/utils/devices.js +140 -0
  43. package/src/utils/errors.js +242 -0
  44. package/src/utils/index.js +65 -0
  45. package/src/utils/temp.js +75 -0
  46. package/src/utils/validators.js +433 -0
  47. package/src/utils.js +14 -1142
@@ -7,13 +7,8 @@ import os from 'os';
7
7
  import {
8
8
  executePageFunction,
9
9
  executePoll,
10
- compilePipeline,
11
- executePipeline,
12
- executeWriteSiteManifest,
13
- loadSiteManifest,
14
- generateLightManifest,
15
- runLightAutoFit,
16
- LIGHT_FIT_SCRIPT
10
+ executeWriteSiteProfile,
11
+ loadSiteProfile
17
12
  } from '../runner/execute-dynamic.js';
18
13
 
19
14
  import { validateStepInternal, validateSteps } from '../runner/step-validator.js';
@@ -50,11 +45,11 @@ function createMockPageController(evalReturnValue, opts = {}) {
50
45
  };
51
46
  }
52
47
 
53
- // Patch SITES_DIR for manifest tests by overriding environment
48
+ // Patch SITES_DIR for profile tests by overriding environment
54
49
  // We use a temp directory to avoid polluting ~/.cdp-skill/sites/
55
50
  const REAL_SITES_DIR = path.join(os.homedir(), '.cdp-skill', 'sites');
56
51
 
57
- async function cleanupTestManifests(domains) {
52
+ async function cleanupTestProfiles(domains) {
58
53
  for (const domain of domains) {
59
54
  const clean = domain.replace(/^www\./, '').replace(/[^a-zA-Z0-9.\-]/g, '_');
60
55
  try {
@@ -288,699 +283,30 @@ describe('executePoll', () => {
288
283
  });
289
284
  });
290
285
 
291
- // ---------------------------------------------------------------------------
292
- // Tests: compilePipeline
293
- // ---------------------------------------------------------------------------
294
-
295
- describe('compilePipeline', () => {
296
- it('should generate valid JS for find+fill', () => {
297
- const js = compilePipeline([{ find: '#name', fill: 'John' }]);
298
- assert.ok(js.includes('document.querySelector'));
299
- assert.ok(js.includes('#name'));
300
- assert.ok(js.includes('John'));
301
- assert.ok(js.includes('nativeSetter'));
302
- assert.ok(js.includes("dispatchEvent(new Event('input'"));
303
- });
304
-
305
- it('should generate valid JS for find+click', () => {
306
- const js = compilePipeline([{ find: '#btn', click: true }]);
307
- assert.ok(js.includes('document.querySelector'));
308
- assert.ok(js.includes('#btn'));
309
- assert.ok(js.includes('.click()'));
310
- });
311
-
312
- it('should generate valid JS for find+type', () => {
313
- const js = compilePipeline([{ find: '#search', type: 'hello' }]);
314
- assert.ok(js.includes('document.querySelector'));
315
- assert.ok(js.includes('#search'));
316
- assert.ok(js.includes('focus'));
317
- assert.ok(js.includes('KeyboardEvent'));
318
- assert.ok(js.includes('hello'));
319
- });
320
-
321
- it('should generate valid JS for find+check', () => {
322
- const js = compilePipeline([{ find: '#agree', check: true }]);
323
- assert.ok(js.includes('document.querySelector'));
324
- assert.ok(js.includes('#agree'));
325
- assert.ok(js.includes('checked'));
326
- assert.ok(js.includes('true'));
327
- });
328
-
329
- it('should generate valid JS for find+select', () => {
330
- const js = compilePipeline([{ find: '#color', select: 'red' }]);
331
- assert.ok(js.includes('document.querySelector'));
332
- assert.ok(js.includes('#color'));
333
- assert.ok(js.includes('"red"'));
334
- });
335
-
336
- it('should generate valid JS for waitFor', () => {
337
- const js = compilePipeline([{ waitFor: '() => document.querySelector("#loaded")' }]);
338
- assert.ok(js.includes('new Promise'));
339
- assert.ok(js.includes('setInterval'));
340
- assert.ok(js.includes('#loaded'));
341
- });
342
-
343
- it('should use custom timeout for waitFor', () => {
344
- const js = compilePipeline([{ waitFor: '() => true', timeout: 5000 }]);
345
- assert.ok(js.includes('5000'));
346
- });
347
-
348
- it('should generate valid JS for sleep', () => {
349
- const js = compilePipeline([{ sleep: 200 }]);
350
- assert.ok(js.includes('setTimeout'));
351
- assert.ok(js.includes('200'));
352
- });
353
-
354
- it('should generate valid JS for return', () => {
355
- const js = compilePipeline([{ return: '() => document.title' }]);
356
- assert.ok(js.includes('document.title'));
357
- assert.ok(js.includes('value:val'));
358
- });
359
-
360
- it('should throw on unrecognized micro-op', () => {
361
- assert.throws(
362
- () => compilePipeline([{ unknown: true }]),
363
- { message: /unrecognized micro-op/ }
364
- );
365
- });
366
-
367
- it('should combine multiple ops in sequence', () => {
368
- const js = compilePipeline([
369
- { find: '#user', fill: 'Alice' },
370
- { find: '#pass', fill: 'secret' },
371
- { find: '#submit', click: true }
372
- ]);
373
- assert.ok(js.includes('#user'));
374
- assert.ok(js.includes('Alice'));
375
- assert.ok(js.includes('#pass'));
376
- assert.ok(js.includes('secret'));
377
- assert.ok(js.includes('#submit'));
378
- assert.ok(js.includes('.click()'));
379
- assert.ok(js.includes('async function'));
380
- assert.ok(js.includes('completed:true'));
381
- });
382
-
383
- it('should include error handling in generated code', () => {
384
- const js = compilePipeline([{ find: '#el', click: true }]);
385
- assert.ok(js.includes('catch'));
386
- assert.ok(js.includes('failedAt'));
387
- assert.ok(js.includes('completed:false'));
388
- });
389
-
390
- it('should track step index in error info', () => {
391
- const js = compilePipeline([
392
- { find: '#a', fill: 'x' },
393
- { find: '#b', fill: 'y' }
394
- ]);
395
- assert.ok(js.includes('step:0'));
396
- assert.ok(js.includes('step:1'));
397
- });
398
- });
399
-
400
- // ---------------------------------------------------------------------------
401
- // Tests: executePipeline
402
- // ---------------------------------------------------------------------------
403
-
404
- describe('executePipeline', () => {
405
- afterEach(() => { mock.reset(); });
406
-
407
- it('should execute array form', async () => {
408
- const pc = createMockPageController({ completed: true, steps: 2, results: [{ok:true},{ok:true}] });
409
- const result = await executePipeline(pc, [
410
- { find: '#a', fill: 'x' },
411
- { find: '#b', fill: 'y' }
412
- ]);
413
-
414
- assert.strictEqual(result.completed, true);
415
- assert.strictEqual(result.steps, 2);
416
- assert.strictEqual(pc.evaluateInFrame.mock.calls.length, 1);
417
- // Should use awaitPromise: true
418
- assert.strictEqual(pc.evaluateInFrame.mock.calls[0].arguments[1].awaitPromise, true);
419
- });
420
-
421
- it('should execute object form with steps and timeout', async () => {
422
- const pc = createMockPageController({ completed: true, steps: 1, results: [{ok:true}] });
423
- const result = await executePipeline(pc, {
424
- steps: [{ find: '#x', click: true }],
425
- timeout: 5000
426
- });
427
-
428
- assert.strictEqual(result.completed, true);
429
- });
430
-
431
- it('should throw on empty array', async () => {
432
- const pc = createMockPageController(null);
433
- await assert.rejects(
434
- () => executePipeline(pc, []),
435
- { message: /non-empty array/ }
436
- );
437
- });
438
-
439
- it('should throw on non-array params without steps', async () => {
440
- const pc = createMockPageController(null);
441
- await assert.rejects(
442
- () => executePipeline(pc, { timeout: 1000 }),
443
- { message: /non-empty array/ }
444
- );
445
- });
446
-
447
- it('should throw on browser exception', async () => {
448
- const pc = createMockPageController(undefined, { exception: 'TypeError: el is null' });
449
- await assert.rejects(
450
- () => executePipeline(pc, [{ find: '#missing', click: true }]),
451
- { message: /pipeline error/ }
452
- );
453
- });
454
-
455
- it('should timeout when evaluation hangs', async () => {
456
- const pc = {
457
- evaluateInFrame: mock.fn(() => new Promise(() => { /* never resolves */ }))
458
- };
459
- await assert.rejects(
460
- () => executePipeline(pc, { steps: [{ find: '#x', click: true }], timeout: 50 }),
461
- { message: /timed out after 50ms/ }
462
- );
463
- });
464
-
465
- it('should return result value from browser', async () => {
466
- const pipelineResult = { completed: false, failedAt: 0, error: 'not found: #x', results: [] };
467
- const pc = createMockPageController(pipelineResult);
468
- const result = await executePipeline(pc, [{ find: '#x', click: true }]);
469
-
470
- assert.strictEqual(result.completed, false);
471
- assert.strictEqual(result.failedAt, 0);
472
- });
473
- });
474
-
475
- // ---------------------------------------------------------------------------
476
- // Tests: executeWriteSiteManifest and loadSiteManifest
477
- // ---------------------------------------------------------------------------
478
-
479
- describe('executeWriteSiteManifest', () => {
480
- const testDomain = `test-dyn-${Date.now()}.example.com`;
481
-
482
- afterEach(async () => {
483
- await cleanupTestManifests([testDomain]);
484
- });
485
-
486
- it('should write a manifest file', async () => {
487
- const result = await executeWriteSiteManifest({
488
- domain: testDomain,
489
- content: '# Test manifest\nContent here.'
490
- });
491
-
492
- assert.strictEqual(result.written, true);
493
- assert.ok(result.path);
494
- assert.strictEqual(result.domain, testDomain);
495
-
496
- // Verify file was written
497
- const content = await fs.readFile(result.path, 'utf8');
498
- assert.strictEqual(content, '# Test manifest\nContent here.');
499
- });
500
-
501
- it('should require domain', async () => {
502
- await assert.rejects(
503
- () => executeWriteSiteManifest({ content: 'test' }),
504
- { message: /requires domain and content/ }
505
- );
506
- });
507
-
508
- it('should require content', async () => {
509
- await assert.rejects(
510
- () => executeWriteSiteManifest({ domain: 'example.com' }),
511
- { message: /requires domain and content/ }
512
- );
513
- });
514
-
515
- it('should throw on missing params', async () => {
516
- await assert.rejects(
517
- () => executeWriteSiteManifest(null),
518
- { message: /requires domain and content/ }
519
- );
520
- });
521
- });
522
-
523
- describe('loadSiteManifest', () => {
524
- const testDomain = `test-load-${Date.now()}.example.com`;
525
-
526
- afterEach(async () => {
527
- await cleanupTestManifests([testDomain]);
528
- });
529
-
530
- it('should return null when no manifest exists', async () => {
531
- const result = await loadSiteManifest('nonexistent-domain-xyz-12345.com');
532
- assert.strictEqual(result, null);
533
- });
534
-
535
- it('should return content when manifest exists', async () => {
536
- // First write a manifest
537
- await executeWriteSiteManifest({
538
- domain: testDomain,
539
- content: '# Loaded content'
540
- });
541
-
542
- const result = await loadSiteManifest(testDomain);
543
- assert.strictEqual(result, '# Loaded content');
544
- });
545
- });
546
-
547
- // ---------------------------------------------------------------------------
548
- // Tests: generateLightManifest
549
- // ---------------------------------------------------------------------------
550
-
551
- describe('generateLightManifest', () => {
552
- it('should generate markdown with detected frameworks', () => {
553
- const detection = {
554
- react: true,
555
- nextjs: true,
556
- bodyChildCount: 10,
557
- interactiveCount: 25,
558
- usesPushState: true,
559
- hasMain: true,
560
- mainSelector: '#root'
561
- };
562
-
563
- const md = generateLightManifest('example.com', detection);
564
-
565
- assert.ok(md.includes('# example.com'));
566
- assert.ok(md.includes('Fitted:'));
567
- assert.ok(md.includes('light'));
568
- assert.ok(md.includes('Fingerprint: bc10-ic25'));
569
- assert.ok(md.includes('React'));
570
- assert.ok(md.includes('Next.js'));
571
- assert.ok(md.includes('SPA with pushState'));
572
- assert.ok(md.includes('mainContent: `#root`'));
573
- });
574
-
575
- it('should generate markdown with no frameworks', () => {
576
- const detection = {
577
- bodyChildCount: 5,
578
- interactiveCount: 3,
579
- hasMain: false,
580
- mainSelector: null
581
- };
582
-
583
- const md = generateLightManifest('plain.com', detection);
584
-
585
- assert.ok(md.includes('# plain.com'));
586
- assert.ok(md.includes('No major framework'));
587
- assert.ok(md.includes('No <main>'));
588
- assert.ok(md.includes('Fingerprint: bc5-ic3'));
589
- });
590
-
591
- it('should include all recognized frameworks', () => {
592
- const detection = {
593
- react: true,
594
- vue: true,
595
- angular: true,
596
- svelte: true,
597
- jquery: true,
598
- turbo: true,
599
- htmx: true,
600
- nuxt: true,
601
- remix: true,
602
- bodyChildCount: 1,
603
- interactiveCount: 1,
604
- hasMain: false,
605
- mainSelector: null
606
- };
607
-
608
- const md = generateLightManifest('multi.com', detection);
609
-
610
- assert.ok(md.includes('React'));
611
- assert.ok(md.includes('Vue'));
612
- assert.ok(md.includes('Angular'));
613
- assert.ok(md.includes('Svelte'));
614
- assert.ok(md.includes('jQuery'));
615
- assert.ok(md.includes('Turbo'));
616
- assert.ok(md.includes('htmx'));
617
- assert.ok(md.includes('Nuxt'));
618
- assert.ok(md.includes('Remix'));
619
- });
620
-
621
- it('should include service worker info', () => {
622
- const detection = {
623
- hasServiceWorker: true,
624
- bodyChildCount: 1,
625
- interactiveCount: 1,
626
- hasMain: false,
627
- mainSelector: null
628
- };
629
-
630
- const md = generateLightManifest('sw.com', detection);
631
- assert.ok(md.includes('Service Worker'));
632
- });
633
-
634
- it('should include generator meta', () => {
635
- const detection = {
636
- metaGenerator: 'WordPress 6.0',
637
- bodyChildCount: 1,
638
- interactiveCount: 1,
639
- hasMain: false,
640
- mainSelector: null
641
- };
642
-
643
- const md = generateLightManifest('wp.com', detection);
644
- assert.ok(md.includes('Generator: WordPress 6.0'));
645
- });
646
-
647
- it('should include date in format YYYY-MM-DD', () => {
648
- const detection = {
649
- bodyChildCount: 0,
650
- interactiveCount: 0,
651
- hasMain: false,
652
- mainSelector: null
653
- };
654
-
655
- const md = generateLightManifest('date.com', detection);
656
- const datePattern = /\d{4}-\d{2}-\d{2}/;
657
- assert.ok(datePattern.test(md));
658
- });
659
-
660
- it('should include notes section', () => {
661
- const detection = {
662
- bodyChildCount: 0,
663
- interactiveCount: 0,
664
- hasMain: false,
665
- mainSelector: null
666
- };
667
-
668
- const md = generateLightManifest('notes.com', detection);
669
- assert.ok(md.includes('## Notes'));
670
- assert.ok(md.includes('Light fit only'));
671
- });
672
- });
673
-
674
- // ---------------------------------------------------------------------------
675
- // Tests: runLightAutoFit
676
- // ---------------------------------------------------------------------------
677
-
678
- describe('runLightAutoFit', () => {
679
- const testDomain = `autofit-${Date.now()}.example.com`;
680
-
681
- afterEach(async () => {
682
- await cleanupTestManifests([testDomain]);
683
- mock.reset();
684
- });
685
-
686
- it('should return null when manifest already exists', async () => {
687
- // Write a manifest first
688
- await executeWriteSiteManifest({
689
- domain: testDomain,
690
- content: '# existing'
691
- });
692
-
693
- const pc = createMockPageController({});
694
- const result = await runLightAutoFit(pc, `https://${testDomain}/page`);
695
-
696
- assert.strictEqual(result, null);
697
- // evaluateInFrame should NOT have been called since manifest exists
698
- assert.strictEqual(pc.evaluateInFrame.mock.calls.length, 0);
699
- });
700
-
701
- it('should create manifest and return detection info on first visit', async () => {
702
- const uniqueDomain = `autofit-new-${Date.now()}.example.com`;
703
- const detection = {
704
- react: true,
705
- bodyChildCount: 12,
706
- interactiveCount: 30,
707
- hasMain: true,
708
- mainSelector: '#app',
709
- usesPushState: true,
710
- title: 'Test App'
711
- };
712
-
713
- const pc = {
714
- evaluateInFrame: mock.fn(() => Promise.resolve({
715
- result: { value: detection },
716
- exceptionDetails: undefined
717
- }))
718
- };
719
-
720
- const result = await runLightAutoFit(pc, `https://${uniqueDomain}/`);
721
-
722
- assert.ok(result);
723
- assert.ok(result.domain);
724
- assert.strictEqual(result.level, 'light');
725
- assert.ok(result.frameworks.includes('react'));
726
-
727
- // Verify the manifest was written
728
- const manifest = await loadSiteManifest(result.domain);
729
- assert.ok(manifest);
730
- assert.ok(manifest.includes('React'));
731
-
732
- // Cleanup
733
- await cleanupTestManifests([uniqueDomain]);
734
- });
735
-
736
- it('should handle invalid URLs gracefully', async () => {
737
- const pc = createMockPageController({});
738
- const result = await runLightAutoFit(pc, 'not-a-valid-url');
739
-
740
- assert.strictEqual(result, null);
741
- });
742
-
743
- it('should return null when browser detection fails', async () => {
744
- const uniqueDomain = `autofit-fail-${Date.now()}.example.com`;
745
- const pc = createMockPageController(undefined, { exception: 'eval error' });
746
-
747
- const result = await runLightAutoFit(pc, `https://${uniqueDomain}/`);
748
-
749
- assert.strictEqual(result, null);
750
- });
751
-
752
- it('should return null when detection returns no value', async () => {
753
- const uniqueDomain = `autofit-noval-${Date.now()}.example.com`;
754
- const pc = {
755
- evaluateInFrame: mock.fn(() => Promise.resolve({
756
- result: { value: null },
757
- exceptionDetails: undefined
758
- }))
759
- };
760
-
761
- const result = await runLightAutoFit(pc, `https://${uniqueDomain}/`);
762
- assert.strictEqual(result, null);
763
- });
764
- });
765
-
766
- // ---------------------------------------------------------------------------
767
- // Tests: LIGHT_FIT_SCRIPT
768
- // ---------------------------------------------------------------------------
769
-
770
- describe('LIGHT_FIT_SCRIPT', () => {
771
- it('should be a non-empty string', () => {
772
- assert.ok(typeof LIGHT_FIT_SCRIPT === 'string');
773
- assert.ok(LIGHT_FIT_SCRIPT.length > 0);
774
- });
775
-
776
- it('should detect common frameworks', () => {
777
- assert.ok(LIGHT_FIT_SCRIPT.includes('React'));
778
- assert.ok(LIGHT_FIT_SCRIPT.includes('Vue'));
779
- assert.ok(LIGHT_FIT_SCRIPT.includes('Angular'));
780
- assert.ok(LIGHT_FIT_SCRIPT.includes('jQuery'));
781
- });
782
- });
783
-
784
- // ---------------------------------------------------------------------------
785
- // Tests: StepValidator - Dynamic step types
786
- // ---------------------------------------------------------------------------
787
-
788
- describe('StepValidator - pageFunction validation', () => {
789
- it('should accept valid string form', () => {
790
- const errors = validateStepInternal({ pageFunction: '() => document.title' });
791
- assert.strictEqual(errors.length, 0);
792
- });
793
-
794
- it('should accept valid object form', () => {
795
- const errors = validateStepInternal({ pageFunction: { fn: '() => 42', refs: true, timeout: 5000 } });
796
- assert.strictEqual(errors.length, 0);
797
- });
798
-
799
- it('should reject empty string', () => {
800
- const errors = validateStepInternal({ pageFunction: '' });
801
- assert.ok(errors.some(e => e.includes('non-empty function string')));
802
- });
803
-
804
- it('should reject missing fn in object form', () => {
805
- const errors = validateStepInternal({ pageFunction: { timeout: 1000 } });
806
- assert.ok(errors.some(e => e.includes('non-empty fn string')));
807
- });
808
-
809
- it('should reject invalid refs type', () => {
810
- const errors = validateStepInternal({ pageFunction: { fn: '() => 1', refs: 'yes' } });
811
- assert.ok(errors.some(e => e.includes('refs must be a boolean')));
812
- });
813
-
814
- it('should reject invalid timeout', () => {
815
- const errors = validateStepInternal({ pageFunction: { fn: '() => 1', timeout: -100 } });
816
- assert.ok(errors.some(e => e.includes('timeout must be a non-negative')));
817
- });
818
-
819
- it('should reject non-string non-object form', () => {
820
- const errors = validateStepInternal({ pageFunction: 42 });
821
- assert.ok(errors.some(e => e.includes('function string or params object')));
822
- });
823
- });
824
-
825
- describe('StepValidator - poll validation', () => {
826
- it('should accept valid string form', () => {
827
- const errors = validateStepInternal({ poll: '() => document.readyState === "complete"' });
828
- assert.strictEqual(errors.length, 0);
829
- });
830
-
831
- it('should accept valid object form', () => {
832
- const errors = validateStepInternal({ poll: { fn: '() => true', interval: 200, timeout: 10000 } });
833
- assert.strictEqual(errors.length, 0);
834
- });
835
-
836
- it('should reject empty string', () => {
837
- const errors = validateStepInternal({ poll: '' });
838
- assert.ok(errors.some(e => e.includes('non-empty function string')));
839
- });
840
-
841
- it('should reject missing fn', () => {
842
- const errors = validateStepInternal({ poll: { interval: 100 } });
843
- assert.ok(errors.some(e => e.includes('non-empty fn string')));
844
- });
845
-
846
- it('should reject invalid interval', () => {
847
- const errors = validateStepInternal({ poll: { fn: '() => true', interval: -50 } });
848
- assert.ok(errors.some(e => e.includes('interval must be a non-negative')));
849
- });
850
-
851
- it('should reject invalid timeout', () => {
852
- const errors = validateStepInternal({ poll: { fn: '() => true', timeout: 'long' } });
853
- assert.ok(errors.some(e => e.includes('timeout must be a non-negative')));
854
- });
855
-
856
- it('should reject non-string non-object form', () => {
857
- const errors = validateStepInternal({ poll: 123 });
858
- assert.ok(errors.some(e => e.includes('function string or params object')));
859
- });
860
- });
861
-
862
- describe('StepValidator - pipeline validation', () => {
863
- it('should accept valid array of micro-ops', () => {
864
- const errors = validateStepInternal({
865
- pipeline: [
866
- { find: '#name', fill: 'John' },
867
- { find: '#submit', click: true }
868
- ]
869
- });
870
- assert.strictEqual(errors.length, 0);
871
- });
872
-
873
- it('should accept valid object form with steps', () => {
874
- const errors = validateStepInternal({
875
- pipeline: {
876
- steps: [{ find: '#btn', click: true }],
877
- timeout: 5000
878
- }
879
- });
880
- assert.strictEqual(errors.length, 0);
881
- });
882
-
883
- it('should reject empty array', () => {
884
- const errors = validateStepInternal({ pipeline: [] });
885
- assert.ok(errors.some(e => e.includes('non-empty array')));
886
- });
887
-
888
- it('should reject invalid micro-ops without find/waitFor/sleep/return', () => {
889
- const errors = validateStepInternal({ pipeline: [{ something: true }] });
890
- assert.ok(errors.some(e => e.includes('unrecognized micro-op')));
891
- });
892
-
893
- it('should reject find without action', () => {
894
- const errors = validateStepInternal({ pipeline: [{ find: '#el' }] });
895
- assert.ok(errors.some(e => e.includes('find requires an action')));
896
- });
897
-
898
- it('should accept find with fill action', () => {
899
- const errors = validateStepInternal({ pipeline: [{ find: '#el', fill: 'val' }] });
900
- assert.strictEqual(errors.length, 0);
901
- });
902
-
903
- it('should accept find with click action', () => {
904
- const errors = validateStepInternal({ pipeline: [{ find: '#el', click: true }] });
905
- assert.strictEqual(errors.length, 0);
906
- });
907
-
908
- it('should accept find with type action', () => {
909
- const errors = validateStepInternal({ pipeline: [{ find: '#el', type: 'text' }] });
910
- assert.strictEqual(errors.length, 0);
911
- });
912
-
913
- it('should accept find with check action', () => {
914
- const errors = validateStepInternal({ pipeline: [{ find: '#el', check: true }] });
915
- assert.strictEqual(errors.length, 0);
916
- });
917
-
918
- it('should accept find with select action', () => {
919
- const errors = validateStepInternal({ pipeline: [{ find: '#el', select: 'opt' }] });
920
- assert.strictEqual(errors.length, 0);
921
- });
922
-
923
- it('should accept waitFor micro-op', () => {
924
- const errors = validateStepInternal({ pipeline: [{ waitFor: '() => true' }] });
925
- assert.strictEqual(errors.length, 0);
926
- });
927
-
928
- it('should accept sleep micro-op', () => {
929
- const errors = validateStepInternal({ pipeline: [{ sleep: 500 }] });
930
- assert.strictEqual(errors.length, 0);
931
- });
932
-
933
- it('should accept return micro-op', () => {
934
- const errors = validateStepInternal({ pipeline: [{ return: '() => document.title' }] });
935
- assert.strictEqual(errors.length, 0);
936
- });
937
-
938
- it('should reject invalid timeout on object form', () => {
939
- const errors = validateStepInternal({
940
- pipeline: {
941
- steps: [{ find: '#el', click: true }],
942
- timeout: -1
943
- }
944
- });
945
- assert.ok(errors.some(e => e.includes('timeout must be a non-negative')));
946
- });
947
-
948
- it('should report errors for multiple invalid ops', () => {
949
- const errors = validateStepInternal({
950
- pipeline: [
951
- { find: '#a' }, // missing action
952
- { bad: true }, // unrecognized
953
- { find: '#b', fill: 'ok' } // valid
954
- ]
955
- });
956
- assert.ok(errors.length >= 2);
957
- });
958
- });
959
-
960
- describe('StepValidator - writeSiteManifest validation', () => {
286
+ describe('StepValidator - writeSiteProfile validation', () => {
961
287
  it('should accept valid params', () => {
962
288
  const errors = validateStepInternal({
963
- writeSiteManifest: { domain: 'example.com', content: '# manifest' }
289
+ writeSiteProfile: { domain: 'example.com', content: '# profile' }
964
290
  });
965
291
  assert.strictEqual(errors.length, 0);
966
292
  });
967
293
 
968
294
  it('should reject missing domain', () => {
969
295
  const errors = validateStepInternal({
970
- writeSiteManifest: { content: '# manifest' }
296
+ writeSiteProfile: { content: '# profile' }
971
297
  });
972
298
  assert.ok(errors.some(e => e.includes('non-empty domain string')));
973
299
  });
974
300
 
975
301
  it('should reject missing content', () => {
976
302
  const errors = validateStepInternal({
977
- writeSiteManifest: { domain: 'example.com' }
303
+ writeSiteProfile: { domain: 'example.com' }
978
304
  });
979
305
  assert.ok(errors.some(e => e.includes('non-empty content string')));
980
306
  });
981
307
 
982
308
  it('should reject non-object params', () => {
983
- const errors = validateStepInternal({ writeSiteManifest: 'test' });
309
+ const errors = validateStepInternal({ writeSiteProfile: 'test' });
984
310
  assert.ok(errors.some(e => e.includes('requires an object')));
985
311
  });
986
312
  });
@@ -1052,12 +378,9 @@ describe('STEP_TYPES includes dynamic steps', () => {
1052
378
  assert.ok(STEP_TYPES.includes('poll'));
1053
379
  });
1054
380
 
1055
- it('should include pipeline', () => {
1056
- assert.ok(STEP_TYPES.includes('pipeline'));
1057
- });
1058
381
 
1059
- it('should include writeSiteManifest', () => {
1060
- assert.ok(STEP_TYPES.includes('writeSiteManifest'));
382
+ it('should include writeSiteProfile', () => {
383
+ assert.ok(STEP_TYPES.includes('writeSiteProfile'));
1061
384
  });
1062
385
  });
1063
386
 
@@ -1125,33 +448,19 @@ describe('step-executors dispatch for dynamic steps', () => {
1125
448
  assert.ok(result.output.resolved);
1126
449
  });
1127
450
 
1128
- it('should dispatch pipeline step', async () => {
1129
- mockDeps.pageController.evaluateInFrame = mock.fn(() => Promise.resolve({
1130
- result: { value: { completed: true, steps: 1, results: [{ok:true}] } },
1131
- exceptionDetails: undefined
1132
- }));
1133
-
1134
- const result = await executeStep(mockDeps, {
1135
- pipeline: [{ find: '#btn', click: true }]
1136
- });
1137
451
 
1138
- assert.strictEqual(result.action, 'pipeline');
1139
- assert.strictEqual(result.status, 'ok');
1140
- assert.ok(result.output.completed);
1141
- });
1142
-
1143
- it('should dispatch writeSiteManifest step', async () => {
452
+ it('should dispatch writeSiteProfile step', async () => {
1144
453
  const uniqueDomain = `dispatch-test-${Date.now()}.example.com`;
1145
454
  const result = await executeStep(mockDeps, {
1146
- writeSiteManifest: { domain: uniqueDomain, content: '# test' }
455
+ writeSiteProfile: { domain: uniqueDomain, content: '# test' }
1147
456
  });
1148
457
 
1149
- assert.strictEqual(result.action, 'writeSiteManifest');
458
+ assert.strictEqual(result.action, 'writeSiteProfile');
1150
459
  assert.strictEqual(result.status, 'ok');
1151
460
  assert.ok(result.output.written);
1152
461
 
1153
462
  // Cleanup
1154
- await cleanupTestManifests([uniqueDomain]);
463
+ await cleanupTestProfiles([uniqueDomain]);
1155
464
  });
1156
465
  });
1157
466
 
@@ -1315,7 +624,7 @@ describe('step-executors hooks', () => {
1315
624
  });
1316
625
  });
1317
626
 
1318
- describe('step-executors goto manifest integration', () => {
627
+ describe('step-executors goto profile integration', () => {
1319
628
  let mockDeps;
1320
629
  const testDomain = `goto-test-${Date.now()}.example.com`;
1321
630
 
@@ -1330,29 +639,19 @@ describe('step-executors goto manifest integration', () => {
1330
639
  mockDeps = {
1331
640
  pageController: {
1332
641
  evaluateInFrame: mock.fn((expr, opts) => {
1333
- // For the URL check in goto
1334
642
  if (expr === 'window.location.href') {
1335
643
  return Promise.resolve({
1336
644
  result: { value: `https://${testDomain}/page` },
1337
645
  exceptionDetails: undefined
1338
646
  });
1339
647
  }
1340
- // For light auto-fit detection
1341
648
  return Promise.resolve({
1342
- result: {
1343
- value: {
1344
- react: true,
1345
- bodyChildCount: 5,
1346
- interactiveCount: 10,
1347
- hasMain: true,
1348
- mainSelector: 'main',
1349
- title: 'Test'
1350
- }
1351
- },
649
+ result: { value: null },
1352
650
  exceptionDetails: undefined
1353
651
  });
1354
652
  }),
1355
653
  navigate: mock.fn(() => Promise.resolve()),
654
+ waitForNetworkSettle: mock.fn(() => Promise.resolve({ settled: true, pendingCount: 0 })),
1356
655
  getUrl: mock.fn(() => Promise.resolve(`https://${testDomain}/page`)),
1357
656
  session: { send: mockSessionSend }
1358
657
  },
@@ -1365,35 +664,36 @@ describe('step-executors goto manifest integration', () => {
1365
664
  });
1366
665
 
1367
666
  afterEach(async () => {
1368
- await cleanupTestManifests([testDomain]);
667
+ await cleanupTestProfiles([testDomain]);
1369
668
  mock.reset();
1370
669
  });
1371
670
 
1372
- it('should load existing manifest on goto', async () => {
1373
- // Write a manifest first
1374
- await executeWriteSiteManifest({
671
+ it('should load existing profile on goto', async () => {
672
+ // Write a profile first
673
+ await executeWriteSiteProfile({
1375
674
  domain: testDomain,
1376
- content: '# Pre-existing manifest'
675
+ content: '# Pre-existing profile'
1377
676
  });
1378
677
 
1379
678
  const result = await executeStep(mockDeps, { goto: `https://${testDomain}/page` });
1380
679
 
1381
680
  assert.strictEqual(result.action, 'goto');
1382
681
  assert.strictEqual(result.status, 'ok');
1383
- assert.ok(result.siteManifest);
1384
- assert.ok(result.siteManifest.includes('Pre-existing manifest'));
682
+ assert.ok(result.siteProfile);
683
+ assert.ok(result.siteProfile.includes('Pre-existing profile'));
1385
684
  });
1386
685
 
1387
- it('should run light auto-fit on goto for unknown domain', async () => {
1388
- // Make sure no manifest exists
1389
- await cleanupTestManifests([testDomain]);
686
+ it('should return profileAvailable false on goto for unknown domain', async () => {
687
+ // Make sure no profile exists
688
+ await cleanupTestProfiles([testDomain]);
1390
689
 
1391
690
  const result = await executeStep(mockDeps, { goto: `https://${testDomain}/page` });
1392
691
 
1393
692
  assert.strictEqual(result.action, 'goto');
1394
693
  assert.strictEqual(result.status, 'ok');
1395
- // Should have created a fit
1396
- assert.ok(result.fitted || result.siteManifest);
694
+ assert.strictEqual(result.profileAvailable, false);
695
+ assert.strictEqual(result.profileDomain, testDomain);
696
+
1397
697
  });
1398
698
  });
1399
699
 
@@ -1407,8 +707,7 @@ describe('validateSteps for dynamic steps', () => {
1407
707
  { goto: 'https://example.com' },
1408
708
  { pageFunction: '() => document.title' },
1409
709
  { poll: { fn: '() => true', timeout: 5000 } },
1410
- { pipeline: [{ find: '#btn', click: true }] },
1411
- { writeSiteManifest: { domain: 'example.com', content: '# test' } }
710
+ { writeSiteProfile: { domain: 'example.com', content: '# test' } }
1412
711
  ]);
1413
712
  assert.strictEqual(result.valid, true);
1414
713
  assert.strictEqual(result.errors.length, 0);
@@ -1418,10 +717,9 @@ describe('validateSteps for dynamic steps', () => {
1418
717
  const result = validateSteps([
1419
718
  { pageFunction: '' },
1420
719
  { poll: { interval: 100 } },
1421
- { pipeline: [] },
1422
- { writeSiteManifest: { domain: 'x' } }
720
+ { writeSiteProfile: { domain: 'x' } }
1423
721
  ]);
1424
722
  assert.strictEqual(result.valid, false);
1425
- assert.strictEqual(result.errors.length, 4);
723
+ assert.strictEqual(result.errors.length, 3);
1426
724
  });
1427
725
  });