Files
routstrd/tests/daemon/auto-refresh.scenario.ts
T
be374601d8 feat(clients): add --manual-refresh and scheduled-refresh toggle (#97)
`routstrd clients` only exposed list/delete/add, so operators had no way to
refresh client integrations on demand or to stop the daemon from rewriting
their client configs every 21 minutes.

- clients --manual-refresh: refresh routstr21 models from Nostr and re-run
  every registered client integration now. The old `routstrd refresh` body
  moves into a shared refreshModelsAndClientsAction() so both commands stay
  in sync.
- clients --disable-automatic-refresh / --enable-automatic-refresh: toggle
  the daemon's scheduled refresh job via a new POST /settings/auto-refresh
  endpoint, so the toggle also works against a remote daemon where the config
  lives on the host. Persisted as autoRefresh.enabled in config.json.
- The daemon refresh job now re-reads autoRefresh on every tick (like the NWC
  auto-refill getter), so toggling takes effect without a restart. While
  disabled it polls once a minute so re-enabling applies promptly.
- Startup still fetches models (the proxy needs them) but skips the client
  integration pass when the job is disabled, so a restart cannot overwrite
  hand-edited client configs.

Adds tests/daemon/auto-refresh.* covering the endpoint contract and the
persisted flag.

Co-authored-by: redshift <213178690+1ftredsh@users.noreply.github.com>
2026-09-08 11:34:13 +00:00

103 lines
3.0 KiB
TypeScript

// Scenario runner for auto-refresh.test.ts — executed as a standalone bun
// process with ROUTSTRD_DIR set before module evaluation, because CONFIG_FILE
// is resolved at import time.
import { createServer } from "http";
import type { AddressInfo } from "net";
const { createDaemonRequestHandler } = await import("../../src/daemon/http/index");
const { loadDaemonConfigSync } = await import("../../src/daemon/config-store");
function assert(cond: unknown, msg: string): void {
if (!cond) {
console.error(`ASSERT-FAIL: ${msg}`);
process.exit(1);
}
}
/**
* Only the settings endpoints are exercised, so every daemon dependency is a
* stub that would throw loudly if the request reached the proxy path.
*/
const stubDeps = {
provider: null,
server: { close() {} },
store: {},
walletClient: {},
walletAdapter: {},
storageAdapter: {},
discoveryAdapter: {},
modelManager: {},
ensureProvidersBootstrapped: async () => {},
getRoutstr21Models: async () => [],
getModelProviders: async () => [],
refreshProvidersAndModels: async () => {},
mode: "apikeys" as const,
maxTokens: 64000,
usageTrackingDriver: {},
providerManager: {},
refundClient: {},
};
async function withServer(
run: (port: number) => Promise<void>,
): Promise<void> {
const server = createServer(
createDaemonRequestHandler(stubDeps as never),
);
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
try {
await run((server.address() as AddressInfo).port);
} finally {
server.close();
}
}
const scenario = process.argv[2];
switch (scenario) {
case "toggle-endpoint": {
await withServer(async (port) => {
const post = (body: unknown) =>
fetch(`http://127.0.0.1:${port}/settings/auto-refresh`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
// Missing config means "enabled" so existing installs are unaffected.
assert(
loadDaemonConfigSync().autoRefresh?.enabled === undefined,
"expected no autoRefresh in a fresh config",
);
const disabled = await post({ enabled: false });
assert(disabled.status === 200, `disable status ${disabled.status}`);
const disabledBody = (await disabled.json()) as {
output?: { autoRefresh?: { enabled?: boolean } };
};
assert(
disabledBody.output?.autoRefresh?.enabled === false,
"disable response did not report enabled=false",
);
assert(
loadDaemonConfigSync().autoRefresh?.enabled === false,
"disable was not persisted to config.json",
);
const enabled = await post({ enabled: true });
assert(enabled.status === 200, `enable status ${enabled.status}`);
assert(
loadDaemonConfigSync().autoRefresh?.enabled === true,
"enable was not persisted to config.json",
);
});
break;
}
default:
console.error(`unknown scenario: ${scenario}`);
process.exit(2);
}
console.log("SCENARIO-OK");