From 80ea3d81af1400b801b301d57568b85a2e2282fd Mon Sep 17 00:00:00 2001 From: richyu Date: Tue, 8 Jul 2025 14:10:53 -0700 Subject: [PATCH 01/13] Add ordered history API endpoint with deprecation warnings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add new /history_v2 endpoint that returns history in array format - Revert /history endpoint to original object format for backward compatibility - Add deprecation warnings to legacy /history endpoints - Add get_ordered_history() method to PromptQueue class - Add comprehensive tests for both history endpoints 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- execution.py | 17 +++ server.py | 19 ++++ tests/inference/test_execution.py | 179 +++++++++++++++++++++++++++++- 3 files changed, 213 insertions(+), 2 deletions(-) diff --git a/execution.py b/execution.py index c3a62f1cb..825f1ed5e 100644 --- a/execution.py +++ b/execution.py @@ -1116,6 +1116,23 @@ class PromptQueue: else: return {} + def get_ordered_history(self, max_items=None, offset=-1): + with self.mutex: + out = [] + + i = 0 + if offset < 0 and max_items is not None: + offset = len(self.history) - max_items + for k in self.history: + if i >= offset: + out.append({k: self.history[k]}) + if max_items is not None and len(out) >= max_items: + break + i += 1 + + + return {"history": out} + def wipe_history(self): with self.mutex: self.history = {} diff --git a/server.py b/server.py index 71a58f0fa..c4c236c11 100644 --- a/server.py +++ b/server.py @@ -652,6 +652,25 @@ class PromptServer(): prompt_id = request.match_info.get("prompt_id", None) return web.json_response(self.prompt_queue.get_history(prompt_id=prompt_id)) + @routes.get("/history_v2") + async def get_ordered_history(request): + max_items = request.rel_url.query.get("max_items", None) + if max_items is not None: + max_items = int(max_items) + + offset = request.rel_url.query.get("offset", None) + if offset is not None: + offset = int(offset) + else: + offset = -1 + + return web.json_response(self.prompt_queue.get_ordered_history(max_items=max_items, offset=offset)) + + @routes.get("/history_v2/{prompt_id}") + async def get_history_v2_prompt_id(request): + prompt_id = request.match_info.get("prompt_id", None) + return web.json_response(self.prompt_queue.get_history(prompt_id=prompt_id)) + @routes.get("/queue") async def get_queue(request): queue_info = {} diff --git a/tests/inference/test_execution.py b/tests/inference/test_execution.py index 9d3d685cc..1b74a0ce7 100644 --- a/tests/inference/test_execution.py +++ b/tests/inference/test_execution.py @@ -63,10 +63,39 @@ class ComfyClient: with urllib.request.urlopen("http://{}/view?{}".format(self.server_address, url_values)) as response: return response.read() - def get_history(self, prompt_id): - with urllib.request.urlopen("http://{}/history/{}".format(self.server_address, prompt_id)) as response: + def get_history(self, prompt_id=None, max_items=None): + if prompt_id: + url = "http://{}/history/{}".format(self.server_address, prompt_id) + else: + url = "http://{}/history".format(self.server_address) + if max_items is not None: + url += "?max_items={}".format(max_items) + with urllib.request.urlopen(url) as response: return json.loads(response.read()) + def get_ordered_history(self, max_items=None, offset=None): + url = "http://{}/history_v2".format(self.server_address) + params = {} + if max_items is not None: + params['max_items'] = str(max_items) + if offset is not None: + params['offset'] = str(offset) + if params: + url += "?" + urllib.parse.urlencode(params) + with urllib.request.urlopen(url) as response: + return json.loads(response.read()) + + def get_history_v2_for_prompt(self, prompt_id): + url = "http://{}/history_v2/{}".format(self.server_address, prompt_id) + with urllib.request.urlopen(url) as response: + return json.loads(response.read()) + + def clear_history(self): + data = json.dumps({"clear": True}).encode('utf-8') + req = urllib.request.Request("http://{}/history".format(self.server_address), data=data) + req.add_header('Content-Type', 'application/json') + urllib.request.urlopen(req) + def set_test_name(self, name): self.test_name = name @@ -585,3 +614,149 @@ class TestExecution: assert len(images) == 2, "Should have 2 images" assert numpy.array(images[0]).min() == 0 and numpy.array(images[0]).max() == 0, "First image should be black" assert numpy.array(images[1]).min() == 0 and numpy.array(images[1]).max() == 0, "Second image should also be black" + + def test_ordered_history_endpoint(self, client: ComfyClient, builder: GraphBuilder): + """Test the ordered history endpoint returns data in chronological order.""" + # Clear history to start fresh + client.clear_history() + + # Run multiple prompts to test ordering + prompt_ids = [] + for _ in range(3): + g = builder + input1 = g.node("StubImage", content="BLACK", height=512, width=512, batch_size=1) + g.node("SaveImage", images=input1.out(0)) + + result = client.run(g) + prompt_ids.append(result.get_prompt_id()) + time.sleep(0.1) # Small delay to ensure different timestamps + + # Test ordered history endpoint + ordered_history = client.get_ordered_history() + assert "history" in ordered_history, "Ordered history should have history key" + assert isinstance(ordered_history["history"], list), "Ordered history should be a list" + assert len(ordered_history["history"]) == 3, "Should have exactly 3 prompts in history" + + # Verify chronological ordering (most recent first) + history_prompt_ids = [] + for item in ordered_history["history"]: + for prompt_id in item.keys(): + history_prompt_ids.append(prompt_id) + + # Should be in chronological order (oldest first, as they're added in completion order) + assert history_prompt_ids == prompt_ids, "History should be in chronological order" + + def test_history_prompt_id_endpoint(self, client: ComfyClient, builder: GraphBuilder): + """Test fetching specific prompt history by ID.""" + g = builder + input1 = g.node("StubImage", content="BLACK", height=512, width=512, batch_size=1) + g.node("SaveImage", images=input1.out(0)) + + result = client.run(g) + prompt_id = result.get_prompt_id() + + # Test legacy history endpoint for specific prompt + legacy_history = client.get_history(prompt_id) + assert prompt_id in legacy_history, "Legacy history should contain prompt ID" + assert "outputs" in legacy_history[prompt_id], "Legacy history should have outputs" + + # Test history_v2 endpoint for specific prompt + specific_history = client.get_history_v2_for_prompt(prompt_id) + assert prompt_id in specific_history, "History v2 should contain prompt ID" + assert specific_history[prompt_id] == legacy_history[prompt_id], "History v2 data should match legacy history" + + def test_history_max_items(self, client: ComfyClient, builder: GraphBuilder): + """Test legacy history endpoint with max_items parameter.""" + # Clear history to start fresh + client.clear_history() + + # Run multiple prompts to test pagination + for _ in range(5): + g = GraphBuilder() # Create fresh GraphBuilder for each run + input1 = g.node("StubImage", content="BLACK", height=512, width=512, batch_size=1) + g.node("SaveImage", images=input1.out(0)) + + client.run(g) + time.sleep(0.1) # Small delay to ensure different timestamps + + # Test max_items parameter on legacy history endpoint + limited_history = client.get_history(max_items=2) + assert len(limited_history) == 2, "History should return exactly max_items" + + def test_ordered_history_max_items_and_offset(self, client: ComfyClient, builder: GraphBuilder): + """Test ordered history endpoint with max_items and offset parameters.""" + # Clear history to start fresh + client.clear_history() + + # Run multiple prompts to test pagination + for _ in range(5): + g = GraphBuilder() # Create fresh GraphBuilder for each run + input1 = g.node("StubImage", content="BLACK", height=512, width=512, batch_size=1) + g.node("SaveImage", images=input1.out(0)) + + client.run(g) + time.sleep(0.1) # Small delay to ensure different timestamps + + # Test max_items parameter on ordered history endpoint + limited_ordered = client.get_ordered_history(max_items=3) + assert "history" in limited_ordered, "Limited ordered history should have history key" + assert len(limited_ordered["history"]) == 3, "Ordered history should return exactly max_items" + + # Test pagination with offset as cursor + full_history = client.get_ordered_history() + assert len(full_history["history"]) == 5, "Should have 5 items in full history" + + # Extract prompt IDs from full history for comparison + full_prompt_ids = [] + for item in full_history["history"]: + for prompt_id in item.keys(): + full_prompt_ids.append(prompt_id) + + # Test proper pagination behavior with offset as cursor + + # Test first page: offset=0, max_items=2 + page1 = client.get_ordered_history(max_items=2, offset=0) + assert len(page1["history"]) == 2, "First page should have 2 items" + page1_ids = [] + for item in page1["history"]: + for prompt_id in item.keys(): + page1_ids.append(prompt_id) + assert page1_ids == full_prompt_ids[0:2], "First page should contain items at indices 0-1" + + # Test second page: offset=2, max_items=2 + page2 = client.get_ordered_history(max_items=2, offset=2) + assert len(page2["history"]) == 2, "Second page should have 2 items" + page2_ids = [] + for item in page2["history"]: + for prompt_id in item.keys(): + page2_ids.append(prompt_id) + assert page2_ids == full_prompt_ids[2:4], "Second page should contain items at indices 2-3" + + # Test third page: offset=4, max_items=2 + page3 = client.get_ordered_history(max_items=2, offset=4) + assert len(page3["history"]) == 1, "Third page should have 1 remaining item" + page3_ids = [] + for item in page3["history"]: + for prompt_id in item.keys(): + page3_ids.append(prompt_id) + assert page3_ids == full_prompt_ids[4:5], "Third page should contain item at index 4" + + # Verify no overlap between pages + all_paginated_ids = page1_ids + page2_ids + page3_ids + assert len(set(all_paginated_ids)) == 5, "All paginated IDs should be unique" + assert set(all_paginated_ids) == set(full_prompt_ids), "Paginated results should cover all items" + + # Test default behavior: get last N items (no offset specified) + # When offset < 0 and max_items is specified, offset = len(history) - max_items + last_2_items = client.get_ordered_history(max_items=2) + assert len(last_2_items["history"]) == 2, "Default behavior should return 2 items" + last_2_ids = [] + for item in last_2_items["history"]: + for prompt_id in item.keys(): + last_2_ids.append(prompt_id) + # This should be equivalent to offset=3 (5-2=3) + assert last_2_ids == full_prompt_ids[3:5], "Default behavior should return last 2 items" + + # Test offset beyond available items + beyond_offset = client.get_ordered_history(max_items=2, offset=10) + assert len(beyond_offset["history"]) == 0, "Offset beyond items should return empty list" From 4a2172758ac89a71e6c4b2c62b41ccbbfdc69d0a Mon Sep 17 00:00:00 2001 From: Richard Yu Date: Wed, 9 Jul 2025 13:51:48 -0700 Subject: [PATCH 02/13] make execution.py more pythonic --- execution.py | 25 ++++++++++++++----------- server.py | 7 ++----- tests/inference/test_execution.py | 30 ++++++------------------------ 3 files changed, 22 insertions(+), 40 deletions(-) diff --git a/execution.py b/execution.py index 825f1ed5e..515c6222b 100644 --- a/execution.py +++ b/execution.py @@ -1116,22 +1116,25 @@ class PromptQueue: else: return {} - def get_ordered_history(self, max_items=None, offset=-1): + def get_ordered_history(self, max_items=None, offset=0): with self.mutex: - out = [] + history_keys = list(self.history.keys()) - i = 0 if offset < 0 and max_items is not None: - offset = len(self.history) - max_items - for k in self.history: - if i >= offset: - out.append({k: self.history[k]}) - if max_items is not None and len(out) >= max_items: - break - i += 1 + offset = max(0, len(history_keys) - max_items) + # Use slice to get the desired range + end_index = offset + max_items if max_items is not None else None + selected_keys = history_keys[offset:end_index] - return {"history": out} + # Build history items with prompt_id field + history_items = [] + for key in selected_keys: + item = copy.deepcopy(self.history[key]) + item["prompt_id"] = key + history_items.append(item) + + return {"history": history_items} def wipe_history(self): with self.mutex: diff --git a/server.py b/server.py index c4c236c11..36fd01bbc 100644 --- a/server.py +++ b/server.py @@ -658,11 +658,8 @@ class PromptServer(): if max_items is not None: max_items = int(max_items) - offset = request.rel_url.query.get("offset", None) - if offset is not None: - offset = int(offset) - else: - offset = -1 + offset = request.rel_url.query.get("offset", 0) + offset = int(offset) return web.json_response(self.prompt_queue.get_ordered_history(max_items=max_items, offset=offset)) diff --git a/tests/inference/test_execution.py b/tests/inference/test_execution.py index 1b74a0ce7..3e06bd740 100644 --- a/tests/inference/test_execution.py +++ b/tests/inference/test_execution.py @@ -638,10 +638,7 @@ class TestExecution: assert len(ordered_history["history"]) == 3, "Should have exactly 3 prompts in history" # Verify chronological ordering (most recent first) - history_prompt_ids = [] - for item in ordered_history["history"]: - for prompt_id in item.keys(): - history_prompt_ids.append(prompt_id) + history_prompt_ids = [item["prompt_id"] for item in ordered_history["history"]] # Should be in chronological order (oldest first, as they're added in completion order) assert history_prompt_ids == prompt_ids, "History should be in chronological order" @@ -707,38 +704,26 @@ class TestExecution: assert len(full_history["history"]) == 5, "Should have 5 items in full history" # Extract prompt IDs from full history for comparison - full_prompt_ids = [] - for item in full_history["history"]: - for prompt_id in item.keys(): - full_prompt_ids.append(prompt_id) + full_prompt_ids = [item["prompt_id"] for item in full_history["history"]] # Test proper pagination behavior with offset as cursor # Test first page: offset=0, max_items=2 page1 = client.get_ordered_history(max_items=2, offset=0) assert len(page1["history"]) == 2, "First page should have 2 items" - page1_ids = [] - for item in page1["history"]: - for prompt_id in item.keys(): - page1_ids.append(prompt_id) + page1_ids = [item["prompt_id"] for item in page1["history"]] assert page1_ids == full_prompt_ids[0:2], "First page should contain items at indices 0-1" # Test second page: offset=2, max_items=2 page2 = client.get_ordered_history(max_items=2, offset=2) assert len(page2["history"]) == 2, "Second page should have 2 items" - page2_ids = [] - for item in page2["history"]: - for prompt_id in item.keys(): - page2_ids.append(prompt_id) + page2_ids = [item["prompt_id"] for item in page2["history"]] assert page2_ids == full_prompt_ids[2:4], "Second page should contain items at indices 2-3" # Test third page: offset=4, max_items=2 page3 = client.get_ordered_history(max_items=2, offset=4) assert len(page3["history"]) == 1, "Third page should have 1 remaining item" - page3_ids = [] - for item in page3["history"]: - for prompt_id in item.keys(): - page3_ids.append(prompt_id) + page3_ids = [item["prompt_id"] for item in page3["history"]] assert page3_ids == full_prompt_ids[4:5], "Third page should contain item at index 4" # Verify no overlap between pages @@ -750,10 +735,7 @@ class TestExecution: # When offset < 0 and max_items is specified, offset = len(history) - max_items last_2_items = client.get_ordered_history(max_items=2) assert len(last_2_items["history"]) == 2, "Default behavior should return 2 items" - last_2_ids = [] - for item in last_2_items["history"]: - for prompt_id in item.keys(): - last_2_ids.append(prompt_id) + last_2_ids = [item["prompt_id"] for item in last_2_items["history"]] # This should be equivalent to offset=3 (5-2=3) assert last_2_ids == full_prompt_ids[3:5], "Default behavior should return last 2 items" From 565b5376202183a399e0ade65baa36c5ebe3295d Mon Sep 17 00:00:00 2001 From: Richard Yu Date: Thu, 10 Jul 2025 14:26:31 -0700 Subject: [PATCH 03/13] remove prompt[2] and prompt[4] from history_v2 response --- execution.py | 6 ++++ tests/inference/test_execution.py | 55 +++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+) diff --git a/execution.py b/execution.py index 515c6222b..b20540e0a 100644 --- a/execution.py +++ b/execution.py @@ -1132,6 +1132,12 @@ class PromptQueue: for key in selected_keys: item = copy.deepcopy(self.history[key]) item["prompt_id"] = key + + # Remove prompt[2] (workflow) and prompt[4] (execute_outputs) to reduce response size + if "prompt" in item: + priority, prompt_id, _, extra_data, _ = item["prompt"] + item["prompt"] = [priority, prompt_id, extra_data] + history_items.append(item) return {"history": history_items} diff --git a/tests/inference/test_execution.py b/tests/inference/test_execution.py index 3e06bd740..df368b00a 100644 --- a/tests/inference/test_execution.py +++ b/tests/inference/test_execution.py @@ -742,3 +742,58 @@ class TestExecution: # Test offset beyond available items beyond_offset = client.get_ordered_history(max_items=2, offset=10) assert len(beyond_offset["history"]) == 0, "Offset beyond items should return empty list" + + def test_ordered_history_prompt_field_filtering_unit(self): + """Unit test for prompt field filtering logic in get_ordered_history.""" + from execution import PromptQueue + + # Mock server + class MockServer: + def queue_updated(self): pass + + # Create queue and add mock history + queue = PromptQueue(MockServer()) + + # Mock history entry with full prompt structure + mock_prompt_tuple = ( + 12345, # priority/timestamp + 'test-prompt-123', # prompt_id + {'nodes': {'1': {'class_type': 'SaveImage'}}}, # workflow (should be filtered) + {'client_id': 'test-client'}, # extra_data + ['1'] # execute_outputs (should be filtered) + ) + + queue.history['test-prompt-123'] = { + 'prompt': mock_prompt_tuple, + 'outputs': {'1': {'images': []}}, + 'status': {'completed': True, 'messages': []}, + 'meta': {'1': {'node_id': '1'}} + } + + # Test get_ordered_history with our filtering + result = queue.get_ordered_history() + + # Verify structure + assert "history" in result, "Result should have history key" + assert len(result["history"]) == 1, "Should have one history item" + + history_item = result["history"][0] + + # Verify prompt_id field is added + assert "prompt_id" in history_item, "History item should have prompt_id field" + assert history_item["prompt_id"] == 'test-prompt-123', "prompt_id should match" + + # Verify prompt field is filtered + filtered_prompt = history_item["prompt"] + assert isinstance(filtered_prompt, list), "Filtered prompt should be a list" + assert len(filtered_prompt) == 3, f"Filtered prompt should have 3 elements, got {len(filtered_prompt)}" + + # Verify correct elements are preserved + assert filtered_prompt[0] == 12345, "Priority should be preserved" + assert filtered_prompt[1] == 'test-prompt-123', "Prompt ID should be preserved" + assert filtered_prompt[2] == {'client_id': 'test-client'}, "Extra data should be preserved" + + # Verify other fields are unchanged + assert history_item["outputs"] == {'1': {'images': []}}, "Outputs should be unchanged" + assert history_item["status"] == {'completed': True, 'messages': []}, "Status should be unchanged" + assert history_item["meta"] == {'1': {'node_id': '1'}}, "Meta should be unchanged" From a2b8688e205552c720aa348d0350c119853e673e Mon Sep 17 00:00:00 2001 From: Richard Yu Date: Thu, 10 Jul 2025 14:50:19 -0700 Subject: [PATCH 04/13] remove unused "completed" and cached node info from history_v2 response --- execution.py | 27 ++++++++++++++++++------ tests/inference/test_execution.py | 35 +++++++++++++++++++++++++++++-- 2 files changed, 54 insertions(+), 8 deletions(-) diff --git a/execution.py b/execution.py index b20540e0a..01c751ecf 100644 --- a/execution.py +++ b/execution.py @@ -1130,13 +1130,28 @@ class PromptQueue: # Build history items with prompt_id field history_items = [] for key in selected_keys: - item = copy.deepcopy(self.history[key]) - item["prompt_id"] = key + history_entry = self.history[key] - # Remove prompt[2] (workflow) and prompt[4] (execute_outputs) to reduce response size - if "prompt" in item: - priority, prompt_id, _, extra_data, _ = item["prompt"] - item["prompt"] = [priority, prompt_id, extra_data] + # Extract and filter prompt data + if "prompt" in history_entry: + priority, prompt_id, _, extra_data, _ = history_entry["prompt"] + filtered_prompt = [priority, prompt_id, extra_data] + else: + filtered_prompt = None + + # Create lightweight history response + item = { + "prompt_id": key, + "outputs": history_entry.get("outputs", {}), + "meta": history_entry.get("meta", {}), + "prompt": filtered_prompt, + "status": { + "status_str": history_entry["status"]["status_str"], + "messages": [(e, {k: v for k, v in d.items() if k != "nodes"}) + if e == "execution_cached" else (e, d) + for e, d in history_entry["status"]["messages"]] + } if history_entry.get("status") else None + } history_items.append(item) diff --git a/tests/inference/test_execution.py b/tests/inference/test_execution.py index df368b00a..af9bb0592 100644 --- a/tests/inference/test_execution.py +++ b/tests/inference/test_execution.py @@ -766,7 +766,14 @@ class TestExecution: queue.history['test-prompt-123'] = { 'prompt': mock_prompt_tuple, 'outputs': {'1': {'images': []}}, - 'status': {'completed': True, 'messages': []}, + 'status': { + 'status_str': 'success', + 'completed': True, # Should be filtered out + 'messages': [ + ('execution_cached', {'nodes': ['node1', 'node2'], 'timestamp': 1234567890}), # 'nodes' should be filtered + ('execution_start', {'timestamp': 1234567800}) # Should remain unchanged + ] + }, 'meta': {'1': {'node_id': '1'}} } @@ -795,5 +802,29 @@ class TestExecution: # Verify other fields are unchanged assert history_item["outputs"] == {'1': {'images': []}}, "Outputs should be unchanged" - assert history_item["status"] == {'completed': True, 'messages': []}, "Status should be unchanged" assert history_item["meta"] == {'1': {'node_id': '1'}}, "Meta should be unchanged" + + # Verify status field filtering + status = history_item["status"] + assert "status_str" in status, "Status should have status_str" + assert status["status_str"] == 'success', "Status string should be preserved" + assert "completed" not in status, "Completed field should be filtered out" + assert "messages" in status, "Status should have messages" + + # Verify message filtering + messages = status["messages"] + assert len(messages) == 2, "Should have 2 messages" + + # Check execution_cached message has nodes filtered out + execution_cached_msg = messages[0] + assert execution_cached_msg[0] == 'execution_cached', "First message should be execution_cached" + cached_data = execution_cached_msg[1] + assert "nodes" not in cached_data, "Nodes field should be filtered from execution_cached messages" + assert "timestamp" in cached_data, "Timestamp should be preserved in execution_cached messages" + assert cached_data["timestamp"] == 1234567890, "Timestamp value should be correct" + + # Check execution_start message remains unchanged + execution_start_msg = messages[1] + assert execution_start_msg[0] == 'execution_start', "Second message should be execution_start" + start_data = execution_start_msg[1] + assert start_data == {'timestamp': 1234567800}, "execution_start message should be unchanged" From fdc2a53653c9860a02570673af460046066e09d9 Mon Sep 17 00:00:00 2001 From: Richard Yu Date: Thu, 10 Jul 2025 15:49:27 -0700 Subject: [PATCH 05/13] remove workflow json from history_v2 response - client will need to use /history_v2/:prompt_id --- execution.py | 3 +++ tests/inference/test_execution.py | 16 ++++++++++++++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/execution.py b/execution.py index 01c751ecf..6df2eaa8c 100644 --- a/execution.py +++ b/execution.py @@ -1135,6 +1135,9 @@ class PromptQueue: # Extract and filter prompt data if "prompt" in history_entry: priority, prompt_id, _, extra_data, _ = history_entry["prompt"] + # Remove workflow from extra_pnginfo + if "extra_pnginfo" in extra_data: + extra_data["extra_pnginfo"].pop("workflow", None) filtered_prompt = [priority, prompt_id, extra_data] else: filtered_prompt = None diff --git a/tests/inference/test_execution.py b/tests/inference/test_execution.py index af9bb0592..682f41184 100644 --- a/tests/inference/test_execution.py +++ b/tests/inference/test_execution.py @@ -759,7 +759,13 @@ class TestExecution: 12345, # priority/timestamp 'test-prompt-123', # prompt_id {'nodes': {'1': {'class_type': 'SaveImage'}}}, # workflow (should be filtered) - {'client_id': 'test-client'}, # extra_data + { # extra_data + 'client_id': 'test-client', + 'extra_pnginfo': { + 'workflow': {'nodes': {'1': {'class_type': 'SaveImage'}}}, # Should be filtered out + 'version': '1.0' # Should be preserved + } + }, ['1'] # execute_outputs (should be filtered) ) @@ -798,7 +804,13 @@ class TestExecution: # Verify correct elements are preserved assert filtered_prompt[0] == 12345, "Priority should be preserved" assert filtered_prompt[1] == 'test-prompt-123', "Prompt ID should be preserved" - assert filtered_prompt[2] == {'client_id': 'test-client'}, "Extra data should be preserved" + + # Verify extra_data filtering + extra_data = filtered_prompt[2] + assert extra_data['client_id'] == 'test-client', "Client ID should be preserved" + assert 'extra_pnginfo' in extra_data, "extra_pnginfo should be present" + assert 'workflow' not in extra_data['extra_pnginfo'], "Workflow should be filtered out" + assert extra_data['extra_pnginfo']['version'] == '1.0', "Other extra_pnginfo data should be preserved" # Verify other fields are unchanged assert history_item["outputs"] == {'1': {'images': []}}, "Outputs should be unchanged" From 36e73633af2046a89e01be97afa120e77b85389f Mon Sep 17 00:00:00 2001 From: Richard Yu Date: Fri, 11 Jul 2025 15:27:30 -0700 Subject: [PATCH 06/13] return prompt as dict rather than array --- execution.py | 6 +++++- tests/inference/test_execution.py | 12 +++++++----- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/execution.py b/execution.py index 6df2eaa8c..fb403d75d 100644 --- a/execution.py +++ b/execution.py @@ -1138,7 +1138,11 @@ class PromptQueue: # Remove workflow from extra_pnginfo if "extra_pnginfo" in extra_data: extra_data["extra_pnginfo"].pop("workflow", None) - filtered_prompt = [priority, prompt_id, extra_data] + filtered_prompt = { + "priority": priority, + "prompt_id": prompt_id, + "extra_data": extra_data + } else: filtered_prompt = None diff --git a/tests/inference/test_execution.py b/tests/inference/test_execution.py index 682f41184..f5dac4fa9 100644 --- a/tests/inference/test_execution.py +++ b/tests/inference/test_execution.py @@ -798,15 +798,17 @@ class TestExecution: # Verify prompt field is filtered filtered_prompt = history_item["prompt"] - assert isinstance(filtered_prompt, list), "Filtered prompt should be a list" - assert len(filtered_prompt) == 3, f"Filtered prompt should have 3 elements, got {len(filtered_prompt)}" + assert isinstance(filtered_prompt, dict), "Filtered prompt should be a dictionary" + assert "priority" in filtered_prompt, "Filtered prompt should have priority" + assert "prompt_id" in filtered_prompt, "Filtered prompt should have prompt_id" + assert "extra_data" in filtered_prompt, "Filtered prompt should have extra_data" # Verify correct elements are preserved - assert filtered_prompt[0] == 12345, "Priority should be preserved" - assert filtered_prompt[1] == 'test-prompt-123', "Prompt ID should be preserved" + assert filtered_prompt["priority"] == 12345, "Priority should be preserved" + assert filtered_prompt["prompt_id"] == 'test-prompt-123', "Prompt ID should be preserved" # Verify extra_data filtering - extra_data = filtered_prompt[2] + extra_data = filtered_prompt["extra_data"] assert extra_data['client_id'] == 'test-client', "Client ID should be preserved" assert 'extra_pnginfo' in extra_data, "extra_pnginfo should be present" assert 'workflow' not in extra_data['extra_pnginfo'], "Workflow should be filtered out" From 0609136d5c640389c8516a1218840e5fb710e92e Mon Sep 17 00:00:00 2001 From: Richard Yu Date: Fri, 11 Jul 2025 16:38:17 -0700 Subject: [PATCH 07/13] deep copy history --- execution.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/execution.py b/execution.py index fb403d75d..7827c3ebc 100644 --- a/execution.py +++ b/execution.py @@ -1130,12 +1130,13 @@ class PromptQueue: # Build history items with prompt_id field history_items = [] for key in selected_keys: - history_entry = self.history[key] + # Deep copy the history entry to avoid modifying the original + history_entry = copy.deepcopy(self.history[key]) # Extract and filter prompt data if "prompt" in history_entry: priority, prompt_id, _, extra_data, _ = history_entry["prompt"] - # Remove workflow from extra_pnginfo + # Remove workflow from extra_pnginfo (safe to modify since we deepcopied) if "extra_pnginfo" in extra_data: extra_data["extra_pnginfo"].pop("workflow", None) filtered_prompt = { From c9a5ecd18480a260c2678f1637bd9c448eb00f6c Mon Sep 17 00:00:00 2001 From: Richard Yu Date: Sat, 12 Jul 2025 14:27:41 -0700 Subject: [PATCH 08/13] update /history_v2/:prompt_id to also have prompts in dict fmt --- execution.py | 20 ++++++++++++++++++ server.py | 2 +- tests/inference/test_execution.py | 35 ++++++++++++++++++++++--------- 3 files changed, 46 insertions(+), 11 deletions(-) diff --git a/execution.py b/execution.py index 7827c3ebc..ec514d58f 100644 --- a/execution.py +++ b/execution.py @@ -1165,6 +1165,26 @@ class PromptQueue: return {"history": history_items} + def get_history_v2(self, prompt_id): + with self.mutex: + if prompt_id in self.history: + history_entry = copy.deepcopy(self.history[prompt_id]) + + # Extract and convert prompt tuple to dict + if "prompt" in history_entry: + priority, prompt_id_inner, prompt_data, extra_data, outputs_to_execute = history_entry["prompt"] + history_entry["prompt"] = { + "priority": priority, + "prompt_id": prompt_id_inner, + "prompt": prompt_data, + "extra_data": extra_data, + "outputs_to_execute": outputs_to_execute + } + + return {prompt_id: history_entry} + else: + return {} + def wipe_history(self): with self.mutex: self.history = {} diff --git a/server.py b/server.py index 36fd01bbc..1fa914e8b 100644 --- a/server.py +++ b/server.py @@ -666,7 +666,7 @@ class PromptServer(): @routes.get("/history_v2/{prompt_id}") async def get_history_v2_prompt_id(request): prompt_id = request.match_info.get("prompt_id", None) - return web.json_response(self.prompt_queue.get_history(prompt_id=prompt_id)) + return web.json_response(self.prompt_queue.get_history_v2(prompt_id=prompt_id)) @routes.get("/queue") async def get_queue(request): diff --git a/tests/inference/test_execution.py b/tests/inference/test_execution.py index f5dac4fa9..2184f544d 100644 --- a/tests/inference/test_execution.py +++ b/tests/inference/test_execution.py @@ -660,9 +660,24 @@ class TestExecution: # Test history_v2 endpoint for specific prompt specific_history = client.get_history_v2_for_prompt(prompt_id) assert prompt_id in specific_history, "History v2 should contain prompt ID" - assert specific_history[prompt_id] == legacy_history[prompt_id], "History v2 data should match legacy history" + + # Verify key fields match between legacy and v2 + v2_data = specific_history[prompt_id] + legacy_data = legacy_history[prompt_id] + + # Check that outputs and status match + assert v2_data["outputs"] == legacy_data["outputs"], "Outputs should match" + assert v2_data["status"] == legacy_data["status"], "Status should match" + + # Verify prompt is converted to dict format in v2 + assert isinstance(v2_data["prompt"], dict), "Prompt should be a dictionary in v2" + assert "prompt_id" in v2_data["prompt"], "Prompt dict should have prompt_id" + assert "priority" in v2_data["prompt"], "Prompt dict should have priority" + assert "prompt" in v2_data["prompt"], "Prompt dict should have prompt data" + assert "extra_data" in v2_data["prompt"], "Prompt dict should have extra_data" + assert "outputs_to_execute" in v2_data["prompt"], "Prompt dict should have outputs_to_execute" - def test_history_max_items(self, client: ComfyClient, builder: GraphBuilder): + def test_history_max_items(self, client: ComfyClient): """Test legacy history endpoint with max_items parameter.""" # Clear history to start fresh client.clear_history() @@ -680,7 +695,7 @@ class TestExecution: limited_history = client.get_history(max_items=2) assert len(limited_history) == 2, "History should return exactly max_items" - def test_ordered_history_max_items_and_offset(self, client: ComfyClient, builder: GraphBuilder): + def test_ordered_history_max_items_and_offset(self, client: ComfyClient): """Test ordered history endpoint with max_items and offset parameters.""" # Clear history to start fresh client.clear_history() @@ -731,13 +746,13 @@ class TestExecution: assert len(set(all_paginated_ids)) == 5, "All paginated IDs should be unique" assert set(all_paginated_ids) == set(full_prompt_ids), "Paginated results should cover all items" - # Test default behavior: get last N items (no offset specified) - # When offset < 0 and max_items is specified, offset = len(history) - max_items - last_2_items = client.get_ordered_history(max_items=2) - assert len(last_2_items["history"]) == 2, "Default behavior should return 2 items" - last_2_ids = [item["prompt_id"] for item in last_2_items["history"]] - # This should be equivalent to offset=3 (5-2=3) - assert last_2_ids == full_prompt_ids[3:5], "Default behavior should return last 2 items" + # Test default behavior: get first N items (no offset specified) + # When offset is not specified, it defaults to 0 + first_2_items = client.get_ordered_history(max_items=2) + assert len(first_2_items["history"]) == 2, "Default behavior should return 2 items" + first_2_ids = [item["prompt_id"] for item in first_2_items["history"]] + # This should be equivalent to offset=0 with max_items=2 + assert first_2_ids == full_prompt_ids[0:2], "Default behavior should return first 2 items" # Test offset beyond available items beyond_offset = client.get_ordered_history(max_items=2, offset=10) From e79f925811ed9a5bf422864ace08539c4d0564bc Mon Sep 17 00:00:00 2001 From: Richard Yu Date: Mon, 14 Jul 2025 18:03:28 -0700 Subject: [PATCH 09/13] document the api output - it's not very clear --- execution.py | 56 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/execution.py b/execution.py index ec514d58f..e7889cbd7 100644 --- a/execution.py +++ b/execution.py @@ -1117,6 +1117,37 @@ class PromptQueue: return {} def get_ordered_history(self, max_items=None, offset=0): + """ + Retrieves execution history in chronological order with pagination support. + Used by the /history API endpoint for lightweight history listings. + + API Output Structure: + { + "history": [ + { + "prompt_id": str, # Unique identifier for this execution + "outputs": dict, # Node outputs {node_id: ui_data} + "meta": dict, # Node metadata {node_id: {node_id, display_node, parent_node, real_node_id}} + "prompt": { + "priority": int, # Execution priority + "prompt_id": str, # Same as root prompt_id + "extra_data": dict # Additional metadata (workflow removed from extra_pnginfo) + } | None, # None if no prompt data available + "status": { + "status_str": str, # "success" | "error" + "messages": [ # Filtered execution event messages + (event_name: str, event_data: dict) + ] + } | None # None if no status recorded + }, + # ... more history items + ] + } + + Parameters: + - max_items: Maximum number of items to return (None = all) + - offset: Starting index (0-based, negative values calculated from end) + """ with self.mutex: history_keys = list(self.history.keys()) @@ -1166,6 +1197,31 @@ class PromptQueue: return {"history": history_items} def get_history_v2(self, prompt_id): + """ + Retrieves execution history for a specific prompt ID in v2 format. + + API Output Structure: + { + "": { + "prompt": { + "priority": int, # Execution priority + "prompt_id": str, # Same as the key + "prompt": dict, # The workflow/node data + "extra_data": dict, # Additional metadata (client_id, etc.) + "outputs_to_execute": list # Node IDs to execute + }, + "outputs": dict, # Node outputs {node_id: ui_data} + "meta": dict, # Node metadata {node_id: {node_id, display_node, parent_node, real_node_id}} + "status": { + "status_str": str, # "success" | "error" + "completed": bool, # Whether execution finished + "messages": list # Execution event messages + } | None # None if no status recorded + } + } + + Returns empty dict {} if prompt_id not found. + """ with self.mutex: if prompt_id in self.history: history_entry = copy.deepcopy(self.history[prompt_id]) From b8963ef9425621bb07fb0e6c1757e7c6d173daa1 Mon Sep 17 00:00:00 2001 From: Richard Yu Date: Mon, 14 Jul 2025 18:12:07 -0700 Subject: [PATCH 10/13] clean up comments --- execution.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/execution.py b/execution.py index e7889cbd7..9e4b4981c 100644 --- a/execution.py +++ b/execution.py @@ -1119,7 +1119,8 @@ class PromptQueue: def get_ordered_history(self, max_items=None, offset=0): """ Retrieves execution history in chronological order with pagination support. - Used by the /history API endpoint for lightweight history listings. + Returns a lightweight list of history objects. + Used by the /history_v2. API Output Structure: { @@ -1154,20 +1155,15 @@ class PromptQueue: if offset < 0 and max_items is not None: offset = max(0, len(history_keys) - max_items) - # Use slice to get the desired range end_index = offset + max_items if max_items is not None else None selected_keys = history_keys[offset:end_index] - # Build history items with prompt_id field history_items = [] for key in selected_keys: - # Deep copy the history entry to avoid modifying the original history_entry = copy.deepcopy(self.history[key]) - # Extract and filter prompt data if "prompt" in history_entry: priority, prompt_id, _, extra_data, _ = history_entry["prompt"] - # Remove workflow from extra_pnginfo (safe to modify since we deepcopied) if "extra_pnginfo" in extra_data: extra_data["extra_pnginfo"].pop("workflow", None) filtered_prompt = { @@ -1178,7 +1174,6 @@ class PromptQueue: else: filtered_prompt = None - # Create lightweight history response item = { "prompt_id": key, "outputs": history_entry.get("outputs", {}), @@ -1198,7 +1193,8 @@ class PromptQueue: def get_history_v2(self, prompt_id): """ - Retrieves execution history for a specific prompt ID in v2 format. + Retrieves execution history for a specific prompt ID. + Used by /history_v2/:prompt_id API Output Structure: { @@ -1226,7 +1222,6 @@ class PromptQueue: if prompt_id in self.history: history_entry = copy.deepcopy(self.history[prompt_id]) - # Extract and convert prompt tuple to dict if "prompt" in history_entry: priority, prompt_id_inner, prompt_data, extra_data, outputs_to_execute = history_entry["prompt"] history_entry["prompt"] = { From 72ec6ce493ec0d1025982c0ac1925ee4678b2f69 Mon Sep 17 00:00:00 2001 From: Richard Yu Date: Fri, 18 Jul 2025 15:30:19 -0700 Subject: [PATCH 11/13] create new dict instead of deepcopy --- execution.py | 58 +++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 42 insertions(+), 16 deletions(-) diff --git a/execution.py b/execution.py index 9e4b4981c..35135be13 100644 --- a/execution.py +++ b/execution.py @@ -1160,31 +1160,48 @@ class PromptQueue: history_items = [] for key in selected_keys: - history_entry = copy.deepcopy(self.history[key]) + history_entry = self.history[key] + # Build filtered prompt without modifying original data + filtered_prompt = None if "prompt" in history_entry: priority, prompt_id, _, extra_data, _ = history_entry["prompt"] - if "extra_pnginfo" in extra_data: - extra_data["extra_pnginfo"].pop("workflow", None) + + # Create new extra_data dict, excluding workflow from extra_pnginfo + filtered_extra_data = {} + for k, v in extra_data.items(): + if k == "extra_pnginfo": + # Create new dict without workflow + filtered_extra_data[k] = { + pk: pv for pk, pv in v.items() + if pk != "workflow" + } + else: + # Reference original value for other keys + filtered_extra_data[k] = v + filtered_prompt = { "priority": priority, "prompt_id": prompt_id, - "extra_data": extra_data + "extra_data": filtered_extra_data } - else: - filtered_prompt = None - item = { - "prompt_id": key, - "outputs": history_entry.get("outputs", {}), - "meta": history_entry.get("meta", {}), - "prompt": filtered_prompt, - "status": { + # Build status without modifying original + status = None + if history_entry.get("status"): + status = { "status_str": history_entry["status"]["status_str"], "messages": [(e, {k: v for k, v in d.items() if k != "nodes"}) if e == "execution_cached" else (e, d) for e, d in history_entry["status"]["messages"]] - } if history_entry.get("status") else None + } + + item = { + "prompt_id": key, + "outputs": history_entry.get("outputs", {}), # Reference, not modified + "meta": history_entry.get("meta", {}), # Reference, not modified + "prompt": filtered_prompt, + "status": status } history_items.append(item) @@ -1220,11 +1237,15 @@ class PromptQueue: """ with self.mutex: if prompt_id in self.history: - history_entry = copy.deepcopy(self.history[prompt_id]) + history_entry = self.history[prompt_id] + # Build new entry without modifying original + new_entry = {} + + # Convert prompt tuple to dict if present if "prompt" in history_entry: priority, prompt_id_inner, prompt_data, extra_data, outputs_to_execute = history_entry["prompt"] - history_entry["prompt"] = { + new_entry["prompt"] = { "priority": priority, "prompt_id": prompt_id_inner, "prompt": prompt_data, @@ -1232,7 +1253,12 @@ class PromptQueue: "outputs_to_execute": outputs_to_execute } - return {prompt_id: history_entry} + # Copy other fields by reference + for key, value in history_entry.items(): + if key != "prompt": + new_entry[key] = value + + return {prompt_id: new_entry} else: return {} From b64df1196fc4fdc28e763b165961af3b46616f31 Mon Sep 17 00:00:00 2001 From: Richard Yu Date: Fri, 18 Jul 2025 15:44:22 -0700 Subject: [PATCH 12/13] remove extra comments --- execution.py | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/execution.py b/execution.py index 35135be13..6804aa0e3 100644 --- a/execution.py +++ b/execution.py @@ -1162,22 +1162,18 @@ class PromptQueue: for key in selected_keys: history_entry = self.history[key] - # Build filtered prompt without modifying original data filtered_prompt = None if "prompt" in history_entry: priority, prompt_id, _, extra_data, _ = history_entry["prompt"] - # Create new extra_data dict, excluding workflow from extra_pnginfo filtered_extra_data = {} for k, v in extra_data.items(): if k == "extra_pnginfo": - # Create new dict without workflow filtered_extra_data[k] = { pk: pv for pk, pv in v.items() if pk != "workflow" } else: - # Reference original value for other keys filtered_extra_data[k] = v filtered_prompt = { @@ -1186,7 +1182,6 @@ class PromptQueue: "extra_data": filtered_extra_data } - # Build status without modifying original status = None if history_entry.get("status"): status = { @@ -1198,8 +1193,8 @@ class PromptQueue: item = { "prompt_id": key, - "outputs": history_entry.get("outputs", {}), # Reference, not modified - "meta": history_entry.get("meta", {}), # Reference, not modified + "outputs": history_entry.get("outputs", {}), + "meta": history_entry.get("meta", {}), "prompt": filtered_prompt, "status": status } @@ -1239,10 +1234,8 @@ class PromptQueue: if prompt_id in self.history: history_entry = self.history[prompt_id] - # Build new entry without modifying original new_entry = {} - # Convert prompt tuple to dict if present if "prompt" in history_entry: priority, prompt_id_inner, prompt_data, extra_data, outputs_to_execute = history_entry["prompt"] new_entry["prompt"] = { @@ -1253,7 +1246,6 @@ class PromptQueue: "outputs_to_execute": outputs_to_execute } - # Copy other fields by reference for key, value in history_entry.items(): if key != "prompt": new_entry[key] = value From 94684db953c58ba5b3c819725cf60903725ee260 Mon Sep 17 00:00:00 2001 From: Richard Yu Date: Fri, 18 Jul 2025 17:14:46 -0700 Subject: [PATCH 13/13] fix lint --- execution.py | 34 +++++++++++++------------- tests/inference/test_execution.py | 40 +++++++++++++++---------------- 2 files changed, 37 insertions(+), 37 deletions(-) diff --git a/execution.py b/execution.py index 6804aa0e3..daf61ba19 100644 --- a/execution.py +++ b/execution.py @@ -1121,7 +1121,7 @@ class PromptQueue: Retrieves execution history in chronological order with pagination support. Returns a lightweight list of history objects. Used by the /history_v2. - + API Output Structure: { "history": [ @@ -1135,7 +1135,7 @@ class PromptQueue: "extra_data": dict # Additional metadata (workflow removed from extra_pnginfo) } | None, # None if no prompt data available "status": { - "status_str": str, # "success" | "error" + "status_str": str, # "success" | "error" "messages": [ # Filtered execution event messages (event_name: str, event_data: dict) ] @@ -1144,7 +1144,7 @@ class PromptQueue: # ... more history items ] } - + Parameters: - max_items: Maximum number of items to return (None = all) - offset: Starting index (0-based, negative values calculated from end) @@ -1161,36 +1161,36 @@ class PromptQueue: history_items = [] for key in selected_keys: history_entry = self.history[key] - + filtered_prompt = None if "prompt" in history_entry: priority, prompt_id, _, extra_data, _ = history_entry["prompt"] - + filtered_extra_data = {} for k, v in extra_data.items(): if k == "extra_pnginfo": filtered_extra_data[k] = { - pk: pv for pk, pv in v.items() + pk: pv for pk, pv in v.items() if pk != "workflow" } else: filtered_extra_data[k] = v - + filtered_prompt = { "priority": priority, "prompt_id": prompt_id, "extra_data": filtered_extra_data } - + status = None if history_entry.get("status"): status = { "status_str": history_entry["status"]["status_str"], - "messages": [(e, {k: v for k, v in d.items() if k != "nodes"}) + "messages": [(e, {k: v for k, v in d.items() if k != "nodes"}) if e == "execution_cached" else (e, d) for e, d in history_entry["status"]["messages"]] } - + item = { "prompt_id": key, "outputs": history_entry.get("outputs", {}), @@ -1198,7 +1198,7 @@ class PromptQueue: "prompt": filtered_prompt, "status": status } - + history_items.append(item) return {"history": history_items} @@ -1207,7 +1207,7 @@ class PromptQueue: """ Retrieves execution history for a specific prompt ID. Used by /history_v2/:prompt_id - + API Output Structure: { "": { @@ -1227,15 +1227,15 @@ class PromptQueue: } | None # None if no status recorded } } - + Returns empty dict {} if prompt_id not found. """ with self.mutex: if prompt_id in self.history: history_entry = self.history[prompt_id] - + new_entry = {} - + if "prompt" in history_entry: priority, prompt_id_inner, prompt_data, extra_data, outputs_to_execute = history_entry["prompt"] new_entry["prompt"] = { @@ -1245,11 +1245,11 @@ class PromptQueue: "extra_data": extra_data, "outputs_to_execute": outputs_to_execute } - + for key, value in history_entry.items(): if key != "prompt": new_entry[key] = value - + return {prompt_id: new_entry} else: return {} diff --git a/tests/inference/test_execution.py b/tests/inference/test_execution.py index 2184f544d..b5f41c70d 100644 --- a/tests/inference/test_execution.py +++ b/tests/inference/test_execution.py @@ -660,15 +660,15 @@ class TestExecution: # Test history_v2 endpoint for specific prompt specific_history = client.get_history_v2_for_prompt(prompt_id) assert prompt_id in specific_history, "History v2 should contain prompt ID" - + # Verify key fields match between legacy and v2 v2_data = specific_history[prompt_id] legacy_data = legacy_history[prompt_id] - + # Check that outputs and status match assert v2_data["outputs"] == legacy_data["outputs"], "Outputs should match" assert v2_data["status"] == legacy_data["status"], "Status should match" - + # Verify prompt is converted to dict format in v2 assert isinstance(v2_data["prompt"], dict), "Prompt should be a dictionary in v2" assert "prompt_id" in v2_data["prompt"], "Prompt dict should have prompt_id" @@ -761,18 +761,18 @@ class TestExecution: def test_ordered_history_prompt_field_filtering_unit(self): """Unit test for prompt field filtering logic in get_ordered_history.""" from execution import PromptQueue - + # Mock server class MockServer: def queue_updated(self): pass - + # Create queue and add mock history queue = PromptQueue(MockServer()) - + # Mock history entry with full prompt structure mock_prompt_tuple = ( 12345, # priority/timestamp - 'test-prompt-123', # prompt_id + 'test-prompt-123', # prompt_id {'nodes': {'1': {'class_type': 'SaveImage'}}}, # workflow (should be filtered) { # extra_data 'client_id': 'test-client', @@ -783,7 +783,7 @@ class TestExecution: }, ['1'] # execute_outputs (should be filtered) ) - + queue.history['test-prompt-123'] = { 'prompt': mock_prompt_tuple, 'outputs': {'1': {'images': []}}, @@ -797,53 +797,53 @@ class TestExecution: }, 'meta': {'1': {'node_id': '1'}} } - + # Test get_ordered_history with our filtering result = queue.get_ordered_history() - + # Verify structure assert "history" in result, "Result should have history key" assert len(result["history"]) == 1, "Should have one history item" - + history_item = result["history"][0] - + # Verify prompt_id field is added assert "prompt_id" in history_item, "History item should have prompt_id field" assert history_item["prompt_id"] == 'test-prompt-123', "prompt_id should match" - + # Verify prompt field is filtered filtered_prompt = history_item["prompt"] assert isinstance(filtered_prompt, dict), "Filtered prompt should be a dictionary" assert "priority" in filtered_prompt, "Filtered prompt should have priority" assert "prompt_id" in filtered_prompt, "Filtered prompt should have prompt_id" assert "extra_data" in filtered_prompt, "Filtered prompt should have extra_data" - + # Verify correct elements are preserved assert filtered_prompt["priority"] == 12345, "Priority should be preserved" assert filtered_prompt["prompt_id"] == 'test-prompt-123', "Prompt ID should be preserved" - + # Verify extra_data filtering extra_data = filtered_prompt["extra_data"] assert extra_data['client_id'] == 'test-client', "Client ID should be preserved" assert 'extra_pnginfo' in extra_data, "extra_pnginfo should be present" assert 'workflow' not in extra_data['extra_pnginfo'], "Workflow should be filtered out" assert extra_data['extra_pnginfo']['version'] == '1.0', "Other extra_pnginfo data should be preserved" - + # Verify other fields are unchanged assert history_item["outputs"] == {'1': {'images': []}}, "Outputs should be unchanged" assert history_item["meta"] == {'1': {'node_id': '1'}}, "Meta should be unchanged" - + # Verify status field filtering status = history_item["status"] assert "status_str" in status, "Status should have status_str" assert status["status_str"] == 'success', "Status string should be preserved" assert "completed" not in status, "Completed field should be filtered out" assert "messages" in status, "Status should have messages" - + # Verify message filtering messages = status["messages"] assert len(messages) == 2, "Should have 2 messages" - + # Check execution_cached message has nodes filtered out execution_cached_msg = messages[0] assert execution_cached_msg[0] == 'execution_cached', "First message should be execution_cached" @@ -851,7 +851,7 @@ class TestExecution: assert "nodes" not in cached_data, "Nodes field should be filtered from execution_cached messages" assert "timestamp" in cached_data, "Timestamp should be preserved in execution_cached messages" assert cached_data["timestamp"] == 1234567890, "Timestamp value should be correct" - + # Check execution_start message remains unchanged execution_start_msg = messages[1] assert execution_start_msg[0] == 'execution_start', "Second message should be execution_start"