datagrok-tools 6.5.0 → 6.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +40 -32
- package/bin/__tests__/build.test.js +116 -0
- package/bin/__tests__/jira-wiki-converter.test.js +101 -0
- package/bin/__tests__/node-dapi.connections.test.js +120 -0
- package/bin/__tests__/node-dapi.groups.test.js +467 -0
- package/bin/commands/publish.js +15 -7
- package/bin/commands/stress-tests.js +3 -0
- package/bin/commands/test.js +12 -16
- package/bin/utils/test-utils.js +24 -0
- package/package-template/src/package-test.ts +10 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,41 +1,49 @@
|
|
|
1
1
|
# Datagrok-tools changelog
|
|
2
2
|
|
|
3
|
-
## 6.5.
|
|
3
|
+
## 6.5.2 (2026-07-27)
|
|
4
4
|
|
|
5
|
-
* `grok
|
|
5
|
+
* `grok publish` — a failed `docker push` is no longer reported as a successful one. `image.json` claimed the tag regardless, so the server recorded a container image that was never published and the spawner failed validation forever ("Image ... not found in any registry") with no recovery short of another publish. The push failure now falls back to an image that is actually in the registry, and says so.
|
|
6
|
+
|
|
7
|
+
## 6.5.1 (2026-07-22)
|
|
8
|
+
|
|
9
|
+
* `grok test` — the Node-pass report now merges into the browser report by column name. The line-wise merge assumed identical column order, so node rows landed misaligned (string values in the integer `ms` column) and the CI test-report upload failed with `invalid input syntax for type integer`.
|
|
10
|
+
|
|
11
|
+
## 6.5.0 (WIP)
|
|
12
|
+
|
|
13
|
+
* `grok test` — added a Node (browserless) pass: tests annotated `{node: true}` run headless under the js-api Node runtime before the browser launches; the browser pass excludes them and is skipped entirely when nothing browser-only matches. New flags: `--skip-node`, `--node-only`. Packages opt in by exporting `testNode()` from `package-test.ts`; others keep the previous behavior.
|
|
6
14
|
|
|
7
15
|
## 6.4.8 (2026-07-22)
|
|
8
16
|
|
|
9
|
-
* GROK-20452: `grok publish`
|
|
10
|
-
* `grok test`
|
|
17
|
+
* GROK-20452: `grok publish` — always `docker build` the tools-generated worker dirs (`dockerfiles/queue`, `dockerfiles/celery`, `dockerfiles/<app>-celery`) instead of reusing a cached local tag. The "found local image" shortcut froze the worker at whatever stock base the daemon had when the tag was first built (CI ran day-old worker code with a fresh base available); the layer cache keeps an unchanged rebuild near-instant.
|
|
18
|
+
* `grok test` — `--no-retry` is now honored for Playwright runs. minimist parsed `--no-retry` as `{retry:false}`, so the flag was silently dropped and failed specs were still retried once; normalized so `--retries=0` reaches Playwright.
|
|
11
19
|
|
|
12
20
|
## 6.4.7 (2026-07-22)
|
|
13
21
|
|
|
14
|
-
* GROK-20452: `grok publish`
|
|
22
|
+
* GROK-20452: `grok publish` — generate `dockerfiles/celery` / `dockerfiles/queue` **before** gathering files for the zip. They were generated after, so the generated Dockerfile never reached the server: `meta.queue` functions fell back to a server-side container named `<pkg>-queue-celery` while the client had built `<pkg>-queue` — image validation 404'd and the worker container never started ("Container is not started" on every call).
|
|
15
23
|
|
|
16
24
|
## 6.4.6 (2026-07-22)
|
|
17
25
|
|
|
18
|
-
* `grok test`
|
|
26
|
+
* `grok test` — the whole-run Puppeteer cap (60 min) is now overridable via `GROK_TEST_INVOCATION_TIMEOUT_MS`. When the cap fires mid-pass all collected results are discarded (empty `test-report.csv`, "Passed tests: 0"), which CI reports as "no results produced"; loaded CI agents can now raise the cap instead.
|
|
19
27
|
|
|
20
28
|
## 6.4.5 (2026-06-23)
|
|
21
29
|
|
|
22
|
-
* `grok test`
|
|
30
|
+
* `grok test` — screen recording (`--record`) is now optional: if `page.screencast()` can't start (e.g. `ffmpeg` is missing on the runner, `spawnSync ffmpeg ENOENT`), the run warns and continues instead of failing the whole Puppeteer pass with 0 tests executed.
|
|
23
31
|
|
|
24
32
|
## 6.4.4 (2026-06-22)
|
|
25
33
|
|
|
26
|
-
* `grok publish`
|
|
34
|
+
* `grok publish` — fixed every publish failing with a silent `exit 1` after a successful upload: a stray `fs.unlinkSync('zip')` threw `ENOENT` (the archive is streamed in-memory, no `zip` file is ever written), and the surrounding `catch` only logged under `--verbose`. The errant unlink is removed and publish errors are now always surfaced.
|
|
27
35
|
|
|
28
36
|
## 6.4.3 (2026-06-22)
|
|
29
37
|
|
|
30
|
-
* `grok api`
|
|
38
|
+
* `grok api` — numeric IVP model inputs are now generated with `nullable: false`, so an emptied input field fails form validation instead of running the solver with a null.
|
|
31
39
|
|
|
32
40
|
## 6.4.2 (2026-06-18)
|
|
33
41
|
|
|
34
|
-
* `grok publish`
|
|
42
|
+
* `grok publish` — registry-aware Docker fallback: when a package's image isn't built locally and the target server has no compatible record, `grok publish` now checks the configured registry and Docker Hub (`docker manifest inspect`) for the expected `datagrok/<name>:<version>` (and content-hashed) tag and uses it, instead of reporting "No fallback available" and failing. Fixes dependency publishes (e.g. Bio → @datagrok/chem) on CI runners where the image exists in the registry but not locally.
|
|
35
43
|
|
|
36
44
|
## 6.4.1 (2026-06-18)
|
|
37
45
|
|
|
38
|
-
* Fixed `grok` failing with `Cannot find module './commands/build'`
|
|
46
|
+
* Fixed `grok` failing with `Cannot find module './commands/build'` — the `.npmignore` `build.js` rule was unanchored and excluded the compiled `bin/commands/build.js` from the published package; anchored it to `/build.js`.
|
|
39
47
|
* Pinned `ignore-walk` to ^6.0.5 so the package still supports Node 18 (9.x requires Node 22+).
|
|
40
48
|
|
|
41
49
|
## 6.4.0 (2026-06-18)
|
|
@@ -47,46 +55,46 @@
|
|
|
47
55
|
|
|
48
56
|
## 6.3.3 (2026-06-16)
|
|
49
57
|
|
|
50
|
-
* Fixed Celery Docker image generation
|
|
58
|
+
* Fixed Celery Docker image generation — the image wasn't built locally on publish.
|
|
51
59
|
|
|
52
60
|
## 6.3.2 (2026-06-15)
|
|
53
61
|
|
|
54
|
-
* `func-gen` webpack plugin
|
|
55
|
-
* `func-gen` webpack plugin
|
|
56
|
-
* `grok stresstest`
|
|
62
|
+
* `func-gen` webpack plugin — generated RichFunctionView model inputs now use the script-form names (argument bounds/step `_t0`/`_t1`/`_h`, loop count `_count`) instead of the deprefixed forms, so the run, fitting, and sensitivity-analysis paths share one set of input names with diff-grok's pipeline. Fixes `Inconsistent inputs: "_t0" is missing` when starting fitting/SA from a Rich Function View.
|
|
63
|
+
* `func-gen` webpack plugin — the generated RichFunctionView model output annotation appends the `DiffStudio Facet` viewer (last), alongside Grid and Line chart.
|
|
64
|
+
* `grok stresstest` — run the ApiTests Node test runner directly via tsx (`src/package-test-node.ts`) instead of a compiled `dist-node` bundle; dropped the obsolete `npm run build-node` step and the `tsconfig-paths-bootstrap.js` / `dist-node` invocation.
|
|
57
65
|
|
|
58
66
|
## 6.3.0 (2026-06-12)
|
|
59
67
|
|
|
60
|
-
* `func-gen` webpack plugin
|
|
68
|
+
* `func-gen` webpack plugin — generates RichFunctionView model wrappers from Diff Studio `#meta.role: model` `.ivp` files. Inputs/output are derived from the parsed IVP, `#meta.icon` becomes the model icon, and `#meta.inputs` lookups are emitted as real `propagateChoice: all` inputs (rendered natively by the Rich Function View, unlike `meta.inputs`). Ships a prebuilt, tree-shaken CJS bundle of diff-grok's IVP parser (`plugins/ivp-parser.bundle.cjs`); regenerate with `npm run update:ivp-parser`.
|
|
61
69
|
|
|
62
70
|
## 6.2.6 (2026-05-26)
|
|
63
71
|
|
|
64
|
-
* `grok s tables upload`
|
|
72
|
+
* `grok s tables upload` — accepts `.d42` binary blobs in addition to `.csv`. Content-Type is auto-detected from the file extension (`application/octet-stream` for d42, `text/csv` otherwise); server content-negotiates and persists either form against the same `/public/v1/tables/{name}` endpoint.
|
|
65
73
|
|
|
66
74
|
## 6.2.5 (2026-05-21)
|
|
67
75
|
|
|
68
|
-
* `grok report read`
|
|
69
|
-
* `grok report read`
|
|
76
|
+
* `grok report read` — renamed `--extract-actions` to `--extract-client-log`; sidecar is now `<stem>_client_log.json`. The old flag is no longer accepted.
|
|
77
|
+
* `grok report read` — legacy `actions` field in pre-consolidation report zips is folded into `clientLog` at read time (stderr warning emitted), so downstream consumers see one canonical field. Companion to the platform-side merge that drops `reports_data.actions` in favor of `client_log`.
|
|
70
78
|
|
|
71
79
|
## 6.2.4 (2026-05-13)
|
|
72
80
|
|
|
73
|
-
* GROK-20097: package template
|
|
81
|
+
* GROK-20097: package template — replaced deprecated `"moduleResolution": "node"` in `tsconfig.json` with `"bundler"`, suppressing the TypeScript warning for new packages.
|
|
74
82
|
|
|
75
83
|
## 6.2.3 (2026-05-06)
|
|
76
84
|
|
|
77
|
-
* `grok test`
|
|
85
|
+
* `grok test` — added `--skip-puppeteer` flag (symmetric counterpart of `--skip-playwright`). Bypasses `loadPackages()` and the Puppeteer/`DG.Test` runner so Playwright-only test directories (e.g. `public/playwright-public`) can run end-to-end via `grok test --skip-puppeteer` without tripping the Dart/JS package loader.
|
|
78
86
|
|
|
79
87
|
## 6.2.2 (2026-05-05)
|
|
80
88
|
|
|
81
|
-
* `grok test`
|
|
89
|
+
* `grok test` — Playwright runner now writes `test-report-playwright.csv` next to the existing merged `test-report.csv`, so CI can ship Playwright rows to a dedicated Datlas reporting bucket without disturbing the legacy `package` flow.
|
|
82
90
|
|
|
83
91
|
## 6.2.1 (2026-05-05)
|
|
84
92
|
|
|
85
|
-
* Reports: `grok report attach <ticket> <file>`
|
|
93
|
+
* Reports: `grok report attach <ticket> <file>` — upload a file as a JIRA issue attachment via REST v2 multipart POST.
|
|
86
94
|
|
|
87
95
|
## 6.2.0 (2026-05-04)
|
|
88
96
|
|
|
89
|
-
* `grok test`
|
|
97
|
+
* `grok test` — Playwright support: when a package's `package.json` declares `"playwrightTests": "<path>"`, `grok test` runs `npx playwright test` against that directory in addition to the existing Puppeteer pass and merges results into a single `test-report.csv`. Auth is unified with the Puppeteer pass (dev key from `~/.grok/config.yaml` → session token → cookie + `localStorage` injection — no login form). Optional `DATAGROK_DEV_KEY_2` env var enables a second-user identity for specs that need it (`DATAGROK_AUTH_TOKEN_2` exposed to specs). New `--skip-playwright` flag opts out of the Playwright pass for a single run.
|
|
90
98
|
|
|
91
99
|
## 6.1.14 (2026-05-01)
|
|
92
100
|
|
|
@@ -98,17 +106,17 @@
|
|
|
98
106
|
|
|
99
107
|
## 6.1.11 (2026-04-27)
|
|
100
108
|
|
|
101
|
-
* `grok report read <path | instance number>`
|
|
102
|
-
* `grok report fetch`
|
|
109
|
+
* `grok report read <path | instance number>` — normalize a report zip/json into one JSON object on stdout (envelope unwrap, `_meta.json` merge, optional `--extract-screenshot` / `--extract-d42` / `--extract-actions`)
|
|
110
|
+
* `grok report fetch` — single round-trip via the new `/reports/by-number/<number>/zip` endpoint, with fallback to the legacy search-then-download path on 404
|
|
103
111
|
|
|
104
112
|
## 6.1.10 (2026-04-17)
|
|
105
113
|
|
|
106
|
-
* `grok s connections save`
|
|
107
|
-
* `grok s connections test`
|
|
108
|
-
* `grok s users save`
|
|
109
|
-
* `grok s groups save`
|
|
110
|
-
* `grok s shares add`
|
|
111
|
-
* `grok s shares list`
|
|
114
|
+
* `grok s connections save` — create/update a connection from a JSON file (`--save-credentials` optional)
|
|
115
|
+
* `grok s connections test` — test connectivity by JSON body or by existing id/name
|
|
116
|
+
* `grok s users save` — create/update a user from a JSON file
|
|
117
|
+
* `grok s groups save` — create/update a group from a JSON file (`--save-relations` optional)
|
|
118
|
+
* `grok s shares add` — share an entity with one or more groups (`--access View|Edit`)
|
|
119
|
+
* `grok s shares list` — list who an entity is shared with
|
|
112
120
|
* Tools: Normalize package name to lowercase in grok create, preserve original as friendlyName
|
|
113
121
|
|
|
114
122
|
## 5.1.3 (2026-02-03)
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
var _vitest = require("vitest");
|
|
4
|
+
var _build = require("../commands/build");
|
|
5
|
+
(0, _vitest.describe)('getNestedValue', () => {
|
|
6
|
+
(0, _vitest.it)('returns value for a simple key', () => {
|
|
7
|
+
(0, _vitest.expect)((0, _build.getNestedValue)({
|
|
8
|
+
name: 'Chem'
|
|
9
|
+
}, 'name')).toBe('Chem');
|
|
10
|
+
});
|
|
11
|
+
(0, _vitest.it)('returns value for a nested path', () => {
|
|
12
|
+
(0, _vitest.expect)((0, _build.getNestedValue)({
|
|
13
|
+
a: {
|
|
14
|
+
b: {
|
|
15
|
+
c: 42
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}, 'a.b.c')).toBe(42);
|
|
19
|
+
});
|
|
20
|
+
(0, _vitest.it)('returns undefined for a missing key', () => {
|
|
21
|
+
(0, _vitest.expect)((0, _build.getNestedValue)({
|
|
22
|
+
name: 'Chem'
|
|
23
|
+
}, 'version')).toBeUndefined();
|
|
24
|
+
});
|
|
25
|
+
(0, _vitest.it)('returns undefined when a mid-path segment is null', () => {
|
|
26
|
+
(0, _vitest.expect)((0, _build.getNestedValue)({
|
|
27
|
+
a: null
|
|
28
|
+
}, 'a.b')).toBeUndefined();
|
|
29
|
+
});
|
|
30
|
+
(0, _vitest.it)('returns undefined when a mid-path segment is missing', () => {
|
|
31
|
+
(0, _vitest.expect)((0, _build.getNestedValue)({
|
|
32
|
+
a: {}
|
|
33
|
+
}, 'a.b.c')).toBeUndefined();
|
|
34
|
+
});
|
|
35
|
+
(0, _vitest.it)('returns undefined for an empty path (splits to empty string key)', () => {
|
|
36
|
+
(0, _vitest.expect)((0, _build.getNestedValue)({
|
|
37
|
+
x: 1
|
|
38
|
+
}, '')).toBeUndefined();
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
const pkg = overrides => ({
|
|
42
|
+
dir: '/tmp/pkg',
|
|
43
|
+
name: overrides.name ?? 'test-pkg',
|
|
44
|
+
friendlyName: overrides.friendlyName ?? overrides.name ?? 'Test Pkg',
|
|
45
|
+
version: overrides.version ?? '1.0.0',
|
|
46
|
+
packageJson: overrides
|
|
47
|
+
});
|
|
48
|
+
(0, _vitest.describe)('applyFilter', () => {
|
|
49
|
+
const packages = [pkg({
|
|
50
|
+
name: 'Chem',
|
|
51
|
+
version: '1.5.0',
|
|
52
|
+
category: 'Cheminformatics'
|
|
53
|
+
}), pkg({
|
|
54
|
+
name: 'Bio',
|
|
55
|
+
version: '2.0.0',
|
|
56
|
+
category: 'Bioinformatics'
|
|
57
|
+
}), pkg({
|
|
58
|
+
name: 'PowerGrid',
|
|
59
|
+
version: '1.5.0',
|
|
60
|
+
category: 'Viewers'
|
|
61
|
+
})];
|
|
62
|
+
(0, _vitest.it)('returns all packages when filter matches all', () => {
|
|
63
|
+
(0, _vitest.expect)((0, _build.applyFilter)(packages, 'name:.')).toHaveLength(3);
|
|
64
|
+
});
|
|
65
|
+
(0, _vitest.it)('filters by exact name match', () => {
|
|
66
|
+
const result = (0, _build.applyFilter)(packages, 'name:^Chem$');
|
|
67
|
+
(0, _vitest.expect)(result).toHaveLength(1);
|
|
68
|
+
(0, _vitest.expect)(result[0].name).toBe('Chem');
|
|
69
|
+
});
|
|
70
|
+
(0, _vitest.it)('filters by partial name (regex substring)', () => {
|
|
71
|
+
const result = (0, _build.applyFilter)(packages, 'name:Bio');
|
|
72
|
+
(0, _vitest.expect)(result).toHaveLength(1);
|
|
73
|
+
(0, _vitest.expect)(result[0].name).toBe('Bio');
|
|
74
|
+
});
|
|
75
|
+
(0, _vitest.it)('returns empty array when nothing matches', () => {
|
|
76
|
+
(0, _vitest.expect)((0, _build.applyFilter)(packages, 'name:NOMATCH')).toHaveLength(0);
|
|
77
|
+
});
|
|
78
|
+
(0, _vitest.it)('filters by version', () => {
|
|
79
|
+
const result = (0, _build.applyFilter)(packages, 'version:^1\\.5');
|
|
80
|
+
(0, _vitest.expect)(result).toHaveLength(2);
|
|
81
|
+
(0, _vitest.expect)(result.map(p => p.name)).toEqual(_vitest.expect.arrayContaining(['Chem', 'PowerGrid']));
|
|
82
|
+
});
|
|
83
|
+
(0, _vitest.it)('applies && conjunction (both conditions must match)', () => {
|
|
84
|
+
const result = (0, _build.applyFilter)(packages, 'name:Chem && version:1\\.5');
|
|
85
|
+
(0, _vitest.expect)(result).toHaveLength(1);
|
|
86
|
+
(0, _vitest.expect)(result[0].name).toBe('Chem');
|
|
87
|
+
});
|
|
88
|
+
(0, _vitest.it)('returns empty when one part of && conjunction fails', () => {
|
|
89
|
+
(0, _vitest.expect)((0, _build.applyFilter)(packages, 'name:Chem && version:^2')).toHaveLength(0);
|
|
90
|
+
});
|
|
91
|
+
(0, _vitest.it)('filters by nested field', () => {
|
|
92
|
+
const withNested = [pkg({
|
|
93
|
+
name: 'A',
|
|
94
|
+
datagrok: {
|
|
95
|
+
apiVersion: '1.0'
|
|
96
|
+
}
|
|
97
|
+
}), pkg({
|
|
98
|
+
name: 'B',
|
|
99
|
+
datagrok: {
|
|
100
|
+
apiVersion: '2.0'
|
|
101
|
+
}
|
|
102
|
+
})];
|
|
103
|
+
const result = (0, _build.applyFilter)(withNested, 'datagrok.apiVersion:^1');
|
|
104
|
+
(0, _vitest.expect)(result).toHaveLength(1);
|
|
105
|
+
(0, _vitest.expect)(result[0].name).toBe('A');
|
|
106
|
+
});
|
|
107
|
+
(0, _vitest.it)('returns empty when field does not exist', () => {
|
|
108
|
+
(0, _vitest.expect)((0, _build.applyFilter)(packages, 'nonexistent:anything')).toHaveLength(0);
|
|
109
|
+
});
|
|
110
|
+
(0, _vitest.it)('treats filter with no colon as field name with match-all pattern', () => {
|
|
111
|
+
// No colon → field = whole string, pattern = /./ (matches any value)
|
|
112
|
+
// The function returns packages where the field exists and is non-empty
|
|
113
|
+
const result = (0, _build.applyFilter)(packages, 'name');
|
|
114
|
+
(0, _vitest.expect)(result).toHaveLength(3);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
var _vitest = require("vitest");
|
|
4
|
+
var _report = require("../commands/report");
|
|
5
|
+
(0, _vitest.describe)('markdownToJiraWiki — basic rules', () => {
|
|
6
|
+
(0, _vitest.it)('converts H1', () => {
|
|
7
|
+
(0, _vitest.expect)((0, _report.markdownToJiraWiki)('# Title')).toBe('h1. Title');
|
|
8
|
+
});
|
|
9
|
+
(0, _vitest.it)('converts H2 / H3 / H6', () => {
|
|
10
|
+
(0, _vitest.expect)((0, _report.markdownToJiraWiki)('## Sub')).toBe('h2. Sub');
|
|
11
|
+
(0, _vitest.expect)((0, _report.markdownToJiraWiki)('### Sub-sub')).toBe('h3. Sub-sub');
|
|
12
|
+
(0, _vitest.expect)((0, _report.markdownToJiraWiki)('###### Deep')).toBe('h6. Deep');
|
|
13
|
+
});
|
|
14
|
+
(0, _vitest.it)('converts bold', () => {
|
|
15
|
+
(0, _vitest.expect)((0, _report.markdownToJiraWiki)('hello **world** foo')).toBe('hello *world* foo');
|
|
16
|
+
});
|
|
17
|
+
(0, _vitest.it)('converts italic with single asterisks', () => {
|
|
18
|
+
(0, _vitest.expect)((0, _report.markdownToJiraWiki)('hello *world* foo')).toBe('hello _world_ foo');
|
|
19
|
+
});
|
|
20
|
+
(0, _vitest.it)('converts strikethrough', () => {
|
|
21
|
+
(0, _vitest.expect)((0, _report.markdownToJiraWiki)('~~gone~~')).toBe('-gone-');
|
|
22
|
+
});
|
|
23
|
+
(0, _vitest.it)('converts links', () => {
|
|
24
|
+
(0, _vitest.expect)((0, _report.markdownToJiraWiki)('see [docs](https://x.example/y)')).toBe('see [docs|https://x.example/y]');
|
|
25
|
+
});
|
|
26
|
+
(0, _vitest.it)('converts blockquote', () => {
|
|
27
|
+
(0, _vitest.expect)((0, _report.markdownToJiraWiki)('> quoted line')).toBe('bq. quoted line');
|
|
28
|
+
});
|
|
29
|
+
(0, _vitest.it)('converts unordered list', () => {
|
|
30
|
+
const md = '- one\n- two\n- three';
|
|
31
|
+
(0, _vitest.expect)((0, _report.markdownToJiraWiki)(md)).toBe('* one\n* two\n* three');
|
|
32
|
+
});
|
|
33
|
+
(0, _vitest.it)('converts one level of nested unordered list', () => {
|
|
34
|
+
const md = '- top\n - sub\n- back';
|
|
35
|
+
(0, _vitest.expect)((0, _report.markdownToJiraWiki)(md)).toBe('* top\n** sub\n* back');
|
|
36
|
+
});
|
|
37
|
+
(0, _vitest.it)('converts ordered list', () => {
|
|
38
|
+
const md = '1. one\n2. two\n3. three';
|
|
39
|
+
(0, _vitest.expect)((0, _report.markdownToJiraWiki)(md)).toBe('# one\n# two\n# three');
|
|
40
|
+
});
|
|
41
|
+
(0, _vitest.it)('converts inline code', () => {
|
|
42
|
+
(0, _vitest.expect)((0, _report.markdownToJiraWiki)('use `foo()` here')).toBe('use {{foo()}} here');
|
|
43
|
+
});
|
|
44
|
+
(0, _vitest.it)('converts plain code fence to noformat', () => {
|
|
45
|
+
const md = '```\nraw code\nmore\n```';
|
|
46
|
+
(0, _vitest.expect)((0, _report.markdownToJiraWiki)(md)).toBe('{noformat}\nraw code\nmore\n{noformat}');
|
|
47
|
+
});
|
|
48
|
+
(0, _vitest.it)('converts code fence with language tag to {code:lang}', () => {
|
|
49
|
+
const md = '```js\nconst x = 1;\n```';
|
|
50
|
+
(0, _vitest.expect)((0, _report.markdownToJiraWiki)(md)).toBe('{code:js}\nconst x = 1;\n{code}');
|
|
51
|
+
});
|
|
52
|
+
(0, _vitest.it)('converts HTML entities / & / < / >', () => {
|
|
53
|
+
(0, _vitest.expect)((0, _report.markdownToJiraWiki)('a b')).toBe('a b');
|
|
54
|
+
(0, _vitest.expect)((0, _report.markdownToJiraWiki)('a&b')).toBe('a&b');
|
|
55
|
+
(0, _vitest.expect)((0, _report.markdownToJiraWiki)('a<b>c')).toBe('a<b>c');
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
(0, _vitest.describe)('markdownToJiraWiki — content-protection inside code fences', () => {
|
|
59
|
+
(0, _vitest.it)('does not transform `{{plates}}` inside a fenced block (run-#4 false-positive case)', () => {
|
|
60
|
+
const md = ['Before', '```', 'context: {{plates}} should stay literal', '# not a heading', '- not a list', '```', 'After'].join('\n');
|
|
61
|
+
const out = (0, _report.markdownToJiraWiki)(md);
|
|
62
|
+
(0, _vitest.expect)(out).toContain('{noformat}\ncontext: {{plates}} should stay literal');
|
|
63
|
+
(0, _vitest.expect)(out).toContain('# not a heading');
|
|
64
|
+
(0, _vitest.expect)(out).toContain('- not a list');
|
|
65
|
+
(0, _vitest.expect)(out).not.toContain('h1.');
|
|
66
|
+
});
|
|
67
|
+
(0, _vitest.it)('does not transform headings/links/lists inside an inline code span', () => {
|
|
68
|
+
const md = 'use `# not a heading` and `[ref](u)` here';
|
|
69
|
+
(0, _vitest.expect)((0, _report.markdownToJiraWiki)(md)).toBe('use {{# not a heading}} and {{[ref](u)}} here');
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
(0, _vitest.describe)('markdownToJiraWiki — edge cases', () => {
|
|
73
|
+
(0, _vitest.it)('handles bold containing italic: **bold *inside* bold**', () => {
|
|
74
|
+
(0, _vitest.expect)((0, _report.markdownToJiraWiki)('**bold *inside* bold**')).toBe('*bold _inside_ bold*');
|
|
75
|
+
});
|
|
76
|
+
(0, _vitest.it)('preserves bold and italic on the same line', () => {
|
|
77
|
+
(0, _vitest.expect)((0, _report.markdownToJiraWiki)('**a** and *b*')).toBe('*a* and _b_');
|
|
78
|
+
});
|
|
79
|
+
(0, _vitest.it)('handles a heading whose text contains backticks', () => {
|
|
80
|
+
(0, _vitest.expect)((0, _report.markdownToJiraWiki)('## use `foo()` for x')).toBe('h2. use {{foo()}} for x');
|
|
81
|
+
});
|
|
82
|
+
(0, _vitest.it)('passes plain text through unchanged', () => {
|
|
83
|
+
const plain = 'Just a normal sentence with no markdown.';
|
|
84
|
+
(0, _vitest.expect)((0, _report.markdownToJiraWiki)(plain)).toBe(plain);
|
|
85
|
+
});
|
|
86
|
+
(0, _vitest.it)('returns empty string unchanged', () => {
|
|
87
|
+
(0, _vitest.expect)((0, _report.markdownToJiraWiki)('')).toBe('');
|
|
88
|
+
});
|
|
89
|
+
(0, _vitest.it)('handles a multi-block document end-to-end', () => {
|
|
90
|
+
const md = ['# Handoff', '', 'Some **bold** intro and a [link](https://x.example).', '', '## Findings', '', '- first', '- second', '', '> note: this matters', '', '```js', 'const x = 1;', '```', '', 'Done here.'].join('\n');
|
|
91
|
+
const out = (0, _report.markdownToJiraWiki)(md);
|
|
92
|
+
(0, _vitest.expect)(out).toContain('h1. Handoff');
|
|
93
|
+
(0, _vitest.expect)(out).toContain('h2. Findings');
|
|
94
|
+
(0, _vitest.expect)(out).toContain('Some *bold* intro and a [link|https://x.example].');
|
|
95
|
+
(0, _vitest.expect)(out).toContain('* first\n* second');
|
|
96
|
+
(0, _vitest.expect)(out).toContain('bq. note: this matters');
|
|
97
|
+
(0, _vitest.expect)(out).toContain('{code:js}\nconst x = 1;\n{code}');
|
|
98
|
+
(0, _vitest.expect)(out).toContain('Done here.');
|
|
99
|
+
(0, _vitest.expect)(out).not.toContain(' ');
|
|
100
|
+
});
|
|
101
|
+
});
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
var _vitest = require("vitest");
|
|
4
|
+
var _nodeDapi = require("../utils/node-dapi");
|
|
5
|
+
function makeMock(responder) {
|
|
6
|
+
const calls = [];
|
|
7
|
+
const client = {
|
|
8
|
+
async request(method, path, body) {
|
|
9
|
+
calls.push({
|
|
10
|
+
method,
|
|
11
|
+
path,
|
|
12
|
+
body
|
|
13
|
+
});
|
|
14
|
+
return responder(method, path, body);
|
|
15
|
+
},
|
|
16
|
+
get(path) {
|
|
17
|
+
return this.request('GET', path);
|
|
18
|
+
},
|
|
19
|
+
post(path, body) {
|
|
20
|
+
return this.request('POST', path, body);
|
|
21
|
+
},
|
|
22
|
+
del(path) {
|
|
23
|
+
return this.request('DELETE', path);
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
return {
|
|
27
|
+
client,
|
|
28
|
+
calls
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
const CONN = {
|
|
32
|
+
name: 'TestPg',
|
|
33
|
+
dataSource: 'PostgreSQL',
|
|
34
|
+
server: 'localhost',
|
|
35
|
+
port: 5432,
|
|
36
|
+
db: 'datagrok'
|
|
37
|
+
};
|
|
38
|
+
(0, _vitest.describe)('NodeConnectionsDataSource.save', () => {
|
|
39
|
+
(0, _vitest.it)('POSTs to /public/v1/connections without saveCredentials by default', async () => {
|
|
40
|
+
const {
|
|
41
|
+
client,
|
|
42
|
+
calls
|
|
43
|
+
} = makeMock((method, path, body) => {
|
|
44
|
+
if (method === 'POST' && path === '/public/v1/connections') return {
|
|
45
|
+
...body,
|
|
46
|
+
id: 'new-id'
|
|
47
|
+
};
|
|
48
|
+
throw new Error(`unexpected ${method} ${path}`);
|
|
49
|
+
});
|
|
50
|
+
const ds = new _nodeDapi.NodeConnectionsDataSource(client);
|
|
51
|
+
const saved = await ds.save(CONN);
|
|
52
|
+
(0, _vitest.expect)(saved.id).toBe('new-id');
|
|
53
|
+
(0, _vitest.expect)(calls[0].path).toBe('/public/v1/connections');
|
|
54
|
+
(0, _vitest.expect)(calls[0].body).toEqual(CONN);
|
|
55
|
+
});
|
|
56
|
+
(0, _vitest.it)('appends saveCredentials=true when requested', async () => {
|
|
57
|
+
const {
|
|
58
|
+
client,
|
|
59
|
+
calls
|
|
60
|
+
} = makeMock((method, path, body) => {
|
|
61
|
+
if (method === 'POST' && path === '/public/v1/connections?saveCredentials=true') return {
|
|
62
|
+
...body,
|
|
63
|
+
id: 'new-id'
|
|
64
|
+
};
|
|
65
|
+
throw new Error(`unexpected ${method} ${path}`);
|
|
66
|
+
});
|
|
67
|
+
const ds = new _nodeDapi.NodeConnectionsDataSource(client);
|
|
68
|
+
await ds.save(CONN, true);
|
|
69
|
+
(0, _vitest.expect)(calls[0].path).toBe('/public/v1/connections?saveCredentials=true');
|
|
70
|
+
});
|
|
71
|
+
(0, _vitest.it)('omits saveCredentials query param when false', async () => {
|
|
72
|
+
const {
|
|
73
|
+
client,
|
|
74
|
+
calls
|
|
75
|
+
} = makeMock((method, path) => {
|
|
76
|
+
if (method === 'POST' && path === '/public/v1/connections') return {
|
|
77
|
+
id: 'new-id'
|
|
78
|
+
};
|
|
79
|
+
throw new Error(`unexpected ${method} ${path}`);
|
|
80
|
+
});
|
|
81
|
+
const ds = new _nodeDapi.NodeConnectionsDataSource(client);
|
|
82
|
+
await ds.save(CONN, false);
|
|
83
|
+
(0, _vitest.expect)(calls[0].path).toBe('/public/v1/connections');
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
(0, _vitest.describe)('NodeConnectionsDataSource.test', () => {
|
|
87
|
+
(0, _vitest.it)('POSTs the connection body to /public/v1/connections/test and resolves on "ok"', async () => {
|
|
88
|
+
const {
|
|
89
|
+
client,
|
|
90
|
+
calls
|
|
91
|
+
} = makeMock((method, path) => {
|
|
92
|
+
if (method === 'POST' && path === '/public/v1/connections/test') return 'ok';
|
|
93
|
+
throw new Error(`unexpected ${method} ${path}`);
|
|
94
|
+
});
|
|
95
|
+
const ds = new _nodeDapi.NodeConnectionsDataSource(client);
|
|
96
|
+
await ds.test(CONN);
|
|
97
|
+
(0, _vitest.expect)(calls[0].body).toEqual(CONN);
|
|
98
|
+
});
|
|
99
|
+
(0, _vitest.it)('strips surrounding quotes on a JSON-encoded "ok"', async () => {
|
|
100
|
+
const {
|
|
101
|
+
client
|
|
102
|
+
} = makeMock(() => '"ok"');
|
|
103
|
+
const ds = new _nodeDapi.NodeConnectionsDataSource(client);
|
|
104
|
+
await (0, _vitest.expect)(ds.test(CONN)).resolves.toBeUndefined();
|
|
105
|
+
});
|
|
106
|
+
(0, _vitest.it)('throws the server error text when the response is not ok', async () => {
|
|
107
|
+
const {
|
|
108
|
+
client
|
|
109
|
+
} = makeMock(() => 'connection refused: tcp://localhost:5432');
|
|
110
|
+
const ds = new _nodeDapi.NodeConnectionsDataSource(client);
|
|
111
|
+
await (0, _vitest.expect)(ds.test(CONN)).rejects.toThrow(/connection refused/);
|
|
112
|
+
});
|
|
113
|
+
(0, _vitest.it)('throws a generic message on empty response', async () => {
|
|
114
|
+
const {
|
|
115
|
+
client
|
|
116
|
+
} = makeMock(() => '');
|
|
117
|
+
const ds = new _nodeDapi.NodeConnectionsDataSource(client);
|
|
118
|
+
await (0, _vitest.expect)(ds.test(CONN)).rejects.toThrow(/failed/i);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
var _vitest = require("vitest");
|
|
4
|
+
var _nodeDapi = require("../utils/node-dapi");
|
|
5
|
+
function makeMock(responder) {
|
|
6
|
+
const calls = [];
|
|
7
|
+
const client = {
|
|
8
|
+
async request(method, path, body) {
|
|
9
|
+
calls.push({
|
|
10
|
+
method,
|
|
11
|
+
path,
|
|
12
|
+
body
|
|
13
|
+
});
|
|
14
|
+
return responder(method, path, body);
|
|
15
|
+
},
|
|
16
|
+
get(path) {
|
|
17
|
+
return this.request('GET', path);
|
|
18
|
+
},
|
|
19
|
+
post(path, body) {
|
|
20
|
+
return this.request('POST', path, body);
|
|
21
|
+
},
|
|
22
|
+
del(path) {
|
|
23
|
+
return this.request('DELETE', path);
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
return {
|
|
27
|
+
client,
|
|
28
|
+
calls
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
const PARENT_ID = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa';
|
|
32
|
+
const ALICE_ID = 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb';
|
|
33
|
+
const BOB_ID = 'cccccccc-cccc-cccc-cccc-cccccccccccc';
|
|
34
|
+
function makeParent(children = []) {
|
|
35
|
+
return {
|
|
36
|
+
id: PARENT_ID,
|
|
37
|
+
friendlyName: 'Admins',
|
|
38
|
+
children
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
(0, _vitest.describe)('NodeGroupsDataSource.resolve', () => {
|
|
42
|
+
(0, _vitest.it)('treats a UUID as a direct ID and calls find()', async () => {
|
|
43
|
+
const {
|
|
44
|
+
client,
|
|
45
|
+
calls
|
|
46
|
+
} = makeMock((_m, path) => {
|
|
47
|
+
if (path.startsWith(`/public/v1/groups/${PARENT_ID}`)) return makeParent();
|
|
48
|
+
throw new Error(`unexpected ${path}`);
|
|
49
|
+
});
|
|
50
|
+
const ds = new _nodeDapi.NodeGroupsDataSource(client);
|
|
51
|
+
const g = await ds.resolve(PARENT_ID);
|
|
52
|
+
(0, _vitest.expect)(g.id).toBe(PARENT_ID);
|
|
53
|
+
(0, _vitest.expect)(calls[0].path).toBe(`/public/v1/groups/${PARENT_ID}`);
|
|
54
|
+
});
|
|
55
|
+
(0, _vitest.it)('uses /lookup for non-UUID names and returns the unique match', async () => {
|
|
56
|
+
const {
|
|
57
|
+
client,
|
|
58
|
+
calls
|
|
59
|
+
} = makeMock((_m, path) => {
|
|
60
|
+
if (path.startsWith('/public/v1/groups/lookup')) return [{
|
|
61
|
+
id: ALICE_ID,
|
|
62
|
+
name: 'alice',
|
|
63
|
+
friendlyName: 'alice',
|
|
64
|
+
personal: true
|
|
65
|
+
}];
|
|
66
|
+
throw new Error(`unexpected ${path}`);
|
|
67
|
+
});
|
|
68
|
+
const ds = new _nodeDapi.NodeGroupsDataSource(client);
|
|
69
|
+
const g = await ds.resolve('alice');
|
|
70
|
+
(0, _vitest.expect)(g.id).toBe(ALICE_ID);
|
|
71
|
+
(0, _vitest.expect)(calls[0].path).toBe('/public/v1/groups/lookup?query=alice');
|
|
72
|
+
});
|
|
73
|
+
(0, _vitest.it)('errors with a list when lookup returns multiple matches', async () => {
|
|
74
|
+
const {
|
|
75
|
+
client
|
|
76
|
+
} = makeMock((_m, path) => {
|
|
77
|
+
if (path.startsWith('/public/v1/groups/lookup')) return [{
|
|
78
|
+
id: 'g1',
|
|
79
|
+
friendlyName: 'alice',
|
|
80
|
+
personal: false
|
|
81
|
+
}, {
|
|
82
|
+
id: 'g2',
|
|
83
|
+
friendlyName: 'alice',
|
|
84
|
+
personal: true
|
|
85
|
+
}];
|
|
86
|
+
throw new Error(`unexpected ${path}`);
|
|
87
|
+
});
|
|
88
|
+
const ds = new _nodeDapi.NodeGroupsDataSource(client);
|
|
89
|
+
await (0, _vitest.expect)(ds.resolve('alice')).rejects.toThrow(/Multiple groups match 'alice'/);
|
|
90
|
+
});
|
|
91
|
+
(0, _vitest.it)('personalOnly filters lookup results to personal groups', async () => {
|
|
92
|
+
const {
|
|
93
|
+
client
|
|
94
|
+
} = makeMock((_m, path) => {
|
|
95
|
+
if (path.startsWith('/public/v1/groups/lookup')) return [{
|
|
96
|
+
id: 'g1',
|
|
97
|
+
friendlyName: 'alice',
|
|
98
|
+
personal: false
|
|
99
|
+
}, {
|
|
100
|
+
id: ALICE_ID,
|
|
101
|
+
friendlyName: 'alice',
|
|
102
|
+
personal: true
|
|
103
|
+
}];
|
|
104
|
+
throw new Error(`unexpected ${path}`);
|
|
105
|
+
});
|
|
106
|
+
const ds = new _nodeDapi.NodeGroupsDataSource(client);
|
|
107
|
+
const g = await ds.resolve('alice', {
|
|
108
|
+
personalOnly: true
|
|
109
|
+
});
|
|
110
|
+
(0, _vitest.expect)(g.id).toBe(ALICE_ID);
|
|
111
|
+
});
|
|
112
|
+
(0, _vitest.it)('errors when personalOnly yields no matches', async () => {
|
|
113
|
+
const {
|
|
114
|
+
client
|
|
115
|
+
} = makeMock((_m, path) => {
|
|
116
|
+
if (path.startsWith('/public/v1/groups/lookup')) return [{
|
|
117
|
+
id: 'g1',
|
|
118
|
+
friendlyName: 'alice',
|
|
119
|
+
personal: false
|
|
120
|
+
}];
|
|
121
|
+
throw new Error(`unexpected ${path}`);
|
|
122
|
+
});
|
|
123
|
+
const ds = new _nodeDapi.NodeGroupsDataSource(client);
|
|
124
|
+
await (0, _vitest.expect)(ds.resolve('alice', {
|
|
125
|
+
personalOnly: true
|
|
126
|
+
})).rejects.toThrow(/No group matching 'alice' \(personal\)/);
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
(0, _vitest.describe)('NodeGroupsDataSource.save', () => {
|
|
130
|
+
(0, _vitest.it)('POSTs the group body to /public/v1/groups and auto-generates an id when missing', async () => {
|
|
131
|
+
const {
|
|
132
|
+
client,
|
|
133
|
+
calls
|
|
134
|
+
} = makeMock((method, path, body) => {
|
|
135
|
+
if (method === 'POST' && path === '/public/v1/groups') return {
|
|
136
|
+
...body,
|
|
137
|
+
friendlyName: 'Chemists'
|
|
138
|
+
};
|
|
139
|
+
throw new Error(`unexpected ${method} ${path}`);
|
|
140
|
+
});
|
|
141
|
+
const ds = new _nodeDapi.NodeGroupsDataSource(client);
|
|
142
|
+
await ds.save({
|
|
143
|
+
friendlyName: 'Chemists'
|
|
144
|
+
});
|
|
145
|
+
(0, _vitest.expect)(calls[0].path).toBe('/public/v1/groups');
|
|
146
|
+
(0, _vitest.expect)(calls[0].body.id).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i);
|
|
147
|
+
});
|
|
148
|
+
(0, _vitest.it)('appends saveRelations=true when requested', async () => {
|
|
149
|
+
const {
|
|
150
|
+
client,
|
|
151
|
+
calls
|
|
152
|
+
} = makeMock((method, path) => {
|
|
153
|
+
if (method === 'POST' && path === '/public/v1/groups?saveRelations=true') return {
|
|
154
|
+
id: 'g'
|
|
155
|
+
};
|
|
156
|
+
throw new Error(`unexpected ${method} ${path}`);
|
|
157
|
+
});
|
|
158
|
+
const ds = new _nodeDapi.NodeGroupsDataSource(client);
|
|
159
|
+
await ds.save({
|
|
160
|
+
id: 'g',
|
|
161
|
+
friendlyName: 'Chemists'
|
|
162
|
+
}, true);
|
|
163
|
+
(0, _vitest.expect)(calls[0].path).toBe('/public/v1/groups?saveRelations=true');
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
(0, _vitest.describe)('NodeGroupsDataSource.addMembers', () => {
|
|
167
|
+
(0, _vitest.it)('appends new relations and POSTs with saveRelations=true', async () => {
|
|
168
|
+
const {
|
|
169
|
+
client,
|
|
170
|
+
calls
|
|
171
|
+
} = makeMock((method, path, _body) => {
|
|
172
|
+
if (method === 'GET' && path === `/public/v1/groups/${PARENT_ID}`) return makeParent();
|
|
173
|
+
if (method === 'GET' && path.startsWith('/public/v1/groups/lookup')) return [{
|
|
174
|
+
id: ALICE_ID,
|
|
175
|
+
friendlyName: 'alice',
|
|
176
|
+
personal: true
|
|
177
|
+
}];
|
|
178
|
+
if (method === 'POST' && path === '/public/v1/groups?saveRelations=true') return {
|
|
179
|
+
id: PARENT_ID
|
|
180
|
+
};
|
|
181
|
+
throw new Error(`unexpected ${method} ${path}`);
|
|
182
|
+
});
|
|
183
|
+
const ds = new _nodeDapi.NodeGroupsDataSource(client);
|
|
184
|
+
const results = await ds.addMembers(PARENT_ID, ['alice']);
|
|
185
|
+
(0, _vitest.expect)(results).toEqual([{
|
|
186
|
+
member: 'alice',
|
|
187
|
+
status: 'added'
|
|
188
|
+
}]);
|
|
189
|
+
const post = calls.find(c => c.method === 'POST');
|
|
190
|
+
(0, _vitest.expect)(post.path).toBe('/public/v1/groups?saveRelations=true');
|
|
191
|
+
(0, _vitest.expect)(post.body.children).toEqual([{
|
|
192
|
+
parent: {
|
|
193
|
+
id: PARENT_ID
|
|
194
|
+
},
|
|
195
|
+
child: {
|
|
196
|
+
id: ALICE_ID
|
|
197
|
+
},
|
|
198
|
+
isAdmin: false
|
|
199
|
+
}]);
|
|
200
|
+
});
|
|
201
|
+
(0, _vitest.it)('supports --admin by setting isAdmin on the new relation', async () => {
|
|
202
|
+
const {
|
|
203
|
+
client,
|
|
204
|
+
calls
|
|
205
|
+
} = makeMock((method, path) => {
|
|
206
|
+
if (method === 'GET' && path === `/public/v1/groups/${PARENT_ID}`) return makeParent();
|
|
207
|
+
if (method === 'GET' && path.startsWith('/public/v1/groups/lookup')) return [{
|
|
208
|
+
id: ALICE_ID,
|
|
209
|
+
friendlyName: 'alice',
|
|
210
|
+
personal: true
|
|
211
|
+
}];
|
|
212
|
+
if (method === 'POST') return {
|
|
213
|
+
id: PARENT_ID
|
|
214
|
+
};
|
|
215
|
+
throw new Error(`unexpected ${method} ${path}`);
|
|
216
|
+
});
|
|
217
|
+
const ds = new _nodeDapi.NodeGroupsDataSource(client);
|
|
218
|
+
await ds.addMembers(PARENT_ID, ['alice'], true);
|
|
219
|
+
const post = calls.find(c => c.method === 'POST');
|
|
220
|
+
(0, _vitest.expect)(post.body.children[0].isAdmin).toBe(true);
|
|
221
|
+
});
|
|
222
|
+
(0, _vitest.it)('is idempotent: re-adding with the same role reports noop and skips POST', async () => {
|
|
223
|
+
const existing = {
|
|
224
|
+
parent: {
|
|
225
|
+
id: PARENT_ID
|
|
226
|
+
},
|
|
227
|
+
child: {
|
|
228
|
+
id: ALICE_ID
|
|
229
|
+
},
|
|
230
|
+
isAdmin: false
|
|
231
|
+
};
|
|
232
|
+
const {
|
|
233
|
+
client,
|
|
234
|
+
calls
|
|
235
|
+
} = makeMock((method, path) => {
|
|
236
|
+
if (method === 'GET' && path === `/public/v1/groups/${PARENT_ID}`) return makeParent([existing]);
|
|
237
|
+
if (method === 'GET' && path.startsWith('/public/v1/groups/lookup')) return [{
|
|
238
|
+
id: ALICE_ID,
|
|
239
|
+
friendlyName: 'alice',
|
|
240
|
+
personal: true
|
|
241
|
+
}];
|
|
242
|
+
if (method === 'POST') return {
|
|
243
|
+
id: PARENT_ID
|
|
244
|
+
};
|
|
245
|
+
throw new Error(`unexpected ${method} ${path}`);
|
|
246
|
+
});
|
|
247
|
+
const ds = new _nodeDapi.NodeGroupsDataSource(client);
|
|
248
|
+
const results = await ds.addMembers(PARENT_ID, ['alice']);
|
|
249
|
+
(0, _vitest.expect)(results).toEqual([{
|
|
250
|
+
member: 'alice',
|
|
251
|
+
status: 'noop'
|
|
252
|
+
}]);
|
|
253
|
+
(0, _vitest.expect)(calls.find(c => c.method === 'POST')).toBeUndefined();
|
|
254
|
+
});
|
|
255
|
+
(0, _vitest.it)('flips isAdmin when re-adding an existing member with a different role', async () => {
|
|
256
|
+
const existing = {
|
|
257
|
+
parent: {
|
|
258
|
+
id: PARENT_ID
|
|
259
|
+
},
|
|
260
|
+
child: {
|
|
261
|
+
id: ALICE_ID
|
|
262
|
+
},
|
|
263
|
+
isAdmin: false
|
|
264
|
+
};
|
|
265
|
+
const {
|
|
266
|
+
client,
|
|
267
|
+
calls
|
|
268
|
+
} = makeMock((method, path) => {
|
|
269
|
+
if (method === 'GET' && path === `/public/v1/groups/${PARENT_ID}`) return makeParent([existing]);
|
|
270
|
+
if (method === 'GET' && path.startsWith('/public/v1/groups/lookup')) return [{
|
|
271
|
+
id: ALICE_ID,
|
|
272
|
+
friendlyName: 'alice',
|
|
273
|
+
personal: true
|
|
274
|
+
}];
|
|
275
|
+
if (method === 'POST') return {
|
|
276
|
+
id: PARENT_ID
|
|
277
|
+
};
|
|
278
|
+
throw new Error(`unexpected ${method} ${path}`);
|
|
279
|
+
});
|
|
280
|
+
const ds = new _nodeDapi.NodeGroupsDataSource(client);
|
|
281
|
+
const results = await ds.addMembers(PARENT_ID, ['alice'], true);
|
|
282
|
+
(0, _vitest.expect)(results).toEqual([{
|
|
283
|
+
member: 'alice',
|
|
284
|
+
status: 'updated'
|
|
285
|
+
}]);
|
|
286
|
+
const post = calls.find(c => c.method === 'POST');
|
|
287
|
+
(0, _vitest.expect)(post.body.children).toHaveLength(1);
|
|
288
|
+
(0, _vitest.expect)(post.body.children[0].isAdmin).toBe(true);
|
|
289
|
+
});
|
|
290
|
+
(0, _vitest.it)('processes a mixed batch: one valid, one unresolvable', async () => {
|
|
291
|
+
const {
|
|
292
|
+
client,
|
|
293
|
+
calls
|
|
294
|
+
} = makeMock((method, path) => {
|
|
295
|
+
if (method === 'GET' && path === `/public/v1/groups/${PARENT_ID}`) return makeParent();
|
|
296
|
+
if (method === 'GET' && path === '/public/v1/groups/lookup?query=alice') return [{
|
|
297
|
+
id: ALICE_ID,
|
|
298
|
+
friendlyName: 'alice',
|
|
299
|
+
personal: true
|
|
300
|
+
}];
|
|
301
|
+
if (method === 'GET' && path === '/public/v1/groups/lookup?query=nobody') return [];
|
|
302
|
+
if (method === 'POST') return {
|
|
303
|
+
id: PARENT_ID
|
|
304
|
+
};
|
|
305
|
+
throw new Error(`unexpected ${method} ${path}`);
|
|
306
|
+
});
|
|
307
|
+
const ds = new _nodeDapi.NodeGroupsDataSource(client);
|
|
308
|
+
const results = await ds.addMembers(PARENT_ID, ['alice', 'nobody']);
|
|
309
|
+
(0, _vitest.expect)(results[0]).toEqual({
|
|
310
|
+
member: 'alice',
|
|
311
|
+
status: 'added'
|
|
312
|
+
});
|
|
313
|
+
(0, _vitest.expect)(results[1].member).toBe('nobody');
|
|
314
|
+
(0, _vitest.expect)(results[1].status).toBe('error');
|
|
315
|
+
(0, _vitest.expect)(results[1].error).toMatch(/No group matching 'nobody'/);
|
|
316
|
+
// POST should still happen for the valid member
|
|
317
|
+
const post = calls.find(c => c.method === 'POST');
|
|
318
|
+
(0, _vitest.expect)(post.body.children).toHaveLength(1);
|
|
319
|
+
(0, _vitest.expect)(post.body.children[0].child.id).toBe(ALICE_ID);
|
|
320
|
+
});
|
|
321
|
+
(0, _vitest.it)('handles multiple members in a single call', async () => {
|
|
322
|
+
const {
|
|
323
|
+
client,
|
|
324
|
+
calls
|
|
325
|
+
} = makeMock((method, path) => {
|
|
326
|
+
if (method === 'GET' && path === `/public/v1/groups/${PARENT_ID}`) return makeParent();
|
|
327
|
+
if (method === 'GET' && path === '/public/v1/groups/lookup?query=alice') return [{
|
|
328
|
+
id: ALICE_ID,
|
|
329
|
+
friendlyName: 'alice',
|
|
330
|
+
personal: true
|
|
331
|
+
}];
|
|
332
|
+
if (method === 'GET' && path === '/public/v1/groups/lookup?query=bob') return [{
|
|
333
|
+
id: BOB_ID,
|
|
334
|
+
friendlyName: 'bob',
|
|
335
|
+
personal: true
|
|
336
|
+
}];
|
|
337
|
+
if (method === 'POST') return {
|
|
338
|
+
id: PARENT_ID
|
|
339
|
+
};
|
|
340
|
+
throw new Error(`unexpected ${method} ${path}`);
|
|
341
|
+
});
|
|
342
|
+
const ds = new _nodeDapi.NodeGroupsDataSource(client);
|
|
343
|
+
const results = await ds.addMembers(PARENT_ID, ['alice', 'bob']);
|
|
344
|
+
(0, _vitest.expect)(results.map(r => r.status)).toEqual(['added', 'added']);
|
|
345
|
+
const post = calls.find(c => c.method === 'POST');
|
|
346
|
+
(0, _vitest.expect)(post.body.children.map(c => c.child.id)).toEqual([ALICE_ID, BOB_ID]);
|
|
347
|
+
// exactly one POST regardless of batch size
|
|
348
|
+
(0, _vitest.expect)(calls.filter(c => c.method === 'POST')).toHaveLength(1);
|
|
349
|
+
});
|
|
350
|
+
});
|
|
351
|
+
(0, _vitest.describe)('NodeGroupsDataSource.removeMembers', () => {
|
|
352
|
+
(0, _vitest.it)('filters the relation out and POSTs with saveRelations=true', async () => {
|
|
353
|
+
const existing = {
|
|
354
|
+
parent: {
|
|
355
|
+
id: PARENT_ID
|
|
356
|
+
},
|
|
357
|
+
child: {
|
|
358
|
+
id: ALICE_ID
|
|
359
|
+
},
|
|
360
|
+
isAdmin: false
|
|
361
|
+
};
|
|
362
|
+
const {
|
|
363
|
+
client,
|
|
364
|
+
calls
|
|
365
|
+
} = makeMock((method, path) => {
|
|
366
|
+
if (method === 'GET' && path === `/public/v1/groups/${PARENT_ID}`) return makeParent([existing]);
|
|
367
|
+
if (method === 'GET' && path.startsWith('/public/v1/groups/lookup')) return [{
|
|
368
|
+
id: ALICE_ID,
|
|
369
|
+
friendlyName: 'alice',
|
|
370
|
+
personal: true
|
|
371
|
+
}];
|
|
372
|
+
if (method === 'POST') return {
|
|
373
|
+
id: PARENT_ID
|
|
374
|
+
};
|
|
375
|
+
throw new Error(`unexpected ${method} ${path}`);
|
|
376
|
+
});
|
|
377
|
+
const ds = new _nodeDapi.NodeGroupsDataSource(client);
|
|
378
|
+
const results = await ds.removeMembers(PARENT_ID, ['alice']);
|
|
379
|
+
(0, _vitest.expect)(results).toEqual([{
|
|
380
|
+
member: 'alice',
|
|
381
|
+
status: 'removed'
|
|
382
|
+
}]);
|
|
383
|
+
const post = calls.find(c => c.method === 'POST');
|
|
384
|
+
(0, _vitest.expect)(post.path).toBe('/public/v1/groups?saveRelations=true');
|
|
385
|
+
(0, _vitest.expect)(post.body.children).toEqual([]);
|
|
386
|
+
});
|
|
387
|
+
(0, _vitest.it)('reports not-member and skips POST when the member isn\'t in the group', async () => {
|
|
388
|
+
const {
|
|
389
|
+
client,
|
|
390
|
+
calls
|
|
391
|
+
} = makeMock((method, path) => {
|
|
392
|
+
if (method === 'GET' && path === `/public/v1/groups/${PARENT_ID}`) return makeParent();
|
|
393
|
+
if (method === 'GET' && path.startsWith('/public/v1/groups/lookup')) return [{
|
|
394
|
+
id: ALICE_ID,
|
|
395
|
+
friendlyName: 'alice',
|
|
396
|
+
personal: true
|
|
397
|
+
}];
|
|
398
|
+
if (method === 'POST') return {
|
|
399
|
+
id: PARENT_ID
|
|
400
|
+
};
|
|
401
|
+
throw new Error(`unexpected ${method} ${path}`);
|
|
402
|
+
});
|
|
403
|
+
const ds = new _nodeDapi.NodeGroupsDataSource(client);
|
|
404
|
+
const results = await ds.removeMembers(PARENT_ID, ['alice']);
|
|
405
|
+
(0, _vitest.expect)(results).toEqual([{
|
|
406
|
+
member: 'alice',
|
|
407
|
+
status: 'not-member'
|
|
408
|
+
}]);
|
|
409
|
+
(0, _vitest.expect)(calls.find(c => c.method === 'POST')).toBeUndefined();
|
|
410
|
+
});
|
|
411
|
+
});
|
|
412
|
+
(0, _vitest.describe)('NodeGroupsDataSource.getMembers', () => {
|
|
413
|
+
(0, _vitest.it)('calls /members without an admin query when admin is undefined', async () => {
|
|
414
|
+
const {
|
|
415
|
+
client,
|
|
416
|
+
calls
|
|
417
|
+
} = makeMock((method, path) => {
|
|
418
|
+
if (method === 'GET' && path === `/public/v1/groups/${PARENT_ID}`) return makeParent();
|
|
419
|
+
if (method === 'GET' && path === `/public/v1/groups/${PARENT_ID}/members`) return [];
|
|
420
|
+
throw new Error(`unexpected ${method} ${path}`);
|
|
421
|
+
});
|
|
422
|
+
const ds = new _nodeDapi.NodeGroupsDataSource(client);
|
|
423
|
+
await ds.getMembers(PARENT_ID);
|
|
424
|
+
(0, _vitest.expect)(calls.map(c => c.path)).toContain(`/public/v1/groups/${PARENT_ID}/members`);
|
|
425
|
+
});
|
|
426
|
+
(0, _vitest.it)('passes admin=true', async () => {
|
|
427
|
+
const {
|
|
428
|
+
client,
|
|
429
|
+
calls
|
|
430
|
+
} = makeMock((method, path) => {
|
|
431
|
+
if (method === 'GET' && path === `/public/v1/groups/${PARENT_ID}`) return makeParent();
|
|
432
|
+
if (method === 'GET' && path === `/public/v1/groups/${PARENT_ID}/members?admin=true`) return [];
|
|
433
|
+
throw new Error(`unexpected ${method} ${path}`);
|
|
434
|
+
});
|
|
435
|
+
const ds = new _nodeDapi.NodeGroupsDataSource(client);
|
|
436
|
+
await ds.getMembers(PARENT_ID, true);
|
|
437
|
+
(0, _vitest.expect)(calls.map(c => c.path)).toContain(`/public/v1/groups/${PARENT_ID}/members?admin=true`);
|
|
438
|
+
});
|
|
439
|
+
(0, _vitest.it)('passes admin=false', async () => {
|
|
440
|
+
const {
|
|
441
|
+
client,
|
|
442
|
+
calls
|
|
443
|
+
} = makeMock((method, path) => {
|
|
444
|
+
if (method === 'GET' && path === `/public/v1/groups/${PARENT_ID}`) return makeParent();
|
|
445
|
+
if (method === 'GET' && path === `/public/v1/groups/${PARENT_ID}/members?admin=false`) return [];
|
|
446
|
+
throw new Error(`unexpected ${method} ${path}`);
|
|
447
|
+
});
|
|
448
|
+
const ds = new _nodeDapi.NodeGroupsDataSource(client);
|
|
449
|
+
await ds.getMembers(PARENT_ID, false);
|
|
450
|
+
(0, _vitest.expect)(calls.map(c => c.path)).toContain(`/public/v1/groups/${PARENT_ID}/members?admin=false`);
|
|
451
|
+
});
|
|
452
|
+
});
|
|
453
|
+
(0, _vitest.describe)('NodeGroupsDataSource.getMemberships', () => {
|
|
454
|
+
(0, _vitest.it)('calls /memberships with admin flag when provided', async () => {
|
|
455
|
+
const {
|
|
456
|
+
client,
|
|
457
|
+
calls
|
|
458
|
+
} = makeMock((method, path) => {
|
|
459
|
+
if (method === 'GET' && path === `/public/v1/groups/${PARENT_ID}`) return makeParent();
|
|
460
|
+
if (method === 'GET' && path === `/public/v1/groups/${PARENT_ID}/memberships?admin=true`) return [];
|
|
461
|
+
throw new Error(`unexpected ${method} ${path}`);
|
|
462
|
+
});
|
|
463
|
+
const ds = new _nodeDapi.NodeGroupsDataSource(client);
|
|
464
|
+
await ds.getMemberships(PARENT_ID, true);
|
|
465
|
+
(0, _vitest.expect)(calls.map(c => c.path)).toContain(`/public/v1/groups/${PARENT_ID}/memberships?admin=true`);
|
|
466
|
+
});
|
|
467
|
+
});
|
package/bin/commands/publish.js
CHANGED
|
@@ -343,6 +343,11 @@ async function processDockerImages(packageName, version, registry, devKey, host,
|
|
|
343
343
|
color.log(` Tagged as ${canonicalTag}`);
|
|
344
344
|
}
|
|
345
345
|
result = pushImage(img.imageName, registryTag, registry);
|
|
346
|
+
if (!result) {
|
|
347
|
+
const fallback = await fallbackImage(img, host, devKey, registry, version, contentHash);
|
|
348
|
+
if (fallback.serverError) color.error(`Cannot resolve fallback: ${fallback.serverError}`);else if (fallback.image) color.warn(`Falling back to ${fallback.image}` + `${fallback.hashMatch === false ? ' (hash mismatch — container will run older code)' : ''}`);else color.error(`No published image available for ${img.imageName}. No container will be available.`);
|
|
349
|
+
result = fallback;
|
|
350
|
+
}
|
|
346
351
|
} else {
|
|
347
352
|
color.warn(`Local image not found. Expected: ${img.fullLocalName}`);
|
|
348
353
|
color.log(` Build it with: docker build -t ${img.fullLocalName} -f ${dockerfilePath} ${dockerfileDir}`);
|
|
@@ -364,7 +369,7 @@ async function processDockerImages(packageName, version, registry, devKey, host,
|
|
|
364
369
|
fallback: true,
|
|
365
370
|
requestedVersion: registryTag
|
|
366
371
|
};
|
|
367
|
-
if (!result || result.fallback) color.warn(`
|
|
372
|
+
if (!result || result.fallback) color.warn(`Could not publish a new image. Falling back to ${fallback.image} (hash mismatch)`);
|
|
368
373
|
} else {
|
|
369
374
|
// The server has no compatible record, but the image may already be
|
|
370
375
|
// published in the configured registry / Docker Hub (e.g. pushed by an
|
|
@@ -394,7 +399,7 @@ async function processDockerImages(packageName, version, registry, devKey, host,
|
|
|
394
399
|
fallback: true,
|
|
395
400
|
requestedVersion: registryTag
|
|
396
401
|
};
|
|
397
|
-
color.error(`Failed to
|
|
402
|
+
color.error(`Failed to publish ${img.fullLocalName}. No container will be available.`);
|
|
398
403
|
}
|
|
399
404
|
}
|
|
400
405
|
}
|
|
@@ -408,6 +413,12 @@ async function processDockerImages(packageName, version, registry, devKey, host,
|
|
|
408
413
|
color.log(`Added ${imageJsonPath}`);
|
|
409
414
|
}
|
|
410
415
|
}
|
|
416
|
+
|
|
417
|
+
// Returns null when the push failed. Never claim a tag that was not published: the server
|
|
418
|
+
// records it as the container's image and the spawner then fails validation forever
|
|
419
|
+
// ("not found in any registry") with no way back except another publish.
|
|
420
|
+
// With no registry configured the local tag is all there is — dev stacks share the host
|
|
421
|
+
// docker daemon and the spawner resolves local images, so that stays a warning.
|
|
411
422
|
function pushImage(imageName, tag, registry) {
|
|
412
423
|
const canonicalImage = `datagrok/${imageName}:${tag}`;
|
|
413
424
|
if (registry) {
|
|
@@ -417,11 +428,8 @@ function pushImage(imageName, tag, registry) {
|
|
|
417
428
|
image: canonicalImage,
|
|
418
429
|
fallback: false
|
|
419
430
|
};
|
|
420
|
-
color.
|
|
421
|
-
return
|
|
422
|
-
image: canonicalImage,
|
|
423
|
-
fallback: false
|
|
424
|
-
};
|
|
431
|
+
color.error(`${remoteTag} was not published — check \`docker login ${registry}\` and your developer key.`);
|
|
432
|
+
return null;
|
|
425
433
|
}
|
|
426
434
|
color.warn(`No registry configured. Image tagged locally only. Run \`grok config --registry\` to configure.`);
|
|
427
435
|
return {
|
|
@@ -50,6 +50,9 @@ async function run(config, args) {
|
|
|
50
50
|
processArgs.push('src/package-test-node.ts');
|
|
51
51
|
processArgs.push(`--apiUrl=${config.url}`);
|
|
52
52
|
processArgs.push(`--devKey=${config.key}`);
|
|
53
|
+
// Explicit even though it's the runner default: the stress baseline must only run
|
|
54
|
+
// stressTest-marked tests regardless of how the runner's default evolves.
|
|
55
|
+
processArgs.push('--mode=stress');
|
|
53
56
|
if (args["concurrent-runs"]) processArgs.push(`--concurrentRuns=${args["concurrent-runs"]}`);
|
|
54
57
|
if (args['loop']) processArgs.push(`--loop`);
|
|
55
58
|
if (args['concurrency-range']) processArgs.push(`--concurrencyRange=${args["concurrency-range"]}`);
|
package/bin/commands/test.js
CHANGED
|
@@ -289,22 +289,18 @@ async function test(args) {
|
|
|
289
289
|
}
|
|
290
290
|
}
|
|
291
291
|
if (nodeRes != null) {
|
|
292
|
-
// Merge the Node pass into the browser result
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
res = await (0, _testUtils.mergeBrowsersResults)([res, nodeRes]);
|
|
305
|
-
} else {
|
|
306
|
-
res.failed = res.failed || nodeRes.failed;
|
|
307
|
-
}
|
|
292
|
+
// Merge the Node pass into the browser result. CSVs merge by column NAME —
|
|
293
|
+
// the two passes emit different column orders, and a line-wise concat would
|
|
294
|
+
// misalign rows (string values in integer columns break the report upload).
|
|
295
|
+
res.csv = testUtils.mergeCsvByHeader(res.csv, nodeRes.csv);
|
|
296
|
+
res.passedAmount += nodeRes.passedAmount;
|
|
297
|
+
res.failedAmount += nodeRes.failedAmount;
|
|
298
|
+
res.skippedAmount += nodeRes.skippedAmount;
|
|
299
|
+
res.failed = res.failed || nodeRes.failed;
|
|
300
|
+
res.verbosePassed = (res.verbosePassed || '') + nodeRes.verbosePassed;
|
|
301
|
+
res.verboseFailed = (res.verboseFailed || '') + nodeRes.verboseFailed;
|
|
302
|
+
res.verboseSkipped = (res.verboseSkipped || '') + nodeRes.verboseSkipped;
|
|
303
|
+
res.modernOutput = res.modernOutput || nodeRes.modernOutput;
|
|
308
304
|
}
|
|
309
305
|
if (!args['skip-playwright'] && !args['node-only']) {
|
|
310
306
|
const ptDir = playwrightRunner.hasPlaywrightTests(curDir);
|
package/bin/utils/test-utils.js
CHANGED
|
@@ -20,6 +20,7 @@ exports.loadPackage = loadPackage;
|
|
|
20
20
|
exports.loadPackages = loadPackages;
|
|
21
21
|
exports.loadTestsList = loadTestsList;
|
|
22
22
|
exports.mergeBrowsersResults = mergeBrowsersResults;
|
|
23
|
+
exports.mergeCsvByHeader = mergeCsvByHeader;
|
|
23
24
|
exports.printBrowsersResult = printBrowsersResult;
|
|
24
25
|
exports.runBrowser = runBrowser;
|
|
25
26
|
exports.runWithTimeout = runWithTimeout;
|
|
@@ -946,6 +947,29 @@ class TestContext {
|
|
|
946
947
|
}
|
|
947
948
|
}
|
|
948
949
|
exports.TestContext = TestContext;
|
|
950
|
+
/**
|
|
951
|
+
* Merges two test-result CSVs by column NAME (union of headers), unlike
|
|
952
|
+
* mergeBrowsersResults' line concatenation which assumes identical column order.
|
|
953
|
+
* Used to merge the Node-pass report into the browser report.
|
|
954
|
+
*/
|
|
955
|
+
function mergeCsvByHeader(a, b) {
|
|
956
|
+
const hasRows = csv => csv && csv.trim().split('\n').length >= 2;
|
|
957
|
+
if (!hasRows(a)) return hasRows(b) ? b : a || b || '';
|
|
958
|
+
if (!hasRows(b)) return a;
|
|
959
|
+
const pa = _papaparse.default.parse(a.trim(), {
|
|
960
|
+
header: true,
|
|
961
|
+
skipEmptyLines: true
|
|
962
|
+
});
|
|
963
|
+
const pb = _papaparse.default.parse(b.trim(), {
|
|
964
|
+
header: true,
|
|
965
|
+
skipEmptyLines: true
|
|
966
|
+
});
|
|
967
|
+
const columns = [...(pa.meta.fields ?? [])];
|
|
968
|
+
for (const f of pb.meta.fields ?? []) if (!columns.includes(f)) columns.push(f);
|
|
969
|
+
return _papaparse.default.unparse([...pa.data, ...pb.data], {
|
|
970
|
+
columns
|
|
971
|
+
});
|
|
972
|
+
}
|
|
949
973
|
function addColumnToCsv(csv, columnName, defaultValue) {
|
|
950
974
|
const results = _papaparse.default.parse(csv, {
|
|
951
975
|
header: true,
|
|
@@ -8,12 +8,20 @@ export { tests };
|
|
|
8
8
|
//input: string category {optional: true}
|
|
9
9
|
//input: string test {optional: true}
|
|
10
10
|
//input: object testContext {optional: true}
|
|
11
|
+
//input: bool excludeNodeTests {optional: true}
|
|
11
12
|
//output: dataframe result
|
|
12
|
-
export async function test(category: string, test: string, testContext: TestContext): Promise<DG.DataFrame> {
|
|
13
|
-
const data = await runTests({ category, test, testContext });
|
|
13
|
+
export async function test(category: string, test: string, testContext: TestContext, excludeNodeTests?: boolean): Promise<DG.DataFrame> {
|
|
14
|
+
const data = await runTests({ category, test, testContext, excludeNodeTests });
|
|
14
15
|
return DG.DataFrame.fromObjects(data)!;
|
|
15
16
|
}
|
|
16
17
|
|
|
18
|
+
/** Headless entry for the `grok test` Node pass — runs only tests marked {node: true}. */
|
|
19
|
+
export async function testNode(pkg: DG.Package,
|
|
20
|
+
options: {category?: string, test?: string, stressTest?: boolean, verbose?: boolean}): Promise<any[]> {
|
|
21
|
+
return await runTests({category: options.category, test: options.test, stressTest: options.stressTest,
|
|
22
|
+
verbose: options.verbose, nodeOnly: true, nodeOptions: {package: pkg}});
|
|
23
|
+
}
|
|
24
|
+
|
|
17
25
|
//name: initAutoTests
|
|
18
26
|
export async function initAutoTests() {
|
|
19
27
|
await initTests(_package, _package.getModule('package-test.js'));
|
package/package.json
CHANGED