pi-rtk-optimizer 0.7.0 → 0.8.0

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.
@@ -1,118 +1,187 @@
1
- import assert from "node:assert/strict";
2
-
3
- import { computeRewriteDecision } from "./command-rewriter.ts";
4
- import { cloneDefaultConfig, runTest } from "./test-helpers.ts";
5
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
6
-
7
- function createMockPi(execResult: { code: number; stdout?: string; stderr?: string }): ExtensionAPI {
8
- return { exec: async () => execResult } as unknown as ExtensionAPI;
9
- }
10
-
11
- runTest("empty command unchanged", async () => {
12
- const config = cloneDefaultConfig();
13
- const decision = await computeRewriteDecision("", config, createMockPi({ code: 1 }));
14
- assert.equal(decision.changed, false);
15
- assert.equal(decision.reason, "empty");
16
- });
17
-
18
- runTest("already rtk unchanged", async () => {
19
- const config = cloneDefaultConfig();
20
- const decision = await computeRewriteDecision("rtk status", config, createMockPi({ code: 1 }));
21
- assert.equal(decision.changed, false);
22
- assert.equal(decision.reason, "already_rtk");
23
- });
24
-
25
- runTest("rtk unsupported heredoc result leaves command unchanged", async () => {
26
- const config = cloneDefaultConfig();
27
- const decision = await computeRewriteDecision("cat <<EOF", config, createMockPi({ code: 1 }));
28
- assert.equal(decision.changed, false);
29
- assert.equal(decision.reason, "no_match");
30
- });
31
-
32
- runTest("quoted heredoc marker is delegated to RTK rewrite", async () => {
33
- const config = cloneDefaultConfig();
34
- const command = 'echo "<<not heredoc" && git status';
35
- const decision = await computeRewriteDecision(
36
- command,
37
- config,
38
- createMockPi({ code: 3, stdout: 'echo "<<not heredoc" && rtk git status' }),
39
- );
40
- assert.equal(decision.changed, true);
41
- assert.equal(decision.rewrittenCommand, 'echo "<<not heredoc" && rtk git status');
42
- assert.equal(decision.reason, "ok");
43
- });
44
-
45
- runTest("legacy category toggles do not pre-filter RTK rewrite source of truth", async () => {
46
- const config = { ...cloneDefaultConfig(), rewriteGitGithub: false };
47
- const decision = await computeRewriteDecision("git status", config, createMockPi({ code: 3, stdout: "rtk git status" }));
48
- assert.equal(decision.changed, true);
49
- assert.equal(decision.rewrittenCommand, "rtk git status");
50
- assert.equal(decision.reason, "ok");
51
- });
52
-
53
- runTest("rtk exit 0 rewrites", async () => {
54
- const config = cloneDefaultConfig();
55
- const decision = await computeRewriteDecision("git status", config, createMockPi({ code: 0, stdout: "rtk git status" }));
56
- assert.equal(decision.changed, true);
57
- assert.equal(decision.rewrittenCommand, "rtk git status");
58
- assert.equal(decision.reason, "ok");
59
- });
60
-
61
- runTest("rtk exit 3 rewrites", async () => {
62
- const config = cloneDefaultConfig();
63
- const decision = await computeRewriteDecision("git status", config, createMockPi({ code: 3, stdout: "rtk git status" }));
64
- assert.equal(decision.changed, true);
65
- assert.equal(decision.rewrittenCommand, "rtk git status");
66
- assert.equal(decision.reason, "ok");
67
- });
68
-
69
- runTest("exit 1 leaves unchanged", async () => {
70
- const config = cloneDefaultConfig();
71
- const decision = await computeRewriteDecision("git status", config, createMockPi({ code: 1 }));
72
- assert.equal(decision.changed, false);
73
- assert.equal(decision.reason, "no_match");
74
- });
75
-
76
- runTest("exit 2 leaves unchanged", async () => {
77
- const config = cloneDefaultConfig();
78
- const decision = await computeRewriteDecision("git status", config, createMockPi({ code: 2, stderr: "denied" }));
79
- assert.equal(decision.changed, false);
80
- assert.equal(decision.reason, "no_match");
81
- });
82
-
83
- runTest("unknown category passes through to RTK", async () => {
84
- const config = cloneDefaultConfig();
85
- const pi = createMockPi({ code: 0, stdout: "rtk custom" });
86
- const decision = await computeRewriteDecision("custom-cmd", config, pi);
87
- assert.equal(decision.changed, true);
88
- assert.equal(decision.rewrittenCommand, "rtk custom");
89
- assert.equal(decision.reason, "ok");
90
- });
91
-
92
- runTest("exec error/timeout leaves unchanged", async () => {
93
- const config = cloneDefaultConfig();
94
- const pi = {
95
- exec: async () => {
96
- throw new Error("timeout");
97
- },
98
- } as unknown as ExtensionAPI;
99
- const decision = await computeRewriteDecision("git status", config, pi);
100
- assert.equal(decision.changed, false);
101
- assert.equal(decision.reason, "no_match");
102
- });
103
-
104
- runTest("compound commands forwarded to RTK", async () => {
105
- const config = cloneDefaultConfig();
106
- let capturedArgs: string[] = [];
107
- const pi = {
108
- exec: async (_cmd: string, args: string[]) => {
109
- capturedArgs = args;
110
- return { code: 0, stdout: "rtk result" };
111
- },
112
- } as unknown as ExtensionAPI;
113
- const decision = await computeRewriteDecision("git status && cargo test", config, pi);
114
- assert.equal(decision.changed, true);
115
- assert.deepEqual(capturedArgs, ["rewrite", "git status && cargo test"]);
116
- });
117
-
118
- console.log("All command-rewriter tests passed.");
1
+ import assert from "node:assert/strict";
2
+
3
+ import { computeRewriteDecision } from "./command-rewriter.ts";
4
+ import { resolveRtkRewrite } from "./rtk-rewrite-provider.ts";
5
+ import { cloneDefaultConfig, runTest } from "./test-helpers.ts";
6
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
7
+
8
+ function createMockPi(execResult: { code: number; stdout?: string; stderr?: string }): ExtensionAPI {
9
+ return {
10
+ exec: async (command: string) => {
11
+ if (command === "which" || command === "where") {
12
+ return { code: 0, stdout: "/usr/local/bin/rtk\n", stderr: "" };
13
+ }
14
+ return execResult;
15
+ },
16
+ } as unknown as ExtensionAPI;
17
+ }
18
+
19
+ await runTest("rtk rewrite uses resolved POSIX executable path", async () => {
20
+ const calls: Array<{ command: string; args: string[] }> = [];
21
+ const pi = {
22
+ exec: async (command: string, args: string[]) => {
23
+ calls.push({ command, args });
24
+ if (command === "which") {
25
+ return { code: 0, stdout: "/opt/rtk/bin/rtk\n", stderr: "" };
26
+ }
27
+ return { code: 3, stdout: "rtk git status", stderr: "" };
28
+ },
29
+ } as unknown as ExtensionAPI;
30
+
31
+ const result = await resolveRtkRewrite(pi, "git status", { platform: "linux" });
32
+
33
+ assert.equal(result.changed, true);
34
+ assert.equal(result.rewrittenCommand, "rtk git status");
35
+ assert.equal(result.executableResolution?.resolvedPath, "/opt/rtk/bin/rtk");
36
+ assert.deepEqual(calls.map((call) => call.command), ["which", "/opt/rtk/bin/rtk"]);
37
+ });
38
+
39
+ await runTest("rtk rewrite uses resolved Windows executable path", async () => {
40
+ const calls: Array<{ command: string; args: string[] }> = [];
41
+ const pi = {
42
+ exec: async (command: string, args: string[]) => {
43
+ calls.push({ command, args });
44
+ if (command === "where") {
45
+ return { code: 0, stdout: "C:\\Tools\\rtk.exe\r\nC:\\Other\\rtk.exe\r\n", stderr: "" };
46
+ }
47
+ return { code: 3, stdout: "rtk git status", stderr: "" };
48
+ },
49
+ } as unknown as ExtensionAPI;
50
+
51
+ const result = await resolveRtkRewrite(pi, "git status", { platform: "win32" });
52
+
53
+ assert.equal(result.changed, true);
54
+ assert.equal(result.executableResolution?.resolvedPath, "C:\\Tools\\rtk.exe");
55
+ assert.deepEqual(calls.map((call) => call.command), ["where", "C:\\Tools\\rtk.exe"]);
56
+ });
57
+
58
+ await runTest("rtk rewrite preserves behavior when executable path resolution fails", async () => {
59
+ const calls: Array<{ command: string; args: string[] }> = [];
60
+ const pi = {
61
+ exec: async (command: string, args: string[]) => {
62
+ calls.push({ command, args });
63
+ if (command === "which") {
64
+ return { code: 1, stdout: "", stderr: "not found" };
65
+ }
66
+ return { code: 3, stdout: "rtk git status", stderr: "" };
67
+ },
68
+ } as unknown as ExtensionAPI;
69
+
70
+ const result = await resolveRtkRewrite(pi, "git status", { platform: "linux" });
71
+
72
+ assert.equal(result.changed, true);
73
+ assert.equal(result.executableResolution?.command, "rtk");
74
+ assert.ok(result.executableResolution?.warning?.includes("which failed"));
75
+ assert.deepEqual(calls.map((call) => call.command), ["which", "rtk"]);
76
+ });
77
+
78
+ await runTest("empty command unchanged", async () => {
79
+ const config = cloneDefaultConfig();
80
+ const decision = await computeRewriteDecision("", config, createMockPi({ code: 1 }));
81
+ assert.equal(decision.changed, false);
82
+ assert.equal(decision.reason, "empty");
83
+ });
84
+
85
+ await runTest("already rtk unchanged", async () => {
86
+ const config = cloneDefaultConfig();
87
+ const decision = await computeRewriteDecision("rtk status", config, createMockPi({ code: 1 }));
88
+ assert.equal(decision.changed, false);
89
+ assert.equal(decision.reason, "already_rtk");
90
+ });
91
+
92
+ await runTest("rtk unsupported heredoc result leaves command unchanged", async () => {
93
+ const config = cloneDefaultConfig();
94
+ const decision = await computeRewriteDecision("cat <<EOF", config, createMockPi({ code: 1 }));
95
+ assert.equal(decision.changed, false);
96
+ assert.equal(decision.reason, "no_match");
97
+ });
98
+
99
+ await runTest("quoted heredoc marker is delegated to RTK rewrite", async () => {
100
+ const config = cloneDefaultConfig();
101
+ const command = 'echo "<<not heredoc" && git status';
102
+ const decision = await computeRewriteDecision(
103
+ command,
104
+ config,
105
+ createMockPi({ code: 3, stdout: 'echo "<<not heredoc" && rtk git status' }),
106
+ );
107
+ assert.equal(decision.changed, true);
108
+ assert.equal(decision.rewrittenCommand, 'echo "<<not heredoc" && rtk git status');
109
+ assert.equal(decision.reason, "ok");
110
+ });
111
+
112
+ await runTest("legacy category toggles do not pre-filter RTK rewrite source of truth", async () => {
113
+ const config = { ...cloneDefaultConfig(), rewriteGitGithub: false };
114
+ const decision = await computeRewriteDecision("git status", config, createMockPi({ code: 3, stdout: "rtk git status" }));
115
+ assert.equal(decision.changed, true);
116
+ assert.equal(decision.rewrittenCommand, "rtk git status");
117
+ assert.equal(decision.reason, "ok");
118
+ });
119
+
120
+ await runTest("rtk exit 0 rewrites", async () => {
121
+ const config = cloneDefaultConfig();
122
+ const decision = await computeRewriteDecision("git status", config, createMockPi({ code: 0, stdout: "rtk git status" }));
123
+ assert.equal(decision.changed, true);
124
+ assert.equal(decision.rewrittenCommand, "rtk git status");
125
+ assert.equal(decision.reason, "ok");
126
+ });
127
+
128
+ await runTest("rtk exit 3 rewrites", async () => {
129
+ const config = cloneDefaultConfig();
130
+ const decision = await computeRewriteDecision("git status", config, createMockPi({ code: 3, stdout: "rtk git status" }));
131
+ assert.equal(decision.changed, true);
132
+ assert.equal(decision.rewrittenCommand, "rtk git status");
133
+ assert.equal(decision.reason, "ok");
134
+ });
135
+
136
+ await runTest("exit 1 leaves unchanged", async () => {
137
+ const config = cloneDefaultConfig();
138
+ const decision = await computeRewriteDecision("git status", config, createMockPi({ code: 1 }));
139
+ assert.equal(decision.changed, false);
140
+ assert.equal(decision.reason, "no_match");
141
+ });
142
+
143
+ await runTest("exit 2 leaves unchanged and surfaces RTK detail", async () => {
144
+ const config = cloneDefaultConfig();
145
+ const decision = await computeRewriteDecision("git status", config, createMockPi({ code: 2, stderr: "denied" }));
146
+ assert.equal(decision.changed, false);
147
+ assert.equal(decision.reason, "no_match");
148
+ assert.equal(decision.warning, "denied");
149
+ });
150
+
151
+ await runTest("unknown category passes through to RTK", async () => {
152
+ const config = cloneDefaultConfig();
153
+ const pi = createMockPi({ code: 0, stdout: "rtk custom" });
154
+ const decision = await computeRewriteDecision("custom-cmd", config, pi);
155
+ assert.equal(decision.changed, true);
156
+ assert.equal(decision.rewrittenCommand, "rtk custom");
157
+ assert.equal(decision.reason, "ok");
158
+ });
159
+
160
+ await runTest("exec error/timeout leaves unchanged and surfaces error detail", async () => {
161
+ const config = cloneDefaultConfig();
162
+ const pi = {
163
+ exec: async () => {
164
+ throw new Error("timeout");
165
+ },
166
+ } as unknown as ExtensionAPI;
167
+ const decision = await computeRewriteDecision("git status", config, pi);
168
+ assert.equal(decision.changed, false);
169
+ assert.equal(decision.reason, "no_match");
170
+ assert.equal(decision.warning, "timeout");
171
+ });
172
+
173
+ await runTest("compound commands forwarded to RTK", async () => {
174
+ const config = cloneDefaultConfig();
175
+ let capturedArgs: string[] = [];
176
+ const pi = {
177
+ exec: async (_cmd: string, args: string[]) => {
178
+ capturedArgs = args;
179
+ return { code: 0, stdout: "rtk result" };
180
+ },
181
+ } as unknown as ExtensionAPI;
182
+ const decision = await computeRewriteDecision("git status && cargo test", config, pi);
183
+ assert.equal(decision.changed, true);
184
+ assert.deepEqual(capturedArgs, ["rewrite", "git status && cargo test"]);
185
+ });
186
+
187
+ console.log("All command-rewriter tests passed.");
@@ -1,43 +1,46 @@
1
- import { resolveRtkRewrite } from "./rtk-rewrite-provider.js";
2
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
3
- import type { RtkIntegrationConfig } from "./types.js";
4
-
5
- export interface RewriteDecision {
6
- changed: boolean;
7
- originalCommand: string;
8
- rewrittenCommand: string;
9
- reason: "ok" | "empty" | "already_rtk" | "no_match";
10
- }
11
-
12
- export async function computeRewriteDecision(
13
- command: string,
14
- _config: RtkIntegrationConfig,
15
- pi: ExtensionAPI,
16
- ): Promise<RewriteDecision> {
17
- if (!command || !command.trim()) {
18
- return { changed: false, originalCommand: command, rewrittenCommand: command, reason: "empty" };
19
- }
20
-
21
- const trimmedStart = command.trimStart();
22
- if (trimmedStart === "rtk" || trimmedStart.startsWith("rtk ")) {
23
- return { changed: false, originalCommand: command, rewrittenCommand: command, reason: "already_rtk" };
24
- }
25
-
26
- const result = await resolveRtkRewrite(pi, command);
27
-
28
- if (result.changed && result.rewrittenCommand) {
29
- return {
30
- changed: true,
31
- originalCommand: command,
32
- rewrittenCommand: result.rewrittenCommand,
33
- reason: "ok",
34
- };
35
- }
36
-
37
- return {
38
- changed: false,
39
- originalCommand: command,
40
- rewrittenCommand: command,
41
- reason: "no_match",
42
- };
43
- }
1
+ import { resolveRtkRewrite, type RtkRewriteProviderOptions } from "./rtk-rewrite-provider.js";
2
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
3
+ import type { RtkIntegrationConfig } from "./types.js";
4
+
5
+ export interface RewriteDecision {
6
+ changed: boolean;
7
+ originalCommand: string;
8
+ rewrittenCommand: string;
9
+ reason: "ok" | "empty" | "already_rtk" | "no_match";
10
+ warning?: string;
11
+ }
12
+
13
+ export async function computeRewriteDecision(
14
+ command: string,
15
+ _config: RtkIntegrationConfig,
16
+ pi: ExtensionAPI,
17
+ rewriteOptions: RtkRewriteProviderOptions = {},
18
+ ): Promise<RewriteDecision> {
19
+ if (!command || !command.trim()) {
20
+ return { changed: false, originalCommand: command, rewrittenCommand: command, reason: "empty" };
21
+ }
22
+
23
+ const trimmedStart = command.trimStart();
24
+ if (trimmedStart === "rtk" || trimmedStart.startsWith("rtk ")) {
25
+ return { changed: false, originalCommand: command, rewrittenCommand: command, reason: "already_rtk" };
26
+ }
27
+
28
+ const result = await resolveRtkRewrite(pi, command, rewriteOptions);
29
+
30
+ if (result.changed && result.rewrittenCommand) {
31
+ return {
32
+ changed: true,
33
+ originalCommand: command,
34
+ rewrittenCommand: result.rewrittenCommand,
35
+ reason: "ok",
36
+ };
37
+ }
38
+
39
+ return {
40
+ changed: false,
41
+ originalCommand: command,
42
+ rewrittenCommand: command,
43
+ reason: "no_match",
44
+ warning: result.error,
45
+ };
46
+ }
@@ -3,31 +3,45 @@ import { mock } from "bun:test";
3
3
 
