Merge nucleic/humble-harbor-viper into dev
This commit is contained in:
@@ -98,6 +98,8 @@ private struct RunnerSection: View {
|
||||
.defaultMaxContainers
|
||||
@AppStorage(RunnerSettings.balancerPolicyKey) private var balancerPolicy = RunnerBalancerPolicy
|
||||
.leastSessions.rawValue
|
||||
@AppStorage(RunnerSettings.intelligenceModeKey) private var intelligenceMode =
|
||||
RunnerIntelligenceMode.mesh.rawValue
|
||||
// Dev/self-host override; empty ⇒ the production control plane.
|
||||
@AppStorage(RunnerSettings.controlURLKey) private var controlURL = ""
|
||||
|
||||
@@ -136,6 +138,22 @@ private struct RunnerSection: View {
|
||||
.tag(RunnerBalancerPolicy.roundRobin.rawValue)
|
||||
}
|
||||
|
||||
// ANTIMATTER_RUNNER §5, item 6: how the runner replaces Apple Foundation
|
||||
// Models. Rides the same debounced settings push; the container picks it up on
|
||||
// its next boot as NUCLEIC_RUNNER_INTELLIGENCE_MODE.
|
||||
Picker("Runner intelligence", selection: $intelligenceMode) {
|
||||
ForEach(RunnerIntelligenceMode.allCases, id: \.rawValue) { mode in
|
||||
Text(mode.displayName).tag(mode.rawValue)
|
||||
}
|
||||
}
|
||||
Text(
|
||||
"How the runner names chats and writes summaries. “Your devices” delegates "
|
||||
+ "to this Mac / your iPhone over the mesh (background work may run on the "
|
||||
+ "phone; time-sensitive work stays on Macs); “Agent” spends a small model's "
|
||||
+ "tokens on the runner itself. Applies when the runner container next boots.")
|
||||
.font(.caption).foregroundStyle(.secondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
|
||||
if isDevBuild {
|
||||
TextField(
|
||||
"Control URL (dev)", text: $controlURL,
|
||||
@@ -214,6 +232,7 @@ private struct RunnerSection: View {
|
||||
// Knob changes ride to the pool debounced; the DO re-clamps server-side.
|
||||
.onChange(of: maxContainers) { _, _ in pushKnobs() }
|
||||
.onChange(of: balancerPolicy) { _, _ in pushKnobs() }
|
||||
.onChange(of: intelligenceMode) { _, _ in pushKnobs() }
|
||||
.onChange(of: runnerEnabled) { _, on in if on { refreshSharedCredentials() } }
|
||||
.onAppear { if runnerEnabled { refreshSharedCredentials() } }
|
||||
}
|
||||
@@ -274,7 +293,8 @@ private struct RunnerSection: View {
|
||||
do {
|
||||
try await store.runnerPoolClient().pushSettings(
|
||||
maxContainers: maxContainers,
|
||||
balancerPolicy: RunnerBalancerPolicy(rawValue: balancerPolicy))
|
||||
balancerPolicy: RunnerBalancerPolicy(rawValue: balancerPolicy),
|
||||
intelligenceMode: RunnerIntelligenceMode(rawValue: intelligenceMode))
|
||||
poolError = nil
|
||||
} catch {
|
||||
poolError = error.localizedDescription
|
||||
|
||||
@@ -1121,11 +1121,20 @@ public enum RunnerSettings {
|
||||
/// The runner's `IntelligenceProviding` choice (ANTIMATTER_RUNNER §5): `agent` runs a small
|
||||
/// SKU of the user's authenticated agent behind strict templates, `mesh` delegates to
|
||||
/// Apple-Intelligence-capable devices on the mesh, `heuristic` skips models entirely. Modes
|
||||
/// 1–2 always terminate in the heuristic fallback when they can't serve.
|
||||
/// 1–2 always terminate in the heuristic fallback when they can't serve. Mirrors the
|
||||
/// `IntelligenceMode` union in `cloud/nucleic-runner/src/pool.ts` (keep in lockstep).
|
||||
public enum RunnerIntelligenceMode: String, CaseIterable, Sendable {
|
||||
case agent
|
||||
case mesh
|
||||
case heuristic
|
||||
|
||||
public var displayName: String {
|
||||
switch self {
|
||||
case .agent: return "Agent (small model)"
|
||||
case .mesh: return "Your devices (Apple Intelligence)"
|
||||
case .heuristic: return "Off (heuristics only)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A registered git repository plus per-project configuration.
|
||||
|
||||
@@ -107,15 +107,17 @@ public actor RunnerPoolClient {
|
||||
|
||||
/// Push the Settings knobs. The DO re-clamps server-side (`clampSettings` — same 1…16
|
||||
/// bounds as `RunnerSettings.maxContainersRange`; keep them in lockstep), so a stale client
|
||||
/// can't exceed the cap.
|
||||
/// can't exceed the cap. `intelligenceMode` reaches a container as
|
||||
/// `NUCLEIC_RUNNER_INTELLIGENCE_MODE` on its next boot (ANTIMATTER_RUNNER §5, item 6).
|
||||
public func pushSettings(
|
||||
maxContainers: Int? = nil, balancerPolicy: RunnerBalancerPolicy? = nil,
|
||||
idleTimeoutSeconds: Int? = nil
|
||||
idleTimeoutSeconds: Int? = nil, intelligenceMode: RunnerIntelligenceMode? = nil
|
||||
) async throws {
|
||||
var patch: [String: Any] = [:]
|
||||
if let maxContainers { patch["maxContainers"] = maxContainers }
|
||||
if let balancerPolicy { patch["balancerPolicy"] = balancerPolicy.rawValue }
|
||||
if let idleTimeoutSeconds { patch["idleTimeoutSeconds"] = idleTimeoutSeconds }
|
||||
if let intelligenceMode { patch["intelligenceMode"] = intelligenceMode.rawValue }
|
||||
_ = try await authed("PATCH", "/v1/pool/settings", body: patch)
|
||||
}
|
||||
|
||||
@@ -257,6 +259,9 @@ public struct RunnerPoolStatus: Decodable, Sendable, Equatable {
|
||||
public let maxContainers: Int
|
||||
public let balancerPolicy: String
|
||||
public let idleTimeoutSeconds: Int
|
||||
/// "mesh" | "agent" | "heuristic" (ANTIMATTER_RUNNER §5); nil from a Worker that
|
||||
/// predates the knob.
|
||||
public let intelligenceMode: String?
|
||||
}
|
||||
|
||||
public struct Instance: Decodable, Sendable, Equatable, Identifiable {
|
||||
|
||||
@@ -17,7 +17,15 @@ import FoundationNetworking
|
||||
/// static musl Linux build.
|
||||
///
|
||||
/// nucleic-smoke '<pairing-code>' --prompt "run `echo hi` in the shell" [--project name]
|
||||
/// [--model sku] [--timeout 300] [--deny]
|
||||
/// [--model sku] [--timeout 300] [--deny] [--expect-intelligence]
|
||||
///
|
||||
/// It also plays an **intelligence worker** (ANTIMATTER_RUNNER §5, item 6): it advertises
|
||||
/// `canProvideIntelligence` (desktop class, so every priority tier is eligible) and answers any
|
||||
/// delegated `intelligenceRequest` with deterministic canned output — no model needed; the point
|
||||
/// is proving the wire path. With `--expect-intelligence` the exercise PASSES when a delegated
|
||||
/// `sessionName` job round-trips end to end: request received here, canned title answered, and
|
||||
/// the host's `sessionUpdated` comes back carrying that exact title. The agent turn itself may
|
||||
/// error (a bare container has no `claude`); the naming path is what's under test.
|
||||
@main
|
||||
struct Smoke {
|
||||
struct Options {
|
||||
@@ -27,8 +35,14 @@ struct Smoke {
|
||||
var model: String?
|
||||
var timeoutSeconds: Double = 300
|
||||
var deny = false // answer approvals with deny instead of allow
|
||||
/// PASS on a delegated-intelligence round trip instead of on turn completion.
|
||||
var expectIntelligence = false
|
||||
}
|
||||
|
||||
/// The canned title the worker answers a delegated `sessionName` job with — and the exact
|
||||
/// string `--expect-intelligence` waits to see come back in a `sessionUpdated`.
|
||||
static let cannedSessionTitle = "Mesh Delegation Proven"
|
||||
|
||||
static func main() async {
|
||||
var options = Options()
|
||||
var args = Array(CommandLine.arguments.dropFirst())
|
||||
@@ -41,6 +55,7 @@ struct Smoke {
|
||||
case "--timeout":
|
||||
options.timeoutSeconds = Double(args.isEmpty ? "" : args.removeFirst()) ?? 300
|
||||
case "--deny": options.deny = true
|
||||
case "--expect-intelligence": options.expectIntelligence = true
|
||||
default:
|
||||
if options.pairingCode.isEmpty, arg.contains("d=") { options.pairingCode = arg }
|
||||
else { fail("unknown argument: \(arg)") }
|
||||
@@ -48,7 +63,7 @@ struct Smoke {
|
||||
}
|
||||
guard !options.pairingCode.isEmpty else {
|
||||
fail("usage: nucleic-smoke '<nucleic://pair?d=…>' [--prompt …] [--project …] "
|
||||
+ "[--model …] [--timeout seconds] [--deny]")
|
||||
+ "[--model …] [--timeout seconds] [--deny] [--expect-intelligence]")
|
||||
}
|
||||
|
||||
let payload: PairingPayload
|
||||
@@ -70,7 +85,14 @@ struct Smoke {
|
||||
channel: channel, identity: identity, hostStaticKey: payload.hostStaticKey,
|
||||
mode: .pair(secret: payload.pairingSecret),
|
||||
deviceID: DeviceIdentity.hostID(ofStaticKey: identity.staticPublicKey),
|
||||
deviceLabel: "nucleic-smoke", scopeClaim: .control)
|
||||
deviceLabel: "nucleic-smoke", scopeClaim: .control,
|
||||
// Offer this process as a mesh AFM worker (ANTIMATTER_RUNNER §5): desktop class so
|
||||
// every priority tier may land here, with a fake top-rank chip so a mesh-mode
|
||||
// runner's queue prefers it. Answers are canned — the wire path is what's tested.
|
||||
clientCaps: WireClientCapabilities(
|
||||
mesh: 1, canProvideIntelligence: true,
|
||||
intelligenceProfile: IntelligenceWorkerProfile(
|
||||
deviceClass: .desktop, chip: "Apple M4 Max")))
|
||||
|
||||
// Hard deadline for the whole exercise — a hung relay/turn must not wedge CI.
|
||||
let timeout = Task {
|
||||
@@ -90,6 +112,7 @@ struct Smoke {
|
||||
var sessionID: SessionID?
|
||||
var sawAssistantActivity = false
|
||||
var approvalsAnswered = 0
|
||||
var intelligenceServed = 0
|
||||
|
||||
for await event in await client.start() {
|
||||
switch event {
|
||||
@@ -124,6 +147,14 @@ struct Smoke {
|
||||
projectID: project.id, message: options.prompt, model: options.model,
|
||||
useWorktree: true, auto: false)))
|
||||
case .sessionUpdated(let summary):
|
||||
// The delegated-naming proof (--expect-intelligence): the canned title we
|
||||
// answered the sessionName job with came back as the session's actual name —
|
||||
// the job round-tripped submit → queue → this worker → rename → broadcast.
|
||||
if options.expectIntelligence, summary.title == cannedSessionTitle {
|
||||
log("PASS — delegated intelligence round-tripped: session renamed to "
|
||||
+ "“\(summary.title)” (\(intelligenceServed) request(s) served)")
|
||||
exit(0)
|
||||
}
|
||||
// The chat we started is the session that appears (or changes) after startChat.
|
||||
if sessionID == nil, startedChat, summary.status.hasTurnInFlight {
|
||||
sessionID = summary.sessionID
|
||||
@@ -135,6 +166,12 @@ struct Smoke {
|
||||
guard summary.sessionID == sessionID else { break }
|
||||
log("status: \(summary.status.rawValue)")
|
||||
if summary.status == .error {
|
||||
// Under --expect-intelligence the turn is allowed to die (a bare container
|
||||
// has no agent CLI) — the naming push can still land after the error.
|
||||
if options.expectIntelligence {
|
||||
log("session errored (tolerated — waiting on the naming round trip)")
|
||||
break
|
||||
}
|
||||
log("FAIL — session errored")
|
||||
exit(1)
|
||||
}
|
||||
@@ -160,6 +197,11 @@ struct Smoke {
|
||||
}
|
||||
case .approvalRequested(let approval):
|
||||
answer(approval, client: client, deny: options.deny, count: &approvalsAnswered)
|
||||
case .intelligenceRequest(let request):
|
||||
// A mesh-mode runner delegated one AFM job here (ANTIMATTER_RUNNER §5). Render
|
||||
// the shared template (proves the kind decodes) and answer canned output.
|
||||
intelligenceServed += 1
|
||||
await serveIntelligence(request, client: client)
|
||||
case .credentialNeeded(let need):
|
||||
// The runner lacks credentials (ANTIMATTER_RUNNER §6) — play the holding
|
||||
// device: read the local files, seal each requested kind to the runner's key,
|
||||
@@ -181,6 +223,32 @@ struct Smoke {
|
||||
}
|
||||
}
|
||||
|
||||
/// Answer one delegated intelligence job with deterministic canned output — the worker
|
||||
/// half of the mesh AFM queue, minus the model. `sessionName` answers the canned title the
|
||||
/// `--expect-intelligence` proof watches for; `classifyTurn` answers the template's DONE
|
||||
/// token; anything else gets a one-line placeholder. An unknown kind answers with `error`
|
||||
/// set, exactly as a real executor does.
|
||||
private static func serveIntelligence(
|
||||
_ request: WireIntelligenceRequest, client: SyncClient
|
||||
) async {
|
||||
let priority = request.priority?.rawValue ?? "background"
|
||||
guard IntelligenceDelegate.job(for: request) != nil else {
|
||||
log("intelligence request \(request.kind.rawValue) (\(priority)) — unknown kind, answering error")
|
||||
await client.send(.intelligenceResult(WireIntelligenceResult(
|
||||
id: request.id, error: "unsupported kind: \(request.kind.rawValue)")))
|
||||
return
|
||||
}
|
||||
let output: String
|
||||
switch request.kind {
|
||||
case .sessionName: output = cannedSessionTitle
|
||||
case .classifyTurn: output = "DONE"
|
||||
default: output = "Delegated \(request.kind.rawValue) output"
|
||||
}
|
||||
log("intelligence request \(request.kind.rawValue) (\(priority)) — answering “\(output)”")
|
||||
await client.send(.intelligenceResult(WireIntelligenceResult(
|
||||
id: request.id, outputs: [output])))
|
||||
}
|
||||
|
||||
/// Answer a `credentialNeeded`: seal every requested kind we can satisfy from this
|
||||
/// machine's own files to the runner's sealing key (AAD = the kind string, so a box can't
|
||||
/// be replayed as another kind) and send one provision envelope.
|
||||
|
||||
@@ -29,6 +29,9 @@ export function buildContainerEnv(base: {
|
||||
instanceToken: string;
|
||||
controlPlaneURL: string;
|
||||
relayURL?: string;
|
||||
/** How nucleicd replaces AFMs (ANTIMATTER_RUNNER §5, item 6): "mesh" | "agent" |
|
||||
* "heuristic", from the pool settings. Omitted ⇒ nucleicd's own default (mesh). */
|
||||
intelligenceMode?: string;
|
||||
}): Record<string, string> {
|
||||
const env: Record<string, string> = {
|
||||
NUCLEIC_RUNNER_POOL_ID: base.poolId,
|
||||
@@ -38,6 +41,7 @@ export function buildContainerEnv(base: {
|
||||
NUCLEIC_RUNNER_CONTROL_URL: base.controlPlaneURL,
|
||||
};
|
||||
if (base.relayURL) env.NUCLEIC_RELAY_URL = base.relayURL;
|
||||
if (base.intelligenceMode) env.NUCLEIC_RUNNER_INTELLIGENCE_MODE = base.intelligenceMode;
|
||||
return env;
|
||||
}
|
||||
|
||||
|
||||
@@ -189,6 +189,7 @@ function makeDriver(env: Env): ContainerDriver {
|
||||
epoch: extraEnv.NUCLEIC_RUNNER_EPOCH ?? "0",
|
||||
instanceToken: extraEnv.NUCLEIC_RUNNER_INSTANCE_TOKEN ?? "",
|
||||
controlPlaneURL: "https://runner.nucleic.blakeslee.xyz",
|
||||
intelligenceMode: extraEnv.NUCLEIC_RUNNER_INTELLIGENCE_MODE,
|
||||
});
|
||||
const res = await stub.fetch("https://container/start", {
|
||||
method: "POST",
|
||||
|
||||
@@ -12,6 +12,12 @@
|
||||
|
||||
export type BalancerPolicy = "least-sessions" | "round-robin";
|
||||
|
||||
/** How a runner replaces Apple Foundation Models (docs/ANTIMATTER_RUNNER.md §5, item 6).
|
||||
* Mirrors Swift's `RunnerIntelligenceMode` (keep in lockstep). Reaches the container as
|
||||
* `NUCLEIC_RUNNER_INTELLIGENCE_MODE` on its next boot — a settings change never restarts a
|
||||
* running container; the scale-to-zero lifecycle re-boots it soon enough. */
|
||||
export type IntelligenceMode = "mesh" | "agent" | "heuristic";
|
||||
|
||||
export interface PoolSettings {
|
||||
/** Total containers the pool may run at once, host-of-record included. 1 ⇒ tier-0 only:
|
||||
* every session runs in place inside the host container. Mirrors `nucleic.macvm.maxConcurrent`. */
|
||||
@@ -19,12 +25,14 @@ export interface PoolSettings {
|
||||
balancerPolicy: BalancerPolicy;
|
||||
/** Session sandboxes idle (zero sessions) longer than this are reaped by the alarm. */
|
||||
idleTimeoutSeconds: number;
|
||||
intelligenceMode: IntelligenceMode;
|
||||
}
|
||||
|
||||
export const DEFAULT_SETTINGS: PoolSettings = {
|
||||
maxContainers: 4,
|
||||
balancerPolicy: "least-sessions",
|
||||
idleTimeoutSeconds: 600,
|
||||
intelligenceMode: "mesh",
|
||||
};
|
||||
|
||||
export const MAX_CONTAINERS_RANGE = { min: 1, max: 16 } as const;
|
||||
@@ -43,6 +51,14 @@ export function clampSettings(current: PoolSettings, patch: Partial<PoolSettings
|
||||
if (patch && typeof patch.idleTimeoutSeconds === "number" && Number.isFinite(patch.idleTimeoutSeconds)) {
|
||||
next.idleTimeoutSeconds = Math.min(IDLE_TIMEOUT_RANGE.max, Math.max(IDLE_TIMEOUT_RANGE.min, Math.floor(patch.idleTimeoutSeconds)));
|
||||
}
|
||||
if (
|
||||
patch &&
|
||||
(patch.intelligenceMode === "mesh" ||
|
||||
patch.intelligenceMode === "agent" ||
|
||||
patch.intelligenceMode === "heuristic")
|
||||
) {
|
||||
next.intelligenceMode = patch.intelligenceMode;
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
@@ -169,7 +185,10 @@ export class RunnerPoolCore {
|
||||
}
|
||||
|
||||
private async settings(): Promise<PoolSettings> {
|
||||
return (await this.state.storage.get<PoolSettings>(K_SETTINGS)) ?? { ...DEFAULT_SETTINGS };
|
||||
// Merge over defaults so a record persisted before a field existed (e.g. an older pool
|
||||
// predating `intelligenceMode`) reads the default rather than `undefined`.
|
||||
const stored = await this.state.storage.get<Partial<PoolSettings>>(K_SETTINGS);
|
||||
return { ...DEFAULT_SETTINGS, ...(stored ?? {}) };
|
||||
}
|
||||
|
||||
private async instances(): Promise<InstanceRecord[]> {
|
||||
@@ -257,6 +276,7 @@ export class RunnerPoolCore {
|
||||
if (existing && existing.state !== "stopped") {
|
||||
return json({ instanceId: "host", epoch: existing.epoch, alreadyRunning: true });
|
||||
}
|
||||
const settings = await this.settings();
|
||||
const epoch = await this.bumpEpoch();
|
||||
const token = await this.deps.mintInstanceToken(this.poolId, "host");
|
||||
await this.deps.driver.start(this.poolId, "host", {
|
||||
@@ -264,6 +284,7 @@ export class RunnerPoolCore {
|
||||
NUCLEIC_RUNNER_INSTANCE_ID: "host",
|
||||
NUCLEIC_RUNNER_EPOCH: String(epoch),
|
||||
NUCLEIC_RUNNER_INSTANCE_TOKEN: token,
|
||||
NUCLEIC_RUNNER_INTELLIGENCE_MODE: settings.intelligenceMode,
|
||||
});
|
||||
const now = this.now();
|
||||
const record: InstanceRecord = {
|
||||
@@ -321,6 +342,7 @@ export class RunnerPoolCore {
|
||||
NUCLEIC_RUNNER_INSTANCE_ID: placement.instanceId,
|
||||
NUCLEIC_RUNNER_EPOCH: String(epoch),
|
||||
NUCLEIC_RUNNER_INSTANCE_TOKEN: token,
|
||||
NUCLEIC_RUNNER_INTELLIGENCE_MODE: settings.intelligenceMode,
|
||||
});
|
||||
const now = this.now();
|
||||
const record: InstanceRecord = {
|
||||
|
||||
@@ -10,6 +10,7 @@ test("buildContainerEnv carries the full instance contract", () => {
|
||||
instanceToken: "tok",
|
||||
controlPlaneURL: "https://runner.example",
|
||||
relayURL: "https://relay.example",
|
||||
intelligenceMode: "agent",
|
||||
});
|
||||
assert.deepEqual(env, {
|
||||
NUCLEIC_RUNNER_POOL_ID: "p1",
|
||||
@@ -18,10 +19,11 @@ test("buildContainerEnv carries the full instance contract", () => {
|
||||
NUCLEIC_RUNNER_INSTANCE_TOKEN: "tok",
|
||||
NUCLEIC_RUNNER_CONTROL_URL: "https://runner.example",
|
||||
NUCLEIC_RELAY_URL: "https://relay.example",
|
||||
NUCLEIC_RUNNER_INTELLIGENCE_MODE: "agent",
|
||||
});
|
||||
});
|
||||
|
||||
test("relay URL is optional (production default lives in nucleicd)", () => {
|
||||
test("relay URL and intelligence mode are optional (defaults live in nucleicd)", () => {
|
||||
const env = buildContainerEnv({
|
||||
poolId: "p1",
|
||||
instanceId: "host",
|
||||
@@ -30,6 +32,7 @@ test("relay URL is optional (production default lives in nucleicd)", () => {
|
||||
controlPlaneURL: "https://runner.example",
|
||||
});
|
||||
assert.equal("NUCLEIC_RELAY_URL" in env, false);
|
||||
assert.equal("NUCLEIC_RUNNER_INTELLIGENCE_MODE" in env, false);
|
||||
});
|
||||
|
||||
test("nucleicd control port is pinned (pairing proxy contract)", () => {
|
||||
|
||||
@@ -40,6 +40,20 @@ test("clampSettings bounds maxContainers and idle timeout, keeps unknown-free",
|
||||
assert.deepEqual(s4, DEFAULT_SETTINGS);
|
||||
});
|
||||
|
||||
test("clampSettings accepts only known intelligence modes (item 6)", () => {
|
||||
assert.equal(DEFAULT_SETTINGS.intelligenceMode, "mesh");
|
||||
const agent = clampSettings(DEFAULT_SETTINGS, { intelligenceMode: "agent" });
|
||||
assert.equal(agent.intelligenceMode, "agent");
|
||||
const heuristic = clampSettings(agent, { intelligenceMode: "heuristic" });
|
||||
assert.equal(heuristic.intelligenceMode, "heuristic");
|
||||
// Garbage keeps the current value — the DO never trusts a client.
|
||||
const bogus = clampSettings(agent, { intelligenceMode: "gpt-99" as never });
|
||||
assert.equal(bogus.intelligenceMode, "agent");
|
||||
// A patch that doesn't mention it leaves it untouched.
|
||||
const untouched = clampSettings(agent, { maxContainers: 2 });
|
||||
assert.equal(untouched.intelligenceMode, "agent");
|
||||
});
|
||||
|
||||
test("least-sessions places on the emptiest live sandbox, ties break deterministically", () => {
|
||||
const placement = placeSandbox({
|
||||
instances: [
|
||||
@@ -179,12 +193,44 @@ test("provision boots the host container once and is idempotent", async () => {
|
||||
assert.equal(first.alreadyRunning, false);
|
||||
assert.equal(started.length, 1);
|
||||
assert.equal(started[0].env.NUCLEIC_RUNNER_INSTANCE_TOKEN, "tok-pool-1-host");
|
||||
// Intelligence mode rides every boot (item 6) — the default when never patched.
|
||||
assert.equal(started[0].env.NUCLEIC_RUNNER_INTELLIGENCE_MODE, "mesh");
|
||||
|
||||
const second = (await (await core.fetch(req("POST", "/provision", "pool"))).json()) as Record<string, unknown>;
|
||||
assert.equal(second.alreadyRunning, true);
|
||||
assert.equal(started.length, 1);
|
||||
});
|
||||
|
||||
test("a patched intelligence mode reaches the next container boot (item 6)", async () => {
|
||||
const { core, started } = makeCore();
|
||||
const patched = (await (
|
||||
await core.fetch(req("PATCH", "/settings", "pool", { intelligenceMode: "agent" }))
|
||||
).json()) as { settings: PoolSettings };
|
||||
assert.equal(patched.settings.intelligenceMode, "agent");
|
||||
|
||||
await core.fetch(req("POST", "/provision", "pool"));
|
||||
assert.equal(started[0].env.NUCLEIC_RUNNER_INTELLIGENCE_MODE, "agent");
|
||||
|
||||
// Sandboxes booted by acquire carry it too.
|
||||
const boot = (await (await core.fetch(req("POST", "/sandbox/acquire", "instance:host"))).json()) as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
assert.equal(boot.decision, "boot");
|
||||
assert.equal(started[1].env.NUCLEIC_RUNNER_INTELLIGENCE_MODE, "agent");
|
||||
});
|
||||
|
||||
test("settings stored before the intelligenceMode field read the default", async () => {
|
||||
const { core, storage } = makeCore();
|
||||
// A pre-item-6 pool persisted a settings record without the field.
|
||||
await storage.put("settings", { maxContainers: 2, balancerPolicy: "round-robin", idleTimeoutSeconds: 120 });
|
||||
const status = (await (await core.fetch(req("GET", "/status", "pool"))).json()) as {
|
||||
settings: PoolSettings;
|
||||
};
|
||||
assert.equal(status.settings.intelligenceMode, "mesh");
|
||||
assert.equal(status.settings.maxContainers, 2);
|
||||
});
|
||||
|
||||
test("acquire boots a sandbox, then places on it once it heartbeats ready", async () => {
|
||||
let now = NOW;
|
||||
const { core, started } = makeCore({ now: () => now });
|
||||
|
||||
Reference in New Issue
Block a user