惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
WordPress大学
WordPress大学
人人都是产品经理
人人都是产品经理
Engineering at Meta
Engineering at Meta
小众软件
小众软件
I
InfoQ
有赞技术团队
有赞技术团队
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
Martin Fowler
Martin Fowler
月光博客
月光博客
雷峰网
雷峰网
aimingoo的专栏
aimingoo的专栏
云风的 BLOG
云风的 BLOG
Last Week in AI
Last Week in AI
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
S
SegmentFault 最新的问题
The GitHub Blog
The GitHub Blog
Y
Y Combinator Blog
V
Visual Studio Blog
博客园 - 叶小钗
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
GbyAI
GbyAI
P
Proofpoint News Feed
Apple Machine Learning Research
Apple Machine Learning Research

Recent Commits to openclaw:main

test: merge chat side-result checks · openclaw/openclaw@ddd2c2a test: merge cron history checks · openclaw/openclaw@f7eb746 test: merge responsive navigation shell checks · openclaw/openclaw@c2e4b47 docs(changelog): add codex oauth fixes · openclaw/openclaw@628e6cd test: merge navigation routing cases · openclaw/openclaw@5d8cecb Tests: mock channel registry bundled fallback · openclaw/openclaw@2b08233 Secrets: avoid broad web search discovery for single plugin config · openclaw/openclaw@a464f59 test: merge config view browser checks · openclaw/openclaw@20cf511 fix(status): align oauth health with runtime · openclaw/openclaw@eed7116 feat: add macOS screen snapshots for monitor preview (#67954) thanks … · openclaw/openclaw@f377db1 fix: report shared auth scopes in hello-ok (#67810) thanks @BunsDev · openclaw/openclaw@0b6c39b Auto-reply: avoid eager bundled route fallback · openclaw/openclaw@3ea1bf4 Tests: narrow session binding contract setup · openclaw/openclaw@54e4e16 fix(macOS): enable undo/redo in webchat composer text input (#34962) · openclaw/openclaw@00951dc Tests: speed up channel setup promotion · openclaw/openclaw@82b529a Docs: refresh agent instructions · openclaw/openclaw@5775fe2 fix(auth): serialize OAuth refresh across agents to fix #26322 (#67876) · openclaw/openclaw@8e79080 test: allow ollama public surface boundary test · openclaw/openclaw@7d4f1a6 Docs: add test performance guardrails · openclaw/openclaw@89706d3 Tests: restore context-engine usage proof · openclaw/openclaw@e4c4f95 Tests: slim context engine runtime coverage · openclaw/openclaw@74c198f ci: retry failed custom checkouts · openclaw/openclaw@0ee5baf test: trim duplicate provider auth onboarding cases · openclaw/openclaw@1ffc02e matrix: fix sessions_spawn --thread subagent session spawning (#67643) · openclaw/openclaw@1ce2596 test: reduce auth choice fixture churn · openclaw/openclaw@857b9cd test: mock health status config boundaries · openclaw/openclaw@9d5ab4a test: mock onboard config io boundary · openclaw/openclaw@299694d test: mock legacy state plugin boundaries · openclaw/openclaw@2713089 test: mock channel install boundaries · openclaw/openclaw@b945248 test: mock doctor preview channel boundaries · openclaw/openclaw@b1a3ad4
test: tighten subagent spawn hook assertions · openclaw/o...
steipete · 2026-05-11 · via Recent Commits to openclaw:main

@@ -54,6 +54,19 @@ function findGatewayRequest(method: string): GatewayRequest | undefined {

5454

return getGatewayRequests().find((request) => request.method === method);

5555

}

565657+

function requireRecord(value: unknown, label: string): Record<string, unknown> {

58+

expect(value, label).toBeTypeOf("object");

59+

expect(value, label).not.toBeNull();

60+

return value as Record<string, unknown>;

61+

}

62+63+

function expectFields(value: unknown, expected: Record<string, unknown>, label = "object"): void {

64+

const record = requireRecord(value, label);

65+

for (const [key, expectedValue] of Object.entries(expected)) {

66+

expect(record[key], `${label}.${key}`).toEqual(expectedValue);

67+

}

68+

}

69+5770

function setConfig(next: Record<string, unknown>) {

5871

hoisted.configOverride = createSubagentSpawnTestConfig(undefined, next);

5972

}

@@ -132,10 +145,14 @@ function expectThreadBindFailureCleanup(

132145

expect(hookRunnerMocks.runSubagentSpawned).not.toHaveBeenCalled();

133146

expectSessionsDeleteWithoutAgentStart();

134147

const deleteCall = findGatewayRequest("sessions.delete");

135-

expect(deleteCall?.params).toMatchObject({

136-

key: result.childSessionKey,

137-

emitLifecycleHooks: false,

138-

});

148+

expectFields(

149+

deleteCall?.params,

150+

{

151+

key: result.childSessionKey,

152+

emitLifecycleHooks: false,

153+

},

154+

"delete params",

155+

);

139156

}

140157141158

beforeAll(async () => {

@@ -215,7 +232,7 @@ describe("sessions_spawn subagent lifecycle hooks", () => {

215232

context: "isolated",

216233

});

217234218-

expect(result).toMatchObject({ status: "accepted", runId: "run-1" });

235+

expectFields(result, { status: "accepted", runId: "run-1" }, "spawn result");

219236

expect(hookRunnerMocks.runSubagentSpawning).toHaveBeenCalledTimes(1);

220237

expect(hookRunnerMocks.runSubagentSpawning).toHaveBeenCalledWith(

221238

{

@@ -242,25 +259,37 @@ describe("sessions_spawn subagent lifecycle hooks", () => {

242259

Record<string, unknown>,

243260

Record<string, unknown>,

244261

];

245-

expect(event).toMatchObject({

246-

runId: "run-1",

247-

agentId: "main",

248-

label: "research",

249-

mode: "session",

250-

requester: {

262+

expectFields(

263+

event,

264+

{

265+

runId: "run-1",

266+

agentId: "main",

267+

label: "research",

268+

mode: "session",

269+

threadRequested: true,

270+

},

271+

"spawned event",

272+

);

273+

expectFields(

274+

event.requester,

275+

{

251276

channel: "discord",

252277

accountId: "work",

253278

to: "channel:123",

254279

threadId: 456,

255280

},

256-

threadRequested: true,

257-

});

281+

"spawned requester",

282+

);

258283

expect(event.childSessionKey).toEqual(expect.stringMatching(/^agent:main:subagent:/));

259-

expect(ctx).toMatchObject({

260-

runId: "run-1",

261-

requesterSessionKey: "main",

262-

childSessionKey: event.childSessionKey,

263-

});

284+

expectFields(

285+

ctx,

286+

{

287+

runId: "run-1",

288+

requesterSessionKey: "main",

289+

childSessionKey: event.childSessionKey,

290+

},

291+

"spawned context",

292+

);

264293

});

265294266295

it("emits subagent_spawned with threadRequested=false when not requested", async () => {

@@ -269,20 +298,28 @@ describe("sessions_spawn subagent lifecycle hooks", () => {

269298

agentTo: "channel:123",

270299

});

271300272-

expect(result).toMatchObject({ status: "accepted", runId: "run-1" });

301+

expectFields(result, { status: "accepted", runId: "run-1" }, "spawn result");

273302

expect(hookRunnerMocks.runSubagentSpawning).not.toHaveBeenCalled();

274303

expect(hookRunnerMocks.runSubagentSpawned).toHaveBeenCalledTimes(1);

275304

const [event] = (hookRunnerMocks.runSubagentSpawned.mock.calls[0] ?? []) as unknown as [

276305

Record<string, unknown>,

277306

];

278-

expect(event).toMatchObject({

279-

mode: "run",

280-

threadRequested: false,

281-

requester: {

307+

expectFields(

308+

event,

309+

{

310+

mode: "run",

311+

threadRequested: false,

312+

},

313+

"spawned event",

314+

);

315+

expectFields(

316+

event.requester,

317+

{

282318

channel: "discord",

283319

to: "channel:123",

284320

},

285-

});

321+

"spawned requester",

322+

);

286323

});

287324288325

it("respects explicit mode=run when thread binding is requested", async () => {

@@ -294,13 +331,17 @@ describe("sessions_spawn subagent lifecycle hooks", () => {

294331

context: "isolated",

295332

});

296333297-

expect(result).toMatchObject({ status: "accepted", runId: "run-1", mode: "run" });

334+

expectFields(result, { status: "accepted", runId: "run-1", mode: "run" }, "spawn result");

298335

expect(hookRunnerMocks.runSubagentSpawning).toHaveBeenCalledTimes(1);

299336

const event = getSpawnedEventCall();

300-

expect(event).toMatchObject({

301-

mode: "run",

302-

threadRequested: true,

303-

});

337+

expectFields(

338+

event,

339+

{

340+

mode: "run",

341+

threadRequested: true,

342+

},

343+

"spawned event",

344+

);

304345

});

305346306347

it("returns error when thread binding cannot be created", async () => {

@@ -377,26 +418,34 @@ describe("sessions_spawn subagent lifecycle hooks", () => {

377418

context: "isolated",

378419

});

379420380-

expect(result).toMatchObject({ status: "error" });

421+

expect(result.status).toBe("error");

381422

expect(hookRunnerMocks.runSubagentEnded).toHaveBeenCalledTimes(1);

382423

const [event] = (hookRunnerMocks.runSubagentEnded.mock.calls[0] ?? []) as unknown as [

383424

Record<string, unknown>,

384425

];

385-

expect(event).toMatchObject({

386-

targetSessionKey: expect.stringMatching(/^agent:main:subagent:/),

387-

accountId: "work",

388-

targetKind: "subagent",

389-

reason: "spawn-failed",

390-

sendFarewell: true,

391-

outcome: "error",

392-

error: "Session failed to start",

393-

});

426+

expect(event.targetSessionKey).toEqual(expect.stringMatching(/^agent:main:subagent:/));

427+

expectFields(

428+

event,

429+

{

430+

accountId: "work",

431+

targetKind: "subagent",

432+

reason: "spawn-failed",

433+

sendFarewell: true,

434+

outcome: "error",

435+

error: "Session failed to start",

436+

},

437+

"ended event",

438+

);

394439

const deleteCall = findGatewayRequest("sessions.delete");

395-

expect(deleteCall?.params).toMatchObject({

396-

key: event.targetSessionKey,

397-

deleteTranscript: true,

398-

emitLifecycleHooks: false,

399-

});

440+

expectFields(

441+

deleteCall?.params,

442+

{

443+

key: event.targetSessionKey,

444+

deleteTranscript: true,

445+

emitLifecycleHooks: false,

446+

},

447+

"delete params",

448+

);

400449

});

401450402451

it("falls back to sessions.delete cleanup when subagent_ended hook is unavailable", async () => {

@@ -411,15 +460,19 @@ describe("sessions_spawn subagent lifecycle hooks", () => {

411460

context: "isolated",

412461

});

413462414-

expect(result).toMatchObject({ status: "error" });

463+

expect(result.status).toBe("error");

415464

expect(hookRunnerMocks.runSubagentEnded).not.toHaveBeenCalled();

416465

const methods = getGatewayMethods();

417466

expect(methods).toContain("sessions.delete");

418467

const deleteCall = findGatewayRequest("sessions.delete");

419-

expect(deleteCall?.params).toMatchObject({

420-

deleteTranscript: true,

421-

emitLifecycleHooks: true,

422-

});

468+

expectFields(

469+

deleteCall?.params,

470+

{

471+

deleteTranscript: true,

472+

emitLifecycleHooks: true,

473+

},

474+

"delete params",

475+

);

423476

});

424477425478

it("cleans up the provisional session when lineage patching fails after thread binding", async () => {

@@ -464,10 +517,14 @@ describe("sessions_spawn subagent lifecycle hooks", () => {

464517

expect(methods).toContain("sessions.delete");

465518

expect(methods).not.toContain("agent");

466519

const deleteCall = findGatewayRequest("sessions.delete");

467-

expect(deleteCall?.params).toMatchObject({

468-

key: result.childSessionKey,

469-

deleteTranscript: true,

470-

emitLifecycleHooks: true,

471-

});

520+

expectFields(

521+

deleteCall?.params,

522+

{

523+

key: result.childSessionKey,

524+

deleteTranscript: true,

525+

emitLifecycleHooks: true,

526+

},

527+

"delete params",

528+

);

472529

});

473530

});