@fraczak/k 7.2.0 → 8.1.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.
Files changed (46) hide show
  1. package/CHANGELOG.md +90 -0
  2. package/DOCS/BIG_PICTURE.md +54 -7
  3. package/DOCS/CODECS.md +21 -13
  4. package/DOCS/CODEC_ARCHITECTURE.md +1 -1
  5. package/DOCS/DICTIONARY.md +13 -4
  6. package/DOCS/FILE_FORMATS.md +3 -2
  7. package/DOCS/JSON_APPROACHES_COMPARISON.md +302 -0
  8. package/DOCS/NAMED_PATTERNS.md +317 -0
  9. package/DOCS/PATTERNS.md +59 -11
  10. package/DOCS/REPL.md +13 -2
  11. package/DOCS/WEB_REPL_REQUIREMENTS.md +3 -2
  12. package/Examples/arithmetics.k +22 -17
  13. package/Examples/bnat.k +21 -11
  14. package/Examples/buda.k +37 -20
  15. package/Examples/byte.k +3 -1
  16. package/Examples/code.k +38 -39
  17. package/Examples/djson.k +789 -0
  18. package/Examples/esc.k +1 -1
  19. package/Examples/karatsuba-mult.k +16 -13
  20. package/Examples/list.k +8 -4
  21. package/Examples/nat.k +17 -11
  22. package/Examples/poly.k +11 -3
  23. package/Examples/vhdl_to_k.k +43 -31
  24. package/README.md +40 -16
  25. package/browser/repl-browser.mjs +2 -0
  26. package/browser/tree-view.mjs +35 -16
  27. package/browser/vfs-data.mjs +10 -8
  28. package/codecs/README.md +1 -1
  29. package/codecs/djson.mjs +375 -84
  30. package/core.k +79 -115
  31. package/index.mjs +3 -0
  32. package/named-patterns.mjs +267 -0
  33. package/object.mjs +31 -3
  34. package/package.json +1 -1
  35. package/parser.jison +68 -5
  36. package/parser.mjs +111 -60
  37. package/pretty.mjs +5 -0
  38. package/repl.mjs +188 -22
  39. package/scripts/build-repl-html.mjs +58 -4
  40. package/scripts/run-tests.mjs +10 -0
  41. package/symbol-table.mjs +20 -2
  42. package/tests/test-codecs.mjs +30 -17
  43. package/tests/test-djson-methods.mjs +285 -0
  44. package/tests/test-named-patterns.mjs +189 -0
  45. package/tests/test-repl-html.mjs +47 -4
  46. package/tests/test-repl.mjs +149 -0
