@hydranium/conformance 1.0.0-next.251 → 1.0.0-next.253

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/README.md CHANGED
@@ -29,6 +29,7 @@ npm install --save-dev @hydranium/conformance
29
29
  | `@hydranium/protocol` | `^1.0.0-next` |
30
30
  | `@jest/globals` | `^29.0.0` |
31
31
  | `vitest` | `^4.0.0` |
32
+ | `vscode-jsonrpc` | `^9.0.0` |
32
33
 
33
34
  `vitest` and `@jest/globals` are optional peers: install the runner you use.
34
35
 
@@ -58,19 +59,29 @@ The GLSP slice is generic over your action type and takes a `GlspFixture` per
58
59
  diagram type. The fixture builds your native actions, and the kit matches the
59
60
  responses by `kind`.
60
61
 
62
+ To check a deployed GLSP server rather than one built in your test process,
63
+ return `connectGlspSocketDriver({ port, diagramType, clientActionKinds })` from
64
+ `connect`. The driver sees only GLSP actions, so a create check's
65
+ `expectMutated` observes the write on a channel you hold, as a host would: the
66
+ `workspace/applyEdit` the server pushes on your LSP connection, for a document
67
+ you opened there first. It may return a promise for that push. A server that
68
+ lays out on the client finishes a load only once its `requestBounds` is
69
+ answered, so pass `respond` to send back the `computedBounds` a client would.
70
+
61
71
  For where the kit fits among your other tests, see *Test your language* in
62
72
  [Adopting Hydranium](https://github.com/eclipse-emfcloud/hydranium/blob/main/docs/ADOPTING.md).
63
73
 
64
74
  ## Entry points
65
75
 
66
- | Subpath | Use it for | Runs in |
67
- | ---------- | ------------------------------------------------------------ | ------- |
68
- | `.` | `LanguageFixture` and the runner-agnostic check primitives. | Node |
69
- | `./data` | The data-head checks and driver port. | Node |
70
- | `./lsp` | The LSP-head checks and driver port. | Node |
71
- | `./glsp` | The GLSP-head checks and driver port. | Node |
72
- | `./vitest` | Run the checks under vitest. | Node |
73
- | `./jest` | Run the checks under Jest. | Node |
76
+ | Subpath | Use it for | Runs in |
77
+ | ------------- | ------------------------------------------------------------ | ------- |
78
+ | `.` | `LanguageFixture` and the runner-agnostic check primitives. | Node |
79
+ | `./data` | The data-head checks and driver port. | Node |
80
+ | `./lsp` | The LSP-head checks and driver port. | Node |
81
+ | `./glsp` | The GLSP-head checks and driver port. | Node |
82
+ | `./glsp/node` | A GLSP driver over a socket, for a deployed server. | Node |
83
+ | `./vitest` | Run the checks under vitest. | Node |
84
+ | `./jest` | Run the checks under Jest. | Node |
74
85
 
75
86
  The subpaths need a TypeScript `moduleResolution` that reads `exports`
76
87
  (`NodeNext` or `Bundler`); see *Requirements* in
@@ -59,8 +59,12 @@ export interface GlspCreateOperationSpec<TAction, TDriver extends GlspConformanc
59
59
  * fixture to its input document, so a fixture whose input was MUTATED by an
60
60
  * earlier check silently expects the wrong number. Give each check pristine
61
61
  * input, or take the delta route above.
62
+ *
63
+ * May answer asynchronously. It runs once the response kind has arrived, and
64
+ * a write observed from another process, a `workspace/applyEdit` say, can
65
+ * arrive after that.
62
66
  */
63
- readonly expectMutated: (driver: TDriver) => boolean;
67
+ readonly expectMutated: (driver: TDriver) => boolean | Promise<boolean>;
64
68
  }
