gentle-pi 2.4.0 → 2.5.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.
- package/README.md +170 -12
- package/assets/agents/gentle-ai-worker.md +9 -0
- package/assets/orchestrator-delegation.md +19 -9
- package/assets/orchestrator.md +5 -5
- package/docs/delegated-verification.md +25 -0
- package/docs/telemetry.md +38 -0
- package/extensions/ask-user-choice.ts +26 -20
- package/extensions/codegraph-tools.ts +94 -5
- package/extensions/gentle-agents.ts +588 -0
- package/extensions/gentle-ai.ts +898 -79
- package/extensions/gentle-shell.ts +547 -0
- package/extensions/gentle-todo.ts +199 -0
- package/extensions/quiet-tools.ts +1 -1
- package/lib/agents-config.ts +318 -0
- package/lib/agents-history.ts +80 -0
- package/lib/agents-protocol.ts +429 -0
- package/lib/agents-runner.ts +490 -0
- package/lib/agents-transcript.ts +87 -0
- package/lib/agents-view.ts +557 -0
- package/lib/agents-widget.ts +222 -0
- package/lib/gentle-ai-renderer.ts +142 -26
- package/lib/native-choice-list.ts +194 -0
- package/lib/native-fullscreen-interaction.ts +47 -0
- package/lib/native-pointer-region.ts +164 -0
- package/lib/native-review-cli.ts +88 -12
- package/lib/review-candidate-view-owner.ts +177 -0
- package/lib/review-candidate-view.ts +127 -35
- package/lib/review-consent-ui.ts +65 -0
- package/lib/review-integration-v2.ts +58 -8
- package/lib/review-last-event-controller.ts +1 -0
- package/lib/review-relay-contract.ts +11 -0
- package/lib/review-repository.ts +2 -2
- package/lib/review-risk-assessment.ts +339 -0
- package/lib/review-session-standing-permission-ipc.ts +309 -0
- package/lib/review-session-standing-permission.ts +219 -0
- package/lib/shell-bar.ts +138 -0
- package/lib/shell-card.ts +136 -0
- package/lib/shell-changes-view.ts +205 -0
- package/lib/shell-changes.ts +210 -0
- package/lib/shell-gauge.ts +40 -0
- package/lib/shell-prompt.ts +119 -0
- package/lib/shell-todo.ts +280 -0
- package/lib/shell-usage-view.ts +76 -0
- package/lib/shell-usage.ts +246 -0
- package/lib/telemetry-trigger.ts +151 -0
- package/package.json +4 -4
- package/runtime/native-review-cli.mjs +87 -11
- package/runtime/review-integration-v2.mjs +58 -8
- package/runtime/review-relay-contract.mjs +11 -0
- package/runtime/review-risk-assessment.mjs +340 -0
- package/runtime/telemetry-trigger.mjs +152 -0
- package/scripts/build-runtime-modules.mjs +2 -0
- package/scripts/gentle-ai-installer.mjs +10 -10
- package/scripts/test-packed-runner.mjs +22 -0
- package/scripts/verify-package-files.mjs +6 -2
- package/skills/_shared/review-ledger-contract.md +3 -1
- package/tests/agents-config.test.ts +143 -0
- package/tests/agents-fake-child.ts +52 -0
- package/tests/agents-history.test.ts +54 -0
- package/tests/agents-protocol.test.ts +153 -0
- package/tests/agents-runner-process.test.ts +111 -0
- package/tests/agents-runner.test.ts +402 -0
- package/tests/agents-transcript.test.ts +30 -0
- package/tests/agents-view.test.ts +274 -0
- package/tests/agents-widget.test.ts +111 -0
- package/tests/ask-user-choice.test.ts +157 -3
- package/tests/codegraph-tools.test.ts +110 -1
- package/tests/devbinary/native-review-parity.devtest.ts +108 -0
- package/tests/fixtures/agents-process-child.mjs +23 -0
- package/tests/gentle-agents.test.ts +741 -0
- package/tests/gentle-ai-binary.test.ts +1 -1
- package/tests/gentle-ai-installer.test.ts +47 -47
- package/tests/gentle-ai-renderer.test.ts +65 -0
- package/tests/gentle-ai.test.ts +28 -12
- package/tests/gentle-card-text.ts +35 -0
- package/tests/gentle-shell.test.ts +527 -0
- package/tests/gentle-todo.test.ts +182 -0
- package/tests/native-choice-list.test.ts +202 -0
- package/tests/native-fullscreen-interaction.test.ts +125 -0
- package/tests/native-pointer-region.test.ts +245 -0
- package/tests/native-review-capability-contract.test.ts +16 -1
- package/tests/native-review-cli.test.ts +40 -0
- package/tests/native-review-consent.test.ts +91 -0
- package/tests/native-review-parity-runtime.test.ts +8 -2
- package/tests/native-review-parity.test.ts +29 -22
- package/tests/orchestrator-budget.test.ts +69 -0
- package/tests/orchestrator-rdd-ownership.test.ts +9 -0
- package/tests/package-manifest.test.ts +17 -6
- package/tests/quiet-tool-rendering.test.ts +96 -37
- package/tests/rdd-aware-verification-contract.test.ts +216 -0
- package/tests/rdd-status-line.test.ts +286 -0
- package/tests/review-candidate-view.test.ts +452 -6
- package/tests/review-contract-prompt.test.ts +3 -0
- package/tests/review-controller-native-recovery.test.ts +29 -4
- package/tests/review-controller-native-routing.test.ts +321 -4
- package/tests/review-controller-workspace-root.test.ts +45 -2
- package/tests/review-controller.test.ts +25 -0
- package/tests/review-host-relay-routing.test.ts +20 -4
- package/tests/review-integration-v2.test.ts +112 -0
- package/tests/review-last-event-closure.test.ts +7 -2
- package/tests/review-relay-contract.test.ts +26 -0
- package/tests/review-repository.test.ts +28 -1
- package/tests/review-risk-assessment.test.ts +626 -0
- package/tests/review-session-standing-permission-controller.test.ts +608 -0
- package/tests/review-session-standing-permission-ipc.test.ts +233 -0
- package/tests/review-session-standing-permission-runtime.test.ts +212 -0
- package/tests/review-session-standing-permission.test.ts +126 -0
- package/tests/shell-bar.test.ts +176 -0
- package/tests/shell-card.test.ts +118 -0
- package/tests/shell-changes-view.test.ts +146 -0
- package/tests/shell-changes.test.ts +182 -0
- package/tests/shell-prompt.test.ts +118 -0
- package/tests/shell-todo.test.ts +170 -0
- package/tests/shell-usage-view.test.ts +62 -0
- package/tests/shell-usage.test.ts +197 -0
- package/tests/telemetry-trigger.test.ts +349 -0
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import { __testing, createGentleAiExtension } from "../extensions/gentle-ai.ts";
|
|
5
|
+
import type { ExecFileAdapter, ExecFileResult } from "../lib/native-review-cli.ts";
|
|
6
|
+
import {
|
|
7
|
+
decodeTelemetryTriggerDecision,
|
|
8
|
+
shouldTriggerTelemetry,
|
|
9
|
+
spawnTelemetryTrigger,
|
|
10
|
+
TELEMETRY_TRIGGER_CONTRACT,
|
|
11
|
+
TELEMETRY_TRIGGER_KILL_TIMEOUT_MS,
|
|
12
|
+
type TelemetryTriggerChildLike,
|
|
13
|
+
type TelemetryTriggerSpawn,
|
|
14
|
+
type TelemetryTriggerSpawnOptions,
|
|
15
|
+
} from "../lib/telemetry-trigger.ts";
|
|
16
|
+
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// gentle-pi#677: gentle-ai owns telemetry end to end (gentle-ai#4309); Pi only
|
|
19
|
+
// nudges the local binary once per process. These tests cover the pure
|
|
20
|
+
// predicate/spawn/decode core in lib/telemetry-trigger.ts and the two ways
|
|
21
|
+
// extensions/gentle-ai.ts wires it in: the once-per-process activation nudge
|
|
22
|
+
// and the foreground /gentle:telemetry slash command relay.
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
interface FakeSpawnRecord {
|
|
26
|
+
command: string;
|
|
27
|
+
args: string[];
|
|
28
|
+
options: TelemetryTriggerSpawnOptions;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function fakeSpawn(): { spawn: TelemetryTriggerSpawn; calls: FakeSpawnRecord[]; killCalls: number } {
|
|
32
|
+
const calls: FakeSpawnRecord[] = [];
|
|
33
|
+
const state = { killCalls: 0 };
|
|
34
|
+
const spawn: TelemetryTriggerSpawn = (command, args, options) => {
|
|
35
|
+
calls.push({ command, args: [...args], options });
|
|
36
|
+
const child: TelemetryTriggerChildLike = {
|
|
37
|
+
unref() {
|
|
38
|
+
return undefined;
|
|
39
|
+
},
|
|
40
|
+
kill() {
|
|
41
|
+
state.killCalls += 1;
|
|
42
|
+
return true;
|
|
43
|
+
},
|
|
44
|
+
on() {
|
|
45
|
+
return undefined;
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
return child;
|
|
49
|
+
};
|
|
50
|
+
return {
|
|
51
|
+
spawn,
|
|
52
|
+
calls,
|
|
53
|
+
get killCalls() {
|
|
54
|
+
return state.killCalls;
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
test("shouldTriggerTelemetry: pure predicate table", () => {
|
|
60
|
+
const cases: Array<[Record<string, string | undefined>, boolean]> = [
|
|
61
|
+
[{}, true],
|
|
62
|
+
[{ DO_NOT_TRACK: "1" }, false],
|
|
63
|
+
[{ DO_NOT_TRACK: "0" }, true],
|
|
64
|
+
[{ GENTLE_AI_TELEMETRY: "0" }, false],
|
|
65
|
+
[{ GENTLE_AI_TELEMETRY: "1" }, true],
|
|
66
|
+
[{ CI: "true" }, false],
|
|
67
|
+
[{ CI: "false" }, true],
|
|
68
|
+
[{ CI: undefined }, true],
|
|
69
|
+
[{ DO_NOT_TRACK: "1", GENTLE_AI_TELEMETRY: "0", CI: "true" }, false],
|
|
70
|
+
];
|
|
71
|
+
for (const [env, expected] of cases) {
|
|
72
|
+
assert.equal(shouldTriggerTelemetry(env), expected, JSON.stringify(env));
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("spawnTelemetryTrigger: launches the expected argv, detached and stdio-ignored", () => {
|
|
77
|
+
const fake = fakeSpawn();
|
|
78
|
+
const result = spawnTelemetryTrigger({
|
|
79
|
+
executable: "/opt/gentle-ai/gentle-ai",
|
|
80
|
+
cwd: "/work/project",
|
|
81
|
+
env: {},
|
|
82
|
+
spawn: fake.spawn,
|
|
83
|
+
});
|
|
84
|
+
assert.deepEqual(result, { spawned: true, reason: "spawned" });
|
|
85
|
+
assert.equal(fake.calls.length, 1);
|
|
86
|
+
const [call] = fake.calls;
|
|
87
|
+
assert.equal(call.command, "/opt/gentle-ai/gentle-ai");
|
|
88
|
+
assert.deepEqual(call.args, ["telemetry", "trigger", "--json"]);
|
|
89
|
+
assert.equal(call.options.cwd, "/work/project");
|
|
90
|
+
assert.equal(call.options.detached, true);
|
|
91
|
+
assert.equal(call.options.stdio, "ignore");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("spawnTelemetryTrigger: never spawns under each kill switch", () => {
|
|
95
|
+
const switches: Array<[Record<string, string | undefined>, string]> = [
|
|
96
|
+
[{ DO_NOT_TRACK: "1" }, "do-not-track"],
|
|
97
|
+
[{ GENTLE_AI_TELEMETRY: "0" }, "telemetry-disabled"],
|
|
98
|
+
[{ CI: "true" }, "ci"],
|
|
99
|
+
];
|
|
100
|
+
for (const [env, reason] of switches) {
|
|
101
|
+
const fake = fakeSpawn();
|
|
102
|
+
const result = spawnTelemetryTrigger({ executable: "gentle-ai", cwd: "/work", env, spawn: fake.spawn });
|
|
103
|
+
assert.deepEqual(result, { spawned: false, reason }, JSON.stringify(env));
|
|
104
|
+
assert.equal(fake.calls.length, 0, JSON.stringify(env));
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("spawnTelemetryTrigger: a throwing spawn is swallowed, never thrown", () => {
|
|
109
|
+
const throwingSpawn: TelemetryTriggerSpawn = () => {
|
|
110
|
+
throw new Error("spawn EAGAIN");
|
|
111
|
+
};
|
|
112
|
+
const result = spawnTelemetryTrigger({ executable: "gentle-ai", cwd: "/work", env: {}, spawn: throwingSpawn });
|
|
113
|
+
assert.deepEqual(result, { spawned: false, reason: "spawn-error" });
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("spawnTelemetryTrigger: the 3 s kill timer fires exactly once and is unref'd", (t) => {
|
|
117
|
+
t.mock.timers.enable({ apis: ["setTimeout"] });
|
|
118
|
+
const fake = fakeSpawn();
|
|
119
|
+
const result = spawnTelemetryTrigger({ executable: "gentle-ai", cwd: "/work", env: {}, spawn: fake.spawn });
|
|
120
|
+
assert.equal(result.spawned, true);
|
|
121
|
+
assert.equal(fake.killCalls, 0);
|
|
122
|
+
t.mock.timers.tick(TELEMETRY_TRIGGER_KILL_TIMEOUT_MS);
|
|
123
|
+
assert.equal(fake.killCalls, 1);
|
|
124
|
+
t.mock.timers.tick(TELEMETRY_TRIGGER_KILL_TIMEOUT_MS);
|
|
125
|
+
assert.equal(fake.killCalls, 1, "the timer must not fire a second time");
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("decodeTelemetryTriggerDecision: decodes a well-formed v1 payload", () => {
|
|
129
|
+
const stdout = JSON.stringify({ schema: TELEMETRY_TRIGGER_CONTRACT, decision: "sent_heartbeat", source: "local-state" });
|
|
130
|
+
assert.deepEqual(decodeTelemetryTriggerDecision(stdout), {
|
|
131
|
+
schema: TELEMETRY_TRIGGER_CONTRACT,
|
|
132
|
+
decision: "sent_heartbeat",
|
|
133
|
+
source: "local-state",
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("decodeTelemetryTriggerDecision: every documented decision round-trips", () => {
|
|
138
|
+
for (const decision of ["enrolled", "sent_install", "sent_heartbeat", "rate_limited", "backoff", "disabled"]) {
|
|
139
|
+
const stdout = JSON.stringify({ schema: TELEMETRY_TRIGGER_CONTRACT, decision, source: "env" });
|
|
140
|
+
assert.equal(decodeTelemetryTriggerDecision(stdout)?.decision, decision);
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test("decodeTelemetryTriggerDecision: an old binary's output decodes as nothing to do", () => {
|
|
145
|
+
assert.equal(decodeTelemetryTriggerDecision("unknown telemetry command\n"), undefined);
|
|
146
|
+
assert.equal(decodeTelemetryTriggerDecision(""), undefined);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("decodeTelemetryTriggerDecision: rejects a mismatched schema, unknown decision, or missing source", () => {
|
|
150
|
+
assert.equal(decodeTelemetryTriggerDecision(JSON.stringify({ schema: "other/v1", decision: "enrolled", source: "cli" })), undefined);
|
|
151
|
+
assert.equal(decodeTelemetryTriggerDecision(JSON.stringify({ schema: TELEMETRY_TRIGGER_CONTRACT, decision: "unheard-of", source: "cli" })), undefined);
|
|
152
|
+
assert.equal(decodeTelemetryTriggerDecision(JSON.stringify({ schema: TELEMETRY_TRIGGER_CONTRACT, decision: "enrolled" })), undefined);
|
|
153
|
+
assert.equal(decodeTelemetryTriggerDecision("[]"), undefined);
|
|
154
|
+
assert.equal(decodeTelemetryTriggerDecision("null"), undefined);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// ---------------------------------------------------------------------------
|
|
158
|
+
// Extension wiring: the once-per-process activation nudge.
|
|
159
|
+
// ---------------------------------------------------------------------------
|
|
160
|
+
|
|
161
|
+
function buildExtensionHarness(overrides: Parameters<typeof createGentleAiExtension>[0] = {}) {
|
|
162
|
+
const handlers = new Map<string, (event: unknown, ctx: ExtensionContext) => Promise<unknown>>();
|
|
163
|
+
const commands = new Map<string, { handler: (args: string, ctx: ExtensionContext) => Promise<void> }>();
|
|
164
|
+
const pi = {
|
|
165
|
+
on(name: string, handler: (event: unknown, ctx: ExtensionContext) => Promise<unknown>) {
|
|
166
|
+
handlers.set(name, handler);
|
|
167
|
+
},
|
|
168
|
+
registerCommand(name: string, command: { handler: (args: string, ctx: ExtensionContext) => Promise<void> }) {
|
|
169
|
+
commands.set(name, command);
|
|
170
|
+
},
|
|
171
|
+
registerTool() {},
|
|
172
|
+
events: { emit() {} },
|
|
173
|
+
} as unknown as ExtensionAPI;
|
|
174
|
+
createGentleAiExtension({ nativeReviewCli: null, ...overrides })(pi);
|
|
175
|
+
return { handlers, commands };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function fakeContext(cwd: string, notifications: Array<{ message: string; severity: string }>): ExtensionContext {
|
|
179
|
+
return {
|
|
180
|
+
cwd,
|
|
181
|
+
hasUI: true,
|
|
182
|
+
ui: {
|
|
183
|
+
notify(message: string, severity: string) {
|
|
184
|
+
notifications.push({ message, severity });
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
} as unknown as ExtensionContext;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
test("activation: spawns the telemetry trigger exactly once for a primary session", async (t) => {
|
|
191
|
+
t.after(() => __testing.resetTelemetryTriggerGuardForTesting());
|
|
192
|
+
__testing.resetTelemetryTriggerGuardForTesting();
|
|
193
|
+
const fake = fakeSpawn();
|
|
194
|
+
const { handlers } = buildExtensionHarness({
|
|
195
|
+
resolveTelemetryTriggerBinary: () => "/opt/gentle-ai/gentle-ai",
|
|
196
|
+
telemetryTriggerSpawn: fake.spawn,
|
|
197
|
+
// Pin the environment: the ambient shell or CI may carry a kill switch
|
|
198
|
+
// (CI=true, DO_NOT_TRACK=1, GENTLE_AI_TELEMETRY=0) that would suppress the spawn.
|
|
199
|
+
processEnv: { PATH: "/bin" },
|
|
200
|
+
});
|
|
201
|
+
const beforeAgentStart = handlers.get("before_agent_start");
|
|
202
|
+
assert.equal(typeof beforeAgentStart, "function");
|
|
203
|
+
const notifications: Array<{ message: string; severity: string }> = [];
|
|
204
|
+
const ctx = fakeContext("/work/project", notifications);
|
|
205
|
+
|
|
206
|
+
// A primary-session event carries no agent name at all.
|
|
207
|
+
await beforeAgentStart!({ systemPrompt: "" }, ctx);
|
|
208
|
+
await beforeAgentStart!({ systemPrompt: "" }, ctx);
|
|
209
|
+
|
|
210
|
+
assert.equal(fake.calls.length, 1, "the trigger must be attempted at most once per process");
|
|
211
|
+
const [call] = fake.calls;
|
|
212
|
+
assert.deepEqual(call.args, ["telemetry", "trigger", "--json"]);
|
|
213
|
+
assert.equal(call.options.cwd, "/work/project");
|
|
214
|
+
assert.equal(call.options.detached, true);
|
|
215
|
+
assert.equal(call.options.stdio, "ignore");
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
test("activation: never spawns for a named or SDD agent event", async (t) => {
|
|
219
|
+
t.after(() => __testing.resetTelemetryTriggerGuardForTesting());
|
|
220
|
+
__testing.resetTelemetryTriggerGuardForTesting();
|
|
221
|
+
const fake = fakeSpawn();
|
|
222
|
+
const { handlers } = buildExtensionHarness({
|
|
223
|
+
resolveTelemetryTriggerBinary: () => "/opt/gentle-ai/gentle-ai",
|
|
224
|
+
telemetryTriggerSpawn: fake.spawn,
|
|
225
|
+
});
|
|
226
|
+
const beforeAgentStart = handlers.get("before_agent_start");
|
|
227
|
+
const notifications: Array<{ message: string; severity: string }> = [];
|
|
228
|
+
const ctx = fakeContext("/work/project", notifications);
|
|
229
|
+
|
|
230
|
+
await beforeAgentStart!({ agentName: "review-risk", systemPrompt: "" }, ctx);
|
|
231
|
+
await beforeAgentStart!({ systemPrompt: "SDD apply executor" }, ctx);
|
|
232
|
+
|
|
233
|
+
assert.equal(fake.calls.length, 0, "named/SDD agents must never trigger the nudge");
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
test("activation: a missing binary or spawn error never affects activation", async (t) => {
|
|
237
|
+
t.after(() => __testing.resetTelemetryTriggerGuardForTesting());
|
|
238
|
+
__testing.resetTelemetryTriggerGuardForTesting();
|
|
239
|
+
const { handlers } = buildExtensionHarness({
|
|
240
|
+
resolveTelemetryTriggerBinary: () => {
|
|
241
|
+
throw new Error("package-local-binary-missing: not installed");
|
|
242
|
+
},
|
|
243
|
+
});
|
|
244
|
+
const beforeAgentStart = handlers.get("before_agent_start");
|
|
245
|
+
const notifications: Array<{ message: string; severity: string }> = [];
|
|
246
|
+
const ctx = fakeContext("/work/project", notifications);
|
|
247
|
+
|
|
248
|
+
// Must resolve cleanly and produce the ordinary orchestrator prompt fields,
|
|
249
|
+
// never throw or notify about the missing binary.
|
|
250
|
+
const outcome = await beforeAgentStart!({ systemPrompt: "base" }, ctx);
|
|
251
|
+
assert.equal(typeof outcome, "object");
|
|
252
|
+
assert.equal(notifications.length, 0);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
// ---------------------------------------------------------------------------
|
|
256
|
+
// Extension wiring: the foreground /gentle:telemetry slash command.
|
|
257
|
+
// ---------------------------------------------------------------------------
|
|
258
|
+
|
|
259
|
+
function fakeAdapter(result: ExecFileResult): { adapter: ExecFileAdapter; requests: Array<{ file: string; arguments: readonly string[] }> } {
|
|
260
|
+
const requests: Array<{ file: string; arguments: readonly string[] }> = [];
|
|
261
|
+
const adapter: ExecFileAdapter = async (request) => {
|
|
262
|
+
requests.push({ file: request.file, arguments: request.arguments });
|
|
263
|
+
return result;
|
|
264
|
+
};
|
|
265
|
+
return { adapter, requests };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
test("/gentle:telemetry relays a status --json payload", async () => {
|
|
269
|
+
const payload = { schema: "gentle-ai.telemetry-status/v1", enabled: true, source: "default" };
|
|
270
|
+
const { adapter, requests } = fakeAdapter({
|
|
271
|
+
stdout: `${JSON.stringify(payload)}\n`,
|
|
272
|
+
stderr: "",
|
|
273
|
+
exitCode: 0,
|
|
274
|
+
signal: null,
|
|
275
|
+
timedOut: false,
|
|
276
|
+
outputLimitExceeded: false,
|
|
277
|
+
});
|
|
278
|
+
const { commands } = buildExtensionHarness({
|
|
279
|
+
resolveTelemetryTriggerBinary: () => "/opt/gentle-ai/gentle-ai",
|
|
280
|
+
telemetryExecFileAdapter: adapter,
|
|
281
|
+
});
|
|
282
|
+
const command = commands.get("gentle:telemetry");
|
|
283
|
+
assert.ok(command, "gentle:telemetry must be registered");
|
|
284
|
+
const notifications: Array<{ message: string; severity: string }> = [];
|
|
285
|
+
await command!.handler("", fakeContext("/work/project", notifications));
|
|
286
|
+
|
|
287
|
+
assert.equal(requests.length, 1);
|
|
288
|
+
assert.deepEqual(requests[0].arguments, ["telemetry", "status", "--json"]);
|
|
289
|
+
assert.equal(notifications.length, 1);
|
|
290
|
+
assert.equal(notifications[0].severity, "info");
|
|
291
|
+
assert.deepEqual(JSON.parse(notifications[0].message), payload);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
test("/gentle:telemetry disable prints a one-line confirmation", async () => {
|
|
295
|
+
const { adapter } = fakeAdapter({
|
|
296
|
+
stdout: `${JSON.stringify({ schema: "gentle-ai.telemetry-status/v1", enabled: false, source: "user" })}\n`,
|
|
297
|
+
stderr: "",
|
|
298
|
+
exitCode: 0,
|
|
299
|
+
signal: null,
|
|
300
|
+
timedOut: false,
|
|
301
|
+
outputLimitExceeded: false,
|
|
302
|
+
});
|
|
303
|
+
const { commands } = buildExtensionHarness({
|
|
304
|
+
resolveTelemetryTriggerBinary: () => "/opt/gentle-ai/gentle-ai",
|
|
305
|
+
telemetryExecFileAdapter: adapter,
|
|
306
|
+
});
|
|
307
|
+
const command = commands.get("gentle:telemetry");
|
|
308
|
+
const notifications: Array<{ message: string; severity: string }> = [];
|
|
309
|
+
await command!.handler("disable", fakeContext("/work/project", notifications));
|
|
310
|
+
|
|
311
|
+
assert.deepEqual(notifications, [{ message: "Gentle AI telemetry disabled.", severity: "info" }]);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
test("/gentle:telemetry relays a typed non-zero failure", async () => {
|
|
315
|
+
const { adapter } = fakeAdapter({
|
|
316
|
+
stdout: "",
|
|
317
|
+
stderr: "unknown telemetry command\n",
|
|
318
|
+
exitCode: 1,
|
|
319
|
+
signal: null,
|
|
320
|
+
timedOut: false,
|
|
321
|
+
outputLimitExceeded: false,
|
|
322
|
+
});
|
|
323
|
+
const { commands } = buildExtensionHarness({
|
|
324
|
+
resolveTelemetryTriggerBinary: () => "/opt/gentle-ai/gentle-ai",
|
|
325
|
+
telemetryExecFileAdapter: adapter,
|
|
326
|
+
});
|
|
327
|
+
const command = commands.get("gentle:telemetry");
|
|
328
|
+
const notifications: Array<{ message: string; severity: string }> = [];
|
|
329
|
+
await command!.handler("preview", fakeContext("/work/project", notifications));
|
|
330
|
+
|
|
331
|
+
assert.equal(notifications.length, 1);
|
|
332
|
+
assert.equal(notifications[0].severity, "error");
|
|
333
|
+
assert.match(notifications[0].message, /unknown telemetry command/);
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
test("/gentle:telemetry rejects an unknown sub-action without calling the binary", async () => {
|
|
337
|
+
const { adapter, requests } = fakeAdapter({ stdout: "{}", stderr: "", exitCode: 0, signal: null, timedOut: false, outputLimitExceeded: false });
|
|
338
|
+
const { commands } = buildExtensionHarness({
|
|
339
|
+
resolveTelemetryTriggerBinary: () => "/opt/gentle-ai/gentle-ai",
|
|
340
|
+
telemetryExecFileAdapter: adapter,
|
|
341
|
+
});
|
|
342
|
+
const command = commands.get("gentle:telemetry");
|
|
343
|
+
const notifications: Array<{ message: string; severity: string }> = [];
|
|
344
|
+
await command!.handler("frobnicate", fakeContext("/work/project", notifications));
|
|
345
|
+
|
|
346
|
+
assert.equal(requests.length, 0);
|
|
347
|
+
assert.equal(notifications.length, 1);
|
|
348
|
+
assert.equal(notifications[0].severity, "warning");
|
|
349
|
+
});
|