Files
Symphony AgentandCodex 3f1e88ca9e feat(server): add run control and presets
Summary:
- Add authenticated server write routes for starting and cancelling runs,
  preset CRUD, preset launch history, and frozen preset snapshots.
- Extend SQLite run history to schema v4 with preset tables, server-run
  metadata, cancellation timestamps, and WAL-enabled server connections.
- Teach runSuite to accept prepared file/id scenario selections and a
  cooperative AbortSignal while preserving existing CLI and dashboard mode.
- Add dashboard start/preset/cancel views plus Docker and Compose packaging
  with a token-protected SQLite volume deployment path.

Rationale:
- Phase 2 needs an operator workflow that can configure a run, save it as a
  reusable preset, launch it from the server UI, and observe/cancel progress.
- Scenario references now use file plus id so duplicate ids across scenario
  files remain deterministic, and write paths validate boundaries at data-root
  and bearer-auth edges.

Tests:
- bun run docs:validate
- bun run test tests/unit/server
- bun run test tests/integration/server
- bun run test:e2e
- bun run dashboard:build
- bun run typecheck
- bun run fast-feedback
- COMPOSE_PROGRESS=plain AGENTPROBE_SERVER_TOKEN=... OPEN_ROUTER_API_KEY=...
  docker compose -p agentprobe-sym22 up --build --detach
- curl -fsS http://127.0.0.1:7878/healthz
- curl -fsS -X POST http://127.0.0.1:7878/api/runs ... dry-run payload
- OPEN_ROUTER_API_KEY=... docker compose -p agentprobe-sym22-missing config
  exited 1 for missing AGENTPROBE_SERVER_TOKEN

Co-authored-by: Codex <[email protected]>
2026-04-17 12:59:23 +00:00

90 lines
2.7 KiB
TypeScript

import { describe, expect, test } from "bun:test";
import { mkdirSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { SuiteController } from "../../../src/runtime/server/controllers/suite-controller.ts";
import { makeTempDir } from "../support.ts";
function writeSharedFiles(root: string): void {
writeFileSync(
join(root, "personas.yaml"),
[
"personas:",
" - id: analyst",
" name: Analyst",
" demographics:",
" role: operator",
" tech_literacy: high",
" domain_expertise: intermediate",
" language_style: terse",
" personality:",
" patience: 3",
" assertiveness: 3",
" detail_orientation: 4",
" cooperativeness: 4",
" emotional_intensity: 1",
" behavior:",
" opening_style: Direct.",
" follow_up_style: Concise.",
" escalation_triggers: []",
" topic_drift: none",
" clarification_compliance: high",
" system_prompt: You are direct.",
"",
].join("\n"),
"utf8",
);
}
function scenarioDoc(name: string): string {
return [
"scenarios:",
" - id: duplicate",
` name: ${name}`,
" tags: [smoke]",
" persona: analyst",
" rubric: support",
" turns:",
" - role: user",
" content: Say hello.",
" use_exact_message: true",
" expectations:",
" expected_behavior: Greets the user.",
"",
].join("\n");
}
describe("server scenario selection", () => {
test("resolves duplicate scenario ids by file and id", () => {
const root = makeTempDir("server-selection");
mkdirSync(root, { recursive: true });
writeSharedFiles(root);
writeFileSync(join(root, "a.yaml"), scenarioDoc("Alpha"), "utf8");
writeFileSync(join(root, "b.yaml"), scenarioDoc("Beta"), "utf8");
const controller = new SuiteController({ dataPath: root });
const resolved = controller.resolveSelection([
{ file: "a.yaml", id: "duplicate" },
{ file: "b.yaml", id: "duplicate" },
]);
expect(resolved.refs).toEqual([
{ file: "a.yaml", id: "duplicate" },
{ file: "b.yaml", id: "duplicate" },
]);
expect(resolved.selectedScenarios.map((scenario) => scenario.name)).toEqual(
["Alpha", "Beta"],
);
});
test("rejects scenario selection paths outside the data root", () => {
const root = makeTempDir("server-selection-outside");
mkdirSync(root, { recursive: true });
const controller = new SuiteController({ dataPath: root });
expect(() =>
controller.resolveSelection([{ file: "../outside.yaml", id: "x" }]),
).toThrow(/data root/);
});
});