@pellux/goodvibes-daemon 1.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +383 -0
- package/LICENSE +21 -0
- package/README.md +125 -0
- package/bin/goodvibes-daemon +100 -0
- package/bin/launcher-support.js +226 -0
- package/package.json +96 -0
- package/scripts/check-bun.sh +20 -0
- package/scripts/postinstall.js +244 -0
- package/src/cli/command-catalog.ts +828 -0
- package/src/cli/completion.ts +299 -0
- package/src/cli/help.ts +167 -0
- package/src/cli/index.ts +21 -0
- package/src/cli/parser.ts +55 -0
- package/src/cli/surface-catalog.ts +26 -0
- package/src/cli/types.ts +63 -0
- package/src/cluster/daemon-ws-call.ts +235 -0
- package/src/cluster/raw-reply-route.ts +111 -0
- package/src/config/checkpoint-settings.ts +113 -0
- package/src/config/run-daemon-config-migration.ts +47 -0
- package/src/config/secret-config.ts +175 -0
- package/src/config/secrets.ts +71 -0
- package/src/config/surface.ts +24 -0
- package/src/core/pairing-banner.ts +82 -0
- package/src/daemon/cli.ts +878 -0
- package/src/daemon/config-command.ts +281 -0
- package/src/daemon/handlers/context.ts +29 -0
- package/src/daemon/handlers/contracts.ts +43 -0
- package/src/daemon/handlers/credentials.ts +139 -0
- package/src/daemon/handlers/drafts/draft-store.ts +427 -0
- package/src/daemon/handlers/drafts/index.ts +17 -0
- package/src/daemon/handlers/drafts/register.ts +331 -0
- package/src/daemon/handlers/errors.ts +18 -0
- package/src/daemon/handlers/inbox/aggregator.ts +375 -0
- package/src/daemon/handlers/inbox/cursor-store.ts +512 -0
- package/src/daemon/handlers/inbox/index.ts +221 -0
- package/src/daemon/handlers/inbox/mapping.ts +192 -0
- package/src/daemon/handlers/inbox/poller.ts +239 -0
- package/src/daemon/handlers/inbox/provider-adapter.ts +171 -0
- package/src/daemon/handlers/inbox/providers/discord.ts +276 -0
- package/src/daemon/handlers/inbox/providers/email.ts +176 -0
- package/src/daemon/handlers/inbox/providers/imap-client.ts +300 -0
- package/src/daemon/handlers/inbox/providers/route-util.ts +24 -0
- package/src/daemon/handlers/inbox/providers/slack.ts +287 -0
- package/src/daemon/handlers/index.ts +117 -0
- package/src/daemon/handlers/register.ts +180 -0
- package/src/daemon/handlers/remote/backends/cloud-terminal.ts +143 -0
- package/src/daemon/handlers/remote/backends/docker.ts +79 -0
- package/src/daemon/handlers/remote/backends/index.ts +40 -0
- package/src/daemon/handlers/remote/backends/local-process.ts +113 -0
- package/src/daemon/handlers/remote/backends/process-runner.ts +127 -0
- package/src/daemon/handlers/remote/backends/ssh.ts +126 -0
- package/src/daemon/handlers/remote/backends/types.ts +97 -0
- package/src/daemon/handlers/remote/dispatcher.ts +181 -0
- package/src/daemon/handlers/remote/index.ts +120 -0
- package/src/daemon/handlers/remote/peer-registry.ts +357 -0
- package/src/daemon/handlers/remote/service.ts +191 -0
- package/src/daemon/handlers/routing/inbox-bridge.ts +71 -0
- package/src/daemon/handlers/routing/index.ts +261 -0
- package/src/daemon/handlers/routing/route-store.ts +319 -0
- package/src/daemon/handlers/routing/routing-resolver.ts +75 -0
- package/src/daemon/handlers/sqlite-store.ts +303 -0
- package/src/daemon/handlers/triage/index.ts +57 -0
- package/src/daemon/handlers/triage/integration.ts +213 -0
- package/src/daemon/handlers/triage/pipeline.ts +274 -0
- package/src/daemon/handlers/triage/scorer.ts +287 -0
- package/src/daemon/handlers/triage/tagger/discord.ts +187 -0
- package/src/daemon/handlers/triage/tagger/imap.ts +384 -0
- package/src/daemon/handlers/triage/tagger/index.ts +184 -0
- package/src/daemon/handlers/triage/tagger/shared.ts +70 -0
- package/src/daemon/handlers/triage/tagger/slack.ts +69 -0
- package/src/daemon/handlers/triage/types.ts +50 -0
- package/src/daemon/lifecycle.ts +41 -0
- package/src/daemon/local-daemon-state.ts +233 -0
- package/src/daemon/pair-command.ts +301 -0
- package/src/daemon/provision-wake-model.ts +81 -0
- package/src/daemon/send/channels.ts +200 -0
- package/src/daemon/send/command.ts +333 -0
- package/src/daemon/send/composition.ts +100 -0
- package/src/daemon/send/failure-text.ts +93 -0
- package/src/daemon/send/inert-text.ts +225 -0
- package/src/daemon/send/stdin.ts +24 -0
- package/src/daemon/service-commands.ts +530 -0
- package/src/daemon/sessions-command.ts +209 -0
- package/src/daemon/status-command.ts +481 -0
- package/src/daemon/webui-command.ts +339 -0
- package/src/runtime/boot-tasks.ts +110 -0
- package/src/runtime/cluster-composition.ts +124 -0
- package/src/runtime/cluster-group-composition.ts +284 -0
- package/src/runtime/conversation-rewind-port.ts +171 -0
- package/src/runtime/credential-composition.ts +54 -0
- package/src/runtime/daemon-handler-composition.ts +76 -0
- package/src/runtime/device-posture-composition.ts +115 -0
- package/src/runtime/disposal-wiring.ts +101 -0
- package/src/runtime/fleet-needs-input-push.ts +61 -0
- package/src/runtime/fleet-services.ts +41 -0
- package/src/runtime/hosted-session-composition.ts +128 -0
- package/src/runtime/index.ts +100 -0
- package/src/runtime/knowledge-services.ts +101 -0
- package/src/runtime/legacy-daemon-migration.ts +605 -0
- package/src/runtime/legacy-daemon-reconcile.ts +448 -0
- package/src/runtime/mail-composition.ts +65 -0
- package/src/runtime/notification-dispatch.ts +86 -0
- package/src/runtime/plugin-composition.ts +111 -0
- package/src/runtime/runtime-services-types.ts +268 -0
- package/src/runtime/services.ts +756 -0
- package/src/runtime/trigger-services.ts +62 -0
- package/src/runtime/trust/checkpoint-eligibility.ts +138 -0
- package/src/runtime/trust/trust-gated-approvals.ts +169 -0
- package/src/runtime/update-check.ts +61 -0
- package/src/runtime/workspace-checkpointing.ts +116 -0
- package/src/testing/daemon-fixture.ts +276 -0
- package/src/testing/hosted-session-failures.ts +92 -0
- package/src/version.ts +26 -0
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* daemon-fixture.ts — a real, running daemon, composed the way this product
|
|
3
|
+
* composes one, for a test to drive.
|
|
4
|
+
*
|
|
5
|
+
* ── Why this is a shipped module and not a test helper ────────────────────
|
|
6
|
+
*
|
|
7
|
+
* Three consumer repositories test their client against the daemon's contract,
|
|
8
|
+
* and each of them hand-builds a composition to test against: a stub session
|
|
9
|
+
* broker here, a fake approval broker there, a catalog with the handlers
|
|
10
|
+
* somebody remembered to attach. They are large (several hundred lines each),
|
|
11
|
+
* they duplicate each other, and — the part that matters — they are each a
|
|
12
|
+
* SECOND idea of what a daemon is. A client contract test passing against a
|
|
13
|
+
* hand-rolled stand-in tells you the stand-in agrees with the client, which is
|
|
14
|
+
* the one agreement that was never in doubt.
|
|
15
|
+
*
|
|
16
|
+
* This module is the alternative: the daemon's own `createRuntimeServices` and
|
|
17
|
+
* its own `DaemonServer`, bound on an ephemeral port with a known token, over a
|
|
18
|
+
* temp home. What a consumer's test talks to is the thing that ships.
|
|
19
|
+
*
|
|
20
|
+
* It deliberately lives under `src/testing/` rather than `src/test/`: the
|
|
21
|
+
* package's `files` list ships `src` and excludes `src/test`, so this path is
|
|
22
|
+
* published while the suites are not, and the main tsconfig typechecks it as
|
|
23
|
+
* ordinary source. Nothing here imports `bun:test`, so a consumer on any runner
|
|
24
|
+
* can use it.
|
|
25
|
+
*
|
|
26
|
+
* ── What it is not ───────────────────────────────────────────────────────
|
|
27
|
+
*
|
|
28
|
+
* Not a mock and not a fixture file. Nothing here fabricates a response. If a
|
|
29
|
+
* verb is unwired in this composition it answers exactly as unwired as it would
|
|
30
|
+
* in production, which is the property a contract test needs and a hand-built
|
|
31
|
+
* stand-in cannot have.
|
|
32
|
+
*
|
|
33
|
+
* ── What a consumer needs, and what is still missing ─────────────────────
|
|
34
|
+
*
|
|
35
|
+
* WORKS TODAY, from inside this package, with no change anywhere else:
|
|
36
|
+
*
|
|
37
|
+
* import { startDaemonFixture } from 'goodvibes-daemon/src/testing/daemon-fixture.ts';
|
|
38
|
+
*
|
|
39
|
+
* The package declares no `exports` map, so a deep path resolves. It is the
|
|
40
|
+
* ugly form of the import, and it is a real one.
|
|
41
|
+
*
|
|
42
|
+
* STILL MISSING, and each is somebody's decision rather than a thing this file
|
|
43
|
+
* can do for itself:
|
|
44
|
+
*
|
|
45
|
+
* 1. A NAMED ENTRY POINT. `goodvibes-daemon/testing` instead of the deep
|
|
46
|
+
* path. That means adding an `exports` map to package.json — and adding
|
|
47
|
+
* one is not additive: an `exports` map REPLACES path-based resolution, so
|
|
48
|
+
* every existing deep import into this package (its own `bin` shim
|
|
49
|
+
* included) stops resolving unless the map enumerates them. That is a
|
|
50
|
+
* packaging change with a blast radius, made once, deliberately, by
|
|
51
|
+
* whoever owns distribution — not a side effect of adding a test helper.
|
|
52
|
+
*
|
|
53
|
+
* 2. THE CONSUMER DEPENDENCY. A consumer repo has to depend on
|
|
54
|
+
* `goodvibes-daemon` to import this at all. Today the agent does (it is
|
|
55
|
+
* what makes its own install fail against an unpublished version); the
|
|
56
|
+
* terminal app and the webui do not. For the terminal app that is a
|
|
57
|
+
* devDependency and a version pin. For the webui, whose suites run in a
|
|
58
|
+
* browser context, the fixture cannot run in-process at all — it needs a
|
|
59
|
+
* launcher script that starts the fixture in a node process and hands the
|
|
60
|
+
* Playwright suite `baseUrl` and `token`. That launcher does not exist and
|
|
61
|
+
* should be written on the webui side, where its runner lives.
|
|
62
|
+
*
|
|
63
|
+
* 3. A RUNTIME FLOOR. This composes real stores, opens a real socket, and
|
|
64
|
+
* starts the runtime graph's pollers. A consumer suite that spins one up
|
|
65
|
+
* per test file pays about a second each and must `stop()` every one; the
|
|
66
|
+
* three hand-built stand-ins it replaces cost nothing and leak nothing.
|
|
67
|
+
* The honest guidance is one fixture per FILE (`beforeAll`/`afterAll`),
|
|
68
|
+
* which is how this repository's own suites use it — not one per test.
|
|
69
|
+
*
|
|
70
|
+
* 4. WHAT IT DOES NOT REPLACE. A stand-in is still the right tool for
|
|
71
|
+
* driving a client through a daemon state that is hard to reach for real:
|
|
72
|
+
* a wedged session, a specific 500, a torn connection. This fixture
|
|
73
|
+
* replaces the stand-ins that exist only to answer normally — which is
|
|
74
|
+
* most of the several hundred lines in each consumer, and all of the part
|
|
75
|
+
* that silently drifts. The refusal shapes it cannot easily produce live
|
|
76
|
+
* are exported separately and pinned against the real engine; see
|
|
77
|
+
* ./hosted-session-failures.ts for the pattern.
|
|
78
|
+
*/
|
|
79
|
+
import { mkdirSync, mkdtempSync, rmSync } from 'node:fs';
|
|
80
|
+
import { tmpdir } from 'node:os';
|
|
81
|
+
import { join } from 'node:path';
|
|
82
|
+
import { ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
|
|
83
|
+
import { DaemonServer } from '@pellux/goodvibes-sdk/platform/daemon';
|
|
84
|
+
import { createRuntimeStore } from '@pellux/goodvibes-sdk/platform/runtime/store';
|
|
85
|
+
import { UserAuthManager } from '@pellux/goodvibes-sdk/platform/security';
|
|
86
|
+
import { createFeatureFlagManager, deriveFeatureStates, RuntimeEventBus } from '@/runtime/index.ts';
|
|
87
|
+
import { createRuntimeServices, type RuntimeServices } from '../runtime/services.ts';
|
|
88
|
+
import { createHostedSessionOptions } from '../runtime/hosted-session-composition.ts';
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* The capabilities a daemon serves once it is running. Seeded from a real
|
|
92
|
+
* config first (so the registry stays complete), then forced on, because a
|
|
93
|
+
* contract test asking "does this daemon serve X" must not be answered by a
|
|
94
|
+
* feature flag that happens to be off in a fresh temp home.
|
|
95
|
+
*/
|
|
96
|
+
const DAEMON_CAPABILITY_FLAGS: readonly string[] = [
|
|
97
|
+
'automation-domain',
|
|
98
|
+
'control-plane-gateway',
|
|
99
|
+
'delivery-engine',
|
|
100
|
+
'hitl-ux-modes',
|
|
101
|
+
'ntfy-surface',
|
|
102
|
+
'permission-divergence-dashboard',
|
|
103
|
+
'policy-as-code',
|
|
104
|
+
'route-binding',
|
|
105
|
+
'service-management',
|
|
106
|
+
'slack-surface',
|
|
107
|
+
'unified-runtime-task',
|
|
108
|
+
'watcher-framework',
|
|
109
|
+
'web-surface',
|
|
110
|
+
'webhook-surface',
|
|
111
|
+
];
|
|
112
|
+
|
|
113
|
+
export interface DaemonFixtureOptions {
|
|
114
|
+
/**
|
|
115
|
+
* Root directory for this fixture's home and workspace. Omitted ⇒ a fresh
|
|
116
|
+
* `mkdtemp` under the OS temp directory, removed by `stop()`. Pass one when
|
|
117
|
+
* the caller has its own temp-tree bookkeeping (this repo's suites do).
|
|
118
|
+
*/
|
|
119
|
+
readonly root?: string;
|
|
120
|
+
/** Bearer token the daemon accepts. Omitted ⇒ a generated per-fixture token. */
|
|
121
|
+
readonly token?: string;
|
|
122
|
+
/** Last-chance hook to write config before the runtime graph is built. */
|
|
123
|
+
readonly configure?: (configManager: ConfigManager) => void;
|
|
124
|
+
/**
|
|
125
|
+
* Feature flag ids to force on, replacing the default daemon capability set.
|
|
126
|
+
* Rarely needed; stated so a caller testing a flag's OFF behaviour can.
|
|
127
|
+
*/
|
|
128
|
+
readonly featureFlagIds?: readonly string[];
|
|
129
|
+
/**
|
|
130
|
+
* Whether this daemon hosts sessions of its own, the way the real entrypoint
|
|
131
|
+
* states it (`src/daemon/cli.ts` passes `createHostedSessionOptions`).
|
|
132
|
+
* Defaults to true: `sessions.hosted.*` is handler-less without it, and a
|
|
133
|
+
* client with no terminal of its own has no other way to start a run.
|
|
134
|
+
*/
|
|
135
|
+
readonly hostSessions?: boolean;
|
|
136
|
+
/**
|
|
137
|
+
* A mailbox address to watch. Omitted ⇒ none, and the inbound-mail
|
|
138
|
+
* composition honestly registers no `email.expectation.*` /
|
|
139
|
+
* `email.inbound.status` handler, exactly as it does on a daemon nobody has
|
|
140
|
+
* configured a mailbox for. Pass one to exercise the provisioned shape.
|
|
141
|
+
* Nothing connects: the composition reads the account name, and no source
|
|
142
|
+
* starts until the supervisor does.
|
|
143
|
+
*/
|
|
144
|
+
readonly watchedMailbox?: string;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export interface DaemonFixture {
|
|
148
|
+
/** The composed runtime graph — the same object `DaemonServer` was handed. */
|
|
149
|
+
readonly services: RuntimeServices;
|
|
150
|
+
/** The running server. */
|
|
151
|
+
readonly daemon: DaemonServer;
|
|
152
|
+
/** `http://127.0.0.1:<bound port>`, valid after `start()` returned. */
|
|
153
|
+
readonly baseUrl: string;
|
|
154
|
+
/** The bearer token this daemon accepts. */
|
|
155
|
+
readonly token: string;
|
|
156
|
+
readonly homeDirectory: string;
|
|
157
|
+
readonly workingDirectory: string;
|
|
158
|
+
/** Fetch a path on this daemon with the bearer token attached. */
|
|
159
|
+
fetch(path: string, init?: RequestInit): Promise<Response>;
|
|
160
|
+
/**
|
|
161
|
+
* Fetch a path with NO credential. A route that exists answers 401; a path
|
|
162
|
+
* nothing serves answers 404 — which is what makes this the side-effect-free
|
|
163
|
+
* way to ask whether a route exists, even for a write verb.
|
|
164
|
+
*/
|
|
165
|
+
fetchAnonymous(path: string, init?: RequestInit): Promise<Response>;
|
|
166
|
+
/** Invoke a gateway verb in-process, the way the control plane dispatches it. */
|
|
167
|
+
invoke<T = unknown>(methodId: string, body?: Record<string, unknown>): Promise<T>;
|
|
168
|
+
/** Stop the server, dispose the graph, and remove a temp root this created. */
|
|
169
|
+
stop(): Promise<void>;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Compose and start a daemon. Always `await fixture.stop()` — the runtime graph
|
|
174
|
+
* starts pollers while it builds, and abandoning it leaves every one of them
|
|
175
|
+
* firing for the rest of the process.
|
|
176
|
+
*/
|
|
177
|
+
export async function startDaemonFixture(options: DaemonFixtureOptions = {}): Promise<DaemonFixture> {
|
|
178
|
+
const ownsRoot = options.root === undefined;
|
|
179
|
+
const root = options.root ?? mkdtempSync(join(tmpdir(), 'goodvibes-daemon-fixture-'));
|
|
180
|
+
const workingDirectory = join(root, 'workspace');
|
|
181
|
+
const homeDirectory = join(root, 'home');
|
|
182
|
+
const configDir = join(homeDirectory, '.goodvibes', 'daemon');
|
|
183
|
+
mkdirSync(workingDirectory, { recursive: true });
|
|
184
|
+
mkdirSync(configDir, { recursive: true });
|
|
185
|
+
|
|
186
|
+
const token = options.token ?? `daemon-fixture-${Math.random().toString(36).slice(2)}`;
|
|
187
|
+
|
|
188
|
+
const configManager = new ConfigManager({
|
|
189
|
+
surfaceRoot: 'tui',
|
|
190
|
+
configDir,
|
|
191
|
+
workingDir: workingDirectory,
|
|
192
|
+
homeDir: homeDirectory,
|
|
193
|
+
});
|
|
194
|
+
if (options.watchedMailbox !== undefined) {
|
|
195
|
+
configManager.set('surfaces.email.inbound.accounts', JSON.stringify([options.watchedMailbox]));
|
|
196
|
+
}
|
|
197
|
+
options.configure?.(configManager);
|
|
198
|
+
|
|
199
|
+
const featureFlags = createFeatureFlagManager();
|
|
200
|
+
const flags = deriveFeatureStates(configManager);
|
|
201
|
+
for (const id of options.featureFlagIds ?? DAEMON_CAPABILITY_FLAGS) flags[id] = 'enabled';
|
|
202
|
+
featureFlags.loadFromConfig({ flags });
|
|
203
|
+
|
|
204
|
+
const services = createRuntimeServices({
|
|
205
|
+
runtimeStore: createRuntimeStore(),
|
|
206
|
+
runtimeBus: new RuntimeEventBus(),
|
|
207
|
+
configManager,
|
|
208
|
+
workingDir: workingDirectory,
|
|
209
|
+
homeDirectory,
|
|
210
|
+
featureFlags,
|
|
211
|
+
getConversationTitle: () => 'daemon fixture',
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
// Ephemeral port: two concurrent test processes must never collide, and
|
|
215
|
+
// injecting a serveFactory also makes DaemonServer skip its pre-bind OS port
|
|
216
|
+
// probe (the facade only probes when serveFactory === Bun.serve).
|
|
217
|
+
let boundPort = 0;
|
|
218
|
+
const capturingServe = ((serveOptions) => {
|
|
219
|
+
const server = Bun.serve(serveOptions);
|
|
220
|
+
if (server.port !== undefined) boundPort = server.port;
|
|
221
|
+
return server;
|
|
222
|
+
}) as typeof Bun.serve;
|
|
223
|
+
|
|
224
|
+
const daemon = new DaemonServer({
|
|
225
|
+
port: 0,
|
|
226
|
+
host: '127.0.0.1',
|
|
227
|
+
userAuth: new UserAuthManager({
|
|
228
|
+
bootstrapFilePath: join(homeDirectory, 'auth-users.json'),
|
|
229
|
+
bootstrapCredentialPath: join(homeDirectory, 'auth-bootstrap.txt'),
|
|
230
|
+
users: [{ username: 'admin', passwordHash: UserAuthManager.hashPassword('admin'), roles: ['admin'] }],
|
|
231
|
+
}),
|
|
232
|
+
runtimeServices: services,
|
|
233
|
+
serveFactory: capturingServe,
|
|
234
|
+
// What the real entrypoint states, for the same reason it states it: the
|
|
235
|
+
// hosted-session verbs are registered by this composition and by nothing
|
|
236
|
+
// else, so a fixture that leaves it out is a daemon a client cannot start a
|
|
237
|
+
// session on — and every contract test written against it would agree.
|
|
238
|
+
...(options.hostSessions === false ? {} : { hostedSessions: createHostedSessionOptions(services) }),
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
daemon.enable({ daemon: true }, token);
|
|
242
|
+
await daemon.start();
|
|
243
|
+
if (!daemon.isRunning) {
|
|
244
|
+
services.dispose();
|
|
245
|
+
if (ownsRoot) rmSync(root, { recursive: true, force: true });
|
|
246
|
+
throw new Error('daemon fixture: the server refused to start');
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const baseUrl = `http://127.0.0.1:${boundPort}`;
|
|
250
|
+
|
|
251
|
+
return {
|
|
252
|
+
services,
|
|
253
|
+
daemon,
|
|
254
|
+
baseUrl,
|
|
255
|
+
token,
|
|
256
|
+
homeDirectory,
|
|
257
|
+
workingDirectory,
|
|
258
|
+
fetch(path, init) {
|
|
259
|
+
const headers = new Headers(init?.headers);
|
|
260
|
+
headers.set('Authorization', `Bearer ${token}`);
|
|
261
|
+
if (!headers.has('Content-Type')) headers.set('Content-Type', 'application/json');
|
|
262
|
+
return fetch(`${baseUrl}${path}`, { ...init, headers });
|
|
263
|
+
},
|
|
264
|
+
fetchAnonymous(path, init) {
|
|
265
|
+
return fetch(`${baseUrl}${path}`, init);
|
|
266
|
+
},
|
|
267
|
+
invoke<T>(methodId: string, body: Record<string, unknown> = {}): Promise<T> {
|
|
268
|
+
return services.gatewayMethods.invoke(methodId, { methodId, body } as never) as Promise<T>;
|
|
269
|
+
},
|
|
270
|
+
async stop(): Promise<void> {
|
|
271
|
+
await daemon.stop();
|
|
272
|
+
services.dispose();
|
|
273
|
+
if (ownsRoot) rmSync(root, { recursive: true, force: true });
|
|
274
|
+
},
|
|
275
|
+
};
|
|
276
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* hosted-session-failures.ts — what `sessions.hosted.*` answers when it refuses.
|
|
3
|
+
*
|
|
4
|
+
* ── Why this is a shipped module ─────────────────────────────────────────
|
|
5
|
+
*
|
|
6
|
+
* The platform's shared mock-daemon fixture set
|
|
7
|
+
* (@pellux/goodvibes-contracts/testing) is generated from each method's OUTPUT
|
|
8
|
+
* schema, so every fixture in it is a 200 carrying a schema-valid success body.
|
|
9
|
+
* There is no failure shape in it for anything, and a client mocked entirely
|
|
10
|
+
* from it has never once been handed a refusal.
|
|
11
|
+
*
|
|
12
|
+
* That matters most for hosted sessions, because a hosted session refuses in
|
|
13
|
+
* four distinct ways ON PURPOSE and the whole point of the distinction is that
|
|
14
|
+
* a client reacts differently to each: "no such session", "that session exists
|
|
15
|
+
* and cannot serve this right now, here is why", "you are at the configured
|
|
16
|
+
* cap", "your argument is malformed". A client that collapses them retries the
|
|
17
|
+
* one thing that will never work.
|
|
18
|
+
*
|
|
19
|
+
* These are the three refusals a client has to handle and could not previously
|
|
20
|
+
* mock. They are declared here rather than hand-written in each consumer, and
|
|
21
|
+
* `src/test/daemon/gateway-hosted-session-failures.test.ts` drives the REAL
|
|
22
|
+
* engine to produce each one and asserts it still matches — so a fixture that
|
|
23
|
+
* drifts from the daemon fails in this repository, not in a consumer's CI six
|
|
24
|
+
* weeks later.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/** The wire shape of a refusal, as `invokeGatewayMethodCall` reports it. */
|
|
28
|
+
export interface HostedSessionFailureFixture {
|
|
29
|
+
/** The verb whose refusal this is. */
|
|
30
|
+
readonly methodId: string;
|
|
31
|
+
/** Machine-readable code — the field a client branches on. */
|
|
32
|
+
readonly code: string;
|
|
33
|
+
/** HTTP status the control plane maps this refusal to. */
|
|
34
|
+
readonly status: number;
|
|
35
|
+
/** What the caller did to earn it, in one line. */
|
|
36
|
+
readonly when: string;
|
|
37
|
+
/** How a client should react. Stated because the codes exist to be reacted to. */
|
|
38
|
+
readonly clientAction: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* A hosted session id that names nothing. 404: the id is wrong, and no amount
|
|
43
|
+
* of retrying it will make it right.
|
|
44
|
+
*/
|
|
45
|
+
export const SESSION_NOT_FOUND: HostedSessionFailureFixture = {
|
|
46
|
+
methodId: 'sessions.hosted.attach',
|
|
47
|
+
code: 'SESSION_NOT_FOUND',
|
|
48
|
+
status: 404,
|
|
49
|
+
when: 'the sessionId names no session this daemon holds',
|
|
50
|
+
clientAction: 'Drop the id. Re-list before addressing a session again.',
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The session is real and cannot serve this request. 409, deliberately not 404:
|
|
55
|
+
* a 404 would invite a retry against an id that is perfectly valid. The message
|
|
56
|
+
* carries the reason (terminated, loop not composable) so a client can say why.
|
|
57
|
+
*/
|
|
58
|
+
export const HOSTED_SESSION_UNAVAILABLE: HostedSessionFailureFixture = {
|
|
59
|
+
methodId: 'sessions.hosted.attach',
|
|
60
|
+
code: 'HOSTED_SESSION_UNAVAILABLE',
|
|
61
|
+
status: 409,
|
|
62
|
+
when: 'the session exists but has terminated, or its loop could not be composed',
|
|
63
|
+
clientAction: 'Show the reason. Offer a new session, never a retry of this one.',
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* At the configured cap. 429: the request is well-formed and the daemon is not
|
|
68
|
+
* broken — there is no room. Retrying after a kill succeeds.
|
|
69
|
+
*/
|
|
70
|
+
export const HOSTED_SESSION_LIMIT_REACHED: HostedSessionFailureFixture = {
|
|
71
|
+
methodId: 'sessions.hosted.create',
|
|
72
|
+
code: 'HOSTED_SESSION_LIMIT_REACHED',
|
|
73
|
+
status: 429,
|
|
74
|
+
when: 'hostedSessions.maxSessions live sessions already exist',
|
|
75
|
+
clientAction: 'Offer to kill one, or to raise hostedSessions.maxSessions. Retry after either.',
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/** Every hosted-session refusal a client must handle, in one list. */
|
|
79
|
+
export const HOSTED_SESSION_FAILURE_FIXTURES: readonly HostedSessionFailureFixture[] = [
|
|
80
|
+
SESSION_NOT_FOUND,
|
|
81
|
+
HOSTED_SESSION_UNAVAILABLE,
|
|
82
|
+
HOSTED_SESSION_LIMIT_REACHED,
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
/** Every `sessions.hosted.*` verb, for a conformance sweep's `onlyIds`. */
|
|
86
|
+
export const HOSTED_SESSION_METHOD_IDS: readonly string[] = [
|
|
87
|
+
'sessions.hosted.create',
|
|
88
|
+
'sessions.hosted.attach',
|
|
89
|
+
'sessions.hosted.detach',
|
|
90
|
+
'sessions.hosted.kill',
|
|
91
|
+
'sessions.hosted.list',
|
|
92
|
+
];
|
package/src/version.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
// Read version from package.json at runtime (eliminates build-time sync issues).
|
|
5
|
+
// Fallback for compiled binaries where package.json may not be present.
|
|
6
|
+
// The prebuild script updates the fallback value before compilation.
|
|
7
|
+
// Uses import.meta.dir (Bun) to locate package.json relative to this file,
|
|
8
|
+
// which is correct regardless of the process working directory.
|
|
9
|
+
let _version = '1.28.0';
|
|
10
|
+
try {
|
|
11
|
+
const pkg = JSON.parse(readFileSync(join(import.meta.dir, '..', 'package.json'), 'utf-8'));
|
|
12
|
+
// Only trust a version read from OUR OWN package.json. A Bun single-file
|
|
13
|
+
// compiled binary resolves import.meta.dir to a virtual root where the
|
|
14
|
+
// `../package.json` path can land on a DIFFERENT package.json (a bundled
|
|
15
|
+
// dependency's, or the embedded runtime's) that reports a placeholder like
|
|
16
|
+
// "0.0.0" — exactly the wrong-version banner a bare daemon launch showed in
|
|
17
|
+
// the field. Guarding on the package name means the prebuild-baked fallback
|
|
18
|
+
// above wins in that case instead of a stray version.
|
|
19
|
+
if (pkg?.name === 'goodvibes-daemon' && typeof pkg.version === 'string' && pkg.version.length > 0) {
|
|
20
|
+
_version = pkg.version;
|
|
21
|
+
}
|
|
22
|
+
} catch {
|
|
23
|
+
// Compiled binary or missing package.json — use fallback
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const VERSION = _version;
|