fix: lock main session deletion
This commit is contained in:
@@ -6,6 +6,7 @@
|
||||
- Identifiers: rename bundle IDs and internal domains to `com.clawdis.*` (macOS: `com.clawdis.mac`, iOS: `com.clawdis.ios`, Android: `com.clawdis.android`) and update the gateway LaunchAgent label to `com.clawdis.gateway`.
|
||||
- Agent tools: drop the `clawdis_` prefix (`browser`, `canvas`, `nodes`, `cron`, `gateway`).
|
||||
- Bash tool: remove `stdinMode: "pty"`/node-pty support; use the tmux skill for real TTYs.
|
||||
- Sessions: primary session key is fixed to `main` (or `global` for global scope); `session.mainKey` is ignored.
|
||||
|
||||
### Features
|
||||
- Gateway: support `gateway.port` + `CLAWDIS_GATEWAY_PORT` across CLI, TUI, and macOS app.
|
||||
@@ -40,6 +41,7 @@
|
||||
- Agent tools: emit verbose tool summaries at tool start (no debounce).
|
||||
- Gateway: split server helpers/tests into hooks/session-utils/ws-log/net modules for better isolation; add unit coverage for hooks/session utils/ws log.
|
||||
- Gateway: extract WS method handling + HTTP/provider/constant helpers to shrink server wiring and improve testability.
|
||||
- Gateway: prevent deleting the main session and abort active runs before deleting other sessions.
|
||||
- Onboarding: fix Control UI basePath usage when showing/opening gateway URLs.
|
||||
- Onboarding: clarify provider requirements (WhatsApp/Signal phone numbers, iMessage Apple ID guidance) in the provider picker.
|
||||
- macOS Connections: move to sidebar + detail layout with structured sections and header actions.
|
||||
|
||||
@@ -306,6 +306,7 @@ extension GatewayConnection {
|
||||
struct SnapshotConfig: Decodable, Sendable {
|
||||
struct Session: Decodable, Sendable {
|
||||
let mainKey: String?
|
||||
let scope: String?
|
||||
}
|
||||
|
||||
let session: Session?
|
||||
@@ -316,9 +317,11 @@ extension GatewayConnection {
|
||||
|
||||
static func mainSessionKey(fromConfigGetData data: Data) throws -> String {
|
||||
let snapshot = try JSONDecoder().decode(ConfigGetSnapshot.self, from: data)
|
||||
let raw = snapshot.config?.session?.mainKey
|
||||
let trimmed = (raw ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
return trimmed.isEmpty ? "main" : trimmed
|
||||
let scope = snapshot.config?.session?.scope?.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if scope == "global" {
|
||||
return "global"
|
||||
}
|
||||
return "main"
|
||||
}
|
||||
|
||||
func mainSessionKey(timeoutMs: Double = 15000) async -> String {
|
||||
|
||||
@@ -435,7 +435,7 @@ final class MenuSessionsInjector: NSObject, NSMenuDelegate {
|
||||
compact.representedObject = row.key
|
||||
menu.addItem(compact)
|
||||
|
||||
if row.key != "main" {
|
||||
if row.key != "main" && row.key != "global" {
|
||||
let del = NSMenuItem(title: "Delete Session", action: #selector(self.deleteSession(_:)), keyEquivalent: "")
|
||||
del.target = self
|
||||
del.representedObject = row.key
|
||||
|
||||
@@ -32,7 +32,7 @@ import Testing
|
||||
}
|
||||
"""
|
||||
let key = try GatewayConnection.mainSessionKey(fromConfigGetData: Data(json.utf8))
|
||||
#expect(key == "primary")
|
||||
#expect(key == "main")
|
||||
}
|
||||
|
||||
@Test func configGetSnapshotMainKeyFallsBackWhenEmptyOrWhitespace() throws {
|
||||
@@ -54,4 +54,14 @@ import Testing
|
||||
let key = try GatewayConnection.mainSessionKey(fromConfigGetData: Data(json.utf8))
|
||||
#expect(key == "main")
|
||||
}
|
||||
|
||||
@Test func configGetSnapshotUsesGlobalScope() throws {
|
||||
let json = """
|
||||
{
|
||||
"config": { "session": { "scope": "global" } }
|
||||
}
|
||||
"""
|
||||
let key = try GatewayConnection.mainSessionKey(fromConfigGetData: Data(json.utf8))
|
||||
#expect(key == "global")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ read_when:
|
||||
- Required: `--message <text>`
|
||||
- Session selection:
|
||||
- If `--session-id` is given, reuse it.
|
||||
- Else if `--to <e164>` is given, derive the session key from `session.scope` (direct chats collapse to `session.mainKey`).
|
||||
- Else if `--to <e164>` is given, derive the session key from `session.scope` (direct chats collapse to `main`, or `global` when scope is global).
|
||||
- Runs the embedded Pi agent (configured via `agent`).
|
||||
- Thinking/verbose:
|
||||
- Flags `--thinking <off|minimal|low|medium|high>` and `--verbose <on|off>` persist into the session store.
|
||||
|
||||
@@ -138,8 +138,7 @@ Example:
|
||||
session: {
|
||||
scope: "per-sender",
|
||||
resetTriggers: ["/new", "/reset"],
|
||||
idleMinutes: 10080,
|
||||
mainKey: "main"
|
||||
idleMinutes: 10080
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -620,7 +620,7 @@ Controls session scoping, idle expiry, reset triggers, and where the session sto
|
||||
idleMinutes: 60,
|
||||
resetTriggers: ["/new", "/reset"],
|
||||
store: "~/.clawdis/sessions/sessions.json",
|
||||
mainKey: "main",
|
||||
// mainKey is ignored; primary key is fixed to "main"
|
||||
sendPolicy: {
|
||||
rules: [
|
||||
{ action: "deny", match: { surface: "discord", chatType: "group" } }
|
||||
|
||||
@@ -50,7 +50,7 @@ message. If the reply is only `HEARTBEAT_OK`, it is dropped.
|
||||
- `prompt`: optional override for the heartbeat body (default: `HEARTBEAT`).
|
||||
|
||||
## Behavior
|
||||
- Runs in the main session (`session.mainKey`, or `global` when scope is global).
|
||||
- Runs in the main session (`main`, or `global` when scope is global).
|
||||
- Uses the main lane queue; if requests are in flight, the wake is retried.
|
||||
- Empty output or `HEARTBEAT_OK` is treated as “ok” and does **not** keep the
|
||||
session alive (`updatedAt` is restored).
|
||||
|
||||
@@ -5,7 +5,7 @@ read_when:
|
||||
---
|
||||
# Web Chat (macOS app)
|
||||
|
||||
The macOS menu bar app shows the WebChat UI as a native SwiftUI view and reuses the **primary Clawd session** (`main` by default, configurable via `session.mainKey`).
|
||||
The macOS menu bar app shows the WebChat UI as a native SwiftUI view and reuses the **primary Clawd session** (`main`, or `global` when scope is global).
|
||||
|
||||
- **Local mode**: connects directly to the local Gateway WebSocket.
|
||||
- **Remote mode**: forwards the Gateway WebSocket control port over SSH and uses that as the data plane.
|
||||
|
||||
@@ -5,7 +5,7 @@ read_when:
|
||||
---
|
||||
# Session Management
|
||||
|
||||
Clawdis treats **one session as primary**. By default the canonical key is `main` for every direct chat; no configuration is required. You can rename it via `session.mainKey` if you really want, but there is still only a single primary session. Older/local sessions can stay on disk, but only the primary key is used for desktop/web chat and direct agent calls.
|
||||
Clawdis treats **one session as primary**. The canonical key is fixed to `main` for direct chats (or `global` when scope is global); no configuration is required. `session.mainKey` is ignored. Older/local sessions can stay on disk, but only the primary key is used for desktop/web chat and direct agent calls.
|
||||
|
||||
## Gateway is the source of truth
|
||||
All session state is **owned by the gateway** (the “master” Clawdis). UI clients (macOS app, WebChat, etc.) must query the gateway for session lists and token counts instead of reading local files.
|
||||
@@ -67,7 +67,7 @@ Runtime override (owner only):
|
||||
idleMinutes: 120,
|
||||
resetTriggers: ["/new", "/reset"],
|
||||
store: "~/.clawdis/sessions/sessions.json",
|
||||
mainKey: "main" // optional rename; still a single primary
|
||||
// mainKey is ignored; primary key is fixed to "main"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -28,7 +28,7 @@ Use SSH tunneling or Tailscale to reach the Gateway WS.
|
||||
- `--url <url>`: Gateway WebSocket URL (defaults to config `gateway.remote.url` or `ws://127.0.0.1:18789`).
|
||||
- `--token <token>`: Gateway token (if required).
|
||||
- `--password <password>`: Gateway password (if required).
|
||||
- `--session <key>`: Session key (default: `session.mainKey` or `main`).
|
||||
- `--session <key>`: Session key (default: `main`, or `global` when scope is global).
|
||||
- `--deliver`: Deliver assistant replies to the provider (default off).
|
||||
- `--thinking <level>`: Override thinking level for sends.
|
||||
- `--timeout-ms <ms>`: Agent timeout in ms (default 30000).
|
||||
|
||||
@@ -98,6 +98,11 @@ type EmbeddedPiQueueHandle = {
|
||||
const log = createSubsystemLogger("agent/embedded");
|
||||
|
||||
const ACTIVE_EMBEDDED_RUNS = new Map<string, EmbeddedPiQueueHandle>();
|
||||
type EmbeddedRunWaiter = {
|
||||
resolve: (ended: boolean) => void;
|
||||
timer: NodeJS.Timeout;
|
||||
};
|
||||
const EMBEDDED_RUN_WAITERS = new Map<string, Set<EmbeddedRunWaiter>>();
|
||||
|
||||
const OAUTH_FILENAME = "oauth.json";
|
||||
const DEFAULT_OAUTH_DIR = path.join(CONFIG_DIR, "credentials");
|
||||
@@ -247,6 +252,43 @@ export function isEmbeddedPiRunStreaming(sessionId: string): boolean {
|
||||
return handle.isStreaming();
|
||||
}
|
||||
|
||||
export function waitForEmbeddedPiRunEnd(
|
||||
sessionId: string,
|
||||
timeoutMs = 15_000,
|
||||
): Promise<boolean> {
|
||||
if (!sessionId || !ACTIVE_EMBEDDED_RUNS.has(sessionId))
|
||||
return Promise.resolve(true);
|
||||
return new Promise((resolve) => {
|
||||
const waiters = EMBEDDED_RUN_WAITERS.get(sessionId) ?? new Set();
|
||||
const waiter: EmbeddedRunWaiter = {
|
||||
resolve,
|
||||
timer: setTimeout(() => {
|
||||
waiters.delete(waiter);
|
||||
if (waiters.size === 0) EMBEDDED_RUN_WAITERS.delete(sessionId);
|
||||
resolve(false);
|
||||
}, Math.max(100, timeoutMs)),
|
||||
};
|
||||
waiters.add(waiter);
|
||||
EMBEDDED_RUN_WAITERS.set(sessionId, waiters);
|
||||
if (!ACTIVE_EMBEDDED_RUNS.has(sessionId)) {
|
||||
waiters.delete(waiter);
|
||||
if (waiters.size === 0) EMBEDDED_RUN_WAITERS.delete(sessionId);
|
||||
clearTimeout(waiter.timer);
|
||||
resolve(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function notifyEmbeddedRunEnded(sessionId: string) {
|
||||
const waiters = EMBEDDED_RUN_WAITERS.get(sessionId);
|
||||
if (!waiters || waiters.size === 0) return;
|
||||
EMBEDDED_RUN_WAITERS.delete(sessionId);
|
||||
for (const waiter of waiters) {
|
||||
clearTimeout(waiter.timer);
|
||||
waiter.resolve(true);
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveEmbeddedSessionLane(key: string) {
|
||||
return resolveSessionLane(key);
|
||||
}
|
||||
@@ -602,6 +644,7 @@ export async function runEmbeddedPiAgent(params: {
|
||||
unsubscribe();
|
||||
if (ACTIVE_EMBEDDED_RUNS.get(params.sessionId) === queueHandle) {
|
||||
ACTIVE_EMBEDDED_RUNS.delete(params.sessionId);
|
||||
notifyEmbeddedRunEnded(params.sessionId);
|
||||
}
|
||||
session.dispose();
|
||||
params.abortSignal?.removeEventListener?.("abort", onAbort);
|
||||
|
||||
@@ -8,6 +8,7 @@ export {
|
||||
isEmbeddedPiRunActive,
|
||||
isEmbeddedPiRunStreaming,
|
||||
queueEmbeddedPiMessage,
|
||||
waitForEmbeddedPiRunEnd,
|
||||
resolveEmbeddedSessionLane,
|
||||
runEmbeddedPiAgent,
|
||||
} from "./pi-embedded-runner.js";
|
||||
|
||||
@@ -14,7 +14,7 @@ export function registerTuiCli(program: Command) {
|
||||
.option("--password <password>", "Gateway password (if required)")
|
||||
.option(
|
||||
"--session <key>",
|
||||
"Session key (default: session.mainKey from config)",
|
||||
'Session key (default: "main", or "global" when scope is global)',
|
||||
)
|
||||
.option("--deliver", "Deliver assistant replies", false)
|
||||
.option("--thinking <level>", "Thinking level override")
|
||||
|
||||
@@ -15,6 +15,7 @@ import { parseDurationMs } from "../cli/parse-duration.js";
|
||||
* - Config is managed externally (read-only from Nix perspective)
|
||||
*/
|
||||
export const isNixMode = process.env.CLAWDIS_NIX_MODE === "1";
|
||||
let warnedMainKeyOverride = false;
|
||||
|
||||
export type ReplyMode = "text" | "command";
|
||||
export type SessionScope = "per-sender" | "global";
|
||||
@@ -1771,6 +1772,22 @@ function applyIdentityDefaults(cfg: ClawdisConfig): ClawdisConfig {
|
||||
return mutated ? next : cfg;
|
||||
}
|
||||
|
||||
function applySessionDefaults(cfg: ClawdisConfig): ClawdisConfig {
|
||||
const session = cfg.session;
|
||||
if (!session || session.mainKey === undefined) return cfg;
|
||||
|
||||
const trimmed = session.mainKey.trim();
|
||||
const next: ClawdisConfig = { ...cfg, session: { ...session } };
|
||||
next.session.mainKey = "main";
|
||||
|
||||
if (trimmed && trimmed !== "main" && !warnedMainKeyOverride) {
|
||||
warnedMainKeyOverride = true;
|
||||
console.warn('session.mainKey is ignored; main session is always "main".');
|
||||
}
|
||||
|
||||
return next;
|
||||
}
|
||||
|
||||
export function loadConfig(): ClawdisConfig {
|
||||
// Read config file (JSON5) if present.
|
||||
const configPath = CONFIG_PATH_CLAWDIS;
|
||||
@@ -1787,7 +1804,9 @@ export function loadConfig(): ClawdisConfig {
|
||||
}
|
||||
return {};
|
||||
}
|
||||
return applyIdentityDefaults(validated.data as ClawdisConfig);
|
||||
return applySessionDefaults(
|
||||
applyIdentityDefaults(validated.data as ClawdisConfig),
|
||||
);
|
||||
} catch (err) {
|
||||
console.error(`Failed to read config at ${configPath}`, err);
|
||||
return {};
|
||||
@@ -1821,7 +1840,9 @@ export function validateConfigObject(
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
config: applyIdentityDefaults(validated.data as ClawdisConfig),
|
||||
config: applySessionDefaults(
|
||||
applyIdentityDefaults(validated.data as ClawdisConfig),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1880,7 +1901,7 @@ export async function readConfigFileSnapshot(): Promise<ConfigFileSnapshot> {
|
||||
const configPath = CONFIG_PATH_CLAWDIS;
|
||||
const exists = fs.existsSync(configPath);
|
||||
if (!exists) {
|
||||
const config = applyTalkApiKey({});
|
||||
const config = applyTalkApiKey(applySessionDefaults({}));
|
||||
const legacyIssues: LegacyConfigIssue[] = [];
|
||||
return {
|
||||
path: configPath,
|
||||
@@ -1934,7 +1955,7 @@ export async function readConfigFileSnapshot(): Promise<ConfigFileSnapshot> {
|
||||
raw,
|
||||
parsed: parsedRes.parsed,
|
||||
valid: true,
|
||||
config: applyTalkApiKey(validated.config),
|
||||
config: applyTalkApiKey(applySessionDefaults(validated.config)),
|
||||
issues: [],
|
||||
legacyIssues,
|
||||
};
|
||||
|
||||
@@ -103,6 +103,13 @@ export function resolveStorePath(store?: string) {
|
||||
return path.resolve(store);
|
||||
}
|
||||
|
||||
export function resolveMainSessionKey(
|
||||
cfg?: { session?: { scope?: SessionScope; mainKey?: string } },
|
||||
): string {
|
||||
if (cfg?.session?.scope === "global") return "global";
|
||||
return "main";
|
||||
}
|
||||
|
||||
function normalizeGroupLabel(raw?: string) {
|
||||
const trimmed = raw?.trim().toLowerCase() ?? "";
|
||||
if (!trimmed) return "";
|
||||
|
||||
@@ -2,6 +2,12 @@ import { randomUUID } from "node:crypto";
|
||||
import fs from "node:fs";
|
||||
import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "../agents/defaults.js";
|
||||
import type { ModelCatalogEntry } from "../agents/model-catalog.js";
|
||||
import {
|
||||
abortEmbeddedPiRun,
|
||||
isEmbeddedPiRunActive,
|
||||
resolveEmbeddedSessionLane,
|
||||
waitForEmbeddedPiRunEnd,
|
||||
} from "../agents/pi-embedded.js";
|
||||
import {
|
||||
buildAllowedModelSet,
|
||||
buildModelAliasIndex,
|
||||
@@ -29,6 +35,7 @@ import {
|
||||
import { buildConfigSchema } from "../config/schema.js";
|
||||
import {
|
||||
loadSessionStore,
|
||||
resolveMainSessionKey,
|
||||
resolveStorePath,
|
||||
type SessionEntry,
|
||||
saveSessionStore,
|
||||
@@ -38,6 +45,7 @@ import {
|
||||
setVoiceWakeTriggers,
|
||||
} from "../infra/voicewake.js";
|
||||
import { defaultRuntime } from "../runtime.js";
|
||||
import { clearCommandLane } from "../process/command-queue.js";
|
||||
import { normalizeSendPolicy } from "../sessions/send-policy.js";
|
||||
import { buildMessageWithAttachments } from "./chat-attachments.js";
|
||||
import {
|
||||
@@ -569,12 +577,37 @@ export function createBridgeHandlers(ctx: BridgeHandlersContext) {
|
||||
};
|
||||
}
|
||||
|
||||
const mainKey = resolveMainSessionKey(loadConfig());
|
||||
if (key === mainKey) {
|
||||
return {
|
||||
ok: false,
|
||||
error: {
|
||||
code: ErrorCodes.INVALID_REQUEST,
|
||||
message: `Cannot delete the main session (${mainKey}).`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const deleteTranscript =
|
||||
typeof p.deleteTranscript === "boolean" ? p.deleteTranscript : true;
|
||||
|
||||
const { storePath, store, entry } = loadSessionEntry(key);
|
||||
const sessionId = entry?.sessionId;
|
||||
const existed = Boolean(store[key]);
|
||||
clearCommandLane(resolveEmbeddedSessionLane(key));
|
||||
if (sessionId && isEmbeddedPiRunActive(sessionId)) {
|
||||
abortEmbeddedPiRun(sessionId);
|
||||
const ended = await waitForEmbeddedPiRunEnd(sessionId, 15_000);
|
||||
if (!ended) {
|
||||
return {
|
||||
ok: false,
|
||||
error: {
|
||||
code: ErrorCodes.UNAVAILABLE,
|
||||
message: `Session ${key} is still active; try again in a moment.`,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
if (existed) delete store[key];
|
||||
await saveSessionStore(storePath, store);
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import { describe, expect, test } from "vitest";
|
||||
import {
|
||||
connectOk,
|
||||
installGatewayTestHooks,
|
||||
embeddedRunMock,
|
||||
piSdkMock,
|
||||
rpcReq,
|
||||
startServerWithClient,
|
||||
@@ -217,4 +218,59 @@ describe("gateway server sessions", () => {
|
||||
ws.close();
|
||||
await server.close();
|
||||
});
|
||||
|
||||
test("sessions.delete rejects main and aborts active runs", async () => {
|
||||
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdis-sessions-"));
|
||||
const storePath = path.join(dir, "sessions.json");
|
||||
testState.sessionStorePath = storePath;
|
||||
|
||||
await fs.writeFile(
|
||||
path.join(dir, "sess-main.jsonl"),
|
||||
`${JSON.stringify({ role: "user", content: "hello" })}\n`,
|
||||
"utf-8",
|
||||
);
|
||||
await fs.writeFile(
|
||||
path.join(dir, "sess-active.jsonl"),
|
||||
`${JSON.stringify({ role: "user", content: "active" })}\n`,
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
await fs.writeFile(
|
||||
storePath,
|
||||
JSON.stringify(
|
||||
{
|
||||
main: { sessionId: "sess-main", updatedAt: Date.now() },
|
||||
"discord:group:dev": {
|
||||
sessionId: "sess-active",
|
||||
updatedAt: Date.now(),
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
embeddedRunMock.activeIds.add("sess-active");
|
||||
embeddedRunMock.waitResults.set("sess-active", true);
|
||||
|
||||
const { server, ws } = await startServerWithClient();
|
||||
await connectOk(ws);
|
||||
|
||||
const mainDelete = await rpcReq(ws, "sessions.delete", { key: "main" });
|
||||
expect(mainDelete.ok).toBe(false);
|
||||
|
||||
const deleted = await rpcReq<{ ok: true; deleted: boolean }>(
|
||||
ws,
|
||||
"sessions.delete",
|
||||
{ key: "discord:group:dev" },
|
||||
);
|
||||
expect(deleted.ok).toBe(true);
|
||||
expect(deleted.payload?.deleted).toBe(true);
|
||||
expect(embeddedRunMock.abortCalls).toEqual(["sess-active"]);
|
||||
expect(embeddedRunMock.waitCalls).toEqual(["sess-active"]);
|
||||
|
||||
ws.close();
|
||||
await server.close();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -39,7 +39,7 @@ export type BridgeStartOpts = {
|
||||
>;
|
||||
};
|
||||
|
||||
const hoisted = vi.hoisted(() => ({
|
||||
const hoisted = vi.hoisted(() => ({
|
||||
bridgeStartCalls: [] as BridgeStartOpts[],
|
||||
bridgeInvoke: vi.fn(async () => ({
|
||||
type: "invoke-res",
|
||||
@@ -66,6 +66,12 @@ const hoisted = vi.hoisted(() => ({
|
||||
agentCommand: vi.fn().mockResolvedValue(undefined),
|
||||
testIsNixMode: { value: false },
|
||||
sessionStoreSaveDelayMs: { value: 0 },
|
||||
embeddedRunMock: {
|
||||
activeIds: new Set<string>(),
|
||||
abortCalls: [] as string[],
|
||||
waitCalls: [] as string[],
|
||||
waitResults: new Map<string, boolean>(),
|
||||
},
|
||||
}));
|
||||
|
||||
export const bridgeStartCalls = hoisted.bridgeStartCalls;
|
||||
@@ -95,6 +101,7 @@ export const testState = {
|
||||
|
||||
export const testIsNixMode = hoisted.testIsNixMode;
|
||||
export const sessionStoreSaveDelayMs = hoisted.sessionStoreSaveDelayMs;
|
||||
export const embeddedRunMock = hoisted.embeddedRunMock;
|
||||
|
||||
vi.mock("@mariozechner/pi-coding-agent", async () => {
|
||||
const actual = await vi.importActual<
|
||||
@@ -284,6 +291,25 @@ vi.mock("../config/config.js", async () => {
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("../agents/pi-embedded.js", async () => {
|
||||
const actual = await vi.importActual<typeof import("../agents/pi-embedded.js")>(
|
||||
"../agents/pi-embedded.js",
|
||||
);
|
||||
return {
|
||||
...actual,
|
||||
isEmbeddedPiRunActive: (sessionId: string) =>
|
||||
embeddedRunMock.activeIds.has(sessionId),
|
||||
abortEmbeddedPiRun: (sessionId: string) => {
|
||||
embeddedRunMock.abortCalls.push(sessionId);
|
||||
return embeddedRunMock.activeIds.has(sessionId);
|
||||
},
|
||||
waitForEmbeddedPiRunEnd: async (sessionId: string) => {
|
||||
embeddedRunMock.waitCalls.push(sessionId);
|
||||
return embeddedRunMock.waitResults.get(sessionId) ?? true;
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("../commands/health.js", () => ({
|
||||
getHealthSnapshot: vi.fn().mockResolvedValue({ ok: true, stub: true }),
|
||||
}));
|
||||
@@ -329,6 +355,10 @@ export function installGatewayTestHooks() {
|
||||
testIsNixMode.value = false;
|
||||
cronIsolatedRun.mockClear();
|
||||
agentCommand.mockClear();
|
||||
embeddedRunMock.activeIds.clear();
|
||||
embeddedRunMock.abortCalls = [];
|
||||
embeddedRunMock.waitCalls = [];
|
||||
embeddedRunMock.waitResults.clear();
|
||||
drainSystemEvents();
|
||||
resetAgentRunContextForTest();
|
||||
const mod = await import("./server.js");
|
||||
|
||||
Reference in New Issue
Block a user