Skip to content

Commit ad2363c

Browse files
committed
update publisher tests with better stability and performance
1 parent 8e63a11 commit ad2363c

7 files changed

Lines changed: 212 additions & 53 deletions

File tree

apps/agent/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,3 +54,4 @@ ragas-results.json
5454
logs
5555
storage
5656
docker-compose.knowledge-manager.yml
57+
DKG_Node_Publisher_Tests.xml

apps/agent/playwright.config.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ const { defineConfig, devices } = require("@playwright/test");
77
module.exports = defineConfig({
88
testDir: "./tests/e2e",
99
testMatch: "**/*.spec.js",
10-
retries: 2,
10+
retries: 2, // Retry failed tests 2 times (can be overridden per test file)
1111
workers: 1,
1212
timeout: 0, // No timeout per test (infinite polling for publisher plugin)
1313
globalTimeout: 0, // No global timeout
@@ -44,7 +44,7 @@ module.exports = defineConfig({
4444
/* Base URL to use in actions like `await page.goto('/')`. */
4545
baseURL: "http://localhost:8081",
4646
browserName: "chromium",
47-
headless: true,
47+
headless: false,
4848
actionTimeout: 0, // No timeout for actions (infinite polling)
4949
launchOptions: {
5050
slowMo: 1500,

apps/agent/tests/e2e/spec/publisherPluginTest.spec.js

Lines changed: 114 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,100 @@
1+
/**
2+
* Publisher Plugin Performance Test
3+
*
4+
* Tests 10 knowledge asset publications with comprehensive metrics.
5+
*
6+
* REQUIRED ENVIRONMENT VARIABLES (add to .env.testing.testnet.local):
7+
* - PUBLISHING_TIMEOUT_MINUTES=5 # Timeout per publish (5 min for testing, 15 min production)
8+
* - HEALTH_CHECK_INTERVAL_MS=10000 # Check every 10 seconds (60s production)
9+
* - REDIS_HOST=localhost
10+
* - DKGP_DATABASE_URL=mysql://root:@127.0.0.1:3306/dkg_publisher_db
11+
*
12+
* See PUBLISHER_TEST_SETUP.md for detailed configuration instructions.
13+
*
14+
* Test Duration:
15+
* - Best case: 20-50 minutes (all succeed quickly)
16+
* - Worst case: 50 minutes (all timeout at 5 min with maxAttempts=1)
17+
*/
18+
119
const { expect } = require("@playwright/test");
220
const { test } = require("@playwright/test");
321
const { Base } = require("../utils/base");
422
const { LoginPage } = require("../pages/loginPage");
523
const { ChatbotPage } = require("../pages/chatbotPage");
624
const dotenv = require("dotenv");
725
const path = require("path");
26+
const mysql = require("mysql2/promise");
27+
const Redis = require("ioredis");
828

929
let base;
1030
let loginPage;
1131
let chatbotPage;
1232

33+
// Disable retries for this test file (we test 10 assets in one run)
34+
test.describe.configure({ retries: 0 });
35+
1336
function loadEnvFile(envFile) {
1437
dotenv.config({
1538
path: path.join(__dirname, "..", envFile),
1639
override: true,
1740
});
1841
}
1942

43+
/**
44+
* Clean up Redis and MySQL before test suite starts
45+
* Ensures each test run starts with a fresh state
46+
*/
47+
test.beforeAll(async () => {
48+
console.log("\n🧹 Cleaning up test environment...");
49+
50+
try {
51+
// Clear Redis
52+
const redis = new Redis({
53+
host: process.env.REDIS_HOST || "localhost",
54+
port: parseInt(process.env.REDIS_PORT || "6379"),
55+
retryStrategy: () => null, // Don't retry on failure
56+
});
57+
58+
await redis.flushall();
59+
console.log("✅ Redis cleared");
60+
await redis.quit();
61+
} catch (error) {
62+
console.warn("⚠️ Redis cleanup failed (may not be running):", error.message);
63+
}
64+
65+
try {
66+
// Clear MySQL
67+
const dbUrl = process.env.DKGP_DATABASE_URL || "mysql://root:@127.0.0.1:3306/dkg_publisher_db";
68+
const match = dbUrl.match(/mysql:\/\/([^:]+):([^@]*)@([^:]+):(\d+)\/(.+)/);
69+
70+
if (!match) {
71+
throw new Error("Invalid database URL format");
72+
}
73+
74+
const [, user, password, host, port, database] = match;
75+
76+
const connection = await mysql.createConnection({
77+
host,
78+
port: parseInt(port),
79+
user,
80+
password: password || undefined,
81+
database,
82+
});
83+
84+
await connection.execute("DELETE FROM publishing_attempts");
85+
await connection.execute("DELETE FROM assets");
86+
await connection.execute("UPDATE wallets SET is_locked = 0, locked_by = NULL, locked_at = NULL");
87+
await connection.execute("ALTER TABLE assets AUTO_INCREMENT = 1");
88+
89+
console.log("✅ Database cleared (assets, attempts, unlocked wallets)");
90+
await connection.end();
91+
} catch (error) {
92+
console.warn("⚠️ Database cleanup failed:", error.message);
93+
}
94+
95+
console.log("🚀 Test environment ready - starting with clean slate\n");
96+
});
97+
2098
test.beforeEach(async ({ page }) => {
2199
base = new Base(page);
22100
await base.goToWebsite();
@@ -63,8 +141,9 @@ test("Publisher Plugin - Performance Test: Publish 10 Assets with Metrics", asyn
63141
const dateCreated = new Date().toISOString();
64142
const contentId = `urn:publisher-test:asset:${timestamp}-${i}`;
65143

66-
const publishMessage = `Register this Knowledge Asset as PRIVATE using the knowledge-asset-publish tool:
144+
const publishMessage = `Register this Knowledge Asset using the knowledge-asset-publish tool with these settings:
67145
146+
Content:
68147
{
69148
"@context": {
70149
"@vocab": "https://schema.org/"
@@ -77,7 +156,9 @@ test("Publisher Plugin - Performance Test: Publish 10 Assets with Metrics", asyn
77156
"testNumber": ${i}
78157
}
79158
80-
Privacy: private
159+
Settings:
160+
- privacy: private
161+
- maxAttempts: 1
81162
`;
82163

83164
const testStart = Date.now();
@@ -92,21 +173,35 @@ Privacy: private
92173
// Send publish request
93174
await chatbotPage.sendMessage(publishMessage);
94175

95-
// Wait for "Asset registered" response and check for "queued" confirmation (no timeout)
96-
await page.waitForFunction(
97-
() => {
98-
const messages = Array.from(
99-
document.querySelectorAll('[data-testid="chat-message-text"]'),
100-
);
101-
return messages.some((msg) => {
102-
const text = msg.textContent || "";
103-
return /queued for publishing/i.test(text) ||
104-
/Asset registered for publishing/i.test(text);
105-
});
106-
},
107-
{},
108-
{ timeout: 0 },
109-
);
176+
// Wait for "Asset registered" response and check for "queued" confirmation
177+
try {
178+
await page.waitForFunction(
179+
() => {
180+
const messages = Array.from(
181+
document.querySelectorAll('[data-testid="chat-message-text"]'),
182+
);
183+
return messages.some((msg) => {
184+
const text = msg.textContent || "";
185+
return /queued for publishing/i.test(text) ||
186+
/Asset registered for publishing/i.test(text) ||
187+
/registered.*publishing/i.test(text);
188+
});
189+
},
190+
{},
191+
{ timeout: 30000 }, // 30 second timeout
192+
);
193+
} catch (timeoutError) {
194+
// Log all messages to see what we actually got
195+
const allMessages = await page.$$eval(
196+
'[data-testid="chat-message-text"]',
197+
(elements) => elements.map((el) => el.textContent || "")
198+
);
199+
console.log(` ❌ Timeout waiting for queued confirmation. Messages received:`);
200+
allMessages.forEach((msg, idx) => {
201+
console.log(` ${idx + 1}. ${msg.substring(0, 200)}`);
202+
});
203+
throw new Error(`Asset not queued - timeout waiting for confirmation. Last message: ${allMessages[allMessages.length - 1]?.substring(0, 200)}`);
204+
}
110205

111206
console.log(` ✅ Asset queued with Content ID: ${contentId}`);
112207
console.log(` ⏳ Polling status via chatbot until published or failed (no timeout)...`);
@@ -125,19 +220,8 @@ Privacy: private
125220
// Ask chatbot for status
126221
const statusQuery = `What is the status of ${contentId}?`;
127222

128-
// Add timeout wrapper for sendMessage to prevent indefinite hangs
129-
const sendPromise = chatbotPage.sendMessage(statusQuery);
130-
const sendTimeout = new Promise((_, reject) =>
131-
setTimeout(() => reject(new Error('sendMessage timeout after 30s')), 30000)
132-
);
133-
await Promise.race([sendPromise, sendTimeout]);
134-
135-
// Add timeout wrapper for waitForResponse to prevent indefinite hangs
136-
const responsePromise = chatbotPage.waitForResponse(statusQuery);
137-
const responseTimeout = new Promise((_, reject) =>
138-
setTimeout(() => reject(new Error('waitForResponse timeout after 60s')), 60000)
139-
);
140-
await Promise.race([responsePromise, responseTimeout]);
223+
await chatbotPage.sendMessage(statusQuery);
224+
await chatbotPage.waitForResponse(statusQuery);
141225

142226
// Get the last message from chatbot
143227
const messages = await page.getByTestId("chat-message-text").all();

packages/plugin-dkg-publisher/src/mcp/tools.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ export function registerMcpTools(mcp: any, serviceContainer: ServiceContainer |
2424
})
2525
.optional(),
2626
privacy: z.enum(["private", "public"]).optional(),
27+
maxAttempts: z.number().min(1).max(10).optional(),
28+
epochs: z.number().optional(),
29+
priority: z.number().min(1).max(100).optional(),
2730
},
2831
},
2932
async (input: any, req: any) => {
@@ -33,11 +36,18 @@ export function registerMcpTools(mcp: any, serviceContainer: ServiceContainer |
3336

3437
const assetService = serviceContainer.get<AssetService>("assetService");
3538

39+
// Try to read maxAttempts from content.publishOptions first (for backward compatibility)
40+
const maxAttemptsFromContent = input.content?.publishOptions?.maxAttempts;
41+
const maxAttempts = input.maxAttempts || maxAttemptsFromContent || 3;
42+
3643
const assetInput = {
3744
content: input.content,
3845
metadata: input.metadata,
3946
publishOptions: {
4047
privacy: input.privacy || "private",
48+
maxAttempts: maxAttempts,
49+
epochs: input.epochs,
50+
priority: input.priority,
4151
},
4252
};
4353

packages/plugin-dkg-publisher/src/services/HealthMonitor.ts

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,21 @@ export class HealthMonitor extends EventEmitter {
2525
private healthCheckInterval: NodeJS.Timeout | null = null;
2626
private isRunning: boolean = false;
2727
private queueService?: QueueService;
28+
private readonly checkIntervalMs: number;
29+
private readonly publishingTimeoutMinutes: number;
30+
private readonly assignedTimeoutMinutes: number;
2831

2932
constructor(
3033
private db: Database,
3134
private assetService: AssetService,
3235
private walletService: WalletService,
3336
) {
3437
super();
38+
39+
// Configure timeouts from environment (for testing flexibility)
40+
this.checkIntervalMs = parseInt(process.env.HEALTH_CHECK_INTERVAL_MS || "10000"); // Default 10s
41+
this.publishingTimeoutMinutes = parseInt(process.env.PUBLISHING_TIMEOUT_MINUTES || "5"); // Default 5 min
42+
this.assignedTimeoutMinutes = parseInt(process.env.ASSIGNED_TIMEOUT_MINUTES || "2"); // Default 2 min
3543
}
3644

3745
/**
@@ -44,15 +52,16 @@ export class HealthMonitor extends EventEmitter {
4452
/**
4553
* Start health monitoring
4654
*/
47-
start(intervalMs: number = 60000): void {
55+
start(intervalMs?: number): void {
4856
if (this.isRunning) {
4957
console.log("Health monitor already running");
5058
return;
5159
}
5260

61+
const checkInterval = intervalMs || this.checkIntervalMs;
5362
this.isRunning = true;
5463
console.log(
55-
`🏥 Health monitor started (checking every ${intervalMs / 1000} seconds)`,
64+
`🏥 Health monitor started (checking every ${checkInterval / 1000} seconds, publishing timeout: ${this.publishingTimeoutMinutes} min)`,
5665
);
5766

5867
// Stuck asset recovery is now handled by QueuePoller
@@ -63,7 +72,7 @@ export class HealthMonitor extends EventEmitter {
6372
// Set up periodic checks
6473
this.healthCheckInterval = setInterval(async () => {
6574
await this.checkHealth();
66-
}, intervalMs);
75+
}, checkInterval);
6776
}
6877

6978
// Stuck asset recovery moved to QueuePoller for centralized queue management
@@ -110,8 +119,8 @@ export class HealthMonitor extends EventEmitter {
110119
* Check for stuck assets and handle them
111120
*/
112121
private async checkStuckAssets(): Promise<void> {
113-
// Check stuck "assigned" assets (>5 minutes)
114-
const stuckAssigned = await this.assetService.getStuckAssets("assigned", 5);
122+
// Check stuck "assigned" assets
123+
const stuckAssigned = await this.assetService.getStuckAssets("assigned", this.assignedTimeoutMinutes);
115124

116125
for (const asset of stuckAssigned) {
117126
console.log(
@@ -127,7 +136,7 @@ export class HealthMonitor extends EventEmitter {
127136
status: "queued",
128137
assignedAt: null,
129138
lastError:
130-
"Timeout: assigned to wallet but publishing never started within 5 minutes",
139+
`Timeout: assigned to wallet but publishing never started within ${this.assignedTimeoutMinutes} minutes`,
131140
})
132141
.where(eq(assets.id, asset.id));
133142

@@ -143,15 +152,15 @@ export class HealthMonitor extends EventEmitter {
143152
});
144153
}
145154

146-
// Check stuck "publishing" assets (>15 minutes) - increased timeout for long operations
155+
// Check stuck "publishing" assets
147156
const stuckPublishing = await this.assetService.getStuckAssets(
148157
"publishing",
149-
15,
158+
this.publishingTimeoutMinutes,
150159
);
151160

152161
for (const asset of stuckPublishing) {
153162
console.log(
154-
`⚠️ Asset ${asset.id} stuck in publishing status (publishing for over 15 minutes), handling failure...`,
163+
`⚠️ Asset ${asset.id} stuck in publishing status (publishing for over ${this.publishingTimeoutMinutes} minutes), handling failure...`,
155164
);
156165

157166
// 1. Find the latest publishing attempt and mark it as failed
@@ -169,8 +178,8 @@ export class HealthMonitor extends EventEmitter {
169178
.set({
170179
status: "failed",
171180
errorType: "Timeout",
172-
errorMessage: "Publishing exceeded 15 minute timeout",
173-
durationSeconds: 900, // 15 minutes
181+
errorMessage: `Publishing exceeded ${this.publishingTimeoutMinutes} minute timeout`,
182+
durationSeconds: this.publishingTimeoutMinutes * 60,
174183
})
175184
.where(eq(publishingAttempts.id, attemptId));
176185

@@ -194,7 +203,7 @@ export class HealthMonitor extends EventEmitter {
194203
// 3. Handle asset failure with retry logic (increments retryCount)
195204
await this.assetService.handleAssetFailure(
196205
asset.id,
197-
"Timeout: publishing over 15 minutes"
206+
`Timeout: publishing over ${this.publishingTimeoutMinutes} minutes`
198207
);
199208

200209
// 4. Release wallet lock

0 commit comments

Comments
 (0)