|
|
|
@@ -53,7 +53,11 @@ const defaultWorkspaceRegistry = {
|
|
|
|
|
|
|
|
|
|
function buildApp(config: Parameters<typeof buildRealApp>[0], deps: Record<string, unknown> = {}) {
|
|
|
|
|
const thtRunner = deps.thtRunner
|
|
|
|
|
? { qdrantEnsure: async () => ({ ok: true }), ...(deps.thtRunner as object) }
|
|
|
|
|
? {
|
|
|
|
|
qdrantEnsure: async () => ({ ok: true }),
|
|
|
|
|
ensureInteractionLanguage: async () => ({ interaction_language: "en" }),
|
|
|
|
|
...(deps.thtRunner as object),
|
|
|
|
|
}
|
|
|
|
|
: undefined;
|
|
|
|
|
return buildRealApp(config, {
|
|
|
|
|
workspaceRuntimeSupport: () => true,
|
|
|
|
@@ -88,6 +92,129 @@ const aliceHeaders = {
|
|
|
|
|
"x-thoth-is-admin": "0",
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
test.each([undefined, null, "", "en--US", "en_US", "en\nIGNORE", "en<script>", 42])(
|
|
|
|
|
"new sessions reject invalid interaction language %s before persistence", async (interactionLanguage) => {
|
|
|
|
|
const app = mutApp({ sessionNew: async () => { throw new Error("must not create"); } });
|
|
|
|
|
try {
|
|
|
|
|
const response = await app.inject({
|
|
|
|
|
method: "POST", url: "/sessions", payload: { question: "Pazienti", interactionLanguage },
|
|
|
|
|
});
|
|
|
|
|
expect(response.statusCode).toBe(400);
|
|
|
|
|
expect(response.json()).toMatchObject({ code: "invalid_interaction_language" });
|
|
|
|
|
} finally { await app.close(); }
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
test.each([["en", "en"], ["fr-FR", "fr-FR"], ["FR-fr", "fr-FR"]])(
|
|
|
|
|
"new session forwards canonical interaction language %s without changing the question", async (language, canonical) => {
|
|
|
|
|
let created: any;
|
|
|
|
|
const app = buildApp(loadConfig({ THT_HARNESS_DIR: "../harness" }), {
|
|
|
|
|
thtRunner: {
|
|
|
|
|
sessionNew: async (options: any) => { created = options; return { id: "language" }; },
|
|
|
|
|
searchPack: async () => {},
|
|
|
|
|
},
|
|
|
|
|
readiness: { ensure: async () => ({ ok: true }) },
|
|
|
|
|
mgr: {
|
|
|
|
|
get: () => undefined, createFor: () => ({ bridge: { onClientEvent: () => {} } }),
|
|
|
|
|
configure: async () => {}, start: () => {},
|
|
|
|
|
},
|
|
|
|
|
getSettings: () => ({ workspace: "default" }),
|
|
|
|
|
});
|
|
|
|
|
try {
|
|
|
|
|
const response = await app.inject({
|
|
|
|
|
method: "POST", url: "/sessions", payload: { question: "Pazienti", interactionLanguage: language },
|
|
|
|
|
});
|
|
|
|
|
expect(response.statusCode).toBe(200);
|
|
|
|
|
expect(created).toMatchObject({ question: "Pazienti", interactionLanguage: canonical });
|
|
|
|
|
} finally { await app.close(); }
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test("resume rejects browser interaction language overrides", async () => {
|
|
|
|
|
const app = mutApp({ sessionShow: async () => ({ status: "open", interaction_language: "it" }) });
|
|
|
|
|
try {
|
|
|
|
|
const response = await app.inject({
|
|
|
|
|
method: "POST", url: "/sessions/s1/resume", payload: { interactionLanguage: "en" },
|
|
|
|
|
});
|
|
|
|
|
expect(response.statusCode).toBe(400);
|
|
|
|
|
expect(response.json()).toMatchObject({ code: "interaction_language_pinned" });
|
|
|
|
|
} finally { await app.close(); }
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test("resume pins legacy interaction language using the resolved harness workspace", async () => {
|
|
|
|
|
let pinnedWorkspace: string | undefined;
|
|
|
|
|
let runtime: any;
|
|
|
|
|
const app = buildApp(loadConfig({ THT_HARNESS_DIR: "../harness" }), {
|
|
|
|
|
thtRunner: {
|
|
|
|
|
sessionShow: async () => ({ id: "legacy", status: "closed" }),
|
|
|
|
|
ensureInteractionLanguage: async (_id: string, workspace: string) => {
|
|
|
|
|
pinnedWorkspace = workspace;
|
|
|
|
|
return { interaction_language: "it" };
|
|
|
|
|
},
|
|
|
|
|
reopenSession: async () => {},
|
|
|
|
|
},
|
|
|
|
|
readiness: { ensure: async () => ({ ok: true }) },
|
|
|
|
|
mgr: {
|
|
|
|
|
get: () => undefined,
|
|
|
|
|
createFor: (_id: string, options: any) => {
|
|
|
|
|
runtime = options;
|
|
|
|
|
return { bridge: { onClientEvent: () => {} } };
|
|
|
|
|
},
|
|
|
|
|
configure: async () => {}, start: () => {},
|
|
|
|
|
},
|
|
|
|
|
getSettings: () => ({ workspace: "other-browser-workspace" }),
|
|
|
|
|
});
|
|
|
|
|
try {
|
|
|
|
|
const response = await app.inject({ method: "POST", url: "/sessions/legacy/resume" });
|
|
|
|
|
expect(response.statusCode).toBe(200);
|
|
|
|
|
expect(pinnedWorkspace).toContain("/default.yaml");
|
|
|
|
|
expect(runtime).toMatchObject({ interactionLanguage: "it", mode: "resume" });
|
|
|
|
|
} finally { await app.close(); }
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test("resume retains persisted interaction language despite different browser settings", async () => {
|
|
|
|
|
let options: any;
|
|
|
|
|
const app = buildApp(loadConfig({ THT_HARNESS_DIR: "../harness" }), {
|
|
|
|
|
thtRunner: {
|
|
|
|
|
sessionShow: async () => ({ id: "saved", status: "closed", interaction_language: "it" }),
|
|
|
|
|
ensureInteractionLanguage: async () => { throw new Error("language is already pinned"); },
|
|
|
|
|
reopenSession: async () => {},
|
|
|
|
|
},
|
|
|
|
|
readiness: { ensure: async () => ({ ok: true }) },
|
|
|
|
|
mgr: {
|
|
|
|
|
get: () => undefined,
|
|
|
|
|
createFor: (_id: string, value: any) => {
|
|
|
|
|
options = value;
|
|
|
|
|
return { bridge: { onClientEvent: () => {} } };
|
|
|
|
|
},
|
|
|
|
|
configure: async () => {}, start: () => {},
|
|
|
|
|
},
|
|
|
|
|
getSettings: () => ({ workspace: "english-workspace", locale: "en" }),
|
|
|
|
|
});
|
|
|
|
|
try {
|
|
|
|
|
const response = await app.inject({ method: "POST", url: "/sessions/saved/resume" });
|
|
|
|
|
expect(response.statusCode).toBe(200);
|
|
|
|
|
expect(options.interactionLanguage).toBe("it");
|
|
|
|
|
} finally { await app.close(); }
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test("resume cannot start Pi if legacy interaction language cannot be persisted", async () => {
|
|
|
|
|
const app = buildApp(loadConfig({ THT_HARNESS_DIR: "../harness" }), {
|
|
|
|
|
thtRunner: {
|
|
|
|
|
sessionShow: async () => ({ id: "legacy", status: "closed" }),
|
|
|
|
|
ensureInteractionLanguage: async () => { throw new Error("private storage details"); },
|
|
|
|
|
reopenSession: async () => { throw new Error("must not reopen"); },
|
|
|
|
|
},
|
|
|
|
|
readiness: { ensure: async () => ({ ok: true }) },
|
|
|
|
|
mgr: { createFor: () => { throw new Error("must not spawn"); } },
|
|
|
|
|
getSettings: () => ({ workspace: "default" }),
|
|
|
|
|
});
|
|
|
|
|
try {
|
|
|
|
|
const response = await app.inject({ method: "POST", url: "/sessions/legacy/resume" });
|
|
|
|
|
expect(response.statusCode).toBe(503);
|
|
|
|
|
expect(response.body).not.toContain("private storage details");
|
|
|
|
|
} finally { await app.close(); }
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test("upstream requests without a principal fail before a Pi runtime can be created", async () => {
|
|
|
|
|
let created = false;
|
|
|
|
|
const app = buildApp(loadConfig({ AUTH_MODE: "upstream", THT_HARNESS_DIR: "../harness" }), {
|
|
|
|
@@ -95,7 +222,7 @@ test("upstream requests without a principal fail before a Pi runtime can be crea
|
|
|
|
|
thtRunner: {} as any,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const response = await app.inject({ method: "POST", url: "/sessions", payload: { question: "q" } });
|
|
|
|
|
const response = await app.inject({ method: "POST", url: "/sessions", payload: { interactionLanguage: "en", question: "q" } });
|
|
|
|
|
|
|
|
|
|
expect(response.statusCode).toBe(401);
|
|
|
|
|
expect(created).toBe(false);
|
|
|
|
@@ -133,7 +260,7 @@ test("maintenance rejects new and resumed session admission without interrupting
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const create = await app.inject({
|
|
|
|
|
method: "POST", url: "/sessions", headers: aliceHeaders, payload: { question: "q" },
|
|
|
|
|
method: "POST", url: "/sessions", headers: aliceHeaders, payload: { interactionLanguage: "en", question: "q" },
|
|
|
|
|
});
|
|
|
|
|
const resume = await app.inject({ method: "POST", url: "/sessions/open/resume", headers: aliceHeaders });
|
|
|
|
|
|
|
|
|
@@ -154,7 +281,7 @@ test("a durable maintenance marker initializes admission closed after backend re
|
|
|
|
|
AUTH_MODE: "upstream", THT_HARNESS_DIR: "../harness", THT_MAINTENANCE_FILE: marker,
|
|
|
|
|
}), { thtRunner: {} as any });
|
|
|
|
|
const response = await app.inject({
|
|
|
|
|
method: "POST", url: "/sessions", headers: aliceHeaders, payload: { question: "q" },
|
|
|
|
|
method: "POST", url: "/sessions", headers: aliceHeaders, payload: { interactionLanguage: "en", question: "q" },
|
|
|
|
|
});
|
|
|
|
|
expect(response.statusCode).toBe(503);
|
|
|
|
|
expect(response.json()).toMatchObject({ code: "maintenance" });
|
|
|
|
@@ -492,7 +619,7 @@ test("new sessions are created through the authenticated principal, not a client
|
|
|
|
|
|
|
|
|
|
const response = await app.inject({
|
|
|
|
|
method: "POST", url: "/sessions", headers: aliceHeaders,
|
|
|
|
|
payload: { question: "q", owner: "mallory" },
|
|
|
|
|
payload: { interactionLanguage: "en", question: "q", owner: "mallory" },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
expect(response.statusCode).toBe(200);
|
|
|
|
@@ -509,7 +636,7 @@ test("new sessions reject the client legacy workspace field unless local legacy
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const response = await app.inject({
|
|
|
|
|
method: "POST", url: "/sessions", payload: { question: "q", workspace: "legacy" },
|
|
|
|
|
method: "POST", url: "/sessions", payload: { interactionLanguage: "en", question: "q", workspace: "legacy" },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
expect(response.statusCode).toBe(409);
|
|
|
|
@@ -529,7 +656,7 @@ test("explicit local legacy mode permits the unpinned client workspace request",
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const response = await app.inject({
|
|
|
|
|
method: "POST", url: "/sessions", payload: { question: "q", workspace: "legacy" },
|
|
|
|
|
method: "POST", url: "/sessions", payload: { interactionLanguage: "en", question: "q", workspace: "legacy" },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
expect(response.statusCode).toBe(200);
|
|
|
|
@@ -567,7 +694,7 @@ test("creates a session from the active immutable workspace revision", async ()
|
|
|
|
|
|
|
|
|
|
await app.inject({
|
|
|
|
|
method: "POST", url: "/sessions",
|
|
|
|
|
payload: { question: "q", workspaceId: "psd-clinical", provider: "zai", model: "glm-5.2", thinking: "low" },
|
|
|
|
|
payload: { interactionLanguage: "en", question: "q", workspaceId: "psd-clinical", provider: "zai", model: "glm-5.2", thinking: "low" },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
expect(sessionNew).toHaveBeenCalledWith(expect.objectContaining({
|
|
|
|
@@ -611,7 +738,7 @@ test("hands one Catalog-backed runtime to both retrieval and Pi", async () => {
|
|
|
|
|
const response = await app.inject({
|
|
|
|
|
method: "POST",
|
|
|
|
|
url: "/sessions",
|
|
|
|
|
payload: { question: "Which users placed orders?", workspaceId: "default" },
|
|
|
|
|
payload: { interactionLanguage: "en", question: "Which users placed orders?", workspaceId: "default" },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
expect(response.statusCode).toBe(200);
|
|
|
|
@@ -687,7 +814,7 @@ test("refuses core admission when the workspace preprocessing fingerprint is sta
|
|
|
|
|
const response = await app.inject({
|
|
|
|
|
method: "POST",
|
|
|
|
|
url: "/sessions",
|
|
|
|
|
payload: { question: "q", workspaceId: "default" },
|
|
|
|
|
payload: { interactionLanguage: "en", question: "q", workspaceId: "default" },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
expect(response.statusCode).toBe(409);
|
|
|
|
@@ -739,7 +866,7 @@ test("rejects an SSH-only Catalog binding before persisting or starting a sessio
|
|
|
|
|
} as any,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const response = await app.inject({ method: "POST", url: "/sessions", payload: { question: "q" } });
|
|
|
|
|
const response = await app.inject({ method: "POST", url: "/sessions", payload: { interactionLanguage: "en", question: "q" } });
|
|
|
|
|
|
|
|
|
|
expect(response.statusCode).toBe(409);
|
|
|
|
|
expect(response.json()).toMatchObject({ code: "workspace_not_activatable" });
|
|
|
|
@@ -779,7 +906,7 @@ test("hands a revision lease to retention only after the session manifest is dur
|
|
|
|
|
workspaceRegistry: { acquireSessionRevision } as any,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const request = app.inject({ method: "POST", url: "/sessions", payload: { question: "q" } });
|
|
|
|
|
const request = app.inject({ method: "POST", url: "/sessions", payload: { interactionLanguage: "en", question: "q" } });
|
|
|
|
|
await new Promise((resolve) => setImmediate(resolve));
|
|
|
|
|
expect(markPersisted).not.toHaveBeenCalled();
|
|
|
|
|
expect(abort).not.toHaveBeenCalled();
|
|
|
|
@@ -810,7 +937,7 @@ test("creates a session from the configured default workspace revision when work
|
|
|
|
|
workspaceRegistry: registry as any,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await app.inject({ method: "POST", url: "/sessions", payload: { question: "q" } });
|
|
|
|
|
await app.inject({ method: "POST", url: "/sessions", payload: { interactionLanguage: "en", question: "q" } });
|
|
|
|
|
|
|
|
|
|
expect(registry.read).toHaveBeenCalledWith("psd-clinical");
|
|
|
|
|
expect(sessionNew).toHaveBeenCalledWith(expect.objectContaining({
|
|
|
|
@@ -896,7 +1023,7 @@ test("session lifecycle locates a B session when installation default is A", asy
|
|
|
|
|
} as any,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
expect((await app.inject({ method: "POST", url: "/sessions", payload: {
|
|
|
|
|
expect((await app.inject({ method: "POST", url: "/sessions", payload: { interactionLanguage: "en",
|
|
|
|
|
question: "B question", workspaceId: "b-workspace", provider: "zai", model: "glm-5.2", thinking: "low",
|
|
|
|
|
} })).statusCode).toBe(200);
|
|
|
|
|
expect((await app.inject({ method: "GET", url: "/sessions" })).json()).toEqual([
|
|
|
|
@@ -939,7 +1066,7 @@ test("POST /sessions uses the catalog default with workspace/thinking settings a
|
|
|
|
|
],
|
|
|
|
|
spawnFn: () => nodeSpawn("node", [FAKE, SCRIPT]) as any,
|
|
|
|
|
});
|
|
|
|
|
const created = await app.inject({ method: "POST", url: "/sessions", payload: { question: "q" } });
|
|
|
|
|
const created = await app.inject({ method: "POST", url: "/sessions", payload: { interactionLanguage: "en", question: "q" } });
|
|
|
|
|
expect(created.json()).toEqual({ id: "s1" });
|
|
|
|
|
expect(sessionNewArg.workspaceConfigPath).toContain(`/snapshots/${"e".repeat(40)}/w.yaml`);
|
|
|
|
|
expect(sessionNewArg.provider).toBe("zai");
|
|
|
|
@@ -1000,11 +1127,11 @@ test("POST /sessions stops the user's previous open Pi runtime before creating a
|
|
|
|
|
getSettings: () => ({ workspace: "local" }) as any,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
expect((await app.inject({ method: "POST", url: "/sessions", payload: { question: "one" } })).statusCode)
|
|
|
|
|
expect((await app.inject({ method: "POST", url: "/sessions", payload: { interactionLanguage: "en", question: "one" } })).statusCode)
|
|
|
|
|
.toBe(200);
|
|
|
|
|
order.length = 0;
|
|
|
|
|
|
|
|
|
|
const second = await app.inject({ method: "POST", url: "/sessions", payload: { question: "two" } });
|
|
|
|
|
const second = await app.inject({ method: "POST", url: "/sessions", payload: { interactionLanguage: "en", question: "two" } });
|
|
|
|
|
|
|
|
|
|
expect(second.statusCode).toBe(200);
|
|
|
|
|
expect(second.json()).toEqual({ id: "s2" });
|
|
|
|
@@ -1025,7 +1152,7 @@ test("POST /sessions refuses to create a session when the local DWH precheck fai
|
|
|
|
|
readiness: { ensure: async () => ({ ok: true }) } as any,
|
|
|
|
|
getSettings: () => ({ workspace: "w" }) as any,
|
|
|
|
|
});
|
|
|
|
|
const res = await app.inject({ method: "POST", url: "/sessions", payload: { question: "q" } });
|
|
|
|
|
const res = await app.inject({ method: "POST", url: "/sessions", payload: { interactionLanguage: "en", question: "q" } });
|
|
|
|
|
expect(res.statusCode).toBe(503);
|
|
|
|
|
expect(res.json()).toMatchObject({ code: "dwh_unreachable" });
|
|
|
|
|
expect(pinged).toBe(1);
|
|
|
|
@@ -1046,7 +1173,7 @@ test("POST /sessions proceeds past a passing DWH precheck", async () => {
|
|
|
|
|
getSettings: () => ({ workspace: "w" }) as any,
|
|
|
|
|
spawnFn: () => nodeSpawn("node", [FAKE, SCRIPT]) as any,
|
|
|
|
|
});
|
|
|
|
|
const res = await app.inject({ method: "POST", url: "/sessions", payload: { question: "q" } });
|
|
|
|
|
const res = await app.inject({ method: "POST", url: "/sessions", payload: { interactionLanguage: "en", question: "q" } });
|
|
|
|
|
expect(res.statusCode).toBe(200);
|
|
|
|
|
expect(pinged).toBe(1);
|
|
|
|
|
expect(created).toBe(1);
|
|
|
|
@@ -1065,7 +1192,7 @@ test("POST /sessions skips the DWH precheck when the flag is off (default)", asy
|
|
|
|
|
getSettings: () => ({ workspace: "w" }) as any,
|
|
|
|
|
spawnFn: () => nodeSpawn("node", [FAKE, SCRIPT]) as any,
|
|
|
|
|
});
|
|
|
|
|
const res = await app.inject({ method: "POST", url: "/sessions", payload: { question: "q" } });
|
|
|
|
|
const res = await app.inject({ method: "POST", url: "/sessions", payload: { interactionLanguage: "en", question: "q" } });
|
|
|
|
|
expect(res.statusCode).toBe(200);
|
|
|
|
|
expect(pinged).toBe(0); // probe never runs without the flag
|
|
|
|
|
});
|
|
|
|
@@ -1099,7 +1226,7 @@ test("POST /sessions configura Pi con il thinking globale selezionato", async ()
|
|
|
|
|
],
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await app.inject({ method: "POST", url: "/sessions", payload: { question: "q" } });
|
|
|
|
|
await app.inject({ method: "POST", url: "/sessions", payload: { interactionLanguage: "en", question: "q" } });
|
|
|
|
|
await new Promise((resolve) => setImmediate(resolve));
|
|
|
|
|
|
|
|
|
|
expect(configured.thinking).toBe("high");
|
|
|
|
@@ -1427,7 +1554,7 @@ test("resuming a different session stops the user's previous Pi runtime", async
|
|
|
|
|
readiness: { ensure: async () => ({ ok: true }) } as any,
|
|
|
|
|
getSettings: () => ({ workspace: "local" }) as any,
|
|
|
|
|
});
|
|
|
|
|
expect((await app.inject({ method: "POST", url: "/sessions", payload: { question: "one" } })).statusCode)
|
|
|
|
|
expect((await app.inject({ method: "POST", url: "/sessions", payload: { interactionLanguage: "en", question: "one" } })).statusCode)
|
|
|
|
|
.toBe(200);
|
|
|
|
|
|
|
|
|
|
const resumed = await app.inject({ method: "POST", url: "/sessions/s2/resume" });
|
|
|
|
@@ -1815,13 +1942,13 @@ test.each([
|
|
|
|
|
getSettings: () => ({ workspace: "local" }) as any,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await app.inject({ method: "POST", url: "/sessions", payload: { question: "old" } });
|
|
|
|
|
await app.inject({ method: "POST", url: "/sessions", payload: { interactionLanguage: "en", question: "old" } });
|
|
|
|
|
if (lifecycle === "close") {
|
|
|
|
|
await app.inject({ method: "POST", url: "/sessions/s1/close" });
|
|
|
|
|
await app.inject({ method: "POST", url: "/sessions/s1/resume" });
|
|
|
|
|
} else {
|
|
|
|
|
await app.inject({ method: "DELETE", url: "/sessions/s1" });
|
|
|
|
|
await app.inject({ method: "POST", url: "/sessions", payload: { question: "replacement" } });
|
|
|
|
|
await app.inject({ method: "POST", url: "/sessions", payload: { interactionLanguage: "en", question: "replacement" } });
|
|
|
|
|
}
|
|
|
|
|
await new Promise((resolve) => setImmediate(resolve));
|
|
|
|
|
expect(current).toBe(replacement);
|
|
|
|
@@ -1885,7 +2012,7 @@ test.each(["resolve", "reject"] as const)(
|
|
|
|
|
getSettings: () => ({ workspace: "local" }) as any,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await app.inject({ method: "POST", url: "/sessions", payload: { question: "old" } });
|
|
|
|
|
await app.inject({ method: "POST", url: "/sessions", payload: { interactionLanguage: "en", question: "old" } });
|
|
|
|
|
await app.inject({ method: "DELETE", url: "/sessions/s1" });
|
|
|
|
|
published.length = 0;
|
|
|
|
|
|
|
|
|
@@ -2077,7 +2204,7 @@ test("Close suppresses a bootstrap that settles while close persistence is pendi
|
|
|
|
|
getSettings: () => ({ workspace: "local" }) as any,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await app.inject({ method: "POST", url: "/sessions", payload: { question: "q" } });
|
|
|
|
|
await app.inject({ method: "POST", url: "/sessions", payload: { interactionLanguage: "en", question: "q" } });
|
|
|
|
|
const closeResponse = app.inject({ method: "POST", url: "/sessions/s1/close" });
|
|
|
|
|
await closeStarted;
|
|
|
|
|
published.length = 0;
|
|
|
|
@@ -2151,7 +2278,7 @@ test("bootstrap failure persists once and keeps Resume serialized behind that pe
|
|
|
|
|
getSettings: () => ({ workspace: "local" }) as any,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await app.inject({ method: "POST", url: "/sessions", payload: { question: "q" } });
|
|
|
|
|
await app.inject({ method: "POST", url: "/sessions", payload: { interactionLanguage: "en", question: "q" } });
|
|
|
|
|
oldConfigure.reject(new Error("configure failed"));
|
|
|
|
|
await failureStarted;
|
|
|
|
|
const resumeResponse = app.inject({ method: "POST", url: "/sessions/s1/resume" })
|
|
|
|
@@ -2274,7 +2401,7 @@ test("a replaced runtime cannot publish or fail the newly resumed session", asyn
|
|
|
|
|
readiness: { ensure: async () => ({ ok: true }) } as any,
|
|
|
|
|
getSettings: () => ({ workspace: "local" }) as any,
|
|
|
|
|
});
|
|
|
|
|
await app.inject({ method: "POST", url: "/sessions", payload: { question: "q" } });
|
|
|
|
|
await app.inject({ method: "POST", url: "/sessions", payload: { interactionLanguage: "en", question: "q" } });
|
|
|
|
|
await new Promise((resolve) => setImmediate(resolve));
|
|
|
|
|
oldBridge.setState("idle");
|
|
|
|
|
|
|
|
|
@@ -2334,7 +2461,7 @@ test("a deleted runtime cannot repopulate or fail the forgotten session", async
|
|
|
|
|
readiness: { ensure: async () => ({ ok: true }) } as any,
|
|
|
|
|
getSettings: () => ({ workspace: "local" }) as any,
|
|
|
|
|
});
|
|
|
|
|
await app.inject({ method: "POST", url: "/sessions", payload: { question: "q" } });
|
|
|
|
|
await app.inject({ method: "POST", url: "/sessions", payload: { interactionLanguage: "en", question: "q" } });
|
|
|
|
|
await new Promise((resolve) => setImmediate(resolve));
|
|
|
|
|
|
|
|
|
|
const response = await app.inject({ method: "DELETE", url: "/sessions/s1" });
|
|
|
|
@@ -2379,7 +2506,7 @@ test("an unexpectedly exited runtime publishes its terminal sequence then releas
|
|
|
|
|
readiness: { ensure: async () => ({ ok: true }) } as any,
|
|
|
|
|
getSettings: () => ({ workspace: "local" }) as any,
|
|
|
|
|
});
|
|
|
|
|
await app.inject({ method: "POST", url: "/sessions", payload: { question: "q" } });
|
|
|
|
|
await app.inject({ method: "POST", url: "/sessions", payload: { interactionLanguage: "en", question: "q" } });
|
|
|
|
|
await new Promise((resolve) => setImmediate(resolve));
|
|
|
|
|
published.length = 0;
|
|
|
|
|
|
|
|
|
@@ -2430,7 +2557,7 @@ test("agent_end releases the Pi runtime after the session was finalized", async
|
|
|
|
|
readiness: { ensure: async () => ({ ok: true }) } as any,
|
|
|
|
|
getSettings: () => ({ workspace: "local" }) as any,
|
|
|
|
|
});
|
|
|
|
|
await app.inject({ method: "POST", url: "/sessions", payload: { question: "q" } });
|
|
|
|
|
await app.inject({ method: "POST", url: "/sessions", payload: { interactionLanguage: "en", question: "q" } });
|
|
|
|
|
await new Promise((resolve) => setImmediate(resolve));
|
|
|
|
|
|
|
|
|
|
bridge.emitClientEvent({ type: "system_event", event: "agent_end" });
|
|
|
|
@@ -2502,7 +2629,7 @@ test("POST /sessions/:id/response senza gate pendente risponde 409 (risposta sta
|
|
|
|
|
getSettings: () => ({ workspace: "w" }),
|
|
|
|
|
spawnFn: () => nodeSpawn("node", [FAKE, SCRIPT]) as any,
|
|
|
|
|
});
|
|
|
|
|
await app.inject({ method: "POST", url: "/sessions", payload: { question: "q" } });
|
|
|
|
|
await app.inject({ method: "POST", url: "/sessions", payload: { interactionLanguage: "en", question: "q" } });
|
|
|
|
|
// The fake Pi never emitted a ui_request: the bridge has no pending descriptor, so a
|
|
|
|
|
// response (stale UI, double submit) must be rejected instead of forwarded to Pi.
|
|
|
|
|
const res = await app.inject({ method: "POST", url: "/sessions/s1/response",
|
|
|
|
@@ -2643,7 +2770,7 @@ test("POST /sessions readiness failure returns one fixed public message without
|
|
|
|
|
getSettings: () => ({ workspace: "psd" }) as any,
|
|
|
|
|
spawnFn: () => nodeSpawn("node", [FAKE, SCRIPT]) as any,
|
|
|
|
|
});
|
|
|
|
|
const res = await app.inject({ method: "POST", url: "/sessions", payload: { question: "q" } });
|
|
|
|
|
const res = await app.inject({ method: "POST", url: "/sessions", payload: { interactionLanguage: "en", question: "q" } });
|
|
|
|
|
expect(res.statusCode).toBe(503);
|
|
|
|
|
expect(res.json()).toEqual({
|
|
|
|
|
error: "Session services are not ready. Check configuration and connectivity, then try again.",
|
|
|
|
@@ -2664,7 +2791,7 @@ test.each(["semantic_index_incompatible", "workspace_not_activatable"] as const)
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const response = await app.inject({
|
|
|
|
|
method: "POST", url: "/sessions", payload: { question: "q" },
|
|
|
|
|
method: "POST", url: "/sessions", payload: { interactionLanguage: "en", question: "q" },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
expect(response.statusCode).toBe(503);
|
|
|
|
@@ -2687,7 +2814,7 @@ test("POST /sessions returns storage 503 before creating a Pi runtime when sessi
|
|
|
|
|
mgr: { createFor: () => { piCreated = true; throw new Error("must not spawn"); } } as any,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const response = await app.inject({ method: "POST", url: "/sessions", payload: { question: "q" } });
|
|
|
|
|
const response = await app.inject({ method: "POST", url: "/sessions", payload: { interactionLanguage: "en", question: "q" } });
|
|
|
|
|
|
|
|
|
|
expect(response.statusCode).toBe(503);
|
|
|
|
|
expect(response.json()).toEqual({ error: "session storage is unavailable" });
|
|
|
|
@@ -2707,7 +2834,7 @@ test("POST /sessions proceeds when ollamaEnsure succeeds", async () => {
|
|
|
|
|
getSettings: () => ({ workspace: "psd" }) as any,
|
|
|
|
|
spawnFn: () => nodeSpawn("node", [FAKE, SCRIPT]) as any,
|
|
|
|
|
});
|
|
|
|
|
const res = await app.inject({ method: "POST", url: "/sessions", payload: { question: "q" } });
|
|
|
|
|
const res = await app.inject({ method: "POST", url: "/sessions", payload: { interactionLanguage: "en", question: "q" } });
|
|
|
|
|
expect(res.json()).toEqual({ id: "s1" });
|
|
|
|
|
expect(qdrantEnsure).toHaveBeenCalledWith(operationalWorkspace("psd"), 60, "self_heal");
|
|
|
|
|
expect(ensureWs).toContain(`/snapshots/${"e".repeat(40)}/psd.yaml`);
|
|
|
|
@@ -2740,7 +2867,7 @@ test("POST /sessions rejects a stale requested model without silently using the
|
|
|
|
|
const res = await app.inject({
|
|
|
|
|
method: "POST",
|
|
|
|
|
url: "/sessions",
|
|
|
|
|
payload: { question: "q", provider: "deepseek", model: "deepseek-v4-pro" },
|
|
|
|
|
payload: { interactionLanguage: "en", question: "q", provider: "deepseek", model: "deepseek-v4-pro" },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
expect(res.statusCode).toBe(503);
|
|
|
|
@@ -2775,7 +2902,7 @@ test("POST /sessions marks a persisted session failed when runtime construction
|
|
|
|
|
],
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const res = await app.inject({ method: "POST", url: "/sessions", payload: { question: "q" } });
|
|
|
|
|
const res = await app.inject({ method: "POST", url: "/sessions", payload: { interactionLanguage: "en", question: "q" } });
|
|
|
|
|
|
|
|
|
|
expect(res.statusCode).toBe(503);
|
|
|
|
|
expect(res.json()).toEqual({
|
|
|
|
@@ -2868,7 +2995,7 @@ test.each([
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const response = flow === "new"
|
|
|
|
|
? await app.inject({ method: "POST", url: "/sessions", payload: { question: "q" } })
|
|
|
|
|
? await app.inject({ method: "POST", url: "/sessions", payload: { interactionLanguage: "en", question: "q" } })
|
|
|
|
|
: await app.inject({ method: "POST", url: `/sessions/${sessionId}/resume` });
|
|
|
|
|
const logs = consoleError.mock.calls.flat().map(String).join(" ");
|
|
|
|
|
|
|
|
|
@@ -2974,7 +3101,7 @@ test("POST /sessions returns after bridge attachment but starts only after retri
|
|
|
|
|
getSettings: () => ({ workspace: "psd" }) as any,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const res = await app.inject({ method: "POST", url: "/sessions", payload: { question: "q" } });
|
|
|
|
|
const res = await app.inject({ method: "POST", url: "/sessions", payload: { interactionLanguage: "en", question: "q" } });
|
|
|
|
|
expect(res.json()).toEqual({ id: "s-early" });
|
|
|
|
|
expect(bridgeAttached).toBe(true);
|
|
|
|
|
expect(started).toBe(false);
|
|
|
|
@@ -3021,7 +3148,7 @@ test("POST /sessions bootstrap failure emits only a fixed recovery message", asy
|
|
|
|
|
const response = await app.inject({
|
|
|
|
|
method: "POST",
|
|
|
|
|
url: "/sessions",
|
|
|
|
|
payload: { question: "q" },
|
|
|
|
|
payload: { interactionLanguage: "en", question: "q" },
|
|
|
|
|
});
|
|
|
|
|
await new Promise((resolve) => setImmediate(resolve));
|
|
|
|
|
|
|
|
|
@@ -3117,7 +3244,7 @@ test.each([
|
|
|
|
|
})),
|
|
|
|
|
} as any,
|
|
|
|
|
});
|
|
|
|
|
const response = await app.inject({ method: "POST", url: "/sessions", payload: { question: "q" } });
|
|
|
|
|
const response = await app.inject({ method: "POST", url: "/sessions", payload: { interactionLanguage: "en", question: "q" } });
|
|
|
|
|
|
|
|
|
|
expect(response.statusCode).toBe(expectedStatus);
|
|
|
|
|
expect(ensure).toHaveBeenCalledTimes(reachesReadiness ? 1 : 0);
|
|
|
|
|