Skip to content

Commit a14654f

Browse files
authored
More automation fixes (#4)
1 parent 354422c commit a14654f

8 files changed

Lines changed: 175 additions & 61 deletions

File tree

.github/workflows/run-zwave-wsl.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ jobs:
9292
--exclude=PI_CertificationData_Rev01 `
9393
--exclude=DD_ManufacturerSpecificCCData_Rev01 `
9494
--exclude=DD_VersionCCData_Rev01 `
95+
--exclude=CSR_LifelineMandatoryReports_Rev03 `
9596
--verbose
9697
env:
9798
CI: true

docs/ipc-protocol.md

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,9 @@ Runner Orchestrator
2929
│ │
3030
│<─────── handleCttPrompt request ────│
3131
│ │
32-
│──── handleCttPrompt response ──────>│
32+
│──── handleCttPrompt response ──────>│ (if handler matched)
33+
│ OR │
34+
│──── noHandler notification ────────>│ (if no handler matched)
3335
│ │
3436
│ ... more prompts ... │
3537
│ │
@@ -59,6 +61,19 @@ Sent immediately after WebSocket connection is established.
5961
**Parameters:**
6062
- `name` (string): Display name for the runner, used in logs
6163

64+
### noHandler
65+
66+
Sent when a `handleCttPrompt` request was received but no prompt handler matched. This allows the orchestrator to cancel the test run in CI mode to prevent hanging.
67+
68+
```json
69+
{
70+
"jsonrpc": "2.0",
71+
"method": "noHandler"
72+
}
73+
```
74+
75+
**Note:** This is a notification only - no response is expected. The runner does not send a response to the original `handleCttPrompt` request when no handler matches, allowing interactive user input to work in non-CI environments.
76+
6277
## Requests (Orchestrator → Runner)
6378

6479
### start
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import {
2+
registerHandler,
3+
type PromptContext,
4+
type PromptResponse,
5+
} from "../../prompt-handlers.ts";
6+
7+
// State key for storing warning context
8+
const DISREGARD_RECOMMENDATION_CONTEXT = "disregardRecommendationContext";
9+
10+
// Enum for different recommendation reasons - extend as needed
11+
enum Recommendation {
12+
IndicatorReportInAGI = "IndicatorReportInAGI",
13+
}
14+
15+
// Context stored in state
16+
type DisregardRecommendationContext = {
17+
recommendation: Recommendation;
18+
};
19+
20+
// Rules for how to respond based on reason - static response or function
21+
type RuleResponse = PromptResponse | ((ctx: PromptContext) => PromptResponse);
22+
23+
const rules: Record<Recommendation, RuleResponse> = {
24+
[Recommendation.IndicatorReportInAGI]: "Yes",
25+
};
26+
27+
registerHandler(/.*/, {
28+
onLog: async (ctx) => {
29+
let recContext: DisregardRecommendationContext | undefined;
30+
// Detect INDICATOR_REPORT not advertised in AGI Command List Report
31+
if (
32+
/INDICATOR_REPORT.+RECOMMENDED.+does not advertise.+AGI Command List Report/is.test(
33+
ctx.logText
34+
)
35+
) {
36+
recContext = {
37+
recommendation: Recommendation.IndicatorReportInAGI,
38+
};
39+
}
40+
41+
if (recContext) {
42+
ctx.state.set(DISREGARD_RECOMMENDATION_CONTEXT, recContext);
43+
}
44+
45+
return undefined;
46+
},
47+
48+
onPrompt: async (ctx) => {
49+
if (
50+
!/Is it intended to disregard the recommendation\?/i.test(ctx.promptText)
51+
) {
52+
return undefined;
53+
}
54+
55+
const context = ctx.state.get(DISREGARD_RECOMMENDATION_CONTEXT) as
56+
| DisregardRecommendationContext
57+
| undefined;
58+
if (!context) return undefined;
59+
60+
ctx.state.delete(DISREGARD_RECOMMENDATION_CONTEXT);
61+
62+
const rule = rules[context.recommendation];
63+
return typeof rule === "function" ? rule(ctx) : rule;
64+
},
65+
});

dut/zwave-js/run.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import type {
2929
SuccessResponse,
3030
ErrorResponse,
3131
ReadyNotification,
32+
NoHandlerNotification,
3233
} from "../../src/runner-ipc.ts";
3334
import {
3435
getHandlersForTest,
@@ -119,6 +120,14 @@ function sendReady(): void {
119120
ws?.send(JSON.stringify(notification));
120121
}
121122

123+
function sendNoHandlerNotification(): void {
124+
const notification: NoHandlerNotification = {
125+
jsonrpc: "2.0",
126+
method: "noHandler",
127+
};
128+
ws?.send(JSON.stringify(notification));
129+
}
130+
122131
// === Request Handlers ===
123132

124133
async function handleStart(id: number, params: StartParams): Promise<void> {
@@ -360,7 +369,8 @@ async function handleCttPrompt(
360369
}
361370
}
362371

363-
// No automatic handler - don't respond, let user input win the race
372+
// No automatic handler - notify orchestrator so it can cancel in CI mode
373+
sendNoHandlerNotification();
364374
}
365375

366376
async function handleCttLog(id: number, params: CttLogParams): Promise<void> {

src/ctt-device-proxy.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,12 +67,18 @@ export class CTTDeviceProxy {
6767
private handleConnection(config: ProxyConfig, clientSocket: net.Socket): void {
6868
console.log(c.dim(`[Proxy ${config.name}] CTT connected`));
6969

70+
// Disable Nagle's algorithm for low-latency communication
71+
clientSocket.setNoDelay(true);
72+
7073
// Connect to target device
7174
const targetSocket = net.createConnection({
7275
host: config.targetHost,
7376
port: config.targetPort,
7477
});
7578

79+
// Disable Nagle's algorithm for low-latency communication
80+
targetSocket.setNoDelay(true);
81+
7682
const connection: ProxyConnection = {
7783
clientSocket,
7884
targetSocket,

src/runner-host.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,11 @@ import {
2020
isSuccessResponse,
2121
isErrorResponse,
2222
isReadyNotification,
23+
isNoHandlerNotification,
2324
DEFAULT_IPC_PORT,
2425
IPC_PORT_ENV_VAR,
2526
} from "./runner-ipc.ts";
27+
import { cancelTestRun } from "./ctt-client.ts";
2628
import c from "ansi-colors";
2729
import * as readline from "readline";
2830

@@ -35,13 +37,16 @@ export interface RunnerHostOptions {
3537
readyTimeout?: number;
3638
/** Callback when runner process exits unexpectedly */
3739
onUnexpectedExit?: () => void;
40+
/** CI mode - cancel test run on unhandled prompts (default: auto-detect via CI env var) */
41+
ciMode?: boolean;
3842
}
3943

4044
export class RunnerHost {
4145
private runnerPath: string;
4246
private ipcPort: number;
4347
private readyTimeout: number;
4448
private onUnexpectedExit?: () => void;
49+
private ciMode: boolean;
4550

4651
private wss?: WebSocketServer;
4752
private runnerProcess?: ChildProcess;
@@ -69,6 +74,7 @@ export class RunnerHost {
6974
this.ipcPort = options.ipcPort ?? DEFAULT_IPC_PORT;
7075
this.readyTimeout = options.readyTimeout ?? 30000;
7176
this.onUnexpectedExit = options.onUnexpectedExit;
77+
this.ciMode = options.ciMode ?? !!process.env.CI;
7278

7379
// Create readline interface for user prompts
7480
this.rl = readline.createInterface({
@@ -376,6 +382,12 @@ export class RunnerHost {
376382
return;
377383
}
378384

385+
// Check for no-handler notification (no prompt handler matched)
386+
if (isNoHandlerNotification(msg)) {
387+
this.handleNoHandler();
388+
return;
389+
}
390+
379391
// Check for response to a pending request
380392
if (isSuccessResponse(msg)) {
381393
// Check if this is a response to the active prompt
@@ -433,4 +445,17 @@ export class RunnerHost {
433445
});
434446
});
435447
}
448+
449+
private async handleNoHandler(): Promise<void> {
450+
if (this.ciMode) {
451+
console.error("\n[CI] Unhandled prompt - cancelling test run to prevent hang\n");
452+
453+
try {
454+
await cancelTestRun();
455+
} catch (error) {
456+
console.error("Failed to cancel test run:", error);
457+
}
458+
}
459+
// In non-CI mode, do nothing - let user input work
460+
}
436461
}

src/runner-ipc.ts

Lines changed: 39 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,16 @@
55
* via WebSocket using these JSON-RPC message formats.
66
*/
77

8+
// === Base JSON-RPC Types ===
9+
10+
interface JsonRpcMessage {
11+
jsonrpc: "2.0";
12+
}
13+
14+
interface JsonRpcMethodMessage extends JsonRpcMessage {
15+
method: string;
16+
}
17+
818
// === Security Key Types ===
919

1020
export interface SecurityKeys {
@@ -51,36 +61,31 @@ export interface TestCaseStartedParams {
5161

5262
// === Request Messages (Orchestrator -> Runner) ===
5363

54-
export interface StartRequest {
55-
jsonrpc: "2.0";
64+
export interface StartRequest extends JsonRpcMethodMessage {
5665
id: number;
5766
method: "start";
5867
params: StartParams;
5968
}
6069

61-
export interface StopRequest {
62-
jsonrpc: "2.0";
70+
export interface StopRequest extends JsonRpcMethodMessage {
6371
id: number;
6472
method: "stop";
6573
params: Record<string, never>;
6674
}
6775

68-
export interface HandleCttPromptRequest {
69-
jsonrpc: "2.0";
76+
export interface HandleCttPromptRequest extends JsonRpcMethodMessage {
7077
id: number;
7178
method: "handleCttPrompt";
7279
params: CttPromptParams;
7380
}
7481

75-
export interface TestCaseStartedRequest {
76-
jsonrpc: "2.0";
82+
export interface TestCaseStartedRequest extends JsonRpcMethodMessage {
7783
id: number;
7884
method: "testCaseStarted";
7985
params: TestCaseStartedParams;
8086
}
8187

82-
export interface HandleCttLogRequest {
83-
jsonrpc: "2.0";
88+
export interface HandleCttLogRequest extends JsonRpcMethodMessage {
8489
id: number;
8590
method: "handleCttLog";
8691
params: CttLogParams;
@@ -90,14 +95,12 @@ export type IpcRequest = StartRequest | StopRequest | HandleCttPromptRequest | T
9095

9196
// === Response Messages (Runner -> Orchestrator) ===
9297

93-
export interface SuccessResponse {
94-
jsonrpc: "2.0";
98+
export interface SuccessResponse extends JsonRpcMessage {
9599
id: number;
96100
result: string; // "ok" for start/stop, button name for handleCttPrompt
97101
}
98102

99-
export interface ErrorResponse {
100-
jsonrpc: "2.0";
103+
export interface ErrorResponse extends JsonRpcMessage {
101104
id: number;
102105
error: {
103106
code: number;
@@ -109,50 +112,48 @@ export type IpcResponse = SuccessResponse | ErrorResponse;
109112

110113
// === Notification Messages (Runner -> Orchestrator) ===
111114

112-
export interface ReadyNotification {
113-
jsonrpc: "2.0";
115+
export interface ReadyNotification extends JsonRpcMethodMessage {
114116
method: "ready";
115117
params: {
116118
name: string; // Runner name for logging
117119
};
118120
}
119121

120-
export type IpcNotification = ReadyNotification;
122+
export interface NoHandlerNotification extends JsonRpcMethodMessage {
123+
method: "noHandler";
124+
}
125+
126+
export type IpcNotification = ReadyNotification | NoHandlerNotification;
121127

122128
// === Type Guards ===
123129

124-
export function isSuccessResponse(msg: unknown): msg is SuccessResponse {
130+
function isJsonRpcMessage(msg: unknown): msg is JsonRpcMessage {
125131
return (
126132
typeof msg === "object" &&
127133
msg !== null &&
128134
"jsonrpc" in msg &&
129-
msg.jsonrpc === "2.0" &&
130-
"id" in msg &&
131-
"result" in msg
135+
msg.jsonrpc === "2.0"
132136
);
133137
}
134138

139+
function isJsonRpcMethodMessage(msg: unknown): msg is JsonRpcMethodMessage {
140+
return isJsonRpcMessage(msg) && "method" in msg;
141+
}
142+
143+
export function isSuccessResponse(msg: unknown): msg is SuccessResponse {
144+
return isJsonRpcMessage(msg) && "id" in msg && "result" in msg;
145+
}
146+
135147
export function isErrorResponse(msg: unknown): msg is ErrorResponse {
136-
return (
137-
typeof msg === "object" &&
138-
msg !== null &&
139-
"jsonrpc" in msg &&
140-
msg.jsonrpc === "2.0" &&
141-
"id" in msg &&
142-
"error" in msg
143-
);
148+
return isJsonRpcMessage(msg) && "id" in msg && "error" in msg;
144149
}
145150

146151
export function isReadyNotification(msg: unknown): msg is ReadyNotification {
147-
return (
148-
typeof msg === "object" &&
149-
msg !== null &&
150-
"jsonrpc" in msg &&
151-
msg.jsonrpc === "2.0" &&
152-
"method" in msg &&
153-
msg.method === "ready" &&
154-
"params" in msg
155-
);
152+
return isJsonRpcMethodMessage(msg) && msg.method === "ready" && "params" in msg;
153+
}
154+
155+
export function isNoHandlerNotification(msg: unknown): msg is NoHandlerNotification {
156+
return isJsonRpcMethodMessage(msg) && msg.method === "noHandler";
156157
}
157158

158159
// === Constants ===

0 commit comments

Comments
 (0)