Skip to content

Commit 84fc9d7

Browse files
authored
test(agent-core-v2): dispose task fixtures before deleting their session dir (#216)
## Related Issue No issue. Found when a CI shard failed on an unrelated PR with `ENOTEMPTY: directory not empty, rmdir '/tmp/pythinker-bg-limit-agent-*/sessions/test-workspace/test-session'`. ## Problem Five `AgentTaskService` cases create a temporary session directory and an agent context over it, then delete the directory in their `finally` block. None of them disposed the context first, so the context was still writing session state into that directory while the delete walked it. On a loaded CI machine the two race and the run fails with `ENOTEMPTY`. The tests also leaked six agent contexts per run. One case already worked around a symptom of this by awaiting `ISessionMetadata.ready`; the other four had nothing. ## What changed - New `cleanupSessionDir(sessionDir, ...contexts)` test helper: disposes every context, then removes the directory with `fs.rm`'s own `maxRetries` / `retryDelay` — the documented remedy for `ENOTEMPTY` / `EBUSY` / `EPERM` from a concurrent writer. - All five cases now hoist their fixture above the `try` and clean up through the helper. No production code changes; no changeset, since nothing user-visible changes. ## Verification - Instrumented the helper once locally: exactly 6 contexts are now disposed per run of this file, none of which were disposed before. - `test/agent/task/taskManager.test.ts` — 48 passed, 3 consecutive runs - `pnpm --filter @pymodel/agent-core-v2 exec vitest run` — 347 files, 5,723 tests passed - `typecheck`, `tsgo`, `check-no-comments`, oxlint — exit 0 ## Checklist - [x] I have read the [CONTRIBUTING](https://github.com/PyModel/pythinker-code/blob/main/CONTRIBUTING.md) document. - [x] I have linked a related issue (external PRs: the issue must have a maintainer's `/approve`). - [x] I have added tests that prove my feature works. - [x] Ran `gen-changesets` skill, or this PR needs no changeset. - [x] Ran `gen-docs` skill, or this PR needs no doc update. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Tests** * Improved cleanup of temporary session data after test runs. * Ensured test-agent contexts are properly disposed of. * Updated session-based tests to use separate reader and writer fixtures where needed. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent afd313c commit 84fc9d7

1 file changed

Lines changed: 22 additions & 11 deletions

File tree

packages/agent-core-v2/test/agent/task/taskManager.test.ts

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,14 @@ function createAgentTaskService(options: {
6666
};
6767
}
6868

69+
async function cleanupSessionDir(
70+
sessionDir: string,
71+
...contexts: readonly (TestAgentContext | undefined)[]
72+
): Promise<void> {
73+
for (const ctx of contexts) await ctx?.dispose();
74+
await rm(sessionDir, { recursive: true, force: true, maxRetries: 10, retryDelay: 50 });
75+
}
76+
6977
function registerProcess(
7078
manager: IAgentTaskService,
7179
proc: IHostProcess,
@@ -681,8 +689,8 @@ describe('AgentTaskService', () => {
681689

682690
it('stops appending persisted foreground output once the output limit trips', async () => {
683691
const sessionDir = await mkdtemp(join(tmpdir(), 'pythinker-bg-limit-fg-'));
692+
const { ctx, manager } = createAgentTaskService({ sessionDir });
684693
try {
685-
const { manager } = createAgentTaskService({ sessionDir });
686694
const chunks = Array.from({ length: 20 }, () => 'x'.repeat(MiB));
687695
const { proc } = sigtermIgnoringProcess(chunks);
688696

@@ -701,14 +709,14 @@ describe('AgentTaskService', () => {
701709
expect(info).toMatchObject({ status: 'killed' });
702710
expect(output.outputSizeBytes).toBeLessThanOrEqual(LIMIT_BYTES);
703711
} finally {
704-
await rm(sessionDir, { recursive: true, force: true });
712+
await cleanupSessionDir(sessionDir, ctx);
705713
}
706714
});
707715

708716
it('stops appending persisted output once the output limit trips for a detached process task', async () => {
709717
const sessionDir = await mkdtemp(join(tmpdir(), 'pythinker-bg-limit-bg-'));
718+
const { ctx, manager } = createAgentTaskService({ sessionDir });
710719
try {
711-
const { manager } = createAgentTaskService({ sessionDir });
712720
const chunks = Array.from({ length: 20 }, () => 'x'.repeat(MiB));
713721
const { proc } = sigtermIgnoringProcess(chunks);
714722

@@ -727,14 +735,14 @@ describe('AgentTaskService', () => {
727735
expect(info?.stopReason ?? '').toMatch(/output limit/i);
728736
expect(output.outputSizeBytes).toBeLessThanOrEqual(LIMIT_BYTES);
729737
} finally {
730-
await rm(sessionDir, { recursive: true, force: true });
738+
await cleanupSessionDir(sessionDir, ctx);
731739
}
732740
});
733741

734742
it('does not cap a detached subagent result larger than the process output limit', async () => {
735743
const sessionDir = await mkdtemp(join(tmpdir(), 'pythinker-bg-limit-agent-'));
744+
const { ctx, manager } = createAgentTaskService({ sessionDir });
736745
try {
737-
const { manager } = createAgentTaskService({ sessionDir });
738746
const result = 'y'.repeat(20 * MiB);
739747
const taskId = manager.registerTask(
740748
agentTask(Promise.resolve({ result }), 'big subagent result'),
@@ -747,7 +755,7 @@ describe('AgentTaskService', () => {
747755
expect(info).toMatchObject({ status: 'completed' });
748756
expect(output.outputSizeBytes).toBe(Buffer.byteLength(result));
749757
} finally {
750-
await rm(sessionDir, { recursive: true, force: true });
758+
await cleanupSessionDir(sessionDir, ctx);
751759
}
752760
});
753761

@@ -1079,16 +1087,19 @@ describe('AgentTaskService', () => {
10791087

10801088
it('persists graceful process shutdown as killed when stop was requested', async () => {
10811089
const sessionDir = await mkdtemp(join(tmpdir(), 'pythinker-bg-stop-race-'));
1090+
const writerFixture = createAgentTaskService({ sessionDir });
1091+
const writer = writerFixture.manager;
1092+
let readerFixture: TaskServiceFixture | undefined;
10821093
try {
1083-
const writer = createAgentTaskService({ sessionDir }).manager;
10841094
const { proc, resolve } = manuallyResolvedProcess();
10851095
const taskId = registerProcess(writer, proc, 'sleep 60', 'persisted race');
10861096

10871097
const stopPromise = writer.stop(taskId, 'user requested');
10881098
resolve(0);
10891099
await stopPromise;
10901100

1091-
const reader = createAgentTaskService({ sessionDir }).manager;
1101+
readerFixture = createAgentTaskService({ sessionDir });
1102+
const reader = readerFixture.manager;
10921103
await reader.loadFromDisk();
10931104

10941105
expect(reader.getTask(taskId)).toMatchObject({
@@ -1098,7 +1109,7 @@ describe('AgentTaskService', () => {
10981109
stopReason: 'user requested',
10991110
});
11001111
} finally {
1101-
await rm(sessionDir, { recursive: true, force: true });
1112+
await cleanupSessionDir(sessionDir, writerFixture.ctx, readerFixture?.ctx);
11021113
}
11031114
});
11041115

@@ -1285,15 +1296,15 @@ describe('AgentTaskService', () => {
12851296

12861297
it('getTask on an unknown id does not create persisted state', async () => {
12871298
const sessionDir = await mkdtemp(join(tmpdir(), 'pythinker-bg-mgr-missing-'));
1299+
const { ctx, manager, persistence } = createAgentTaskService({ sessionDir });
12881300
try {
1289-
const { ctx, manager, persistence } = createAgentTaskService({ sessionDir });
12901301

12911302
expect(manager.getTask('bash-bogusss0')).toBeUndefined();
12921303

12931304
expect(await persistence!.listTasks()).toEqual([]);
12941305
await ctx.get(ISessionMetadata).ready;
12951306
} finally {
1296-
await rm(sessionDir, { recursive: true, force: true });
1307+
await cleanupSessionDir(sessionDir, ctx);
12971308
}
12981309
});
12991310

0 commit comments

Comments
 (0)