mirror of
https://gh-proxy.org/https://github.com/klinsmaya/multica.git
synced 2026-11-08 17:36:45 +08:00
* fix: fail private runtime owner mismatches (PUCK-89) Co-authored-by: multica-agent <[email protected]> * fix: keep runtime mismatch repair off idle claim path * test: cover mixed runtime claim outcomes (PUCK-89) Co-authored-by: multica-agent <[email protected]> * fix: settle runtime owner mismatches during claim * fix: reject ownerless agents on private runtime claims * chore: restore claim version sampling layout (PUCK-89) Co-authored-by: multica-agent <[email protected]> * fix: re-authorize delivery at the finalize boundary, dedicated runtime_access_denied client copy, fixture convention (PUCK-89) Blocker 1 — final delivery gate: FinalizeTaskClaim now runs a caller- supplied authorize closure inside its transaction. The singular and batch claim paths share finalizeClaimDelivery, which re-locks the runtime row (FOR UPDATE), re-reads the agent, and re-verifies the private-runtime owner fence against CURRENT ownership before the task token commits. A concurrent re-registration that would change owner_id blocks until the gate commits, closing the stale-snapshot TOCTOU. Mismatched tasks settle through the existing failClaimedTaskBeforeLaunch -> FailTask path; singular keeps 200 {"task":null}, batch skips the task and keeps returning valid ones. Regressions cover both paths. Blocker 2 — client copy: runtime_access_denied gets dedicated actionable copy (make runtime public / rebind-copy agent) on the issues blocked-trigger mapping, both chat send-failure toasts, the chat failure-reason map, the mobile dispatch-reason helper, and the autopilot run-now toast. Generic fallbacks unchanged; locale keys added for en/ko/ja/zh-Hans. Blocker 3 — fixture convention: runtime_access_denied_test.go now uses testutil.Call; daemon_runtime_access_test.go seeds its chat task via the dbfx.Task fixture. Test semantics unchanged. Co-authored-by: multica-agent <[email protected]> * fix: bind final task token to locked runtime owner Co-authored-by: multica-agent <[email protected]> * fix: keep delivered user context on current owner Co-authored-by: multica-agent <[email protected]> * test: use testutil.Call in settlement-failure claim regression Convert the last manual httptest.NewRecorder flow in TestFinalizeClaimDelivery_SettlementFailureIsUnsettled to the repository's required testutil.Call helper (hard convention from review) and drop the now-unused net/http/httptest import. No production code changes. Co-authored-by: multica-agent <[email protected]> * PUCK-132: settle queued private-runtime owner mismatches as runtime_access_denied Align persisted task failure semantics with admission semantics: queued private-runtime ownership mismatches now settle as runtime_access_denied, letting clients reach the dedicated recovery copy instead of the generic invalid_task_identity copy. Actual task/agent identity violations (agent rebound, agent deleted, response identity mismatch, error_agent_runtime_changed) keep invalid_task_identity. - add taskfailure.ReasonRuntimeAccessDenied (permanent, non-retryable ownership authorization failure; agent process never launched) - swap the reason in the two owner-mismatch settlement paths in handler/daemon.go (response-assembly recheck + delivery-gate authz-error default branch, including ownerless-runtime denial) - regression A (queued issue task), B (queued chat task + assistant failure message via existing FailTask path), C (agent rebind keeps invalid_task_identity) Co-authored-by: multica-agent <[email protected]> * PUCK-132: map persisted runtime access failures in clients * fix(i18n): add missing fr translations for runtime_access_denied keys Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: multica-agent <[email protected]> Co-authored-by: worker-opencode <[email protected]> Co-authored-by: puck-181 <puck-181@local>
65 lines
2.6 KiB
TypeScript
65 lines
2.6 KiB
TypeScript
// @vitest-environment node
|
|
import { describe, expect, it } from "vitest";
|
|
import { dispatchReasonCode, sendFailureMessage } from "./dispatch-reason";
|
|
|
|
// Shaped like `apps/mobile/data/api.ts:ApiError` — a thrown Error carrying the
|
|
// parsed response body. Built inline rather than imported because `@/data/api`
|
|
// pulls react-native into a node-environment suite; the only field under test
|
|
// is `body`, which is what the helper reads.
|
|
const apiError = (body: unknown) => Object.assign(new Error("request failed"), { body });
|
|
|
|
describe("dispatchReasonCode", () => {
|
|
it("reads reason_code off a structured rejection body", () => {
|
|
const err = apiError({
|
|
error: "invocation not allowed",
|
|
reason_code: "invocation_not_allowed",
|
|
});
|
|
expect(dispatchReasonCode(err)).toBe("invocation_not_allowed");
|
|
});
|
|
|
|
it("returns undefined for an unstructured failure", () => {
|
|
expect(dispatchReasonCode(new Error("network down"))).toBeUndefined();
|
|
expect(dispatchReasonCode(apiError(undefined))).toBeUndefined();
|
|
expect(dispatchReasonCode(apiError("plain text body"))).toBeUndefined();
|
|
expect(dispatchReasonCode(null)).toBeUndefined();
|
|
});
|
|
|
|
it("ignores a non-string or empty reason_code", () => {
|
|
expect(dispatchReasonCode(apiError({ reason_code: 7 }))).toBeUndefined();
|
|
expect(dispatchReasonCode(apiError({ reason_code: "" }))).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe("sendFailureMessage", () => {
|
|
// The point of the helper: a revoked permission must not read as a transient
|
|
// failure the user should retry (MUL-6380).
|
|
it("names revoked permission instead of suggesting a retry", () => {
|
|
const message = sendFailureMessage(
|
|
apiError({ reason_code: "invocation_not_allowed" }),
|
|
);
|
|
expect(message).toMatch(/no longer have permission/i);
|
|
expect(message).not.toMatch(/try again/i);
|
|
});
|
|
|
|
it("names the runtime for agent_runtime_required", () => {
|
|
expect(
|
|
sendFailureMessage(apiError({ reason_code: "agent_runtime_required" })),
|
|
).toMatch(/runtime/i);
|
|
});
|
|
|
|
// PUCK-89: a private-runtime owner mismatch never resolves by retrying. The
|
|
// message must name the real fixes instead of the generic retry advice.
|
|
it("gives runtime_access_denied actionable copy, not a retry", () => {
|
|
const message = sendFailureMessage(
|
|
apiError({ reason_code: "runtime_access_denied" }),
|
|
);
|
|
expect(message).toMatch(/private runtime/i);
|
|
expect(message).toMatch(/public|rebind/i);
|
|
expect(message).not.toMatch(/try again/i);
|
|
});
|
|
|
|
it("falls back to a retryable message for anything else", () => {
|
|
expect(sendFailureMessage(new Error("timeout"))).toMatch(/try again/i);
|
|
});
|
|
});
|