65
69
  /**
66
70
  * Per-diagram-type GLSP fixture, generic over the adopter action type and the
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/glsp/index.ts"],"names":[],"mappings":"AAAA;;;;;;;kFAOkF;AAoBlF,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,6BAA6B,CAAC;AAC3D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAEhE;;;;;;GAMG;AACH,MAAM,WAAW,qBAAqB,CAAC,OAAO,CAAE,SAAQ,OAAO;IAC5D,uFAAuF;IACvF,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,gFAAgF;IAChF,QAAQ,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI,CAAC;IAChC,qFAAqF;IACrF,UAAU,CAAC,CAAC,SAAS,OAAO,GAAG,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;CACxF;AAED;;;;GAIG;AACH,MAAM,WAAW,uBAAuB,CAAC,OAAO,EAAE,OAAO,SAAS,qBAAqB,CAAC,OAAO,CAAC;IAC7F;;;;;;;;;;;;OAYG;IACH,QAAQ,CAAC,MAAM,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,OAAO,CAAC;IAC9C,sHAAsH;IACtH,QAAQ,CAAC,oBAAoB,EAAE,MAAM,CAAC;IACtC;;;;;;;;;;;;;;OAcG;IACH,QAAQ,CAAC,aAAa,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,OAAO,CAAC;CACvD;AAED;;;;;GAKG;AACH,MAAM,WAAW,WAAW,CAAC,OAAO,EAAE,OAAO,SAAS,qBAAqB,CAAC,OAAO,CAAC;IACjF;;;;;;;;OAQG;IACH,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,gGAAgG;IAChG,QAAQ,CAAC,OAAO,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5D;;;;;;;;OAQG;IACH,QAAQ,CAAC,YAAY,EAAE,MAAM,OAAO,CAAC;IACrC,oGAAoG;IACpG,QAAQ,CAAC,oBAAoB,EAAE,MAAM,CAAC;IACtC,6HAA6H;IAC7H,QAAQ,CAAC,cAAc,CAAC,EAAE,CAAC,QAAQ,EAAE,OAAO,KAAK,OAAO,CAAC;IACzD,oEAAoE;IACpE,QAAQ,CAAC,eAAe,CAAC,EAAE,uBAAuB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;CACvE;AAED,wCAAwC;AACxC,MAAM,WAAW,sBAAsB,CAAC,OAAO,EAAE,OAAO,SAAS,qBAAqB,CAAC,OAAO,CAAC;IAC5F;;;;;OAKG;IACH,QAAQ,CAAC,OAAO,EAAE,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACnD,iCAAiC;IACjC,QAAQ,CAAC,QAAQ,EAAE,aAAa,CAAC,WAAW,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC;IAChE,2DAA2D;IAC3D,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;CAC/B;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,OAAO,SAAS,qBAAqB,CAAC,OAAO,CAAC,EACpF,OAAO,EAAE,sBAAsB,CAAC,OAAO,EAAE,OAAO,CAAC,GACjD,gBAAgB,EAAE,CAoEpB"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/glsp/index.ts"],"names":[],"mappings":"AAAA;;;;;;;kFAOkF;AAoBlF,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,6BAA6B,CAAC;AAC3D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAEhE;;;;;;GAMG;AACH,MAAM,WAAW,qBAAqB,CAAC,OAAO,CAAE,SAAQ,OAAO;IAC5D,uFAAuF;IACvF,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,gFAAgF;IAChF,QAAQ,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI,CAAC;IAChC,qFAAqF;IACrF,UAAU,CAAC,CAAC,SAAS,OAAO,GAAG,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;CACxF;AAED;;;;GAIG;AACH,MAAM,WAAW,uBAAuB,CAAC,OAAO,EAAE,OAAO,SAAS,qBAAqB,CAAC,OAAO,CAAC;IAC7F;;;;;;;;;;;;OAYG;IACH,QAAQ,CAAC,MAAM,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,OAAO,CAAC;IAC9C,sHAAsH;IACtH,QAAQ,CAAC,oBAAoB,EAAE,MAAM,CAAC;IACtC;;;;;;;;;;;;;;;;;;OAkBG;IACH,QAAQ,CAAC,aAAa,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CAC1E;AAED;;;;;GAKG;AACH,MAAM,WAAW,WAAW,CAAC,OAAO,EAAE,OAAO,SAAS,qBAAqB,CAAC,OAAO,CAAC;IACjF;;;;;;;;OAQG;IACH,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,gGAAgG;IAChG,QAAQ,CAAC,OAAO,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5D;;;;;;;;OAQG;IACH,QAAQ,CAAC,YAAY,EAAE,MAAM,OAAO,CAAC;IACrC,oGAAoG;IACpG,QAAQ,CAAC,oBAAoB,EAAE,MAAM,CAAC;IACtC,6HAA6H;IAC7H,QAAQ,CAAC,cAAc,CAAC,EAAE,CAAC,QAAQ,EAAE,OAAO,KAAK,OAAO,CAAC;IACzD,oEAAoE;IACpE,QAAQ,CAAC,eAAe,CAAC,EAAE,uBAAuB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;CACvE;AAED,wCAAwC;AACxC,MAAM,WAAW,sBAAsB,CAAC,OAAO,EAAE,OAAO,SAAS,qBAAqB,CAAC,OAAO,CAAC;IAC5F;;;;;OAKG;IACH,QAAQ,CAAC,OAAO,EAAE,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACnD,iCAAiC;IACjC,QAAQ,CAAC,QAAQ,EAAE,aAAa,CAAC,WAAW,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC;IAChE,2DAA2D;IAC3D,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;CAC/B;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,OAAO,SAAS,qBAAqB,CAAC,OAAO,CAAC,EACpF,OAAO,EAAE,sBAAsB,CAAC,OAAO,EAAE,OAAO,CAAC,GACjD,gBAAgB,EAAE,CAoEpB"}
package/lib/glsp/index.js CHANGED
@@ -79,7 +79,7 @@ export function buildGlspChecks(options) {
79
79
  await driver.nextAction(diagram.expectedResponseKind);
80
80
  driver.dispatch(operation.action(driver));
81
81
  await driver.nextAction(operation.expectedResponseKind);
82
- assert.ok(operation.expectMutated(driver), 'expectMutated was false — the create operation did not mutate the source model');
82
+ assert.ok(await operation.expectMutated(driver), 'expectMutated was false — the create operation did not mutate the source model');
83
83
  }
84
84
  finally {
85
85
  driver.dispose();
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/glsp/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/glsp/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,62 @@
1
+ /********************************************************************************
2
+ * Copyright (c) 2026 EclipseSource and others.
3
+ *
4
+ * This program and the accompanying materials are made available under the
5
+ * terms of the MIT License which is available in the project root.
6
+ *
7
+ * SPDX-License-Identifier: MIT
8
+ ********************************************************************************/
9
+ import type { GlspConformanceDriver } from '../index.js';
10
+ /** Configuration for {@link connectGlspSocketDriver}. */
11
+ export interface GlspSocketDriverOptions<TAction extends {
12
+ readonly kind: string;
13
+ } = {
14
+ readonly kind: string;
15
+ }> {
16
+ readonly port: number;
17
+ /** Defaults to `127.0.0.1`. */
18
+ readonly host?: string;
19
+ /** The diagram type the client session is opened for. */
20
+ readonly diagramType: string;
21
+ /**
22
+ * The action kinds this client handles, which the server routes back to it.
23
+ * Declare every kind the server sends during the checks: one it sends that
24
+ * neither side handles fails the server's dispatch, and the session gets a
25
+ * rejected request or an error message instead.
26
+ */
27
+ readonly clientActionKinds: readonly string[];
28
+ /** Defaults to a fresh id per driver, so drivers sharing one server never share a session. */
29
+ readonly clientSessionId?: string;
30
+ readonly applicationId?: string;
31
+ /**
32
+ * The GLSP protocol version announced in `initialize`. The server refuses
33
+ * `initialize` unless it equals its own, so a server on a newer protocol
34
+ * needs this. Defaults to `1.0.0`.
35
+ */
36
+ readonly protocolVersion?: string;
37
+ /**
38
+ * The client's reply to a received action, sent back to the server, or
39
+ * `undefined` for none. A server that lays out on the client finishes a
40
+ * `requestModel` only once its `requestBounds` is answered with
41
+ * `computedBounds`, so a load without this stops half way. A throw fails
42
+ * every pending and later wait with it.
43
+ */
44
+ readonly respond?: (action: TAction) => TAction | undefined;
45
+ /** Default bound for {@link GlspConformanceDriver.nextAction}, and the bound on the TCP connect and on each `start()` request. */
46
+ readonly timeoutMs?: number;
47
+ }
48
+ /** A {@link GlspConformanceDriver} over a socket, with the actions it received. */
49
+ export interface GlspSocketDriver<TAction extends {
50
+ readonly kind: string;
51
+ }> extends GlspConformanceDriver<TAction> {
52
+ /** Every action the server sent to this session, in arrival order; append-only. */
53
+ readonly actions: ReadonlyArray<TAction>;
54
+ }
55
+ /**
56
+ * Open a TCP connection to a GLSP server and wrap it as a driver. The session is
57
+ * not initialised until the kit calls `start()`.
58
+ */
59
+ export declare function connectGlspSocketDriver<TAction extends {
60
+ readonly kind: string;
61
+ }>(options: GlspSocketDriverOptions<TAction>): Promise<GlspSocketDriver<TAction>>;
62
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/glsp/node/index.ts"],"names":[],"mappings":"AAAA;;;;;;;kFAOkF;AAclF,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AAMzD,yDAAyD;AACzD,MAAM,WAAW,uBAAuB,CAAC,OAAO,SAAS;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE;IAC3G,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,+BAA+B;IAC/B,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,yDAAyD;IACzD,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B;;;;;OAKG;IACH,QAAQ,CAAC,iBAAiB,EAAE,SAAS,MAAM,EAAE,CAAC;IAC9C,8FAA8F;IAC9F,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC;;;;OAIG;IACH,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC;IAClC;;;;;;OAMG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,OAAO,GAAG,SAAS,CAAC;IAC5D,kIAAkI;IAClI,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED,mFAAmF;AACnF,MAAM,WAAW,gBAAgB,CAAC,OAAO,SAAS;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAE,SAAQ,qBAAqB,CAAC,OAAO,CAAC;IAChH,mFAAmF;IACnF,QAAQ,CAAC,OAAO,EAAE,aAAa,CAAC,OAAO,CAAC,CAAC;CAC3C;AAgBD;;;GAGG;AACH,wBAAsB,uBAAuB,CAAC,OAAO,SAAS;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,EACpF,OAAO,EAAE,uBAAuB,CAAC,OAAO,CAAC,GACzC,OAAO,CAAC,gBAAgB,CAAC,OAAO,CAAC,CAAC,CAuIpC"}
@@ -0,0 +1,183 @@
1
+ /********************************************************************************
2
+ * Copyright (c) 2026 EclipseSource and others.
3
+ *
4
+ * This program and the accompanying materials are made available under the
5
+ * terms of the MIT License which is available in the project root.
6
+ *
7
+ * SPDX-License-Identifier: MIT
8
+ ********************************************************************************/
9
+ /**
10
+ * The `@hydranium/conformance/glsp/node` subpath: a socket driver for the
11
+ * {@link GlspConformanceDriver} port, over a real TCP socket, for proving a
12
+ * deployed GLSP server rather than a container built in the test process.
13
+ *
14
+ * It speaks GLSP's JSON-RPC by method name, so like the rest of the kit it names
15
+ * no `@eclipse-glsp/*` type and the adopter's fixtures supply every action.
16
+ */
17
+ import { randomUUID } from 'node:crypto';
18
+ import * as net from 'node:net';
19
+ import { createMessageConnection, SocketMessageReader, SocketMessageWriter } from 'vscode-jsonrpc/node';
20
+ const DEFAULT_PROTOCOL_VERSION = '1.0.0';
21
+ const DEFAULT_TIMEOUT_MS = 15_000;
22
+ function isActionMessage(value) {
23
+ if (typeof value !== 'object' || value === null || !('clientId' in value) || !('action' in value)) {
24
+ return false;
25
+ }
26
+ const { action } = value;
27
+ return typeof action === 'object' && action !== null && 'kind' in action && typeof action.kind === 'string';
28
+ }
29
+ /**
30
+ * Open a TCP connection to a GLSP server and wrap it as a driver. The session is
31
+ * not initialised until the kit calls `start()`.
32
+ */
33
+ export async function connectGlspSocketDriver(options) {
34
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
35
+ const clientSessionId = options.clientSessionId ?? `glsp-socket-${randomUUID()}`;
36
+ const socket = await openSocket(options.host ?? '127.0.0.1', options.port, timeoutMs);
37
+ const connection = createMessageConnection(new SocketMessageReader(socket), new SocketMessageWriter(socket), console);
38
+ const actions = [];
39
+ const send = (action) => {
40
+ // Fire-and-forget, so neither a closed connection, which throws here, nor
41
+ // a failed write reaches the caller; the wait that follows reports it.
42
+ try {
43
+ connection.sendNotification('process', { clientId: clientSessionId, action }).catch(() => undefined);
44
+ }
45
+ catch {
46
+ // Already reported by the closed state every later wait reads.
47
+ }
48
+ };
49
+ const consumed = new WeakSet();
50
+ const waiters = [];
51
+ // Why no awaited action can arrive any more, once something ended the session.
52
+ let ended;
53
+ // A wait still pending when the session ends fails at once, naming why,
54
+ // rather than at its timeout reading like a declined operation.
55
+ function end(reason, cause) {
56
+ ended ??= { reason, cause };
57
+ for (const waiter of waiters.splice(0)) {
58
+ waiter.fail(new Error(`${ended.reason} while waiting for '${waiter.kind}'`, { cause: ended.cause }));
59
+ }
60
+ }
61
+ const closedReason = `GLSP connection to session ${clientSessionId} closed`;
62
+ connection.onClose(() => end(closedReason));
63
+ connection.onNotification('process', (message) => {
64
+ if (!isActionMessage(message) || message.clientId !== clientSessionId) {
65
+ return;
66
+ }
67
+ // The fixture's own action type; the guard has checked the one field read here.
68
+ const action = message.action;
69
+ actions.push(action);
70
+ const index = waiters.findIndex(waiter => waiter.kind === action.kind);
71
+ if (index >= 0) {
72
+ const [waiter] = waiters.splice(index, 1);
73
+ consumed.add(action);
74
+ waiter.resolve(action);
75
+ }
76
+ // The connection only logs a handler's throw, so a fixture bug would
77
+ // otherwise surface as an unrelated wait timing out.
78
+ let reply;
79
+ try {
80
+ reply = options.respond?.(action);
81
+ }
82
+ catch (error) {
83
+ end(`respond threw on '${action.kind}' (${String(error)})`, error);
84
+ return;
85
+ }
86
+ if (reply) {
87
+ send(reply);
88
+ }
89
+ });
90
+ connection.listen();
91
+ let disposed = false;
92
+ return {
93
+ actions,
94
+ async start() {
95
+ // Bounded, so a server that accepts the socket and never answers fails
96
+ // naming the request, when the runner's own timeout is the longer one.
97
+ await within(connection.sendRequest('initialize', {
98
+ applicationId: options.applicationId ?? 'hydranium-conformance',
99
+ protocolVersion: options.protocolVersion ?? DEFAULT_PROTOCOL_VERSION
100
+ }), timeoutMs, 'initialize');
101
+ await within(connection.sendRequest('initializeClientSession', {
102
+ clientSessionId,
103
+ diagramType: options.diagramType,
104
+ clientActionKinds: options.clientActionKinds
105
+ }), timeoutMs, 'initializeClientSession');
106
+ },
107
+ dispatch: send,
108
+ nextAction(kind, waitMs = timeoutMs) {
109
+ const existing = actions.find(action => action.kind === kind && !consumed.has(action));
110
+ if (existing) {
111
+ consumed.add(existing);
112
+ return Promise.resolve(existing);
113
+ }
114
+ if (ended) {
115
+ return Promise.reject(new Error(`${ended.reason}; no '${kind}' action can arrive`, { cause: ended.cause }));
116
+ }
117
+ return new Promise((resolve, reject) => {
118
+ const waiter = {
119
+ kind,
120
+ resolve: (action) => {
121
+ clearTimeout(timer);
122
+ resolve(action);
123
+ },
124
+ fail: (error) => {
125
+ clearTimeout(timer);
126
+ reject(error);
127
+ }
128
+ };
129
+ const timer = setTimeout(() => {
130
+ const at = waiters.indexOf(waiter);
131
+ if (at >= 0) {
132
+ waiters.splice(at, 1);
133
+ }
134
+ // Naming what did arrive tells an operation the server declined
135
+ // (a status or nothing at all) apart from a transport that hangs.
136
+ const seen = actions.map(action => action.kind).join(', ') || 'nothing';
137
+ reject(new Error(`No '${kind}' action within ${waitMs}ms on session ${clientSessionId}; received: ${seen}`));
138
+ }, waitMs);
139
+ waiters.push(waiter);
140
+ });
141
+ },
142
+ dispose() {
143
+ if (disposed) {
144
+ return;
145
+ }
146
+ disposed = true;
147
+ // A disposed connection fires no close event, so the waits end here.
148
+ end(closedReason);
149
+ connection.dispose();
150
+ socket.destroy();
151
+ }
152
+ };
153
+ }
154
+ function within(request, timeoutMs, method) {
155
+ return new Promise((resolve, reject) => {
156
+ const timer = setTimeout(() => reject(new Error(`GLSP server did not answer '${method}' within ${timeoutMs}ms`)), timeoutMs);
157
+ request.then(value => {
158
+ clearTimeout(timer);
159
+ resolve(value);
160
+ }, (error) => {
161
+ clearTimeout(timer);
162
+ reject(error);
163
+ });
164
+ });
165
+ }
166
+ function openSocket(host, port, timeoutMs) {
167
+ return new Promise((resolve, reject) => {
168
+ const socket = net.createConnection({ host, port });
169
+ const timer = setTimeout(() => {
170
+ socket.destroy();
171
+ reject(new Error(`TCP connect to GLSP server at ${host}:${port} timed out after ${timeoutMs}ms`));
172
+ }, timeoutMs);
173
+ socket.once('connect', () => {
174
+ clearTimeout(timer);
175
+ resolve(socket);
176
+ });
177
+ socket.once('error', error => {
178
+ clearTimeout(timer);
179
+ reject(error);
180
+ });
181
+ });
182
+ }
183
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/glsp/node/index.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hydranium/conformance",
3
- "version": "1.0.0-next.251",
3
+ "version": "1.0.0-next.253",
4
4
  "description": "Protocol conformance kit (TCK) for hydranium adopters. Per-head slices (/data, /lsp, /glsp) of fixture-parameterized, protocol-only smoke checks an adopter runs against its own server.",
