per-session models: every session owns its model + thinking level from birth
The reported bug (2026-09-28): every chat ran glm-5.3-flash regardless of any pick, while composer and settings UIs agreed with each other and lied about the run. Root cause: a fossil default-settings record (settings.json chat section, frozen pre-slots-wave) was stamped verbatim into every new chat at birth by the new-session flow, and resolveConversationSpec prefers the per-chat record over the strong slot forever. Owner redesign — models are per session, always; the strong slot is the new-session default: - POST /api/chats stamps the newborn verbatim from the strong slot (model + thinking level); no client settings PUT on create anymore - the composer control is dual-bound: in-session it reads/writes the session's own record (GET /api/chats/:id, PUT .../settings); on the draft surfaces it binds the strong slot (picking there = changing the default) - thinking levels follow the identical mechanics (confirmed by owner) - the zombie default-settings system dies whole: useDefaultSettings, /api/settings/defaults, src/store/default-settings.ts, the chat section of UserSettings (sanitizer strips fossil copies), the legacy default-settings.json fold, and DEFAULT_SETTINGS_FILE - catalogDefaultsFor collapses to the strong slot alone - thinking display for a legacy unstamped session resolves exactly like the engine (session ?? strong ?? model ladder) Tests: new chat-birth-stamp suite pins the stamp, the unconfigured boot, the unset level, and the copy-not-live-view regression; defaults pins rewritten strong-only; legacy-migration suite deleted.
This commit is contained in:
@@ -5,7 +5,6 @@ import { renderToStaticMarkup } from "react-dom/server";
|
||||
import { describe, it } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { DraftShell, startDraftSession } from "../components/new-session.tsx";
|
||||
import type { ChatSettings } from "../src/core/index.ts";
|
||||
|
||||
/**
|
||||
* The sessionless draft view (/chat/new, owner 2026-09-13): "+ New session"
|
||||
@@ -28,7 +27,7 @@ function fakeFetch(id: string) {
|
||||
describe("startDraftSession — the create-on-send contract", () => {
|
||||
it("creates the session titled with the first message, returns the prompt redirect", async () => {
|
||||
const { f, calls } = fakeFetch("c_new");
|
||||
const target = await startDraftSession("teach me orbital mechanics", undefined, f);
|
||||
const target = await startDraftSession("teach me orbital mechanics", f);
|
||||
assert.equal(target, "/chat/c_new?prompt=teach%20me%20orbital%20mechanics");
|
||||
assert.equal(calls.length, 1);
|
||||
assert.equal(calls[0]!.url, "/api/chats");
|
||||
@@ -36,26 +35,16 @@ describe("startDraftSession — the create-on-send contract", () => {
|
||||
assert.deepEqual(calls[0]!.body, { title: "teach me orbital mechanics" });
|
||||
});
|
||||
|
||||
it("persists the lifted pre-chat settings after creation", async () => {
|
||||
const { f, calls } = fakeFetch("c_2");
|
||||
const settings: ChatSettings = { model: { provider: "openrouter", id: "qwen/qwen3.8-flash" }, thinkingLevel: "medium" };
|
||||
await startDraftSession("hello", settings, f);
|
||||
assert.equal(calls.length, 2);
|
||||
assert.equal(calls[1]!.url, "/api/chats/c_2/settings");
|
||||
assert.equal(calls[1]!.method, "PUT");
|
||||
assert.deepEqual(calls[1]!.body, settings);
|
||||
});
|
||||
|
||||
it("no settings lift → no settings write (a single create call)", async () => {
|
||||
it("no settings PUT on create — the server stamps the newborn (per-session models, owner 2026-09-28)", async () => {
|
||||
const { f, calls } = fakeFetch("c_3");
|
||||
await startDraftSession("hello", undefined, f);
|
||||
assert.equal(calls.length, 1);
|
||||
await startDraftSession("hello", f);
|
||||
assert.equal(calls.length, 1, "a single create call — the birth stamp is server-side");
|
||||
});
|
||||
|
||||
it("empty or whitespace text never creates anything", async () => {
|
||||
const { f, calls } = fakeFetch("never");
|
||||
assert.equal(await startDraftSession("", undefined, f), null);
|
||||
assert.equal(await startDraftSession(" ", undefined, f), null);
|
||||
assert.equal(await startDraftSession("", f), null);
|
||||
assert.equal(await startDraftSession(" ", f), null);
|
||||
assert.equal(calls.length, 0);
|
||||
});
|
||||
});
|
||||
@@ -68,14 +57,12 @@ describe("DraftShell — the sessionless surface", () => {
|
||||
text=""
|
||||
onChange={() => {}}
|
||||
onSend={() => {}}
|
||||
settings={undefined}
|
||||
onSettingsSaved={() => {}}
|
||||
/>,
|
||||
);
|
||||
// The chat composer, ready for the first message.
|
||||
assert.ok(html.includes('data-testid="composer"'));
|
||||
// The pre-chat model control (fix wave: the strong-slot control —
|
||||
// app-wide, no per-chat write, no empty-chatId defer).
|
||||
// The draft's model control binds the strong slot — the
|
||||
// new-session default (per-session models, owner 2026-09-28).
|
||||
assert.ok(html.includes('data-testid="stats-model"'));
|
||||
// No CTA by default: a configured surface stays quiet.
|
||||
assert.ok(!html.includes('data-testid="setup-cta"'));
|
||||
@@ -94,8 +81,6 @@ describe("DraftShell — the sessionless surface", () => {
|
||||
text=""
|
||||
onChange={() => {}}
|
||||
onSend={() => {}}
|
||||
settings={undefined}
|
||||
onSettingsSaved={() => {}}
|
||||
setupCta={
|
||||
<button type="button" data-testid="setup-cta">
|
||||
Set up your models — chats start after the three picks
|
||||
@@ -118,8 +103,6 @@ describe("DraftShell — the sessionless surface", () => {
|
||||
text=""
|
||||
onChange={() => {}}
|
||||
onSend={() => {}}
|
||||
settings={undefined}
|
||||
onSettingsSaved={() => {}}
|
||||
/>,
|
||||
);
|
||||
assert.ok(!/\/chat\/c[a-z0-9_]+/.test(html));
|
||||
|
||||
Reference in New Issue
Block a user