4
4
  import { cloneDefaultConfig, runTest } from "./test-helpers.ts";
5
5
 
6
- mock.module("@mariozechner/pi-coding-agent", () => ({
6
+ mock.module("@earendil-works/pi-coding-agent", () => ({
7
7
  getAgentDir: () => "/tmp/.pi/agent",
8
8
  getSettingsListTheme: () => ({}),
9
9
  }));
10
10
 
11
- mock.module("@mariozechner/pi-tui", () => ({
12
- Box: class {},
11
+ const settingsListInputs: string[] = [];
12
+ const settingsListUpdates: Array<{ id: string; value: string }> = [];
13
+
14
+ mock.module("@earendil-works/pi-tui", () => ({
15
+ Box: class {
16
+ addChild(): void {}
17
+ },
13
18
  Container: class {
14
19
  addChild(): void {}
15
20
  render(): string[] {
16
- return [];
21
+ return ["settings-content"];
17
22
  }
18
23
  invalidate(): void {}
19
24
  },
20
25
  SettingsList: class {
21
- handleInput(): void {}
22
- updateValue(): void {}
26
+ handleInput(data: string): void {
27
+ settingsListInputs.push(data);
28
+ }
29
+ updateValue(id: string, value: string): void {
30
+ settingsListUpdates.push({ id, value });
31
+ }
23
32
  },
24
33
  Spacer: class {},
25
34
  Text: class {},
26
- truncateToWidth: (text: string) => text,
35
+ truncateToWidth: (text: string, width: number) => text.slice(0, width),
27
36
  visibleWidth: (text: string) => text.length,
28
37
  }));
29
38
 
39
+ function stripAnsi(text: string): string {
40
+ return text.replace(/\x1b\[[0-9;]*m/g, "");
41
+ }
42
+
30
43
  const { registerRtkIntegrationCommand } = await import("./config-modal.ts");
44
+ const { ZellijModal, ZellijSettingsModal } = await import("./zellij-modal.ts");
31
45
  const { getRtkArgumentCompletions } = await import("./command-completions.ts");
32
46
 
33
47
  type Notification = { message: string; level: "info" | "warning" | "error" };
@@ -62,6 +76,60 @@ function lastNotification(notifications: Notification[]): Notification {
62
76
  return notifications[notifications.length - 1] as Notification;
63
77
  }
64
78
 
79
+ function createThemeStub(): { fg: (_name: string, text: string) => string; bold: (text: string) => string } {
80
+ return {
81
+ fg: (_name: string, text: string) => text,
82
+ bold: (text: string) => text,
83
+ };
84
+ }
85
+
86
+ runTest("zellij settings modal renders overlay frame and delegates non-enter input", () => {
87
+ settingsListInputs.length = 0;
88
+ settingsListUpdates.length = 0;
89
+ const settingsModal = new ZellijSettingsModal(
90
+ {
91
+ title: "RTK Integration Settings",
92
+ settings: [
93
+ {
94
+ id: "enabled",
95
+ label: "Enabled",
96
+ description: "Enable integration",
97
+ currentValue: "on",
98
+ values: ["on", "off"],
99
+ },
100
+ ],
101
+ onChange: () => {},
102
+ onClose: () => {},
103
+ helpText: "Esc: close",
104
+ },
105
+ createThemeStub() as never,
106
+ );
107
+ const modal = new ZellijModal(settingsModal, {
108
+ titleBar: {
109
+ left: { text: "RTK Integration Settings", maxWidth: 30, color: "accent" },
110
+ right: { text: "pi-rtk-optimizer", maxWidth: 20, color: "dim" },
111
+ },
112
+ helpUndertitle: { text: "Esc: close", color: "dim" },
113
+ overlay: { anchor: "center", width: 86, maxHeight: "85%", margin: 1 },
114
+ });
115
+
116
+ const rendered = modal.renderModal(86);
117
+ settingsModal.handleInput("\r");
118
+ settingsModal.handleInput("j");
119
+ settingsModal.updateValue("enabled", "off");
120
+
121
+ assert.equal(rendered.visibleWidth, 86);
122
+ assert.equal(rendered.contentWidth, 82);
123
+ assert.ok(stripAnsi(rendered.lines[0] ?? "").includes("RTK Integration Settings"));
124
+ assert.ok(stripAnsi(rendered.lines[rendered.lines.length - 1] ?? "").includes("Esc: close"));
125
+ assert.deepEqual(modal.getOverlayOptions(), {
126
+ overlay: true,
127
+ overlayOptions: { anchor: "center", width: 86, maxHeight: "85%", margin: 1 },
128
+ });
129
+ assert.deepEqual(settingsListInputs, ["j"]);
130
+ assert.deepEqual(settingsListUpdates, [{ id: "enabled", value: "off" }]);
131
+ });
132
+
65
133
  runTest("command completions return top-level and filtered RTK subcommands", () => {
66
134
  const topLevel = getRtkArgumentCompletions("");
67
135
  assert.ok(Array.isArray(topLevel));
@@ -77,12 +145,7 @@ runTest("command completions return top-level and filtered RTK subcommands", ()
77
145
  assert.equal(getRtkArgumentCompletions("zzz"), null);
78
146
  });
79
147
 
80
- async function runAsyncTest(name: string, testFn: () => Promise<void>): Promise<void> {
81
- await testFn();
82
- console.log(`[PASS] ${name}`);
83
- }
84
-
85
- await runAsyncTest("config modal command handlers route RTK subcommands to controller actions", async () => {
148
+ await runTest("config modal command handlers route RTK subcommands to controller actions", async () => {
86
149
  const config = cloneDefaultConfig();
87
150
  const controllerState = {
88
151
  config,
@@ -101,7 +164,7 @@ await runAsyncTest("config modal command handlers route RTK subcommands to contr
101
164
  getRuntimeStatus: () => ({ rtkAvailable: false, lastError: "not found" }),
102
165
  refreshRuntimeStatus: async () => {
103
166
  controllerState.refreshed += 1;
104
- return { rtkAvailable: false, lastError: "not found" };
167
+ return { rtkAvailable: true, rtkExecutablePath: "C:/Tools/rtk.exe" };
105
168
  },
106
169
  getMetricsSummary: () => "metrics summary",
107
170
  clearMetrics: () => {
@@ -110,15 +173,16 @@ await runAsyncTest("config modal command handlers route RTK subcommands to contr
110
173
  };
111
174
 
112
175
  let registeredName = "";
113
- let definition: {
176
+ type RegisteredCommandDefinition = {
114
177
  description: string;
115
178
  getArgumentCompletions?: (argumentPrefix: string) => Array<{ value: string; label: string; description?: string }> | null;
116
179
  handler: (args: string, ctx: CommandContextStub) => Promise<void>;
117
- } | null = null;
180
+ };
181
+ let definition: RegisteredCommandDefinition | undefined;
118
182
 
119
183
  registerRtkIntegrationCommand(
120
184
  {
121
- registerCommand(name: string, nextDefinition: typeof definition) {
185
+ registerCommand(name: string, nextDefinition: RegisteredCommandDefinition) {
122
186
  registeredName = name;
123
187
  definition = nextDefinition;
124
188
  },
@@ -127,44 +191,46 @@ await runAsyncTest("config modal command handlers route RTK subcommands to contr
127
191
  );
128
192
 
129
193
  assert.equal(registeredName, "rtk");
130
- assert.ok(definition !== null);
131
- assert.ok((definition?.description ?? "").includes("Configure RTK rewrite"));
132
- assert.ok(typeof definition?.getArgumentCompletions === "function");
194
+ if (!definition) {
195
+ throw new Error("Expected /rtk command definition to be registered");
196
+ }
197
+ assert.ok(definition.description.includes("Configure RTK rewrite"));
198
+ assert.ok(typeof definition.getArgumentCompletions === "function");
133
199
 
134
200
  const infoCtx = createNotifyContext(true);
135
- await definition?.handler("help", infoCtx.ctx);
201
+ await definition.handler("help", infoCtx.ctx);
136
202
  assert.ok(lastNotification(infoCtx.notifications).message.includes("Usage: /rtk"));
137
203
 
138
- await definition?.handler("show", infoCtx.ctx);
204
+ await definition.handler("show", infoCtx.ctx);
139
205
  assert.ok(lastNotification(infoCtx.notifications).message.includes("mode=rewrite"));
140
206
  assert.ok(lastNotification(infoCtx.notifications).message.includes("rewriteSource=rtk"));
141
207
  assert.equal(lastNotification(infoCtx.notifications).message.includes("categories="), false);
142
208
 
143
- await definition?.handler("path", infoCtx.ctx);
209
+ await definition.handler("path", infoCtx.ctx);
144
210
  assert.equal(lastNotification(infoCtx.notifications).message, "rtk config: C:/tmp/pi-rtk-optimizer/config.json");
145
211
 
146
- await definition?.handler("verify", infoCtx.ctx);
212
+ await definition.handler("verify", infoCtx.ctx);
147
213
  assert.equal(controllerState.refreshed, 1);
148
- assert.equal(lastNotification(infoCtx.notifications).level, "warning");
149
- assert.ok(lastNotification(infoCtx.notifications).message.includes("not available: not found"));
214
+ assert.equal(lastNotification(infoCtx.notifications).level, "info");
215
+ assert.ok(lastNotification(infoCtx.notifications).message.includes("available at C:/Tools/rtk.exe"));
150
216
 
151
- await definition?.handler("stats", infoCtx.ctx);
217
+ await definition.handler("stats", infoCtx.ctx);
152
218
  assert.equal(lastNotification(infoCtx.notifications).message, "metrics summary");
153
219
 
154
- await definition?.handler("clear-stats", infoCtx.ctx);
220
+ await definition.handler("clear-stats", infoCtx.ctx);
155
221
  assert.equal(controllerState.cleared, 1);
156
222
  assert.equal(lastNotification(infoCtx.notifications).message, "RTK metrics cleared.");
157
223
 
158
- await definition?.handler("reset", infoCtx.ctx);
224
+ await definition.handler("reset", infoCtx.ctx);
159
225
  assert.equal(controllerState.lastSavedMode, "rewrite");
160
226
  assert.equal(lastNotification(infoCtx.notifications).message, "RTK integration settings reset to defaults.");
161
227
 
162
- await definition?.handler("unknown", infoCtx.ctx);
228
+ await definition.handler("unknown", infoCtx.ctx);
163
229
  assert.equal(lastNotification(infoCtx.notifications).level, "warning");
164
230
  assert.ok(lastNotification(infoCtx.notifications).message.includes("Usage: /rtk"));
165
231
 
166
232
  const headlessCtx = createNotifyContext(false);
167
- await definition?.handler("", headlessCtx.ctx);
233
+ await definition.handler("", headlessCtx.ctx);
168
234
  assert.equal(lastNotification(headlessCtx.notifications).message, "/rtk requires interactive TUI mode.");
169
235
  });
170
236