@teamscala/service-boot 0.23.329 → 0.23.331
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/package.json +538 -522
- package/src/agreement-gate.d.ts +19 -0
- package/src/asset-cache-key.d.ts +11 -0
- package/src/asset-optimize.d.ts +17 -0
- package/src/asset-proxy.d.ts +11 -0
- package/src/asset-social-card.d.ts +17 -0
- package/src/attach-agreement-magic.d.ts +20 -0
- package/src/attach-portal-members.d.ts +23 -0
- package/src/attach-proposal-accept.d.ts +26 -0
- package/src/attach-proposal-magic.d.ts +18 -0
- package/src/attach-transfers-upload.d.ts +27 -0
- package/src/auth-gate.d.ts +28 -0
- package/src/auth-gate.ts +6 -68
- package/src/bind-page-handler.d.ts +18 -0
- package/src/build-client-assets/input-hashing.d.ts +25 -0
- package/src/build-client-assets/source-candidates.d.ts +27 -0
- package/src/build-client-assets.d.ts +50 -0
- package/src/build-client-assets.test.ts +1 -1
- package/src/build-client-assets.ts +1 -1
- package/src/compose-page-title.d.ts +41 -0
- package/src/content-gate/access.d.ts +15 -0
- package/src/content-gate/config-types.d.ts +99 -0
- package/src/content-gate/gate.d.ts +13 -0
- package/src/content-gate/parse-cookie.d.ts +9 -0
- package/src/content-gate/token.d.ts +48 -0
- package/src/content-page-floor.d.ts +39 -0
- package/src/discover-blocks.d.ts +28 -0
- package/src/discover-blocks.ts +1 -1
- package/src/expand-roster.d.ts +34 -0
- package/src/fn-routers/agreement-completion-email.d.ts +14 -0
- package/src/fn-routers/agreement-send-router.d.ts +82 -0
- package/src/fn-routers/ai-health-router.d.ts +35 -0
- package/src/fn-routers/auth-router.d.ts +47 -0
- package/src/fn-routers/cache-admin-router.d.ts +57 -0
- package/src/fn-routers/chat-session-router.d.ts +219 -0
- package/src/fn-routers/email-defaults.d.ts +1 -0
- package/src/fn-routers/email-link-ttl.d.ts +1 -0
- package/src/fn-routers/index.d.ts +18 -0
- package/src/fn-routers/index.ts +0 -2
- package/src/fn-routers/panel-data-router.d.ts +19 -0
- package/src/fn-routers/persist-chat-messages.d.ts +36 -0
- package/src/fn-routers/plans-router.d.ts +25 -0
- package/src/fn-routers/proposal-send-router.d.ts +58 -0
- package/src/fn-routers/public-agreement-router.d.ts +289 -0
- package/src/fn-routers/public-assessment-router.d.ts +68 -0
- package/src/fn-routers/public-gpt-session-router.d.ts +143 -0
- package/src/fn-routers/sign-in-flow-router.d.ts +107 -0
- package/src/fn-routers/task-sync-assign.d.ts +305 -0
- package/src/fn-routers/task-sync-assign.test.ts +1 -2
- package/src/fn-routers/task-sync-assign.ts +4 -4
- package/src/fn-routers/task-sync-bind.test.ts +18 -18
- package/src/fn-routers/task-sync-brief.d.ts +96 -0
- package/src/fn-routers/task-sync-brief.test.ts +17 -20
- package/src/fn-routers/task-sync-brief.ts +14 -15
- package/src/fn-routers/task-sync-close.d.ts +41 -0
- package/src/fn-routers/task-sync-dispatch.d.ts +61 -0
- package/src/fn-routers/task-sync-dispatch.test.ts +33 -51
- package/src/fn-routers/task-sync-dispatch.ts +124 -0
- package/src/fn-routers/task-sync-lane.d.ts +49 -0
- package/src/fn-routers/task-sync-lane.ts +125 -0
- package/src/fn-routers/task-sync-projection.d.ts +121 -0
- package/src/fn-routers/task-sync-projection.test.ts +0 -26
- package/src/fn-routers/task-sync-projection.ts +0 -51
- package/src/fn-routers/task-sync-rename.d.ts +91 -0
- package/src/fn-routers/task-sync-router.d.ts +444 -0
- package/src/fn-routers/task-sync-router.test.ts +2 -14
- package/src/fn-routers/task-sync-router.ts +1111 -0
- package/src/fn-routers/tasks-router.d.ts +25 -0
- package/src/fn-routers/user-preferences-router.d.ts +77 -0
- package/src/fn-routers/work-items-router.d.ts +102 -0
- package/src/gate-ttls.d.ts +6 -0
- package/src/gpt-gate.d.ts +25 -0
- package/src/handlers/bun-stable-watch.d.ts +1 -0
- package/src/handlers/pane-outbox-drain.d.ts +1 -0
- package/src/handlers/projects-digest.d.ts +26 -0
- package/src/handlers/prune-stale-data.d.ts +30 -0
- package/src/handlers/registry-stale-inactive-sweep.d.ts +1 -0
- package/src/handlers/scala-tools-currency-watch.d.ts +1 -0
- package/src/handlers/singleton-tools-currency-watch.d.ts +1 -0
- package/src/handlers/sweep-event-log.d.ts +1 -0
- package/src/handlers/sweep-host-cache.d.ts +1 -0
- package/src/handlers/sweep-host-cache.ts +26 -143
- package/src/handlers/upstream-issue-sync.d.ts +1 -0
- package/src/handlers/whatsapp-liveness-logic.d.ts +34 -0
- package/src/handlers/whatsapp-liveness.d.ts +4 -0
- package/src/hono-error-handler.d.ts +25 -0
- package/src/install-harness.d.ts +45 -0
- package/src/install-logging.d.ts +34 -0
- package/src/load-service-install-row.d.ts +24 -0
- package/src/load-service-prisma-client.d.ts +18 -0
- package/src/mount-orpc-router.d.ts +15 -0
- package/src/mount-orpc-router.ts +1 -1
- package/src/mount-server-packages.d.ts +29 -0
- package/src/mount.d.ts +9 -0
- package/src/mutable-app-ref.d.ts +44 -0
- package/src/page-route-shell-types.d.ts +12 -0
- package/src/page-server-orpc/audit-revert-router.d.ts +37 -0
- package/src/page-server-orpc/hub-mcp-client.d.ts +12 -0
- package/src/page-server-orpc/portal-diagnostics-router.d.ts +49 -0
- package/src/page-server-orpc/puck-pages-router.d.ts +58 -0
- package/src/page-server-orpc/server-client.d.ts +16 -0
- package/src/page-server-orpc/templates-router.d.ts +34 -0
- package/src/page-server-orpc/user-preferences-router.d.ts +79 -0
- package/src/pg-boss-handler-registry.d.ts +27 -0
- package/src/pg-boss-mount.d.ts +42 -0
- package/src/proposal-authorized-emails.d.ts +28 -0
- package/src/proposal-chat-inject.d.ts +17 -0
- package/src/proposal-gate.d.ts +18 -0
- package/src/read-html-template.d.ts +38 -0
- package/src/read-viewport-from-request.d.ts +18 -0
- package/src/readiness-gate.d.ts +16 -0
- package/src/register-api-routes.d.ts +18 -0
- package/src/register-crash-handlers.d.ts +15 -0
- package/src/register-page-routes.d.ts +27 -0
- package/src/register-page-routes.ts +3 -0
- package/src/register-page-server-client.d.ts +11 -0
- package/src/register-registry-page-route/ai-chat-prefetch.d.ts +13 -0
- package/src/register-registry-page-route/graph-cache.d.ts +30 -0
- package/src/register-registry-page-route/meta-title.d.ts +2 -0
- package/src/register-registry-page-route/proposal-approval-email-token.d.ts +14 -0
- package/src/register-registry-page-route/proposal-brand-stamping.d.ts +28 -0
- package/src/register-registry-page-route/resolve-page-data.d.ts +1 -0
- package/src/register-registry-page-route.d.ts +27 -0
- package/src/register-static-assets.d.ts +13 -0
- package/src/register-ws-routes.d.ts +28 -0
- package/src/resolve-app-base-url.d.ts +13 -0
- package/src/resolve-brand-favicon.d.ts +28 -0
- package/src/resolve-brand-favicon.ts +1 -1
- package/src/resolve-from-loader-config.d.ts +32 -0
- package/src/resolve-service-context.d.ts +17 -0
- package/src/resolve-service-port.d.ts +75 -0
- package/src/resolve-theme-mode.d.ts +42 -0
- package/src/resolvers/fetch-org-brand.d.ts +2 -0
- package/src/resolvers/portal-route-paths.d.ts +39 -0
- package/src/resolvers/website-by-host-map.d.ts +12 -0
- package/src/resource-chat-inject.d.ts +46 -0
- package/src/resource-not-found.d.ts +60 -0
- package/src/run-boot-hook-list.d.ts +19 -0
- package/src/run-boot-hooks.d.ts +31 -0
- package/src/scala-tools-currency-watchdog.d.ts +10 -0
- package/src/security-headers.d.ts +12 -0
- package/src/serve/serve.d.ts +46 -0
- package/src/serve/stamp-transport-identity.d.ts +32 -0
- package/src/serve/stamp-transport-identity.ts +2 -14
- package/src/server-error-page.d.ts +29 -0
- package/src/start-scheduled-jobs.d.ts +18 -0
- package/src/two-phase-boot.d.ts +136 -0
- package/src/two-phase-boot.ts +17 -85
- package/src/two-phase-health-gate.test.ts +2 -13
- package/src/types.d.ts +46 -0
- package/src/ui-service.d.ts +46 -0
- package/src/ws-boot-self-test.d.ts +19 -0
- package/src/auth-gate.test.ts +0 -70
- package/src/fn-routers/task-sync-lane.test.ts +0 -36
- package/src/handlers/sweep-host-cache.test.ts +0 -133
- package/src/serve/stamp-transport-identity.test.ts +0 -24
- package/src/two-phase-health-gate-client-only.test.ts +0 -77
package/src/types.d.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @system service-boot
|
|
3
|
+
* @status handwritten
|
|
4
|
+
* @edit edit directly
|
|
5
|
+
*
|
|
6
|
+
* Shared type definitions for service-boot boot helpers.
|
|
7
|
+
*/
|
|
8
|
+
type RendererComponent = (props: Record<string, unknown>) => unknown;
|
|
9
|
+
export interface RendererComponentEntry {
|
|
10
|
+
render: RendererComponent;
|
|
11
|
+
fields?: Record<string, {
|
|
12
|
+
type: string;
|
|
13
|
+
}>;
|
|
14
|
+
/** block_schema.defaults, carried onto the entry by discoverBlocks so
|
|
15
|
+
* buildPuckConfig merges them under authored props at SSR
|
|
16
|
+
* (blocks-fully-programmable). */
|
|
17
|
+
defaultProps?: Record<string, unknown>;
|
|
18
|
+
}
|
|
19
|
+
export interface PageHandlerExtras {
|
|
20
|
+
htmlTemplate: string;
|
|
21
|
+
userId?: string;
|
|
22
|
+
orgId?: string;
|
|
23
|
+
searchParams?: Record<string, string>;
|
|
24
|
+
nonce?: string;
|
|
25
|
+
}
|
|
26
|
+
export type CreatePageHandler = (config: PageHandlerConfig) => (request: Request, extras: PageHandlerExtras) => Promise<Response>;
|
|
27
|
+
export interface PageHandlerConfig {
|
|
28
|
+
components: Record<string, RendererComponentEntry>;
|
|
29
|
+
providers?: string[];
|
|
30
|
+
loadData: (request: Request) => Promise<unknown>;
|
|
31
|
+
renderMode?: "public" | "portals" | "utility";
|
|
32
|
+
/**
|
|
33
|
+
* Owning service slug — used by the page-server render catch to resolve
|
|
34
|
+
* the branded logo on the render-error page. Mirrors service-runtime's
|
|
35
|
+
* PageHandlerConfig.
|
|
36
|
+
*/
|
|
37
|
+
serviceSlug?: string;
|
|
38
|
+
/**
|
|
39
|
+
* Redirect a signed-in user's `/` to their org-scoped portal dashboard.
|
|
40
|
+
* Set only for services serving a `/*` catchall page_route (the portal);
|
|
41
|
+
* single-site portal-mode services serve their real page at `/`.
|
|
42
|
+
* Mirrors service-runtime's PageHandlerConfig (1.15.82).
|
|
43
|
+
*/
|
|
44
|
+
portalHomeRedirect?: boolean;
|
|
45
|
+
}
|
|
46
|
+
export type { ServiceInstallConfig } from "@teamscala/db/registry/loader/load-service-install-row";
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @system ui-service-boot
|
|
3
|
+
* @status handwritten
|
|
4
|
+
* @edit edit directly
|
|
5
|
+
*
|
|
6
|
+
* Boots a `kind: "ui"` service. Reads the service_install registry row and
|
|
7
|
+
* orchestrates a fixed pipeline of single-purpose stages onto one Hono app
|
|
8
|
+
* served by @teamscala/service-runtime/serve (one Bun.serve in dev and prod).
|
|
9
|
+
*/
|
|
10
|
+
import { Hono } from "hono";
|
|
11
|
+
import type { ServiceInstallConfig } from "./load-service-install-row.ts";
|
|
12
|
+
export interface ResolvedBlock {
|
|
13
|
+
slug: string;
|
|
14
|
+
packagePath: string;
|
|
15
|
+
}
|
|
16
|
+
export interface UiServiceBootResult {
|
|
17
|
+
kind: "ui";
|
|
18
|
+
blocks: ResolvedBlock[];
|
|
19
|
+
routes: string[];
|
|
20
|
+
port: number;
|
|
21
|
+
app: Hono;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Page-server symbols injected by the caller. service-boot is tier 4
|
|
25
|
+
* (server) and must not import from tier 2 (`@teamscala/service-runtime`)
|
|
26
|
+
* per `installer-and-runtime` Rule 1. The runtime's bin entrypoint
|
|
27
|
+
* boot pipeline imports these from `page-server/` (same package —
|
|
28
|
+
* legal) and passes them down here.
|
|
29
|
+
*/
|
|
30
|
+
export interface PageServerDeps {
|
|
31
|
+
createPageHandler: import("./types.ts").CreatePageHandler;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Boots a UI service using two-phase boot for fast availability.
|
|
35
|
+
*
|
|
36
|
+
* Phase 1 (fast): Build and serve minimal tool-only app (ORPC, data routes,
|
|
37
|
+
* auth gates). This becomes available within ~10s, enabling the MCP tool
|
|
38
|
+
* surface to accept requests immediately after restart.
|
|
39
|
+
*
|
|
40
|
+
* Phase 2 (background): Build full app (pages, hydration) and atomically swap
|
|
41
|
+
* it in. The readiness gate flips to ready after Phase 1, not Phase 2.
|
|
42
|
+
*
|
|
43
|
+
* The interim mitigation (503 + gateway retry + circuit breaker) remains in
|
|
44
|
+
* place and is NOT undone.
|
|
45
|
+
*/
|
|
46
|
+
export declare function bootUiService(config: ServiceInstallConfig, serviceSlug: string, deps: PageServerDeps, databaseUrl: string): Promise<UiServiceBootResult>;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @system server-boot
|
|
3
|
+
* @status handwritten
|
|
4
|
+
* @edit edit directly
|
|
5
|
+
*
|
|
6
|
+
* Post-listen WebSocket self-test. Runs after serveStart binds the port.
|
|
7
|
+
* For each declared websocket_routes path, opens a synthetic WebSocket
|
|
8
|
+
* connection and asserts 101 upgrade. Non-101 → BootFatalError.
|
|
9
|
+
*
|
|
10
|
+
* This catches the class of bugs where:
|
|
11
|
+
* - WS route registered after ORPC catch-all (order-dependent 404)
|
|
12
|
+
* - fetchWithPeerPort clones the Request on WS upgrade (breaks Bun.upgrade identity)
|
|
13
|
+
* - websocket_routes config row is stale / missing a handler
|
|
14
|
+
*
|
|
15
|
+
* Runs synchronously after serveStart so the boot aborts before the service
|
|
16
|
+
* reports healthy.
|
|
17
|
+
*/
|
|
18
|
+
import type { ServiceInstallConfig } from "@teamscala/db/registry/loader/load-service-install-row";
|
|
19
|
+
export declare function runWsBootSelfTest(config: ServiceInstallConfig, serviceSlug: string, port: number): Promise<void>;
|
package/src/auth-gate.test.ts
DELETED
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @system service-boot
|
|
3
|
-
* @status handwritten
|
|
4
|
-
* @edit edit directly
|
|
5
|
-
*
|
|
6
|
-
* The signed-out screen must be able to render its own branding.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { describe, expect, test } from "bun:test";
|
|
10
|
-
import { isStaticAssetPath } from "./auth-gate.ts";
|
|
11
|
-
|
|
12
|
-
describe("static assets are never behind the login gate", () => {
|
|
13
|
-
/**
|
|
14
|
-
* The regression this file exists for.
|
|
15
|
-
*
|
|
16
|
-
* Public paths were an ENUMERATED allowlist: `/assets/` was on it, `/logos/`
|
|
17
|
-
* never was. So `/logos/cropped.png` matched no prefix, fell through to the
|
|
18
|
-
* session gate, and answered 302 → /sign-in. A browser cannot act on a
|
|
19
|
-
* redirect it received for an <img>, so it painted nothing — a blank logo
|
|
20
|
-
* pulsing under a "starting up" splash, on the ONE screen guaranteed to need
|
|
21
|
-
* branding, for a user who by definition has no session.
|
|
22
|
-
*
|
|
23
|
-
* It kept coming back because nothing was broken by any change: the list
|
|
24
|
-
* simply never covered the path, and any build that moves images re-creates
|
|
25
|
-
* the gap. Enumerating what is public makes every new asset private by
|
|
26
|
-
* default; a predicate makes that unrepresentable.
|
|
27
|
-
*/
|
|
28
|
-
test("the logo that broke the signed-out screen is public", () => {
|
|
29
|
-
expect(isStaticAssetPath("/logos/cropped.png")).toBe(true);
|
|
30
|
-
expect(isStaticAssetPath("/logos/companyLogo.png")).toBe(true);
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
test("every asset kind a browser fetches without a session is public", () => {
|
|
34
|
-
for (const p of [
|
|
35
|
-
"/logos/dark.svg",
|
|
36
|
-
"/img/hero.webp",
|
|
37
|
-
"/brand/mark.avif",
|
|
38
|
-
"/fonts/inter.woff2",
|
|
39
|
-
"/styles/main.css",
|
|
40
|
-
"/assets/chunk-4efmn1eh.js",
|
|
41
|
-
"/favicon.ico",
|
|
42
|
-
]) {
|
|
43
|
-
expect(isStaticAssetPath(p)).toBe(true);
|
|
44
|
-
}
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
test("case does not decide whether an image is reachable", () => {
|
|
48
|
-
expect(isStaticAssetPath("/logos/Company.PNG")).toBe(true);
|
|
49
|
-
expect(isStaticAssetPath("/logos/Mark.SVG")).toBe(true);
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* The predicate widens what is PUBLIC, so its limit is the security-relevant
|
|
54
|
-
* half: pages and APIs must keep failing closed. A path is an asset because
|
|
55
|
-
* of its EXTENSION, never because it merely contains an asset-looking word.
|
|
56
|
-
*/
|
|
57
|
-
test("pages and APIs are still gated", () => {
|
|
58
|
-
for (const p of [
|
|
59
|
-
"/",
|
|
60
|
-
"/dashboard",
|
|
61
|
-
"/api/rpc/fn/workItems/list",
|
|
62
|
-
"/settings/logos",
|
|
63
|
-
"/logos",
|
|
64
|
-
"/png",
|
|
65
|
-
"/reports/quarterly.pdf",
|
|
66
|
-
]) {
|
|
67
|
-
expect(isStaticAssetPath(p)).toBe(false);
|
|
68
|
-
}
|
|
69
|
-
});
|
|
70
|
-
});
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @system service-boot
|
|
3
|
-
* @status handwritten
|
|
4
|
-
* @edit edit directly
|
|
5
|
-
*
|
|
6
|
-
* Behavioral tests for canonical CLI-session lane resolution.
|
|
7
|
-
*/
|
|
8
|
-
import { describe, expect, it } from "bun:test";
|
|
9
|
-
import { resolveLaneWorkName } from "./task-sync-lane.ts";
|
|
10
|
-
|
|
11
|
-
describe("resolveLaneWorkName", () => {
|
|
12
|
-
it("reads the visible label from the active cli_sessions route", async () => {
|
|
13
|
-
let query: unknown;
|
|
14
|
-
const name = await resolveLaneWorkName(
|
|
15
|
-
{
|
|
16
|
-
cli_sessions: {
|
|
17
|
-
findFirst: async (args: unknown) => {
|
|
18
|
-
query = args;
|
|
19
|
-
return { id: "session-1", label: "ZeroStack" };
|
|
20
|
-
},
|
|
21
|
-
},
|
|
22
|
-
} as never,
|
|
23
|
-
"tmux:%7",
|
|
24
|
-
);
|
|
25
|
-
expect(name).toBe("ZeroStack");
|
|
26
|
-
expect(query).toMatchObject({ where: { host_route: { path: ["pane_id"], equals: "tmux:%7" } } });
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
it("returns no unverified title when no active session owns the pane", async () => {
|
|
30
|
-
const name = await resolveLaneWorkName(
|
|
31
|
-
{ cli_sessions: { findFirst: async () => null } } as never,
|
|
32
|
-
"tmux:%8",
|
|
33
|
-
);
|
|
34
|
-
expect(name).toBeNull();
|
|
35
|
-
});
|
|
36
|
-
});
|
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @system server-boot
|
|
3
|
-
* @status handwritten
|
|
4
|
-
* @edit edit directly
|
|
5
|
-
*
|
|
6
|
-
* Unit tests for the `retention_dirs` helpers (`expandGlob`, `checkDirStale`) —
|
|
7
|
-
* the whole-directory prune that bounds Rust `target/` waste. The file-by-mtime
|
|
8
|
-
* `retention` path is unchanged; these cover ONLY the new build-cache model:
|
|
9
|
-
* (1) glob expansion reaches every repo's target dir; (2) an actively-built dir
|
|
10
|
-
* is kept (short-circuits on a fresh file); (3) an abandoned dir is reported
|
|
11
|
-
* stale with its full size. Lives next to the handler per co-located-test rule.
|
|
12
|
-
*/
|
|
13
|
-
import { describe, expect, test, beforeEach, afterEach } from "bun:test";
|
|
14
|
-
import { mkdtempSync, mkdirSync, writeFileSync, rmSync, utimesSync } from "node:fs";
|
|
15
|
-
import { join } from "node:path";
|
|
16
|
-
import { tmpdir } from "node:os";
|
|
17
|
-
import { expandGlob, checkDirStale, parseRootDiskPct } from "./sweep-host-cache.ts";
|
|
18
|
-
|
|
19
|
-
const DAY = 86_400_000;
|
|
20
|
-
const now = Date.now();
|
|
21
|
-
const daysAgo = (n: number) => new Date(now - n * DAY);
|
|
22
|
-
const touch = (p: string, t: Date) => utimesSync(p, t, t);
|
|
23
|
-
|
|
24
|
-
let root: string;
|
|
25
|
-
beforeEach(() => {
|
|
26
|
-
root = mkdtempSync(join(tmpdir(), "sweep-"));
|
|
27
|
-
});
|
|
28
|
-
afterEach(() => {
|
|
29
|
-
rmSync(root, { recursive: true, force: true });
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
describe("expandGlob", () => {
|
|
33
|
-
test("expands a single-segment wildcard to concrete dirs", () => {
|
|
34
|
-
mkdirSync(join(root, "scala-rust", "target"), { recursive: true });
|
|
35
|
-
mkdirSync(join(root, "scala-os", "target"), { recursive: true });
|
|
36
|
-
mkdirSync(join(root, "scala-os", "src"), { recursive: true });
|
|
37
|
-
const matches = expandGlob(`${root}/*/target`).sort();
|
|
38
|
-
expect(matches).toEqual(
|
|
39
|
-
[join(root, "scala-os", "target"), join(root, "scala-rust", "target")].sort(),
|
|
40
|
-
);
|
|
41
|
-
});
|
|
42
|
-
test("literal segment matches only itself", () => {
|
|
43
|
-
mkdirSync(join(root, "scala-rust", "target"), { recursive: true });
|
|
44
|
-
mkdirSync(join(root, "other", "target"), { recursive: true });
|
|
45
|
-
const matches = expandGlob(`${root}/scala-rust/target`);
|
|
46
|
-
expect(matches).toEqual([join(root, "scala-rust", "target")]);
|
|
47
|
-
});
|
|
48
|
-
test("no matches when the wildcard segment has no children", () => {
|
|
49
|
-
const matches = expandGlob(`${root}/does-not-exist/*/target`);
|
|
50
|
-
expect(matches).toEqual([]);
|
|
51
|
-
});
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
describe("checkDirStale", () => {
|
|
55
|
-
test("stale dir: all files older than cutoff → stale + full size", async () => {
|
|
56
|
-
const dir = join(root, "target");
|
|
57
|
-
mkdirSync(join(dir, "debug", "deps"), { recursive: true });
|
|
58
|
-
writeFileSync(join(dir, "debug", "deps", "a.o"), "x".repeat(40));
|
|
59
|
-
writeFileSync(join(dir, "debug", "deps", "b.o"), "y".repeat(60));
|
|
60
|
-
touch(join(dir, "debug", "deps", "a.o"), daysAgo(40));
|
|
61
|
-
touch(join(dir, "debug", "deps", "b.o"), daysAgo(35));
|
|
62
|
-
const res = await checkDirStale(dir, now - 30 * DAY);
|
|
63
|
-
expect(res.stale).toBe(true);
|
|
64
|
-
expect(res.fileCount).toBe(2);
|
|
65
|
-
expect(res.bytes).toBe(100);
|
|
66
|
-
});
|
|
67
|
-
test("active dir: a file newer than cutoff → not stale (short-circuit)", async () => {
|
|
68
|
-
const dir = join(root, "target");
|
|
69
|
-
mkdirSync(join(dir, "debug"), { recursive: true });
|
|
70
|
-
writeFileSync(join(dir, "debug", "old.o"), "x".repeat(40));
|
|
71
|
-
touch(join(dir, "debug", "old.o"), daysAgo(40));
|
|
72
|
-
writeFileSync(join(dir, "debug", "fresh.o"), "y".repeat(60));
|
|
73
|
-
touch(join(dir, "debug", "fresh.o"), daysAgo(1));
|
|
74
|
-
const res = await checkDirStale(dir, now - 30 * DAY);
|
|
75
|
-
expect(res.stale).toBe(false);
|
|
76
|
-
});
|
|
77
|
-
test("empty dir → stale with zero bytes", async () => {
|
|
78
|
-
const dir = join(root, "target");
|
|
79
|
-
mkdirSync(dir, { recursive: true });
|
|
80
|
-
const res = await checkDirStale(dir, now - 30 * DAY);
|
|
81
|
-
expect(res.stale).toBe(true);
|
|
82
|
-
expect(res.bytes).toBe(0);
|
|
83
|
-
expect(res.fileCount).toBe(0);
|
|
84
|
-
});
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
describe("parseRootDiskPct", () => {
|
|
88
|
-
test("extracts root-fs Use% from df --output=pcent,target output", () => {
|
|
89
|
-
const df =
|
|
90
|
-
"Use% Mounted on\n" +
|
|
91
|
-
" 97% /\n" +
|
|
92
|
-
" 45% /home\n";
|
|
93
|
-
expect(parseRootDiskPct(df)).toBe(97);
|
|
94
|
-
});
|
|
95
|
-
test("picks the root mount (/) over nested mounts", () => {
|
|
96
|
-
const df =
|
|
97
|
-
"Use% Mounted on\n" +
|
|
98
|
-
" 10% /proc/x\n" +
|
|
99
|
-
" 76% /\n";
|
|
100
|
-
expect(parseRootDiskPct(df)).toBe(76);
|
|
101
|
-
});
|
|
102
|
-
test("returns null when no root-mount line present", () => {
|
|
103
|
-
const df = "Use% Mounted on\n 40% /mnt/other\n";
|
|
104
|
-
expect(parseRootDiskPct(df)).toBeNull();
|
|
105
|
-
});
|
|
106
|
-
test("returns null on empty / malformed output", () => {
|
|
107
|
-
expect(parseRootDiskPct("")).toBeNull();
|
|
108
|
-
expect(parseRootDiskPct("Use% Mounted on")).toBeNull();
|
|
109
|
-
});
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
describe("disk-threshold early-warning regression lock", () => {
|
|
113
|
-
const { readFileSync } = require("node:fs");
|
|
114
|
-
// Source-shape lock (same pattern as singleton-tools-currency-watch.test.ts):
|
|
115
|
-
// the df-check that rides this periodic sweep was once removed in favour of an
|
|
116
|
-
// in-process interval watchdog — the wrong architecture (interval watchdogs
|
|
117
|
-
// are forbidden; observability is event-driven). This asserts the corrected
|
|
118
|
-
// shape survives: the sweep handler MUST carry the event-driven host.disk.high
|
|
119
|
-
// emission (logger.warn -> GlitchTip) + the pure parseRootDiskPct helper that
|
|
120
|
-
// backs it. Removing either fails this test. The asserted surface is the
|
|
121
|
-
// disk early-warning cell of host-resources-have-complete-monitoring.
|
|
122
|
-
const src = readFileSync(import.meta.dir + "/sweep-host-cache.ts", "utf-8");
|
|
123
|
-
test("handler emits the host.disk.high event (event-driven, not an interval)", () => {
|
|
124
|
-
expect(src).toContain('log.warn("host.disk.high"');
|
|
125
|
-
expect(src).toContain('kind: "host.disk.high"');
|
|
126
|
-
});
|
|
127
|
-
test("handler reads df via the sanctioned spawn (no in-process poller)", () => {
|
|
128
|
-
expect(src).toContain('command: ["df", "--output=pcent,target"]');
|
|
129
|
-
});
|
|
130
|
-
test("threshold parse is the exported pure helper", () => {
|
|
131
|
-
expect(src).toContain("export function parseRootDiskPct");
|
|
132
|
-
});
|
|
133
|
-
});
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import { summariseLocalCommand } from "./stamp-transport-identity.ts";
|
|
3
|
-
|
|
4
|
-
describe("local command attribution", () => {
|
|
5
|
-
test("never copies command arguments into telemetry", () => {
|
|
6
|
-
const token = "operator-token-must-not-appear";
|
|
7
|
-
const summary = summariseLocalCommand([
|
|
8
|
-
"curl",
|
|
9
|
-
"-H",
|
|
10
|
-
`x-operator-spawn-token: ${token}`,
|
|
11
|
-
"http://127.0.0.1:3021/rpc/spawn",
|
|
12
|
-
]);
|
|
13
|
-
expect(summary).toBe("curl [arguments redacted]");
|
|
14
|
-
expect(summary).not.toContain(token);
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
test("keeps a lone executable name", () => {
|
|
18
|
-
expect(summariseLocalCommand(["scala-picker"])).toBe("scala-picker");
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
test("omits an empty command line", () => {
|
|
22
|
-
expect(summariseLocalCommand([])).toBeNull();
|
|
23
|
-
});
|
|
24
|
-
});
|
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @system service-boot
|
|
3
|
-
* @status handwritten
|
|
4
|
-
* @edit edit directly
|
|
5
|
-
*
|
|
6
|
-
* A CLIENT-ONLY service (`render_mode=client`) must reach ready, even though it
|
|
7
|
-
* never registers an SSR page handler.
|
|
8
|
-
*
|
|
9
|
-
* Separate file from `two-phase-health-gate.test.ts` on purpose: `fullAppReady`
|
|
10
|
-
* is module state that only transitions false -> true and has no reset hook, so
|
|
11
|
-
* the two ready-paths cannot both be exercised in one module instance. A reset
|
|
12
|
-
* hook would exist only for tests.
|
|
13
|
-
*
|
|
14
|
-
* THE REGRESSION THIS LOCKS (measured 2026-08-07 on scala-agents-ui). Readiness
|
|
15
|
-
* used to be flipped INSIDE `setPageHandler`, which was honest while every
|
|
16
|
-
* service was SSR. `render_mode=client` made a service that completes phase 2
|
|
17
|
-
* and never calls it, so `/health` answered `{"status":"starting"}` FOREVER
|
|
18
|
-
* while the process was healthy and serving — boot completed in 5023ms, "Full
|
|
19
|
-
* app built: 50 block(s)" logged, `GET /` correctly 302'd to sign-in, and three
|
|
20
|
-
* untouched UI services reported ok beside it.
|
|
21
|
-
*
|
|
22
|
-
* That is precisely what `readiness-gated-boot` forbids ("binary: fully-ready or
|
|
23
|
-
* cleanly-unavailable"): a permanently-un-ready health signal makes the
|
|
24
|
-
* activator's wake gate and every health-gated router treat a working service as
|
|
25
|
-
* down. Exercised through a real Hono request against the REAL exported handler
|
|
26
|
-
* (`proxy-is-not-behaviour`) — a source grep would prove the call exists, never
|
|
27
|
-
* that the endpoint answers 200.
|
|
28
|
-
*/
|
|
29
|
-
|
|
30
|
-
import { beforeAll, describe, expect, test } from "bun:test";
|
|
31
|
-
import { Hono } from "hono";
|
|
32
|
-
import {
|
|
33
|
-
__resetPhase2StateForTest,
|
|
34
|
-
getPageHandlerState,
|
|
35
|
-
healthHandler,
|
|
36
|
-
markFullAppReady,
|
|
37
|
-
} from "./two-phase-boot.ts";
|
|
38
|
-
|
|
39
|
-
/** Mount the real handler exactly as buildMinimalApp does. */
|
|
40
|
-
const app = new Hono().get("/health", healthHandler);
|
|
41
|
-
|
|
42
|
-
describe("/health for a client-only service", () => {
|
|
43
|
-
// Bun shares one module instance across every test file in a run, so the SSR
|
|
44
|
-
// readiness test's setPageHandler leaks into this file when both run together.
|
|
45
|
-
// Without this reset the suite passes file-by-file and fails in CI.
|
|
46
|
-
beforeAll(() => {
|
|
47
|
-
__resetPhase2StateForTest();
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
test("before phase 2 completes it reports 503, not ready", async () => {
|
|
51
|
-
expect(getPageHandlerState().ready).toBe(false);
|
|
52
|
-
|
|
53
|
-
const res = await app.request("/health");
|
|
54
|
-
|
|
55
|
-
expect(res.status).toBe(503);
|
|
56
|
-
expect(await res.json()).toEqual({ status: "starting" });
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
test("reaches ready without an SSR page handler", async () => {
|
|
60
|
-
// The real client-only phase-2 path: buildFullApp finishes and marks the
|
|
61
|
-
// app ready WITHOUT setPageHandler, because there is no SSR handler.
|
|
62
|
-
markFullAppReady();
|
|
63
|
-
|
|
64
|
-
const state = getPageHandlerState();
|
|
65
|
-
expect(state.ready).toBe(true);
|
|
66
|
-
// The discriminator: ready is true while hasHandler stays false. Asserting
|
|
67
|
-
// both is what separates this from the SSR path — a fix that flipped ready
|
|
68
|
-
// by registering a dummy handler would pass `ready` and fail here.
|
|
69
|
-
expect(state.hasHandler).toBe(false);
|
|
70
|
-
|
|
71
|
-
const res = await app.request("/health");
|
|
72
|
-
|
|
73
|
-
expect(res.status).toBe(200);
|
|
74
|
-
expect(res.ok).toBe(true);
|
|
75
|
-
expect(await res.json()).toEqual({ status: "ok" });
|
|
76
|
-
});
|
|
77
|
-
});
|