oh-pi 0.1.77 → 0.1.78

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.
@@ -3,6 +3,15 @@ import { join } from "node:path";
3
3
  import { KEYBINDING_SCHEMES, MODEL_CAPABILITIES, PROVIDERS } from "../registry.js";
4
4
  import { resources } from "./resources.js";
5
5
  import { ensureDir, syncDir } from "./install.js";
6
+ const ANT_COLONY_AUTOTRIGGER_GUIDE = `## Ant Colony Auto-Trigger
7
+ If the ant_colony tool is available, automatically use it when the task is complex:
8
+ - 3 or more files likely need changes
9
+ - 2 or more independent workstreams exist
10
+ - large refactor / migration / feature implementation
11
+
12
+ For simple one-file tasks, execute directly without colony overhead.
13
+ After launching ant_colony, use passive mode: wait for COLONY_SIGNAL updates and do not poll bg_colony_status unless the user explicitly asks for a manual snapshot.
14
+ `;
6
15
  /** 步骤 1-2: 生成 auth.json + settings.json */
7
16
  export function writeProviderEnv(agentDir, config) {
8
17
  // auth.json
@@ -103,6 +112,9 @@ export function writeAgents(agentDir, config) {
103
112
  const lang = langNames[config.locale] ?? config.locale;
104
113
  content = `## Language\nAlways respond in ${lang}. Use the user's language for all conversations and explanations. Code, commands, and technical terms can remain in English.\n\n${content}`;
105
114
  }
115
+ if (config.extensions.includes("ant-colony") && config.agents !== "colony-operator") {
116
+ content = `${content.trimEnd()}\n\n${ANT_COLONY_AUTOTRIGGER_GUIDE}`;
117
+ }
106
118
  writeFileSync(join(agentDir, "AGENTS.md"), content);
107
119
  }