5
5
  "keywords": [
6
6
  "conformance",
@@ -49,6 +49,10 @@
49
49
  "types": "./lib/glsp/index.d.ts",
50
50
  "default": "./lib/glsp/index.js"
51
51
  },
52
+ "./glsp/node": {
53
+ "types": "./lib/glsp/node/index.d.ts",
54
+ "default": "./lib/glsp/node/index.js"
55
+ },
52
56
  "./jest": {
53
57
  "types": "./lib/jest/index.d.ts",
54
58
  "default": "./lib/jest/index.js"
@@ -71,19 +75,21 @@
71
75
  "watch": "tsc -b -w --preserveWatchOutput"
72
76
  },
73
77
  "devDependencies": {
74
- "@hydranium/protocol": "1.0.0-next.251",
78
+ "@hydranium/protocol": "1.0.0-next.253",
75
79
  "@jest/globals": "^29.7.0",
76
80
  "@types/jest": "^29.5.12",
77
81
  "esbuild": "^0.28.0",
78
82
  "jest": "^29.7.0",
79
83
  "rimraf": "^5.0.0",
80
84
  "typescript": "^7.0.2",
81
- "vitest": "^4.0.0"
85
+ "vitest": "^4.0.0",
86
+ "vscode-jsonrpc": "9.0.0"
82
87
  },
