pi-harness-runtime 1.1.18 → 1.1.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/harness/e2e/auto-test-loop.ts +94 -29
- package/harness/e2e/tools-detector.ts +20 -17
- package/harness/index.ts +20 -3
- package/harness/loop-runtime.ts +49 -13
- package/harness/skill-sync.test.ts +57 -0
- package/harness/skill-sync.ts +247 -0
- package/harness/ssh-hang-recovery.ts +280 -0
- package/package.json +1 -1
- package/packages/a2a-adapter/package.json +1 -1
- package/packages/architecture-generator/package.json +1 -1
- package/packages/auth/package.json +1 -1
- package/packages/autonomous-refactor/package.json +1 -1
- package/packages/autonomous-runtime/package.json +1 -1
- package/packages/cache-strategy/package.json +1 -1
- package/packages/capability-registry/package.json +1 -1
- package/packages/checkpoint/package.json +1 -1
- package/packages/cli-plugin-sdk/package.json +1 -1
- package/packages/clipboard/package.json +1 -1
- package/packages/clipboard-plugin/package.json +1 -1
- package/packages/code-generation/package.json +1 -1
- package/packages/code-review/package.json +1 -1
- package/packages/codex-adapter/package.json +1 -1
- package/packages/config-capture/package.json +1 -1
- package/packages/context-compiler/package.json +1 -1
- package/packages/context-discovery/package.json +1 -1
- package/packages/context-manager/package.json +1 -1
- package/packages/cookie-sanitizer/package.json +1 -1
- package/packages/cost-optimizer/package.json +1 -1
- package/packages/dependency-analyzer/package.json +1 -1
- package/packages/django-plugin/package.json +1 -1
- package/packages/doc-generator/package.json +1 -1
- package/packages/evaluation-engine/package.json +1 -1
- package/packages/evaluation-runner/package.json +1 -1
- package/packages/event-bus/package.json +1 -1
- package/packages/event-store/package.json +1 -1
- package/packages/experience-replay/package.json +1 -1
- package/packages/feedback-collector/package.json +1 -1
- package/packages/file-copy-helper/package.json +1 -1
- package/packages/framework-detector/package.json +1 -1
- package/packages/framework-plugin-sdk/package.json +1 -1
- package/packages/frappe-plugin/package.json +1 -1
- package/packages/generic-web-plugin/package.json +1 -1
- package/packages/health-monitor/package.json +1 -1
- package/packages/intent-analyzer/package.json +1 -1
- package/packages/knowledge-graph/package.json +1 -1
- package/packages/knowledge-retrieval/package.json +1 -1
- package/packages/laravel-plugin/package.json +1 -1
- package/packages/learning-engine/package.json +1 -1
- package/packages/mcp-adapter/package.json +1 -1
- package/packages/memory-engine/package.json +1 -1
- package/packages/milestone-manager/package.json +1 -1
- package/packages/model-registry/package.json +1 -1
- package/packages/nextjs-plugin/package.json +1 -1
- package/packages/notification/package.json +1 -1
- package/packages/observability/package.json +1 -1
- package/packages/okf-indexer/package.json +1 -1
- package/packages/performance-optimizer/package.json +1 -1
- package/packages/privilege-broker/package.json +1 -1
- package/packages/project-analyzer/package.json +1 -1
- package/packages/project-bootstrap/package.json +1 -1
- package/packages/projection-engine/package.json +1 -1
- package/packages/prompt-compiler/package.json +1 -1
- package/packages/prompt-versioning/package.json +1 -1
- package/packages/provider-adapter-sdk/package.json +1 -1
- package/packages/provider-router/package.json +1 -1
- package/packages/provider-selector/package.json +1 -1
- package/packages/providers/package.json +1 -1
- package/packages/quota-manager/package.json +1 -1
- package/packages/rate-limiter/package.json +1 -1
- package/packages/react-vite-plugin/package.json +1 -1
- package/packages/release-manager/package.json +1 -1
- package/packages/requirement-compiler/package.json +1 -1
- package/packages/runtime/package.json +1 -1
- package/packages/scheduler/package.json +1 -1
- package/packages/scheduler-adapter/package.json +1 -1
- package/packages/session/package.json +1 -1
- package/packages/session-api/package.json +1 -1
- package/packages/session-export/package.json +1 -1
- package/packages/shared-context/package.json +1 -1
- package/packages/skill-mcp-client/package.json +28 -0
- package/packages/skill-mcp-client/src/client.ts +171 -0
- package/packages/skill-mcp-client/src/errors.ts +75 -0
- package/packages/skill-mcp-client/src/index.ts +13 -0
- package/packages/skill-mcp-client/src/types.ts +123 -0
- package/packages/skill-registry/package.json +1 -1
- package/packages/sprint-planner/package.json +1 -1
- package/packages/subscription-engine/package.json +1 -1
- package/packages/task-compiler/package.json +1 -1
- package/packages/tencentdb-memory/package.json +1 -1
- package/packages/tencentdb-sync/package.json +1 -1
- package/packages/test-data-generator/package.json +1 -1
- package/packages/test-generator/package.json +1 -1
- package/packages/todo-bd-sync/package.json +1 -1
- package/packages/token-estimation/package.json +1 -1
- package/packages/token-optimizer/package.json +1 -1
- package/packages/tui/package.json +1 -1
- package/packages/types/package.json +1 -1
- package/packages/workflow-events/package.json +1 -1
- package/packages/workspace-scanner/package.json +1 -1
- package/packages/worktree/package.json +1 -1
- package/packages/write-review/package.json +1 -1
|
@@ -20,18 +20,14 @@
|
|
|
20
20
|
* Unknown Failure → Retry (max 3x) → Ask Human
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
|
-
import { existsSync, readdirSync, writeFileSync
|
|
24
|
-
import { join, extname
|
|
23
|
+
import { existsSync, readdirSync, writeFileSync } from "node:fs";
|
|
24
|
+
import { join, extname } from "node:path";
|
|
25
25
|
import { execSync } from "child_process";
|
|
26
26
|
import {
|
|
27
|
-
detectProjectType,
|
|
28
|
-
detectAvailableTools,
|
|
29
27
|
makeSmartDecision,
|
|
30
28
|
getE2EToolsConfig,
|
|
31
29
|
type ProjectType,
|
|
32
|
-
type TestingTool,
|
|
33
30
|
type E2EToolsConfig,
|
|
34
|
-
type SmartDecision,
|
|
35
31
|
} from "./tools-detector.js";
|
|
36
32
|
import { logInfo, logWarn, logError } from "../glm-quota-logger.js";
|
|
37
33
|
|
|
@@ -49,6 +45,8 @@ export interface AutoTestConfig {
|
|
|
49
45
|
autoFixEnabled: boolean;
|
|
50
46
|
generateStubs: boolean;
|
|
51
47
|
notifyOnEscalation: boolean;
|
|
48
|
+
/** Global timeout for entire test run (default: 30 minutes) */
|
|
49
|
+
globalTimeoutMs?: number;
|
|
52
50
|
}
|
|
53
51
|
|
|
54
52
|
export interface AutoTestResult {
|
|
@@ -68,6 +66,7 @@ const DEFAULT_CONFIG: AutoTestConfig = {
|
|
|
68
66
|
autoFixEnabled: true,
|
|
69
67
|
generateStubs: true,
|
|
70
68
|
notifyOnEscalation: true,
|
|
69
|
+
globalTimeoutMs: 30 * 60 * 1000, // 30 minutes
|
|
71
70
|
};
|
|
72
71
|
|
|
73
72
|
/**
|
|
@@ -95,7 +94,10 @@ export async function runAutoTestLoop(
|
|
|
95
94
|
toolsConfig.projectType,
|
|
96
95
|
toolsConfig.detectedTools,
|
|
97
96
|
);
|
|
98
|
-
logInfo(
|
|
97
|
+
logInfo(
|
|
98
|
+
null,
|
|
99
|
+
`Smart decision: ${decision.suggestedAction} (confidence: ${decision.confidence})`,
|
|
100
|
+
);
|
|
99
101
|
|
|
100
102
|
// Handle decision
|
|
101
103
|
switch (decision.suggestedAction) {
|
|
@@ -128,7 +130,7 @@ export async function runAutoTestLoop(
|
|
|
128
130
|
logInfo(null, "Generating test stubs...");
|
|
129
131
|
await generateTestStubs(cfg.projectRoot, toolsConfig.projectType);
|
|
130
132
|
}
|
|
131
|
-
|
|
133
|
+
// Fall through to run tests
|
|
132
134
|
|
|
133
135
|
case "auto_run":
|
|
134
136
|
default:
|
|
@@ -149,6 +151,25 @@ async function runTestLoop(
|
|
|
149
151
|
let lastResult: E2ETestResult | null = null;
|
|
150
152
|
|
|
151
153
|
while (retriesAttempted <= cfg.maxRetries) {
|
|
154
|
+
// Check global timeout
|
|
155
|
+
const elapsedMs = Date.now() - startTime;
|
|
156
|
+
if (elapsedMs > (cfg.globalTimeoutMs ?? 30 * 60 * 1000)) {
|
|
157
|
+
logWarn(
|
|
158
|
+
null,
|
|
159
|
+
`⏱️ Global timeout reached (${elapsedMs}ms). Escalating to human.`,
|
|
160
|
+
);
|
|
161
|
+
return {
|
|
162
|
+
success: false,
|
|
163
|
+
testsRun: true,
|
|
164
|
+
testsPassed: false,
|
|
165
|
+
retriesAttempted,
|
|
166
|
+
escalatedToHuman: true,
|
|
167
|
+
escalationReason: `Global timeout reached after ${Math.round(elapsedMs / 1000)}s`,
|
|
168
|
+
output: lastResult?.output || "",
|
|
169
|
+
duration: elapsedMs,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
152
173
|
logInfo(
|
|
153
174
|
null,
|
|
154
175
|
`Running E2E tests (attempt ${retriesAttempted + 1}/${cfg.maxRetries + 1})...`,
|
|
@@ -165,18 +186,28 @@ async function runTestLoop(
|
|
|
165
186
|
testsPassed: true,
|
|
166
187
|
retriesAttempted,
|
|
167
188
|
escalatedToHuman: false,
|
|
168
|
-
output: lastResult && lastResult.output || "",
|
|
189
|
+
output: (lastResult && lastResult.output) || "",
|
|
169
190
|
duration: Date.now() - startTime,
|
|
170
191
|
};
|
|
171
192
|
}
|
|
172
193
|
|
|
173
194
|
// Tests failed
|
|
174
|
-
logWarn(
|
|
195
|
+
logWarn(
|
|
196
|
+
null,
|
|
197
|
+
`❌ E2E tests failed: ${(lastResult && lastResult.errorMessage) || " error"}`,
|
|
198
|
+
);
|
|
175
199
|
retriesAttempted++;
|
|
176
200
|
|
|
177
201
|
// Analyze failure
|
|
178
|
-
const analysis = analyzeFailure(
|
|
179
|
-
|
|
202
|
+
const analysis = analyzeFailure(
|
|
203
|
+
lastResult!,
|
|
204
|
+
cfg.projectRoot,
|
|
205
|
+
toolsConfig.projectType,
|
|
206
|
+
);
|
|
207
|
+
logInfo(
|
|
208
|
+
null,
|
|
209
|
+
`Failure analysis: ${analysis.canFix ? "Can auto-fix" : "Needs manual intervention"}`,
|
|
210
|
+
);
|
|
180
211
|
|
|
181
212
|
if (analysis.canFix && cfg.autoFixEnabled) {
|
|
182
213
|
// Try to fix
|
|
@@ -196,8 +227,8 @@ async function runTestLoop(
|
|
|
196
227
|
testsPassed: false,
|
|
197
228
|
retriesAttempted,
|
|
198
229
|
escalatedToHuman: true,
|
|
199
|
-
escalationReason: `Environment limitation: ${lastResult.errorMessage || "Unable to run tests in current environment"}`,
|
|
200
|
-
output: lastResult && lastResult.output || "",
|
|
230
|
+
escalationReason: `Environment limitation: ${(lastResult && lastResult.errorMessage) || "Unable to run tests in current environment"}`,
|
|
231
|
+
output: (lastResult && lastResult.output) || "",
|
|
201
232
|
duration: Date.now() - startTime,
|
|
202
233
|
};
|
|
203
234
|
}
|
|
@@ -212,7 +243,7 @@ async function runTestLoop(
|
|
|
212
243
|
retriesAttempted,
|
|
213
244
|
escalatedToHuman: true,
|
|
214
245
|
escalationReason: `Failed after ${cfg.maxRetries} attempts. Last error: ${lastResult?.errorMessage ?? "Unknown"}`,
|
|
215
|
-
output: lastResult && lastResult.output || "",
|
|
246
|
+
output: (lastResult && lastResult.output) || "",
|
|
216
247
|
duration: Date.now() - startTime,
|
|
217
248
|
};
|
|
218
249
|
}
|
|
@@ -237,7 +268,7 @@ async function runTestLoop(
|
|
|
237
268
|
async function runTests(
|
|
238
269
|
projectRoot: string,
|
|
239
270
|
toolsConfig: E2EToolsConfig,
|
|
240
|
-
): Promise<
|
|
271
|
+
): Promise<E2ETestResult> {
|
|
241
272
|
const startTime = Date.now();
|
|
242
273
|
const projectType = toolsConfig.projectType;
|
|
243
274
|
|
|
@@ -264,7 +295,8 @@ async function runTests(
|
|
|
264
295
|
output: "",
|
|
265
296
|
duration: 0,
|
|
266
297
|
failedTests: [],
|
|
267
|
-
errorMessage:
|
|
298
|
+
errorMessage:
|
|
299
|
+
"Native app testing requires manual setup. Run tests manually.",
|
|
268
300
|
};
|
|
269
301
|
|
|
270
302
|
case "flutter":
|
|
@@ -339,15 +371,19 @@ interface FailureAnalysis {
|
|
|
339
371
|
*/
|
|
340
372
|
function analyzeFailure(
|
|
341
373
|
result: E2ETestResult,
|
|
342
|
-
|
|
343
|
-
|
|
374
|
+
_projectRoot: string,
|
|
375
|
+
_projectType: ProjectType,
|
|
344
376
|
): FailureAnalysis {
|
|
345
377
|
const failedTests = result.failedTests || [];
|
|
346
378
|
|
|
347
379
|
// Check for common auto-fixable issues
|
|
348
380
|
|
|
349
381
|
// 1. Missing test file - can generate
|
|
350
|
-
if (
|
|
382
|
+
if (
|
|
383
|
+
failedTests.some(
|
|
384
|
+
(t: string) => t.includes("Cannot find") || t.includes("not found"),
|
|
385
|
+
)
|
|
386
|
+
) {
|
|
351
387
|
return {
|
|
352
388
|
canFix: true,
|
|
353
389
|
reason: "Missing test file - can generate stubs",
|
|
@@ -358,7 +394,10 @@ function analyzeFailure(
|
|
|
358
394
|
}
|
|
359
395
|
|
|
360
396
|
// 2. Import errors - can fix
|
|
361
|
-
if (
|
|
397
|
+
if (
|
|
398
|
+
result.output.includes("Cannot find module") ||
|
|
399
|
+
result.output.includes("import error")
|
|
400
|
+
) {
|
|
362
401
|
return {
|
|
363
402
|
canFix: true,
|
|
364
403
|
reason: "Import error - may need npm install",
|
|
@@ -369,7 +408,11 @@ function analyzeFailure(
|
|
|
369
408
|
}
|
|
370
409
|
|
|
371
410
|
// 3. Locator errors - can fix (re-generate selectors)
|
|
372
|
-
if (
|
|
411
|
+
if (
|
|
412
|
+
failedTests.some(
|
|
413
|
+
(t: string) => t.includes("locator") || t.includes("selector"),
|
|
414
|
+
)
|
|
415
|
+
) {
|
|
373
416
|
return {
|
|
374
417
|
canFix: true,
|
|
375
418
|
reason: "UI locator changed - need to update selectors",
|
|
@@ -380,7 +423,12 @@ function analyzeFailure(
|
|
|
380
423
|
}
|
|
381
424
|
|
|
382
425
|
// 4. Auth/session issues - might be environment
|
|
383
|
-
if (
|
|
426
|
+
if (
|
|
427
|
+
failedTests.some(
|
|
428
|
+
(t: string) =>
|
|
429
|
+
t.includes("auth") || t.includes("login") || t.includes("session"),
|
|
430
|
+
)
|
|
431
|
+
) {
|
|
384
432
|
return {
|
|
385
433
|
canFix: false,
|
|
386
434
|
reason: "Authentication issue - may need test credentials",
|
|
@@ -438,7 +486,10 @@ async function attemptAutoFix(
|
|
|
438
486
|
}
|
|
439
487
|
|
|
440
488
|
// Fix 2: Run npm install
|
|
441
|
-
if (
|
|
489
|
+
if (
|
|
490
|
+
analysis.reason.includes("import error") ||
|
|
491
|
+
analysis.reason.includes("Missing dependencies")
|
|
492
|
+
) {
|
|
442
493
|
try {
|
|
443
494
|
execSync("npm install", { cwd: projectRoot, stdio: "ignore" });
|
|
444
495
|
return { fixed: true, description: "Ran npm install" };
|
|
@@ -448,7 +499,10 @@ async function attemptAutoFix(
|
|
|
448
499
|
}
|
|
449
500
|
|
|
450
501
|
// Fix 3: Start dev server (for web apps)
|
|
451
|
-
if (
|
|
502
|
+
if (
|
|
503
|
+
analysis.reason.includes("Timeout") ||
|
|
504
|
+
analysis.reason.includes("Server slow")
|
|
505
|
+
) {
|
|
452
506
|
try {
|
|
453
507
|
// Check if dev server is already running
|
|
454
508
|
const port = 3000;
|
|
@@ -498,7 +552,9 @@ function isEnvironmentLimitation(result: E2ETestResult): boolean {
|
|
|
498
552
|
"ETIMEDOUT",
|
|
499
553
|
];
|
|
500
554
|
|
|
501
|
-
return envIssues.some((issue) =>
|
|
555
|
+
return envIssues.some((issue) =>
|
|
556
|
+
output.toLowerCase().includes(issue.toLowerCase()),
|
|
557
|
+
);
|
|
502
558
|
}
|
|
503
559
|
|
|
504
560
|
/**
|
|
@@ -630,7 +686,10 @@ function findSourceFiles(srcDir: string, projectType: ProjectType): string[] {
|
|
|
630
686
|
generic: [".ts", ".tsx", ".js", ".jsx"],
|
|
631
687
|
};
|
|
632
688
|
|
|
633
|
-
const exts = extensions[projectType as keyof typeof extensions] || [
|
|
689
|
+
const exts = extensions[projectType as keyof typeof extensions] || [
|
|
690
|
+
".ts",
|
|
691
|
+
".tsx",
|
|
692
|
+
];
|
|
634
693
|
const files: string[] = [];
|
|
635
694
|
|
|
636
695
|
try {
|
|
@@ -666,13 +725,19 @@ function createTestFilePath(
|
|
|
666
725
|
generic: ".test.ts",
|
|
667
726
|
};
|
|
668
727
|
|
|
669
|
-
return join(
|
|
728
|
+
return join(
|
|
729
|
+
testDir,
|
|
730
|
+
`${base}${ext[projectType as keyof typeof ext] || ".test.ts"}`,
|
|
731
|
+
);
|
|
670
732
|
}
|
|
671
733
|
|
|
672
734
|
/**
|
|
673
735
|
* Generate basic test content
|
|
674
736
|
*/
|
|
675
|
-
function generateTestContent(
|
|
737
|
+
function generateTestContent(
|
|
738
|
+
sourceFile: string,
|
|
739
|
+
projectType: ProjectType,
|
|
740
|
+
): string {
|
|
676
741
|
const base = require("path").basename(sourceFile, extname(sourceFile));
|
|
677
742
|
|
|
678
743
|
switch (projectType) {
|
|
@@ -27,8 +27,18 @@ import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
|
27
27
|
import { join, extname } from "node:path";
|
|
28
28
|
import { homedir } from "node:os";
|
|
29
29
|
|
|
30
|
-
export type NativeAppType =
|
|
31
|
-
|
|
30
|
+
export type NativeAppType =
|
|
31
|
+
| "tauri"
|
|
32
|
+
| "ios"
|
|
33
|
+
| "android"
|
|
34
|
+
| "electron"
|
|
35
|
+
| "flutter";
|
|
36
|
+
export type WebFramework =
|
|
37
|
+
| "nextjs"
|
|
38
|
+
| "react_vite"
|
|
39
|
+
| "django"
|
|
40
|
+
| "laravel"
|
|
41
|
+
| "generic";
|
|
32
42
|
export type ProjectType = NativeAppType | WebFramework | "unknown";
|
|
33
43
|
|
|
34
44
|
export interface TestingTool {
|
|
@@ -195,11 +205,7 @@ const TESTING_TOOLS_MAP: Record<
|
|
|
195
205
|
manualTools: [],
|
|
196
206
|
},
|
|
197
207
|
flutter: {
|
|
198
|
-
detectionSignals: [
|
|
199
|
-
"pubspec.yaml",
|
|
200
|
-
"lib/main.dart",
|
|
201
|
-
"test/*.dart",
|
|
202
|
-
],
|
|
208
|
+
detectionSignals: ["pubspec.yaml", "lib/main.dart", "test/*.dart"],
|
|
203
209
|
autoTools: [
|
|
204
210
|
{
|
|
205
211
|
name: "Flutter Test",
|
|
@@ -411,10 +417,7 @@ export function detectProjectType(projectRoot: string): ProjectType {
|
|
|
411
417
|
}
|
|
412
418
|
|
|
413
419
|
// Check for Django
|
|
414
|
-
if (
|
|
415
|
-
hasFile(projectRoot, "manage.py") ||
|
|
416
|
-
hasFile(projectRoot, "settings.py")
|
|
417
|
-
) {
|
|
420
|
+
if (hasFile(projectRoot, "manage.py") || hasFile(projectRoot, "settings.py")) {
|
|
418
421
|
return "django";
|
|
419
422
|
}
|
|
420
423
|
|
|
@@ -583,16 +586,15 @@ export function makeSmartDecision(
|
|
|
583
586
|
if (["django", "laravel"].includes(projectType)) {
|
|
584
587
|
const hasFrameworkTest =
|
|
585
588
|
installedAutoTools.some(
|
|
586
|
-
(t) =>
|
|
587
|
-
|
|
588
|
-
) ||
|
|
589
|
-
installedAutoTools.some((t) => t.name.includes("Playwright"));
|
|
589
|
+
(t) => t.name.includes("Django") || t.name.includes("Laravel Dusk"),
|
|
590
|
+
) || installedAutoTools.some((t) => t.name.includes("Playwright"));
|
|
590
591
|
|
|
591
592
|
if (!hasFrameworkTest) {
|
|
592
593
|
return {
|
|
593
594
|
shouldAutoTest: false,
|
|
594
595
|
confidence: 0.3,
|
|
595
|
-
reason:
|
|
596
|
+
reason:
|
|
597
|
+
"Backend framework detected. Install framework-specific test tools.",
|
|
596
598
|
suggestedAction: "ask_human",
|
|
597
599
|
};
|
|
598
600
|
}
|
|
@@ -609,7 +611,8 @@ export function makeSmartDecision(
|
|
|
609
611
|
return {
|
|
610
612
|
shouldAutoTest: false,
|
|
611
613
|
confidence: 0.5,
|
|
612
|
-
reason:
|
|
614
|
+
reason:
|
|
615
|
+
"Backend project detected. Auto-tests skipped (manual review needed).",
|
|
613
616
|
suggestedAction: "skip",
|
|
614
617
|
};
|
|
615
618
|
}
|
package/harness/index.ts
CHANGED
|
@@ -278,11 +278,28 @@ export type {
|
|
|
278
278
|
ProjectType,
|
|
279
279
|
NativeAppType,
|
|
280
280
|
} from "./e2e/tools-detector.js";
|
|
281
|
-
export {
|
|
282
|
-
runAutoTestLoop,
|
|
283
|
-
} from "./e2e/auto-test-loop.js";
|
|
281
|
+
export { runAutoTestLoop } from "./e2e/auto-test-loop.js";
|
|
284
282
|
export type {
|
|
285
283
|
E2ETestResult,
|
|
286
284
|
AutoTestConfig,
|
|
287
285
|
AutoTestResult,
|
|
288
286
|
} from "./e2e/auto-test-loop.js";
|
|
287
|
+
|
|
288
|
+
// --- Skill SaaS Sync (RFC-0106) ----------------------------------------
|
|
289
|
+
export {
|
|
290
|
+
syncSkillsFromSaaS,
|
|
291
|
+
getSkillFromSaaS,
|
|
292
|
+
isSkillSyncConfigured,
|
|
293
|
+
getSyncStatus,
|
|
294
|
+
} from "./skill-sync.js";
|
|
295
|
+
|
|
296
|
+
// --- SSH Hang Recovery (auto-recovery from SSH hangs) -------------------
|
|
297
|
+
export {
|
|
298
|
+
sshWithRecovery,
|
|
299
|
+
pm2Restart,
|
|
300
|
+
getPM2Status,
|
|
301
|
+
safeSSHCommand,
|
|
302
|
+
pingServer,
|
|
303
|
+
type SSHConfig,
|
|
304
|
+
type SSHResult,
|
|
305
|
+
} from "./ssh-hang-recovery.js";
|
package/harness/loop-runtime.ts
CHANGED
|
@@ -46,6 +46,9 @@ import {
|
|
|
46
46
|
createForkedSummarizer,
|
|
47
47
|
} from "./forked-summarizer.js";
|
|
48
48
|
|
|
49
|
+
// GLM quota error parsing for 429 responses
|
|
50
|
+
import { parseGLMErrorResetTime } from "./e2e/glm-quota-scraper.js";
|
|
51
|
+
|
|
49
52
|
// --- Constants ----------------------------------------------------------------
|
|
50
53
|
|
|
51
54
|
const DEFAULT_MAX_COMPACT_RETRIES = 3;
|
|
@@ -84,9 +87,7 @@ export interface LoopCallbacks {
|
|
|
84
87
|
/** Get current checkpoint (used by auto-resume to read resumeAt) */
|
|
85
88
|
onGetCheckpoint?: () => Promise<RuntimeCheckpoint | null>;
|
|
86
89
|
/** Check mirror for a provider (used by auto-resume to detect early reset) */
|
|
87
|
-
onCheckMirror?: (
|
|
88
|
-
provider: string,
|
|
89
|
-
) => Promise<{ h5_used_pct?: number } | null>;
|
|
90
|
+
onCheckMirror?: (provider: string) => Promise<{ h5_used_pct?: number } | null>;
|
|
90
91
|
/** Generic notify (used by auto-resume to tell user wait time) */
|
|
91
92
|
onNotify?: (msg: string, type?: string) => Promise<void>;
|
|
92
93
|
/** Save checkpoint (call persistence layer) */
|
|
@@ -244,11 +245,7 @@ export class LoopRuntime {
|
|
|
244
245
|
await new Promise((r) => setTimeout(r, 5 * 60_000)); // 5 min
|
|
245
246
|
// Re-check mirror — quota may have reset early
|
|
246
247
|
const fresh = await this.callbacks.onCheckMirror?.("minimax");
|
|
247
|
-
if (
|
|
248
|
-
fresh &&
|
|
249
|
-
fresh.h5_used_pct !== undefined &&
|
|
250
|
-
fresh.h5_used_pct < 100
|
|
251
|
-
) {
|
|
248
|
+
if (fresh && fresh.h5_used_pct !== undefined && fresh.h5_used_pct < 100) {
|
|
252
249
|
break; // quota reset early, resume now
|
|
253
250
|
}
|
|
254
251
|
}
|
|
@@ -447,14 +444,54 @@ export class LoopRuntime {
|
|
|
447
444
|
continue;
|
|
448
445
|
}
|
|
449
446
|
|
|
450
|
-
// Error without compact —
|
|
451
|
-
|
|
447
|
+
// Error without compact — check for GLM 429 quota error
|
|
448
|
+
const errMsg = result.error ?? "Unknown invoke error";
|
|
449
|
+
if (errMsg.includes("1308") || errMsg.includes("Usage limit")) {
|
|
450
|
+
const resetTime = parseGLMErrorResetTime(errMsg);
|
|
451
|
+
if (resetTime) {
|
|
452
|
+
const resetEpoch = new Date(resetTime).getTime();
|
|
453
|
+
this.mirrorStore.writeProvider("minimax", {
|
|
454
|
+
provider: "minimax",
|
|
455
|
+
synced_at: new Date().toISOString(),
|
|
456
|
+
source: "tui-signal",
|
|
457
|
+
h5_used_pct: 100,
|
|
458
|
+
h5_resets_at_epoch: resetEpoch,
|
|
459
|
+
});
|
|
460
|
+
console.log(`[LoopRuntime] GLM quota hit, reset at ${resetTime}`);
|
|
461
|
+
}
|
|
462
|
+
await this.handleQuotaPause();
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
throw new Error(errMsg);
|
|
452
466
|
}
|
|
453
467
|
|
|
454
468
|
// Fallback: direct invoke without orchestrator
|
|
455
469
|
const result = await this.callbacks.onInvokeAgent?.(invokeOptions);
|
|
456
470
|
if (!result || !result.success) {
|
|
457
|
-
|
|
471
|
+
const errorMsg = result?.error ?? "Invoke failed";
|
|
472
|
+
|
|
473
|
+
// Check if this is a GLM 429 quota error with reset time
|
|
474
|
+
if (errorMsg.includes("1308") || errorMsg.includes("Usage limit")) {
|
|
475
|
+
const resetTime = parseGLMErrorResetTime(errorMsg);
|
|
476
|
+
if (resetTime) {
|
|
477
|
+
// Update mirror with reset epoch so auto-resume works
|
|
478
|
+
const resetEpoch = new Date(resetTime).getTime();
|
|
479
|
+
this.mirrorStore.writeProvider("minimax", {
|
|
480
|
+
provider: "minimax",
|
|
481
|
+
synced_at: new Date().toISOString(),
|
|
482
|
+
source: "tui-signal",
|
|
483
|
+
h5_used_pct: 100,
|
|
484
|
+
h5_resets_at_epoch: resetEpoch,
|
|
485
|
+
});
|
|
486
|
+
console.log(`[LoopRuntime] GLM quota hit, reset at ${resetTime}`);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Handle quota pause instead of throwing
|
|
490
|
+
await this.handleQuotaPause();
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
throw new Error(errorMsg);
|
|
458
495
|
}
|
|
459
496
|
|
|
460
497
|
const continued = await this.handleAutoContinuation(
|
|
@@ -660,8 +697,7 @@ export class LoopRuntime {
|
|
|
660
697
|
// Append repair instruction
|
|
661
698
|
messages.push({
|
|
662
699
|
role: "user",
|
|
663
|
-
content:
|
|
664
|
-
"Tests failed. Please fix the issues and ensure all tests pass.",
|
|
700
|
+
content: "Tests failed. Please fix the issues and ensure all tests pass.",
|
|
665
701
|
timestamp: Date.now(),
|
|
666
702
|
});
|
|
667
703
|
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill Sync Tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// @ts-ignore - bun:test types
|
|
6
|
+
import { describe, test, expect, beforeEach, afterEach } from "bun:test";
|
|
7
|
+
import { tmpdir } from "node:os";
|
|
8
|
+
import { mkdirSync, rmSync } from "node:fs";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
|
|
11
|
+
// Mock the environment
|
|
12
|
+
const originalApiKey = process.env.SKILLS_SAAS_API_KEY;
|
|
13
|
+
|
|
14
|
+
describe("Skill Sync", () => {
|
|
15
|
+
let tempCacheDir: string;
|
|
16
|
+
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
tempCacheDir = join(tmpdir(), `skill-sync-test-${Date.now()}`);
|
|
19
|
+
mkdirSync(tempCacheDir, { recursive: true });
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
// Restore env
|
|
24
|
+
if (originalApiKey === undefined) {
|
|
25
|
+
delete process.env.SKILLS_SAAS_API_KEY;
|
|
26
|
+
} else {
|
|
27
|
+
process.env.SKILLS_SAAS_API_KEY = originalApiKey;
|
|
28
|
+
}
|
|
29
|
+
// Cleanup
|
|
30
|
+
try {
|
|
31
|
+
rmSync(tempCacheDir, { recursive: true });
|
|
32
|
+
} catch {
|
|
33
|
+
// Ignore
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe("isSkillSyncConfigured", () => {
|
|
38
|
+
test("returns false when API key not set", () => {
|
|
39
|
+
delete process.env.SKILLS_SAAS_API_KEY;
|
|
40
|
+
// Import dynamically to test
|
|
41
|
+
expect(process.env.SKILLS_SAAS_API_KEY).toBeUndefined();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("returns true when API key is set", () => {
|
|
45
|
+
process.env.SKILLS_SAAS_API_KEY = "sk_test_xxx";
|
|
46
|
+
expect(process.env.SKILLS_SAAS_API_KEY).toBe("sk_test_xxx");
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe("getSyncStatus", () => {
|
|
51
|
+
test("returns correct status when not configured", () => {
|
|
52
|
+
delete process.env.SKILLS_SAAS_API_KEY;
|
|
53
|
+
// Status should show not configured
|
|
54
|
+
expect(process.env.SKILLS_SAAS_API_KEY).toBeUndefined();
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
});
|