@rogatio/cli 6.8.0 → 6.9.1

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.
@@ -729,6 +729,10 @@
729
729
  .rogatio-editor :focus-visible {
730
730
  outline-color: Highlight;
731
731
  }
732
+ .rogatio-editor [data-test-rule-link] {
733
+ color: LinkText;
734
+ background: transparent;
735
+ }
732
736
  .rogatio-editor [data-desktop-route-rail] {
733
737
  border-bottom-color: ButtonText;
734
738
  }
@@ -741,10 +745,40 @@
741
745
  transition: none;
742
746
  }
743
747
  }
744
- .rogatio-editor [data-test-description] {
748
+ .rogatio-editor [data-test-description],
749
+ .rogatio-editor [data-test-checking] {
745
750
  color: var(--rogatio-muted);
746
751
  margin-block: 0 1rem;
747
752
  }
753
+ .rogatio-editor [data-test-samples] {
754
+ display: flex;
755
+ flex-wrap: wrap;
756
+ gap: 0.5rem;
757
+ margin-top: 0.75rem;
758
+ }
759
+ .rogatio-editor [data-test-outcome] {
760
+ margin: 0;
761
+ }
762
+ .rogatio-editor [data-test-rule-link] {
763
+ display: inline;
764
+ margin: 0;
765
+ padding: 0;
766
+ border: 0;
767
+ background: none;
768
+ color: var(--rogatio-primary);
769
+ font: inherit;
770
+ text-decoration: underline;
771
+ cursor: pointer;
772
+ }
773
+ .rogatio-editor [data-test-misses] {
774
+ margin-top: 0.5rem;
775
+ }
776
+ .rogatio-editor [data-test-misses] summary {
777
+ cursor: pointer;
778
+ }
779
+ .rogatio-editor [data-test-miss-reason] {
780
+ color: var(--rogatio-muted);
781
+ }
748
782
  .rogatio-editor [data-test-panel] {
749
783
  display: grid;
750
784
  gap: 1rem;
@@ -1742,6 +1742,31 @@ var HTTP_METHODS2 = [
1742
1742
  "CONNECT",
1743
1743
  "TRACE"
1744
1744
  ];
1745
+ var RESOURCE_TYPE_LABELS = {
1746
+ main_frame: "Page",
1747
+ sub_frame: "Frame",
1748
+ stylesheet: "Stylesheet",
1749
+ script: "Script",
1750
+ image: "Image",
1751
+ font: "Font",
1752
+ object: "Plugin",
1753
+ media: "Media",
1754
+ xmlhttprequest: "Fetch",
1755
+ ping: "Ping",
1756
+ csp_report: "CSP report",
1757
+ websocket: "WebSocket",
1758
+ webtransport: "WebTransport",
1759
+ webbundle: "Web Bundle",
1760
+ other: "Other"
1761
+ };
1762
+ var TEST_CASE_LIMIT = 256;
1763
+ var RULE_TYPE_LABELS = {
1764
+ redirect: "Redirect",
1765
+ query: "Query parameters",
1766
+ header: "Header",
1767
+ "response-body": "Response body",
1768
+ "request-body": "Request body"
1769
+ };
1745
1770
  var COMMON_RULE_FIELDS = /* @__PURE__ */ new Set([
1746
1771
  "id",
1747
1772
  "name",
@@ -2033,6 +2058,80 @@ function displayName(value, fallback) {
2033
2058
  const text = safeText(value, fallback);
2034
2059
  return text.length > 0 ? text : fallback;
2035
2060
  }
2061
+ function checkingCopy(method, resourceType) {
2062
+ if (method === "GET" && resourceType === "main_frame") {
2063
+ return "Checking these as page loads (GET).";
2064
+ }
2065
+ if (method === "" && resourceType === "") {
2066
+ return "The method and the resource type are not tested.";
2067
+ }
2068
+ const resourceLabel = resourceType === "" ? "" : RESOURCE_TYPE_LABELS[resourceType];
2069
+ if (method === "") {
2070
+ const subject = resourceType === "main_frame" ? "page loads" : `${resourceLabel} requests`;
2071
+ return `Checking these as ${subject}. The method is not tested.`;
2072
+ }
2073
+ if (resourceType === "") {
2074
+ return `Checking these as ${method} requests. The resource type is not tested.`;
2075
+ }
2076
+ if (resourceType === "main_frame") {
2077
+ return `Checking these as page loads (${method}).`;
2078
+ }
2079
+ return `Checking these as ${resourceLabel} requests (${method}).`;
2080
+ }
2081
+ function exactUrlFromSource(source) {
2082
+ if (source?.key !== "url" || source.operator !== "regex") {
2083
+ return null;
2084
+ }
2085
+ if (typeof source.value !== "string") return null;
2086
+ const value = source.value;
2087
+ if (!value.startsWith("^") || !value.endsWith("$") || value.length < 2) {
2088
+ return null;
2089
+ }
2090
+ let decoded = "";
2091
+ for (let index = 1; index < value.length - 1; index += 1) {
2092
+ const char = value[index];
2093
+ if (char === "\\") {
2094
+ index += 1;
2095
+ const escaped = value[index];
2096
+ if (escaped === void 0 || index >= value.length - 1) return null;
2097
+ decoded += escaped;
2098
+ continue;
2099
+ }
2100
+ if ("^$\\.*+?()[]{}|".includes(char)) return null;
2101
+ decoded += char;
2102
+ }
2103
+ const converted = urlToExactRegex(decoded);
2104
+ if (!converted.ok || converted.source !== value) return null;
2105
+ return decoded;
2106
+ }
2107
+ function isDryRunResult(value) {
2108
+ if (!isRecord4(value)) return false;
2109
+ const summary = value.summary;
2110
+ return Array.isArray(value.results) && Array.isArray(value.errors) && isRecord4(summary) && typeof summary.matchedUrlCount === "number" && typeof summary.urlCount === "number";
2111
+ }
2112
+ function previewSentence(preview) {
2113
+ if (!preview || preview.summary.length === 0) return null;
2114
+ switch (preview.kind) {
2115
+ case "redirect":
2116
+ return `The browser would go to ${preview.summary}.`;
2117
+ case "query":
2118
+ return `The query would be ${preview.summary}.`;
2119
+ case "header":
2120
+ return `The header would be ${preview.summary}.`;
2121
+ case "request-body":
2122
+ return `The request body would be ${preview.summary}.`;
2123
+ case "response-body":
2124
+ return `The response body would be ${preview.summary}.`;
2125
+ default:
2126
+ return null;
2127
+ }
2128
+ }
2129
+ function missReason(rule) {
2130
+ if (rule.source.state === "unmatched") return "URL pattern";
2131
+ if (rule.method.state === "unmatched") return "method";
2132
+ if (rule.resourceType.state === "unmatched") return "resource type";
2133
+ return "URL pattern";
2134
+ }
2036
2135
  function isHTMLElement(value) {
2037
2136
  return value !== null && typeof value === "object" && value.nodeType === 1 && typeof value.appendChild === "function";
2038
2137
  }
@@ -2171,9 +2270,8 @@ var EditorControllerImpl = class {
2171
2270
  controlNumber = 0;
2172
2271
  previousFocus;
2173
2272
  testUrls = "";
2174
- testMethod = "";
2175
- testResourceType = "";
2176
- testMaxCases = "256";
2273
+ testMethod = "GET";
2274
+ testResourceType = "main_frame";
2177
2275
  testResult = void 0;
2178
2276
  testRunning = false;
2179
2277
  testRequestId = 0;
@@ -2349,10 +2447,6 @@ var EditorControllerImpl = class {
2349
2447
  this.testUrls = target.value;
2350
2448
  return;
2351
2449
  }
2352
- if (target.dataset.testMaxCases !== void 0) {
2353
- this.testMaxCases = target.value;
2354
- return;
2355
- }
2356
2450
  const path = target.dataset.path;
2357
2451
  if (!path || target.type === "checkbox" || this.extensionControls.has(path))
2358
2452
  return;
@@ -2387,10 +2481,12 @@ var EditorControllerImpl = class {
2387
2481
  }
2388
2482
  if (target instanceof HTMLSelectElement && target.dataset.testMethod !== void 0) {
2389
2483
  this.testMethod = target.value || "";
2484
+ this.render();
2390
2485
  return;
2391
2486
  }
2392
2487
  if (target instanceof HTMLSelectElement && target.dataset.testResourceType !== void 0) {
2393
2488
  this.testResourceType = target.value || "";
2489
+ this.render();
2394
2490
  return;
2395
2491
  }
2396
2492
  const path = target.dataset.path;
@@ -2571,6 +2667,17 @@ var EditorControllerImpl = class {
2571
2667
  void this.runTest();
2572
2668
  return;
2573
2669
  }
2670
+ if (command === "test:open-rule") {
2671
+ this.navigateToRule(element.dataset.groupId, element.dataset.ruleId);
2672
+ return;
2673
+ }
2674
+ if (command === "test:try-url") {
2675
+ const url = element.dataset.url;
2676
+ if (!url) return;
2677
+ this.testUrls = url;
2678
+ this.statusMessage = "";
2679
+ this.render();
2680
+ }
2574
2681
  }
2575
2682
  collectDiagnostics(value) {
2576
2683
  let hostDiagnostics;
@@ -3282,65 +3389,111 @@ var EditorControllerImpl = class {
3282
3389
  this.focusRequest = path;
3283
3390
  this.render();
3284
3391
  }
3392
+ ruleCount() {
3393
+ let count = 0;
3394
+ for (const group of this.draft.groups) count += group.rules.length;
3395
+ return count;
3396
+ }
3397
+ testLines() {
3398
+ const entries = [];
3399
+ const rawLines = this.testUrls.split(/\r?\n/);
3400
+ for (let index = 0; index < rawLines.length; index += 1) {
3401
+ const url = rawLines[index]?.trim() ?? "";
3402
+ if (url.length > 0) entries.push({ url, line: index + 1 });
3403
+ }
3404
+ return entries;
3405
+ }
3406
+ showDryRunDiagnostics(diagnostics) {
3407
+ this.testResult = void 0;
3408
+ this.testRunning = false;
3409
+ this.errors = [...diagnostics];
3410
+ const count = diagnostics.length;
3411
+ this.statusMessage = count === 0 ? "The project could not be tested." : `${count} validation error${count === 1 ? "" : "s"} found.`;
3412
+ const first = diagnostics[0];
3413
+ if (first) {
3414
+ this.navigateToPath(first.path);
3415
+ return;
3416
+ }
3417
+ this.render();
3418
+ }
3285
3419
  async runTest() {
3286
3420
  if (!this.options.dryRun) {
3287
3421
  this.statusMessage = "Dry-run is not configured for this editor instance.";
3288
3422
  this.render();
3289
3423
  return;
3290
3424
  }
3291
- const urlsText = this.testUrls.trim();
3292
- if (!urlsText) {
3425
+ if (this.ruleCount() === 0) {
3426
+ this.testResult = void 0;
3427
+ this.testRunning = false;
3428
+ this.statusMessage = "This project has no rules to test.";
3429
+ this.render();
3430
+ return;
3431
+ }
3432
+ const lines = this.testLines();
3433
+ if (lines.length === 0) {
3293
3434
  this.statusMessage = "Please enter at least one URL to test.";
3294
3435
  this.render();
3295
3436
  return;
3296
3437
  }
3297
- const lines = urlsText.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0);
3298
- const cases = lines.map((url) => ({
3299
- url,
3300
- method: this.testMethod === "" ? void 0 : this.testMethod,
3301
- resourceType: this.testResourceType === "" ? void 0 : this.testResourceType
3302
- }));
3303
- const maxCasesRaw = Number.parseInt(this.testMaxCases, 10);
3304
- const maxCases = Number.isSafeInteger(maxCasesRaw) && maxCasesRaw > 0 ? maxCasesRaw : void 0;
3438
+ if (lines.length > TEST_CASE_LIMIT) {
3439
+ this.testResult = void 0;
3440
+ this.testRunning = false;
3441
+ this.statusMessage = "Only 256 URLs can be checked at once.";
3442
+ this.render();
3443
+ return;
3444
+ }
3445
+ const cases = lines.map((entry) => {
3446
+ const testCase = { url: entry.url };
3447
+ if (this.testMethod !== "") testCase.method = this.testMethod;
3448
+ if (this.testResourceType !== "") {
3449
+ testCase.resourceType = this.testResourceType;
3450
+ }
3451
+ return testCase;
3452
+ });
3305
3453
  const requestId = ++this.testRequestId;
3306
3454
  this.testRunning = true;
3307
- this.statusMessage = "Running dry-run...";
3455
+ this.statusMessage = "Checking URLs...";
3308
3456
  this.render();
3309
3457
  try {
3310
- const result = await this.options.dryRun(
3311
- this.getDraft(),
3312
- cases,
3313
- maxCases === void 0 ? void 0 : { maxCases }
3314
- );
3458
+ const outcome = await this.options.dryRun(this.getDraft(), cases);
3315
3459
  if (requestId !== this.testRequestId) return;
3316
- this.testResult = result;
3317
3460
  this.testRunning = false;
3318
- const matched = result.summary.matchedUrlCount;
3319
- const total = result.summary.urlCount;
3320
- this.statusMessage = `Test complete: ${matched}/${total} URLs matched at least one rule.`;
3461
+ if (isDryRunResult(outcome)) {
3462
+ this.testResult = outcome;
3463
+ const checked = outcome.summary.urlCount;
3464
+ this.statusMessage = outcome.errors.length > 0 ? `Checked ${checked} URL${checked === 1 ? "" : "s"}. Some lines are not http(s) URLs.` : `Checked ${checked} URL${checked === 1 ? "" : "s"}.`;
3465
+ this.render();
3466
+ return;
3467
+ }
3468
+ const diagnostics = normalizeDiagnostics(
3469
+ isRecord4(outcome) ? outcome.diagnostics : void 0
3470
+ );
3471
+ this.showDryRunDiagnostics(diagnostics);
3321
3472
  } catch (e) {
3322
3473
  if (requestId !== this.testRequestId) return;
3474
+ this.testResult = void 0;
3323
3475
  this.testRunning = false;
3324
3476
  const message = e instanceof Error ? e.message : "Test failed";
3325
3477
  this.statusMessage = `Test error: ${message}`;
3478
+ this.render();
3326
3479
  }
3327
- this.render();
3328
3480
  }
3329
3481
  renderTestResults(result) {
3330
3482
  const resultsSection = this.host.querySelector(
3331
3483
  "[data-test-results]"
3332
3484
  );
3333
- if (!resultsSection) return;
3485
+ if (!resultsSection || !isDryRunResult(result)) return;
3334
3486
  resultsSection.replaceChildren();
3335
3487
  const heading = this.document.createElement("h3");
3336
3488
  heading.textContent = "Results";
3337
3489
  resultsSection.append(heading);
3490
+ const lines = this.testLines();
3338
3491
  if (result.errors.length > 0) {
3339
3492
  const errorsList = this.document.createElement("ul");
3340
3493
  errorsList.dataset.testErrors = "true";
3341
3494
  for (const err of result.errors) {
3342
3495
  const item = this.document.createElement("li");
3343
- item.textContent = `${err.code}: ${err.message}${err.index !== void 0 ? ` (case ${err.index})` : ""}`;
3496
+ item.textContent = this.testErrorText(err, lines);
3344
3497
  errorsList.append(item);
3345
3498
  }
3346
3499
  resultsSection.append(errorsList);
@@ -3355,73 +3508,135 @@ var EditorControllerImpl = class {
3355
3508
  for (const urlResult of result.results) {
3356
3509
  const card = this.document.createElement("article");
3357
3510
  card.dataset.testResultCard = "true";
3358
- const matched = urlResult.matchedRuleCount > 0;
3359
- card.dataset.matched = matched ? "true" : "false";
3360
- const urlHeader = this.document.createElement("div");
3361
- urlHeader.dataset.testResultHeader = "true";
3362
- const urlText = this.document.createElement("strong");
3363
- urlText.textContent = urlResult.url;
3364
- const urlBadge = this.document.createElement("span");
3365
- urlBadge.dataset.testBadge = "true";
3366
- urlBadge.dataset.variant = matched ? "matched" : "unmatched";
3367
- urlBadge.textContent = matched ? "MATCHED" : "NO MATCH";
3368
- urlHeader.append(urlText, urlBadge);
3369
- card.append(urlHeader);
3370
- for (const rule of urlResult.rules) {
3371
- const ruleDiv = this.document.createElement("div");
3372
- ruleDiv.dataset.testRule = "true";
3373
- const ruleHeader = this.document.createElement("div");
3374
- ruleHeader.dataset.testRuleHeader = "true";
3375
- const ruleName = this.document.createElement("strong");
3376
- ruleName.textContent = this.testRuleLabel(rule.groupId, rule.ruleId);
3377
- const ruleBadge = this.document.createElement("span");
3378
- ruleBadge.dataset.testBadge = "true";
3379
- ruleBadge.dataset.variant = rule.matched ? "matched" : "unmatched";
3380
- ruleBadge.textContent = rule.matched ? "\u2713 MATCHED" : "\u2717 NOT MATCHED";
3381
- ruleHeader.append(ruleName, ruleBadge);
3382
- ruleDiv.append(ruleHeader);
3383
- const dims = [
3384
- { label: "source", dim: rule.source },
3385
- { label: "method", dim: rule.method },
3386
- { label: "resourceType", dim: rule.resourceType }
3387
- ];
3388
- for (const { label, dim } of dims) {
3389
- const dimDiv = this.document.createElement("div");
3390
- dimDiv.dataset.testDimension = "true";
3391
- const badge = this.document.createElement("span");
3392
- badge.dataset.testBadge = "true";
3393
- badge.dataset.variant = dim.state === "matched" ? "matched" : dim.state === "unmatched" ? "unmatched" : "na";
3394
- badge.textContent = dim.state.toUpperCase();
3395
- dimDiv.append(
3396
- badge,
3397
- this.document.createTextNode(`${label}: ${dim.detail}`)
3398
- );
3399
- ruleDiv.append(dimDiv);
3400
- }
3401
- if (rule.actionPreview) {
3402
- const apDiv = this.document.createElement("div");
3403
- apDiv.dataset.testActionPreview = "true";
3404
- apDiv.textContent = `Action preview: ${rule.actionPreview.kind} - ${rule.actionPreview.summary}`;
3405
- ruleDiv.append(apDiv);
3406
- }
3407
- card.append(ruleDiv);
3511
+ const matches = urlResult.rules.filter((rule) => rule.matched);
3512
+ const misses = urlResult.rules.filter((rule) => !rule.matched);
3513
+ card.dataset.matched = matches.length > 0 ? "true" : "false";
3514
+ for (const rule of matches) {
3515
+ card.append(this.renderTestOutcome(urlResult.url, rule));
3516
+ }
3517
+ if (misses.length > 0) {
3518
+ card.append(this.renderTestMisses(misses));
3408
3519
  }
3409
3520
  resultsSection.append(card);
3410
3521
  }
3411
3522
  }
3523
+ testErrorText(err, lines) {
3524
+ if (err.code === "dryrun.batch-limit") {
3525
+ return "Only 256 URLs can be checked at once.";
3526
+ }
3527
+ const entry = err.index === void 0 ? void 0 : lines[err.index];
3528
+ if (err.code === "dryrun.invalid-url" && entry) {
3529
+ return `Line ${entry.line} is not an http(s) URL: ${entry.url}`;
3530
+ }
3531
+ if (entry) {
3532
+ return `Line ${entry.line} could not be checked: ${entry.url}`;
3533
+ }
3534
+ return err.message;
3535
+ }
3536
+ renderTestOutcome(url, rule) {
3537
+ const names = this.testRuleNames(rule.groupId, rule.ruleId);
3538
+ const typeLabel = this.testRuleTypeLabel(rule.groupId, rule.ruleId);
3539
+ const outcome = this.document.createElement("p");
3540
+ outcome.dataset.testOutcome = "true";
3541
+ const lead = this.document.createElement("span");
3542
+ lead.textContent = names.groupKnown ? `${url} matches ${typeLabel} in ${names.groupName} / ` : `${url} matches ${typeLabel} in `;
3543
+ const ruleButton = this.createCommandButton(
3544
+ names.ruleLabel,
3545
+ "test:open-rule",
3546
+ false,
3547
+ { groupId: rule.groupId, ruleId: rule.ruleId }
3548
+ );
3549
+ ruleButton.dataset.testRuleLink = "true";
3550
+ ruleButton.removeAttribute("data-btn");
3551
+ const tailParts = ["."];
3552
+ const preview = previewSentence(rule.actionPreview);
3553
+ if (preview) tailParts.push(` ${preview}`);
3554
+ if (rule.method.state === "not-applicable") {
3555
+ tailParts.push(" The method was not tested.");
3556
+ }
3557
+ if (rule.resourceType.state === "not-applicable") {
3558
+ tailParts.push(" The resource type was not tested.");
3559
+ }
3560
+ if (this.groupIsOffInBrowser(rule.groupId)) {
3561
+ tailParts.push(
3562
+ " This group is off in Chrome, so the browser will not apply this rule."
3563
+ );
3564
+ }
3565
+ const tail = this.document.createElement("span");
3566
+ tail.textContent = tailParts.join("");
3567
+ outcome.append(lead, ruleButton, tail);
3568
+ return outcome;
3569
+ }
3570
+ renderTestMisses(misses) {
3571
+ const details = this.document.createElement("details");
3572
+ details.dataset.testMisses = "true";
3573
+ const summary = this.document.createElement("summary");
3574
+ summary.textContent = misses.length === 1 ? "1 rule did not match" : `${misses.length} rules did not match`;
3575
+ const list = this.document.createElement("ul");
3576
+ for (const rule of misses) {
3577
+ const names = this.testRuleNames(rule.groupId, rule.ruleId);
3578
+ const item = this.document.createElement("li");
3579
+ const ruleButton = this.createCommandButton(
3580
+ names.ruleLabel,
3581
+ "test:open-rule",
3582
+ false,
3583
+ { groupId: rule.groupId, ruleId: rule.ruleId }
3584
+ );
3585
+ ruleButton.dataset.testRuleLink = "true";
3586
+ ruleButton.removeAttribute("data-btn");
3587
+ const reason = this.document.createElement("span");
3588
+ reason.dataset.testMissReason = "true";
3589
+ reason.textContent = missReason(rule);
3590
+ item.append(ruleButton, this.document.createTextNode(" \u2014 "), reason);
3591
+ list.append(item);
3592
+ }
3593
+ details.append(summary, list);
3594
+ return details;
3595
+ }
3412
3596
  /**
3413
3597
  * A test-result row is identified by the names the user actually sees. The dry
3414
3598
  * result carries ids only, and the draft the editor handed over is the source
3415
- * of truth for both, so no contract changes. A rule that is no longer in the
3416
- * draft falls back to its id rather than rendering a blank row.
3599
+ * of truth for both. A rule that is no longer in the draft falls back to its id.
3417
3600
  */
3418
- testRuleLabel(groupId, ruleId) {
3601
+ testRuleNames(groupId, ruleId) {
3419
3602
  const group = this.groupById(groupId);
3420
- if (!group) return `${groupId}/${ruleId}`;
3603
+ if (!group) {
3604
+ return {
3605
+ groupKnown: false,
3606
+ ruleKnown: false,
3607
+ groupName: groupId,
3608
+ ruleLabel: `${groupId}/${ruleId}`
3609
+ };
3610
+ }
3421
3611
  const groupName = displayName(group.name, "Unnamed group");
3422
3612
  const rule = this.ruleById(groupId, ruleId);
3423
- if (!rule) return `${groupName} / ${ruleId}`;
3424
- return `${groupName} / ${displayName(rule.name, "Unnamed rule")}`;
3613
+ if (!rule) {
3614
+ return {
3615
+ groupKnown: true,
3616
+ ruleKnown: false,
3617
+ groupName,
3618
+ ruleLabel: ruleId
3619
+ };
3620
+ }
3621
+ return {
3622
+ groupKnown: true,
3623
+ ruleKnown: true,
3624
+ groupName,
3625
+ ruleLabel: displayName(rule.name, "Unnamed rule")
3626
+ };
3627
+ }
3628
+ testRuleTypeLabel(groupId, ruleId) {
3629
+ const rule = this.ruleById(groupId, ruleId);
3630
+ const type = rule && typeof rule.type === "string" ? rule.type : "";
3631
+ return RULE_TYPE_LABELS[type] ?? "the rule";
3632
+ }
3633
+ groupIsOffInBrowser(groupId) {
3634
+ const enablement = this.options.groupEnablement;
3635
+ if (!enablement) return false;
3636
+ const group = this.groupById(groupId);
3637
+ const saved = group ? this.savedGroupId(group) : void 0;
3638
+ const id = saved && saved.length > 0 ? saved : groupId;
3639
+ return enablement.isEnabled(id) !== true;
3425
3640
  }
3426
3641
  navigate(route, groupId) {
3427
3642
  if (route === "project") {
@@ -3732,17 +3947,6 @@ var EditorControllerImpl = class {
3732
3947
  );
3733
3948
  }
3734
3949
  tools.append(this.createCommandButton("Validate", "validate", this.saving));
3735
- if (this.route.kind === "test") {
3736
- tools.append(
3737
- this.createCommandButton(
3738
- "Run test",
3739
- "test:run",
3740
- this.saving || this.testRunning,
3741
- {},
3742
- "primary"
3743
- )
3744
- );
3745
- }
3746
3950
  const commit = this.document.createElement("div");
3747
3951
  commit.dataset.actionCluster = "commit";
3748
3952
  commit.append(
@@ -3890,19 +4094,6 @@ var EditorControllerImpl = class {
3890
4094
  this.createGroupActionClusters(groupId, groupName)
3891
4095
  );
3892
4096
  }
3893
- } else {
3894
- const run = this.document.createElement("div");
3895
- run.dataset.actionCluster = "safe";
3896
- run.append(
3897
- this.createCommandButton(
3898
- "Run test",
3899
- "test:run",
3900
- this.saving || this.testRunning,
3901
- {},
3902
- "primary"
3903
- )
3904
- );
3905
- this.appendDockRow(dock, "Test", run);
3906
4097
  }
3907
4098
  this.appendDockRow(dock, "Project", this.createProjectActionRow());
3908
4099
  this.form.append(dock);
@@ -4055,7 +4246,7 @@ var EditorControllerImpl = class {
4055
4246
  this.form.append(heading);
4056
4247
  const description = this.document.createElement("p");
4057
4248
  description.dataset.testDescription = "true";
4058
- description.textContent = "Run offline dry-run tests against the current project. Enter test cases (one URL per line) and optional method/resource type defaults. No network requests are made.";
4249
+ description.textContent = "Check whether these URLs match your rules. Nothing is contacted, and nothing is saved.";
4059
4250
  this.form.append(description);
4060
4251
  const panel = this.document.createElement("div");
4061
4252
  panel.dataset.testPanel = "true";
@@ -4072,90 +4263,127 @@ var EditorControllerImpl = class {
4072
4263
  urlsTextarea.placeholder = "https://example.com/page\nhttps://example.com/script.js\nhttps://other.com/";
4073
4264
  urlsTextarea.value = this.testUrls;
4074
4265
  urlsTextarea.dataset.testUrls = "true";
4266
+ urlsTextarea.dataset.editorKey = "test-urls";
4075
4267
  urlsLabel.append(urlsTextarea);
4076
4268
  urlsField.append(urlsLabel);
4077
4269
  urlsFieldset.append(urlsField);
4270
+ const samples = this.renderSampleUrlButtons();
4271
+ if (samples) urlsFieldset.append(samples);
4078
4272
  panel.append(urlsFieldset);
4079
- const defaultsFieldset = this.document.createElement("fieldset");
4080
- const defaultsLegend = this.document.createElement("legend");
4081
- defaultsLegend.textContent = "Defaults (applied to all URLs without explicit values)";
4082
- defaultsFieldset.append(defaultsLegend);
4273
+ const checking = this.document.createElement("p");
4274
+ checking.dataset.testChecking = "true";
4275
+ checking.textContent = checkingCopy(this.testMethod, this.testResourceType);
4276
+ panel.append(checking);
4083
4277
  const defaultsGrid = this.document.createElement("div");
4084
4278
  defaultsGrid.dataset.testDefaults = "true";
4085
4279
  const methodField = this.document.createElement("div");
4086
4280
  methodField.dataset.editorField = "true";
4087
4281
  const methodLabel = this.document.createElement("label");
4088
- methodLabel.textContent = "Default HTTP method";
4282
+ methodLabel.textContent = "Method";
4089
4283
  const methodSelect = this.document.createElement("select");
4090
4284
  methodSelect.dataset.testMethod = "true";
4285
+ methodSelect.dataset.editorKey = "test-method";
4286
+ for (const method of HTTP_METHODS2) {
4287
+ const option = this.document.createElement("option");
4288
+ option.value = method;
4289
+ option.textContent = method;
4290
+ methodSelect.append(option);
4291
+ }
4292
+ const methodAny = this.document.createElement("option");
4293
+ methodAny.value = "";
4294
+ methodAny.textContent = "Any method";
4295
+ methodSelect.append(methodAny);
4091
4296
  methodSelect.value = this.testMethod;
4092
- const methodEmpty = this.document.createElement("option");
4093
- methodEmpty.value = "";
4094
- methodEmpty.textContent = "(none \u2014 not-applicable)";
4095
- methodSelect.append(methodEmpty);
4096
- for (const m of HTTP_METHODS2) {
4097
- const opt = this.document.createElement("option");
4098
- opt.value = m;
4099
- opt.textContent = m;
4100
- methodSelect.append(opt);
4101
- }
4102
4297
  methodLabel.append(methodSelect);
4103
4298
  methodField.append(methodLabel);
4104
4299
  const rtField = this.document.createElement("div");
4105
4300
  rtField.dataset.editorField = "true";
4106
4301
  const rtLabel = this.document.createElement("label");
4107
- rtLabel.textContent = "Default resource type";
4302
+ rtLabel.textContent = "Resource type";
4108
4303
  const rtSelect = this.document.createElement("select");
4109
4304
  rtSelect.dataset.testResourceType = "true";
4305
+ rtSelect.dataset.editorKey = "test-resource-type";
4306
+ for (const resourceType of RESOURCE_TYPES2) {
4307
+ const option = this.document.createElement("option");
4308
+ option.value = resourceType;
4309
+ option.textContent = RESOURCE_TYPE_LABELS[resourceType];
4310
+ rtSelect.append(option);
4311
+ }
4312
+ const rtAny = this.document.createElement("option");
4313
+ rtAny.value = "";
4314
+ rtAny.textContent = "Any resource type";
4315
+ rtSelect.append(rtAny);
4110
4316
  rtSelect.value = this.testResourceType;
4111
- const rtEmpty = this.document.createElement("option");
4112
- rtEmpty.value = "";
4113
- rtEmpty.textContent = "(none \u2014 not-applicable)";
4114
- rtSelect.append(rtEmpty);
4115
- for (const rt of RESOURCE_TYPES2) {
4116
- const opt = this.document.createElement("option");
4117
- opt.value = rt;
4118
- opt.textContent = rt;
4119
- rtSelect.append(opt);
4120
- }
4121
4317
  rtLabel.append(rtSelect);
4122
4318
  rtField.append(rtLabel);
4123
4319
  defaultsGrid.append(methodField, rtField);
4124
- defaultsFieldset.append(defaultsGrid);
4125
- panel.append(defaultsFieldset);
4126
- const maxCasesField = this.document.createElement("div");
4127
- maxCasesField.dataset.editorField = "true";
4128
- const maxCasesLabel = this.document.createElement("label");
4129
- maxCasesLabel.textContent = "Max test cases";
4130
- const maxCasesInput = this.document.createElement("input");
4131
- maxCasesInput.type = "number";
4132
- maxCasesInput.min = "1";
4133
- maxCasesInput.max = "10000";
4134
- maxCasesInput.value = this.testMaxCases;
4135
- maxCasesInput.style.width = "8rem";
4136
- maxCasesInput.dataset.testMaxCases = "true";
4137
- maxCasesLabel.append(maxCasesInput);
4138
- maxCasesField.append(maxCasesLabel);
4139
- panel.append(maxCasesField);
4140
- const runBtn = this.createButton("Run test", "test:run");
4141
- runBtn.type = "button";
4320
+ panel.append(defaultsGrid);
4321
+ const runBtn = this.createCommandButton(
4322
+ "Run test",
4323
+ "test:run",
4324
+ this.saving || this.testRunning,
4325
+ {},
4326
+ "primary"
4327
+ );
4142
4328
  runBtn.dataset.testRun = "true";
4143
- runBtn.dataset.command = "test:run";
4144
- runBtn.dataset.btn = "primary";
4145
- runBtn.disabled = this.saving || this.testRunning;
4146
4329
  panel.append(runBtn);
4147
4330
  this.form.append(panel);
4148
4331
  const resultsSection = this.document.createElement("section");
4149
4332
  resultsSection.dataset.testResults = "true";
4150
4333
  this.form.append(resultsSection);
4151
- if (this.testResult) {
4334
+ if (this.ruleCount() === 0) {
4335
+ const empty = this.document.createElement("p");
4336
+ empty.dataset.testEmpty = "true";
4337
+ empty.textContent = "This project has no rules to test.";
4338
+ resultsSection.append(empty);
4339
+ } else if (this.testResult) {
4152
4340
  this.renderTestResults(this.testResult);
4153
4341
  } else if (this.testRunning) {
4154
4342
  const pending = this.document.createElement("p");
4155
4343
  pending.dataset.testPending = "true";
4156
- pending.textContent = "Running dry-run...";
4344
+ pending.textContent = "Checking URLs...";
4157
4345
  resultsSection.append(pending);
4346
+ } else if (this.statusMessage === "Only 256 URLs can be checked at once.") {
4347
+ const limit = this.document.createElement("p");
4348
+ limit.dataset.testLimit = "true";
4349
+ limit.textContent = this.statusMessage;
4350
+ resultsSection.append(limit);
4351
+ }
4352
+ }
4353
+ renderSampleUrlButtons() {
4354
+ const samples = [];
4355
+ for (const group of this.draft.groups) {
4356
+ const groupId = safeText(group.id);
4357
+ for (const rule of group.rules) {
4358
+ const url = exactUrlFromSource(rule.source);
4359
+ if (!url) continue;
4360
+ samples.push({
4361
+ groupId,
4362
+ ruleId: safeText(rule.id),
4363
+ url,
4364
+ name: displayName(rule.name, "Unnamed rule")
4365
+ });
4366
+ }
4367
+ }
4368
+ if (samples.length === 0) return void 0;
4369
+ const row = this.document.createElement("div");
4370
+ row.dataset.testSamples = "true";
4371
+ for (const sample of samples) {
4372
+ const button = this.createCommandButton(
4373
+ "Try a URL from this rule",
4374
+ "test:try-url",
4375
+ false,
4376
+ { groupId: sample.groupId, ruleId: sample.ruleId, url: sample.url }
4377
+ );
4378
+ if (samples.length > 1) {
4379
+ button.setAttribute(
4380
+ "aria-label",
4381
+ `Try a URL from this rule, ${sample.name}`
4382
+ );
4383
+ }
4384
+ row.append(button);
4158
4385
  }
4386
+ return row;
4159
4387
  }
4160
4388
  renderRule(group, rule, groupIndex, ruleIndex) {
4161
4389
  const groupId = safeText(group.id);
@@ -26335,14 +26335,14 @@ function normalizeOptions(raw) {
26335
26335
  }
26336
26336
  maxCases = maxCasesProperty.value;
26337
26337
  }
26338
- let previewAction2;
26338
+ let previewAction;
26339
26339
  if (previewProperty.present) {
26340
26340
  if (previewProperty.value !== void 0 && typeof previewProperty.value !== "function") {
26341
26341
  return { ok: false };
26342
26342
  }
26343
- previewAction2 = previewProperty.value;
26343
+ previewAction = previewProperty.value;
26344
26344
  }
26345
- return { ok: true, maxCases, previewAction: previewAction2 };
26345
+ return { ok: true, maxCases, previewAction };
26346
26346
  }
26347
26347
  function dryRunProject(operations, cases, options) {
26348
26348
  const normalizedOptions = normalizeOptions(options);
@@ -26470,6 +26470,55 @@ function dryRunProject(operations, cases, options) {
26470
26470
  }
26471
26471
  };
26472
26472
  }
26473
+ function captureSubject(matcher, url) {
26474
+ if (matcher.source.key === "url") return url;
26475
+ try {
26476
+ return new URL(url).hostname;
26477
+ } catch {
26478
+ return null;
26479
+ }
26480
+ }
26481
+ function previewRuleAction(operation, url) {
26482
+ const subject = captureSubject(operation.matcher, url);
26483
+ if (subject === null) return null;
26484
+ const captures = matchUrlCaptures(operation.matcher.source.value, subject);
26485
+ if (captures === null) return null;
26486
+ const expand = (value) => substituteUrlCaptures(value, captures);
26487
+ if (operation.kind === "redirect") {
26488
+ return {
26489
+ kind: "redirect",
26490
+ summary: expand(operation.redirect.destination)
26491
+ };
26492
+ }
26493
+ if (operation.kind === "query") {
26494
+ const values = operation.action.params.map(
26495
+ (param) => param.operation === "remove" ? `${param.name}=<removed>` : `${param.name}=${expand(param.value ?? "")}`
26496
+ ).join(", ");
26497
+ return { kind: "query", summary: values };
26498
+ }
26499
+ if (operation.kind === "header") {
26500
+ if (operation.header.operation === "remove") {
26501
+ return { kind: "header", summary: `${operation.header.name}=<removed>` };
26502
+ }
26503
+ return {
26504
+ kind: "header",
26505
+ summary: `${operation.header.name}=${expand(operation.header.value ?? "")}`
26506
+ };
26507
+ }
26508
+ if (operation.kind === "request-body" && operation.requestBody.mode === "replace") {
26509
+ return {
26510
+ kind: operation.kind,
26511
+ summary: expand(operation.requestBody.body)
26512
+ };
26513
+ }
26514
+ if (operation.kind === "response-body" && "mode" in operation.responseBody && operation.responseBody.mode === "replace") {
26515
+ return {
26516
+ kind: operation.kind,
26517
+ summary: expand(operation.responseBody.body)
26518
+ };
26519
+ }
26520
+ return null;
26521
+ }
26473
26522
 
26474
26523
  // packages/cli/src/server/routes.ts
26475
26524
  function generateCsrfToken() {
@@ -26540,18 +26589,6 @@ function bodyErrorResponse(error) {
26540
26589
  body: { code: "invalid-json", message: "Invalid JSON body" }
26541
26590
  };
26542
26591
  }
26543
- function toMatcherOperations(operations) {
26544
- return operations.map(
26545
- ({ groupId, ruleId, name, matcher, redactSensitiveInLogs }) => ({
26546
- kind: "matcher",
26547
- groupId,
26548
- ruleId,
26549
- name,
26550
- matcher,
26551
- redactSensitiveInLogs
26552
- })
26553
- );
26554
- }
26555
26592
  function isRecord2(value) {
26556
26593
  return typeof value === "object" && value !== null && !Array.isArray(value);
26557
26594
  }
@@ -26864,10 +26901,13 @@ function createRoutes(context) {
26864
26901
  );
26865
26902
  return;
26866
26903
  }
26904
+ const requested = isRecord2(body.options) && typeof body.options.maxCases === "number" && Number.isSafeInteger(body.options.maxCases) && body.options.maxCases > 0 ? body.options.maxCases : void 0;
26905
+ const options = { previewAction: previewRuleAction };
26906
+ if (requested !== void 0) options.maxCases = requested;
26867
26907
  const result = dryRunProject(
26868
- toMatcherOperations(compileResult.operations),
26908
+ compileResult.operations,
26869
26909
  body.cases,
26870
- body.options ?? {}
26910
+ options
26871
26911
  );
26872
26912
  res.writeHead(200, { "Content-Type": "application/json" });
26873
26913
  res.end(JSON.stringify(result));
@@ -27872,7 +27912,41 @@ function generateEditorHtml(apiBase, csrfToken, filePath, aiConfigured) {
27872
27912
  },
27873
27913
  body: JSON.stringify({ project: currentProject, cases, options }),
27874
27914
  });
27875
- return res.json();
27915
+ let payload = null;
27916
+ try {
27917
+ payload = await res.json();
27918
+ } catch (error) {
27919
+ payload = null;
27920
+ }
27921
+ if (!res.ok) {
27922
+ const diagnostics = payload && Array.isArray(payload.diagnostics)
27923
+ ? payload.diagnostics.map(function (diagnostic) {
27924
+ return {
27925
+ code: diagnostic && typeof diagnostic.code === 'string'
27926
+ ? diagnostic.code
27927
+ : 'editor.validation-failed',
27928
+ severity: 'error',
27929
+ path: diagnostic && typeof diagnostic.path === 'string'
27930
+ ? diagnostic.path
27931
+ : '',
27932
+ message: diagnostic && typeof diagnostic.message === 'string'
27933
+ ? diagnostic.message
27934
+ : 'The project could not be tested.',
27935
+ };
27936
+ })
27937
+ : [{
27938
+ code: payload && typeof payload.code === 'string'
27939
+ ? payload.code
27940
+ : 'editor.validation-failed',
27941
+ severity: 'error',
27942
+ path: '',
27943
+ message: payload && typeof payload.message === 'string'
27944
+ ? payload.message
27945
+ : 'The project could not be tested.',
27946
+ }];
27947
+ return { ok: false, diagnostics: diagnostics };
27948
+ }
27949
+ return payload;
27876
27950
  },
27877
27951
  onCancel: () => {
27878
27952
  cancel();
@@ -29947,7 +30021,7 @@ function errorMessage(error) {
29947
30021
  // packages/cli/src/commands/runtime.ts
29948
30022
  import { chmod, mkdir as mkdir4, rm as rm2, writeFile as writeFile4 } from "node:fs/promises";
29949
30023
  import { dirname as dirname7, join as join7, resolve as resolve6 } from "node:path";
29950
- function toMatcherOperations2(operations) {
30024
+ function toMatcherOperations(operations) {
29951
30025
  return operations.map(
29952
30026
  ({ groupId, ruleId, name, matcher, redactSensitiveInLogs }) => ({
29953
30027
  kind: "matcher",
@@ -30239,7 +30313,7 @@ async function runtimeHostCommand(args, options = {}) {
30239
30313
  const normalized = normalizeRuntimePreset({
30240
30314
  version: 1,
30241
30315
  limits: RUNTIME_LIMITS,
30242
- matchers: toMatcherOperations2(compileResult.operations),
30316
+ matchers: toMatcherOperations(compileResult.operations),
30243
30317
  grants: []
30244
30318
  });
30245
30319
  if (!normalized.ok) {
@@ -30284,55 +30358,6 @@ function usageError(message) {
30284
30358
  return `Error: ${message}
30285
30359
  `;
30286
30360
  }
30287
- function captureSubject(matcher, url) {
30288
- if (matcher.source.key === "url") return url;
30289
- try {
30290
- return new URL(url).hostname;
30291
- } catch {
30292
- return null;
30293
- }
30294
- }
30295
- function previewAction(operation, url) {
30296
- const subject = captureSubject(operation.matcher, url);
30297
- if (subject === null) return null;
30298
- const captures = matchUrlCaptures(operation.matcher.source.value, subject);
30299
- if (captures === null) return null;
30300
- const expand = (value) => substituteUrlCaptures(value, captures);
30301
- if (operation.kind === "redirect") {
30302
- return {
30303
- kind: "redirect",
30304
- summary: expand(operation.redirect.destination)
30305
- };
30306
- }
30307
- if (operation.kind === "query") {
30308
- const values = operation.action.params.map(
30309
- (param) => param.operation === "remove" ? `${param.name}=<removed>` : `${param.name}=${expand(param.value ?? "")}`
30310
- ).join(", ");
30311
- return { kind: "query", summary: values };
30312
- }
30313
- if (operation.kind === "header") {
30314
- if (operation.header.operation === "remove") {
30315
- return { kind: "header", summary: `${operation.header.name}=<removed>` };
30316
- }
30317
- return {
30318
- kind: "header",
30319
- summary: `${operation.header.name}=${expand(operation.header.value ?? "")}`
30320
- };
30321
- }
30322
- if (operation.kind === "request-body" && operation.requestBody.mode === "replace") {
30323
- return {
30324
- kind: operation.kind,
30325
- summary: expand(operation.requestBody.body)
30326
- };
30327
- }
30328
- if (operation.kind === "response-body" && "mode" in operation.responseBody && operation.responseBody.mode === "replace") {
30329
- return {
30330
- kind: operation.kind,
30331
- summary: expand(operation.responseBody.body)
30332
- };
30333
- }
30334
- return null;
30335
- }
30336
30361
  function isUrl(value) {
30337
30362
  return parseTestUrl(value).ok;
30338
30363
  }
@@ -30387,7 +30412,7 @@ function addDefaults(cases, defaultMethod, defaultResourceType) {
30387
30412
  );
30388
30413
  }
30389
30414
  function resultOptions(maxCases) {
30390
- const options = { previewAction };
30415
+ const options = { previewAction: previewRuleAction };
30391
30416
  if (maxCases !== void 0) options.maxCases = maxCases;
30392
30417
  return options;
30393
30418
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rogatio/cli",
3
- "version": "6.8.0",
3
+ "version": "6.9.1",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Local-first browser request and response rules — editor host, file verification, test runner, and runtime dispatch.",