83
88
  "peerDependencies": {
84
- "@hydranium/protocol": "1.0.0-next.251",
89
+ "@hydranium/protocol": "1.0.0-next.253",
85
90
  "@jest/globals": "^29.0.0",
86
- "vitest": "^4.0.0"
91
+ "vitest": "^4.0.0",
92
+ "vscode-jsonrpc": "^9.0.0"
87
93
  },
88
94
  "peerDependenciesMeta": {
89
95
  "@jest/globals": {
@@ -96,5 +102,6 @@
96
102
  "engines": {
97
103
  "node": ">=22.13"
98
104
  },
105
+ "//peerDependencies": "`vscode-jsonrpc` takes a caret, like the other framework packages, and is already required by the `@hydranium/protocol` peer. Only `./glsp/node` imports it, sending GLSP's messages by method name, so the copy it resolves never has to match GLSP's own.",
99
106
  "//prepack": "The publish guard, a `prepack` rather than a `prepare`: `prepack` runs only when a tarball is made (`npm pack`, `npm publish`) and never on install, so it cannot break the install it has no business touching. What it defends against: `files` lists `lib`, `lib` is gitignored, and a `files` entry matching nothing is skipped SILENTLY — so `npm publish` from an unbuilt tree emits a tarball of `src` and nothing else, with no error."
100
107
  }
package/src/glsp/index.ts CHANGED
@@ -80,8 +80,12 @@ export interface GlspCreateOperationSpec<TAction, TDriver extends GlspConformanc
80
80
  * fixture to its input document, so a fixture whose input was MUTATED by an
81
81
  * earlier check silently expects the wrong number. Give each check pristine
82
82
  * input, or take the delta route above.
83
+ *
84
+ * May answer asynchronously. It runs once the response kind has arrived, and
85
+ * a write observed from another process, a `workspace/applyEdit` say, can
86
+ * arrive after that.
83
87
  */
84
- readonly expectMutated: (driver: TDriver) => boolean;
88
+ readonly expectMutated: (driver: TDriver) => boolean | Promise<boolean>;
85
89
  }
