Unverified 提交 98de516b authored 作者: keppo-bot[bot]'s avatar keppo-bot[bot] 提交者: GitHub

Surface shared Supabase deploy output in local agent (#3288)

## Summary - Append shared Supabase deploy warnings and errors as inline dyad-output cards in Local Agent mode. - Keep post-turn commits non-fatal when shared edge function redeploys fail, matching single function deploy behavior. - Refresh formatting/lint for eval fixtures touched by pre-commit checks. ## Test plan - npm run fmt && npm run lint:fix && npm run ts - npm test 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- devin-review-badge-begin --> --- <a href="https://app.devin.ai/review/dyad-sh/dyad/pull/3288" target="_blank"> <picture> <source media="(prefers-color-scheme: dark)" srcset="https://static.devin.ai/assets/gh-open-in-devin-review-dark.svg?v=1"> <img src="https://static.devin.ai/assets/gh-open-in-devin-review-light.svg?v=1" alt="Open in Devin Review"> </picture> </a> <!-- devin-review-badge-end --> --------- Co-authored-by: 's avatarWill Chen <7344640+wwwillchen@users.noreply.github.com>
上级 158b1bcb
...@@ -135,6 +135,7 @@ The stashed changes will be automatically merged back after the rebase completes ...@@ -135,6 +135,7 @@ The stashed changes will be automatically merged back after the rebase completes
### Conflict resolution tips ### Conflict resolution tips
- **Modify/delete conflicts**: When a rebase shows `CONFLICT (modify/delete): <file> deleted in <commit> and modified in HEAD`, use `git rm <file>` (not `git add`) to resolve by confirming the deletion. Use `git add <file>` only when you want to keep the modified version instead. - **Modify/delete conflicts**: When a rebase shows `CONFLICT (modify/delete): <file> deleted in <commit> and modified in HEAD`, use `git rm <file>` (not `git add`) to resolve by confirming the deletion. Use `git add <file>` only when you want to keep the modified version instead.
- **Non-interactive rebase continue**: After resolving conflicts, prefer `GIT_EDITOR=true git rebase --continue` in agent shells. Plain `git rebase --continue` can open `vi` for `COMMIT_EDITMSG` and fail with `error: vi died of signal 15` when stdin is not interactive.
- **Before rebasing:** If `npm install` modified `package-lock.json` (common in CI/local), discard changes with `git restore package-lock.json` to avoid "unstaged changes" errors - **Before rebasing:** If `npm install` modified `package-lock.json` (common in CI/local), discard changes with `git restore package-lock.json` to avoid "unstaged changes" errors
- When resolving import conflicts (e.g., `<<<<<<< HEAD` with different imports), keep **both** imports if both are valid and needed by the component - When resolving import conflicts (e.g., `<<<<<<< HEAD` with different imports), keep **both** imports if both are valid and needed by the component
- When resolving conflicts in i18n-related commits, watch for duplicate constant definitions that conflict with imports from `@/lib/schemas` (e.g., `DEFAULT_ZOOM_LEVEL`) - When resolving conflicts in i18n-related commits, watch for duplicate constant definitions that conflict with imports from `@/lib/schemas` (e.g., `DEFAULT_ZOOM_LEVEL`)
......
...@@ -11,6 +11,10 @@ Agent tool definitions live in `src/pro/main/ipc/handlers/local_agent/tools/`. E ...@@ -11,6 +11,10 @@ Agent tool definitions live in `src/pro/main/ipc/handlers/local_agent/tools/`. E
- Use `fs.promises` (not sync `fs` methods) in any code running on the Electron main process (e.g., `todo_persistence.ts`) to avoid blocking the event loop. - Use `fs.promises` (not sync `fs` methods) in any code running on the Electron main process (e.g., `todo_persistence.ts`) to avoid blocking the event loop.
## User-visible tool output
- For Local Agent post-tool side effects that happen after the model/tool loop (for example shared Supabase function redeploys), use `ctx.onXmlComplete(...)` with escaped `<dyad-output>` content to surface warnings/errors inline. `warningMessages` creates toast warnings, and throwing turns the whole stream into a `ChatErrorBox`.
## Stream retries ## Stream retries
- When extending `handleLocalAgentStream` retry behavior, do not only match transport errors like `"terminated"`. Providers can emit structured stream errors such as `{ type: "error", error: { type: "server_error", ... } }`, and those transient 5xx / rate-limit failures need explicit retry classification too. - When extending `handleLocalAgentStream` retry behavior, do not only match transport errors like `"terminated"`. Providers can emit structured stream errors such as `{ type: "error", error: { type: "server_error", ... } }`, and those transient 5xx / rate-limit failures need explicit retry classification too.
......
// fetch_client.ts — authenticated fetch wrapper used by all service layers // fetch_client.ts — authenticated fetch wrapper used by all service layers
import { createLogger } from "./logger";
import { getAuthToken } from "./auth"; import { getAuthToken } from "./auth";
const logger = createLogger("fetch-client");
const BASE_URL = const BASE_URL =
process.env.SERVICE_BASE_URL ?? "https://api.internal.example.com"; process.env.SERVICE_BASE_URL ?? "https://api.internal.example.com";
const DEFAULT_TIMEOUT_MS = 8_000; const DEFAULT_TIMEOUT_MS = 8_000;
......
...@@ -16,12 +16,6 @@ interface ProcessResult { ...@@ -16,12 +16,6 @@ interface ProcessResult {
error?: string; error?: string;
} }
interface FulfillmentResult {
transactionId: string;
trackingNumber: string;
estimatedDelivery: string;
}
interface RefundResult { interface RefundResult {
refundId: string; refundId: string;
amount: number; amount: number;
...@@ -30,29 +24,29 @@ interface RefundResult { ...@@ -30,29 +24,29 @@ interface RefundResult {
// ── External service stubs ───────────────────────────────────────────────── // ── External service stubs ─────────────────────────────────────────────────
async function getInventory(productId: string): Promise<InventoryItem | null> { async function getInventory(_productId: string): Promise<InventoryItem | null> {
// Implementation elided — hits the warehouse API // Implementation elided — hits the warehouse API
return null; return null;
} }
async function reserveInventory( async function reserveInventory(
productId: string, _productId: string,
quantity: number, _quantity: number,
): Promise<boolean> { ): Promise<boolean> {
// Implementation elided — locks inventory for the order // Implementation elided — locks inventory for the order
return true; return true;
} }
async function releaseInventory( async function releaseInventory(
productId: string, _productId: string,
quantity: number, _quantity: number,
): Promise<void> { ): Promise<void> {
// Implementation elided — releases a previously-reserved hold // Implementation elided — releases a previously-reserved hold
} }
async function chargePayment( async function chargePayment(
method: PaymentMethod, _method: PaymentMethod,
amount: number, _amount: number,
): Promise<{ transactionId: string }> { ): Promise<{ transactionId: string }> {
// Implementation elided — hits the payment gateway // Implementation elided — hits the payment gateway
return { transactionId: "txn_placeholder" }; return { transactionId: "txn_placeholder" };
...@@ -67,8 +61,8 @@ async function refundPayment( ...@@ -67,8 +61,8 @@ async function refundPayment(
} }
async function createShipment( async function createShipment(
address: ShippingAddress, _address: ShippingAddress,
items: string[], _items: string[],
): Promise<{ trackingNumber: string; estimatedDelivery: string }> { ): Promise<{ trackingNumber: string; estimatedDelivery: string }> {
// Implementation elided — hits the shipping API // Implementation elided — hits the shipping API
return { return {
...@@ -79,25 +73,25 @@ async function createShipment( ...@@ -79,25 +73,25 @@ async function createShipment(
}; };
} }
async function cancelShipment(trackingNumber: string): Promise<void> { async function cancelShipment(_trackingNumber: string): Promise<void> {
// Implementation elided — cancels a shipment before it ships // Implementation elided — cancels a shipment before it ships
} }
async function saveOrder(order: Order): Promise<string> { async function saveOrder(_order: Order): Promise<string> {
// Implementation elided — writes to DB // Implementation elided — writes to DB
return "order_placeholder"; return "order_placeholder";
} }
async function updateOrderStatus( async function updateOrderStatus(
orderId: string, _orderId: string,
status: string, _status: string,
): Promise<void> { ): Promise<void> {
// Implementation elided — updates DB record // Implementation elided — updates DB record
} }
async function notifyOrderConfirmed( async function notifyOrderConfirmed(
orderId: string, _orderId: string,
email: string, _email: string,
): Promise<void> { ): Promise<void> {
// Implementation elided — sends confirmation email // Implementation elided — sends confirmation email
} }
...@@ -270,7 +264,7 @@ export async function getOrderStatus(orderId: string): Promise<string | null> { ...@@ -270,7 +264,7 @@ export async function getOrderStatus(orderId: string): Promise<string | null> {
export async function listOrdersForCustomer( export async function listOrdersForCustomer(
customerEmail: string, customerEmail: string,
page: number, page: number,
limit: number, _limit: number,
): Promise<Order[]> { ): Promise<Order[]> {
logger.info(`Listing orders for customer ${customerEmail} (page=${page})`); logger.info(`Listing orders for customer ${customerEmail} (page=${page})`);
// Implementation elided — queries DB // Implementation elided — queries DB
......
...@@ -19,15 +19,6 @@ interface User { ...@@ -19,15 +19,6 @@ interface User {
isActive: boolean; isActive: boolean;
} }
interface CreateUserBody {
email: string;
name: string;
age: number;
role: UserRole;
bio?: string;
avatarUrl?: string;
}
interface UpdateUserBody { interface UpdateUserBody {
name?: string; name?: string;
age?: number; age?: number;
......
...@@ -276,7 +276,7 @@ vi.mock("@/pro/main/ipc/handlers/local_agent/tool_definitions", () => ({ ...@@ -276,7 +276,7 @@ vi.mock("@/pro/main/ipc/handlers/local_agent/tool_definitions", () => ({
vi.mock( vi.mock(
"@/pro/main/ipc/handlers/local_agent/processors/file_operations", "@/pro/main/ipc/handlers/local_agent/processors/file_operations",
() => ({ () => ({
deployAllFunctionsIfNeeded: vi.fn(async () => {}), deployAllFunctionsIfNeeded: vi.fn(async () => ({ success: true })),
commitAllChanges: vi.fn(async () => ({ commitHash: "abc123" })), commitAllChanges: vi.fn(async () => ({ commitHash: "abc123" })),
}), }),
); );
...@@ -304,6 +304,10 @@ vi.mock("@/ipc/handlers/compaction/compaction_handler", () => ({ ...@@ -304,6 +304,10 @@ vi.mock("@/ipc/handlers/compaction/compaction_handler", () => ({
import { handleLocalAgentStream } from "@/pro/main/ipc/handlers/local_agent/local_agent_handler"; import { handleLocalAgentStream } from "@/pro/main/ipc/handlers/local_agent/local_agent_handler";
import { DyadError, DyadErrorKind } from "@/errors/dyad_error"; import { DyadError, DyadErrorKind } from "@/errors/dyad_error";
import { buildAgentToolSet } from "@/pro/main/ipc/handlers/local_agent/tool_definitions"; import { buildAgentToolSet } from "@/pro/main/ipc/handlers/local_agent/tool_definitions";
import {
commitAllChanges,
deployAllFunctionsIfNeeded,
} from "@/pro/main/ipc/handlers/local_agent/processors/file_operations";
// ============================================================================ // ============================================================================
// Tests // Tests
...@@ -468,6 +472,89 @@ describe("handleLocalAgentStream", () => { ...@@ -468,6 +472,89 @@ describe("handleLocalAgentStream", () => {
warningMessages: [warningMessage], warningMessages: [warningMessage],
}); });
}); });
it("appends shared-module Supabase deploy warnings as dyad-output", async () => {
const { event } = createFakeEvent();
mockSettings = buildTestSettings({ enableDyadPro: true });
mockChatData = buildTestChat({
supabaseProjectId: "supabase-project-id",
});
mockStreamResult = createFakeStream([{ type: "text-delta", text: "ok" }]);
vi.mocked(deployAllFunctionsIfNeeded).mockResolvedValueOnce({
success: true,
warning:
"Some Supabase functions failed to deploy: Failed to bundle get-user-role: Rate limited (429): Too Many Requests",
});
await handleLocalAgentStream(
event,
{ chatId: 1, prompt: "test" },
new AbortController(),
{
placeholderMessageId: 10,
systemPrompt: "You are helpful",
dyadRequestId,
},
);
const contentUpdates = dbOperations.updates.filter(
(u) => u.data.content !== undefined,
);
const finalContent = contentUpdates[contentUpdates.length - 1].data
.content as string;
expect(finalContent).toContain('<dyad-output type="warning"');
expect(finalContent).toContain(
'message="Supabase function deploy warning"',
);
expect(finalContent).toContain(
"Some Supabase functions failed to deploy: Failed to bundle get-user-role: Rate limited (429): Too Many Requests",
);
expect(commitAllChanges).toHaveBeenCalled();
});
it("appends shared-module Supabase deploy failures as dyad-output and still commits", async () => {
const { event, getMessagesByChannel } = createFakeEvent();
mockSettings = buildTestSettings({ enableDyadPro: true });
mockChatData = buildTestChat({
supabaseProjectId: "supabase-project-id",
});
mockStreamResult = createFakeStream([{ type: "text-delta", text: "ok" }]);
vi.mocked(deployAllFunctionsIfNeeded).mockResolvedValueOnce({
success: false,
error:
"Failed to redeploy Supabase functions: RateLimitError: Rate limited (429): Too Many Requests",
});
await handleLocalAgentStream(
event,
{ chatId: 1, prompt: "test" },
new AbortController(),
{
placeholderMessageId: 10,
systemPrompt: "You are helpful",
dyadRequestId,
},
);
const errorMessages = getMessagesByChannel("chat:response:error");
expect(errorMessages).toHaveLength(0);
const contentUpdates = dbOperations.updates.filter(
(u) => u.data.content !== undefined,
);
const finalContent = contentUpdates[contentUpdates.length - 1].data
.content as string;
expect(finalContent).toContain('<dyad-output type="error"');
expect(finalContent).toContain(
'message="Failed to deploy Supabase functions"',
);
expect(finalContent).toContain(
"Failed to redeploy Supabase functions: RateLimitError: Rate limited (429): Too Many Requests",
);
expect(commitAllChanges).toHaveBeenCalled();
});
}); });
describe("Context compaction setting", () => { describe("Context compaction setting", () => {
......
...@@ -1358,7 +1358,17 @@ export async function handleLocalAgentStream( ...@@ -1358,7 +1358,17 @@ export async function handleLocalAgentStream(
// In read-only and plan mode, skip deploys and commits // In read-only and plan mode, skip deploys and commits
if (!readOnly && !planModeOnly) { if (!readOnly && !planModeOnly) {
// Deploy all Supabase functions if shared modules changed // Deploy all Supabase functions if shared modules changed
await deployAllFunctionsIfNeeded(ctx); const deployResult = await deployAllFunctionsIfNeeded(ctx);
if (deployResult.warning) {
ctx.onXmlComplete(
`<dyad-output type="warning" message="${escapeXmlAttr("Supabase function deploy warning")}">${escapeXmlContent(deployResult.warning)}</dyad-output>`,
);
}
if (!deployResult.success) {
ctx.onXmlComplete(
`<dyad-output type="error" message="${escapeXmlAttr("Failed to deploy Supabase functions")}">${escapeXmlContent(deployResult.error ?? "Unknown deploy error")}</dyad-output>`,
);
}
// Commit all changes // Commit all changes
const commitResult = await commitAllChanges(ctx, ctx.chatSummary); const commitResult = await commitAllChanges(ctx, ctx.chatSummary);
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论