@fraczak/k 6.4.0 → 6.6.0

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.
@@ -9,6 +9,7 @@ import {
9
9
  createState,
10
10
  decodeWire,
11
11
  evaluateInput,
12
+ executeKVM,
12
13
  exportPatternGraph,
13
14
  patternToPropertyList,
14
15
  propertyListToPattern,
@@ -38,6 +39,7 @@ import {
38
39
  runTimedIterations,
39
40
  runTimedIterationsAsync,
40
41
  shouldStrictFail,
42
+ safeDeepEqual,
41
43
  toPlainObject,
42
44
  tryCompileCase,
43
45
  wireInput,
@@ -77,6 +79,9 @@ const iterations = parsePositiveIntEnv("ITERATIONS", 3);
77
79
  // ARM64 options
78
80
  const arm64WarmupIterations = parseNonNegativeIntEnv("ARM64_WARMUP_ITERATIONS", 1);
79
81
  const arm64OptLevel = process.env.ARM64_OPT || "-O2";
82
+ if (!process.env.K_ARENA_CAPACITY_MB && !process.env.K_ARENA_MB) {
83
+ process.env.K_ARENA_CAPACITY_MB = "4096";
84
+ }
80
85
 
81
86
  // LLVM options
82
87
  const llvmWarmupIterations = parseNonNegativeIntEnv("LLVM_WARMUP_ITERATIONS", 1);
@@ -529,3 +534,329 @@ console.log(`Conformance validation: ${validationSummary.join(", ")} cases match
529
534
  if (runLLVM && shouldStrictFail(testSuite)) process.exitCode = 1;
530
535
  if (runWasm && testSuite.some(tc => tc.wasmConformance !== "ok")) process.exitCode = 1;
531
536
  if (runARM64 && testSuite.some(tc => tc.arm64Conformance !== "ok")) process.exitCode = 1;
537
+
538
+ // =========================================================================
539
+ // Squaring Scale & Memory Profiling Benchmark
540
+ // Expression: s = {()x,()y}times; 987654321 s s s s s s s s s s
541
+ // =========================================================================
542
+
543
+ function countBitsValue(val) {
544
+ let count = 0;
545
+ let cur = val?.tag === "+" || val?.tag === "-" ? val.value : val;
546
+ while (cur && (cur.tag === "0" || cur.tag === "1")) {
547
+ count++;
548
+ cur = cur.value;
549
+ }
550
+ return count;
551
+ }
552
+
553
+ function getProcessMemory(pid) {
554
+ try {
555
+ const status = fs.readFileSync(`/proc/${pid}/status`, "utf8");
556
+ const vmrss = status.match(/VmRSS:\s+(\d+)\s+kB/);
557
+ const vmhwm = status.match(/VmHWM:\s+(\d+)\s+kB/);
558
+ return {
559
+ rssMb: vmrss ? Number((parseInt(vmrss[1], 10) / 1024).toFixed(1)) : null,
560
+ peakRssMb: vmhwm ? Number((parseInt(vmhwm[1], 10) / 1024).toFixed(1)) : null
561
+ };
562
+ } catch {
563
+ return { rssMb: null, peakRssMb: null };
564
+ }
565
+ }
566
+
567
+ function getNodeMemory() {
568
+ const mem = process.memoryUsage();
569
+ return {
570
+ heapMb: Number((mem.heapUsed / (1024 * 1024)).toFixed(1)),
571
+ rssMb: Number((mem.rss / (1024 * 1024)).toFixed(1))
572
+ };
573
+ }
574
+
575
+ const squaringSteps = parsePositiveIntEnv("SQUARING_STEPS", 10);
576
+ const squaringInputText = process.env.SQUARING_INPUT || "987654321";
577
+ const squaringStepTimeoutMs = parsePositiveIntEnv("SQUARING_STEP_TIMEOUT_MS", 60000);
578
+
579
+ console.log("\n=================== SQUARING SCALE & MEMORY BENCHMARK ===================");
580
+ console.log(`Expression: s = {()x,()y}times; ${squaringInputText} ${"s ".repeat(squaringSteps).trim()}`);
581
+ console.log(`Evaluation: ${squaringSteps} successive squarings doubling bit length each step`);
582
+
583
+ await evaluateInput("s = {()x,()y}times;", state);
584
+ codes.load(state.codes);
585
+ const sRel = prepareRelation(state, "s", {
586
+ source: `${arithmeticsLibrarySource}\ns = {()x,()y}times;\ns`,
587
+ sourceLabel: "@fraczak/k/Examples/arithmetics.k#s"
588
+ });
589
+
590
+ const sInitialVal = intValue(squaringInputText);
591
+ const { inputWire: sInitWire, inputPattern: sInputPattern } = wireInput(sInitialVal);
592
+
593
+ // Build executables for persistent servers
594
+ let sLlvmServer = null;
595
+ if (runLLVM) {
596
+ const sLlvmCompiled = tryCompileCase({
597
+ object: sRel.object,
598
+ relationName: sRel.relationName,
599
+ relHash: sRel.relHash,
600
+ inputPattern: sInputPattern,
601
+ cacheDir,
602
+ sourceLabel: "@fraczak/k/Examples/arithmetics.k#s"
603
+ });
604
+ if (sLlvmCompiled.status === "ok") {
605
+ sLlvmServer = new PersistentExecutable(sLlvmCompiled.exePath);
606
+ }
607
+ }
608
+
609
+ let sWasmServer = null;
610
+ if (runWasm) {
611
+ try {
612
+ const sWasmPath = path.join(cacheDir, "perf_int_wasm_s.wasm");
613
+ const sWasmExe = path.join(cacheDir, "perf_int_wasm_s.exe");
614
+ const wasmBuf = await compileWasmArtifactFromObject(sRel.object, { entry: "__main__" });
615
+ fs.writeFileSync(sWasmPath, wasmBuf);
616
+ const runBin = fileURLToPath(import.meta.resolve("../backends/wasm/bin/k-wasm-run.mjs"));
617
+ fs.writeFileSync(sWasmExe, `#!/bin/sh\nexec node "${runBin}" "${sWasmPath}" "$@"\n`, { mode: 0o755 });
618
+ sWasmServer = new PersistentExecutable(sWasmExe);
619
+ } catch (e) {
620
+ console.log("Failed to build Wasm executable for 's':", e.message);
621
+ }
622
+ }
623
+
624
+ let sArm64Server = null;
625
+ if (runARM64) {
626
+ try {
627
+ const sArm64Exe = path.join(cacheDir, "perf_int_arm64_s");
628
+ compileARM64ArtifactFromObject(sRel.object, {
629
+ entry: "__main__",
630
+ outputPath: sArm64Exe,
631
+ optLevel: arm64OptLevel
632
+ });
633
+ sArm64Server = new PersistentExecutable(sArm64Exe);
634
+ } catch (e) {
635
+ console.log("Failed to build ARM64 executable for 's':", e.message);
636
+ }
637
+ }
638
+
639
+ const squaringLanes = [];
640
+ if (runBaselines) {
641
+ squaringLanes.push({ id: "jsAware", name: "Native JS (Aware)", type: "jsAware", memLabel: "heap" });
642
+ squaringLanes.push({ id: "jsFree", name: "Native JS (Free)", type: "jsFree", memLabel: "heap" });
643
+ squaringLanes.push({ id: "kvm", name: "kVM (Env-Free)", type: "kvm", memLabel: "heap" });
644
+ }
645
+ if (runLLVM && sLlvmServer) {
646
+ squaringLanes.push({ id: "llvm", name: llvmLaneName(), type: "persistent", server: sLlvmServer, memLabel: "peak RSS" });
647
+ }
648
+ if (runWasm && sWasmServer) {
649
+ squaringLanes.push({ id: "wasm", name: wasmLaneName(), type: "persistent", server: sWasmServer, memLabel: "peak RSS" });
650
+ }
651
+ if (runARM64 && sArm64Server) {
652
+ squaringLanes.push({ id: "arm64", name: arm64LaneName(arm64OptLevel), type: "persistent", server: sArm64Server, memLabel: "peak RSS" });
653
+ }
654
+
655
+ const squaringResults = {};
656
+ for (const lane of squaringLanes) squaringResults[lane.id] = [];
657
+ const stepMetadata = [];
658
+
659
+ try {
660
+ // 1. Persistent child processes (ARM64, Wasm, LLVM)
661
+ for (const lane of squaringLanes.filter(l => l.type === "persistent")) {
662
+ console.log(`==> Running ${lane.name} squaring lane...`);
663
+ let curWire = sInitWire;
664
+ for (let step = 1; step <= squaringSteps; step++) {
665
+ try {
666
+ const t0 = performance.now();
667
+ curWire = await lane.server.request(curWire);
668
+ const t1 = performance.now();
669
+ const timeMs = t1 - t0;
670
+ const mem = getProcessMemory(lane.server.child.pid);
671
+ squaringResults[lane.id].push({ status: "ok", timeMs, memMb: mem.peakRssMb, wire: curWire });
672
+ if (!stepMetadata[step - 1]) {
673
+ stepMetadata[step - 1] = {
674
+ wireLen: curWire.length,
675
+ bits: null,
676
+ canonicalWire: curWire
677
+ };
678
+ }
679
+ if (timeMs > squaringStepTimeoutMs) break;
680
+ } catch (e) {
681
+ squaringResults[lane.id].push({ status: "failed", error: e.message?.split("\n")[0] || String(e) });
682
+ break;
683
+ }
684
+ }
685
+ }
686
+
687
+ // 2. In-process baselines (JS Aware, JS Free, kVM)
688
+ if (runBaselines) {
689
+ // JS Aware
690
+ console.log("==> Running Native JS (Envelope-Aware) squaring lane...");
691
+ let curValAware = sInitialVal;
692
+ run.defs = state;
693
+ for (let step = 1; step <= squaringSteps; step++) {
694
+ try {
695
+ const t0 = performance.now();
696
+ curValAware = run(codes.find, sRel.relDef.def, curValAware, sRel.relDef.typePatternGraph);
697
+ const t1 = performance.now();
698
+ const timeMs = t1 - t0;
699
+ const mem = getNodeMemory();
700
+ squaringResults.jsAware.push({ status: "ok", timeMs, memMb: mem.heapMb, value: curValAware });
701
+ if (stepMetadata[step - 1] && stepMetadata[step - 1].bits == null) {
702
+ stepMetadata[step - 1].bits = countBitsValue(curValAware);
703
+ }
704
+ if (timeMs > squaringStepTimeoutMs) break;
705
+ } catch (e) {
706
+ squaringResults.jsAware.push({ status: "failed", error: e.message?.split("\n")[0] || String(e) });
707
+ break;
708
+ }
709
+ }
710
+
711
+ // JS Free
712
+ console.log("==> Running Native JS (Envelope-Free) squaring lane...");
713
+ let curValFree = sInitialVal;
714
+ run_converged.defs = state;
715
+ for (let step = 1; step <= squaringSteps; step++) {
716
+ try {
717
+ const t0 = performance.now();
718
+ curValFree = run_converged(codes.find, sRel.relDef.def, curValFree, sRel.relDef.typePatternGraph);
719
+ const t1 = performance.now();
720
+ const timeMs = t1 - t0;
721
+ const mem = getNodeMemory();
722
+ squaringResults.jsFree.push({ status: "ok", timeMs, memMb: mem.heapMb, value: curValFree });
723
+ if (stepMetadata[step - 1] && stepMetadata[step - 1].bits == null) {
724
+ stepMetadata[step - 1].bits = countBitsValue(curValFree);
725
+ }
726
+ if (timeMs > squaringStepTimeoutMs) break;
727
+ } catch (e) {
728
+ squaringResults.jsFree.push({ status: "failed", error: e.message?.split("\n")[0] || String(e) });
729
+ break;
730
+ }
731
+ }
732
+
733
+ // kVM
734
+ console.log("==> Running kVM Interpreter (Env-Free) squaring lane...");
735
+ let curValKvm = sInitialVal;
736
+ const kvmContext = { rels: state.rels, findCode: codes.find, options: { envelopeFree: true } };
737
+ for (let step = 1; step <= squaringSteps; step++) {
738
+ try {
739
+ const t0 = performance.now();
740
+ curValKvm = executeKVM(sRel.kvmFunc, curValKvm, kvmContext);
741
+ const t1 = performance.now();
742
+ const timeMs = t1 - t0;
743
+ const mem = getNodeMemory();
744
+ squaringResults.kvm.push({ status: "ok", timeMs, memMb: mem.heapMb, value: curValKvm });
745
+ if (stepMetadata[step - 1] && stepMetadata[step - 1].bits == null) {
746
+ stepMetadata[step - 1].bits = countBitsValue(curValKvm);
747
+ }
748
+ if (timeMs > squaringStepTimeoutMs) break;
749
+ } catch (e) {
750
+ squaringResults.kvm.push({ status: "failed", error: e.message?.split("\n")[0] || String(e) });
751
+ break;
752
+ }
753
+ }
754
+ }
755
+
756
+ } finally {
757
+ if (sArm64Server) sArm64Server.close();
758
+ if (sWasmServer) sWasmServer.close();
759
+ if (sLlvmServer) sLlvmServer.close();
760
+ }
761
+
762
+ // For any remaining steps without bit counts, decode canonicalWire safely to get bits:
763
+ for (let step = 1; step <= squaringSteps; step++) {
764
+ const meta = stepMetadata[step - 1];
765
+ if (meta && meta.bits == null && meta.canonicalWire) {
766
+ try {
767
+ const decoded = decodeWire(meta.canonicalWire).value;
768
+ meta.bits = countBitsValue(decoded);
769
+ } catch {}
770
+ }
771
+ }
772
+
773
+ // Display results table
774
+ console.log("\n======================== SQUARING SCALING & MEMORY BENCHMARK ========================");
775
+ console.log(`Expression: s = {()x,()y}times; ${squaringInputText} ${"s ".repeat(squaringSteps).trim()}`);
776
+ console.log(`Initial: ${squaringInputText} (${countBitsValue(sInitialVal)} bits)`);
777
+ console.log("----------------------------------------------------------------------------------------------------------------------------------");
778
+ const headerCols = ["Step", "Bits", "Wire(B)", ...squaringLanes.map(l => `${l.name} (${l.memLabel})`)];
779
+ const colWidths = [4, 6, 7, ...squaringLanes.map(l => Math.max(22, l.name.length + l.memLabel.length + 5))];
780
+ console.log(headerCols.map((h, i) => i < 3 ? h.padStart(colWidths[i]) : h.padEnd(colWidths[i])).join(" | "));
781
+ console.log(colWidths.map(w => "-".repeat(w)).join("-|-"));
782
+
783
+ for (let step = 1; step <= squaringSteps; step++) {
784
+ const meta = stepMetadata[step - 1] || { bits: "?", wireLen: "?" };
785
+ const cols = [
786
+ String(step).padStart(colWidths[0]),
787
+ String(meta.bits ?? "?").padStart(colWidths[1]),
788
+ String(meta.wireLen ?? "?").padStart(colWidths[2])
789
+ ];
790
+ for (let li = 0; li < squaringLanes.length; li++) {
791
+ const lane = squaringLanes[li];
792
+ const res = squaringResults[lane.id][step - 1];
793
+ const w = colWidths[3 + li];
794
+ if (!res) {
795
+ cols.push("-".padEnd(w));
796
+ } else if (res.status === "ok") {
797
+ const t = res.timeMs < 10 ? `${res.timeMs.toFixed(2)}ms` : `${res.timeMs.toFixed(1)}ms`;
798
+ const m = res.memMb != null ? `${res.memMb}MB` : "";
799
+ cols.push(`${t} / ${m}`.padEnd(w));
800
+ } else {
801
+ let errText = res.error;
802
+ if (errText.includes("Maximum call stack")) {
803
+ errText = "Stack overflow (V8)";
804
+ } else if (errText.includes("status null") || errText.includes("failed with status")) {
805
+ errText = "Arena limit reached";
806
+ }
807
+ cols.push(errText.slice(0, w - 1).padEnd(w));
808
+ }
809
+ }
810
+ console.log(cols.join(" | "));
811
+ }
812
+ console.log("==================================================================================================================================");
813
+
814
+ // Conformance check across all computed steps
815
+ let squaringConformanceOk = true;
816
+ for (let step = 1; step <= squaringSteps; step++) {
817
+ const meta = stepMetadata[step - 1];
818
+ if (!meta?.canonicalWire) continue;
819
+ for (const lane of squaringLanes) {
820
+ const res = squaringResults[lane.id][step - 1];
821
+ if (res?.status === "ok") {
822
+ if (res.wire) {
823
+ if (Buffer.compare(res.wire, meta.canonicalWire) !== 0) {
824
+ squaringConformanceOk = false;
825
+ console.log(`Squaring conformance mismatch on step ${step} (${lane.name})`);
826
+ }
827
+ } else if (res.value) {
828
+ try {
829
+ const wireVal = decodeWire(meta.canonicalWire).value;
830
+ if (!safeDeepEqual(res.value, wireVal)) {
831
+ squaringConformanceOk = false;
832
+ console.log(`Squaring conformance mismatch on step ${step} (${lane.name})`);
833
+ }
834
+ } catch {}
835
+ }
836
+ }
837
+ }
838
+ }
839
+
840
+ console.log("\nSquaring Performance & Scaling Summary:");
841
+ for (const lane of squaringLanes) {
842
+ const okSteps = squaringResults[lane.id].filter(r => r.status === "ok");
843
+ const maxStep = okSteps.length;
844
+ const lastOk = okSteps[okSteps.length - 1];
845
+ const peakMem = okSteps.reduce((max, r) => Math.max(max, r.memMb || 0), 0);
846
+ const failureReason = squaringResults[lane.id][maxStep]?.error;
847
+ let statusDetail = "";
848
+ if (failureReason) {
849
+ if (failureReason.includes("Maximum call stack")) statusDetail = " (V8 call stack overflow)";
850
+ else if (failureReason.includes("status null") || failureReason.includes("failed with status")) statusDetail = " (arena capacity limit reached)";
851
+ else statusDetail = ` (${failureReason.slice(0, 30)})`;
852
+ }
853
+ const timeStr = lastOk ? ` (step ${maxStep} in ${lastOk.timeMs < 10 ? lastOk.timeMs.toFixed(2) : lastOk.timeMs.toFixed(1)}ms)` : "";
854
+ console.log(` - ${lane.name.padEnd(32)}: reached Step ${String(maxStep).padStart(2)}/${squaringSteps}${timeStr}, peak ${lane.memLabel}: ${peakMem}MB${statusDetail}`);
855
+ }
856
+
857
+ if (squaringConformanceOk) {
858
+ console.log("\nSquaring Conformance: All completed squaring steps match expected values across all backends!\n");
859
+ } else {
860
+ console.log("\nSquaring Conformance: Warning, some step outputs differed between backends!\n");
861
+ process.exitCode = 1;
862
+ }
@@ -26,6 +26,7 @@ const tests = [
26
26
  ["conformance/run.mjs", node, ["conformance/run.mjs"]],
27
27
  ["tests/test-repl.mjs", node, ["tests/test-repl.mjs"]],
28
28
  ["tests/test-repl-html.mjs", node, ["tests/test-repl-html.mjs"]],
29
+ ["tests/test-tree-view.mjs", node, ["tests/test-tree-view.mjs"]],
29
30
  ["tests/test-kvm.mjs", node, ["tests/test-kvm.mjs"]],
30
31
  ["tests/test-kvm-polymorphic.mjs", node, ["tests/test-kvm-polymorphic.mjs"]],
31
32
  ["tests/test-arithmetics.mjs", node, ["tests/test-arithmetics.mjs"]],
@@ -287,6 +287,147 @@ if (chromiumBin) {
287
287
  })()`);
288
288
  assert.strictEqual(selectionCheck, true, "Selection in terminal output should hold after click");
289
289
 
290
+ // === Test Tree View Functionality ===
291
+ console.log(" Verifying Tree View: initial depth <= 3, folding/unfolding, mode toggle, and pattern cycles...");
292
+
293
+ // 1. Evaluate a 5-level deep nested product: { { { { {} e } d } c } b }
294
+ await evaluateAsync(`window.kRepl.executeCommand("{ { { { {} e } d } c } b }")`);
295
+
296
+ const treeCheck = await evaluateAsync(`(() => {
297
+ const entries = document.querySelectorAll(".terminal-entry");
298
+ const lastEntry = entries[entries.length - 1];
299
+ const container = lastEntry.querySelector(".k-entry-container");
300
+ if (!container) return { error: "No .k-entry-container found" };
301
+
302
+ const treeView = container.querySelector(".k-tree-view");
303
+ const rawView = container.querySelector(".k-raw-view");
304
+ const treeBtn = container.querySelector(".k-btn-mode.active");
305
+ const rawBtn = container.querySelectorAll(".k-btn-mode")[1];
306
+
307
+ // Check Value section
308
+ const valSection = container.querySelector(".k-tree-value-section");
309
+ if (!valSection) return { error: "No value section" };
310
+
311
+ // Check product details nodes hierarchy
312
+ // Level 1: root product (depth 1)
313
+ const level1 = valSection.querySelector(".k-tree-section-body > .k-tree-node.k-tree-product");
314
+ if (!level1 || !level1.open) return { error: "Level 1 product should be open" };
315
+
316
+ // Level 2: .b product (depth 2)
317
+ const level2 = level1.querySelector(".k-tree-children .k-tree-node.k-tree-product");
318
+ if (!level2 || !level2.open) return { error: "Level 2 product should be open" };
319
+
320
+ // Level 3: .c product (depth 3)
321
+ const level3 = level2.querySelector(".k-tree-children .k-tree-node.k-tree-product");
322
+ if (!level3 || !level3.open) return { error: "Level 3 product should be open" };
323
+
324
+ // Level 4: .d product (depth 4) -> MUST BE COLLAPSED (open === false)
325
+ const level4 = level3.querySelector(".k-tree-children .k-tree-node.k-tree-product");
326
+ if (!level4) return { error: "Level 4 product node missing" };
327
+ if (level4.open) return { error: "Level 4 product should be collapsed (open === false)" };
328
+
329
+ // Check pattern section exists and has cycle or vardef
330
+ const patSection = container.querySelector(".k-tree-pat-section");
331
+ if (!patSection) return { error: "No pattern section" };
332
+
333
+ // Test Mode Toggle: click Raw
334
+ rawBtn.click();
335
+ const rawVisible = rawView.style.display !== "none" && treeView.style.display === "none";
336
+
337
+ // Click Tree back
338
+ container.querySelector(".k-btn-mode").click();
339
+ const treeVisible = treeView.style.display !== "none" && rawView.style.display === "none";
340
+
341
+ // Test Unfolding Level 4
342
+ level4.open = true;
343
+ level4.dispatchEvent(new Event("toggle"));
344
+
345
+ // After toggle, level 4's children should be populated with .e
346
+ const level4Text = level4.textContent;
347
+ const unfoldedHasE = level4Text.includes(".e") || level4Text.includes("42");
348
+
349
+ return {
350
+ success: true,
351
+ rawVisible,
352
+ treeVisible,
353
+ unfoldedHasE
354
+ };
355
+ })()`);
356
+
357
+ assert.strictEqual(treeCheck.success, true, `Tree view validation failed: ${treeCheck.error}`);
358
+ assert.strictEqual(treeCheck.rawVisible, true, "Clicking Raw should show raw view and hide tree view");
359
+ assert.strictEqual(treeCheck.treeVisible, true, "Clicking Tree should restore tree view");
360
+ assert.strictEqual(treeCheck.unfoldedHasE, true, "Unfolding level 4 should lazily populate its children");
361
+
362
+ // 2. Test recursive pattern tree with cycle detection
363
+ await evaluateAsync(`window.kRepl.executeCommand("10")`);
364
+ const patCycleCheck = await evaluateAsync(`(() => {
365
+ const entries = document.querySelectorAll(".terminal-entry");
366
+ const lastEntry = entries[entries.length - 1];
367
+ const patSection = lastEntry.querySelector(".k-tree-pat-section");
368
+ if (!patSection) return { error: "No pattern section for 10" };
369
+
370
+ const cycleBadges = patSection.querySelectorAll(".k-badge-cycle");
371
+ const varDefs = patSection.querySelectorAll(".k-pat-vardef");
372
+
373
+ return {
374
+ hasCycleBadge: cycleBadges.length > 0,
375
+ hasVarDef: varDefs.length > 0
376
+ };
377
+ })()`);
378
+
379
+ assert.strictEqual(patCycleCheck.hasCycleBadge, true, "Pattern tree for recursive type should have cycle badge");
380
+ assert.strictEqual(patCycleCheck.hasVarDef, true, "Pattern tree for recursive type should have variable definition (=X0)");
381
+
382
+ // 3. Test variant chain depth limits and Expand All / Collapse All on 10
383
+ const variantDepthAndActionsCheck = await evaluateAsync(`(() => {
384
+ const entries = document.querySelectorAll(".terminal-entry");
385
+ const lastEntry = entries[entries.length - 1];
386
+ const valSection = lastEntry.querySelector(".k-tree-value-section");
387
+
388
+ // Hierarchy for 10 (variant chain: + -> 1 -> 0 -> 1 -> ...)
389
+ const v1 = valSection?.querySelector(".k-tree-section-body > .k-tree-node.k-tree-variant");
390
+ const v2 = v1?.querySelector(".k-tree-children .k-tree-node.k-tree-variant");
391
+ const v3 = v2?.querySelector(".k-tree-children .k-tree-node.k-tree-variant");
392
+ const v4 = v3?.querySelector(".k-tree-children .k-tree-node.k-tree-variant");
393
+
394
+ const v1Open = !!v1?.open;
395
+ const v2Open = !!v2?.open;
396
+ const v3Open = !!v3?.open;
397
+ const v4Collapsed = v4 ? !v4.open : false;
398
+
399
+ // Click Expand All
400
+ const actionBtns = lastEntry.querySelectorAll(".k-btn-action");
401
+ const expandBtn = actionBtns[0];
402
+ const collapseBtn = actionBtns[1];
403
+
404
+ expandBtn.click();
405
+ const allOpenAfterExpand = v4.open;
406
+
407
+ // Click Collapse All
408
+ collapseBtn.click();
409
+ const newV4 = valSection?.querySelector(
410
+ ".k-tree-section-body > .k-tree-node.k-tree-variant .k-tree-children .k-tree-node.k-tree-variant .k-tree-children .k-tree-node.k-tree-variant .k-tree-children .k-tree-node.k-tree-variant"
411
+ );
412
+ const v4CollapsedAgain = newV4 ? !newV4.open : false;
413
+
414
+ return {
415
+ v1Open,
416
+ v2Open,
417
+ v3Open,
418
+ v4Collapsed,
419
+ allOpenAfterExpand,
420
+ v4CollapsedAgain
421
+ };
422
+ })()`);
423
+
424
+ assert.strictEqual(variantDepthAndActionsCheck.v1Open, true, "Variant level 1 must be open");
425
+ assert.strictEqual(variantDepthAndActionsCheck.v2Open, true, "Variant level 2 must be open");
426
+ assert.strictEqual(variantDepthAndActionsCheck.v3Open, true, "Variant level 3 must be open");
427
+ assert.strictEqual(variantDepthAndActionsCheck.v4Collapsed, true, "Variant level 4 must be collapsed by default");
428
+ assert.strictEqual(variantDepthAndActionsCheck.allOpenAfterExpand, true, "Expand button should expand collapsed nodes");
429
+ assert.strictEqual(variantDepthAndActionsCheck.v4CollapsedAgain, true, "Collapse button should reset deeper nodes to collapsed");
430
+
290
431
  ws.close();
291
432
  console.log(" Browser execution verified successfully!");
292
433
  } finally {