Compare commits
2
Commits
6974b710de
...
707fe1b3c0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
707fe1b3c0 | ||
|
|
ed033e71d5 |
@@ -10,6 +10,17 @@ async function post(request) {
|
|||||||
return Response.json(result, { status: 200 });
|
return Response.json(result, { status: 200 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (result.code === "PROVIDER_UNAVAILABLE") {
|
||||||
|
console.error("[api/cases/start] provider unavailable", {
|
||||||
|
error: result.error ?? "Start case failed",
|
||||||
|
providerApiPath: result.providerApiPath,
|
||||||
|
});
|
||||||
|
return Response.json(
|
||||||
|
{ success: false, error: "Reasoning service is temporarily unavailable." },
|
||||||
|
{ status: 503 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const status =
|
const status =
|
||||||
result.statusCode === 400
|
result.statusCode === 400
|
||||||
? 400
|
? 400
|
||||||
|
|||||||
@@ -144,9 +144,15 @@ async function post(request) {
|
|||||||
});
|
});
|
||||||
console.info("[api/focused-investigation/deconstruct] end", {
|
console.info("[api/focused-investigation/deconstruct] end", {
|
||||||
targetNodeId,
|
targetNodeId,
|
||||||
status: 500,
|
status: e?.code === "PROVIDER_UNAVAILABLE" ? 503 : 500,
|
||||||
elapsedMs,
|
elapsedMs,
|
||||||
});
|
});
|
||||||
|
if (e?.code === "PROVIDER_UNAVAILABLE") {
|
||||||
|
return Response.json(
|
||||||
|
{ error: "Reasoning service is temporarily unavailable." },
|
||||||
|
{ status: 503 },
|
||||||
|
);
|
||||||
|
}
|
||||||
return Response.json(
|
return Response.json(
|
||||||
{ error: e.message || "Unknown server error" },
|
{ error: e.message || "Unknown server error" },
|
||||||
{ status: 500 },
|
{ status: 500 },
|
||||||
|
|||||||
@@ -3,6 +3,12 @@
|
|||||||
> **Role:** Concise operational snapshot for resuming work today. Not a historical diary.
|
> **Role:** Concise operational snapshot for resuming work today. Not a historical diary.
|
||||||
> The design evolution archive index at `docs/design-evolution/README.md` provides progressive loading of experiment history; load the relevant chapter only when a specific historical question requires it.
|
> The design evolution archive index at `docs/design-evolution/README.md` provides progressive loading of experiment history; load the relevant chapter only when a specific historical question requires it.
|
||||||
|
|
||||||
|
## Focused-investigation provider outage boundary
|
||||||
|
|
||||||
|
- Focused-investigation outage handling now sanitizes provider failure at the API boundary: unavailable focused reasoning returns a controlled HTTP 503, and raw provider/Ollama diagnostics no longer leave that boundary.
|
||||||
|
- The existing generic Retry UX remains unchanged. A live deployed outage had already proven investigation preservation.
|
||||||
|
- `/api/cases/start` and `/api/cases/update` outage sanitization remain separately unverified; this change does not claim those routes are fixed.
|
||||||
|
|
||||||
## v0.62d production Docker packaging — LIVE PROVEN
|
## v0.62d production Docker packaging — LIVE PROVEN
|
||||||
|
|
||||||
- `Dockerfile` — minimal multi-stage Alpine build (Node 22), Next.js standalone output mode
|
- `Dockerfile` — minimal multi-stage Alpine build (Node 22), Next.js standalone output mode
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
# Current Project State — Confidence Engine
|
# Current Project State — Confidence Engine
|
||||||
|
|
||||||
|
## Focused-Investigation Provider Outage Boundary
|
||||||
|
|
||||||
|
- Focused-investigation outage handling now sanitizes provider failure at the API boundary: unavailable focused reasoning returns a controlled HTTP 503, and raw provider/Ollama diagnostics no longer leave that boundary.
|
||||||
|
- The existing generic Retry UX remains unchanged. A live deployed outage had already proven investigation preservation.
|
||||||
|
- `/api/cases/start` and `/api/cases/update` outage sanitization remain separately unverified; this change does not claim those routes are fixed.
|
||||||
|
|
||||||
## v0.62d Production Docker Packaging — LIVE PROVEN
|
## v0.62d Production Docker Packaging — LIVE PROVEN
|
||||||
|
|
||||||
- `Dockerfile` created: multi-stage Alpine build (Node 22), Next.js standalone output, configurable port 3000, health-check boundary via `/api/health`
|
- `Dockerfile` created: multi-stage Alpine build (Node 22), Next.js standalone output, configurable port 3000, health-check boundary via `/api/health`
|
||||||
|
|||||||
@@ -439,6 +439,9 @@ class OllamaLlmProvider {
|
|||||||
`- Use a smaller model (e.g., llama3.1 instead of llama3.1:70b)\n` +
|
`- Use a smaller model (e.g., llama3.1 instead of llama3.1:70b)\n` +
|
||||||
`- Check Ollama logs: \`ollama serve\` or look at your system logs`
|
`- Check Ollama logs: \`ollama serve\` or look at your system logs`
|
||||||
);
|
);
|
||||||
|
if (e?.name === "AbortError" || e instanceof TypeError) {
|
||||||
|
error.code = "PROVIDER_UNAVAILABLE";
|
||||||
|
}
|
||||||
error.providerApiPath = apiUsed;
|
error.providerApiPath = apiUsed;
|
||||||
error.providerExecution = providerExecution;
|
error.providerExecution = providerExecution;
|
||||||
throw error;
|
throw error;
|
||||||
|
|||||||
@@ -8,6 +8,10 @@ vi.mock("@/lib/graph/orchestrator.js", () => ({
|
|||||||
startCase: (...args) => mockStartCase(...args),
|
startCase: (...args) => mockStartCase(...args),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
vi.mock("@/lib/supabase/api-auth.js", () => ({
|
||||||
|
withAuthenticatedApi: (handler) => handler,
|
||||||
|
}));
|
||||||
|
|
||||||
describe("app/api/cases/start route", () => {
|
describe("app/api/cases/start route", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.resetModules();
|
vi.resetModules();
|
||||||
@@ -123,6 +127,33 @@ describe("app/api/cases/start route", () => {
|
|||||||
expect(errorSpy).not.toHaveBeenCalled();
|
expect(errorSpy).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("returns a sanitized 503 when the provider is unavailable", async () => {
|
||||||
|
mockStartCase.mockResolvedValue({
|
||||||
|
success: false,
|
||||||
|
code: "PROVIDER_UNAVAILABLE",
|
||||||
|
error: "Ollama /api/generate request timed out after 5 minutes",
|
||||||
|
providerApiPath: "/api/generate",
|
||||||
|
providerExecution: { generateRequestAttempted: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
const { POST } = await import("@/app/api/cases/start/route.js");
|
||||||
|
const response = await POST(
|
||||||
|
new Request("http://localhost/api/cases/start", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({ scenario: "Scenario text" }),
|
||||||
|
headers: { "content-type": "application/json" },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(response.status).toBe(503);
|
||||||
|
const body = await response.json();
|
||||||
|
expect(body).toEqual({
|
||||||
|
success: false,
|
||||||
|
error: "Reasoning service is temporarily unavailable.",
|
||||||
|
});
|
||||||
|
expect(JSON.stringify(body)).not.toMatch(/ollama|generate|timed out/i);
|
||||||
|
});
|
||||||
|
|
||||||
it("returns provider/internal failures as 5xx without stack traces", async () => {
|
it("returns provider/internal failures as 5xx without stack traces", async () => {
|
||||||
const rawResponse = `{"reconstruction":{"observedStates":[{"id":"obs-1"${"x".repeat(2500)}}]}}`;
|
const rawResponse = `{"reconstruction":{"observedStates":[{"id":"obs-1"${"x".repeat(2500)}}]}}`;
|
||||||
mockStartCase.mockResolvedValue({
|
mockStartCase.mockResolvedValue({
|
||||||
|
|||||||
@@ -9,6 +9,10 @@
|
|||||||
import { afterEach, beforeEach, describe, it, expect, vi } from "vitest";
|
import { afterEach, beforeEach, describe, it, expect, vi } from "vitest";
|
||||||
import { focusedDeconstructJsonSchema, validateFocusedDeconstructSchema } from "@/lib/graph/focused-investigation";
|
import { focusedDeconstructJsonSchema, validateFocusedDeconstructSchema } from "@/lib/graph/focused-investigation";
|
||||||
|
|
||||||
|
vi.mock("@/lib/supabase/api-auth.js", () => ({
|
||||||
|
withAuthenticatedApi: (handler) => handler,
|
||||||
|
}));
|
||||||
|
|
||||||
// ── helpers ──────────────────────────────────────────────────────────────
|
// ── helpers ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
function makeMockProvider(inventedTargetNodeId) {
|
function makeMockProvider(inventedTargetNodeId) {
|
||||||
@@ -389,6 +393,36 @@ describe("focused-deconstruct targetNodeId identity boundary", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("returns a sanitized 503 when the provider is unavailable", async () => {
|
||||||
|
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||||
|
vi.doMock("@/lib/llm/provider", () => ({
|
||||||
|
getProvider: () => ({
|
||||||
|
generateReconstruction: vi.fn().mockRejectedValue(Object.assign(new Error(
|
||||||
|
"Ollama /api/generate request timed out after 5 minutes",
|
||||||
|
), { code: "PROVIDER_UNAVAILABLE", providerApiPath: "/api/generate" })),
|
||||||
|
}),
|
||||||
|
getProviderModelName: () => "configured-model",
|
||||||
|
}));
|
||||||
|
const { POST } = await import("../app/api/focused-investigation/deconstruct/route.js");
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await POST(new Request("http://localhost/api/focused-investigation/deconstruct", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
targetNodeId: "node-id", targetLabel: "label", targetDescription: "description",
|
||||||
|
centralStatement: "central", question: "question?", answer: "answer.",
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
expect(response.status).toBe(503);
|
||||||
|
const json = await response.json();
|
||||||
|
expect(json).toEqual({ error: "Reasoning service is temporarily unavailable." });
|
||||||
|
expect(JSON.stringify(json)).not.toMatch(/ollama|generate|timed out/i);
|
||||||
|
} finally {
|
||||||
|
errorSpy.mockRestore();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it("preserves the 502 validation-failure contract with diagnostics", async () => {
|
it("preserves the 502 validation-failure contract with diagnostics", async () => {
|
||||||
vi.doMock("@/lib/llm/provider", () => ({
|
vi.doMock("@/lib/llm/provider", () => ({
|
||||||
getProvider: () => ({
|
getProvider: () => ({
|
||||||
|
|||||||
Reference in New Issue
Block a user