@pi-unipi/subagents 0.2.2 → 0.2.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.
Files changed (63) hide show
  1. package/package.json +1 -1
  2. package/src/__tests__/badge-generation.test.ts +244 -0
  3. package/src/index.ts +28 -7
  4. package/dist/__tests__/config.test.d.ts +0 -11
  5. package/dist/__tests__/config.test.d.ts.map +0 -1
  6. package/dist/__tests__/config.test.js +0 -196
  7. package/dist/__tests__/config.test.js.map +0 -1
  8. package/dist/__tests__/esc-propagation.test.d.ts +0 -10
  9. package/dist/__tests__/esc-propagation.test.d.ts.map +0 -1
  10. package/dist/__tests__/esc-propagation.test.js +0 -140
  11. package/dist/__tests__/esc-propagation.test.js.map +0 -1
  12. package/dist/__tests__/file-lock.test.d.ts +0 -12
  13. package/dist/__tests__/file-lock.test.d.ts.map +0 -1
  14. package/dist/__tests__/file-lock.test.js +0 -187
  15. package/dist/__tests__/file-lock.test.js.map +0 -1
  16. package/dist/__tests__/workflow-integration.test.d.ts +0 -12
  17. package/dist/__tests__/workflow-integration.test.d.ts.map +0 -1
  18. package/dist/__tests__/workflow-integration.test.js +0 -261
  19. package/dist/__tests__/workflow-integration.test.js.map +0 -1
  20. package/dist/agent-manager.d.ts +0 -75
  21. package/dist/agent-manager.d.ts.map +0 -1
  22. package/dist/agent-manager.js +0 -268
  23. package/dist/agent-manager.js.map +0 -1
  24. package/dist/agent-runner.d.ts +0 -51
  25. package/dist/agent-runner.d.ts.map +0 -1
  26. package/dist/agent-runner.js +0 -254
  27. package/dist/agent-runner.js.map +0 -1
  28. package/dist/config.d.ts +0 -24
  29. package/dist/config.d.ts.map +0 -1
  30. package/dist/config.js +0 -132
  31. package/dist/config.js.map +0 -1
  32. package/dist/conversation-viewer.d.ts +0 -40
  33. package/dist/conversation-viewer.d.ts.map +0 -1
  34. package/dist/conversation-viewer.js +0 -276
  35. package/dist/conversation-viewer.js.map +0 -1
  36. package/dist/custom-agents.d.ts +0 -14
  37. package/dist/custom-agents.d.ts.map +0 -1
  38. package/dist/custom-agents.js +0 -106
  39. package/dist/custom-agents.js.map +0 -1
  40. package/dist/file-lock.d.ts +0 -42
  41. package/dist/file-lock.d.ts.map +0 -1
  42. package/dist/file-lock.js +0 -91
  43. package/dist/file-lock.js.map +0 -1
  44. package/dist/index.d.ts +0 -10
  45. package/dist/index.d.ts.map +0 -1
  46. package/dist/index.js +0 -653
  47. package/dist/index.js.map +0 -1
  48. package/dist/model-resolver.d.ts +0 -19
  49. package/dist/model-resolver.d.ts.map +0 -1
  50. package/dist/model-resolver.js +0 -61
  51. package/dist/model-resolver.js.map +0 -1
  52. package/dist/prompts.d.ts +0 -13
  53. package/dist/prompts.d.ts.map +0 -1
  54. package/dist/prompts.js +0 -31
  55. package/dist/prompts.js.map +0 -1
  56. package/dist/types.d.ts +0 -96
  57. package/dist/types.d.ts.map +0 -1
  58. package/dist/types.js +0 -36
  59. package/dist/types.js.map +0 -1
  60. package/dist/widget.d.ts +0 -55
  61. package/dist/widget.d.ts.map +0 -1
  62. package/dist/widget.js +0 -404
  63. package/dist/widget.js.map +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/subagents",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "description": "Subagents for UniPi — parallel execution, file locking, workflow integration",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -0,0 +1,244 @@