86
90
 
87
91
  /**
@@ -197,7 +201,7 @@ export function buildGlspChecks<TAction, TDriver extends GlspConformanceDriver<T
197
201
  driver.dispatch(operation.action(driver));
198
202
  await driver.nextAction(operation.expectedResponseKind);
199
203
  assert.ok(
200
- operation.expectMutated(driver),
204
+ await operation.expectMutated(driver),
201
205
  'expectMutated was false — the create operation did not mutate the source model'
202
206
  );
203
207
  } finally {
@@ -0,0 +1,258 @@
1
+ /********************************************************************************
2
+ * Copyright (c) 2026 EclipseSource and others.
3
+ *
4
+ * This program and the accompanying materials are made available under the
5
+ * terms of the MIT License which is available in the project root.
6
+ *
7
+ * SPDX-License-Identifier: MIT
8
+ ********************************************************************************/
9
+
10
+ /**
11
+ * The `@hydranium/conformance/glsp/node` subpath: a socket driver for the
12
+ * {@link GlspConformanceDriver} port, over a real TCP socket, for proving a
13
+ * deployed GLSP server rather than a container built in the test process.
14
+ *
15
+ * It speaks GLSP's JSON-RPC by method name, so like the rest of the kit it names
16
+ * no `@eclipse-glsp/*` type and the adopter's fixtures supply every action.
17
+ */
18
+
19
+ import { randomUUID } from 'node:crypto';
20
+ import * as net from 'node:net';
21
+ import { createMessageConnection, SocketMessageReader, SocketMessageWriter, type MessageConnection } from 'vscode-jsonrpc/node';
22
+ import type { GlspConformanceDriver } from '../index.js';
23
+
24
+ const DEFAULT_PROTOCOL_VERSION = '1.0.0';
25
+
26
+ const DEFAULT_TIMEOUT_MS = 15_000;
27
+
28
+ /** Configuration for {@link connectGlspSocketDriver}. */
29
+ export interface GlspSocketDriverOptions<TAction extends { readonly kind: string } = { readonly kind: string }> {
30
+ readonly port: number;
31
+ /** Defaults to `127.0.0.1`. */
32
+ readonly host?: string;
33
+ /** The diagram type the client session is opened for. */
34
+ readonly diagramType: string;
35
+ /**
36
+ * The action kinds this client handles, which the server routes back to it.
37
+ * Declare every kind the server sends during the checks: one it sends that
38
+ * neither side handles fails the server's dispatch, and the session gets a
39
+ * rejected request or an error message instead.
40
+ */
41
+ readonly clientActionKinds: readonly string[];
42
+ /** Defaults to a fresh id per driver, so drivers sharing one server never share a session. */
43
+ readonly clientSessionId?: string;
44
+ readonly applicationId?: string;
45
+ /**
46
+ * The GLSP protocol version announced in `initialize`. The server refuses
47
+ * `initialize` unless it equals its own, so a server on a newer protocol
48
+ * needs this. Defaults to `1.0.0`.
49
+ */
50
+ readonly protocolVersion?: string;
51
+ /**
52
+ * The client's reply to a received action, sent back to the server, or
53
+ * `undefined` for none. A server that lays out on the client finishes a
54
+ * `requestModel` only once its `requestBounds` is answered with
55
+ * `computedBounds`, so a load without this stops half way. A throw fails
56
+ * every pending and later wait with it.
57
+ */
58
+ readonly respond?: (action: TAction) => TAction | undefined;
59
+ /** Default bound for {@link GlspConformanceDriver.nextAction}, and the bound on the TCP connect and on each `start()` request. */
60
+ readonly timeoutMs?: number;
61
+ }
62
+
63
+ /** A {@link GlspConformanceDriver} over a socket, with the actions it received. */
64
+ export interface GlspSocketDriver<TAction extends { readonly kind: string }> extends GlspConformanceDriver<TAction> {
65
+ /** Every action the server sent to this session, in arrival order; append-only. */
66
+ readonly actions: ReadonlyArray<TAction>;
67
+ }
68
+
69
+ /** GLSP's `process` notification payload, in either direction. */
70
+ interface ActionMessage<TAction> {
71
+ readonly clientId: string;
72
+ readonly action: TAction;
73
+ }
74
+
75
+ function isActionMessage(value: unknown): value is ActionMessage<{ readonly kind: string }> {
76
+ if (typeof value !== 'object' || value === null || !('clientId' in value) || !('action' in value)) {
77
+ return false;
78
+ }
79
+ const { action } = value;
80
+ return typeof action === 'object' && action !== null && 'kind' in action && typeof action.kind === 'string';
81
+ }
82
+
83
+ /**
84
+ * Open a TCP connection to a GLSP server and wrap it as a driver. The session is
85
+ * not initialised until the kit calls `start()`.
86
+ */
87
+ export async function connectGlspSocketDriver<TAction extends { readonly kind: string }>(
88
+ options: GlspSocketDriverOptions<TAction>
89
+ ): Promise<GlspSocketDriver<TAction>> {
90
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
91
+ const clientSessionId = options.clientSessionId ?? `glsp-socket-${randomUUID()}`;
92
+ const socket = await openSocket(options.host ?? '127.0.0.1', options.port, timeoutMs);
93
+ const connection: MessageConnection = createMessageConnection(new SocketMessageReader(socket), new SocketMessageWriter(socket), console);
94
+
95
+ const actions: TAction[] = [];
96
+ const send = (action: TAction): void => {
97
+ // Fire-and-forget, so neither a closed connection, which throws here, nor
98
+ // a failed write reaches the caller; the wait that follows reports it.
99
+ try {
100
+ connection.sendNotification('process', { clientId: clientSessionId, action }).catch(() => undefined);
101
+ } catch {
102
+ // Already reported by the closed state every later wait reads.
103
+ }
104
+ };
105
+ const consumed = new WeakSet<TAction>();
106
+ const waiters: Array<{ readonly kind: string; readonly resolve: (action: TAction) => void; readonly fail: (error: Error) => void }> = [];
107
+ // Why no awaited action can arrive any more, once something ended the session.
108
+ let ended: { readonly reason: string; readonly cause?: unknown } | undefined;
109
+
110
+ // A wait still pending when the session ends fails at once, naming why,
111
+ // rather than at its timeout reading like a declined operation.
112
+ function end(reason: string, cause?: unknown): void {
113
+ ended ??= { reason, cause };
114
+ for (const waiter of waiters.splice(0)) {
115
+ waiter.fail(new Error(`${ended.reason} while waiting for '${waiter.kind}'`, { cause: ended.cause }));
116
+ }
117
+ }
118
+ const closedReason = `GLSP connection to session ${clientSessionId} closed`;
119
+ connection.onClose(() => end(closedReason));
120
+
121
+ connection.onNotification('process', (message: unknown) => {
122
+ if (!isActionMessage(message) || message.clientId !== clientSessionId) {
123
+ return;
124
+ }
125
+ // The fixture's own action type; the guard has checked the one field read here.
126
+ const action = message.action as TAction;
127
+ actions.push(action);
128
+ const index = waiters.findIndex(waiter => waiter.kind === action.kind);
129
+ if (index >= 0) {
130
+ const [waiter] = waiters.splice(index, 1);
131
+ consumed.add(action);
132
+ waiter.resolve(action);
133
+ }
134
+ // The connection only logs a handler's throw, so a fixture bug would
135
+ // otherwise surface as an unrelated wait timing out.
136
+ let reply: TAction | undefined;
137
+ try {
138
+ reply = options.respond?.(action);
139
+ } catch (error: unknown) {
140
+ end(`respond threw on '${action.kind}' (${String(error)})`, error);
141
+ return;
142
+ }
143
+ if (reply) {
144
+ send(reply);
145
+ }
146
+ });
147
+ connection.listen();
148
+
149
+ let disposed = false;
150
+
151
+ return {
152
+ actions,
153
+
154
+ async start(): Promise<void> {
155
+ // Bounded, so a server that accepts the socket and never answers fails
156
+ // naming the request, when the runner's own timeout is the longer one.
157
+ await within(
158
+ connection.sendRequest('initialize', {
159
+ applicationId: options.applicationId ?? 'hydranium-conformance',
160
+ protocolVersion: options.protocolVersion ?? DEFAULT_PROTOCOL_VERSION
161
+ }),
162
+ timeoutMs,
163
+ 'initialize'
164
+ );
165
+ await within(
166
+ connection.sendRequest('initializeClientSession', {
167
+ clientSessionId,
168
+ diagramType: options.diagramType,
169
+ clientActionKinds: options.clientActionKinds
170
+ }),
171
+ timeoutMs,
172
+ 'initializeClientSession'
173
+ );
174
+ },
175
+
176
+ dispatch: send,
177
+
178
+ nextAction<T extends TAction = TAction>(kind: string, waitMs = timeoutMs): Promise<T> {
179
+ const existing = actions.find(action => action.kind === kind && !consumed.has(action));
180
+ if (existing) {
181
+ consumed.add(existing);
182
+ return Promise.resolve(existing as T);
183
+ }
184
+ if (ended) {
185
+ return Promise.reject(new Error(`${ended.reason}; no '${kind}' action can arrive`, { cause: ended.cause }));
186
+ }
187
+ return new Promise<T>((resolve, reject) => {
188
+ const waiter = {
189
+ kind,
190
+ resolve: (action: TAction): void => {
191
+ clearTimeout(timer);
192
+ resolve(action as T);
193
+ },
194
+ fail: (error: Error): void => {
195
+ clearTimeout(timer);
196
+ reject(error);
197
+ }
198
+ };
199
+ const timer = setTimeout(() => {
200
+ const at = waiters.indexOf(waiter);
201
+ if (at >= 0) {
202
+ waiters.splice(at, 1);
203
+ }
204
+ // Naming what did arrive tells an operation the server declined
205
+ // (a status or nothing at all) apart from a transport that hangs.
206
+ const seen = actions.map(action => action.kind).join(', ') || 'nothing';
207
+ reject(new Error(`No '${kind}' action within ${waitMs}ms on session ${clientSessionId}; received: ${seen}`));
208
+ }, waitMs);
209
+ waiters.push(waiter);
210
+ });
211
+ },
212
+
213
+ dispose(): void {
214
+ if (disposed) {
215
+ return;
216
+ }
217
+ disposed = true;
218
+ // A disposed connection fires no close event, so the waits end here.
219
+ end(closedReason);
220
+ connection.dispose();
221
+ socket.destroy();
222
+ }
223
+ };
224
+ }
225
+
226
+ function within<T>(request: Promise<T>, timeoutMs: number, method: string): Promise<T> {
227
+ return new Promise<T>((resolve, reject) => {
228
+ const timer = setTimeout(() => reject(new Error(`GLSP server did not answer '${method}' within ${timeoutMs}ms`)), timeoutMs);
229
+ request.then(
230
+ value => {
231
+ clearTimeout(timer);
232
+ resolve(value);
233
+ },
234
+ (error: unknown) => {
235
+ clearTimeout(timer);
236
+ reject(error);
237
+ }
238
+ );
239
+ });
240
+ }
241
+
242
+ function openSocket(host: string, port: number, timeoutMs: number): Promise<net.Socket> {
243
+ return new Promise<net.Socket>((resolve, reject) => {
244
+ const socket = net.createConnection({ host, port });
245
+ const timer = setTimeout(() => {
246
+ socket.destroy();
247
+ reject(new Error(`TCP connect to GLSP server at ${host}:${port} timed out after ${timeoutMs}ms`));
248
+ }, timeoutMs);
249
+ socket.once('connect', () => {
250
+ clearTimeout(timer);
251
+ resolve(socket);
252
+ });
253
+ socket.once('error', error => {
254
+ clearTimeout(timer);
255
+ reject(error);
256
+ });
257
+ });
258
+ }