Skip to content

Commit d675c17

Browse files
TheLarkInnCopilot
andauthored
[rushd] Wire layer: protocol, transport & presentation (WS1) (#5922)
* [rushd] Wire layer: protocol, transport & presentation (WS1, #5896) Add the engine-agnostic rushd wire layer as three new 0.x packages plus additive rush-lib engine instrumentation: - @rushstack/rush-daemon-protocol: frame taxonomy (0x01 control-json, 0x02/0x03 log-stdout/stderr, 0x04 stdin, 0x05 event), length-prefixed binary codec, DAEMON_PROTOCOL_VERSION, hello/version negotiation with typed mismatch errors, and per-subscription verbosity filtering at serialization. Event envelope mirrors the @rushstack/reporter contract as a placeholder pending its merge (#5858). - @rushstack/rush-daemon-transport: workspace-key hashing, per-user runtime-dir socket/pipe paths, net listener/connector with backpressure, and PID/lockfile stale-socket reclaim. - @rushstack/rush-terminal-renderer: client reporter host with StreamCollator-backed per-op collation (byte-parity with legacy), per-client verbosity, and FORCE_COLOR/COLUMNS child-env threading. - rush-lib: optional internal IOperationGraphEventSink dual-emit (structured status/activity/header events + per-op raw output tap) with byte-identical legacy output. - build-tests/rushd-wire-e2e-test: cross-layer conformance suite (golden parity over a real socket, raw-stream integrity, verbosity isolation, failure propagation). - rigs: shared strict-codegen ESLint mixin for the new packages, with inline suppression disabled (noInlineConfig). Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 47388e8f-8d41-4ca4-819b-688ea6c510a2 * [rushd] Address WS1 review: protocol hardening, performance, and safety Review pass over the wire-layer packages (#5922): Protocol (@rushstack/rush-daemon-protocol): - kind/payload naming throughout (frames + control messages); control messages are a uniform { kind, payload } discriminated union so kind reads stay monomorphic. - Uint8Array wire payloads instead of Buffer, so the protocol is platform-agnostic and drops the @types/node peer dependency. - FrameDecoder uses ECMAScript private fields and accumulates received bytes in a SegmentBuffer (no per-push Buffer.concat); payloads copy out once per completed frame. - encodeDaemonFrames returns a Uint8Array[] (no batch concat); the transport writes parts sequentially. - Containment checks use numeric ranges/Sets; the event-type list is as-const with the union derived from it (list and type cannot drift). - Error codes are a plain string union; DaemonProtocolError accepts { cause } per the standard Error convention. - decodeDaemonEventFrame structurally validates envelopes (typed error instead of routing malformed input); new isDaemonEventEnvelope guard. - Envelope optional fields moved to the end of the layout; log chunk encoding measures the id once and allocates the payload once; TS target ES2022. Transport (@rushstack/rush-daemon-transport): - Decoder/handler failures in the socket callback now fail the connection closed instead of crashing the daemon (new ConnectionRobustness test). - Reclaim is serialized through a dedicated .reclaim mutex (wx create, dead-PID steal) so a concurrent starter cannot unlink a socket another process just bound; the daemon lockfile is written after bind so a stale record never reads as a live owner. Renderer/e2e: track the renames and Uint8Array decode boundary. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 47388e8f-8d41-4ca4-819b-688ea6c510a2 * [rushd] Fix CI: annotate literal lists to clear typedef warnings rush retest runs with warnings-as-failures; the reviewer-requested as-const literal lists tripped the friendly-locals @typescript-eslint/typedef rule (variableDeclaration). Annotate DAEMON_EVENT_TYPES and DAEMON_CONTROL_MESSAGE_KINDS with explicit literal-tuple types and derive the unions from them, preserving the single-source-of-truth invariant (adding a member requires updating the tuple annotation, which the compiler enforces via the derived union) with zero lint warnings. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 47388e8f-8d41-4ca4-819b-688ea6c510a2 * chore: regenerate README package table (repo-toolbox readme) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 47388e8f-8d41-4ca4-819b-688ea6c510a2 * [rushd] Fix e2e golden comparison on Windows (OS newline normalization) The renderer pipeline normalizes newlines to the OS default (CRLF on Windows) via colorsNewlinesTransform, but the e2e test sink compared the raw LF-carrying chunks, so the byte-parity assertions failed only on Windows. Normalize the captured golden to the OS newline before comparing so the test is platform-correct. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 47388e8f-8d41-4ca4-819b-688ea6c510a2 * [rushd] Fix e2e golden newline normalization to not double-apply CRLF The previous OS-newline normalization mapped every LF to CRLF, producing CRCRLF on Windows for text that already carried CRLF. Normalize only lone-LF newlines (leave existing CRLF intact) so the golden comparison is correct on both platforms. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 47388e8f-8d41-4ca4-819b-688ea6c510a2 * [rushd] Make e2e golden newline normalization idempotent Collapse existing CRLF to LF before re-applying the OS newline, so the golden comparison is correct on Windows (no CRCRLF) and a no-op on POSIX. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 47388e8f-8d41-4ca4-819b-688ea6c510a2 * [rushd] Emit OS newline for renderer global activity lines The legacy collated pipeline normalizes to OS newlines via TextRewriterTransform(OsDefault), but the renderer wrote global activityChanged lines with a raw LF, producing mixed LF/CRLF output on Windows. Write the client OS newline instead so global status lines and collated blocks are byte-consistent. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 47388e8f-8d41-4ca4-819b-688ea6c510a2 --------- Co-authored-by: TheLarkInn <TheLarkInn@users.noreply.github.com> Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 47388e8f-8d41-4ca4-819b-688ea6c510a2
1 parent c106ea9 commit d675c17

139 files changed

Lines changed: 7481 additions & 234 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎README.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,11 +97,14 @@ These GitHub repositories provide supplementary resources for Rush Stack:
9797
| [/libraries/package-extractor](./libraries/package-extractor/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Fpackage-extractor.svg)](https://badge.fury.io/js/%40rushstack%2Fpackage-extractor) | [changelog](./libraries/package-extractor/CHANGELOG.md) | [@rushstack/package-extractor](https://www.npmjs.com/package/@rushstack/package-extractor) |
9898
| [/libraries/problem-matcher](./libraries/problem-matcher/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Fproblem-matcher.svg)](https://badge.fury.io/js/%40rushstack%2Fproblem-matcher) | [changelog](./libraries/problem-matcher/CHANGELOG.md) | [@rushstack/problem-matcher](https://www.npmjs.com/package/@rushstack/problem-matcher) |
9999
| [/libraries/rig-package](./libraries/rig-package/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Frig-package.svg)](https://badge.fury.io/js/%40rushstack%2Frig-package) | [changelog](./libraries/rig-package/CHANGELOG.md) | [@rushstack/rig-package](https://www.npmjs.com/package/@rushstack/rig-package) |
100+
| [/libraries/rush-daemon-protocol](./libraries/rush-daemon-protocol/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Frush-daemon-protocol.svg)](https://badge.fury.io/js/%40rushstack%2Frush-daemon-protocol) | [changelog](./libraries/rush-daemon-protocol/CHANGELOG.md) | [@rushstack/rush-daemon-protocol](https://www.npmjs.com/package/@rushstack/rush-daemon-protocol) |
101+
| [/libraries/rush-daemon-transport](./libraries/rush-daemon-transport/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Frush-daemon-transport.svg)](https://badge.fury.io/js/%40rushstack%2Frush-daemon-transport) | [changelog](./libraries/rush-daemon-transport/CHANGELOG.md) | [@rushstack/rush-daemon-transport](https://www.npmjs.com/package/@rushstack/rush-daemon-transport) |
100102
| [/libraries/rush-lib](./libraries/rush-lib/) | [![npm version](https://badge.fury.io/js/%40microsoft%2Frush-lib.svg)](https://badge.fury.io/js/%40microsoft%2Frush-lib) | | [@microsoft/rush-lib](https://www.npmjs.com/package/@microsoft/rush-lib) |
101103
| [/libraries/rush-pnpm-kit-v10](./libraries/rush-pnpm-kit-v10/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Frush-pnpm-kit-v10.svg)](https://badge.fury.io/js/%40rushstack%2Frush-pnpm-kit-v10) | [changelog](./libraries/rush-pnpm-kit-v10/CHANGELOG.md) | [@rushstack/rush-pnpm-kit-v10](https://www.npmjs.com/package/@rushstack/rush-pnpm-kit-v10) |
102104
| [/libraries/rush-pnpm-kit-v8](./libraries/rush-pnpm-kit-v8/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Frush-pnpm-kit-v8.svg)](https://badge.fury.io/js/%40rushstack%2Frush-pnpm-kit-v8) | [changelog](./libraries/rush-pnpm-kit-v8/CHANGELOG.md) | [@rushstack/rush-pnpm-kit-v8](https://www.npmjs.com/package/@rushstack/rush-pnpm-kit-v8) |
103105
| [/libraries/rush-pnpm-kit-v9](./libraries/rush-pnpm-kit-v9/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Frush-pnpm-kit-v9.svg)](https://badge.fury.io/js/%40rushstack%2Frush-pnpm-kit-v9) | [changelog](./libraries/rush-pnpm-kit-v9/CHANGELOG.md) | [@rushstack/rush-pnpm-kit-v9](https://www.npmjs.com/package/@rushstack/rush-pnpm-kit-v9) |
104106
| [/libraries/rush-sdk](./libraries/rush-sdk/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Frush-sdk.svg)](https://badge.fury.io/js/%40rushstack%2Frush-sdk) | | [@rushstack/rush-sdk](https://www.npmjs.com/package/@rushstack/rush-sdk) |
107+
| [/libraries/rush-terminal-renderer](./libraries/rush-terminal-renderer/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Frush-terminal-renderer.svg)](https://badge.fury.io/js/%40rushstack%2Frush-terminal-renderer) | [changelog](./libraries/rush-terminal-renderer/CHANGELOG.md) | [@rushstack/rush-terminal-renderer](https://www.npmjs.com/package/@rushstack/rush-terminal-renderer) |
105108
| [/libraries/stream-collator](./libraries/stream-collator/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Fstream-collator.svg)](https://badge.fury.io/js/%40rushstack%2Fstream-collator) | [changelog](./libraries/stream-collator/CHANGELOG.md) | [@rushstack/stream-collator](https://www.npmjs.com/package/@rushstack/stream-collator) |
106109
| [/libraries/terminal](./libraries/terminal/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Fterminal.svg)](https://badge.fury.io/js/%40rushstack%2Fterminal) | [changelog](./libraries/terminal/CHANGELOG.md) | [@rushstack/terminal](https://www.npmjs.com/package/@rushstack/terminal) |
107110
| [/libraries/tree-pattern](./libraries/tree-pattern/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Ftree-pattern.svg)](https://badge.fury.io/js/%40rushstack%2Ftree-pattern) | [changelog](./libraries/tree-pattern/CHANGELOG.md) | [@rushstack/tree-pattern](https://www.npmjs.com/package/@rushstack/tree-pattern) |
@@ -226,6 +229,7 @@ These GitHub repositories provide supplementary resources for Rush Stack:
226229
| [/build-tests/rush-package-manager-integration-test](./build-tests/rush-package-manager-integration-test/) | Integration tests for non-pnpm package managers in Rush. |
227230
| [/build-tests/rush-project-change-analyzer-test](./build-tests/rush-project-change-analyzer-test/) | This is an example project that uses rush-lib's ProjectChangeAnalyzer to |
228231
| [/build-tests/rush-redis-cobuild-plugin-integration-test](./build-tests/rush-redis-cobuild-plugin-integration-test/) | Tests connecting to an redis server |
232+
| [/build-tests/rushd-wire-e2e-test](./build-tests/rushd-wire-e2e-test/) | End-to-end conformance tests for the rushd wire layer (protocol + transport + renderer against the rush-lib engine) |
229233
| [/build-tests/set-webpack-public-path-plugin-test](./build-tests/set-webpack-public-path-plugin-test/) | Building this project tests the set-webpack-public-path-plugin |
230234
| [/build-tests/webpack-local-version-test](./build-tests/webpack-local-version-test/) | Building this project tests the rig loading for the local version of webpack |
231235
| [/eslint/local-eslint-config](./eslint/local-eslint-config/) | An ESLint configuration consumed projects inside the rushstack repo. |
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# Agent coding contract — `rushd-wire-e2e-test`
2+
3+
This package is governed by an **ultra-strict lint policy** for generated code. All of the
4+
rules below are enabled to `error` in `eslint.config.js` via the shared
5+
`local-node-rig/profiles/default/includes/eslint/flat/mixins/strict-codegen.js` mixin.
6+
They apply to **all** TypeScript in this package, **including tests** (`src/**/*.test.ts`).
7+
8+
## Enforced rules (do not attempt to bypass)
9+
10+
| Rule | Setting |
11+
| ---- | ------- |
12+
| `complexity` | `['error', 3]` |
13+
| `max-depth` | `['error', 3]` |
14+
| `max-lines-per-function` | `['error', 30]` |
15+
| `max-lines` | `['error', 100]` — every file, including this means: keep files small; split modules |
16+
| `max-params` | `['error', 4]` — use options objects |
17+
| `@typescript-eslint/no-magic-numbers` | `'error'` — every numeric literal must be a named constant |
18+
| `@typescript-eslint/prefer-nullish-coalescing` | `'error'` — use `??`, not `\|\|` or nullish-guard ternaries |
19+
| `import/enforce-node-protocol-usage` | `['error', 'always']` — write `node:crypto`, never `crypto` |
20+
| `import/order` | `['error', { alphabetize: asc, grouped, newlines-between: always }]` |
21+
| `sort-imports` | `['error', { ignoreDeclarationSort: true }]` — sort named members |
22+
| `@typescript-eslint/consistent-type-imports` | `['error', { fixStyle: 'separate-type-imports' }]` — `import type { X }`, never inline `type` specifiers |
23+
| `import/no-relative-parent-imports` | `'error'` for non-test source — no `../` imports outside tests |
24+
| `no-eval`, `@typescript-eslint/no-implied-eval` | `'error'` |
25+
26+
## Suppression is forbidden — mechanically enforced
27+
28+
- `linterOptions.noInlineConfig: true` makes **every** `eslint-disable*` comment a lint error.
29+
- `reportUnusedDisableDirectives: 'error'` flags stale suppressions.
30+
- Therefore, as an agent working in this package you MUST NOT:
31+
- add `eslint-disable`, `eslint-disable-next-line`, `eslint-env`, or inline `/* eslint ... */` config comments;
32+
- add entries to any `.eslint-bulk-suppressions.json`;
33+
- add `eslintIgnore` keys to `package.json`;
34+
- add `@ts-nocheck` or `@ts-ignore` comments;
35+
- weaken, reorder, or remove the `strict-codegen` mixin in `eslint.config.js`.
36+
- If a rule fires, **fix the code** (extract a constant, split the function/module, restructure) — never silence it.
37+
38+
## Deferred rules (do not emulate with hacks)
39+
40+
The following intended rules have no existing implementation in this repository's ESLint
41+
toolchain and are **not yet enabled** (the user will wire them up later):
42+
`no-magic-strings`, `no-object-mutation`, `no-array-mutation`,
43+
`no-placeholder-implementation`, and the custom zero-tolerance import rules
44+
(`no-re-export`, `require-clean-barrel`, `require-barrel-relative-exports`,
45+
`no-export-alias`, `no-dynamic-import`, `no-hardcoded-secrets`,
46+
`no-parent-internal-access`). Write code that would already satisfy them: prefer immutable
47+
update patterns and named string constants, and never land stubs or `TODO` implementations.
48+
49+
## Design notes for this package
50+
51+
- Test-only project: adapts the rush-lib engine's dual-emit into wire frames and proves conformance (golden parity, raw streams, verbosity isolation, backpressure) through the real protocol and transport packages.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"extends": "local-node-rig/profiles/default/config/jest.config.json"
3+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
// The "rig.json" file directs tools to look for their config files in an external package.
3+
// Documentation for this system: https://www.npmjs.com/package/@rushstack/rig-package
4+
"$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json",
5+
6+
"rigPackageName": "local-node-rig"
7+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
2+
// See LICENSE in the project root for license information.
3+
4+
const nodeProfile = require('local-node-rig/profiles/default/includes/eslint/flat/profile/node');
5+
const friendlyLocalsMixin = require('local-node-rig/profiles/default/includes/eslint/flat/mixins/friendly-locals');
6+
const tsdocMixin = require('local-node-rig/profiles/default/includes/eslint/flat/mixins/tsdoc');
7+
const strictCodegenMixin = require('local-node-rig/profiles/default/includes/eslint/flat/mixins/strict-codegen');
8+
9+
module.exports = [
10+
...nodeProfile,
11+
...friendlyLocalsMixin,
12+
...tsdocMixin,
13+
// IMPORTANT: The strict-codegen mixin must remain last so its rules win conflicts.
14+
...strictCodegenMixin,
15+
{
16+
files: ['**/*.ts', '**/*.tsx'],
17+
languageOptions: {
18+
parserOptions: {
19+
tsconfigRootDir: __dirname
20+
}
21+
}
22+
}
23+
];
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"name": "rushd-wire-e2e-test",
3+
"description": "End-to-end conformance tests for the rushd wire layer (protocol + transport + renderer against the rush-lib engine)",
4+
"version": "1.0.0",
5+
"private": true,
6+
"license": "MIT",
7+
"scripts": {
8+
"build": "heft build --clean",
9+
"_phase:build": "heft run --only build -- --clean",
10+
"_phase:test": "heft run --only test -- --clean"
11+
},
12+
"devDependencies": {
13+
"@microsoft/rush-lib": "workspace:*",
14+
"@rushstack/heft": "workspace:*",
15+
"@rushstack/node-core-library": "workspace:*",
16+
"@rushstack/rush-daemon-protocol": "workspace:*",
17+
"@rushstack/rush-daemon-transport": "workspace:*",
18+
"@rushstack/rush-terminal-renderer": "workspace:*",
19+
"@rushstack/terminal": "workspace:*",
20+
"eslint": "~9.37.0",
21+
"local-node-rig": "workspace:*"
22+
}
23+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
2+
// See LICENSE in the project root for license information.
3+
4+
/**
5+
* End-to-end conformance tests for the rushd wire layer. See `src/test/`.
6+
*
7+
* @packageDocumentation
8+
*/
9+
10+
export {};
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
2+
// See LICENSE in the project root for license information.
3+
4+
// Inline operation runners that use runWithTerminalAsync, matching the
5+
// production runners (ShellOperationRunner, IPCOperationRunner).
6+
7+
import * as os from 'node:os';
8+
9+
import type { RushConfigurationProject } from '@microsoft/rush-lib/lib/api/RushConfigurationProject';
10+
import type {
11+
IOperationRunner,
12+
IOperationRunnerContext
13+
} from '@microsoft/rush-lib/lib/logic/operations/IOperationRunner';
14+
import { Operation } from '@microsoft/rush-lib/lib/logic/operations/Operation';
15+
import type { OperationStatus } from '@microsoft/rush-lib/lib/logic/operations/OperationStatus';
16+
17+
function createRunner(name: string, status: OperationStatus): IOperationRunner {
18+
return {
19+
name,
20+
reportTiming: true,
21+
silent: false,
22+
cacheable: false,
23+
warningsAreAllowed: false,
24+
isNoOp: false,
25+
executeAsync: async (context: IOperationRunnerContext) =>
26+
await context.runWithTerminalAsync(
27+
async (terminal) => {
28+
terminal.writeLine(`${name}-out ünïcode ✓`);
29+
terminal.writeErrorLine(`${name}-err`);
30+
return status;
31+
},
32+
{ createLogFile: false, logFileSuffix: '' }
33+
),
34+
getConfigHash: () => 'e2e'
35+
};
36+
}
37+
38+
/** Creates a fixture operation writing deterministic unicode output. */
39+
export function createScenarioOperation(name: string, status: OperationStatus): Operation {
40+
return new Operation({
41+
runner: createRunner(name, status),
42+
logFilenameIdentifier: name,
43+
phase: {
44+
name: 'phase',
45+
allowWarningsOnSuccess: false,
46+
associatedParameters: new Set(),
47+
dependencies: { self: new Set(), upstream: new Set() },
48+
isSynthetic: false,
49+
logFilenameIdentifier: 'phase',
50+
missingScriptBehavior: 'silent'
51+
},
52+
project: {
53+
packageName: name,
54+
projectFolder: os.tmpdir()
55+
} as unknown as RushConfigurationProject
56+
});
57+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
2+
// See LICENSE in the project root for license information.
3+
4+
// Builds and runs a small rush-lib operation graph with the dual-emit sink
5+
// attached, capturing both the legacy terminal output and the wire frames.
6+
7+
import { OperationGraph } from '@microsoft/rush-lib/lib/logic/operations/OperationGraph';
8+
import type { IOperationGraphOptions } from '@microsoft/rush-lib/lib/logic/operations/OperationGraph';
9+
import { OperationStatus } from '@microsoft/rush-lib/lib/logic/operations/OperationStatus';
10+
11+
import { createScenarioOperation } from './EngineRunners';
12+
import { TestWritable } from './TestWritable';
13+
import { WireAdapter } from './WireAdapter';
14+
15+
const PARALLELISM: number = 1;
16+
17+
/** Options for {@link runEngineScenarioAsync}. */
18+
export interface IEngineScenarioOptions {
19+
/** Run the engine in quiet mode (stdout discarded from the collated terminal). */
20+
readonly quiet: boolean;
21+
/** Make the `beta` operation fail. */
22+
readonly failing?: boolean;
23+
}
24+
25+
/** The captured result of one engine run. */
26+
export interface IEngineScenarioResult {
27+
/** The legacy terminal output (golden reference). */
28+
readonly writable: TestWritable;
29+
/** The wire frames produced by the dual-emit sink. */
30+
readonly adapter: WireAdapter;
31+
}
32+
33+
/** Runs the fixture graph to completion with the wire adapter attached. */
34+
export async function runEngineScenarioAsync(
35+
options: IEngineScenarioOptions
36+
): Promise<IEngineScenarioResult> {
37+
const writable: TestWritable = new TestWritable();
38+
const adapter: WireAdapter = new WireAdapter();
39+
const graphOptions: IOperationGraphOptions = {
40+
quietMode: options.quiet,
41+
debugMode: false,
42+
parallelism: PARALLELISM,
43+
allowOversubscription: true,
44+
destinations: [writable],
45+
abortController: new AbortController()
46+
};
47+
const graph: OperationGraph = new OperationGraph(
48+
new Set([
49+
createScenarioOperation('alpha', OperationStatus.Success),
50+
createScenarioOperation(
51+
'beta',
52+
options.failing ? OperationStatus.Failure : OperationStatus.Success
53+
)
54+
]),
55+
graphOptions
56+
);
57+
graph.eventSink = adapter;
58+
await graph.executeAsync({});
59+
return { writable, adapter };
60+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
2+
// See LICENSE in the project root for license information.
3+
4+
// Shared client-side frame dispatch: decoded frames into a renderer host.
5+
6+
import { DaemonFrameType, decodeDaemonEventFrame, decodeDaemonLogChunk } from '@rushstack/rush-daemon-protocol';
7+
import type { IDaemonFrame, IDaemonLogChunk } from '@rushstack/rush-daemon-protocol';
8+
import type { DaemonRendererHost } from '@rushstack/rush-terminal-renderer';
9+
10+
const WIRE_DECODER: InstanceType<typeof TextDecoder> = new TextDecoder();
11+
12+
/** Returns true for `0x02`/`0x03` log frames. */
13+
export function isLogFrame(frame: IDaemonFrame): boolean {
14+
return frame.kind === DaemonFrameType.logStdout || frame.kind === DaemonFrameType.logStderr;
15+
}
16+
17+
/** Maps a log frame type to its stream name. */
18+
export function toStream(frame: IDaemonFrame): 'stdout' | 'stderr' {
19+
return frame.kind === DaemonFrameType.logStderr ? 'stderr' : 'stdout';
20+
}
21+
22+
/** Routes one decoded frame into the renderer host. */
23+
export function dispatchFrame(host: DaemonRendererHost, frame: IDaemonFrame): void {
24+
if (frame.kind === DaemonFrameType.event) {
25+
host.handleEvent(decodeDaemonEventFrame(frame.payload));
26+
return;
27+
}
28+
if (isLogFrame(frame)) {
29+
const log: IDaemonLogChunk = decodeDaemonLogChunk(frame.payload);
30+
host.handleLogChunk(log.operationId, toStream(frame), log.chunk);
31+
}
32+
}
33+
34+
/** Collects log frame payloads into per-operation ordered text chunks. */
35+
export function collectLogChunk(perOperation: Map<string, string[]>, frame: IDaemonFrame): void {
36+
if (!isLogFrame(frame)) {
37+
return;
38+
}
39+
const log: IDaemonLogChunk = decodeDaemonLogChunk(frame.payload);
40+
const chunks: string[] = perOperation.get(log.operationId) ?? [];
41+
chunks.push(WIRE_DECODER.decode(log.chunk));
42+
perOperation.set(log.operationId, chunks);
43+
}

0 commit comments

Comments
 (0)