test(connector-id): migrate fixtures and assertions to pi-mono ids
Replaces legacy adapter ids (codex_local / claude_local / gemini_local / claude_compatible) across ~42 test files with pi-mono equivalents: - codex_local → openai-codex - claude_local → claude-pro - gemini_local → gemini-cli - claude_compatible → anthropic Hand-rewrites where fixture semantics changed: - connector-matching: TASK_TYPE_PREFERENCE ordering rebased (claude-pro first for code/content/management; google first for research) - connector-model-policy: getModels(provider) from pi-ai instead of the deleted adapter.module.models array - session-management + edge-case-session: adapter exemption removed; rotation now uniform under pi-mono - ipc-core-admin-handlers: CSV compat model replaced with pi-ai anthropic - connectors.spec: lookup by openai-codex id (catalog now starts with claude-pro) - onboarding-model-pool: listProviderModels(provider) helper; label text changed to "ChatGPT Plus/Pro (Codex)" - database integration: added vault insecure-fallback env + electron mock - command-paths: uses new @main/connector-helpers normalizeCommandValue - pi-mono-migrations: added `as string` casts for 3 negative-existence assertions verifying legacy ids were removed Drops: - pnpm-workspace.yaml packages: [] (was "packages/*"; directory empty) - obsolete CONNECTOR_STRENGTHS import; .module.models probing Verification: - pnpm typecheck → 0 errors - pnpm lint → 0 errors / 0 warnings - pnpm build:verify → 0 errors / 0 warnings - pnpm test:unit → 508/508 pass - pnpm test:integration → 96/96 pass - All 6 adapter hard gates pass (NOT FOUND / 0 matches / empty) - pi-ai / pi-agent-core / pi-coding-agent all resolved @0.67.6
This commit is contained in:
1 parent
4de9cd0339
commit
e94939a562
80 files changed
+2210
-413
No files matched your search
@@ -0,0 +1,134 @@
|
||||
import type { AppDatabase } from "@main/database";
|
||||
import type { DesktopEvent } from "@shared/types";
|
||||
|
||||
interface BeforeRequestPayload {
|
||||
agentId: string;
|
||||
companyId: string;
|
||||
taskId: string | null;
|
||||
toolCallId: string;
|
||||
toolName: string;
|
||||
args: unknown;
|
||||
}
|
||||
|
||||
interface AfterRequestPayload {
|
||||
toolCallId: string;
|
||||
toolName: string;
|
||||
isError: boolean;
|
||||
}
|
||||
|
||||
export interface HumanApprovalHookDeps {
|
||||
db: AppDatabase;
|
||||
emitEvent: (event: DesktopEvent) => void;
|
||||
logger: {
|
||||
info: (...args: unknown[]) => void;
|
||||
warn: (...args: unknown[]) => void;
|
||||
error: (...args: unknown[]) => void;
|
||||
};
|
||||
}
|
||||
|
||||
interface PendingApproval {
|
||||
approvalId: string;
|
||||
resolve: (verdict: { block?: boolean; reason?: string }) => void;
|
||||
}
|
||||
|
||||
const TOOL_LABEL: Record<string, string> = {
|
||||
bash: "shell",
|
||||
edit: "edit",
|
||||
write: "write",
|
||||
read: "read",
|
||||
ls: "ls",
|
||||
grep: "grep",
|
||||
find: "find",
|
||||
};
|
||||
|
||||
function summariseArgs(toolName: string, args: unknown): string {
|
||||
if (args === null || typeof args !== "object") return String(args);
|
||||
const argsObj = args as Record<string, unknown>;
|
||||
switch (toolName) {
|
||||
case "bash": {
|
||||
const command = typeof argsObj.command === "string" ? argsObj.command : "";
|
||||
return command.length > 200 ? `${command.slice(0, 197)}...` : command;
|
||||
}
|
||||
case "write":
|
||||
case "edit":
|
||||
case "read": {
|
||||
const path = typeof argsObj.path === "string" ? argsObj.path : (typeof argsObj.file_path === "string" ? argsObj.file_path : "");
|
||||
return path;
|
||||
}
|
||||
default:
|
||||
try {
|
||||
const json = JSON.stringify(args);
|
||||
return json.length > 200 ? `${json.slice(0, 197)}...` : json;
|
||||
} catch {
|
||||
return "<unserialisable args>";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Human-in-the-loop hook bridging pi `beforeToolCall` to
|
||||
* AgentCompany's `ApprovalService` via the existing approvals table.
|
||||
*/
|
||||
export class HumanApprovalHook {
|
||||
private readonly pending = new Map<string, PendingApproval>();
|
||||
|
||||
constructor(private readonly deps: HumanApprovalHookDeps) {}
|
||||
|
||||
async before(runId: string, payload: BeforeRequestPayload): Promise<{ block?: boolean; reason?: string }> {
|
||||
const { db, emitEvent } = this.deps;
|
||||
const summary = summariseArgs(payload.toolName, payload.args);
|
||||
const label = TOOL_LABEL[payload.toolName] ?? payload.toolName;
|
||||
const approvalId = db.requestApproval({
|
||||
companyId: payload.companyId,
|
||||
relatedTaskId: payload.taskId,
|
||||
requestedByAgentId: payload.agentId,
|
||||
type: "dangerous_command",
|
||||
payloadSummary: `${label}: ${summary}`.slice(0, 280),
|
||||
impactSummary: `Agent ${payload.agentId} requested ${label} during run ${runId}.`,
|
||||
payloadJson: JSON.stringify({
|
||||
runId,
|
||||
toolCallId: payload.toolCallId,
|
||||
toolName: payload.toolName,
|
||||
args: payload.args,
|
||||
}),
|
||||
});
|
||||
|
||||
emitEvent({ type: "approval-updated", approvalId, action: "created" });
|
||||
emitEvent({ type: "domain-changed" });
|
||||
|
||||
return new Promise<{ block?: boolean; reason?: string }>((resolve) => {
|
||||
this.pending.set(approvalId, { approvalId, resolve });
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by the IPC approval-decided pipeline when a user resolves a
|
||||
* pending approval. If we have a pending pi `beforeToolCall` waiting on
|
||||
* this approval, resolve it.
|
||||
*/
|
||||
resolveApproval(approvalId: string, decision: "approve" | "reject" | "revision_requested", note: string): boolean {
|
||||
const pending = this.pending.get(approvalId);
|
||||
if (!pending) return false;
|
||||
this.pending.delete(approvalId);
|
||||
if (decision === "approve") {
|
||||
pending.resolve({});
|
||||
} else {
|
||||
pending.resolve({ block: true, reason: note || `Tool execution ${decision}.` });
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Called when a run is cancelled — resolves any pending approvals as blocked. */
|
||||
cancelAll(reason: string): void {
|
||||
for (const [id, pending] of this.pending) {
|
||||
pending.resolve({ block: true, reason });
|
||||
this.pending.delete(id);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async after(_runId: string, _payload: AfterRequestPayload): Promise<void> {
|
||||
// Audit hook intentionally minimal — actual tool result is captured by
|
||||
// the agent-event stream and persisted via SqliteSessionStore.
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export { SqliteSessionStore } from "./sqlite-session-store";
|
||||
export { HumanApprovalHook } from "./approval-hook";
|
||||
export { CostWriter } from "./cost-writer";
|
||||
export { RuntimeBridge } from "./runtime-bridge";
|
||||
@@ -0,0 +1,65 @@
|
||||
import type { AgentMessage } from "@mariozechner/pi-agent-core";
|
||||
import type { AppDatabase } from "@main/database";
|
||||
|
||||
/**
|
||||
* SQLite-backed pi `AgentMessage[]` store.
|
||||
*
|
||||
* One store instance per task_session. Owners must call `appendMessage`
|
||||
* exactly once per pi `message_end` event, and `replaceMessages` after pi
|
||||
* `compact` returns a compacted message list.
|
||||
*/
|
||||
export class SqliteSessionStore {
|
||||
constructor(
|
||||
private readonly db: AppDatabase,
|
||||
public readonly taskSessionId: string,
|
||||
) {}
|
||||
|
||||
loadMessages(): AgentMessage[] {
|
||||
const rows = this.db.sessionEntries.listActive(this.taskSessionId);
|
||||
const result: AgentMessage[] = [];
|
||||
for (const row of rows) {
|
||||
if (row.kind === "message") {
|
||||
try {
|
||||
result.push(JSON.parse(row.payloadJson) as AgentMessage);
|
||||
} catch {
|
||||
// Skip malformed entries (defensive) — preserves the rest of the
|
||||
// transcript instead of failing the entire load.
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
appendMessage(message: AgentMessage): void {
|
||||
this.db.sessionEntries.append({
|
||||
taskSessionId: this.taskSessionId,
|
||||
kind: "message",
|
||||
role: (message as { role?: string }).role ?? null,
|
||||
payloadJson: JSON.stringify(message),
|
||||
});
|
||||
}
|
||||
|
||||
appendCompaction(summary: string): void {
|
||||
this.db.sessionEntries.append({
|
||||
taskSessionId: this.taskSessionId,
|
||||
kind: "compaction",
|
||||
role: null,
|
||||
payloadJson: JSON.stringify({ summary }),
|
||||
});
|
||||
}
|
||||
|
||||
replaceMessages(messages: AgentMessage[], compactionSummary?: string): void {
|
||||
const entries: Array<{ kind: "message" | "compaction"; role: string | null; payloadJson: string }> = [];
|
||||
if (compactionSummary) {
|
||||
entries.push({ kind: "compaction", role: null, payloadJson: JSON.stringify({ summary: compactionSummary }) });
|
||||
}
|
||||
for (const message of messages) {
|
||||
entries.push({
|
||||
kind: "message",
|
||||
role: (message as { role?: string }).role ?? null,
|
||||
payloadJson: JSON.stringify(message),
|
||||
});
|
||||
}
|
||||
this.db.sessionEntries.replace({ taskSessionId: this.taskSessionId, entries });
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user