@@ -0,0 +1,285 @@
1
+ // tests/test-djson-methods.mjs
2
+ // Test suite for standard dynamic JSON methods working on json? type.
3
+
4
+ import assert from "node:assert/strict";
5
+ import { readFileSync } from "node:fs";
6
+ import path from "node:path";
7
+ import { fileURLToPath } from "node:url";
8
+
9
+ import k from "../index.mjs";
10
+ import { Value } from "../Value.mjs";
11
+ import {
12
+ fromJsonValue,
13
+ toJsonValue
14
+ } from "../codecs/djson.mjs";
15
+ import {
16
+ textToStringValue,
17
+ stringValueToText
18
+ } from "../codecs/runtime/unicode-string.mjs";
19
+
20
+ const root = path.resolve(
21
+ path.dirname(fileURLToPath(import.meta.url)),
22
+ ".."
23
+ );
24
+ const libPath = path.join(root, "Examples", "djson.k");
25
+ const djsonK = readFileSync(libPath, "utf8");
26
+
27
+ function compileOp(op) {
28
+ return k.compile(`${djsonK}\n${op}\n`);
29
+ }
30
+
31
+ function makeIndex(n) {
32
+ if (n === 0) {
33
+ return Value.variant(
34
+ "0",
35
+ Value.variant("_", Value.product({}))
36
+ );
37
+ }
38
+ const s = n.toString(2);
39
+ let v = Value.variant("_", Value.product({}));
40
+ for (let i = s.length - 1; i >= 0; i--) {
41
+ v = Value.variant(s[i], v);
42
+ }
43
+ return v;
44
+ }
45
+
46
+ console.log("==> Running Dynamic JSON (json?) Methods Test Suite");
47
+
48
+ // Test 1: Object Accessors (get, has?, set, del)
49
+ console.log("--> Test 1: Object Accessors (get, has?, set, del)");
50
+ {
51
+ const fnGet = compileOp("get");
52
+ const fnHas = compileOp("has?");
53
+ const fnSet = compileOp("set");
54
+ const fnDel = compileOp("del");
55
+
56
+ const doc = {
57
+ name: "Alice",
58
+ age: 30,
59
+ tags: ["developer", "k"]
60
+ };
61
+ const kDoc = fromJsonValue(doc);
62
+
63
+ // get "name"
64
+ const resName = fnGet(Value.product({
65
+ key: textToStringValue("name"),
66
+ json: kDoc
67
+ }));
68
+ assert.equal(toJsonValue(resName), "Alice");
69
+
70
+ // get "age"
71
+ const resAge = fnGet(Value.product({
72
+ key: textToStringValue("age"),
73
+ json: kDoc
74
+ }));
75
+ assert.equal(toJsonValue(resAge), 30);
76
+
77
+ // has? hit
78
+ const hit = fnHas(Value.product({
79
+ key: textToStringValue("name"),
80
+ json: kDoc
81
+ }));
82
+ assert.ok(hit);
83
+
84
+ // has? miss
85
+ const miss = fnHas(Value.product({
86
+ key: textToStringValue("missing"),
87
+ json: kDoc
88
+ }));
89
+ assert.equal(miss, undefined);
90
+
91
+ // set "city" = "Paris"
92
+ const withCity = fnSet(Value.product({
93
+ key: textToStringValue("city"),
94
+ val: fromJsonValue("Paris"),
95
+ json: kDoc
96
+ }));
97
+ assert.equal(toJsonValue(withCity).city, "Paris");
98
+ assert.equal(toJsonValue(withCity).name, "Alice");
99
+
100
+ // del "age"
101
+ const withoutAge = fnDel(Value.product({
102
+ key: textToStringValue("age"),
103
+ json: withCity
104
+ }));
105
+ assert.equal(toJsonValue(withoutAge).age, undefined);
106
+ assert.equal(toJsonValue(withoutAge).city, "Paris");
107
+
108
+ console.log(" Passed: Object get, has?, set, del on json?.");
109
+ }
110
+
111
+ // Test 2: List Accessors (nth, has_index?, set_nth, del_nth, push)
112
+ console.log("--> Test 2: List Accessors (nth, set_nth, del_nth, push)");
113
+ {
114
+ const fnNth = compileOp("nth");
115
+ const fnHasIdx = compileOp("has_index?");
116
+ const fnSetNth = compileOp("set_nth");
117
+ const fnDelNth = compileOp("del_nth");
118
+ const fnPush = compileOp("push");
119
+
120
+ const list = [10, 20, 30];
121
+ const kList = fromJsonValue(list);
122
+
123
+ // nth 0, 1, 2
124
+ assert.equal(
125
+ toJsonValue(fnNth(Value.product({
126
+ index: makeIndex(0),
127
+ json: kList
128
+ }))),
129
+ 10
130
+ );
131
+ assert.equal(
132
+ toJsonValue(fnNth(Value.product({
133
+ index: makeIndex(1),
134
+ json: kList
135
+ }))),
136
+ 20
137
+ );
138
+ assert.equal(
139
+ toJsonValue(fnNth(Value.product({
140
+ index: makeIndex(2),
141
+ json: kList
142
+ }))),
143
+ 30
144
+ );
145
+
146
+ // has_index? hit and miss
147
+ assert.ok(fnHasIdx(Value.product({
148
+ index: makeIndex(1),
149
+ json: kList
150
+ })));
151
+ assert.equal(
152
+ fnHasIdx(Value.product({
153
+ index: makeIndex(99),
154
+ json: kList
155
+ })),
156
+ undefined
157
+ );
158
+
159
+ // set_nth 1 -> 99
160
+ const updatedList = fnSetNth(Value.product({
161
+ index: makeIndex(1),
162
+ val: fromJsonValue(99),
163
+ json: kList
164
+ }));
165
+ assert.deepEqual(toJsonValue(updatedList), [10, 99, 30]);
166
+
167
+ // del_nth 1
168
+ const delList = fnDelNth(Value.product({
169
+ index: makeIndex(1),
170
+ json: updatedList
171
+ }));
172
+ assert.deepEqual(toJsonValue(delList), [10, 30]);
173
+ assert.equal(
174
+ fnHasIdx(Value.product({
175
+ index: makeIndex(1),
176
+ json: delList
177
+ })),
178
+ undefined
179
+ );
180
+
181
+ // push 40
182
+ const pushed = fnPush(Value.product({
183
+ val: fromJsonValue(40),
184
+ json: kList
185
+ }));
186
+ assert.deepEqual(toJsonValue(pushed), [10, 20, 30, 40]);
187
+
188
+ console.log(" Passed: List nth, set_nth, del_nth, push on json?.");
189
+ }
190
+
191
+ // Test 3: Keys and Values (keys, keys_raw, values, values_raw)
192
+ console.log("--> Test 3: Keys and Values (keys, values)");
193
+ {
194
+ const fnKeys = compileOp("keys");
195
+ const fnKeysRaw = compileOp("keys_raw");
196
+ const fnValues = compileOp("values");
197
+ const fnValuesRaw = compileOp("values_raw");
198
+
199
+ const doc = { b: 2, a: 1, c: 3 };
200
+ const kDoc = fromJsonValue(doc);
201
+
202
+ // keys (json? list of string)
203
+ const resKeys = fnKeys(kDoc);
204
+ assert.deepEqual(toJsonValue(resKeys), ["a", "b", "c"]);
205
+
206
+ // keys_raw (raw K cons-list of strings)
207
+ const resKeysRaw = fnKeysRaw(kDoc);
208
+ const rawKeyStrings = [];
209
+ let curr = resKeysRaw;
210
+ while (curr && curr.type === "<>" && curr.tag === "cons") {
211
+ rawKeyStrings.push(stringValueToText(curr.value.product.car));
212
+ curr = curr.value.product.cdr;
213
+ }
214
+ assert.deepEqual(rawKeyStrings, ["a", "b", "c"]);
215
+
216
+ // values on object (json? list of values)
217
+ const resValues = fnValues(kDoc);
218
+ assert.deepEqual(toJsonValue(resValues), [1, 2, 3]);
219
+
220
+ // values on list (json? identity)
221
+ const listJson = fromJsonValue([100, 200]);
222
+ assert.deepEqual(toJsonValue(fnValues(listJson)), [100, 200]);
223
+
224
+ console.log(" Passed: Keys and values extraction on json?.");
225
+ }
226
+
227
+ // Test 4: Cardinality and Length (length, length_nat)
228
+ console.log("--> Test 4: Cardinality and Length (length, length_nat)");
229
+ {
230
+ const fnLength = compileOp("length");
231
+
232
+ const kObj = fromJsonValue({ x: 1, y: 2, z: 3, w: 4 });
233
+ const kArr = fromJsonValue(["a", "b", "c"]);
234
+ const kStr = fromJsonValue("hello");
235
+
236
+ // length returns json? number (float64)
237
+ assert.equal(toJsonValue(fnLength(kObj)), 4);
238
+ assert.equal(toJsonValue(fnLength(kArr)), 3);
239
+ assert.equal(toJsonValue(fnLength(kStr)), 5);
240
+
241
+ console.log(
242
+ " Passed: Length queries on json? object, list, string."
243
+ );
244
+ }
245
+
246
+ // Test 5: Type Predicates and type_of
247
+ console.log("--> Test 5: Type Predicates and type_of");
248
+ {
249
+ const fnTypeOf = compileOp("type_of");
250
+ const fnIsObj = compileOp("is_object?");
251
+ const fnIsList = compileOp("is_list?");
252
+ const fnIsStr = compileOp("is_string?");
253
+ const fnIsNum = compileOp("is_number?");
254
+ const fnIsBool = compileOp("is_bool?");
255
+ const fnIsNull = compileOp("is_null?");
256
+
257
+ const kObj = fromJsonValue({ a: 1 });
258
+ const kArr = fromJsonValue([1, 2]);
259
+ const kStr = fromJsonValue("hi");
260
+ const kNum = fromJsonValue(3.14);
261
+ const kBool = fromJsonValue(true);
262
+ const kNull = fromJsonValue(null);
263
+
264
+ assert.equal(fnTypeOf(kObj).tag, "object");
265
+ assert.equal(fnTypeOf(kArr).tag, "list");
266
+ assert.equal(fnTypeOf(kStr).tag, "string");
267
+ assert.equal(fnTypeOf(kNum).tag, "number");
268
+ assert.equal(fnTypeOf(kBool).tag, "bool");
269
+ assert.equal(fnTypeOf(kNull).tag, "null");
270
+
271
+ assert.ok(fnIsObj(kObj));
272
+ assert.equal(fnIsObj(kArr), undefined);
273
+
274
+ assert.ok(fnIsList(kArr));
275
+ assert.equal(fnIsList(kObj), undefined);
276
+
277
+ assert.ok(fnIsStr(kStr));
278
+ assert.ok(fnIsNum(kNum));
279
+ assert.ok(fnIsBool(kBool));
280
+ assert.ok(fnIsNull(kNull));
281
+
282
+ console.log(" Passed: Type predicates and type_of inspection.");
283
+ }
284
+
285
+ console.log("==> All Dynamic JSON Methods Tests Passed Successfully!");
@@ -0,0 +1,189 @@
1
+ // tests/test-named-patterns.mjs
2
+ // Comprehensive test suite for named patterns and type schemes.
3
+
4
+ import assert from "node:assert/strict";
5
+ import k from "../index.mjs";
6
+ import { Value } from "../Value.mjs";
7
+
8
+ console.log("==> Running Named Patterns Test Suite");
9
+
10
+ // Test 1: 0-arity pattern and composite product
11
+ console.log("--> Test 1: 0-arity pattern (bit[]) and product (byte[])");
12
+ {
13
+ const script = `
14
+ bit[] = < {}0, {}1 >;
15
+ byte[] = {
16
+ bit[] b0, bit[] b1, bit[] b2, bit[] b3,
17
+ bit[] b4, bit[] b5, bit[] b6, bit[] b7
18
+ };
19
+
20
+ is_byte = ? byte[];
21
+
22
+ sample = {
23
+ {}|0 b0, {}|1 b1, {}|0 b2, {}|1 b3,
24
+ {}|0 b4, {}|1 b5, {}|0 b6, {}|1 b7
25
+ };
26
+
27
+ sample is_byte
28
+ `;
29
+ const fn = k.compile(script);
30
+ const out = fn(Value.product({}));
31
+ assert.equal(out.type, "{}");
32
+ assert.equal(out.product.b0.tag, "0");
33
+ assert.equal(out.product.b1.tag, "1");
34
+ console.log(" Passed: 0-arity patterns and composition.");
35
+ }
36
+
37
+ // Test 2: Parameterized list pattern
38
+ console.log("--> Test 2: Parameterized pattern (list[X])");
39
+ {
40
+ const script = `
41
+ bit[] = < {}0, {}1 >;
42
+ list[X] = < {}nil, { X car, Y cdr } cons > = Y;
43
+
44
+ is_bit_list = ? list[bit[]];
45
+
46
+ empty = {}|nil is_bit_list;
47
+ one = { {}|1 car, {}|nil cdr }|cons is_bit_list;
48
+ two = {
49
+ {}|0 car,
50
+ { {}|1 car, {}|nil cdr }|cons cdr
51
+ }|cons is_bit_list;
52
+
53
+ { empty empty, one one, two two }
54
+ `;
55
+ const fn = k.compile(script);
56
+ const out = fn(Value.product({}));
57
+ assert.equal(out.product.empty.tag, "nil");
58
+ assert.equal(out.product.one.tag, "cons");
59
+ assert.equal(out.product.one.value.product.car.tag, "1");
60
+ assert.equal(out.product.two.tag, "cons");
61
+ assert.equal(out.product.two.value.product.car.tag, "0");
62
+ console.log(" Passed: Parameterized list pattern.");
63
+ }
64
+
65
+ // Test 3: Multi-parameter templates and composition
66
+ console.log("--> Test 3: Multi-parameter templates (pair, dict)");
67
+ {
68
+ const script = `
69
+ bit[] = < {}0, {}1 >;
70
+ pair[A, B] = { A key, B val };
71
+ list[X] = < {}nil, { X car, Y cdr } cons > = Y;
72
+ dict[K, V] = list[pair[K, V]];
73
+
74
+ is_dict = ? dict[bit[], bit[]];
75
+
76
+ entry = { {}|0 key, {}|1 val };
77
+ d = { entry car, {}|nil cdr }|cons is_dict;
78
+
79
+ d
80
+ `;
81
+ const fn = k.compile(script);
82
+ const out = fn(Value.product({}));
83
+ assert.equal(out.tag, "cons");
84
+ assert.equal(out.value.product.car.product.key.tag, "0");
85
+ assert.equal(out.value.product.car.product.val.tag, "1");
86
+ console.log(" Passed: Multi-parameter template composition.");
87
+ }
88
+
89
+ // Test 4: Existential internal variables (type unification across fields)
90
+ console.log("--> Test 4: Existential variables (same_pair[])");
91
+ {
92
+ const scriptOk = `
93
+ same_pair[] = { T first, T second };
94
+ check = ? same_pair[];
95
+ { {}|0 first, {}|1 second } check
96
+ `;
97
+ const fnOk = k.compile(scriptOk);
98
+ const outOk = fnOk(Value.product({}));
99
+ assert.equal(outOk.product.first.tag, "0");
100
+ assert.equal(outOk.product.second.tag, "1");
101
+
102
+ const scriptBad = `
103
+ same_pair[] = { T first, T second };
104
+ check = ? same_pair[];
105
+ { {} first, {}|0 second } check
106
+ `;
107
+ assert.throws(
108
+ () => k.compile(scriptBad),
109
+ /Cannot unify/
110
+ );
111
+ console.log(" Passed: Existential variables unified across fields.");
112
+ }
113
+
114
+ // Test 5: Recursive tree with internal knot and list
115
+ console.log("--> Test 5: Recursive tree with knot T (tree[X])");
116
+ {
117
+ const script = `
118
+ list[X] = < {}nil, { X car, Y cdr } cons > = Y;
119
+ tree[X] = < {}nil, { X val, list[T] children } node > = T;
120
+
121
+ bit[] = < {}0, {}1 >;
122
+ is_tree = ? tree[bit[]];
123
+
124
+ leaf = { {}|1 val, {}|nil children }|node is_tree;
125
+ root = {
126
+ {}|0 val,
127
+ { leaf car, {}|nil cdr }|cons children
128
+ }|node is_tree;
129
+
130
+ { leaf leaf, root root }
131
+ `;
132
+ const fn = k.compile(script);
133
+ const out = fn(Value.product({}));
134
+ assert.equal(out.product.leaf.tag, "node");
135
+ assert.equal(out.product.leaf.value.product.val.tag, "1");
136
+ assert.equal(out.product.root.tag, "node");
137
+ assert.equal(out.product.root.value.product.val.tag, "0");
138
+ console.log(" Passed: Recursive tree template with list port.");
139
+ }
140
+
141
+ // Test 6: Polymorphic relation with shared type variable
142
+ console.log("--> Test 6: Polymorphic relation ({ X car, list[X] cdr })");
143
+ {
144
+ const script = `
145
+ list[X] = < {}nil, { X car, Y cdr } cons > = Y;
146
+ head = ? { X car, list[X] cdr } .car;
147
+
148
+ bit[] = < {}0, {}1 >;
149
+ h1 = { {}|1 car, {}|nil cdr } head;
150
+
151
+ h1
152
+ `;
153
+ const fn = k.compile(script);
154
+ const out = fn(Value.product({}));
155
+ assert.equal(out.tag, "1");
156
+ console.log(" Passed: Polymorphic relation head with list[X].");
157
+ }
158
+
159
+ // Test 7: Error handling for templates
160
+ console.log("--> Test 7: Error handling (unknown, arity, cycles, dupes)");
161
+ {
162
+ assert.throws(
163
+ () => k.compile("check = ? missing[]; ()"),
164
+ /Unknown pattern template 'missing\[\]'/
165
+ );
166
+
167
+ assert.throws(
168
+ () => k.compile("list[X] = {}; check = ? list[]; ()"),
169
+ /Pattern 'list' expects 1 argument, got 0/
170
+ );
171
+
172
+ assert.throws(
173
+ () => k.compile("pair[A, B] = {}; check = ? pair[{}]; ()"),
174
+ /Pattern 'pair' expects 2 arguments, got 1/
175
+ );
176
+
177
+ assert.throws(
178
+ () => k.compile("a[] = b[]; b[] = a[]; check = ? a[]; ()"),
179
+ /Cyclic pattern template dependency detected/
180
+ );
181
+
182
+ assert.throws(
183
+ () => k.compile("dupe[X, X] = {}; ()"),
184
+ /Duplicate parameter 'X' in pattern 'dupe'/
185
+ );
186
+ console.log(" Passed: Error conditions correctly handled.");
187
+ }
188
+
189
+ console.log("==> All Named Patterns Tests Passed Successfully!");
@@ -140,6 +140,7 @@ if (chromiumBin) {
140
140
  const expectedFiles = [
141
141
  "arithmetics.k",
142
142
  "core.k",
143
+ "djson.k",
143
144
  "djson.mjs",
144
145
  "ieee.k",
145
146
  "ieee.mjs",
@@ -522,9 +523,9 @@ if (chromiumBin) {
522
523
  const v4Collapsed = v4 ? !v4.open : false;
523
524
 
524
525
  // Click Expand All
525
- const actionBtns = lastEntry.querySelectorAll(".k-btn-action");
526
- const expandBtn = actionBtns[0];
527
- const collapseBtn = actionBtns[1];
526
+ const toolbarActionBtns = lastEntry.querySelectorAll(".k-output-toolbar .k-btn-action");
527
+ const expandBtn = toolbarActionBtns[0];
528
+ const collapseBtn = toolbarActionBtns[1];
528
529
 
529
530
  expandBtn.click();
530
531
  const allOpenAfterExpand = v4.open;
@@ -536,13 +537,48 @@ if (chromiumBin) {
536
537
  );
537
538
  const v4CollapsedAgain = newV4 ? !newV4.open : false;
538
539
 
540
+ // Verify Copy buttons on Value and Pattern section headers
541
+ const patSection = lastEntry.querySelector(".k-tree-pat-section");
542
+ const copyValBtn = valSection?.querySelector("summary.k-tree-section-summary > .k-btn-copy-value");
543
+ const copyPatBtn = patSection?.querySelector("summary.k-tree-section-summary > .k-btn-copy-pattern");
544
+
545
+ const copiedTexts = [];
546
+ const origWriteText = navigator.clipboard?.writeText;
547
+ if (navigator.clipboard) {
548
+ navigator.clipboard.writeText = async (txt) => { copiedTexts.push(txt); };
549
+ } else {
550
+ Object.defineProperty(navigator, "clipboard", {
551
+ value: { writeText: async (txt) => { copiedTexts.push(txt); } },
552
+ configurable: true
553
+ });
554
+ }
555
+
556
+ const valOpenBefore = valSection?.open;
557
+ copyValBtn?.click();
558
+ const valOpenAfter = valSection?.open;
559
+
560
+ const patOpenBefore = patSection?.open;
561
+ copyPatBtn?.click();
562
+ const patOpenAfter = patSection?.open;
563
+
564
+ if (origWriteText && navigator.clipboard) {
565
+ navigator.clipboard.writeText = origWriteText;
566
+ }
567
+
539
568
  return {
540
569
  v1Open,
541
570
  v2Open,
542
571
  v3Open,
543
572
  v4Collapsed,
544
573
  allOpenAfterExpand,
545
- v4CollapsedAgain
574
+ v4CollapsedAgain,
575
+ toolbarBtnCount: toolbarActionBtns.length,
576
+ hasCopyValBtn: !!copyValBtn,
577
+ hasCopyPatBtn: !!copyPatBtn,
578
+ valDidNotToggle: valOpenBefore === valOpenAfter,
579
+ patDidNotToggle: patOpenBefore === patOpenAfter,
580
+ copiedValText: copiedTexts[0] || "",
581
+ copiedPatText: copiedTexts[1] || ""
546
582
  };
547
583
  })()`);
548
584
 
@@ -552,6 +588,13 @@ if (chromiumBin) {
552
588
  assert.strictEqual(variantDepthAndActionsCheck.v4Collapsed, true, "Variant level 4 must be collapsed by default");
553
589
  assert.strictEqual(variantDepthAndActionsCheck.allOpenAfterExpand, true, "Expand button should expand collapsed nodes");
554
590
  assert.strictEqual(variantDepthAndActionsCheck.v4CollapsedAgain, true, "Collapse button should reset deeper nodes to collapsed");
591
+ assert.strictEqual(variantDepthAndActionsCheck.toolbarBtnCount, 2, "Toolbar action group should only have Expand and Collapse buttons");
592
+ assert.strictEqual(variantDepthAndActionsCheck.hasCopyValBtn, true, "Value section summary should have Copy button");
593
+ assert.strictEqual(variantDepthAndActionsCheck.hasCopyPatBtn, true, "Pattern section summary should have Copy button");
594
+ assert.strictEqual(variantDepthAndActionsCheck.valDidNotToggle, true, "Clicking Value Copy button should not toggle Value details");
595
+ assert.strictEqual(variantDepthAndActionsCheck.patDidNotToggle, true, "Clicking Pattern Copy button should not toggle Pattern details");
596
+ assert.strictEqual(variantDepthAndActionsCheck.copiedValText, "{}|_|0|1|0|1", "Value Copy button should copy K value definition");
597
+ assert(variantDepthAndActionsCheck.copiedPatText.startsWith("?<"), `Pattern Copy button should copy K pattern filter definition, got: ${variantDepthAndActionsCheck.copiedPatText}`);
555
598
 
556
599
  // 4. Verify shortened welcome banner
557
600
  const bannerCheck = await evaluateAsync(`(() => {