feat(task-10): SQL routes + /workspaces + /models + fix sql preview path bug

Harness:
- preview_cmd FILE positional arg made optional; when omitted with --session,
  path is derived via _session_sql_file (mirrors export_cmd) — fixes the
  deferred Task-5 bug where the backend passed sessions/<id>/sql_final.sql
  relative to harnessDir, which broke for workspace-dependent paths.
- New pytest: test_preview_session_no_file_resolves_sql_final

Backend:
- ThtRunner.sqlPreview: drop positional file arg; use --session only
- New routes/sql.ts: POST /sessions/:id/sql/preview + /export
- New routes/meta.ts: GET /workspaces (yaml scan) + GET /models (injectable
  seam + graceful fallback to {models:[]})
- app.ts: register sqlRoutes + metaRoutes; add listModels to BuildAppDeps
- tht-runner.test.ts: add sqlPreview argv assertion (no file path)
- test/routes-sql-meta.test.ts: 9 tests (sql preview/export + meta routes)

Tests: harness 233 passed; backend 29 passed; build clean.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-27 21:31:35 +02:00
co-authored by Claude Sonnet 4.6
parent 061002f85c
commit d630cac3ab
8 changed files with 330 additions and 4 deletions
+25
View File
@@ -39,3 +39,28 @@ test("sessionNew with missing workspace file falls back to default configPath ar
expect(bin).toBe("tht");
expect(argv).toEqual(["-c", "config/tht.yaml", "session", "new", "q", "--json"]);
});
test("sqlPreview argv has no positional file — uses --session to resolve path", async () => {
// The harness preview_cmd now resolves sql_final.sql from the session workspace;
// the backend must NOT pass a sessions/<id>/sql_final.sql positional arg.
(spawn as any).mockClear();
const r = new ThtRunner({ thtBin: "tht", harnessDir: "/nope", configPath: "config/tht.yaml" });
// stub json() via run() — just need spawn call captured
r.run = async () => ({ code: 0, stdout: '{"columns":[],"rows":[],"execution_ms":1,"truncated":false}', stderr: "" });
await r.sqlPreview("ses1", { limit: 10, offset: 5 });
// Verify via the patched run — we stub run() so spawn isn't called again.
// Instead confirm directly that sqlPreview builds the right args by inspecting run calls.
// We swap back to a spy on run itself.
const runSpy = vi.fn().mockResolvedValue({
code: 0,
stdout: '{"columns":["c"],"rows":[[1]],"execution_ms":2,"truncated":false}',
stderr: "",
});
const r2 = new ThtRunner({ thtBin: "tht", harnessDir: "/nope", configPath: "config/tht.yaml" });
r2.run = runSpy;
await r2.sqlPreview("ses2", { limit: 20, offset: 0 });
const [calledArgs] = runSpy.mock.calls[0];
// Must NOT include any positional file path before --session
expect(calledArgs).toEqual(["sql", "preview", "--session", "ses2", "--json", "--limit", "20", "--offset", "0"]);
expect(calledArgs).not.toContain("sessions/ses2/sql_final.sql");
});