tmux-agent 0.1.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/.codex/skills/speckit/SKILL.md +173 -0
- package/.codex/skills/speckit/assets/templates/checklist-template.md +49 -0
- package/.codex/skills/speckit/assets/templates/notes-entrypoints-template.md +11 -0
- package/.codex/skills/speckit/assets/templates/notes-questions-template.md +7 -0
- package/.codex/skills/speckit/assets/templates/notes-readme-template.md +36 -0
- package/.codex/skills/speckit/assets/templates/notes-session-template.md +21 -0
- package/.codex/skills/speckit/assets/templates/plan-template.md +126 -0
- package/.codex/skills/speckit/assets/templates/spec-template.md +135 -0
- package/.codex/skills/speckit/assets/templates/tasks-template.md +269 -0
- package/.codex/skills/speckit/references/acceptance.md +183 -0
- package/.codex/skills/speckit/references/analyze.md +186 -0
- package/.codex/skills/speckit/references/checklist.md +302 -0
- package/.codex/skills/speckit/references/clarify-auto.md +69 -0
- package/.codex/skills/speckit/references/clarify-detailed.md +78 -0
- package/.codex/skills/speckit/references/clarify.md +189 -0
- package/.codex/skills/speckit/references/constitution.md +90 -0
- package/.codex/skills/speckit/references/group.md +89 -0
- package/.codex/skills/speckit/references/implement-task.md +115 -0
- package/.codex/skills/speckit/references/implement.md +129 -0
- package/.codex/skills/speckit/references/notes.md +82 -0
- package/.codex/skills/speckit/references/plan-deep.md +87 -0
- package/.codex/skills/speckit/references/plan-from-questions.md +115 -0
- package/.codex/skills/speckit/references/plan-from-review.md +89 -0
- package/.codex/skills/speckit/references/plan.md +97 -0
- package/.codex/skills/speckit/references/review-plan.md +156 -0
- package/.codex/skills/speckit/references/specify.md +246 -0
- package/.codex/skills/speckit/references/tasks.md +155 -0
- package/.codex/skills/speckit/references/taskstoissues.md +33 -0
- package/.codex/skills/speckit/scripts/bash/check-prerequisites.sh +206 -0
- package/.codex/skills/speckit/scripts/bash/common.sh +191 -0
- package/.codex/skills/speckit/scripts/bash/create-new-feature.sh +259 -0
- package/.codex/skills/speckit/scripts/bash/extract-coded-points.sh +322 -0
- package/.codex/skills/speckit/scripts/bash/extract-spec-ids.sh +238 -0
- package/.codex/skills/speckit/scripts/bash/extract-tasks.sh +295 -0
- package/.codex/skills/speckit/scripts/bash/extract-user-stories.sh +312 -0
- package/.codex/skills/speckit/scripts/bash/setup-notes.sh +182 -0
- package/.codex/skills/speckit/scripts/bash/setup-plan.sh +110 -0
- package/.codex/skills/speckit/scripts/bash/show-todo-tasks.sh +257 -0
- package/.codex/skills/speckit/scripts/bash/spec-group-checklist.sh +402 -0
- package/.codex/skills/speckit/scripts/bash/spec-group-members.sh +215 -0
- package/.codex/skills/speckit/scripts/bash/spec-registry-graph.sh +399 -0
- package/.specify/memory/constitution.md +67 -0
- package/.specify/templates/agent-file-template.md +28 -0
- package/.specify/templates/checklist-template.md +49 -0
- package/.specify/templates/plan-template.md +126 -0
- package/.specify/templates/spec-template.md +135 -0
- package/.specify/templates/tasks-template.md +269 -0
- package/README.md +128 -0
- package/README.zh-CN.md +127 -0
- package/bun.lock +269 -0
- package/dist/cli/commands/codex/forkHome.js +88 -0
- package/dist/cli/commands/codex/send.js +55 -0
- package/dist/cli/commands/codex/sessionInfo.js +42 -0
- package/dist/cli/commands/codex/spawn.js +68 -0
- package/dist/cli/commands/find.js +26 -0
- package/dist/cli/commands/paneKill.js +33 -0
- package/dist/cli/commands/paneSpawn.js +40 -0
- package/dist/cli/commands/paneTitle.js +33 -0
- package/dist/cli/commands/read.js +34 -0
- package/dist/cli/commands/send.js +51 -0
- package/dist/cli/commands/snapshot.js +19 -0
- package/dist/cli/commands/ui/select.js +41 -0
- package/dist/cli/commands/windowKill.js +25 -0
- package/dist/cli/commands/windowLs.js +15 -0
- package/dist/cli/commands/windowNew.js +28 -0
- package/dist/cli/commands/windowRename.js +25 -0
- package/dist/cli/index.js +365 -0
- package/dist/cli/parse.js +39 -0
- package/dist/lib/codex/forkHome.js +101 -0
- package/dist/lib/codex/isCodexPane.js +55 -0
- package/dist/lib/codex/send.js +58 -0
- package/dist/lib/codex/sessionInfo.js +449 -0
- package/dist/lib/codex/spawn.js +246 -0
- package/dist/lib/contracts/types.js +2 -0
- package/dist/lib/fs/safeRm.js +32 -0
- package/dist/lib/io/readStdin.js +14 -0
- package/dist/lib/os/process.js +55 -0
- package/dist/lib/output/format.js +95 -0
- package/dist/lib/proc/lsof.js +42 -0
- package/dist/lib/proc/ps.js +60 -0
- package/dist/lib/targeting/errors.js +13 -0
- package/dist/lib/targeting/resolvePaneTarget.js +91 -0
- package/dist/lib/targeting/resolveWindowTarget.js +40 -0
- package/dist/lib/targeting/scope.js +58 -0
- package/dist/lib/tmux/capturePane.js +20 -0
- package/dist/lib/tmux/exec.js +66 -0
- package/dist/lib/tmux/paneOps.js +29 -0
- package/dist/lib/tmux/paste.js +23 -0
- package/dist/lib/tmux/sendKeys.js +47 -0
- package/dist/lib/tmux/session.js +29 -0
- package/dist/lib/tmux/snapshotPanes.js +46 -0
- package/dist/lib/tmux/snapshotWindows.js +24 -0
- package/dist/lib/tmux/windowOps.js +32 -0
- package/dist/lib/ui/popupSelect.js +432 -0
- package/dist/lib/ui/popupSupport.js +76 -0
- package/package.json +23 -0
- package/src/cli/commands/codex/forkHome.ts +141 -0
- package/src/cli/commands/codex/send.ts +83 -0
- package/src/cli/commands/codex/sessionInfo.ts +59 -0
- package/src/cli/commands/codex/spawn.ts +90 -0
- package/src/cli/commands/find.ts +40 -0
- package/src/cli/commands/paneKill.ts +49 -0
- package/src/cli/commands/paneSpawn.ts +53 -0
- package/src/cli/commands/paneTitle.ts +50 -0
- package/src/cli/commands/read.ts +48 -0
- package/src/cli/commands/send.ts +71 -0
- package/src/cli/commands/snapshot.ts +28 -0
- package/src/cli/commands/ui/select.ts +49 -0
- package/src/cli/commands/windowKill.ts +35 -0
- package/src/cli/commands/windowLs.ts +20 -0
- package/src/cli/commands/windowNew.ts +40 -0
- package/src/cli/commands/windowRename.ts +36 -0
- package/src/cli/index.ts +430 -0
- package/src/lib/codex/forkHome.ts +148 -0
- package/src/lib/codex/isCodexPane.ts +56 -0
- package/src/lib/codex/send.ts +84 -0
- package/src/lib/codex/sessionInfo.ts +521 -0
- package/src/lib/codex/spawn.ts +305 -0
- package/src/lib/contracts/types.ts +30 -0
- package/src/lib/fs/safeRm.ts +32 -0
- package/src/lib/io/readStdin.ts +11 -0
- package/src/lib/output/format.ts +105 -0
- package/src/lib/proc/lsof.ts +44 -0
- package/src/lib/proc/ps.ts +70 -0
- package/src/lib/targeting/errors.ts +25 -0
- package/src/lib/targeting/resolvePaneTarget.ts +106 -0
- package/src/lib/targeting/resolveWindowTarget.ts +45 -0
- package/src/lib/targeting/scope.ts +76 -0
- package/src/lib/tmux/capturePane.ts +21 -0
- package/src/lib/tmux/exec.ts +90 -0
- package/src/lib/tmux/paneOps.ts +35 -0
- package/src/lib/tmux/paste.ts +20 -0
- package/src/lib/tmux/sendKeys.ts +72 -0
- package/src/lib/tmux/session.ts +27 -0
- package/src/lib/tmux/snapshotPanes.ts +52 -0
- package/src/lib/tmux/snapshotWindows.ts +23 -0
- package/src/lib/tmux/windowOps.ts +43 -0
- package/src/lib/ui/popupSelect.ts +561 -0
- package/src/lib/ui/popupSupport.ts +84 -0
- package/tests/e2e/codexForkHome.test.ts +146 -0
- package/tests/e2e/codexSessionInfo.test.ts +112 -0
- package/tests/e2e/codexTuiSend.test.ts +68 -0
- package/tests/integration/codexSpawn.test.ts +113 -0
- package/tests/integration/paneOps.test.ts +60 -0
- package/tests/integration/sendRead.test.ts +52 -0
- package/tests/integration/snapshot.test.ts +39 -0
- package/tests/integration/tmuxHarness.ts +39 -0
- package/tests/integration/windowOps.test.ts +60 -0
- package/tests/unit/codexSend.test.ts +105 -0
- package/tests/unit/codexSessionInfo.test.ts +88 -0
- package/tests/unit/codexSpawn.test.ts +34 -0
- package/tests/unit/keys.test.ts +30 -0
- package/tests/unit/outputFormat.test.ts +52 -0
- package/tests/unit/popupSelect.test.ts +77 -0
- package/tests/unit/popupSupport.test.ts +109 -0
- package/tests/unit/resolvePaneTarget.test.ts +43 -0
- package/tests/unit/resolveWindowTarget.test.ts +36 -0
- package/tests/unit/safeRm.test.ts +41 -0
- package/tests/unit/scope.test.ts +57 -0
- package/tsconfig.json +14 -0
- package/vitest.config.ts +16 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { codexSendCommand } from "../../src/cli/commands/codex/send";
|
|
3
|
+
import { codexSend } from "../../src/lib/codex/send";
|
|
4
|
+
import { readStdin } from "../../src/lib/io/readStdin";
|
|
5
|
+
import { resolveSessionScope } from "../../src/lib/targeting/scope";
|
|
6
|
+
import { snapshotPanes } from "../../src/lib/tmux/snapshotPanes";
|
|
7
|
+
import { tmuxExec } from "../../src/lib/tmux/exec";
|
|
8
|
+
import { pasteText } from "../../src/lib/tmux/paste";
|
|
9
|
+
import { capturePane } from "../../src/lib/tmux/capturePane";
|
|
10
|
+
|
|
11
|
+
vi.mock("../../src/lib/io/readStdin", () => ({
|
|
12
|
+
readStdin: vi.fn()
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
vi.mock("../../src/lib/targeting/scope", () => ({
|
|
16
|
+
resolveSessionScope: vi.fn(),
|
|
17
|
+
resolveWindowScope: vi.fn()
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
vi.mock("../../src/lib/tmux/snapshotPanes", () => ({
|
|
21
|
+
snapshotPanes: vi.fn()
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
vi.mock("../../src/lib/tmux/exec", () => ({
|
|
25
|
+
tmuxExec: vi.fn()
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
vi.mock("../../src/lib/tmux/paste", () => ({
|
|
29
|
+
pasteText: vi.fn()
|
|
30
|
+
}));
|
|
31
|
+
|
|
32
|
+
vi.mock("../../src/lib/tmux/capturePane", () => ({
|
|
33
|
+
capturePane: vi.fn()
|
|
34
|
+
}));
|
|
35
|
+
|
|
36
|
+
describe("codex send", () => {
|
|
37
|
+
it("treats key expressions as key mode", async () => {
|
|
38
|
+
vi.mocked(tmuxExec).mockResolvedValue({ stdout: "", stderr: "", exitCode: 0 });
|
|
39
|
+
vi.mocked(capturePane).mockResolvedValue("tail");
|
|
40
|
+
|
|
41
|
+
const result = await codexSend("%1", "Ctrl+C", { captureTailLines: 5 });
|
|
42
|
+
|
|
43
|
+
expect(vi.mocked(pasteText)).not.toHaveBeenCalled();
|
|
44
|
+
expect(vi.mocked(tmuxExec)).toHaveBeenCalledWith(["send-keys", "-t", "%1", "C-c"]);
|
|
45
|
+
expect(result.mode).toBe("keys");
|
|
46
|
+
expect(result.capture_tail).toBe("tail");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("reads stdin when text is '-'", async () => {
|
|
50
|
+
vi.useFakeTimers();
|
|
51
|
+
vi.mocked(resolveSessionScope).mockResolvedValue({ session: "s", source: "explicit" });
|
|
52
|
+
vi.mocked(snapshotPanes).mockResolvedValue([
|
|
53
|
+
{
|
|
54
|
+
id: "%1",
|
|
55
|
+
idx: 0,
|
|
56
|
+
pid: 123,
|
|
57
|
+
command: "zsh",
|
|
58
|
+
title: "codex",
|
|
59
|
+
status: "active",
|
|
60
|
+
windowId: "@1"
|
|
61
|
+
}
|
|
62
|
+
]);
|
|
63
|
+
vi.mocked(readStdin).mockResolvedValue("hello from stdin");
|
|
64
|
+
vi.mocked(tmuxExec).mockResolvedValue({ stdout: "", stderr: "", exitCode: 0 });
|
|
65
|
+
vi.mocked(pasteText).mockResolvedValue();
|
|
66
|
+
|
|
67
|
+
const promise = codexSendCommand("%1", "-", { json: true });
|
|
68
|
+
await vi.advanceTimersByTimeAsync(250);
|
|
69
|
+
const output = await promise;
|
|
70
|
+
vi.useRealTimers();
|
|
71
|
+
|
|
72
|
+
const parsed = JSON.parse(output);
|
|
73
|
+
expect(parsed.requested).toEqual({ target: "%1", text_source: "stdin" });
|
|
74
|
+
expect(parsed.resolved.id).toBe("%1");
|
|
75
|
+
expect(parsed.sent.submit).toBe("Enter");
|
|
76
|
+
expect(parsed.sent.submit_delay_ms).toBe(250);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("returns captured tail when --capture-tail is set without --json", async () => {
|
|
80
|
+
vi.useFakeTimers();
|
|
81
|
+
vi.mocked(resolveSessionScope).mockResolvedValue({ session: "s", source: "explicit" });
|
|
82
|
+
vi.mocked(snapshotPanes).mockResolvedValue([
|
|
83
|
+
{
|
|
84
|
+
id: "%1",
|
|
85
|
+
idx: 0,
|
|
86
|
+
pid: 123,
|
|
87
|
+
command: "zsh",
|
|
88
|
+
title: "codex",
|
|
89
|
+
status: "active",
|
|
90
|
+
windowId: "@1"
|
|
91
|
+
}
|
|
92
|
+
]);
|
|
93
|
+
vi.mocked(tmuxExec).mockResolvedValue({ stdout: "", stderr: "", exitCode: 0 });
|
|
94
|
+
vi.mocked(pasteText).mockResolvedValue();
|
|
95
|
+
vi.mocked(capturePane).mockResolvedValue("tail");
|
|
96
|
+
|
|
97
|
+
const promise = codexSendCommand("%1", "继续", { captureTail: 5 });
|
|
98
|
+
await vi.advanceTimersByTimeAsync(250);
|
|
99
|
+
const output = await promise;
|
|
100
|
+
vi.useRealTimers();
|
|
101
|
+
|
|
102
|
+
expect(output).toBe("tail");
|
|
103
|
+
expect(vi.mocked(capturePane)).toHaveBeenCalledWith("%1", 5);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { describe, expect, it, vi } from "vitest";
|
|
5
|
+
import { resolveCodexSessionInfo } from "../../src/lib/codex/sessionInfo";
|
|
6
|
+
import { listProcesses } from "../../src/lib/proc/ps";
|
|
7
|
+
import { listOpenFiles } from "../../src/lib/proc/lsof";
|
|
8
|
+
|
|
9
|
+
vi.mock("../../src/lib/proc/ps", () => ({
|
|
10
|
+
listProcesses: vi.fn()
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
vi.mock("../../src/lib/proc/lsof", () => ({
|
|
14
|
+
listOpenFiles: vi.fn()
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
describe("codex session-info", () => {
|
|
18
|
+
it("rejects multiple rollout candidates with diagnostics", async () => {
|
|
19
|
+
const codexHome = await fs.mkdtemp(path.join(os.tmpdir(), "agent-tmux-codex-"));
|
|
20
|
+
const sessionsDir = path.join(codexHome, "sessions");
|
|
21
|
+
await fs.mkdir(sessionsDir, { recursive: true });
|
|
22
|
+
const rolloutA = path.join(
|
|
23
|
+
sessionsDir,
|
|
24
|
+
"rollout-11111111-1111-1111-1111-111111111111.jsonl"
|
|
25
|
+
);
|
|
26
|
+
const rolloutB = path.join(
|
|
27
|
+
sessionsDir,
|
|
28
|
+
"rollout-22222222-2222-2222-2222-222222222222.jsonl"
|
|
29
|
+
);
|
|
30
|
+
await fs.writeFile(rolloutA, "{}", "utf8");
|
|
31
|
+
await fs.writeFile(rolloutB, "{}", "utf8");
|
|
32
|
+
|
|
33
|
+
vi.mocked(listProcesses).mockResolvedValue([
|
|
34
|
+
{ pid: 100, ppid: 1, tty: "ttys001", command: "codex" },
|
|
35
|
+
{ pid: 101, ppid: 1, tty: "ttys002", command: "codex" }
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
vi.mocked(listOpenFiles).mockImplementation(async (pid: number) => {
|
|
39
|
+
if (pid === 100) {
|
|
40
|
+
return [rolloutA];
|
|
41
|
+
}
|
|
42
|
+
if (pid === 101) {
|
|
43
|
+
return [rolloutB];
|
|
44
|
+
}
|
|
45
|
+
return [];
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
await resolveCodexSessionInfo({ codexHome });
|
|
50
|
+
throw new Error("expected resolveCodexSessionInfo to throw");
|
|
51
|
+
} catch (error: unknown) {
|
|
52
|
+
expect(error).toBeInstanceOf(Error);
|
|
53
|
+
const message = (error as Error).message;
|
|
54
|
+
expect(message).toContain("multiple codex sessions found");
|
|
55
|
+
expect(message).toContain(`pid=100`);
|
|
56
|
+
expect(message).toContain(rolloutA);
|
|
57
|
+
expect(message).toContain(`pid=101`);
|
|
58
|
+
expect(message).toContain(rolloutB);
|
|
59
|
+
} finally {
|
|
60
|
+
await fs.rm(codexHome, { recursive: true, force: true });
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("returns session_id and rollout_path when unique", async () => {
|
|
65
|
+
const codexHome = await fs.mkdtemp(path.join(os.tmpdir(), "agent-tmux-codex-"));
|
|
66
|
+
const sessionsDir = path.join(codexHome, "sessions");
|
|
67
|
+
await fs.mkdir(sessionsDir, { recursive: true });
|
|
68
|
+
const rollout = path.join(
|
|
69
|
+
sessionsDir,
|
|
70
|
+
"rollout-33333333-3333-3333-3333-333333333333.jsonl"
|
|
71
|
+
);
|
|
72
|
+
await fs.writeFile(rollout, "{}", "utf8");
|
|
73
|
+
|
|
74
|
+
vi.mocked(listProcesses).mockResolvedValue([
|
|
75
|
+
{ pid: 100, ppid: 1, tty: "ttys001", command: "codex" }
|
|
76
|
+
]);
|
|
77
|
+
vi.mocked(listOpenFiles).mockResolvedValue([rollout]);
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
const info = await resolveCodexSessionInfo({ codexHome });
|
|
81
|
+
expect(info.sessionId).toBe("33333333-3333-3333-3333-333333333333");
|
|
82
|
+
expect(info.rolloutPath).toBe(rollout);
|
|
83
|
+
} finally {
|
|
84
|
+
await fs.rm(codexHome, { recursive: true, force: true });
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { codexSpawnCommand } from "../../src/cli/commands/codex/spawn";
|
|
3
|
+
import { tmuxExec } from "../../src/lib/tmux/exec";
|
|
4
|
+
|
|
5
|
+
vi.mock("../../src/lib/tmux/exec", () => ({
|
|
6
|
+
tmuxExec: vi.fn()
|
|
7
|
+
}));
|
|
8
|
+
|
|
9
|
+
describe("codex spawn", () => {
|
|
10
|
+
it("refuses to run without an origin target outside tmux", async () => {
|
|
11
|
+
const previous = {
|
|
12
|
+
TMUX: process.env.TMUX,
|
|
13
|
+
TMUX_PANE: process.env.TMUX_PANE,
|
|
14
|
+
CODEX_PANE_ID: process.env.CODEX_PANE_ID,
|
|
15
|
+
ORIGIN_PANE_ID: process.env.ORIGIN_PANE_ID
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
delete process.env.TMUX;
|
|
19
|
+
delete process.env.TMUX_PANE;
|
|
20
|
+
delete process.env.CODEX_PANE_ID;
|
|
21
|
+
delete process.env.ORIGIN_PANE_ID;
|
|
22
|
+
|
|
23
|
+
vi.mocked(tmuxExec).mockResolvedValue({ stdout: "", stderr: "", exitCode: 0 });
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
await expect(codexSpawnCommand({})).rejects.toThrow("requires --origin");
|
|
27
|
+
} finally {
|
|
28
|
+
process.env.TMUX = previous.TMUX;
|
|
29
|
+
process.env.TMUX_PANE = previous.TMUX_PANE;
|
|
30
|
+
process.env.CODEX_PANE_ID = previous.CODEX_PANE_ID;
|
|
31
|
+
process.env.ORIGIN_PANE_ID = previous.ORIGIN_PANE_ID;
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { classifySendInput, parseKeyExpression } from "../../src/lib/tmux/sendKeys";
|
|
3
|
+
|
|
4
|
+
describe("send keys parsing", () => {
|
|
5
|
+
it("parses Ctrl+<A-Z>", () => {
|
|
6
|
+
expect(parseKeyExpression("Ctrl+C")).toBe("C-c");
|
|
7
|
+
expect(parseKeyExpression("Ctrl+z")).toBe("C-z");
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it("parses Enter", () => {
|
|
11
|
+
expect(parseKeyExpression("Enter")).toBe("Enter");
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("classifies key mode without enter", () => {
|
|
15
|
+
const result = classifySendInput("Ctrl+C", false);
|
|
16
|
+
expect(result.mode).toBe("keys");
|
|
17
|
+
expect(result.enter).toBe(false);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("classifies text mode with enter default", () => {
|
|
21
|
+
const result = classifySendInput("echo hi", false);
|
|
22
|
+
expect(result.mode).toBe("text");
|
|
23
|
+
expect(result.enter).toBe(true);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("respects no-enter for text", () => {
|
|
27
|
+
const result = classifySendInput("echo hi", true);
|
|
28
|
+
expect(result.enter).toBe(false);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
formatCommandTitle,
|
|
4
|
+
formatPaneStatus,
|
|
5
|
+
formatPaneTable,
|
|
6
|
+
formatWindowStatus,
|
|
7
|
+
formatWindowTable
|
|
8
|
+
} from "../../src/lib/output/format";
|
|
9
|
+
import type { PaneInfo, WindowInfo } from "../../src/lib/contracts/types";
|
|
10
|
+
|
|
11
|
+
describe("output format", () => {
|
|
12
|
+
it("formats window status", () => {
|
|
13
|
+
expect(formatWindowStatus("active")).toBe("[Active]");
|
|
14
|
+
expect(formatWindowStatus("inactive")).toBe("[Idle]");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("formats pane status", () => {
|
|
18
|
+
expect(formatPaneStatus("active")).toBe("[Active]");
|
|
19
|
+
expect(formatPaneStatus("idle")).toBe("[Idle]");
|
|
20
|
+
expect(formatPaneStatus("dead")).toBe("[Dead]");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("formats command/title", () => {
|
|
24
|
+
expect(formatCommandTitle("vim", "app.py")).toContain("vim");
|
|
25
|
+
expect(formatCommandTitle("vim", "app.py")).toContain("app.py");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("formats window table", () => {
|
|
29
|
+
const windows: WindowInfo[] = [
|
|
30
|
+
{ widx: 0, wid: "@0", name: "dev", status: "active" }
|
|
31
|
+
];
|
|
32
|
+
const table = formatWindowTable(windows);
|
|
33
|
+
expect(table).toContain("WIDX");
|
|
34
|
+
expect(table).toContain("@0");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("formats pane table", () => {
|
|
38
|
+
const panes: PaneInfo[] = [
|
|
39
|
+
{
|
|
40
|
+
idx: 0,
|
|
41
|
+
id: "%630",
|
|
42
|
+
status: "active",
|
|
43
|
+
command: "vim",
|
|
44
|
+
title: "app.py",
|
|
45
|
+
windowId: "@0"
|
|
46
|
+
}
|
|
47
|
+
];
|
|
48
|
+
const table = formatPaneTable(panes);
|
|
49
|
+
expect(table).toContain("COMMAND/TITLE");
|
|
50
|
+
expect(table).toContain("%630");
|
|
51
|
+
});
|
|
52
|
+
});
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
vi.mock("../../src/lib/ui/popupSupport", () => ({
|
|
4
|
+
checkPopupSupport: vi.fn()
|
|
5
|
+
}));
|
|
6
|
+
|
|
7
|
+
vi.mock("../../src/lib/tmux/exec", () => ({
|
|
8
|
+
tmuxExec: vi.fn()
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
import { popupSelect, normalizePopupSelectSpec } from "../../src/lib/ui/popupSelect";
|
|
12
|
+
import { checkPopupSupport } from "../../src/lib/ui/popupSupport";
|
|
13
|
+
import { tmuxExec } from "../../src/lib/tmux/exec";
|
|
14
|
+
|
|
15
|
+
describe("popup select", () => {
|
|
16
|
+
it("normalizes timeout_ms (default 1h, 0 means no timeout)", () => {
|
|
17
|
+
const normalizedDefault = normalizePopupSelectSpec({ choices: ["a"] });
|
|
18
|
+
expect(normalizedDefault.timeoutMs).toBe(3600000);
|
|
19
|
+
expect(normalizedDefault.waitForFocusTimeoutMs).toBe(3600000);
|
|
20
|
+
expect(normalizedDefault.waitForResultTimeoutMs).toBe(3600000);
|
|
21
|
+
|
|
22
|
+
const normalizedNoTimeout = normalizePopupSelectSpec({ choices: ["a"], timeout_ms: 0 });
|
|
23
|
+
expect(normalizedNoTimeout.timeoutMs).toBeNull();
|
|
24
|
+
expect(normalizedNoTimeout.waitForFocusTimeoutMs).toBeNull();
|
|
25
|
+
expect(normalizedNoTimeout.waitForResultTimeoutMs).toBeNull();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("supports percent sizing for width/height", () => {
|
|
29
|
+
const normalized = normalizePopupSelectSpec({
|
|
30
|
+
choices: ["a"],
|
|
31
|
+
width: "80%",
|
|
32
|
+
height: "60%"
|
|
33
|
+
});
|
|
34
|
+
expect(normalized.width).toBe("80%");
|
|
35
|
+
expect(normalized.height).toBe("60%");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("times out when popup never returns", async () => {
|
|
39
|
+
const previousPane = process.env.TMUX_PANE;
|
|
40
|
+
process.env.TMUX_PANE = "%1";
|
|
41
|
+
|
|
42
|
+
vi.mocked(checkPopupSupport).mockResolvedValue({ ok: true });
|
|
43
|
+
vi.mocked(tmuxExec).mockImplementation((args: string[]) => {
|
|
44
|
+
if (args[0] === "display-message") {
|
|
45
|
+
return Promise.resolve({ stdout: "@1\n", stderr: "", exitCode: 0 });
|
|
46
|
+
}
|
|
47
|
+
if (args[0] === "list-clients") {
|
|
48
|
+
return Promise.resolve({
|
|
49
|
+
stdout: "client1\t/dev/ttys001\tfocused\t$0\tsession\t@1\t%1\n",
|
|
50
|
+
stderr: "",
|
|
51
|
+
exitCode: 0
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
if (args[0] === "display-popup") {
|
|
55
|
+
return new Promise(() => undefined);
|
|
56
|
+
}
|
|
57
|
+
return Promise.resolve({ stdout: "", stderr: "", exitCode: 0 });
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
const promise = popupSelect({
|
|
62
|
+
mode: "single",
|
|
63
|
+
defer_if_unfocused: false,
|
|
64
|
+
defer_until_pane_active: false,
|
|
65
|
+
timeout_ms: 200,
|
|
66
|
+
choices: ["a"]
|
|
67
|
+
});
|
|
68
|
+
await expect(promise).rejects.toThrow("ui select timed out");
|
|
69
|
+
} finally {
|
|
70
|
+
if (previousPane === undefined) {
|
|
71
|
+
delete process.env.TMUX_PANE;
|
|
72
|
+
} else {
|
|
73
|
+
process.env.TMUX_PANE = previousPane;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
});
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { EventEmitter } from "node:events";
|
|
2
|
+
import { PassThrough } from "node:stream";
|
|
3
|
+
import { describe, expect, it, vi } from "vitest";
|
|
4
|
+
|
|
5
|
+
type SpawnReturn = EventEmitter & { stdout?: PassThrough; stderr?: PassThrough; stdin?: PassThrough };
|
|
6
|
+
|
|
7
|
+
let spawnImpl: (command: string, args: string[]) => SpawnReturn;
|
|
8
|
+
|
|
9
|
+
vi.mock("node:child_process", () => ({
|
|
10
|
+
spawn: (command: string, args: string[]) => spawnImpl(command, args)
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
import { checkPopupSupport } from "../../src/lib/ui/popupSupport";
|
|
14
|
+
|
|
15
|
+
function spawnOk(output: string): SpawnReturn {
|
|
16
|
+
const child: SpawnReturn = Object.assign(new EventEmitter(), {
|
|
17
|
+
stdout: new PassThrough(),
|
|
18
|
+
stderr: new PassThrough()
|
|
19
|
+
});
|
|
20
|
+
process.nextTick(() => {
|
|
21
|
+
child.stdout?.write(output);
|
|
22
|
+
child.stdout?.end();
|
|
23
|
+
child.emit("close", 0);
|
|
24
|
+
});
|
|
25
|
+
return child;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function spawnFail(stderr: string): SpawnReturn {
|
|
29
|
+
const child: SpawnReturn = Object.assign(new EventEmitter(), {
|
|
30
|
+
stdout: new PassThrough(),
|
|
31
|
+
stderr: new PassThrough()
|
|
32
|
+
});
|
|
33
|
+
process.nextTick(() => {
|
|
34
|
+
child.stderr?.write(stderr);
|
|
35
|
+
child.stderr?.end();
|
|
36
|
+
child.emit("close", 1);
|
|
37
|
+
});
|
|
38
|
+
return child;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
describe("popup support gate", () => {
|
|
42
|
+
it("rejects when not inside tmux", async () => {
|
|
43
|
+
const previous = { TMUX: process.env.TMUX, TMUX_PANE: process.env.TMUX_PANE };
|
|
44
|
+
delete process.env.TMUX;
|
|
45
|
+
delete process.env.TMUX_PANE;
|
|
46
|
+
|
|
47
|
+
spawnImpl = () => spawnOk("");
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
const support = await checkPopupSupport();
|
|
51
|
+
expect(support.ok).toBe(false);
|
|
52
|
+
expect(support.reason).toContain("inside tmux");
|
|
53
|
+
} finally {
|
|
54
|
+
process.env.TMUX = previous.TMUX;
|
|
55
|
+
process.env.TMUX_PANE = previous.TMUX_PANE;
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("rejects when tmux lacks display-popup", async () => {
|
|
60
|
+
const previous = { TMUX: process.env.TMUX, TMUX_PANE: process.env.TMUX_PANE };
|
|
61
|
+
process.env.TMUX = "/tmp/tmux,123,0";
|
|
62
|
+
process.env.TMUX_PANE = "%1";
|
|
63
|
+
|
|
64
|
+
spawnImpl = (command, args) => {
|
|
65
|
+
if (command === "tmux" && args[0] === "list-commands") {
|
|
66
|
+
return spawnOk("split-window\nlist-panes\n");
|
|
67
|
+
}
|
|
68
|
+
if (command === "fzf") {
|
|
69
|
+
return spawnOk("0.0.0\n");
|
|
70
|
+
}
|
|
71
|
+
return spawnFail("unexpected command");
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
const support = await checkPopupSupport();
|
|
76
|
+
expect(support.ok).toBe(false);
|
|
77
|
+
expect(support.reason).toContain("display-popup");
|
|
78
|
+
} finally {
|
|
79
|
+
process.env.TMUX = previous.TMUX;
|
|
80
|
+
process.env.TMUX_PANE = previous.TMUX_PANE;
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("rejects when fzf is missing", async () => {
|
|
85
|
+
const previous = { TMUX: process.env.TMUX, TMUX_PANE: process.env.TMUX_PANE };
|
|
86
|
+
process.env.TMUX = "/tmp/tmux,123,0";
|
|
87
|
+
process.env.TMUX_PANE = "%1";
|
|
88
|
+
|
|
89
|
+
spawnImpl = (command, args) => {
|
|
90
|
+
if (command === "tmux" && args[0] === "list-commands") {
|
|
91
|
+
return spawnOk("display-popup\n");
|
|
92
|
+
}
|
|
93
|
+
if (command === "fzf") {
|
|
94
|
+
return spawnFail("not found");
|
|
95
|
+
}
|
|
96
|
+
return spawnFail("unexpected command");
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
const support = await checkPopupSupport();
|
|
101
|
+
expect(support.ok).toBe(false);
|
|
102
|
+
expect(support.reason).toContain("fzf");
|
|
103
|
+
} finally {
|
|
104
|
+
process.env.TMUX = previous.TMUX;
|
|
105
|
+
process.env.TMUX_PANE = previous.TMUX_PANE;
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { resolvePaneTarget } from "../../src/lib/targeting/resolvePaneTarget";
|
|
3
|
+
import { TargetResolutionError } from "../../src/lib/targeting/errors";
|
|
4
|
+
import type { PaneInfo } from "../../src/lib/contracts/types";
|
|
5
|
+
|
|
6
|
+
const panes: PaneInfo[] = [
|
|
7
|
+
{ idx: 0, id: "%100", status: "active", command: "vim", title: "app.py", windowId: "@0" },
|
|
8
|
+
{ idx: 1, id: "%101", status: "idle", command: "npm", title: "run dev", windowId: "@0" },
|
|
9
|
+
{ idx: 2, id: "%102", status: "idle", command: "vim", title: "main.ts", windowId: "@0" },
|
|
10
|
+
{ idx: 0, id: "%200", status: "idle", command: "vim", title: "README.md", windowId: "@1" }
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
describe("resolvePaneTarget", () => {
|
|
14
|
+
it("resolves by pane id", () => {
|
|
15
|
+
const resolved = resolvePaneTarget(panes, "%101", { windowId: "@0" });
|
|
16
|
+
expect(resolved.id).toBe("%101");
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("resolves by idx within window", () => {
|
|
20
|
+
const resolved = resolvePaneTarget(panes, "0", { windowId: "@0" });
|
|
21
|
+
expect(resolved.id).toBe("%100");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("resolves by keyword", () => {
|
|
25
|
+
const resolved = resolvePaneTarget(panes, "npm", { windowId: "@0" });
|
|
26
|
+
expect(resolved.id).toBe("%101");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("throws on ambiguous keyword", () => {
|
|
30
|
+
expect(() => resolvePaneTarget(panes, "vim", { windowId: "@0" })).toThrow(
|
|
31
|
+
TargetResolutionError
|
|
32
|
+
);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("supports composite target", () => {
|
|
36
|
+
const resolved = resolvePaneTarget(panes, "@0%100");
|
|
37
|
+
expect(resolved.id).toBe("%100");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("throws on invalid composite target", () => {
|
|
41
|
+
expect(() => resolvePaneTarget(panes, "@2%100")).toThrow(TargetResolutionError);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { resolveWindowTarget } from "../../src/lib/targeting/resolveWindowTarget";
|
|
3
|
+
import { TargetResolutionError } from "../../src/lib/targeting/errors";
|
|
4
|
+
import type { WindowInfo } from "../../src/lib/contracts/types";
|
|
5
|
+
|
|
6
|
+
const windows: WindowInfo[] = [
|
|
7
|
+
{ widx: 0, wid: "@0", name: "dev", status: "active" },
|
|
8
|
+
{ widx: 1, wid: "@1", name: "logs", status: "inactive" },
|
|
9
|
+
{ widx: 2, wid: "@2", name: "dev-api", status: "inactive" }
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
describe("resolveWindowTarget", () => {
|
|
13
|
+
it("resolves by window id", () => {
|
|
14
|
+
const resolved = resolveWindowTarget(windows, "@1");
|
|
15
|
+
expect(resolved.wid).toBe("@1");
|
|
16
|
+
expect(resolved.widx).toBe(1);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("resolves by window index", () => {
|
|
20
|
+
const resolved = resolveWindowTarget(windows, "2");
|
|
21
|
+
expect(resolved.wid).toBe("@2");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("resolves by keyword", () => {
|
|
25
|
+
const resolved = resolveWindowTarget(windows, "logs");
|
|
26
|
+
expect(resolved.wid).toBe("@1");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("throws on ambiguous keyword", () => {
|
|
30
|
+
expect(() => resolveWindowTarget(windows, "dev")).toThrow(TargetResolutionError);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("throws on missing target", () => {
|
|
34
|
+
expect(() => resolveWindowTarget(windows, "missing")).toThrow(TargetResolutionError);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { describe, expect, it } from "vitest";
|
|
5
|
+
import { safeRemove } from "../../src/lib/fs/safeRm";
|
|
6
|
+
|
|
7
|
+
describe("safeRemove", () => {
|
|
8
|
+
it("refuses to remove allowed_root itself", async () => {
|
|
9
|
+
const root = await fs.mkdtemp(path.join(os.tmpdir(), "agent-tmux-safeRm-"));
|
|
10
|
+
try {
|
|
11
|
+
await expect(safeRemove(root, root)).rejects.toThrow("refuse to remove allowed root");
|
|
12
|
+
} finally {
|
|
13
|
+
await fs.rm(root, { recursive: true, force: true });
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("refuses to remove a path outside allowed_root", async () => {
|
|
18
|
+
const allowed = await fs.mkdtemp(path.join(os.tmpdir(), "agent-tmux-safeRm-allowed-"));
|
|
19
|
+
const outside = await fs.mkdtemp(path.join(os.tmpdir(), "agent-tmux-safeRm-outside-"));
|
|
20
|
+
try {
|
|
21
|
+
await expect(safeRemove(outside, allowed)).rejects.toThrow("outside allowed_root");
|
|
22
|
+
} finally {
|
|
23
|
+
await fs.rm(allowed, { recursive: true, force: true });
|
|
24
|
+
await fs.rm(outside, { recursive: true, force: true });
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("removes a path under allowed_root", async () => {
|
|
29
|
+
const allowed = await fs.mkdtemp(path.join(os.tmpdir(), "agent-tmux-safeRm-allowed-"));
|
|
30
|
+
const target = path.join(allowed, "subdir");
|
|
31
|
+
await fs.mkdir(target, { recursive: true });
|
|
32
|
+
await fs.writeFile(path.join(target, "file.txt"), "ok", "utf8");
|
|
33
|
+
|
|
34
|
+
const removed = await safeRemove(target, allowed);
|
|
35
|
+
expect(removed).toBe(true);
|
|
36
|
+
|
|
37
|
+
await expect(fs.stat(target)).rejects.toBeTruthy();
|
|
38
|
+
await fs.rm(allowed, { recursive: true, force: true });
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { resolveSessionScope, resolveWindowScope } from "../../src/lib/targeting/scope";
|
|
3
|
+
import { tmuxExec } from "../../src/lib/tmux/exec";
|
|
4
|
+
import { snapshotWindows } from "../../src/lib/tmux/snapshotWindows";
|
|
5
|
+
import type { WindowInfo } from "../../src/lib/contracts/types";
|
|
6
|
+
|
|
7
|
+
vi.mock("../../src/lib/tmux/exec", () => ({
|
|
8
|
+
tmuxExec: vi.fn()
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
vi.mock("../../src/lib/tmux/snapshotWindows", () => ({
|
|
12
|
+
snapshotWindows: vi.fn()
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
const tmuxExecMock = vi.mocked(tmuxExec);
|
|
16
|
+
const snapshotWindowsMock = vi.mocked(snapshotWindows);
|
|
17
|
+
|
|
18
|
+
const windows: WindowInfo[] = [
|
|
19
|
+
{ widx: 0, wid: "@0", name: "dev", status: "inactive" },
|
|
20
|
+
{ widx: 1, wid: "@1", name: "logs", status: "active" }
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
describe("scope", () => {
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
tmuxExecMock.mockReset();
|
|
26
|
+
snapshotWindowsMock.mockReset();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("resolves explicit session", async () => {
|
|
30
|
+
const result = await resolveSessionScope("work");
|
|
31
|
+
expect(result.session).toBe("work");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("resolves session inside tmux", async () => {
|
|
35
|
+
tmuxExecMock.mockResolvedValue({ stdout: "dev\n", stderr: "", exitCode: 0 });
|
|
36
|
+
const result = await resolveSessionScope(undefined, { TMUX: "1" });
|
|
37
|
+
expect(result.session).toBe("dev");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("requires explicit session when ambiguous", async () => {
|
|
41
|
+
tmuxExecMock.mockResolvedValue({
|
|
42
|
+
stdout: "a\nb\n",
|
|
43
|
+
stderr: "",
|
|
44
|
+
exitCode: 0
|
|
45
|
+
});
|
|
46
|
+
await expect(resolveSessionScope(undefined, {})).rejects.toThrow(
|
|
47
|
+
"default session is ambiguous"
|
|
48
|
+
);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("chooses active window by default", async () => {
|
|
52
|
+
snapshotWindowsMock.mockResolvedValue(windows);
|
|
53
|
+
const result = await resolveWindowScope("dev");
|
|
54
|
+
expect(result.wid).toBe("@1");
|
|
55
|
+
expect(result.name).toBe("logs");
|
|
56
|
+
});
|
|
57
|
+
});
|