-
Notifications
You must be signed in to change notification settings - Fork 277
[Fix] Subtasks fail to return when users work across windows #1471
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
6e947f7
82edb4c
35e0c31
633beab
55a5878
931086a
eeec7b7
5ab572d
2a5afaa
a14cbc5
9254a63
fe38302
4af1f02
ffa9a41
919f883
d1830c5
32402b6
89ff8c1
42f0c5a
8a308ab
eea1dce
a68d2e7
180a755
9d1bc3e
e518d55
7cde85a
4698172
53d6711
29b686e
ccfa4c0
406c886
851005c
fcee590
7bd0ce7
89175d8
21ac215
9453e63
03328d3
3e76847
cb4293a
c5a05ed
966b3c3
068fc5a
83b929b
81ffc9c
80e7446
6f6891c
b77f21a
7b75f3c
1a0b0b6
4ea3c64
cd45e80
2344a83
e022743
96ff9ad
0233444
64086a3
df0205f
6c74c56
4de5c60
7496338
c167f28
dc9267b
fb2474e
ea3fbe2
6b0ae92
ab70262
e99bf9e
9a547e3
670c121
fdd5d52
edb26f2
b3824d5
702f4b2
81c6b98
adddd8b
e7c6672
ddcb0d0
e7ba95c
1ddb3dc
45e5137
45ef778
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,65 @@ | ||||||||||
| import { AsyncTaskTracker } from "./async-task-tracker.js" | ||||||||||
|
|
||||||||||
| describe("AsyncTaskTracker", () => { | ||||||||||
| it("drains resolving and rejecting tasks", async () => { | ||||||||||
| const tracker = new AsyncTaskTracker() | ||||||||||
| let resolveTask!: () => void | ||||||||||
| let rejectTask!: (error: Error) => void | ||||||||||
| const resolving = new Promise<void>((resolve) => (resolveTask = resolve)) | ||||||||||
| const rejecting = new Promise<void>((_resolve, reject) => (rejectTask = reject)) | ||||||||||
|
|
||||||||||
| expect(tracker.track(resolving)).toBe(resolving) | ||||||||||
| expect(tracker.track(rejecting)).toBe(rejecting) | ||||||||||
| const drained = vi.fn() | ||||||||||
| const drain = tracker.drain().then(drained) | ||||||||||
| await Promise.resolve() | ||||||||||
| expect(drained).not.toHaveBeenCalled() | ||||||||||
|
|
||||||||||
| resolveTask() | ||||||||||
| rejectTask(new Error("expected rejection")) | ||||||||||
| await drain | ||||||||||
| expect(drained).toHaveBeenCalledOnce() | ||||||||||
| }) | ||||||||||
|
|
||||||||||
| it("includes tasks tracked while a drain is in progress", async () => { | ||||||||||
| const tracker = new AsyncTaskTracker() | ||||||||||
| let resolveFirst!: () => void | ||||||||||
| let resolveSecond!: () => void | ||||||||||
| tracker.track(new Promise<void>((resolve) => (resolveFirst = resolve))) | ||||||||||
| const drain = tracker.drain() | ||||||||||
| tracker.track(new Promise<void>((resolve) => (resolveSecond = resolve))) | ||||||||||
|
|
||||||||||
| resolveFirst() | ||||||||||
| let drained = false | ||||||||||
| void drain.then(() => (drained = true)) | ||||||||||
| await Promise.resolve() | ||||||||||
| expect(drained).toBe(false) | ||||||||||
| resolveSecond() | ||||||||||
| await drain | ||||||||||
| expect(drained).toBe(true) | ||||||||||
| }) | ||||||||||
|
|
||||||||||
| it("stops guarded callbacks before draining tracked work", async () => { | ||||||||||
| const tracker = new AsyncTaskTracker() | ||||||||||
| const callback = vi.fn(async (value: string) => value.length) | ||||||||||
| let resolveTask!: () => void | ||||||||||
| const task = tracker.track(new Promise<void>((resolve) => (resolveTask = resolve))) | ||||||||||
| expect(tracker.isActive).toBe(true) | ||||||||||
| await expect(tracker.runIfActive(callback, "active")).resolves.toBe(6) | ||||||||||
| await expect(tracker.trackIfActive(async () => "tracked")).resolves.toBe("tracked") | ||||||||||
|
|
||||||||||
| let drained = false | ||||||||||
| const close = tracker.closeAndDrain().then(() => (drained = true)) | ||||||||||
| await Promise.resolve() | ||||||||||
| expect(drained).toBe(false) | ||||||||||
| expect(tracker.isActive).toBe(false) | ||||||||||
| await expect(tracker.runIfActive(callback, "closed")).resolves.toBeUndefined() | ||||||||||
| await expect(tracker.trackIfActive(async () => "closed")).rejects.toThrow("tracker is closed") | ||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Assert that closed The callback on Line 57 is not observable. A regression that starts the callback and then rejects can pass this assertion. Use a mock callback and assert that it was not called after As per path instructions, tests must include behavior-focused negative and error cases. Proposed test change- await expect(tracker.trackIfActive(async () => "closed")).rejects.toThrow("tracker is closed")
+ const closedCallback = vi.fn(async () => "closed")
+ await expect(tracker.trackIfActive(closedCallback)).rejects.toThrow("tracker is closed")
+ expect(closedCallback).not.toHaveBeenCalled()📝 Committable suggestion
Suggested change
🤖 Prompt for AI AgentsSource: Path instructions |
||||||||||
| expect(callback).toHaveBeenCalledOnce() | ||||||||||
|
|
||||||||||
| resolveTask() | ||||||||||
| await task | ||||||||||
| await close | ||||||||||
| expect(drained).toBe(true) | ||||||||||
| }) | ||||||||||
| }) | ||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| export class AsyncTaskTracker { | ||
| private readonly tasks = new Set<Promise<unknown>>() | ||
| private active = true | ||
|
|
||
| get isActive(): boolean { | ||
| return this.active | ||
| } | ||
|
|
||
| track<T>(task: Promise<T>): Promise<T> { | ||
| this.tasks.add(task) | ||
|
zoomote[bot] marked this conversation as resolved.
|
||
| void task.then( | ||
| () => this.tasks.delete(task), | ||
| () => this.tasks.delete(task), | ||
| ) | ||
| return task | ||
| } | ||
|
|
||
| trackIfActive<T>(callback: () => Promise<T>): Promise<T> { | ||
| if (!this.active) return Promise.reject(new Error("Async task tracker is closed")) | ||
| return this.track(callback()) | ||
| } | ||
|
|
||
| runIfActive<T, Result>(callback: (value: T) => Promise<Result>, value: T): Promise<Result | undefined> { | ||
| return this.active ? callback(value) : Promise.resolve(undefined) | ||
| } | ||
|
|
||
| async drain(): Promise<void> { | ||
| while (this.tasks.size) await Promise.allSettled(this.tasks) | ||
| } | ||
|
|
||
| async closeAndDrain(): Promise<void> { | ||
| this.active = false | ||
| await this.drain() | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.