@xynogen/pix-commands 0.6.1 → 0.6.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xynogen/pix-commands",
3
- "version": "0.6.1",
3
+ "version": "0.6.3",
4
4
  "description": "Pi extension — slash commands for cache clearing and isolated side questions",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -13,6 +13,7 @@
13
13
  },
14
14
  "files": [
15
15
  "src",
16
+ "!src/**/*.test.*",
16
17
  "README.md",
17
18
  "LICENSE"
18
19
  ],
package/src/btw/render.ts CHANGED
@@ -49,10 +49,10 @@ export function registerBtwRenderer(
49
49
  // header text embedded inside Markdown can confuse wrapping and parsing.
50
50
  const card = new Box(1, 1, (text) => theme.bg("selectedBg", text));
51
51
  card.addChild(
52
- new Text(`${statusGlyph} ${theme.bold("BTW")} ${theme.fg("dim", `· ${meta}`)}`, 0, 0),
52
+ new Text(`${statusGlyph} ${theme.bold("BTW")} ${theme.fg("muted", `· ${meta}`)}`, 0, 0),
53
53
  );
54
54
  card.addChild(
55
- new Text(`${theme.fg("accent", "▐")} ${theme.fg("muted", details.question)}`, 0, 0),
55
+ new Text(`${theme.fg("accent", "▐")} ${theme.fg("dim", details.question)}`, 0, 0),
56
56
  );
57
57
  card.addChild(new Spacer(1));
58
58
 
@@ -69,7 +69,7 @@ export function registerBtwRenderer(
69
69
  card.addChild(new Text(theme.fg("thinkingText", details.thinking), 0, 0));
70
70
  card.addChild(new Spacer(1));
71
71
  } else {
72
- card.addChild(new Text(theme.fg("dim", "› reasoning hidden — expand to view"), 0, 0));
72
+ card.addChild(new Text(theme.fg("muted", "› reasoning hidden — expand to view"), 0, 0));
73
73
  card.addChild(new Spacer(1));
74
74
  }
75
75
  }
package/src/btw/widget.ts CHANGED
@@ -108,23 +108,23 @@ function finishedLine(job: BtwWidgetJob, theme: WidgetTheme): string {
108
108
  if (job.status === "completed") {
109
109
  mark = theme.fg("success", "\u2713");
110
110
  } else if (job.status === "stopped") {
111
- mark = theme.fg("dim", "\u25a0");
112
- suffix = theme.fg("dim", " stopped");
111
+ mark = theme.fg("muted", "\u25a0");
112
+ suffix = theme.fg("muted", " stopped");
113
113
  } else {
114
114
  mark = theme.fg("error", "\u2717");
115
115
  suffix = theme.fg("error", job.error ? ` ${job.error.slice(0, 60)}` : " error");
116
116
  }
117
117
  const model = theme.fg("muted", `[${job.model}]`);
118
- const stats = theme.fg("dim", statsFor(job, end));
119
- return `${mark} ${theme.fg("dim", `#${job.id}`)} ${model} ${theme.fg("dim", "\u00b7")} ${stats}${suffix}`;
118
+ const stats = theme.fg("muted", statsFor(job, end));
119
+ return `${mark} ${theme.fg("dim", `#${job.id}`)} ${model} ${theme.fg("muted", "\u00b7")} ${stats}${suffix}`;
120
120
  }
121
121
 
122
122
  function runningLine(job: BtwWidgetJob, theme: WidgetTheme, frame: string, now: number): string {
123
123
  const model = theme.fg("muted", `[${job.model}]`);
124
- const stats = theme.fg("dim", statsFor(job, now));
124
+ const stats = theme.fg("muted", statsFor(job, now));
125
125
  const activeMap = new Map<string, string>(job.activeTools.map((name, i) => [String(i), name]));
126
126
  const activity = theme.fg("dim", describeActivity(activeMap, job.text));
127
- const dot = theme.fg("dim", "\u00b7");
127
+ const dot = theme.fg("muted", "\u00b7");
128
128
  return `${theme.fg("accent", frame)} ${theme.fg("toolTitle", theme.bold(`#${job.id}`))} ${model} ${dot} ${stats} ${dot} ${activity}`;
129
129
  }