108
120
  catch { /* template not found, skip */ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,60 @@
1
+ import { describe, it, expect, afterEach } from "vitest";
2
+ import { mkdtempSync, readFileSync, rmSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { writeAgents } from "./writers.js";
6
+ const tempDirs = [];
7
+ function makeTempDir() {
8
+ const dir = mkdtempSync(join(tmpdir(), "oh-pi-writers-"));
9
+ tempDirs.push(dir);
10
+ return dir;
11
+ }
12
+ function makeConfig(overrides) {
13
+ return {
14
+ providers: [],
15
+ theme: "dark",
16
+ keybindings: "default",
17
+ extensions: [],
18
+ prompts: [],
19
+ agents: "general-developer",
20
+ thinking: "medium",
21
+ ...overrides,
22
+ };
23
+ }
24
+ afterEach(() => {
25
+ for (const dir of tempDirs.splice(0)) {
26
+ rmSync(dir, { recursive: true, force: true });
27
+ }
28
+ });
29
+ describe("writeAgents", () => {
30
+ it("appends ant-colony auto-trigger guidance for non-colony operator agents", () => {
31
+ const dir = makeTempDir();
32
+ writeAgents(dir, makeConfig({
33
+ agents: "general-developer",
34
+ extensions: ["ant-colony"],
35
+ }));
36
+ const content = readFileSync(join(dir, "AGENTS.md"), "utf8");
37
+ expect(content).toContain("## Ant Colony Auto-Trigger");
38
+ expect(content).toContain("automatically use it when the task is complex");
39
+ expect(content).toContain("COLONY_SIGNAL");
40
+ });
41
+ it("does not append guidance when ant-colony extension is disabled", () => {
42
+ const dir = makeTempDir();
43
+ writeAgents(dir, makeConfig({
44
+ agents: "general-developer",
45
+ extensions: [],
46
+ }));
47
+ const content = readFileSync(join(dir, "AGENTS.md"), "utf8");
48
+ expect(content).not.toContain("## Ant Colony Auto-Trigger");
49
+ });
50
+ it("does not append duplicate guidance for colony-operator template", () => {
51
+ const dir = makeTempDir();
52
+ writeAgents(dir, makeConfig({
53
+ agents: "colony-operator",
54
+ extensions: ["ant-colony"],
55
+ }));
56
+ const content = readFileSync(join(dir, "AGENTS.md"), "utf8");
57
+ expect(content).not.toContain("## Ant Colony Auto-Trigger");
58
+ expect(content).toContain("You command an autonomous ant colony");
59
+ });
60
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oh-pi",
3
- "version": "0.1.77",
3
+ "version": "0.1.78",
4
4
  "description": "One-click setup for pi-coding-agent. Like oh-my-zsh for pi.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -10,7 +10,9 @@
10
10
  "files": [
11
11
  "dist",
12
12
  "pi-package",
13
- "README.md"
13
+ "README.md",
14
+ "!**/*.test.ts",
15
+ "!**/*.spec.ts"
14
16
  ],
15
17
  "scripts": {
16
18
  "build": "tsc",
@@ -1,70 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import { defaultConcurrency, adapt } from "./concurrency.js";
3
- import type { ConcurrencyConfig, ConcurrencySample } from "./types.js";
4
-
5
- const mkSample = (o: Partial<ConcurrencySample> = {}): ConcurrencySample => ({
6
- timestamp: Date.now(), concurrency: 2, cpuLoad: 0.3, memFree: 4e9, throughput: 1, ...o,
7
- });
8
-
9
- describe("defaultConcurrency", () => {
10
- it("returns valid config", () => {
11
- const c = defaultConcurrency();
12
- expect(c.current).toBe(2);
13
- expect(c.min).toBe(1);
14
- expect(c.max).toBeGreaterThanOrEqual(1);
15
- expect(c.max).toBeLessThanOrEqual(8);
16
- expect(c.history).toEqual([]);
17
- });
18
- });
19
-
20
- describe("adapt", () => {
21
- it("drops to min when no pending tasks", () => {
22
- const cfg: ConcurrencyConfig = { current: 4, min: 1, max: 8, optimal: 3, history: [mkSample(), mkSample()] };
23
- expect(adapt(cfg, 0).current).toBe(1);
24
- });
25
-
26
- it("cold start gives half max", () => {
27
- const cfg: ConcurrencyConfig = { current: 2, min: 1, max: 8, optimal: 3, history: [mkSample()] };
28
- expect(adapt(cfg, 10).current).toBe(4);
29
- });
30
-
31
- it("reduces when CPU > 85%", () => {
32
- const s = mkSample({ cpuLoad: 0.9 });
33
- const cfg: ConcurrencyConfig = { current: 4, min: 1, max: 8, optimal: 3, history: [s, s, s] };
34
- expect(adapt(cfg, 10).current).toBeLessThan(4);
35
- });
36
-
37
- it("reduces when memory low", () => {
38
- const s = mkSample({ memFree: 100 * 1024 * 1024 });
39
- const cfg: ConcurrencyConfig = { current: 4, min: 1, max: 8, optimal: 3, history: [s, s] };
40
- expect(adapt(cfg, 10).current).toBeLessThan(4);
41
- });
42
-
43
- it("increases during exploration when throughput rising", () => {
44
- const s1 = mkSample({ throughput: 1 });
45
- const s2 = mkSample({ throughput: 2 });
46
- const cfg: ConcurrencyConfig = { current: 3, min: 1, max: 8, optimal: 3, history: [s1, s2] };
47
- expect(adapt(cfg, 10).current).toBeGreaterThanOrEqual(3);
48
- });
49
-
50
- it("does not exceed max", () => {
51
- const s1 = mkSample({ throughput: 1 });
52
- const s2 = mkSample({ throughput: 5 });
53
- const cfg: ConcurrencyConfig = { current: 8, min: 1, max: 8, optimal: 3, history: [s1, s2] };
54
- expect(adapt(cfg, 100).current).toBeLessThanOrEqual(8);
55
- });
56
-
57
- it("does not exceed pending task count", () => {
58
- const s1 = mkSample({ throughput: 1 });
59
- const s2 = mkSample({ throughput: 5 });
60
- const cfg: ConcurrencyConfig = { current: 3, min: 1, max: 8, optimal: 3, history: [s1, s2] };
61
- expect(adapt(cfg, 2).current).toBeLessThanOrEqual(2);
62
- });
63
-
64
- it("respects rate limit cooldown", () => {
65
- const s1 = mkSample({ throughput: 1 });
66
- const s2 = mkSample({ throughput: 2 });
67
- const cfg: ConcurrencyConfig = { current: 3, min: 1, max: 8, optimal: 3, history: [s1, s2], lastRateLimitAt: Date.now() };
68
- expect(adapt(cfg, 10).current).toBeLessThanOrEqual(3);
69
- });
70
- });
@@ -1,62 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import { buildImportGraph, dependencyDepth, taskDependsOn } from "./deps.js";
3
- import type { ImportGraph } from "./deps.js";
4
-
5
- describe("buildImportGraph", () => {
6
- it("returns empty graph for empty files", () => {
7
- const graph = buildImportGraph([], "/tmp");
8
- expect(graph.imports.size).toBe(0);
9
- expect(graph.importedBy.size).toBe(0);
10
- });
11
-
12
- it("returns empty graph for nonexistent files", () => {
13
- const graph = buildImportGraph(["nonexistent.ts"], "/tmp");
14
- expect(graph.imports.size).toBe(0);
15
- });
16
- });
17
-
18
- describe("dependencyDepth", () => {
19
- it("returns 0 for file with no dependents", () => {
20
- const graph: ImportGraph = { imports: new Map(), importedBy: new Map() };
21
- expect(dependencyDepth("a.ts", graph)).toBe(0);
22
- });
23
-
24
- it("counts direct dependents", () => {
25
- const graph: ImportGraph = {
26
- imports: new Map([["b.ts", new Set(["a.ts"])]]),
27
- importedBy: new Map([["a.ts", new Set(["b.ts"])]]),
28
- };
29
- expect(dependencyDepth("a.ts", graph)).toBe(1);
30
- });
31
-
32
- it("counts transitive dependents", () => {
33
- const graph: ImportGraph = {
34
- imports: new Map([["b.ts", new Set(["a.ts"])], ["c.ts", new Set(["b.ts"])]]),
35
- importedBy: new Map([["a.ts", new Set(["b.ts"])], ["b.ts", new Set(["c.ts"])]]),
36
- };
37
- expect(dependencyDepth("a.ts", graph)).toBe(2);
38
- });
39
- });
40
-
41
- describe("taskDependsOn", () => {
42
- it("returns true when taskA imports taskB file", () => {
43
- const graph: ImportGraph = {
44
- imports: new Map([["a.ts", new Set(["b.ts"])]]),
45
- importedBy: new Map([["b.ts", new Set(["a.ts"])]]),
46
- };
47
- expect(taskDependsOn(["a.ts"], ["b.ts"], graph)).toBe(true);
48
- });
49
-
50
- it("returns false when no dependency", () => {
51
- const graph: ImportGraph = {
52
- imports: new Map([["a.ts", new Set(["c.ts"])]]),
53
- importedBy: new Map(),
54
- };
55
- expect(taskDependsOn(["a.ts"], ["b.ts"], graph)).toBe(false);
56
- });
57
-
58
- it("returns false for empty file lists", () => {
59
- const graph: ImportGraph = { imports: new Map(), importedBy: new Map() };
60
- expect(taskDependsOn([], [], graph)).toBe(false);
61
- });
62
- });
@@ -1,130 +0,0 @@
1
- import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
- import * as fs from "node:fs";
3
- import * as path from "node:path";
4
- import * as os from "node:os";
5
- import { Nest } from "./nest.js";
6
- import type { ColonyState, Pheromone } from "./types.js";
7
-
8
- const mkState = (overrides: Partial<ColonyState> = {}): ColonyState => ({
9
- id: "test-colony", goal: "test", status: "working",
10
- tasks: [], ants: [], pheromones: [],
11
- concurrency: { current: 2, min: 1, max: 4, optimal: 3, history: [] },
12
- metrics: { tasksTotal: 0, tasksDone: 0, tasksFailed: 0, antsSpawned: 0, totalCost: 0, totalTokens: 0, startTime: Date.now(), throughputHistory: [] },
13
- maxCost: null, modelOverrides: {}, createdAt: Date.now(), finishedAt: null,
14
- ...overrides,
15
- });
16
-
17
- const mkPheromone = (overrides: Partial<Pheromone> = {}): Pheromone => ({
18
- id: `p-${Math.random().toString(36).slice(2)}`, type: "warning", antId: "ant-1", antCaste: "worker",
19
- taskId: "t-1", content: "test", files: ["a.ts"], strength: 1.0, createdAt: Date.now(),
20
- ...overrides,
21
- });
22
-
23
- let tmpDir: string;
24
- let nest: Nest;
25
-
26
- beforeEach(() => {
27
- tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "nest-test-"));
28
- nest = new Nest(tmpDir, "test-colony");
29
- nest.init(mkState());
30
- });
31
-
32
- afterEach(() => {
33
- try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch { /* ignore */ }
34
- });
35
-
36
- describe("getStateLight", () => {
37
- it("returns state without triggering pheromone read", () => {
38
- nest.dropPheromone(mkPheromone());
39
- const light = nest.getStateLight();
40
- expect(light.id).toBe("test-colony");
41
- expect(light.tasks).toEqual([]);
42
- // pheromones should not be populated by getStateLight
43
- // (it returns stateCache which has empty pheromones from init)
44
- });
45
-
46
- it("includes tasks from cache", () => {
47
- nest.writeTask({
48
- id: "t-1", parentId: null, title: "Test", description: "desc",
49
- caste: "worker", status: "pending", priority: 3, files: [],
50
- claimedBy: null, result: null, error: null, spawnedTasks: [],
51
- createdAt: Date.now(), startedAt: null, finishedAt: null,
52
- });
53
- const light = nest.getStateLight();
54
- expect(light.tasks).toHaveLength(1);
55
- expect(light.tasks[0].id).toBe("t-1");
56
- });
57
- });
58
-
59
- describe("countWarnings", () => {
60
- it("returns 0 when no pheromones", () => {
61
- expect(nest.countWarnings(["a.ts"])).toBe(0);
62
- });
63
-
64
- it("counts warning pheromones for matching files", () => {
65
- nest.dropPheromone(mkPheromone({ type: "warning", files: ["a.ts"] }));
66
- nest.dropPheromone(mkPheromone({ type: "warning", files: ["a.ts"] }));
67
- nest.dropPheromone(mkPheromone({ type: "completion", files: ["a.ts"] }));
68
- expect(nest.countWarnings(["a.ts"])).toBe(2);
69
- });
70
-
71
- it("counts repellent pheromones", () => {
72
- nest.dropPheromone(mkPheromone({ type: "repellent", files: ["b.ts"] }));
73
- expect(nest.countWarnings(["b.ts"])).toBe(1);
74
- });
75
-
76
- it("returns 0 for unrelated files", () => {
77
- nest.dropPheromone(mkPheromone({ type: "warning", files: ["a.ts"] }));
78
- expect(nest.countWarnings(["c.ts"])).toBe(0);
79
- });
80
- });
81
-
82
- describe("pheromone dirty flag", () => {
83
- it("rebuilds index after dropPheromone", () => {
84
- nest.dropPheromone(mkPheromone({ type: "discovery", files: ["x.ts"] }));
85
- const pheromones = nest.getAllPheromones();
86
- expect(pheromones.length).toBe(1);
87
- expect(pheromones[0].type).toBe("discovery");
88
- });
89
-
90
- it("does not rebuild index when nothing changed", () => {
91
- nest.dropPheromone(mkPheromone({ files: ["x.ts"] }));
92
- nest.getAllPheromones(); // builds index, clears dirty
93
- // Second call should use cached index (no new data, no GC)
94
- const p2 = nest.getAllPheromones();
95
- expect(p2.length).toBe(1);
96
- });
97
- });
98
-
99
- describe("claimNextTask", () => {
100
- it("claims highest scored pending task", () => {
101
- nest.writeTask({
102
- id: "t-low", parentId: null, title: "Low", description: "",
103
- caste: "worker", status: "pending", priority: 5, files: [],
104
- claimedBy: null, result: null, error: null, spawnedTasks: [],
105
- createdAt: Date.now(), startedAt: null, finishedAt: null,
106
- });
107
- nest.writeTask({
108
- id: "t-high", parentId: null, title: "High", description: "",
109
- caste: "worker", status: "pending", priority: 1, files: [],
110
- claimedBy: null, result: null, error: null, spawnedTasks: [],
111
- createdAt: Date.now(), startedAt: null, finishedAt: null,
112
- });
113
- const claimed = nest.claimNextTask("worker", "ant-1");
114
- expect(claimed).not.toBeNull();
115
- expect(claimed!.id).toBe("t-high");
116
- expect(claimed!.status).toBe("claimed");
117
- });
118
-
119
- it("returns null when no pending tasks", () => {
120
- expect(nest.claimNextTask("worker", "ant-1")).toBeNull();
121
- });
122
- });
123
-
124
- describe("withStateLock spin", () => {
125
- it("updateState works under normal conditions", () => {
126
- nest.updateState({ status: "reviewing" });
127
- const state = nest.getStateLight();
128
- expect(state.status).toBe("reviewing");
129
- });
130
- });
@@ -1,143 +0,0 @@
1
- import { describe, it, expect, vi } from "vitest";
2
-
3
- vi.mock("@mariozechner/pi-coding-agent", () => ({
4
- AuthStorage: class {},
5
- createAgentSession: vi.fn(),
6
- createReadTool: vi.fn(), createBashTool: vi.fn(), createEditTool: vi.fn(),
7
- createWriteTool: vi.fn(), createGrepTool: vi.fn(), createFindTool: vi.fn(),
8
- createLsTool: vi.fn(), ModelRegistry: class {}, SessionManager: { inMemory: vi.fn() },
9
- SettingsManager: { inMemory: vi.fn() }, createExtensionRuntime: vi.fn(),
10
- }));
11
- vi.mock("@mariozechner/pi-ai", () => ({ getModel: vi.fn() }));
12
- vi.mock("./spawner.js", async () => {
13
- const actual = await vi.importActual<any>("./spawner.js");
14
- return { ...actual, makePheromoneId: () => "p-test" };
15
- });
16
-
17
- import { parseSubTasks, extractPheromones } from "./parser.js";
18
-
19
- describe("parseSubTasks", () => {
20
- it("parses markdown TASK blocks", () => {
21
- const output = `## Recommended Tasks
22
- ### TASK: Fix login
23
- - description: Fix the login bug
24
- - files: src/auth.ts
25
- - caste: worker
26
- - priority: 2`;
27
- const tasks = parseSubTasks(output);
28
- expect(tasks).toHaveLength(1);
29
- expect(tasks[0].title).toBe("Fix login");
30
- expect(tasks[0].description).toBe("Fix the login bug");
31
- expect(tasks[0].files).toEqual(["src/auth.ts"]);
32
- expect(tasks[0].caste).toBe("worker");
33
- expect(tasks[0].priority).toBe(2);
34
- });
35
-
36
- it("parses JSON block", () => {
37
- const output = '```json\n[{"title":"Task A","description":"Do A","files":["a.ts"],"caste":"scout","priority":1}]\n```';
38
- const tasks = parseSubTasks(output);
39
- expect(tasks).toHaveLength(1);
40
- expect(tasks[0].title).toBe("Task A");
41
- expect(tasks[0].caste).toBe("scout");
42
- });
43
-
44
- it("defaults caste to worker for invalid", () => {
45
- const tasks = parseSubTasks('```json\n[{"title":"X","caste":"invalid"}]\n```');
46
- expect(tasks[0].caste).toBe("worker");
47
- });
48
-
49
- it("defaults priority to 3", () => {
50
- const tasks = parseSubTasks('```json\n[{"title":"X"}]\n```');
51
- expect(tasks[0].priority).toBe(3);
52
- });
53
-
54
- it("returns empty for no tasks", () => {
55
- expect(parseSubTasks("no tasks here")).toEqual([]);
56
- });
57
-
58
- it("parses multiple markdown tasks", () => {
59
- const output = `### TASK: A
60
- - description: Do A
61
- - files: a.ts
62
- - caste: worker
63
- - priority: 1
64
-
65
- ### TASK: B
66
- - description: Do B
67
- - files: b.ts
68
- - caste: soldier
69
- - priority: 2`;
70
- const tasks = parseSubTasks(output);
71
- expect(tasks).toHaveLength(2);
72
- });
73
-
74
- it("parses context field", () => {
75
- const output = `### TASK: Fix it
76
- - description: Fix bug
77
- - files: x.ts
78
- - caste: worker
79
- - priority: 3
80
- - context: some relevant code`;
81
- const tasks = parseSubTasks(output);
82
- expect(tasks[0].context).toBeTruthy();
83
- });
84
-
85
- it("parses chinese task format with full-width colon", () => {
86
- const output = `### 任务:生成重启检查报告
87
- - 描述:创建重启检查文档
88
- - 文件:docs/ant-colony-restart-check.md
89
- - 角色:worker
90
- - 优先级:1`;
91
- const tasks = parseSubTasks(output);
92
- expect(tasks).toHaveLength(1);
93
- expect(tasks[0].title).toContain("重启检查报告");
94
- expect(tasks[0].files).toEqual(["docs/ant-colony-restart-check.md"]);
95
- expect(tasks[0].caste).toBe("worker");
96
- expect(tasks[0].priority).toBe(1);
97
- });
98
-
99
- it("does not infer tasks from plain next-step narrative", () => {
100
- const output = `目前发现如下\n\n下一步我会继续定位:
101
- - 写入 docs/ant-colony-restart-check.md
102
- - 校验 src/index.ts 的入口流程`;
103
- const tasks = parseSubTasks(output);
104
- expect(tasks).toEqual([]);
105
- });
106
-
107
- it("parses bold markdown field keys", () => {
108
- const output = `### TASK: Harden parser
109
- - **description**: support bold fields
110
- - **files**: pi-package/extensions/ant-colony/parser.ts
111
- - **caste**: worker
112
- - **priority**: 2`;
113
- const tasks = parseSubTasks(output);
114
- expect(tasks).toHaveLength(1);
115
- expect(tasks[0].files).toEqual(["pi-package/extensions/ant-colony/parser.ts"]);
116
- });
117
- });
118
-
119
- describe("extractPheromones", () => {
120
- it("extracts discovery section", () => {
121
- const p = extractPheromones("ant-1", "scout", "t-1", "## Discoveries\n- Found auth\n\n## Other\nstuff", ["a.ts"]);
122
- expect(p.some(x => x.type === "discovery")).toBe(true);
123
- });
124
-
125
- it("extracts warning section", () => {
126
- const p = extractPheromones("ant-1", "scout", "t-1", "## Warnings\n- Conflict\n", []);
127
- expect(p.some(x => x.type === "warning")).toBe(true);
128
- });
129
-
130
- it("adds repellent on failure", () => {
131
- const p = extractPheromones("ant-1", "worker", "t-1", "output", ["a.ts"], true);
132
- expect(p.some(x => x.type === "repellent")).toBe(true);
133
- });
134
-
135
- it("returns empty for no matching sections", () => {
136
- expect(extractPheromones("ant-1", "worker", "t-1", "nothing", [])).toEqual([]);
137
- });
138
-
139
- it("extracts Files Changed as completion", () => {
140
- const p = extractPheromones("ant-1", "worker", "t-1", "## Files Changed\n- src/foo.ts\n", ["src/foo.ts"]);
141
- expect(p.some(x => x.type === "completion")).toBe(true);
142
- });
143
- });
@@ -1,99 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import { CASTE_PROMPTS, buildPrompt } from "./prompts.js";
3
- import type { Task } from "./types.js";
4
-
5
- const mkTask = (overrides: Partial<Task> = {}): Task => ({
6
- id: "t-1", parentId: null, title: "Test task", description: "Do something",
7
- caste: "worker", status: "pending", priority: 3, files: [], claimedBy: null,
8
- result: null, error: null, spawnedTasks: [], createdAt: 0, startedAt: null, finishedAt: null,
9
- ...overrides,
10
- });
11
-
12
- describe("CASTE_PROMPTS", () => {
13
- it("has all castes", () => {
14
- for (const c of ["scout", "worker", "soldier"] as const) {
15
- expect(typeof CASTE_PROMPTS[c]).toBe("string");
16
- expect(CASTE_PROMPTS[c].length).toBeGreaterThan(0);
17
- }
18
- });
19
- });
20
-
21
- describe("buildPrompt", () => {
22
- it("includes task title and description", () => {
23
- const r = buildPrompt(mkTask(), "", "System");
24
- expect(r).toContain("Test task");
25
- expect(r).toContain("Do something");
26
- });
27
-
28
- it("includes pheromone context", () => {
29
- const r = buildPrompt(mkTask(), "Found auth at src/auth.ts", "System");
30
- expect(r).toContain("Found auth at src/auth.ts");
31
- });
32
-
33
- it("includes files scope", () => {
34
- const r = buildPrompt(mkTask({ files: ["a.ts", "b.ts"] }), "", "System");
35
- expect(r).toContain("a.ts, b.ts");
36
- });
37
-
38
- it("includes turn limit when provided", () => {
39
- const r = buildPrompt(mkTask(), "", "System", 10);
40
- expect(r).toContain("10");
41
- expect(r).toContain("Turn Limit");
42
- });
43
-
44
- it("omits turn limit when not provided", () => {
45
- expect(buildPrompt(mkTask(), "", "System")).not.toContain("Turn Limit");
46
- });
47
-
48
- it("includes pre-loaded context", () => {
49
- const r = buildPrompt(mkTask({ context: "code snippet" }), "", "System");
50
- expect(r).toContain("code snippet");
51
- });
52
-
53
- it("adds Chinese hint for Chinese descriptions", () => {
54
- const r = buildPrompt(mkTask({ description: "修复登录问题" }), "", "System");
55
- expect(r).toContain("Chinese");
56
- });
57
-
58
- // Bio 3: 串联觅食 — tandem context
59
- it("includes tandem parent result when provided", () => {
60
- const r = buildPrompt(mkTask(), "", "System", 10, { parentResult: "Parent found auth module at src/auth.ts" });
61
- expect(r).toContain("Tandem Context");
62
- expect(r).toContain("Parent found auth module");
63
- });
64
-
65
- it("includes tandem prior error when provided", () => {
66
- const r = buildPrompt(mkTask(), "", "System", 10, { priorError: "TypeError: cannot read property" });
67
- expect(r).toContain("Prior Attempt Failed");
68
- expect(r).toContain("TypeError: cannot read property");
69
- });
70
-
71
- it("includes both tandem fields when provided", () => {
72
- const r = buildPrompt(mkTask(), "", "System", 10, {
73
- parentResult: "Scout found files",
74
- priorError: "Build failed",
75
- });
76
- expect(r).toContain("Tandem Context");
77
- expect(r).toContain("Prior Attempt Failed");
78
- });
79
-
80
- it("omits tandem sections when not provided", () => {
81
- const r = buildPrompt(mkTask(), "", "System", 10);
82
- expect(r).not.toContain("Tandem Context");
83
- expect(r).not.toContain("Prior Attempt Failed");
84
- });
85
-
86
- it("omits tandem sections when empty object", () => {
87
- const r = buildPrompt(mkTask(), "", "System", 10, {});
88
- expect(r).not.toContain("Tandem Context");
89
- expect(r).not.toContain("Prior Attempt Failed");
90
- });
91
-
92
- it("truncates long parent result", () => {
93
- const longResult = "x".repeat(5000);
94
- const r = buildPrompt(mkTask(), "", "System", 10, { parentResult: longResult });
95
- expect(r).toContain("Tandem Context");
96
- // Should be truncated to 3000 chars
97
- expect(r.length).toBeLessThan(longResult.length);
98
- });
99
- });
@@ -1,227 +0,0 @@
1
- import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
2
- import * as fs from "node:fs";
3
- import * as path from "node:path";
4
- import * as os from "node:os";
5
-
6
- vi.mock("@mariozechner/pi-coding-agent", () => ({
7
- AuthStorage: class {},
8
- createAgentSession: vi.fn(),
9
- createReadTool: vi.fn(), createBashTool: vi.fn(), createEditTool: vi.fn(),
10
- createWriteTool: vi.fn(), createGrepTool: vi.fn(), createFindTool: vi.fn(),
11
- createLsTool: vi.fn(), ModelRegistry: class {}, SessionManager: { inMemory: vi.fn() },
12
- SettingsManager: { inMemory: vi.fn() }, createExtensionRuntime: vi.fn(),
13
- }));
14
- vi.mock("@mariozechner/pi-ai", () => ({ getModel: vi.fn() }));
15
-
16
- import { classifyError, quorumMergeTasks, shouldUseScoutQuorum, validateExecutionPlan } from "./queen.js";
17
- import { Nest } from "./nest.js";
18
- import type { ColonyState, Task } from "./types.js";
19
-
20
- // ═══ classifyError ═══
21
-
22
- describe("classifyError", () => {
23
- it("classifies TypeError", () => {
24
- expect(classifyError("TypeError: cannot read property 'x'")).toBe("type_error");
25
- });
26
-
27
- it("classifies TS errors", () => {
28
- expect(classifyError("TS2345: Argument of type")).toBe("type_error");
29
- });
30
-
31
- it("classifies permission errors", () => {
32
- expect(classifyError("EACCES: permission denied")).toBe("permission");
33
- });
34
-
35
- it("classifies 401", () => {
36
- expect(classifyError("Error: 401 Unauthorized")).toBe("permission");
37
- });
38
-
39
- it("classifies timeout", () => {
40
- expect(classifyError("Error: Timeout after 5000ms")).toBe("timeout");
41
- });
42
-
43
- it("classifies ETIMEDOUT", () => {
44
- expect(classifyError("connect ETIMEDOUT 1.2.3.4")).toBe("timeout");
45
- });
46
-
47
- it("classifies ENOENT", () => {
48
- expect(classifyError("ENOENT: no such file or directory")).toBe("not_found");
49
- });
50
-
51
- it("classifies Cannot find module", () => {
52
- expect(classifyError("Cannot find module './foo'")).toBe("not_found");
53
- });
54
-
55
- it("classifies syntax errors", () => {
56
- expect(classifyError("SyntaxError: Unexpected token")).toBe("syntax");
57
- });
58
-
59
- it("classifies rate limit", () => {
60
- expect(classifyError("Error: 429 Too Many Requests")).toBe("rate_limit");
61
- });
62
-
63
- it("returns unknown for unrecognized errors", () => {
64
- expect(classifyError("Something completely different")).toBe("unknown");
65
- });
66
-
67
- it("handles empty string", () => {
68
- expect(classifyError("")).toBe("unknown");
69
- });
70
- });
71
-
72
- describe("shouldUseScoutQuorum", () => {
73
- it("returns true for multi-step goals", () => {
74
- expect(shouldUseScoutQuorum("1) scan repo; 2) write report; 3) review output")).toBe(true);
75
- });
76
-
77
- it("returns false for simple single-step goals", () => {
78
- expect(shouldUseScoutQuorum("List top-level files")).toBe(false);
79
- });
80
- });
81
-
82
- describe("validateExecutionPlan", () => {
83
- it("accepts well-formed worker tasks", () => {
84
- const plan = validateExecutionPlan([
85
- mkTask({ id: "t-plan-1", caste: "worker", title: "Do x", description: "desc", priority: 1, files: ["a.ts"] }),
86
- ]);
87
- expect(plan.ok).toBe(true);
88
- expect(plan.issues).toEqual([]);
89
- });
90
-
91
- it("rejects empty plans", () => {
92
- const plan = validateExecutionPlan([]);
93
- expect(plan.ok).toBe(false);
94
- expect(plan.issues).toContain("no_pending_worker_tasks");
95
- });
96
-
97
- it("flags non-worker cates as invalid for execution phase", () => {
98
- const plan = validateExecutionPlan([
99
- mkTask({ id: "t-plan-2", caste: "scout" as any }),
100
- ]);
101
- expect(plan.ok).toBe(false);
102
- expect(plan.issues.some(i => i.includes("invalid_caste"))).toBe(true);
103
- });
104
- });
105
-
106
- // ═══ quorumMergeTasks ═══
107
-
108
- const mkState = (overrides: Partial<ColonyState> = {}): ColonyState => ({
109
- id: "test-colony", goal: "test", status: "working",
110
- tasks: [], ants: [], pheromones: [],
111
- concurrency: { current: 2, min: 1, max: 4, optimal: 3, history: [] },
112
- metrics: { tasksTotal: 0, tasksDone: 0, tasksFailed: 0, antsSpawned: 0, totalCost: 0, totalTokens: 0, startTime: Date.now(), throughputHistory: [] },
113
- maxCost: null, modelOverrides: {}, createdAt: Date.now(), finishedAt: null,
114
- ...overrides,
115
- });
116
-
117
- const mkTask = (overrides: Partial<Task> = {}): Task => ({
118
- id: `t-${Math.random().toString(36).slice(2)}`, parentId: null,
119
- title: "Test task", description: "Do something",
120
- caste: "worker", status: "pending", priority: 3, files: [],
121
- claimedBy: null, result: null, error: null, spawnedTasks: [],
122
- createdAt: Date.now(), startedAt: null, finishedAt: null,
123
- ...overrides,
124
- });
125
-
126
- let tmpDir: string;
127
- let nest: Nest;
128
-
129
- beforeEach(() => {
130
- tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "queen-test-"));
131
- nest = new Nest(tmpDir, "test-colony");
132
- nest.init(mkState());
133
- });
134
-
135
- afterEach(() => {
136
- try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch { /* ignore */ }
137
- });
138
-
139
- describe("quorumMergeTasks", () => {
140
- it("does nothing with 0-1 tasks", () => {
141
- const t1 = mkTask({ files: ["a.ts"], priority: 3 });
142
- nest.writeTask(t1);
143
- quorumMergeTasks(nest);
144
- const tasks = nest.getAllTasks().filter(t => t.status === "pending");
145
- expect(tasks).toHaveLength(1);
146
- });
147
-
148
- it("merges duplicate tasks with same files", () => {
149
- const t1 = mkTask({ id: "t-1", title: "Fix auth", files: ["a.ts", "b.ts"], priority: 3 });
150
- const t2 = mkTask({ id: "t-2", title: "Fix auth v2", files: ["a.ts", "b.ts"], priority: 3 });
151
- nest.writeTask(t1);
152
- nest.writeTask(t2);
153
- quorumMergeTasks(nest);
154
- const pending = nest.getAllTasks().filter(t => t.status === "pending");
155
- const done = nest.getAllTasks().filter(t => t.status === "done");
156
- expect(pending).toHaveLength(1);
157
- expect(done).toHaveLength(1);
158
- expect(done[0].result).toContain("quorum");
159
- });
160
-
161
- it("boosts priority of merged task", () => {
162
- const t1 = mkTask({ id: "t-1", files: ["x.ts"], priority: 3 });
163
- const t2 = mkTask({ id: "t-2", files: ["x.ts"], priority: 3 });
164
- nest.writeTask(t1);
165
- nest.writeTask(t2);
166
- quorumMergeTasks(nest);
167
- const pending = nest.getAllTasks().filter(t => t.status === "pending");
168
- expect(pending[0].priority).toBe(2); // boosted from 3 to 2
169
- });
170
-
171
- it("does not merge tasks with different files", () => {
172
- const t1 = mkTask({ id: "t-1", files: ["a.ts"] });
173
- const t2 = mkTask({ id: "t-2", files: ["b.ts"] });
174
- nest.writeTask(t1);
175
- nest.writeTask(t2);
176
- quorumMergeTasks(nest);
177
- const pending = nest.getAllTasks().filter(t => t.status === "pending");
178
- expect(pending).toHaveLength(2);
179
- });
180
-
181
- it("merges context from duplicate tasks", () => {
182
- const t1 = mkTask({ id: "t-1", files: ["a.ts"], context: "context A" });
183
- const t2 = mkTask({ id: "t-2", files: ["a.ts"], context: "context B" });
184
- nest.writeTask(t1);
185
- nest.writeTask(t2);
186
- quorumMergeTasks(nest);
187
- const pending = nest.getAllTasks().filter(t => t.status === "pending");
188
- expect(pending[0].context).toContain("context A");
189
- expect(pending[0].context).toContain("context B");
190
- });
191
-
192
- it("handles three duplicates", () => {
193
- const t1 = mkTask({ id: "t-1", files: ["a.ts"], priority: 4 });
194
- const t2 = mkTask({ id: "t-2", files: ["a.ts"], priority: 4 });
195
- const t3 = mkTask({ id: "t-3", files: ["a.ts"], priority: 4 });
196
- nest.writeTask(t1);
197
- nest.writeTask(t2);
198
- nest.writeTask(t3);
199
- quorumMergeTasks(nest);
200
- const pending = nest.getAllTasks().filter(t => t.status === "pending");
201
- const done = nest.getAllTasks().filter(t => t.status === "done");
202
- expect(pending).toHaveLength(1);
203
- expect(done).toHaveLength(2);
204
- expect(pending[0].priority).toBe(3); // boosted from 4 to 3
205
- });
206
-
207
- it("skips non-pending tasks", () => {
208
- const t1 = mkTask({ id: "t-1", files: ["a.ts"], status: "done" });
209
- const t2 = mkTask({ id: "t-2", files: ["a.ts"], status: "pending" });
210
- nest.writeTask(t1);
211
- nest.writeTask(t2);
212
- quorumMergeTasks(nest);
213
- // Only 1 pending, so no merge
214
- const pending = nest.getAllTasks().filter(t => t.status === "pending");
215
- expect(pending).toHaveLength(1);
216
- });
217
-
218
- it("does not boost priority below 1", () => {
219
- const t1 = mkTask({ id: "t-1", files: ["a.ts"], priority: 1 });
220
- const t2 = mkTask({ id: "t-2", files: ["a.ts"], priority: 1 });
221
- nest.writeTask(t1);
222
- nest.writeTask(t2);
223
- quorumMergeTasks(nest);
224
- const pending = nest.getAllTasks().filter(t => t.status === "pending");
225
- expect(pending[0].priority).toBe(1); // stays at 1
226
- });
227
- });
@@ -1,44 +0,0 @@
1
- import { describe, it, expect, vi } from "vitest";
2
-
3
- vi.mock("@mariozechner/pi-coding-agent", () => ({
4
- AuthStorage: class {},
5
- createAgentSession: vi.fn(),
6
- createReadTool: vi.fn(), createBashTool: vi.fn(), createEditTool: vi.fn(),
7
- createWriteTool: vi.fn(), createGrepTool: vi.fn(), createFindTool: vi.fn(),
8
- createLsTool: vi.fn(), ModelRegistry: class {}, SessionManager: { inMemory: vi.fn() },
9
- SettingsManager: { inMemory: vi.fn() }, createExtensionRuntime: vi.fn(),
10
- }));
11
- vi.mock("@mariozechner/pi-ai", () => ({ getModel: vi.fn() }));
12
-
13
- import { makeAntId, makePheromoneId, makeTaskId } from "./spawner.js";
14
-
15
- describe("makeAntId", () => {
16
- it("includes caste name", () => {
17
- expect(makeAntId("scout")).toContain("scout");
18
- expect(makeAntId("worker")).toContain("worker");
19
- });
20
-
21
- it("returns unique ids", () => {
22
- expect(makeAntId("worker")).not.toBe(makeAntId("worker"));
23
- });
24
- });
25
-
26
- describe("makePheromoneId", () => {
27
- it("starts with p-", () => {
28
- expect(makePheromoneId()).toMatch(/^p-/);
29
- });
30
-
31
- it("returns unique ids", () => {
32
- expect(makePheromoneId()).not.toBe(makePheromoneId());
33
- });
34
- });
35
-
36
- describe("makeTaskId", () => {
37
- it("starts with t-", () => {
38
- expect(makeTaskId()).toMatch(/^t-/);
39
- });
40
-
41
- it("returns unique ids", () => {
42
- expect(makeTaskId()).not.toBe(makeTaskId());
43
- });
44
- });
@@ -1,36 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import { DEFAULT_ANT_CONFIGS } from "./types.js";
3
- import type { AntCaste } from "./types.js";
4
-
5
- describe("DEFAULT_ANT_CONFIGS", () => {
6
- const castes: AntCaste[] = ["scout", "worker", "soldier", "drone"];
7
-
8
- it("has all castes", () => {
9
- for (const c of castes) expect(DEFAULT_ANT_CONFIGS).toHaveProperty(c);
10
- });
11
-
12
- it("each config has caste/model/tools/maxTurns", () => {
13
- for (const c of castes) {
14
- const cfg = DEFAULT_ANT_CONFIGS[c];
15
- expect(cfg.caste).toBe(c);
16
- expect(typeof cfg.model).toBe("string");
17
- expect(Array.isArray(cfg.tools)).toBe(true);
18
- expect(cfg.maxTurns).toBeGreaterThan(0);
19
- }
20
- });
21
-
22
- it("scout has no write tools", () => {
23
- expect(DEFAULT_ANT_CONFIGS.scout.tools).not.toContain("edit");
24
- expect(DEFAULT_ANT_CONFIGS.scout.tools).not.toContain("write");
25
- });
26
-
27
- it("worker has edit and write", () => {
28
- expect(DEFAULT_ANT_CONFIGS.worker.tools).toContain("edit");
29
- expect(DEFAULT_ANT_CONFIGS.worker.tools).toContain("write");
30
- });
31
-
32
- it("drone only has bash with 1 turn", () => {
33
- expect(DEFAULT_ANT_CONFIGS.drone.tools).toEqual(["bash"]);
34
- expect(DEFAULT_ANT_CONFIGS.drone.maxTurns).toBe(1);
35
- });
36
- });
@@ -1,84 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import { formatDuration, formatCost, formatTokens, statusIcon, statusLabel, progressBar, casteIcon, buildReport } from "./ui.js";
3
- import type { ColonyState } from "./types.js";
4
-
5
- describe("formatDuration", () => {
6
- it("0ms", () => expect(formatDuration(0)).toBe("0s"));
7
- it("5000ms", () => expect(formatDuration(5000)).toBe("5s"));
8
- it("59000ms", () => expect(formatDuration(59000)).toBe("59s"));
9
- it("60000ms", () => expect(formatDuration(60000)).toBe("1m0s"));
10
- it("90000ms", () => expect(formatDuration(90000)).toBe("1m30s"));
11
- });
12
-
13
- describe("formatCost", () => {
14
- it("0.001", () => expect(formatCost(0.001)).toBe("$0.0010"));
15
- it("0.009", () => expect(formatCost(0.009)).toBe("$0.0090"));
16
- it("0.01", () => expect(formatCost(0.01)).toBe("$0.01"));
17
- it("1.5", () => expect(formatCost(1.5)).toBe("$1.50"));
18
- });
19
-
20
- describe("formatTokens", () => {
21
- it("500", () => expect(formatTokens(500)).toBe("500"));
22
- it("999", () => expect(formatTokens(999)).toBe("999"));
23
- it("1500", () => expect(formatTokens(1500)).toBe("1.5k"));
24
- it("1500000", () => expect(formatTokens(1500000)).toBe("1.5M"));
25
- });
26
-
27
- describe("statusIcon", () => {
28
- it("launched", () => expect(statusIcon("launched")).toBe("🚀"));
29
- it("scouting", () => expect(statusIcon("scouting")).toBe("🔍"));
30
- it("working", () => expect(statusIcon("working")).toBe("⚒️"));
31
- it("planning_recovery", () => expect(statusIcon("planning_recovery")).toBe("♻️"));
32
- it("reviewing", () => expect(statusIcon("reviewing")).toBe("🛡️"));
33
- it("task_done", () => expect(statusIcon("task_done")).toBe("✅"));
34
- it("done", () => expect(statusIcon("done")).toBe("✅"));
35
- it("failed", () => expect(statusIcon("failed")).toBe("❌"));
36
- it("budget_exceeded", () => expect(statusIcon("budget_exceeded")).toBe("💰"));
37
- it("unknown", () => expect(statusIcon("xyz")).toBe("🐜"));
38
- });
39
-
40
- describe("statusLabel", () => {
41
- it("launched", () => expect(statusLabel("launched")).toBe("LAUNCHED"));
42
- it("scouting", () => expect(statusLabel("scouting")).toBe("SCOUTING"));
43
- it("planning_recovery", () => expect(statusLabel("planning_recovery")).toBe("PLANNING_RECOVERY"));
44
- it("task_done", () => expect(statusLabel("task_done")).toBe("TASK_DONE"));
45
- it("budget_exceeded", () => expect(statusLabel("budget_exceeded")).toBe("BUDGET_EXCEEDED"));
46
- it("unknown", () => expect(statusLabel("custom")).toBe("CUSTOM"));
47
- });
48
-
49
- describe("progressBar", () => {
50
- it("0%", () => expect(progressBar(0, 10)).toBe("[----------]"));
51
- it("50%", () => expect(progressBar(0.5, 10)).toBe("[#####-----]"));
52
- it("100%", () => expect(progressBar(1, 10)).toBe("[##########]"));
53
- });
54
-
55
- describe("casteIcon", () => {
56
- it("scout", () => expect(casteIcon("scout")).toBe("🔍"));
57
- it("soldier", () => expect(casteIcon("soldier")).toBe("🛡️"));
58
- it("drone", () => expect(casteIcon("drone")).toBe("⚙️"));
59
- it("worker", () => expect(casteIcon("worker")).toBe("⚒️"));
60
- it("unknown", () => expect(casteIcon("xyz")).toBe("⚒️"));
61
- });
62
-
63
- describe("buildReport", () => {
64
- it("builds report with goal, status, cost, tasks", () => {
65
- const state: ColonyState = {
66
- id: "c-1", goal: "Test goal", status: "done",
67
- tasks: [
68
- { id: "t1", parentId: null, title: "Task A", description: "", caste: "worker", status: "done", priority: 3, files: [], claimedBy: null, result: null, error: null, spawnedTasks: [], createdAt: 0, startedAt: 0, finishedAt: 1000 },
69
- { id: "t2", parentId: null, title: "Task B", description: "", caste: "worker", status: "failed", priority: 3, files: [], claimedBy: null, result: null, error: "some error", spawnedTasks: [], createdAt: 0, startedAt: 0, finishedAt: 1000 },
70
- ],
71
- ants: [], pheromones: [],
72
- concurrency: { current: 2, min: 1, max: 4, optimal: 3, history: [] },
73
- metrics: { tasksTotal: 2, tasksDone: 1, tasksFailed: 1, antsSpawned: 2, totalCost: 0.05, totalTokens: 1000, startTime: 0, throughputHistory: [] },
74
- maxCost: null, modelOverrides: {}, createdAt: 0, finishedAt: 5000,
75
- };
76
- const report = buildReport(state);
77
- expect(report).toContain("Test goal");
78
- expect(report).toContain("✅");
79
- expect(report).toContain("$0.05");
80
- expect(report).toContain("Task A");
81
- expect(report).toContain("Task B");
82
- expect(report).toContain("some error");
83
- });
84
- });
@@ -1,15 +0,0 @@
1
- import { describe, it, expect, vi } from "vitest";
2
-
3
- vi.mock("@mariozechner/pi-coding-agent", () => ({}));
4
-
5
- import { isNewer } from "./auto-update";
6
-
7
- describe("isNewer", () => {
8
- it("patch newer", () => expect(isNewer("1.0.1", "1.0.0")).toBe(true));
9
- it("minor newer", () => expect(isNewer("1.1.0", "1.0.0")).toBe(true));
10
- it("major newer", () => expect(isNewer("2.0.0", "1.9.9")).toBe(true));
11
- it("equal", () => expect(isNewer("1.0.0", "1.0.0")).toBe(false));
12
- it("older", () => expect(isNewer("1.0.0", "1.0.1")).toBe(false));
13
- it("0.1.70 > 0.1.69", () => expect(isNewer("0.1.70", "0.1.69")).toBe(true));
14
- it("0.1.69 < 0.1.70", () => expect(isNewer("0.1.69", "0.1.70")).toBe(false));
15
- });
@@ -1,26 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import { DANGEROUS_PATTERNS, PROTECTED_PATHS } from "./safe-guard";
3
-
4
- describe("DANGEROUS_PATTERNS", () => {
5
- const matches = (cmd: string) => DANGEROUS_PATTERNS.some((p) => p.test(cmd));
6
-
7
- it("matches rm -rf /", () => expect(matches("rm -rf /")).toBe(true));
8
- it("matches rm -f file.txt", () => expect(matches("rm -f file.txt")).toBe(true));
9
- it("matches rm --force file", () => expect(matches("rm --force file")).toBe(true));
10
- it("matches sudo rm file", () => expect(matches("sudo rm file")).toBe(true));
11
- it("matches DROP TABLE users", () => expect(matches("DROP TABLE users")).toBe(true));
12
- it("matches TRUNCATE TABLE foo", () => expect(matches("TRUNCATE TABLE foo")).toBe(true));
13
- it("matches DELETE FROM users", () => expect(matches("DELETE FROM users")).toBe(true));
14
- it("matches chmod 777 /tmp", () => expect(matches("chmod 777 /tmp")).toBe(true));
15
- it("matches mkfs /dev/sda", () => expect(matches("mkfs /dev/sda")).toBe(true));
16
- it("matches dd if=/dev/zero", () => expect(matches("dd if=/dev/zero")).toBe(true));
17
- it("does not match ls -la", () => expect(matches("ls -la")).toBe(false));
18
- it("does not match echo hello", () => expect(matches("echo hello")).toBe(false));
19
- });
20
-
21
- describe("PROTECTED_PATHS", () => {
22
- it("contains all expected paths", () => {
23
- expect(PROTECTED_PATHS).toEqual([".env", ".git/", "node_modules/", ".pi/", "id_rsa", ".ssh/"]);
24
- });
25
- it("has length 6", () => expect(PROTECTED_PATHS).toHaveLength(6));
26
- });