mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-04-30 03:00:41 -04:00
fix(copilot): resolve merge conflict with dev + improve frontend test coverage
- Resolve merge conflict in routes.py (keep both TaskDecompositionResponse and Memory*Response types) and openapi.json (re-exported from backend) - Move computeRemainingSeconds from DecomposeGoal.tsx to helpers.tsx so it's testable as a pure function - Add 9 tests for computeRemainingSeconds covering: null/error output, missing/unparseable created_at, correct remaining calculation, zero clamp when deadline passed, total clamp for future timestamps, and fallback when auto_approve_seconds is missing Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -21,43 +21,19 @@ import { ToolErrorCard } from "../../components/ToolErrorCard/ToolErrorCard";
|
||||
import { StepItem } from "./components/StepItem";
|
||||
import {
|
||||
AccordionIcon,
|
||||
computeRemainingSeconds,
|
||||
getAnimationText,
|
||||
getDecomposeGoalOutput,
|
||||
isDecompositionOutput,
|
||||
isErrorOutput,
|
||||
FALLBACK_COUNTDOWN_SECONDS,
|
||||
ToolIcon,
|
||||
type DecomposeGoalOutput,
|
||||
} from "./helpers";
|
||||
|
||||
// Fallback used only if the backend response omits auto_approve_seconds
|
||||
// (older sessions). The authoritative value comes from the tool output.
|
||||
const FALLBACK_COUNTDOWN_SECONDS = 60;
|
||||
const RADIUS = 15;
|
||||
const CIRCUMFERENCE = 2 * Math.PI * RADIUS;
|
||||
|
||||
/**
|
||||
* Compute remaining countdown seconds, deriving elapsed time from the
|
||||
* backend-stamped ``created_at`` so the timer reflects real elapsed time
|
||||
* when the user reopens the session — instead of restarting from full.
|
||||
*
|
||||
* Falls back to the full countdown when ``created_at`` is missing (older
|
||||
* sessions stored before this field existed) or unparseable. Clamps to
|
||||
* ``[0, total]`` to defend against client clock skew producing future
|
||||
* timestamps.
|
||||
*/
|
||||
function computeRemainingSeconds(
|
||||
output: DecomposeGoalOutput | null,
|
||||
fallback: number,
|
||||
): number {
|
||||
if (!output || !isDecompositionOutput(output)) return fallback;
|
||||
const total = output.auto_approve_seconds ?? fallback;
|
||||
if (!output.created_at) return total;
|
||||
const createdAtMs = new Date(output.created_at).getTime();
|
||||
if (Number.isNaN(createdAtMs)) return total;
|
||||
const elapsedSec = (Date.now() - createdAtMs) / 1000;
|
||||
return Math.max(0, Math.min(total, Math.round(total - elapsedSec)));
|
||||
}
|
||||
|
||||
interface EditableStep {
|
||||
step_id: string;
|
||||
description: string;
|
||||
|
||||
@@ -4,8 +4,10 @@
|
||||
* Covers: parseOutput / getDecomposeGoalOutput, type guards, getAnimationText
|
||||
*/
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
computeRemainingSeconds,
|
||||
FALLBACK_COUNTDOWN_SECONDS,
|
||||
getAnimationText,
|
||||
getDecomposeGoalOutput,
|
||||
isDecompositionOutput,
|
||||
@@ -206,3 +208,88 @@ describe("getAnimationText", () => {
|
||||
expect(text.toLowerCase()).toContain("analyzing");
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// computeRemainingSeconds
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const DECOMPOSITION_BASE: TaskDecompositionOutput = {
|
||||
type: "task_decomposition",
|
||||
message: "Plan",
|
||||
goal: "Build agent",
|
||||
steps: [{ step_id: "s1", description: "Step 1", action: "add_block" }],
|
||||
step_count: 1,
|
||||
requires_approval: true,
|
||||
auto_approve_seconds: 60,
|
||||
created_at: new Date().toISOString(),
|
||||
};
|
||||
|
||||
describe("computeRemainingSeconds", () => {
|
||||
it("returns fallback when output is null", () => {
|
||||
expect(computeRemainingSeconds(null, 60)).toBe(60);
|
||||
});
|
||||
|
||||
it("returns fallback when output is an error", () => {
|
||||
const err: DecomposeErrorOutput = { type: "error", error: "oops" };
|
||||
expect(computeRemainingSeconds(err, 60)).toBe(60);
|
||||
});
|
||||
|
||||
it("returns auto_approve_seconds when created_at is missing", () => {
|
||||
const noTimestamp = { ...DECOMPOSITION_BASE, created_at: undefined };
|
||||
expect(computeRemainingSeconds(noTimestamp, 99)).toBe(60);
|
||||
});
|
||||
|
||||
it("returns auto_approve_seconds when created_at is unparseable", () => {
|
||||
const badTimestamp = { ...DECOMPOSITION_BASE, created_at: "not-a-date" };
|
||||
expect(computeRemainingSeconds(badTimestamp, 99)).toBe(60);
|
||||
});
|
||||
|
||||
it("returns correct remaining seconds for a recent timestamp", () => {
|
||||
vi.useFakeTimers();
|
||||
const now = new Date("2026-01-01T00:00:30Z");
|
||||
vi.setSystemTime(now);
|
||||
const output = {
|
||||
...DECOMPOSITION_BASE,
|
||||
created_at: "2026-01-01T00:00:00Z",
|
||||
};
|
||||
// 30s elapsed → 60 - 30 = 30
|
||||
expect(computeRemainingSeconds(output, 60)).toBe(30);
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("clamps to 0 when deadline has passed", () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date("2026-01-01T00:02:00Z"));
|
||||
const output = {
|
||||
...DECOMPOSITION_BASE,
|
||||
created_at: "2026-01-01T00:00:00Z",
|
||||
};
|
||||
expect(computeRemainingSeconds(output, 60)).toBe(0);
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("clamps to total when client clock is ahead (future timestamp)", () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date("2026-01-01T00:00:00Z"));
|
||||
const output = {
|
||||
...DECOMPOSITION_BASE,
|
||||
created_at: "2026-01-01T00:00:10Z",
|
||||
};
|
||||
// elapsed = -10 → total - (-10) = 70, clamped to 60
|
||||
expect(computeRemainingSeconds(output, 60)).toBe(60);
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("uses fallback when auto_approve_seconds is missing", () => {
|
||||
const noAutoApprove = {
|
||||
...DECOMPOSITION_BASE,
|
||||
auto_approve_seconds: undefined,
|
||||
created_at: undefined,
|
||||
};
|
||||
expect(computeRemainingSeconds(noAutoApprove, 42)).toBe(42);
|
||||
});
|
||||
|
||||
it("exports FALLBACK_COUNTDOWN_SECONDS as 60", () => {
|
||||
expect(FALLBACK_COUNTDOWN_SECONDS).toBe(60);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -42,6 +42,33 @@ export type DecomposeGoalOutput =
|
||||
| TaskDecompositionOutput
|
||||
| DecomposeErrorOutput;
|
||||
|
||||
// Fallback used only if the backend response omits auto_approve_seconds
|
||||
// (older sessions). The authoritative value comes from the tool output.
|
||||
export const FALLBACK_COUNTDOWN_SECONDS = 60;
|
||||
|
||||
/**
|
||||
* Compute remaining countdown seconds, deriving elapsed time from the
|
||||
* backend-stamped ``created_at`` so the timer reflects real elapsed time
|
||||
* when the user reopens the session — instead of restarting from full.
|
||||
*
|
||||
* Falls back to the full countdown when ``created_at`` is missing (older
|
||||
* sessions stored before this field existed) or unparseable. Clamps to
|
||||
* ``[0, total]`` to defend against client clock skew producing future
|
||||
* timestamps.
|
||||
*/
|
||||
export function computeRemainingSeconds(
|
||||
output: DecomposeGoalOutput | null,
|
||||
fallback: number,
|
||||
): number {
|
||||
if (!output || !isDecompositionOutput(output)) return fallback;
|
||||
const total = output.auto_approve_seconds ?? fallback;
|
||||
if (!output.created_at) return total;
|
||||
const createdAtMs = new Date(output.created_at).getTime();
|
||||
if (Number.isNaN(createdAtMs)) return total;
|
||||
const elapsedSec = (Date.now() - createdAtMs) / 1000;
|
||||
return Math.max(0, Math.min(total, Math.round(total - elapsedSec)));
|
||||
}
|
||||
|
||||
function parseOutput(output: unknown): DecomposeGoalOutput | null {
|
||||
if (!output) return null;
|
||||
if (typeof output === "string") {
|
||||
|
||||
Reference in New Issue
Block a user