130
130
 
@@ -152,15 +152,15 @@ export function renderBtwWidget(
152
152
  const spinner = SPINNER[frame % SPINNER.length] ?? "";
153
153
 
154
154
  const runningLines = running.map((j) =>
155
- truncate(`${theme.fg("dim", "\u251c\u2500")} ${runningLine(j, theme, spinner, now)}`),
155
+ truncate(`${theme.fg("muted", "\u251c\u2500")} ${runningLine(j, theme, spinner, now)}`),
156
156
  );
157
157
  const finishedLines = finished.map((j) =>
158
- truncate(`${theme.fg("dim", "\u251c\u2500")} ${finishedLine(j, theme)}`),
158
+ truncate(`${theme.fg("muted", "\u251c\u2500")} ${finishedLine(j, theme)}`),
159
159
  );
160
160
 
161
161
  const lines: string[] = [
162
162
  truncate(
163
- `${theme.fg(headingColor, headingIcon)} ${theme.fg(headingColor, `BTW (${running.length})`)}`,
163
+ `${theme.fg(headingColor, headingIcon)} ${theme.fg(headingColor, "BTW")}${theme.fg("muted", ` (${running.length})`)}`,
164
164
  ),
165
165
  ];
166
166
 
@@ -173,7 +173,7 @@ export function renderBtwWidget(
173
173
  const hidden = body.length - shown.length;
174
174
  lines.push(...shown);
175
175
  lines.push(
176
- truncate(`${theme.fg("dim", "\u251c\u2500")} ${theme.fg("dim", `+${hidden} more`)}`),
176
+ truncate(`${theme.fg("muted", "\u251c\u2500")} ${theme.fg("muted", `+${hidden} more`)}`),
177
177
  );
178
178
  }
179
179
 
@@ -1,46 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import { registerBtw, shortModelName } from "./index.ts";
3
-
4
- describe("BTW display helpers", () => {
5
- test("prefers model display name and falls back to id", () => {
6
- expect(shortModelName({ id: "id", name: "Friendly" })).toBe("Friendly");
7
- expect(shortModelName({ id: "id", name: " " })).toBe("id");
8
- });
9
-
10
- test("registers a display-only entry renderer, never a context-bearing message renderer", () => {
11
- let entryRenderer: string | undefined;
12
- let messageRenderer: string | undefined;
13
- const pi = {
14
- on() {},
15
- registerCommand() {},
16
- registerEntryRenderer(name: string) {
17
- entryRenderer = name;
18
- },
19
- registerMessageRenderer(name: string) {
20
- messageRenderer = name;
21
- },
22
- } as any;
23
- registerBtw(pi);
24
-
25
- // pix-btw-answer must be a CustomEntry (display-only, never in LLM context),
26
- // not a CustomMessageEntry — that is what lets the card land mid-stream.
27
- expect(entryRenderer).toBe("pix-btw-answer");
28
- expect(messageRenderer).toBeUndefined();
29
- });
30
-
31
- test("does not register a context handler (BTW cards never enter LLM context)", () => {
32
- const events: string[] = [];
33
- const pi = {
34
- on(event: string) {
35
- events.push(event);
36
- },
37
- registerCommand() {},
38
- registerEntryRenderer() {},
39
- } as any;
40
- registerBtw(pi);
41
-
42
- // A CustomEntry is ignored by buildSessionContext, so there is nothing to
43
- // strip — the old pi.on("context", filterBtwMessages) hack is gone.
44
- expect(events).not.toContain("context");
45
- });
46
- });
@@ -1,102 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import { type ExtensionAPI, initTheme } from "@earendil-works/pi-coding-agent";
3
- import { type BtwMessageDetails, formatDuration, registerBtwRenderer } from "./render.ts";
4
-
5
- const stripAnsi = (text: string) => text.replace(/\x1b\[[0-9;]*m/g, "");
6
-
7
- function captureRenderer() {
8
- let renderer: ((entry: unknown, options: unknown, theme: unknown) => unknown) | undefined;
9
- const pi = {
10
- registerEntryRenderer(_name: string, fn: typeof renderer) {
11
- renderer = fn;
12
- },
13
- } as unknown as ExtensionAPI;
14
- registerBtwRenderer(pi);
15
- if (!renderer) throw new Error("renderer was not registered");
16
- return renderer;
17
- }
18
-
19
- const backgrounds: string[] = [];
20
- const theme = {
21
- fg: (_color: string, text: string) => text,
22
- bg: (color: string, text: string) => {
23
- backgrounds.push(color);
24
- return text;
25
- },
26
- bold: (text: string) => text,
27
- };
28
-
29
- function render(details: BtwMessageDetails, expanded = false): string {
30
- const renderer = captureRenderer();
31
- const component = renderer(
32
- { type: "custom", customType: "pix-btw-answer", data: details },
33
- { expanded },
34
- theme,
35
- ) as {
36
- render(width: number): string[];
37
- };
38
- return stripAnsi(component.render(80).join("\n"));
39
- }
40
-
41
- describe("BTW renderer", () => {
42
- test("formats durations compactly", () => {
43
- expect(formatDuration(450)).toBe("450ms");
44
- expect(formatDuration(2_100)).toBe("2.1s");
45
- expect(formatDuration(65_000)).toBe("1m 5s");
46
- });
47
-
48
- test("renders metadata and question as distinct side-thread card chrome", () => {
49
- backgrounds.length = 0;
50
- const output = render({
51
- question: "hello",
52
- answer: "Hi!",
53
- thinking: "",
54
- model: "GPT-5.6",
55
- thinkingLevel: "high",
56
- durationMs: 2_100,
57
- toolUses: 0,
58
- });
59
- expect(output).toContain("✓ BTW · GPT-5.6 · high · 2.1s");
60
- expect(output).toContain("▐ hello");
61
- expect(output).not.toContain("SIDE THREAD");
62
- expect(backgrounds).toContain("selectedBg");
63
- expect(backgrounds).not.toContain("customMessageBg");
64
- });
65
-
66
- test("renders the answer as Markdown", () => {
67
- initTheme();
68
- const output = render({
69
- question: "show markdown",
70
- answer: "## Heading\n\n- alpha\n- beta\n\n`code`",
71
- thinking: "",
72
- model: "Model",
73
- thinkingLevel: "medium",
74
- durationMs: 1_000,
75
- toolUses: 1,
76
- });
77
- expect(output).toContain("Heading");
78
- expect(output).toContain("alpha");
79
- expect(output).toContain("beta");
80
- expect(output).toContain("code");
81
- });
82
-
83
- test("hides reasoning by default and reveals it when expanded", () => {
84
- initTheme();
85
- const details: BtwMessageDetails = {
86
- question: "why",
87
- answer: "Because.",
88
- thinking: "first I considered the mutex",
89
- model: "Model",
90
- thinkingLevel: "high",
91
- durationMs: 1_000,
92
- toolUses: 0,
93
- };
94
- const collapsed = render(details, false);
95
- expect(collapsed).toContain("reasoning hidden");
96
- expect(collapsed).not.toContain("first I considered the mutex");
97
-
98
- const expanded = render(details, true);
99
- expect(expanded).toContain("Reasoning");
100
- expect(expanded).toContain("first I considered the mutex");
101
- });
102
- });
@@ -1,128 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import type { Api, Model } from "@earendil-works/pi-ai";
3
- import type { LoadExtensionsResult } from "@earendil-works/pi-coding-agent";
4
- import {
5
- BTW_CTX_TURNS,
6
- BTW_SYSTEM_PROMPT,
7
- buildContextPreamble,
8
- lastAssistantText,
9
- makeLeanExtensions,
10
- selectBtwTools,
11
- snapshotMainSettings,
12
- } from "./session.ts";
13
-
14
- describe("BTW system prompt", () => {
15
- test("is the exact lean Pix identity", () => {
16
- expect(BTW_SYSTEM_PROMPT).toBe(
17
- "You are Pix Coding Agent. You help users accomplish any task they request.",
18
- );
19
- });
20
- });
21
-
22
- describe("snapshotMainSettings", () => {
23
- test("captures model, thinking, cwd, and a defensive copy of active tools", () => {
24
- const tools = ["read", "fetch"];
25
- const model = { id: "model-id", name: "Model" } as Model<Api>;
26
- const snapshot = snapshotMainSettings({ cwd: "/project", model } as never, "high", tools);
27
- tools.push("write");
28
- expect(snapshot.cwd).toBe("/project");
29
- expect(snapshot.model).toBe(model);
30
- expect(snapshot.thinkingLevel).toBe("high");
31
- expect(snapshot.activeToolNames).toEqual(["read", "fetch"]);
32
- });
33
-
34
- test("rejects invocation when the main session has no model", () => {
35
- expect(() =>
36
- snapshotMainSettings({ cwd: "/project", model: undefined } as never, "medium", []),
37
- ).toThrow("No model is selected");
38
- });
39
- });
40
-
41
- describe("selectBtwTools", () => {
42
- test("uses the main active tools and removes duplicates without reordering", () => {
43
- expect(selectBtwTools(["read", "fetch", "read", "agent"])).toEqual(["read", "fetch", "agent"]);
44
- });
45
- });
46
-
47
- describe("makeLeanExtensions", () => {
48
- test("removes discovered before_agent_start mutators but preserves inline override", () => {
49
- const regularHandlers = new Map<string, never[]>([
50
- ["before_agent_start", []],
51
- ["tool_call", []],
52
- ]);
53
- const inlineHandlers = new Map<string, never[]>([["before_agent_start", []]]);
54
- const base = {
55
- extensions: [
56
- { path: "/extensions/pix-prompts.ts", handlers: regularHandlers },
57
- { path: "<inline:1>", handlers: inlineHandlers },
58
- ],
59
- errors: [],
60
- runtime: {},
61
- } as unknown as LoadExtensionsResult;
62
-
63
- const result = makeLeanExtensions(base);
64
- expect(result.extensions[0]?.handlers.has("before_agent_start")).toBe(false);
65
- expect(result.extensions[0]?.handlers.has("tool_call")).toBe(true);
66
- expect(result.extensions[1]?.handlers.has("before_agent_start")).toBe(true);
67
- // Do not mutate the loader's original extension records.
68
- expect(regularHandlers.has("before_agent_start")).toBe(true);
69
- });
70
- });
71
-
72
- describe("buildContextPreamble", () => {
73
- const msg = (role: string, text: string) => ({
74
- type: "message",
75
- message: { role, content: [{ type: "text", text }] },
76
- });
77
-
78
- test("flattens recent user/assistant turns into a labeled preamble", () => {
79
- const out = buildContextPreamble([msg("user", "hi"), msg("assistant", "hello")]);
80
- expect(out).toContain("User: hi");
81
- expect(out).toContain("Assistant: hello");
82
- expect(out).toContain("read-only context");
83
- });
84
-
85
- test("keeps only the last N turns", () => {
86
- const entries = Array.from({ length: BTW_CTX_TURNS + 5 }, (_, i) => msg("user", `q${i}`));
87
- const out = buildContextPreamble(entries);
88
- expect(out).not.toContain("q0"); // dropped
89
- expect(out).toContain(`q${BTW_CTX_TURNS + 4}`); // kept
90
- expect(out).toContain(`most recent ${BTW_CTX_TURNS} turn`);
91
- });
92
-
93
- test("skips non-message entries, tool noise, and string content", () => {
94
- const out = buildContextPreamble([
95
- { type: "model_change", message: undefined },
96
- { type: "message", message: { role: "tool", content: "ignored" } },
97
- { type: "message", message: { role: "user", content: "plain string" } },
98
- ]);
99
- expect(out).toContain("User: plain string");
100
- expect(out).not.toContain("ignored");
101
- });
102
-
103
- test("returns empty when there are no usable turns", () => {
104
- expect(buildContextPreamble([{ type: "compaction", message: undefined }])).toBe("");
105
- expect(buildContextPreamble([])).toBe("");
106
- });
107
- });
108
-
109
- describe("lastAssistantText", () => {
110
- test("returns text from the latest assistant response", () => {
111
- const messages = [
112
- { role: "assistant", content: [{ type: "text", text: "old" }] },
113
- { role: "user", content: "question" },
114
- {
115
- role: "assistant",
116
- content: [
117
- { type: "thinking", thinking: "hidden" },
118
- { type: "text", text: "latest" },
119
- ],
120
- },
121
- ];
122
- expect(lastAssistantText(messages)).toBe("latest");
123
- });
124
-
125
- test("returns an empty string when there is no assistant text", () => {
126
- expect(lastAssistantText([{ role: "user", content: "hello" }])).toBe("");
127
- });
128
- });
@@ -1,99 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import {
3
- type BtwWidgetJob,
4
- DEFAULT_LINGER_BASE_MS,
5
- ERROR_LINGER_SCALE,
6
- hasVisibleJobs,
7
- lingerWindows,
8
- OK_LINGER_SCALE,
9
- renderBtwWidget,
10
- shouldShowFinished,
11
- type WidgetTheme,
12
- } from "./widget.ts";
13
-
14
- const theme: WidgetTheme = {
15
- fg: (_color, text) => text,
16
- bold: (text) => text,
17
- };
18
-
19
- function job(overrides: Partial<BtwWidgetJob>): BtwWidgetJob {
20
- return {
21
- id: 1,
22
- model: "Model",
23
- status: "running",
24
- startedAt: 0,
25
- activeTools: [],
26
- text: "",
27
- toolUses: 0,
28
- turnCount: 0,
29
- outputTokens: 0,
30
- contextUsage: null,
31
- ...overrides,
32
- };
33
- }
34
-
35
- const render = (jobs: BtwWidgetJob[], now = 1_000) =>
36
- renderBtwWidget(jobs, theme, 0, now, 200).join("\n");
37
-
38
- describe("BTW widget layout", () => {
39
- test("empty when there are no running or lingering jobs", () => {
40
- expect(renderBtwWidget([], theme, 0, 1_000, 200)).toEqual([]);
41
- const old = job({ status: "completed", completedAt: 0 });
42
- expect(renderBtwWidget([old], theme, 0, 999_999, 200)).toEqual([]);
43
- });
44
-
45
- test("running heading is hollow and shows the running count", () => {
46
- const out = render([job({ id: 7, model: "GPT" })]);
47
- expect(out).toContain("\u25cb BTW (1)");
48
- expect(out).toContain("#7");
49
- expect(out).toContain("[GPT]");
50
- });
51
-
52
- test("finished jobs linger with a check, then drop after the window", () => {
53
- const done = job({ status: "completed", completedAt: 1_000, toolUses: 2, turnCount: 1 });
54
- // Default base 10s × 3 = 30s ok-window.
55
- expect(shouldShowFinished(done, 1_500)).toBe(true);
56
- expect(shouldShowFinished(done, 40_000)).toBe(false); // 39s elapsed > 30s window
57
-
58
- const out = render([done], 1_500);
59
- expect(out).toContain("\u2713");
60
- // All jobs finished → filled heading disk.
61
- expect(out).toContain("\u25cf BTW (0)");
62
- });
63
-
64
- test("errors linger longer and show the message", () => {
65
- const failed = job({ status: "error", completedAt: 1_000, error: "boom" });
66
- // Default base 10s × 9 = 90s error-window; 40s elapsed is past ok (30s) but well inside error.
67
- expect(shouldShowFinished(failed, 40_000)).toBe(true);
68
- expect(render([failed], 40_000)).toContain("boom");
69
- });
70
-
71
- test("linger windows scale off the config collapse delay", () => {
72
- // Defaults derive from the fallback base.
73
- expect(lingerWindows()).toEqual({
74
- ok: DEFAULT_LINGER_BASE_MS * OK_LINGER_SCALE,
75
- error: DEFAULT_LINGER_BASE_MS * ERROR_LINGER_SCALE,
76
- });
77
- // A custom base (e.g. config collapse.delaySec = 4 → 4000ms) scales both.
78
- expect(lingerWindows(4_000)).toEqual({ ok: 12_000, error: 36_000 });
79
- // Non-positive base falls back to the default so windows never collapse to 0.
80
- expect(lingerWindows(0)).toEqual(lingerWindows());
81
-
82
- // shouldShowFinished honors the threaded base: a 4s base drops an ok job at 13s.
83
- const done = job({ status: "completed", completedAt: 0 });
84
- expect(shouldShowFinished(done, 11_000, 4_000)).toBe(true); // < 12s
85
- expect(shouldShowFinished(done, 13_000, 4_000)).toBe(false); // > 12s
86
- });
87
-
88
- test("overflow collapses excess rows into a +N more line", () => {
89
- const many = Array.from({ length: 20 }, (_, i) => job({ id: i + 1 }));
90
- const lines = renderBtwWidget(many, theme, 0, 1_000, 200);
91
- expect(lines.length).toBeLessThanOrEqual(12);
92
- expect(lines.at(-1)).toContain("more");
93
- });
94
-
95
- test("hasVisibleJobs mirrors render visibility", () => {
96
- expect(hasVisibleJobs([job({})], 1_000)).toBe(true);
97
- expect(hasVisibleJobs([job({ status: "completed", completedAt: 0 })], 999_999)).toBe(false);
98
- });
99
- });
@@ -1,74 +0,0 @@
1
- import { afterEach, describe, expect, test } from "bun:test";
2
- import { createEventBus, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
3
- import { icon } from "@xynogen/pix-pretty/icon-catalog";
4
- import { getUnattendedMode } from "@xynogen/pix-runtime";
5
- import extension from "./extension.ts";
6
-
7
- afterEach(() => {
8
- delete (globalThis as { __pixOnce?: WeakMap<object, Set<string>> }).__pixOnce;
9
- });
10
-
11
- describe("pix-commands registration", () => {
12
- function host() {
13
- const commands: string[] = [];
14
- const handlers = new Map<string, (args: string, ctx: never) => Promise<void>>();
15
- const renderers: string[] = [];
16
- const pi = {
17
- events: createEventBus(),
18
- registerCommand(
19
- name: string,
20
- options: { handler?: (args: string, ctx: never) => Promise<void> },
21
- ) {
22
- commands.push(name);
23
- if (options.handler) handlers.set(name, options.handler);
24
- },
25
- registerEntryRenderer(name: string) {
26
- renderers.push(name);
27
- },
28
- on() {},
29
- } as unknown as ExtensionAPI;
30
- return { pi, commands, handlers, renderers };
31
- }
32
-
33
- test("registers /clear, /btw, /afk, /yolo, and the BTW renderer once per Pi instance", () => {
34
- const { pi, commands, renderers } = host();
35
- extension(pi);
36
- extension(pi);
37
- expect(commands).toEqual(["clear", "btw", "afk", "yolo"]);
38
- expect(renderers).toEqual(["pix-btw-answer"]);
39
- });
40
-
41
- test("registers again for a fresh Pi session", () => {
42
- const first = host();
43
- const second = host();
44
- extension(first.pi);
45
- extension(second.pi);
46
- expect(first.commands).toEqual(["clear", "btw", "afk", "yolo"]);
47
- expect(second.commands).toEqual(["clear", "btw", "afk", "yolo"]);
48
- });
49
-
50
- test("/afk toggles shared state and status", async () => {
51
- const { pi, handlers } = host();
52
- extension(pi);
53
- const statuses: Array<string | undefined> = [];
54
- const notices: string[] = [];
55
- const ctx = {
56
- ui: {
57
- theme: { fg: (color: string, text: string) => `<${color}>${text}</${color}>` },
58
- setStatus: (_key: string, text: string | undefined) => statuses.push(text),
59
- notify: (text: string) => notices.push(text),
60
- },
61
- };
62
- const handler = handlers.get("afk");
63
- if (!handler) throw new Error("/afk not registered");
64
-
65
- await handler("", ctx as never);
66
- expect(getUnattendedMode(pi.events)).toBe("afk");
67
- expect(statuses.at(-1)).toBe(`<error>${icon("afk")} AFK</error>`);
68
- expect(notices.at(-1)).toContain("yellow gates auto-allow");
69
-
70
- await handler("", ctx as never);
71
- expect(getUnattendedMode(pi.events)).toBe("off");
72
- expect(statuses.at(-1)).toBeUndefined();
73
- });
74
- });
@@ -1,76 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import { createEventBus } from "@earendil-works/pi-coding-agent";
3
- import {
4
- confirmYoloConsent,
5
- getMode,
6
- modelScore,
7
- setMode,
8
- unattendedBanner,
9
- YOLO_MIN_SCORE,
10
- } from "./unattended.ts";
11
-
12
- describe("unattended mode state", () => {
13
- test("setMode updates one session", () => {
14
- const events = createEventBus();
15
- setMode(events, "afk");
16
- expect(getMode(events)).toBe("afk");
17
-
18
- setMode(events, "yolo");
19
- expect(getMode(events)).toBe("yolo");
20
-
21
- setMode(events, "off");
22
- expect(getMode(events)).toBe("off");
23
- });
24
- });
25
-
26
- describe("modelScore", () => {
27
- test("returns null for an empty / off-catalog model", () => {
28
- expect(modelScore({ model: undefined })).toBeNull();
29
- expect(modelScore({ model: { id: "definitely-not-a-real-model-xyz" } })).toBeNull();
30
- });
31
- });
32
-
33
- describe("unattendedBanner", () => {
34
- test("off => no banner", () => {
35
- const events = createEventBus();
36
- setMode(events, "off");
37
- expect(unattendedBanner(events)).toBeUndefined();
38
- });
39
-
40
- test("afk banner names auto-deny of red and root", () => {
41
- const events = createEventBus();
42
- setMode(events, "afk");
43
- const b = unattendedBanner(events) ?? "";
44
- expect(b).toContain('mode="afk"');
45
- expect(b).toContain("auto-DENY");
46
- });
47
-
48
- test("yolo banner demands red/root self-justification", () => {
49
- const events = createEventBus();
50
- setMode(events, "yolo");
51
- const b = unattendedBanner(events) ?? "";
52
- expect(b).toContain('mode="yolo"');
53
- expect(b).toContain("blast radius");
54
- expect(b).toContain("reversible");
55
- });
56
- });
57
-
58
- describe("YOLO score threshold", () => {
59
- test("is a sane capability floor", () => {
60
- expect(YOLO_MIN_SCORE).toBe(75);
61
- });
62
- });
63
-
64
- describe("confirmYoloConsent (session gate)", () => {
65
- test("short-circuits true once consent is recorded for the session", async () => {
66
- const events = createEventBus();
67
- const { setYoloConsent } = await import("@xynogen/pix-runtime");
68
- setYoloConsent(events, true);
69
- // No ui passed: if it did not short-circuit it would return false.
70
- expect(await confirmYoloConsent(events, {})).toBe(true);
71
- });
72
-
73
- test("refuses when there is no ui to render the warning", async () => {
74
- expect(await confirmYoloConsent(createEventBus(), {})).toBe(false);
75
- });
76
- });