1
+ /**
2
+ * Test: Badge Generation Flow
3
+ *
4
+ * Tests the full badge name generation flow to identify and verify
5
+ * fixes for "Generating session name..." getting stuck.
6
+ *
7
+ * BUG 1 — Tool mismatch:
8
+ * Background agent was told to "Call the set_session_name tool" but the tool
9
+ * doesn't exist in the agent's session (only builtin tools available).
10
+ * FIX: Changed prompt to output title directly, parse in onComplete callback.
11
+ *
12
+ * BUG 2 — Wrong event bus:
13
+ * Cross-module events emitted via pi.events.emit() but listeners used pi.on()
14
+ * (extension lifecycle events) — completely different event bus.
15
+ * FIX: Changed all cross-module listeners to pi.events.on().
16
+ */
17
+
18
+ import { describe, it } from "node:test";
19
+ import assert from "node:assert/strict";
20
+ import { readFileSync, existsSync } from "node:fs";
21
+ import { join } from "node:path";
22
+
23
+ const ROOT = join(import.meta.dirname, "../../../..");
24
+
25
+ // ─── Helpers ────────────────────────────────────────────────────────
26
+
27
+ function readSource(relativePath: string): string {
28
+ const fullPath = join(ROOT, relativePath);
29
+ if (!existsSync(fullPath)) throw new Error(`File not found: ${fullPath}`);
30
+ return readFileSync(fullPath, "utf-8");
31
+ }
32
+
33
+ // ─── Test: Tool availability in spawned agent ──────────────────────
34
+
35
+ describe("Badge generation — tool availability", () => {
36
+ it("agent-runner uses only builtin tools, NOT extension-registered tools", () => {
37
+ const src = readSource("packages/subagents/src/agent-runner.ts");
38
+
39
+ const builtinMatch = src.match(
40
+ /const BUILTIN_TOOL_NAMES\s*=\s*(\[.*?\])/s,
41
+ );
42
+ assert.ok(builtinMatch, "BUILTIN_TOOL_NAMES should be defined");
43
+
44
+ const builtinTools: string[] = eval(builtinMatch[1]);
45
+ assert.deepStrictEqual(builtinTools, [
46
+ "read", "bash", "edit", "write", "grep", "find", "ls",
47
+ ]);
48
+ });
49
+
50
+ it("set_session_name is NOT in the agent's tool list", () => {
51
+ const src = readSource("packages/subagents/src/agent-runner.ts");
52
+ const builtinMatch = src.match(
53
+ /const BUILTIN_TOOL_NAMES\s*=\s*(\[.*?\])/s,
54
+ );
55
+ const tools: string[] = eval(builtinMatch![1]);
56
+ assert.ok(!tools.includes("set_session_name"));
57
+ });
58
+ });
59
+
60
+ // ─── Test: Prompt no longer references non-existent tool ───────────
61
+
62
+ describe("Badge generation — prompt fix", () => {
63
+ it("prompt asks agent to OUTPUT the title directly (not call a tool)", () => {
64
+ const src = readSource("packages/subagents/src/index.ts");
65
+
66
+ assert.ok(
67
+ src.includes("Reply with ONLY the title"),
68
+ "Prompt should ask agent to reply with only the title",
69
+ );
70
+
71
+ assert.ok(
72
+ !src.includes("Call the set_session_name tool"),
73
+ "Prompt should NOT tell agent to call set_session_name",
74
+ );
75
+ });
76
+ });
77
+
78
+ // ─── Test: onComplete extracts name from result ────────────────────
79
+
80
+ describe("Badge generation — onComplete callback", () => {
81
+ it("onComplete extracts name from agent result and calls pi.setSessionName", () => {
82
+ const src = readSource("packages/subagents/src/index.ts");
83
+
84
+ assert.ok(
85
+ src.includes('record.description === "Generate session name"'),
86
+ "Should detect badge generation agents by description",
87
+ );
88
+
89
+ assert.ok(
90
+ src.includes("pi.setSessionName(name)"),
91
+ "Should call pi.setSessionName with extracted name",
92
+ );
93
+ });
94
+ });
95
+
96
+ // ─── Test: Cross-module event bus — the critical fix ───────────────
97
+
98
+ describe("Badge generation — event bus (CRITICAL FIX)", () => {
99
+ it("emitEvent uses pi.events.emit (not pi.on)", () => {
100
+ const src = readSource("packages/core/utils.ts");
101
+
102
+ assert.ok(
103
+ src.includes("pi.events.emit(eventName, payload)"),
104
+ "emitEvent should use pi.events.emit()",
105
+ );
106
+ });
107
+
108
+ it("subagents listens via pi.events.on (NOT pi.on)", () => {
109
+ const src = readSource("packages/subagents/src/index.ts");
110
+
111
+ // Must use pi.events.on for cross-module events
112
+ assert.ok(
113
+ src.includes("pi.events.on(UNIPI_EVENTS.BADGE_GENERATE_REQUEST"),
114
+ "Subagents should listen via pi.events.on",
115
+ );
116
+
117
+ // Should NOT use pi.on for custom events
118
+ const piOnMatch = src.match(/pi\.on\(UNIPI_EVENTS\.BADGE_GENERATE_REQUEST/g);
119
+ assert.ok(!piOnMatch, "Should NOT use pi.on() for cross-module events");
120
+ });
121
+
122
+ it("utility BADGE_GENERATE_REQUEST listener is removed (input handler already shows overlay)", () => {
123
+ const src = readSource("packages/utility/src/index.ts");
124
+
125
+ // Should NOT have a separate BADGE_GENERATE_REQUEST listener
126
+ // The input handler already shows the overlay and emits the event
127
+ assert.ok(
128
+ !src.includes("pi.events.on(UNIPI_EVENTS.BADGE_GENERATE_REQUEST"),
129
+ "Utility should NOT have a separate BADGE_GENERATE_REQUEST listener",
130
+ );
131
+ });
132
+
133
+ it("workflow listens for MODULE_READY via pi.events.on (NOT pi.on)", () => {
134
+ const src = readSource("packages/workflow/index.ts");
135
+
136
+ assert.ok(
137
+ src.includes("pi.events.on(UNIPI_EVENTS.MODULE_READY"),
138
+ "Workflow should listen via pi.events.on",
139
+ );
140
+
141
+ const piOnMatch = src.match(/pi\.on\(UNIPI_EVENTS\.MODULE_READY/g);
142
+ assert.ok(!piOnMatch, "Should NOT use pi.on() for cross-module events");
143
+ });
144
+
145
+ it("pi.on() is ONLY used for known lifecycle events", () => {
146
+ const subagentsSrc = readSource("packages/subagents/src/index.ts");
147
+ const utilitySrc = readSource("packages/utility/src/index.ts");
148
+
149
+ // These are valid lifecycle events that should use pi.on()
150
+ const validLifecycleEvents = [
151
+ "session_start", "session_shutdown", "input",
152
+ "tool_call", "tool_execution_start",
153
+ ];
154
+
155
+ // Check that pi.on() is only used with lifecycle events
156
+ const piOnPattern = /pi\.on\("([^"]+)"/g;
157
+ let match;
158
+ while ((match = piOnPattern.exec(subagentsSrc)) !== null) {
159
+ assert.ok(
160
+ validLifecycleEvents.includes(match[1]),
161
+ `subagents: pi.on("${match[1]}") should be a lifecycle event, use pi.events.on() for custom events`,
162
+ );
163
+ }
164
+ while ((match = piOnPattern.exec(utilitySrc)) !== null) {
165
+ assert.ok(
166
+ validLifecycleEvents.includes(match[1]),
167
+ `utility: pi.on("${match[1]}") should be a lifecycle event`,
168
+ );
169
+ }
170
+ });
171
+ });
172
+
173
+ // ─── Test: Event flow ──────────────────────────────────────────────
174
+
175
+ describe("Badge generation — event flow", () => {
176
+ it("utility emits BADGE_GENERATE_REQUEST on first input", () => {
177
+ const src = readSource("packages/utility/src/index.ts");
178
+
179
+ assert.ok(src.includes("BADGE_GENERATE_REQUEST"));
180
+ assert.ok(src.includes('source: "input-hook"'));
181
+ });
182
+
183
+ it("BADGE_GENERATE_REQUEST event is defined in core", () => {
184
+ const src = readSource("packages/core/events.ts");
185
+ assert.ok(src.includes("BADGE_GENERATE_REQUEST"));
186
+ });
187
+ });
188
+
189
+ // ─── Test: Model resolution ────────────────────────────────────────
190
+
191
+ describe("Badge generation — model resolution", () => {
192
+ it("reads generationModel from badge.json instead of hardcoding", () => {
193
+ const src = readSource("packages/subagents/src/index.ts");
194
+
195
+ assert.ok(!src.includes('"openai/gpt-oss-20b"'));
196
+ assert.ok(src.includes(".unipi/config/badge.json"));
197
+ assert.ok(src.includes("parsed.generationModel"));
198
+ });
199
+ });
200
+
201
+ // ─── Summary ───────────────────────────────────────────────────────
202
+
203
+ describe("Badge generation — ROOT CAUSE SUMMARY", () => {
204
+ it("BUG 1 FIXED: prompt no longer references non-existent tool", () => {
205
+ const src = readSource("packages/subagents/src/index.ts");
206
+
207
+ assert.ok(!src.includes("Call the set_session_name tool"),
208
+ "FIXED: prompt no longer tells agent to call set_session_name");
209
+ assert.ok(src.includes("Reply with ONLY the title"),
210
+ "FIXED: prompt asks agent to reply with only the title");
211
+ });
212
+
213
+ it("BUG 1 FIXED: onComplete extracts name and sets it directly", () => {
214
+ const src = readSource("packages/subagents/src/index.ts");
215
+
216
+ assert.ok(src.includes('record.description === "Generate session name"'),
217
+ "FIXED: onComplete detects badge generation agents");
218
+ assert.ok(src.includes("pi.setSessionName(name)"),
219
+ "FIXED: onComplete calls pi.setSessionName directly");
220
+ });
221
+
222
+ it("BUG 2 FIXED: cross-module events use pi.events.on, not pi.on", () => {
223
+ const subagentsSrc = readSource("packages/subagents/src/index.ts");
224
+ const utilitySrc = readSource("packages/utility/src/index.ts");
225
+ const workflowSrc = readSource("packages/workflow/index.ts");
226
+
227
+ // Subagents: correct event bus
228
+ assert.ok(
229
+ subagentsSrc.includes("pi.events.on(UNIPI_EVENTS.BADGE_GENERATE_REQUEST"),
230
+ "subagents: must use pi.events.on for BADGE_GENERATE_REQUEST",
231
+ );
232
+
233
+ // Utility: no duplicate listener (input handler already handles it)
234
+ assert.ok(
235
+ !utilitySrc.includes("pi.events.on(UNIPI_EVENTS.BADGE_GENERATE_REQUEST"),
236
+ "utility: no duplicate BADGE_GENERATE_REQUEST listener", );
237
+
238
+ // Workflow: correct event bus
239
+ assert.ok(
240
+ workflowSrc.includes("pi.events.on(UNIPI_EVENTS.MODULE_READY"),
241
+ "workflow: must use pi.events.on for MODULE_READY",
242
+ );
243
+ });
244
+ });
package/src/index.ts CHANGED
@@ -179,6 +179,16 @@ export default function (pi: ExtensionAPI) {
179
179
  );
180
180
  }
