wave 2 (platform stream): settings storage & plumbing foundation (P2)

- src/core/settings.ts: versioned DELTA record schema + sanitizer (unknown keys preserved)
  — general/chat/agent{models,tools,prompts}/appearance/graph sections
- src/core/settings-resolution.ts: pure resolveSettings (factory defaults → user delta),
  extraction/graph-context channel predicates
- src/store/settings.ts + prompt-overrides.ts: SettingsStore/PromptOverrideStore adapters
  wired through stores.ts (effectiveSettings() fresh per call); lazy migration of
  default-settings.json into record.chat
- app/api/settings/** routes (record, section bodies, prompt overrides per segment)
- chat-handler: graph gates at trigger call-sites + toolToggles injection seam
- config.ts: SETTINGS_FILE + PROMPTS_OVERRIDE_DIR paths

587/587 tests; typecheck/lint/build clean
This commit is contained in:
amosblomqvist
2026-09-19 18:05:30 +03:00
parent 36c5d19437
commit cb4fe25c89
24 changed files with 2096 additions and 101 deletions
+8 -51
View File
@@ -1,20 +1,17 @@
/**
* Tests for the default-settings wave (2026-09-18): the shared settings-body
* parser (the one seam behind BOTH settings routes — per-chat and defaults —
* so their parse behavior is identical) and the default-settings store
* (data/default-settings.json read/write, first-boot null semantics).
* Tests for the shared settings-body parser, chat-section flavor
* (default-settings wave, 2026-09-18; record-backed 2026-09-19): the seam
* behind BOTH chat-settings routes — per-chat and the defaults alias.
*
* The store tests write the real repo data file (npm test's shared data/,
* disposable scratch by doctrine) and clean up after themselves.
* The record store, the legacy-file migration, and the record-backed
* default-settings facade are covered in tests/settings-store.test.ts
* (one fs owner per file — the test runner executes files in parallel).
*/
import assert from "node:assert/strict";
import fs from "node:fs";
import { afterEach, describe, it } from "node:test";
import { GRAPH_CONTEXT_TOKENS_MAX, type ChatSettings } from "../src/core";
import { describe, it } from "node:test";
import { GRAPH_CONTEXT_TOKENS_MAX } from "../src/core";
import { parseSettingsBody } from "../src/server/settings-body.ts";
import { DEFAULT_SETTINGS_FILE } from "../config.ts";
import { readDefaultSettings, writeDefaultSettings } from "../src/store/default-settings.ts";
describe("parseSettingsBody (shared by both settings routes)", () => {
it("an empty object parses to {} — the clear-overrides write", () => {
@@ -66,43 +63,3 @@ describe("parseSettingsBody (shared by both settings routes)", () => {
assert.equal(ok.ok, true);
});
});
describe("the default-settings store", () => {
afterEach(() => {
try {
fs.rmSync(DEFAULT_SETTINGS_FILE);
} catch {
/* nothing to clean */
}
});
it("reads null on first boot (no file) — the surfaces use config defaults", () => {
try {
fs.rmSync(DEFAULT_SETTINGS_FILE);
} catch {
/* absent already */
}
assert.equal(readDefaultSettings(), null);
});
it("write then read roundtrips verbatim — validated shape, no sanitizer games", () => {
const settings: ChatSettings = {
model: { provider: "openrouter", id: "z-ai/glm-5.3-flash" },
thinkingLevel: "high",
graphContextTokens: 1024,
};
writeDefaultSettings(settings);
assert.deepEqual(readDefaultSettings(), settings);
});
it("an empty {} record roundtrips (all overrides cleared)", () => {
writeDefaultSettings({});
assert.deepEqual(readDefaultSettings(), {});
});
it("a corrupt file reads as null — defaults, never a crash", () => {
fs.rmSync(DEFAULT_SETTINGS_FILE, { force: true }); // a directory from a broken earlier run also blocks
fs.writeFileSync(DEFAULT_SETTINGS_FILE, "{ not json", "utf-8");
assert.equal(readDefaultSettings(), null);
});
});