-
Notifications
You must be signed in to change notification settings - Fork 1
Fix nectar dormancy: live-reload projects/credentials so post-boot bindings activate without restart #26
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix nectar dormancy: live-reload projects/credentials so post-boot bindings activate without restart #26
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -62,6 +62,7 @@ import type { HiveGraphStore, AsyncHiveGraphStore } from "./hive-graph/store.js" | |
| import { DeepLakeHiveGraphStore } from "./hive-graph/deeplake-store.js"; | ||
| import { InMemoryHiveGraphStore } from "./hive-graph/memory-store.js"; | ||
| import { loadDeepLakeCredentials, credentialsPath } from "./hive-graph/deeplake-credentials.js"; | ||
| import { LiveActiveProjects } from "./hive-graph/live-active-projects.js"; | ||
| import { broodPrereqsFromEnv, formatFirstRunGuidance } from "./brood-prereqs.js"; | ||
| import { resolveNectarTunables } from "./config-file.js"; | ||
| import { resolveProjectScope, type ProjectScopeSource } from "./hive-graph/project-scope.js"; | ||
|
|
@@ -1303,33 +1304,56 @@ async function runDaemon(): Promise<void> { | |
| // factory stands up one brood + watch context per active project, each rooted | ||
| // at its own bound path and scoped to its own tenancy project id. Live brood | ||
| // (describe) deps are attached only when Portkey is configured. | ||
| const controlOptions: ProjectsControlOptions = | ||
| creds !== undefined ? { expect: { org: creds.orgId, workspace: creds.workspaceId } } : {}; | ||
| const broodDeps = | ||
| creds !== undefined && portkey.enabled | ||
| ? { portkey: portkey as PortkeyEnabled, embedProvider, embedModelId: embedModel } | ||
| : undefined; | ||
|
|
||
| const activeProjects = | ||
| durableStore !== undefined && creds !== undefined | ||
| ? { | ||
| resolve: () => readActiveProjects(controlOptions).resolution, | ||
| factory: (project: ResolvedProject): RunningContext => | ||
| createProjectContext({ | ||
| project, | ||
| tenancy: { orgId: creds.orgId, workspaceId: creds.workspaceId, projectId: project.projectId }, | ||
| store: durableStore, | ||
| ...(broodDeps !== undefined ? { broodDeps } : {}), | ||
| log: (line) => process.stderr.write(`${JSON.stringify({ ts: new Date().toISOString(), ...line })}\n`), | ||
| }), | ||
| } | ||
| : undefined; | ||
| // | ||
| // W1-N remediation (live-reload of bindings/credentials without a restart): | ||
| // the seam is wired UNCONDITIONALLY (not gated on a boot-time credentials | ||
| // snapshot) and delegates to `LiveActiveProjects`, which re-resolves | ||
| // `~/.deeplake/credentials.json` on EVERY reconcile tick. So a daemon that | ||
| // booted BEFORE login, then had a project bound afterward, activates that | ||
| // project on its next tick (or immediately, when the credentials watch fires a | ||
| // reconcile the moment credentials appear) - no daemon restart. The tenancy | ||
| // `expect` guard is derived fresh from the currently-resolved credentials, so | ||
| // it is never frozen at boot. Absent creds -> an empty resolution (dormant). | ||
| const liveControlOptions: Omit<ProjectsControlOptions, "expect"> = {}; | ||
| const liveActive = new LiveActiveProjects({ | ||
| loadCredentials: () => { | ||
| try { | ||
| return loadDeepLakeCredentials(); | ||
| } catch { | ||
| return undefined; | ||
| } | ||
| }, | ||
| createStore: (credentials) => new DeepLakeHiveGraphStore({ credentials }), | ||
| buildContext: ({ project, credentials, store }): RunningContext => | ||
| createProjectContext({ | ||
| project, | ||
| tenancy: { orgId: credentials.orgId, workspaceId: credentials.workspaceId, projectId: project.projectId }, | ||
| store, | ||
| ...(portkey.enabled | ||
| ? { broodDeps: { portkey: portkey as PortkeyEnabled, embedProvider, embedModelId: embedModel } } | ||
| : {}), | ||
| log: (line) => process.stderr.write(`${JSON.stringify({ ts: new Date().toISOString(), ...line })}\n`), | ||
| }), | ||
| controlOptions: liveControlOptions, | ||
| onError: (err) => | ||
| process.stderr.write( | ||
| `nectar daemon: active-project resolve failed (non-fatal): ${err instanceof Error ? err.message : String(err)}\n`, | ||
| ), | ||
| }); | ||
| const activeProjects = { | ||
| resolve: () => liveActive.resolve(), | ||
| factory: (project: ResolvedProject): RunningContext => liveActive.factory(project), | ||
| }; | ||
|
|
||
| // The daemon-level enricher stays scoped to the PRIMARY active project at boot | ||
| // (the common single-project case); per-project enrichment for additional | ||
| // active projects is a documented follow-up. Absent creds/Portkey/active set | ||
| // -> the enricher stays dormant (honest). | ||
| const primary = durableStore !== undefined ? readActiveProjects(controlOptions).resolution.active[0] : undefined; | ||
| // The daemon-level enricher + hive-graph API stay scoped to the PRIMARY active | ||
| // project resolved at boot (the common single-project case); per-project | ||
| // enrichment for additional active projects is a documented follow-up. These | ||
| // read the BOOT-time tenancy snapshot (creds at boot); the multi-root brood + | ||
| // watch supervisor above is the credentials-live path that removes the dormancy | ||
| // bug. Absent creds/Portkey/active set -> the enricher stays dormant (honest). | ||
| const bootControlOptions: ProjectsControlOptions = | ||
| creds !== undefined ? { expect: { org: creds.orgId, workspace: creds.workspaceId } } : {}; | ||
| const primary = durableStore !== undefined ? readActiveProjects(bootControlOptions).resolution.active[0] : undefined; | ||
| const primaryTenancy: Tenancy | undefined = | ||
| primary !== undefined && creds !== undefined | ||
| ? { orgId: creds.orgId, workspaceId: creds.workspaceId, projectId: primary.projectId } | ||
|
|
@@ -1348,13 +1372,17 @@ async function runDaemon(): Promise<void> { | |
| // pre-login fleet posture is honest; the watch below flips it healthy the | ||
| // moment credentials appear (a-AC-2), no restart needed. | ||
| // | ||
| // ACCEPTED DEVIATION (W1-N, ledger 2026-07-04): credentials appearing after | ||
| // boot flip /health healthy and reconcile active projects, but the | ||
| // creds-dependent subsystems assembled above from this boot-time snapshot | ||
| // (the durable store, brood wiring, enricher loop) are NOT hot-attached; | ||
| // they stay dormant until the next daemon restart. Same posture as | ||
| // honeycomb. If hot-attach ever lands, it belongs in the credentialsWatch | ||
| // onChange path in daemon.ts. | ||
| // W1-N remediation (live-reload without a daemon restart): the multi-root | ||
| // brood + watch supervisor (the `activeProjects` seam below) now re-resolves | ||
| // credentials + `projects.json` on EVERY reconcile tick via | ||
| // `LiveActiveProjects`, so credentials appearing after boot (and any project | ||
| // bound afterward) DO hot-attach - the daemon starts brooding/watching the | ||
| // newly-active project on its next tick, or immediately when the credentials | ||
| // watch fires `reconcileActiveProjects`. Remaining boot-snapshot scope: the | ||
| // PRIMARY-project enricher loop + hive-graph API assembled below still read | ||
| // the boot-time creds (a documented single-project follow-up), so those two | ||
| // subsystems alone stay dormant until the next restart when creds arrive | ||
| // late. The dormancy-until-restart bug (no brooding at all) is fixed. | ||
| storageCredentialsPresent: creds !== undefined, | ||
| credentialsWatch: { | ||
| probe: () => { | ||
|
|
@@ -1366,7 +1394,7 @@ async function runDaemon(): Promise<void> { | |
| } | ||
| }, | ||
| }, | ||
| ...(activeProjects !== undefined ? { activeProjects } : {}), | ||
| activeProjects, | ||
| ...(live !== undefined && primaryTenancy !== undefined && primary !== undefined | ||
| ? { | ||
| tenancy: primaryTenancy, | ||
|
|
@@ -1400,17 +1428,17 @@ async function runDaemon(): Promise<void> { | |
| // PRD-019b: the projects + brooding-control endpoints (consumed by Hive's | ||
| // dashboard). Persisting a toggle triggers an immediate active-set reconcile. | ||
| try { | ||
| if (activeProjects !== undefined) { | ||
| { | ||
| mountProjectsApi(daemon, { | ||
| view: () => { | ||
| const { resolution, cache } = readActiveProjects(controlOptions); | ||
| const { resolution, cache } = readActiveProjects(bootControlOptions); | ||
| return buildProjectsView(resolution, cache, (id) => daemonWatcherState(daemon, id)); | ||
| }, | ||
| setProject: (projectId, brooding) => { | ||
| persistProjectBrooding(projectId, brooding, controlOptions); | ||
| persistProjectBrooding(projectId, brooding, bootControlOptions); | ||
| }, | ||
| setGlobal: (global) => { | ||
| persistGlobalBrooding(global, controlOptions); | ||
| persistGlobalBrooding(global, bootControlOptions); | ||
|
Comment on lines
+1431
to
+1441
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Confirm expect drives tenancy scoping in projects.json cache resolution,
# and that an empty options ({}) skips the guard.
rg -nP -C4 '\bexpect\b' src/projects-control.ts
fd -t f 'projects-cache*' src -x rg -nP -C4 'expect|tenancy|mismatch' {}Repository: legioncodeinc/nectar Length of output: 1060 🏁 Script executed: #!/bin/bash
set -euo pipefail
# Inspect the relevant areas in src/cli.ts and the projects-control implementation.
sed -n '1330,1460p' src/cli.ts
printf '\n--- projects-control ---\n'
sed -n '1,220p' src/projects-control.tsRepository: legioncodeinc/nectar Length of output: 11643 🏁 Script executed: #!/bin/bash
set -euo pipefail
# Inspect the cache loading and active-project resolution semantics.
ast-grep outline src/hive-graph/project-scope.ts --view expanded
printf '\n---\n'
ast-grep outline src/hive-graph/active-projects.ts --view expanded
printf '\n--- loadProjectsCache / resolveActiveProjects ---\n'
rg -n -C3 'function loadProjectsCache|function resolveActiveProjects|expect|tenancy|binding|workspace|org' src/hive-graph/project-scope.ts src/hive-graph/active-projects.tsRepository: legioncodeinc/nectar Length of output: 19889 🏁 Script executed: #!/bin/bash
set -euo pipefail
# Find the live tenancy source and whether the projects API can reuse it.
rg -n -C3 'liveActive|liveControlOptions|readActiveProjects\(|reconcileActiveProjects|credentialsWatch' src/cli.tsRepository: legioncodeinc/nectar Length of output: 3476 Dashboard 🤖 Prompt for AI Agents |
||
| }, | ||
| reconcile: () => daemon.reconcileActiveProjects(), | ||
| // b-AC-6: the HTTP body carries only this path + a stable reason; the | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,233 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| * W1-N remediation: live-reload of project bindings + credentials WITHOUT a | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| * daemon restart. Proves the credentials-live active-project resolver | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| * (`LiveActiveProjects`) picks up a `projects.json` binding written AFTER "boot" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| * on a subsequent resolve, and that the whole path stays dormant + fail-soft | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| * while credentials are absent. | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Hermetic: a temp `~/.deeplake` cache dir + a temp brooding-state dir + an | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| * injected (mutable) credentials seam. No real home, no network, no fs.watch, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| * no real sleeps - the "next tick" is just a second `resolve()` call. Runs | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| * against the compiled `dist/` output. | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { test } from "node:test"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| import assert from "node:assert/strict"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { tmpdir } from "node:os"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { join } from "node:path"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { LiveActiveProjects } from "../dist/hive-graph/live-active-projects.js"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { assembleDaemon } from "../dist/index.js"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| import type { DeepLakeCredentials } from "../dist/hive-graph/deeplake-credentials.js"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| import type { AsyncHiveGraphStore } from "../dist/hive-graph/store.js"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| import type { ResolvedProject } from "../dist/hive-graph/active-projects.js"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| import type { RunningContext } from "../dist/project-supervisor.js"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { get } from "node:http"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| const CREDS: DeepLakeCredentials = { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| apiUrl: "https://api.deeplake.test", | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| token: "tok-abcd", | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| orgId: "org-1", | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| workspaceId: "ws-1", | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** A trivial async store stand-in (the resolver never queries it; the supervisor holds it). */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const fakeStore = {} as unknown as AsyncHiveGraphStore; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** Write a schema-v1 projects.json binding `path -> projectId` into `dir/projects.json`. */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| function writeProjectsCache(dir: string, org: string, workspace: string, bindings: Array<{ path: string; projectId: string }>): void { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| writeFileSync( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| join(dir, "projects.json"), | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| JSON.stringify({ schemaVersion: 1, org, workspace, bindings, projects: [] }), | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| "utf8", | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| function tempDir(prefix: string): string { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| return mkdtempSync(join(tmpdir(), prefix)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| test("W1-N: a projects.json binding written AFTER boot is picked up on the next resolve (no restart)", () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const cacheDir = tempDir("nectar-live-cache-"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const stateDir = tempDir("nectar-live-state-"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const builtFor: string[] = []; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Credentials are PRESENT here (the daemon booted after login); the point of | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| // this test is that a BINDING added after boot activates without a restart. | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const live = new LiveActiveProjects({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| loadCredentials: () => CREDS, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| createStore: () => fakeStore, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| buildContext: ({ project }): RunningContext => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| builtFor.push(project.projectId); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| return { projectId: project.projectId, path: project.path, watcherState: () => "running", start: async () => {}, stop: async () => {} }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| controlOptions: { cacheDir, broodingState: { dir: stateDir } }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Boot: no bindings yet -> dormant. | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| writeProjectsCache(cacheDir, CREDS.orgId, CREDS.workspaceId, []); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert.equal(live.resolve().active.length, 0, "no bindings => nothing active at boot"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| // A project is bound AFTER boot (honeycomb writes projects.json). | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| writeProjectsCache(cacheDir, CREDS.orgId, CREDS.workspaceId, [{ path: "/work/repo-a", projectId: "proj-a" }]); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| // The NEXT resolve (the reconcile loop's next tick) sees the new binding. | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const after = live.resolve(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert.equal(after.active.length, 1, "the post-boot binding is now active"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert.equal(after.active[0].projectId, "proj-a"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert.equal(after.active[0].path, "/work/repo-a"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| // The factory builds a context for it against the live credentials. | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const ctx = live.factory(after.active[0]); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert.equal(ctx.projectId, "proj-a"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert.deepEqual(builtFor, ["proj-a"]); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } finally { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| rmSync(cacheDir, { recursive: true, force: true }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| rmSync(stateDir, { recursive: true, force: true }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| test("W1-N: with credentials absent the resolver is dormant and fail-soft (empty resolution, factory is a no-op)", async () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const cacheDir = tempDir("nectar-live-cache-"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const stateDir = tempDir("nectar-live-state-"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| let present = false; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| let buildContextCalls = 0; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const live = new LiveActiveProjects({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| loadCredentials: () => (present ? CREDS : undefined), | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| createStore: () => fakeStore, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| buildContext: (): RunningContext => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| buildContextCalls += 1; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw new Error("buildContext must not be reached while credentials are absent"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| controlOptions: { cacheDir, broodingState: { dir: stateDir } }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| // A binding exists on disk, but credentials are ABSENT -> dormant regardless. | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| writeProjectsCache(cacheDir, CREDS.orgId, CREDS.workspaceId, [{ path: "/work/repo-a", projectId: "proj-a" }]); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert.equal(live.resolve().active.length, 0, "no credentials => dormant even with a binding present"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| // A factory reached without creds (a race) yields a no-op context, never a throw. | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const ctx = live.factory({ projectId: "proj-a", path: "/work/repo-a", brooding: "active" }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert.equal(buildContextCalls, 0, "buildContext is skipped without credentials"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert.equal(ctx.watcherState(), "stopped"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| await ctx.start(); // must not throw | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| await ctx.stop(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Credentials appear (login lands): the SAME resolver now activates the binding. | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| present = true; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const after = live.resolve(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert.equal(after.active.length, 1, "credentials appearing activates the already-present binding"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert.equal(after.active[0].projectId, "proj-a"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } finally { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| rmSync(cacheDir, { recursive: true, force: true }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| rmSync(stateDir, { recursive: true, force: true }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| test("W1-N: a mismatched-tenancy cache reads empty (the expect guard is derived from the live credentials)", () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const cacheDir = tempDir("nectar-live-cache-"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const stateDir = tempDir("nectar-live-state-"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const live = new LiveActiveProjects({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| loadCredentials: () => CREDS, // org-1 / ws-1 | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| createStore: () => fakeStore, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| buildContext: ({ project }): RunningContext => ({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| projectId: project.projectId, path: project.path, watcherState: () => "running", start: async () => {}, stop: async () => {}, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }), | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| controlOptions: { cacheDir, broodingState: { dir: stateDir } }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Cache synced for a DIFFERENT tenancy -> the guard drops it (wrong projects). | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| writeProjectsCache(cacheDir, "org-OTHER", "ws-OTHER", [{ path: "/work/repo-a", projectId: "proj-a" }]); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert.equal(live.resolve().active.length, 0, "a tenancy-mismatched cache reads empty"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Re-synced for the live tenancy -> activates (no restart, no reconstruction). | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| writeProjectsCache(cacheDir, CREDS.orgId, CREDS.workspaceId, [{ path: "/work/repo-a", projectId: "proj-a" }]); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert.equal(live.resolve().active.length, 1, "the correctly-scoped cache activates"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } finally { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| rmSync(cacheDir, { recursive: true, force: true }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| rmSync(stateDir, { recursive: true, force: true }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| // ── Integration: the daemon's reconcile loop drives LiveActiveProjects ──────── | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| function fetchHealth(port: number): Promise<{ status: number; body: any }> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| return new Promise((resolve, reject) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| get({ host: "127.0.0.1", port, path: "/health" }, (res) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const chunks: Buffer[] = []; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| res.on("data", (c: Buffer) => chunks.push(c)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| res.on("end", () => resolve({ status: res.statusCode ?? 0, body: JSON.parse(Buffer.concat(chunks).toString("utf8")) })); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }).on("error", reject); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+154
to
+162
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win Harden No request timeout is set, so a hung/unresponsive server would leave the test pending indefinitely rather than failing fast. Also, 🛡️ Proposed fix function fetchHealth(port: number): Promise<{ status: number; body: any }> {
return new Promise((resolve, reject) => {
- get({ host: "127.0.0.1", port, path: "/health" }, (res) => {
+ const req = get({ host: "127.0.0.1", port, path: "/health", timeout: 2000 }, (res) => {
const chunks: Buffer[] = [];
res.on("data", (c: Buffer) => chunks.push(c));
- res.on("end", () => resolve({ status: res.statusCode ?? 0, body: JSON.parse(Buffer.concat(chunks).toString("utf8")) }));
+ res.on("end", () => {
+ try {
+ resolve({ status: res.statusCode ?? 0, body: JSON.parse(Buffer.concat(chunks).toString("utf8")) });
+ } catch (err) {
+ reject(err);
+ }
+ });
}).on("error", reject);
+ req.on("timeout", () => req.destroy(new Error("fetchHealth timed out")));
});
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** A timer that captures scheduled callbacks without firing them (reconcile is driven explicitly). */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| function inertTimer() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| return { set: (_fn: () => void, _ms: number) => 1, clear: (_h: unknown) => {} }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| test("W1-N integration: a daemon booted dormant activates a post-boot binding on a driven reconcile tick", async () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const runtimeDir = tempDir("nectar-live-daemon-"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const cacheDir = tempDir("nectar-live-cache-"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const stateDir = tempDir("nectar-live-state-"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const events: string[] = []; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Credentials appear AFTER boot (login lands late). | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| let present = false; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| const live = new LiveActiveProjects({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| loadCredentials: () => (present ? CREDS : undefined), | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| createStore: () => fakeStore, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| buildContext: ({ project }): RunningContext => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| let state: "stopped" | "running" = "stopped"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| projectId: project.projectId, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| path: project.path, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| watcherState: () => state, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| start: async () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| events.push(`start:${project.projectId}`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| state = "running"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| stop: async () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| events.push(`stop:${project.projectId}`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| state = "stopped"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| controlOptions: { cacheDir, broodingState: { dir: stateDir } }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| const daemon = assembleDaemon({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| port: 0, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| runtimeDir, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| log: () => {}, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| activeProjects: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| resolve: () => live.resolve(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| factory: (p: ResolvedProject) => live.factory(p), | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| timer: inertTimer(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const port = await daemon.start(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Dormant at boot: no credentials on disk yet. | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert.equal((await fetchHealth(port)).body.activeProjects.count, 0); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Login lands and honeycomb binds a project (both appear on disk). | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| present = true; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| writeProjectsCache(cacheDir, CREDS.orgId, CREDS.workspaceId, [{ path: "/work/repo-a", projectId: "proj-a" }]); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| // The reconcile loop's next tick (driven explicitly) activates the project - | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| // no daemon restart. | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| await daemon.reconcileActiveProjects(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert.deepEqual(events, ["start:proj-a"]); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const health = await fetchHealth(port); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert.equal(health.body.activeProjects.count, 1); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert.equal(health.body.activeProjects.projects[0].projectId, "proj-a"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert.equal(health.body.activeProjects.projects[0].watcher, "running"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } finally { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| await daemon.shutdown(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| rmSync(runtimeDir, { recursive: true, force: true }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| rmSync(cacheDir, { recursive: true, force: true }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| rmSync(stateDir, { recursive: true, force: true }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🗄️ Data Integrity & Integration | 🔴 Critical | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
Repository: legioncodeinc/nectar
Length of output: 50377
🏁 Script executed:
Repository: legioncodeinc/nectar
Length of output: 26253
🏁 Script executed:
Repository: legioncodeinc/nectar
Length of output: 1647
🏁 Script executed:
Repository: legioncodeinc/nectar
Length of output: 30919
🏁 Script executed:
Repository: legioncodeinc/nectar
Length of output: 1183
Reuse the boot-time Deep Lake store for the primary active project
LiveActiveProjects.createStorealways builds a newDeepLakeHiveGraphStore, while the primary enricher still uses the boot-timedurableStore(src/cli.ts:1326-1331,src/cli.ts:1361-1414). That splitsseqHighWateracross two async store instances for the same tenancy, so the registration bridge and enricher can still mint the same(nectar, seq)under Deep Lake read-after-write lag. Reuse the boot store when creds match, or route both paths through one shared store instance.🤖 Prompt for AI Agents