181
181
 
182
+ // Badge generation: extract name from agent result and set directly
183
+ if (record.description === "Generate session name" && record.result && record.status === "completed") {
184
+ const name = record.result.split("\n")[0]?.trim().slice(0, 50) ?? "";
185
+ if (name && !name.startsWith("Error") && !name.includes("error")) {
186
+ try {
187
+ pi.setSessionName(name);
188
+ } catch { /* best effort */ }
189
+ }
190
+ }
191
+
182
192
  pi.events.emit("subagents:completed", {
183
193
  id: record.id,
184
194
  type: record.type,
@@ -339,20 +349,31 @@ export default function (pi: ExtensionAPI) {
339
349
  });
340
350
 
341
351
  // Listen for badge generation requests — spawn background agent
342
- pi.on(UNIPI_EVENTS.BADGE_GENERATE_REQUEST as any, async (event: any) => {
352
+ pi.events.on(UNIPI_EVENTS.BADGE_GENERATE_REQUEST, async (event: any) => {
343
353
  if (!sessionCtx) return;
344
354
 
345
355
  const summary = event?.conversationSummary ?? "";
346
356
  const prompt = summary
347
- ? `Generate a concise session title (MAX 5 WORDS) for this conversation:\n\n"${summary}"\n\nCall the set_session_name tool with the name. Do not explain.`
348
- : `Generate a concise session title (MAX 5 WORDS) for the current session. Call the set_session_name tool. Do not explain.`;
349
-
350
- // Try with openai/gpt-oss-20b, fallback to inherit
351
- const modelInput = "openai/gpt-oss-20b";
357
+ ? `Generate a concise session title (MAX 5 WORDS) for this conversation:\n\n"${summary}"\n\nReply with ONLY the title. No quotes, no explanation, no punctuation.`
358
+ : `Generate a concise session title (MAX 5 WORDS) for the current session. Reply with ONLY the title. No quotes, no explanation, no punctuation.`;
359
+
360
+ // Try with configured model, fallback to inherit
361
+ let modelInput: string | undefined = undefined;
362
+ try {
363
+ const fs = await import("node:fs");
364
+ const path = await import("node:path");
365
+ const configPath = path.resolve(process.cwd(), ".unipi/config/badge.json");
366
+ if (fs.existsSync(configPath)) {
367
+ const parsed = JSON.parse(fs.readFileSync(configPath, "utf-8"));
368
+ if (typeof parsed.generationModel === "string" && parsed.generationModel !== "inherit") {
369
+ modelInput = parsed.generationModel;
370
+ }
371
+ }
372
+ } catch { /* ignore — inherit parent model */ }
352
373
  let resolvedModel: any = undefined;
353
374
 
354
375
  // Check if model is available
355
- if (sessionCtx.modelRegistry) {
376
+ if (modelInput && sessionCtx.modelRegistry) {
356
377
  const { resolveModel } = await import("./model-resolver.js");
357
378
  const result = resolveModel(modelInput, sessionCtx.modelRegistry);
358
379
  if (typeof result !== "string") {
@@ -1,11 +0,0 @@
1
- /**
2
- * Test: Config auto-generation and corruption recovery
3
- *
4
- * Verifies:
5
- * - Missing config → auto-generated with defaults
6
- * - Corrupted config → renamed to .json.bak, fresh generated
7
- * - Workspace config overrides global config
8
- * - Atomic writes prevent corruption
9
- */
10
- export {};
11
- //# sourceMappingURL=config.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"config.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/config.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG"}
@@ -1,196 +0,0 @@
1
- /**
2
- * Test: Config auto-generation and corruption recovery
3
- *
4
- * Verifies:
5
- * - Missing config → auto-generated with defaults
6
- * - Corrupted config → renamed to .json.bak, fresh generated
7
- * - Workspace config overrides global config
8
- * - Atomic writes prevent corruption
9
- */
10
- import { describe, it, beforeEach, afterEach } from "node:test";
11
- import assert from "node:assert/strict";
12
- import { existsSync, mkdirSync, readFileSync, writeFileSync, rmSync, renameSync } from "node:fs";
13
- import { join } from "node:path";
14
- import { tmpdir } from "node:os";
15
- // Inline config implementation for testing
16
- const DEFAULT_CONFIG = {
17
- maxConcurrent: 4,
18
- enabled: true,
19
- types: {
20
- explore: { enabled: true },
21
- work: { enabled: true },
22
- },
23
- };
24
- function loadConfigFromPath(filePath) {
25
- if (!existsSync(filePath))
26
- return null;
27
- try {
28
- const content = readFileSync(filePath, "utf-8");
29
- const parsed = JSON.parse(content);
30
- if (typeof parsed !== "object" || parsed === null)
31
- return null;
32
- return parsed;
33
- }
34
- catch {
35
- return null;
36
- }
37
- }
38
- function repairCorrupted(filePath) {
39
- const backupPath = filePath + ".bak";
40
- try {
41
- renameSync(filePath, backupPath);
42
- }
43
- catch {
44
- // If rename fails, just overwrite
45
- }
46
- writeConfigAtomic(filePath, DEFAULT_CONFIG);
47
- return DEFAULT_CONFIG;
48
- }
49
- function writeConfigAtomic(filePath, config) {
50
- const tmpPath = filePath + ".tmp";
51
- writeFileSync(tmpPath, JSON.stringify(config, null, 2), "utf-8");
52
- renameSync(tmpPath, filePath);
53
- }
54
- function initConfig(globalDir, workspaceDir) {
55
- const globalPath = join(globalDir, "subagents.json");
56
- const workspacePath = join(workspaceDir, "subagents.json");
57
- // Load or create global config
58
- let globalConfig = loadConfigFromPath(globalPath);
59
- if (globalConfig === null) {
60
- globalConfig = repairCorrupted(globalPath);
61
- }
62
- // Load workspace override if exists
63
- const workspaceConfig = loadConfigFromPath(workspacePath);
64
- if (workspaceConfig) {
65
- // Merge: workspace overrides global on any field present
66
- return {
67
- ...globalConfig,
68
- ...workspaceConfig,
69
- types: {
70
- ...globalConfig.types,
71
- ...workspaceConfig.types,
72
- },
73
- };
74
- }
75
- return globalConfig;
76
- }
77
- describe("Config Management", () => {
78
- let testDir;
79
- let globalDir;
80
- let workspaceDir;
81
- beforeEach(() => {
82
- // Create temp directories for testing
83
- testDir = join(tmpdir(), `subagents-test-${Date.now()}`);
84
- globalDir = join(testDir, "global");
85
- workspaceDir = join(testDir, "workspace");
86
- mkdirSync(globalDir, { recursive: true });
87
- mkdirSync(workspaceDir, { recursive: true });
88
- });
89
- afterEach(() => {
90
- // Cleanup
91
- rmSync(testDir, { recursive: true, force: true });
92
- });
93
- describe("Missing config", () => {
94
- it("should auto-generate with defaults when no config exists", () => {
95
- const config = initConfig(globalDir, workspaceDir);
96
- assert.deepEqual(config, DEFAULT_CONFIG);
97
- assert.equal(existsSync(join(globalDir, "subagents.json")), true);
98
- // Verify the generated file
99
- const content = readFileSync(join(globalDir, "subagents.json"), "utf-8");
100
- const parsed = JSON.parse(content);
101
- assert.deepEqual(parsed, DEFAULT_CONFIG);
102
- });
103
- it("should create global config even if workspace exists", () => {
104
- // Write workspace config only
105
- const workspaceConfig = { maxConcurrent: 8, enabled: true, types: {} };
106
- writeFileSync(join(workspaceDir, "subagents.json"), JSON.stringify(workspaceConfig));
107
- const config = initConfig(globalDir, workspaceDir);
108
- // Global should be created
109
- assert.equal(existsSync(join(globalDir, "subagents.json")), true);
110
- // Config should merge
111
- assert.equal(config.maxConcurrent, 8); // workspace overrides
112
- });
113
- });
114
- describe("Corrupted config", () => {
115
- it("should backup corrupted config and generate fresh", () => {
116
- const configPath = join(globalDir, "subagents.json");
117
- const backupPath = configPath + ".bak";
118
- // Write corrupted JSON
119
- writeFileSync(configPath, "{ invalid json !!!");
120
- const config = initConfig(globalDir, workspaceDir);
121
- // Should have created backup
122
- assert.equal(existsSync(backupPath), true, "Backup should exist");
123
- // Backup should contain corrupted content
124
- assert.equal(readFileSync(backupPath, "utf-8"), "{ invalid json !!!");
125
- // New config should be defaults
126
- assert.deepEqual(config, DEFAULT_CONFIG);
127
- // New file should be valid JSON
128
- const content = readFileSync(configPath, "utf-8");
129
- assert.deepEqual(JSON.parse(content), DEFAULT_CONFIG);
130
- });
131
- it("should handle completely empty file", () => {
132
- const configPath = join(globalDir, "subagents.json");
133
- writeFileSync(configPath, "");
134
- const config = initConfig(globalDir, workspaceDir);
135
- assert.deepEqual(config, DEFAULT_CONFIG);
136
- assert.equal(existsSync(configPath + ".bak"), true);
137
- });
138
- it("should handle non-object JSON", () => {
139
- const configPath = join(globalDir, "subagents.json");
140
- writeFileSync(configPath, '"just a string"');
141
- const config = initConfig(globalDir, workspaceDir);
142
- // JSON.parse succeeds but returns string, not object
143
- // loadConfigFromPath checks typeof === "object"
144
- assert.deepEqual(config, DEFAULT_CONFIG);
145
- });
146
- });
147
- describe("Workspace override", () => {
148
- it("should merge workspace config with global", () => {
149
- const globalConfig = { maxConcurrent: 4, enabled: true, types: { explore: { enabled: true } } };
150
- const workspaceConfig = { maxConcurrent: 8, types: { work: { enabled: false } } };
151
- writeFileSync(join(globalDir, "subagents.json"), JSON.stringify(globalConfig));
152
- writeFileSync(join(workspaceDir, "subagents.json"), JSON.stringify(workspaceConfig));
153
- const config = initConfig(globalDir, workspaceDir);
154
- assert.equal(config.maxConcurrent, 8); // workspace overrides
155
- assert.equal(config.enabled, true); // global preserved
156
- assert.deepEqual(config.types.explore, { enabled: true }); // global preserved
157
- assert.deepEqual(config.types.work, { enabled: false }); // workspace added
158
- });
159
- it("should override specific fields only", () => {
160
- const globalConfig = {
161
- maxConcurrent: 4,
162
- enabled: true,
163
- types: { explore: { enabled: true }, work: { enabled: true } },
164
- };
165
- const workspaceConfig = { enabled: false };
166
- writeFileSync(join(globalDir, "subagents.json"), JSON.stringify(globalConfig));
167
- writeFileSync(join(workspaceDir, "subagents.json"), JSON.stringify(workspaceConfig));
168
- const config = initConfig(globalDir, workspaceDir);
169
- assert.equal(config.maxConcurrent, 4); // global preserved
170
- assert.equal(config.enabled, false); // workspace overrides
171
- assert.deepEqual(config.types, { explore: { enabled: true }, work: { enabled: true } }); // global preserved
172
- });
173
- it("should handle empty workspace config", () => {
174
- const globalConfig = { maxConcurrent: 4, enabled: true, types: {} };
175
- writeFileSync(join(globalDir, "subagents.json"), JSON.stringify(globalConfig));
176
- writeFileSync(join(workspaceDir, "subagents.json"), "{}");
177
- const config = initConfig(globalDir, workspaceDir);
178
- assert.deepEqual(config, globalConfig);
179
- });
180
- });
181
- describe("Atomic writes", () => {
182
- it("should write config atomically", () => {
183
- const configPath = join(globalDir, "subagents.json");
184
- const config = { maxConcurrent: 8, enabled: false, types: {} };
185
- writeConfigAtomic(configPath, config);
186
- // Should have main file
187
- assert.equal(existsSync(configPath), true);
188
- // Should not have temp file
189
- assert.equal(existsSync(configPath + ".tmp"), false);
190
- // Content should be valid
191
- const content = readFileSync(configPath, "utf-8");
192
- assert.deepEqual(JSON.parse(content), config);
193
- });
194
- });
195
- });
196
- //# sourceMappingURL=config.test.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"config.test.js","sourceRoot":"","sources":["../../src/__tests__/config.test.ts"],"names":[],"mappings":"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"}
@@ -1,10 +0,0 @@
1
- /**
2
- * Test: ESC propagation — all children abort on parent ESC
3
- *
4
- * Verifies:
5
- * - forwardAbortSignal wires parent signal to child session
6
- * - abortAll stops all running agents
7
- * - All agents stop within reasonable time
8
- */
9
- export {};
10
- //# sourceMappingURL=esc-propagation.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"esc-propagation.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/esc-propagation.test.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG"}