indus-swarms 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/AGENTS.md +14 -0
- package/LICENSE +21 -0
- package/README.md +119 -0
- package/docs/claude-parity.md +151 -0
- package/docs/field-notes-teams-setup.md +107 -0
- package/docs/smoke-test-plan.md +146 -0
- package/eslint.config.js +74 -0
- package/extensions/teams/README.md +23 -0
- package/extensions/teams/activity-tracker.ts +234 -0
- package/extensions/teams/cleanup.ts +31 -0
- package/extensions/teams/fs-lock.ts +87 -0
- package/extensions/teams/hooks.ts +363 -0
- package/extensions/teams/index.ts +18 -0
- package/extensions/teams/leader-attach-commands.ts +221 -0
- package/extensions/teams/leader-inbox.ts +214 -0
- package/extensions/teams/leader-info-commands.ts +140 -0
- package/extensions/teams/leader-lifecycle-commands.ts +559 -0
- package/extensions/teams/leader-messaging-commands.ts +148 -0
- package/extensions/teams/leader-plan-commands.ts +95 -0
- package/extensions/teams/leader-spawn-command.ts +149 -0
- package/extensions/teams/leader-task-commands.ts +435 -0
- package/extensions/teams/leader-team-command.ts +382 -0
- package/extensions/teams/leader-teams-tool.ts +1075 -0
- package/extensions/teams/leader.ts +925 -0
- package/extensions/teams/mailbox.ts +131 -0
- package/extensions/teams/model-policy.ts +142 -0
- package/extensions/teams/names.ts +121 -0
- package/extensions/teams/paths.ts +37 -0
- package/extensions/teams/protocol.ts +241 -0
- package/extensions/teams/spawn-types.ts +36 -0
- package/extensions/teams/task-store.ts +544 -0
- package/extensions/teams/team-attach-claim.ts +205 -0
- package/extensions/teams/team-config.ts +335 -0
- package/extensions/teams/team-discovery.ts +59 -0
- package/extensions/teams/teammate-rpc.ts +261 -0
- package/extensions/teams/teams-panel.ts +1186 -0
- package/extensions/teams/teams-style.ts +322 -0
- package/extensions/teams/teams-ui-shared.ts +89 -0
- package/extensions/teams/teams-widget.ts +212 -0
- package/extensions/teams/worker.ts +605 -0
- package/extensions/teams/worktree.ts +103 -0
- package/package.json +53 -0
- package/scripts/e2e-rpc-test.mjs +277 -0
- package/scripts/integration-claim-test.mts +157 -0
- package/scripts/integration-hooks-remediation-test.mts +382 -0
- package/scripts/integration-spawn-overrides-test.mts +398 -0
- package/scripts/integration-todo-test.mts +533 -0
- package/scripts/lib/pi-workers.ts +105 -0
- package/scripts/smoke-test.mts +764 -0
- package/scripts/start-tmux-team.sh +91 -0
- package/skills/agent-teams/SKILL.md +180 -0
- package/tsconfig.strict.json +22 -0
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration test: validate /team spawn model + thinking overrides end-to-end.
|
|
3
|
+
*
|
|
4
|
+
* What this covers:
|
|
5
|
+
* - Spawn with explicit --model <provider>/<modelId> + --thinking <level>
|
|
6
|
+
* - Spawn with --model <modelId> only (provider inherited from leader)
|
|
7
|
+
* - Validation errors for invalid thinking and invalid model override formats
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* npx tsx scripts/integration-spawn-overrides-test.mts
|
|
11
|
+
* npx tsx scripts/integration-spawn-overrides-test.mts --timeoutSec 90
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import * as fs from "node:fs";
|
|
15
|
+
import * as os from "node:os";
|
|
16
|
+
import * as path from "node:path";
|
|
17
|
+
import * as readline from "node:readline";
|
|
18
|
+
import { spawn, type ChildProcess } from "node:child_process";
|
|
19
|
+
import { fileURLToPath } from "node:url";
|
|
20
|
+
|
|
21
|
+
import { sleep, terminateAll } from "./lib/pi-workers.js";
|
|
22
|
+
|
|
23
|
+
interface MemberSnapshot {
|
|
24
|
+
name: string;
|
|
25
|
+
status?: string;
|
|
26
|
+
meta?: Record<string, unknown>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface NotifyEvent {
|
|
30
|
+
notifyType: string;
|
|
31
|
+
message: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
type RpcCommand = { id?: string; type: "get_state" } | { id?: string; type: "prompt"; message: string };
|
|
35
|
+
|
|
36
|
+
type RpcResponse = {
|
|
37
|
+
id?: string;
|
|
38
|
+
type: "response";
|
|
39
|
+
command: string;
|
|
40
|
+
success: boolean;
|
|
41
|
+
data?: unknown;
|
|
42
|
+
error?: string;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
type PendingRequest = {
|
|
46
|
+
resolve: (v: RpcResponse) => void;
|
|
47
|
+
reject: (e: Error) => void;
|
|
48
|
+
timeout: NodeJS.Timeout;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
function parseArgs(argv: readonly string[]): { timeoutSec: number } {
|
|
52
|
+
let timeoutSec = 90;
|
|
53
|
+
|
|
54
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
55
|
+
const a = argv[i];
|
|
56
|
+
if (a === "--timeoutSec") {
|
|
57
|
+
const v = argv[i + 1];
|
|
58
|
+
if (v) timeoutSec = Number.parseInt(v, 10);
|
|
59
|
+
i += 1;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (!Number.isFinite(timeoutSec) || timeoutSec < 20) timeoutSec = 90;
|
|
64
|
+
return { timeoutSec };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function assert(condition: boolean, message: string): void {
|
|
68
|
+
if (!condition) throw new Error(message);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function isRecord(v: unknown): v is Record<string, unknown> {
|
|
72
|
+
return typeof v === "object" && v !== null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function safeJsonParse(line: string): unknown | null {
|
|
76
|
+
try {
|
|
77
|
+
return JSON.parse(line) as unknown;
|
|
78
|
+
} catch {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function isRpcResponse(v: unknown): v is RpcResponse {
|
|
84
|
+
if (!isRecord(v)) return false;
|
|
85
|
+
if (v.type !== "response") return false;
|
|
86
|
+
if (typeof v.command !== "string") return false;
|
|
87
|
+
if (typeof v.success !== "boolean") return false;
|
|
88
|
+
if (v.id !== undefined && typeof v.id !== "string") return false;
|
|
89
|
+
if (v.error !== undefined && typeof v.error !== "string") return false;
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function isNotifyExtensionUiRequest(v: unknown): v is { notifyType: string; message: string } {
|
|
94
|
+
if (!isRecord(v)) return false;
|
|
95
|
+
if (v.type !== "extension_ui_request") return false;
|
|
96
|
+
if (v.method !== "notify") return false;
|
|
97
|
+
if (typeof v.message !== "string") return false;
|
|
98
|
+
if (typeof v.notifyType !== "string") return false;
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function waitFor(
|
|
103
|
+
fn: () => boolean | Promise<boolean>,
|
|
104
|
+
opts: { timeoutMs: number; pollMs: number; label: string },
|
|
105
|
+
): Promise<void> {
|
|
106
|
+
const { timeoutMs, pollMs, label } = opts;
|
|
107
|
+
const deadline = Date.now() + timeoutMs;
|
|
108
|
+
while (Date.now() < deadline) {
|
|
109
|
+
if (await fn()) return;
|
|
110
|
+
await sleep(pollMs);
|
|
111
|
+
}
|
|
112
|
+
throw new Error(`Timeout waiting for ${label}`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function readConfig(teamDir: string): unknown | null {
|
|
116
|
+
const configPath = path.join(teamDir, "config.json");
|
|
117
|
+
try {
|
|
118
|
+
return JSON.parse(fs.readFileSync(configPath, "utf8")) as unknown;
|
|
119
|
+
} catch {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function findMember(teamDir: string, name: string): MemberSnapshot | null {
|
|
125
|
+
const cfg = readConfig(teamDir);
|
|
126
|
+
if (!isRecord(cfg)) return null;
|
|
127
|
+
|
|
128
|
+
const members = cfg.members;
|
|
129
|
+
if (!Array.isArray(members)) return null;
|
|
130
|
+
|
|
131
|
+
for (const m of members) {
|
|
132
|
+
if (!isRecord(m)) continue;
|
|
133
|
+
if (m.name !== name) continue;
|
|
134
|
+
|
|
135
|
+
const status = typeof m.status === "string" ? m.status : undefined;
|
|
136
|
+
const meta = isRecord(m.meta) ? m.meta : undefined;
|
|
137
|
+
return { name, status, meta };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function getMetaString(meta: Record<string, unknown> | undefined, key: string): string | undefined {
|
|
144
|
+
if (!meta) return undefined;
|
|
145
|
+
const v = meta[key];
|
|
146
|
+
return typeof v === "string" ? v : undefined;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function extractSessionId(resp: RpcResponse): string | null {
|
|
150
|
+
if (!isRecord(resp.data)) return null;
|
|
151
|
+
const sessionId = resp.data.sessionId;
|
|
152
|
+
return typeof sessionId === "string" ? sessionId : null;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function messagesContain(events: readonly NotifyEvent[], needle: string): boolean {
|
|
156
|
+
const n = needle.toLowerCase();
|
|
157
|
+
return events.some((e) => e.message.toLowerCase().includes(n));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const { timeoutSec } = parseArgs(process.argv.slice(2));
|
|
161
|
+
|
|
162
|
+
const teamsRootDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-teams-spawn-overrides-"));
|
|
163
|
+
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
|
164
|
+
const repoRoot = path.resolve(scriptDir, "..");
|
|
165
|
+
const entryPath = path.join(repoRoot, "extensions", "teams", "index.ts");
|
|
166
|
+
|
|
167
|
+
if (!fs.existsSync(entryPath)) {
|
|
168
|
+
throw new Error(`Teams extension entry not found: ${entryPath}`);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
console.log(`teamsRootDir: ${teamsRootDir}`);
|
|
172
|
+
console.log(`entryPath: ${entryPath}`);
|
|
173
|
+
|
|
174
|
+
const leaderEnv = {
|
|
175
|
+
...process.env,
|
|
176
|
+
PI_TEAMS_ROOT_DIR: teamsRootDir,
|
|
177
|
+
PI_TEAMS_WORKER: "0",
|
|
178
|
+
PI_TEAMS_TEAM_ID: "",
|
|
179
|
+
PI_TEAMS_AGENT_NAME: "",
|
|
180
|
+
PI_TEAMS_TASK_LIST_ID: "",
|
|
181
|
+
PI_TEAMS_LEAD_NAME: "",
|
|
182
|
+
PI_TEAMS_AUTO_CLAIM: "",
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const leaderArgs = [
|
|
186
|
+
"--mode",
|
|
187
|
+
"rpc",
|
|
188
|
+
"--no-session",
|
|
189
|
+
"--provider",
|
|
190
|
+
"openai-codex",
|
|
191
|
+
"--model",
|
|
192
|
+
"gpt-5.1-codex-mini",
|
|
193
|
+
"--thinking",
|
|
194
|
+
"minimal",
|
|
195
|
+
"--no-extensions",
|
|
196
|
+
"-e",
|
|
197
|
+
entryPath,
|
|
198
|
+
];
|
|
199
|
+
|
|
200
|
+
const leader = spawn("indusagi", leaderArgs, {
|
|
201
|
+
cwd: repoRoot,
|
|
202
|
+
env: leaderEnv,
|
|
203
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
const procs: ChildProcess[] = [leader];
|
|
207
|
+
const notifications: NotifyEvent[] = [];
|
|
208
|
+
const pending = new Map<string, PendingRequest>();
|
|
209
|
+
let nextId = 1;
|
|
210
|
+
let stderr = "";
|
|
211
|
+
|
|
212
|
+
leader.stderr.on("data", (d: Buffer | string) => {
|
|
213
|
+
stderr += d.toString();
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
leader.on("close", () => {
|
|
217
|
+
for (const [id, p] of pending.entries()) {
|
|
218
|
+
clearTimeout(p.timeout);
|
|
219
|
+
p.reject(new Error(`Leader closed before response (id=${id}). stderr=${stderr}`));
|
|
220
|
+
}
|
|
221
|
+
pending.clear();
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
const rl = readline.createInterface({ input: leader.stdout, crlfDelay: Infinity });
|
|
225
|
+
rl.on("line", (line: string) => {
|
|
226
|
+
const obj = safeJsonParse(line);
|
|
227
|
+
if (obj === null) return;
|
|
228
|
+
|
|
229
|
+
if (isRpcResponse(obj)) {
|
|
230
|
+
if (!obj.id) return;
|
|
231
|
+
const req = pending.get(obj.id);
|
|
232
|
+
if (!req) return;
|
|
233
|
+
pending.delete(obj.id);
|
|
234
|
+
clearTimeout(req.timeout);
|
|
235
|
+
req.resolve(obj);
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (isNotifyExtensionUiRequest(obj)) {
|
|
240
|
+
notifications.push({ notifyType: obj.notifyType, message: obj.message });
|
|
241
|
+
console.log(`[notify:${obj.notifyType}] ${obj.message}`);
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
const send = async (command: RpcCommand): Promise<RpcResponse> => {
|
|
246
|
+
const id = command.id ?? `req-${nextId++}`;
|
|
247
|
+
const payload = JSON.stringify({ ...command, id }) + "\n";
|
|
248
|
+
|
|
249
|
+
leader.stdin.write(payload);
|
|
250
|
+
|
|
251
|
+
return await new Promise<RpcResponse>((resolve, reject) => {
|
|
252
|
+
const timeout = setTimeout(() => {
|
|
253
|
+
if (!pending.has(id)) return;
|
|
254
|
+
pending.delete(id);
|
|
255
|
+
reject(new Error(`Timeout waiting for response to ${command.type}. stderr=${stderr}`));
|
|
256
|
+
}, timeoutSec * 1000);
|
|
257
|
+
|
|
258
|
+
pending.set(id, { resolve, reject, timeout });
|
|
259
|
+
});
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
const sendPrompt = async (message: string): Promise<void> => {
|
|
263
|
+
console.log(`prompt: ${message}`);
|
|
264
|
+
const resp = await send({ type: "prompt", message });
|
|
265
|
+
if (!resp.success) {
|
|
266
|
+
throw new Error(`Prompt failed: ${resp.error ?? "unknown error"}`);
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
let teamDir = "";
|
|
271
|
+
|
|
272
|
+
const waitForMemberStatus = async (name: string, status: "online" | "offline"): Promise<MemberSnapshot> => {
|
|
273
|
+
let snapshot: MemberSnapshot | null = null;
|
|
274
|
+
await waitFor(
|
|
275
|
+
() => {
|
|
276
|
+
snapshot = findMember(teamDir, name);
|
|
277
|
+
return snapshot !== null && snapshot.status === status;
|
|
278
|
+
},
|
|
279
|
+
{ timeoutMs: timeoutSec * 1000, pollMs: 250, label: `member ${name} ${status}` },
|
|
280
|
+
);
|
|
281
|
+
if (!snapshot) throw new Error(`Missing snapshot for ${name}`);
|
|
282
|
+
return snapshot;
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
const waitForMemberOverrides = async (
|
|
286
|
+
name: string,
|
|
287
|
+
expectedThinking: string,
|
|
288
|
+
expectedModel: string,
|
|
289
|
+
): Promise<MemberSnapshot> => {
|
|
290
|
+
let snapshot: MemberSnapshot | null = null;
|
|
291
|
+
await waitFor(
|
|
292
|
+
() => {
|
|
293
|
+
snapshot = findMember(teamDir, name);
|
|
294
|
+
if (!snapshot || snapshot.status !== "online") return false;
|
|
295
|
+
const thinking = getMetaString(snapshot.meta, "thinkingLevel");
|
|
296
|
+
const model = getMetaString(snapshot.meta, "model");
|
|
297
|
+
return thinking === expectedThinking && model === expectedModel;
|
|
298
|
+
},
|
|
299
|
+
{ timeoutMs: timeoutSec * 1000, pollMs: 250, label: `member ${name} override metadata` },
|
|
300
|
+
);
|
|
301
|
+
if (!snapshot) throw new Error(`Missing snapshot for ${name}`);
|
|
302
|
+
return snapshot;
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
try {
|
|
306
|
+
const state = await send({ type: "get_state" });
|
|
307
|
+
if (!state.success) throw new Error(`get_state failed: ${state.error ?? "unknown error"}`);
|
|
308
|
+
|
|
309
|
+
const leaderSessionId = extractSessionId(state);
|
|
310
|
+
if (!leaderSessionId) throw new Error(`No sessionId in get_state response: ${JSON.stringify(state)}`);
|
|
311
|
+
|
|
312
|
+
teamDir = path.join(teamsRootDir, leaderSessionId);
|
|
313
|
+
console.log(`leaderSessionId: ${leaderSessionId}`);
|
|
314
|
+
console.log(`teamDir: ${teamDir}`);
|
|
315
|
+
|
|
316
|
+
// 1) Explicit provider/model + thinking override.
|
|
317
|
+
await sendPrompt("/team spawn alice fresh --model openai-codex/gpt-5.1-codex-mini --thinking high");
|
|
318
|
+
const alice = await waitForMemberOverrides("alice", "high", "openai-codex/gpt-5.1-codex-mini");
|
|
319
|
+
|
|
320
|
+
assert(
|
|
321
|
+
getMetaString(alice.meta, "model") === "openai-codex/gpt-5.1-codex-mini",
|
|
322
|
+
`alice model mismatch: ${getMetaString(alice.meta, "model") ?? "<missing>"}`,
|
|
323
|
+
);
|
|
324
|
+
assert(
|
|
325
|
+
getMetaString(alice.meta, "thinkingLevel") === "high",
|
|
326
|
+
`alice thinking mismatch: ${getMetaString(alice.meta, "thinkingLevel") ?? "<missing>"}`,
|
|
327
|
+
);
|
|
328
|
+
console.log("OK: alice model/thinking overrides recorded");
|
|
329
|
+
|
|
330
|
+
// 2) Model id only -> inherit provider from leader.
|
|
331
|
+
await sendPrompt("/team spawn bob fresh --model gpt-5.1-codex-mini --thinking low");
|
|
332
|
+
const bob = await waitForMemberOverrides("bob", "low", "openai-codex/gpt-5.1-codex-mini");
|
|
333
|
+
|
|
334
|
+
assert(
|
|
335
|
+
getMetaString(bob.meta, "model") === "openai-codex/gpt-5.1-codex-mini",
|
|
336
|
+
`bob model mismatch: ${getMetaString(bob.meta, "model") ?? "<missing>"}`,
|
|
337
|
+
);
|
|
338
|
+
assert(
|
|
339
|
+
getMetaString(bob.meta, "thinkingLevel") === "low",
|
|
340
|
+
`bob thinking mismatch: ${getMetaString(bob.meta, "thinkingLevel") ?? "<missing>"}`,
|
|
341
|
+
);
|
|
342
|
+
console.log("OK: bob inherited provider for model-only override");
|
|
343
|
+
|
|
344
|
+
// 3) Invalid thinking value is rejected by command parser.
|
|
345
|
+
const beforeInvalidThinking = notifications.length;
|
|
346
|
+
await sendPrompt("/team spawn charlie fresh --thinking nope");
|
|
347
|
+
await sleep(150);
|
|
348
|
+
const invalidThinkingEvents = notifications.slice(beforeInvalidThinking);
|
|
349
|
+
|
|
350
|
+
assert(findMember(teamDir, "charlie") === null, "charlie should not be spawned for invalid thinking");
|
|
351
|
+
assert(
|
|
352
|
+
messagesContain(invalidThinkingEvents, "invalid thinking level"),
|
|
353
|
+
"expected invalid thinking level notification",
|
|
354
|
+
);
|
|
355
|
+
console.log("OK: invalid thinking level rejected");
|
|
356
|
+
|
|
357
|
+
// 4) Invalid model override shape is rejected by spawn layer.
|
|
358
|
+
const beforeInvalidModel = notifications.length;
|
|
359
|
+
await sendPrompt("/team spawn dave fresh --model openai-codex/ --thinking low");
|
|
360
|
+
await sleep(150);
|
|
361
|
+
const invalidModelEvents = notifications.slice(beforeInvalidModel);
|
|
362
|
+
|
|
363
|
+
assert(findMember(teamDir, "dave") === null, "dave should not be spawned for invalid model override");
|
|
364
|
+
assert(messagesContain(invalidModelEvents, "invalid model override"), "expected invalid model override notification");
|
|
365
|
+
console.log("OK: invalid model override rejected");
|
|
366
|
+
|
|
367
|
+
// 5) Deprecated model override is rejected by model policy.
|
|
368
|
+
const beforeDeprecatedModel = notifications.length;
|
|
369
|
+
await sendPrompt("/team spawn erin fresh --model anthropic/claude-sonnet-4 --thinking low");
|
|
370
|
+
await sleep(150);
|
|
371
|
+
const deprecatedModelEvents = notifications.slice(beforeDeprecatedModel);
|
|
372
|
+
|
|
373
|
+
assert(findMember(teamDir, "erin") === null, "erin should not be spawned for deprecated model override");
|
|
374
|
+
assert(messagesContain(deprecatedModelEvents, "deprecated"), "expected deprecated model override notification");
|
|
375
|
+
console.log("OK: deprecated model override rejected");
|
|
376
|
+
|
|
377
|
+
// Shutdown spawned teammates.
|
|
378
|
+
await sendPrompt("/team shutdown");
|
|
379
|
+
await waitForMemberStatus("alice", "offline");
|
|
380
|
+
await waitForMemberStatus("bob", "offline");
|
|
381
|
+
console.log("OK: teammates shutdown cleanly");
|
|
382
|
+
|
|
383
|
+
console.log("PASS: integration spawn override test passed");
|
|
384
|
+
} finally {
|
|
385
|
+
try {
|
|
386
|
+
rl.close();
|
|
387
|
+
} catch {
|
|
388
|
+
// ignore
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
await terminateAll(procs);
|
|
392
|
+
|
|
393
|
+
try {
|
|
394
|
+
fs.rmSync(teamsRootDir, { recursive: true, force: true });
|
|
395
|
+
} catch {
|
|
396
|
+
// ignore
|
|
397
|
+
}
|
|
398
|
+
}
|