@teamscala/service-boot 0.23.288 → 0.23.290
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
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
},
|
|
7
7
|
"name": "@teamscala/service-boot",
|
|
8
8
|
"type": "module",
|
|
9
|
-
"version": "0.23.
|
|
9
|
+
"version": "0.23.290",
|
|
10
10
|
"sideEffects": false,
|
|
11
11
|
"files": [
|
|
12
12
|
"src"
|
|
@@ -489,14 +489,6 @@
|
|
|
489
489
|
"types": "./src/handlers/scala-tools-currency-watch.d.ts",
|
|
490
490
|
"default": "./src/handlers/scala-tools-currency-watch.ts"
|
|
491
491
|
},
|
|
492
|
-
"./handlers/scheduled-browser-tests": {
|
|
493
|
-
"types": "./src/handlers/scheduled-browser-tests.d.ts",
|
|
494
|
-
"default": "./src/handlers/scheduled-browser-tests.ts"
|
|
495
|
-
},
|
|
496
|
-
"./handlers/scheduled-browser-tests.ts": {
|
|
497
|
-
"types": "./src/handlers/scheduled-browser-tests.d.ts",
|
|
498
|
-
"default": "./src/handlers/scheduled-browser-tests.ts"
|
|
499
|
-
},
|
|
500
492
|
"./handlers/sweep-event-log": {
|
|
501
493
|
"types": "./src/handlers/sweep-event-log.d.ts",
|
|
502
494
|
"default": "./src/handlers/sweep-event-log.ts"
|
package/src/pg-boss-mount.d.ts
CHANGED
|
@@ -17,7 +17,6 @@ import "./handlers/sweep-host-cache.ts";
|
|
|
17
17
|
import "./handlers/bun-stable-watch.ts";
|
|
18
18
|
import "./handlers/whatsapp-liveness.ts";
|
|
19
19
|
import "./handlers/upstream-issue-sync.ts";
|
|
20
|
-
import "./handlers/scheduled-browser-tests.ts";
|
|
21
20
|
import "./handlers/scala-tools-currency-watch.ts";
|
|
22
21
|
import "./handlers/registry-stale-inactive-sweep.ts";
|
|
23
22
|
import "./handlers/pane-outbox-drain.ts";
|
package/src/pg-boss-mount.ts
CHANGED
|
@@ -30,7 +30,6 @@ import "./handlers/sweep-host-cache.ts";
|
|
|
30
30
|
import "./handlers/bun-stable-watch.ts";
|
|
31
31
|
import "./handlers/whatsapp-liveness.ts";
|
|
32
32
|
import "./handlers/upstream-issue-sync.ts";
|
|
33
|
-
import "./handlers/scheduled-browser-tests.ts";
|
|
34
33
|
import "./handlers/scala-tools-currency-watch.ts";
|
|
35
34
|
import "./handlers/registry-stale-inactive-sweep.ts";
|
|
36
35
|
import "./handlers/pane-outbox-drain.ts";
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @system server-boot
|
|
3
|
+
* @status handwritten
|
|
4
|
+
* @edit edit directly
|
|
5
|
+
*
|
|
6
|
+
* Phase-2 step isolation: a client-asset build failure must not prevent
|
|
7
|
+
* scheduled jobs and pg-boss from mounting.
|
|
8
|
+
*
|
|
9
|
+
* Asserted against the SOURCE rather than by booting a service, because the
|
|
10
|
+
* behaviour is an ordering/error-handling property of buildFullApp and booting
|
|
11
|
+
* it here would need a database, a registry, a Vite build and a live pg-boss.
|
|
12
|
+
* The property is structural: buildClientAssets must be inside a try/catch that
|
|
13
|
+
* appears BEFORE the startScheduledJobs and mountPgBoss calls. That is exactly
|
|
14
|
+
* what regressed, and a source assertion fails the moment someone re-awaits it
|
|
15
|
+
* unguarded — which is the regression this locks.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { describe, expect, test } from "bun:test";
|
|
19
|
+
|
|
20
|
+
const src = await Bun.file(new URL("./two-phase-boot.ts", import.meta.url)).text();
|
|
21
|
+
|
|
22
|
+
/** The body of buildFullApp, where phase-2 ordering lives. */
|
|
23
|
+
function buildFullAppBody(): string {
|
|
24
|
+
const start = src.indexOf("export async function buildFullApp");
|
|
25
|
+
expect(start).toBeGreaterThan(-1);
|
|
26
|
+
return src.slice(start);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe("phase-2 step isolation", () => {
|
|
30
|
+
/*
|
|
31
|
+
* THE MEASURED REGRESSION. buildClientAssets was awaited unguarded, so a
|
|
32
|
+
* throw aborted buildFullApp before startScheduledJobs and mountPgBoss ran.
|
|
33
|
+
* The caller's catch logged "Failed to build full app, continuing with
|
|
34
|
+
* minimal app", which reads as UI-only degradation while every scheduled job
|
|
35
|
+
* and pg-boss handler on the service had silently died.
|
|
36
|
+
*
|
|
37
|
+
* 2026-07-30: a stale portal-ui artifact importing the retired @teamgbg scope
|
|
38
|
+
* threw ENOENT on a missing theme CSS file in scala-agents. No
|
|
39
|
+
* "scheduledJobs DONE" line appeared for ~20 minutes across repeated boots,
|
|
40
|
+
* the pane-outbox drain never mounted, and the operator's WhatsApp assistant
|
|
41
|
+
* went silent — because of a stylesheet. The service reported `online` and
|
|
42
|
+
* answered /health throughout.
|
|
43
|
+
*/
|
|
44
|
+
test("buildClientAssets is wrapped so its failure cannot abort phase-2", () => {
|
|
45
|
+
const body = buildFullAppBody();
|
|
46
|
+
const call = body.indexOf("buildClientAssets(serviceSlug)");
|
|
47
|
+
expect(call).toBeGreaterThan(-1);
|
|
48
|
+
|
|
49
|
+
// A try must open before the call and a catch must follow it, so a throw
|
|
50
|
+
// is contained rather than propagating out of buildFullApp.
|
|
51
|
+
const tryBefore = body.lastIndexOf("try {", call);
|
|
52
|
+
expect(tryBefore).toBeGreaterThan(-1);
|
|
53
|
+
expect(body.slice(call).indexOf("catch")).toBeGreaterThan(-1);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
/*
|
|
57
|
+
* THE LOAD-BEARING ASSERTION. A `catch` must appear BETWEEN the build call and
|
|
58
|
+
* the startScheduledJobs call. That is what makes this test fail if the guard
|
|
59
|
+
* is removed: unwrap the await and there is no catch in that span, so the
|
|
60
|
+
* expectation cannot be satisfied by some unrelated try/catch elsewhere in the
|
|
61
|
+
* function. Ordering alone would NOT catch the regression — the original buggy
|
|
62
|
+
* code also had jobs after the build; it just had no catch in between.
|
|
63
|
+
*/
|
|
64
|
+
test("a catch sits between the build and the jobs, so a throw cannot skip them", () => {
|
|
65
|
+
const body = buildFullAppBody();
|
|
66
|
+
const build = body.indexOf("buildClientAssets(serviceSlug)");
|
|
67
|
+
const jobs = body.indexOf("startScheduledJobs(");
|
|
68
|
+
const boss = body.indexOf("mountPgBoss(");
|
|
69
|
+
expect(build).toBeGreaterThan(-1);
|
|
70
|
+
expect(jobs).toBeGreaterThan(build);
|
|
71
|
+
expect(boss).toBeGreaterThan(build);
|
|
72
|
+
|
|
73
|
+
const between = body.slice(build, jobs);
|
|
74
|
+
expect(between).toContain("catch");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("the build failure is logged, so a degraded bundle is never silent", () => {
|
|
78
|
+
const body = buildFullAppBody();
|
|
79
|
+
const guardCatch = body.indexOf("catch", body.indexOf("buildClientAssets(serviceSlug)"));
|
|
80
|
+
// The catch must record something; swallowing here would reproduce the
|
|
81
|
+
// original invisibility from the other direction.
|
|
82
|
+
expect(body.slice(guardCatch, guardCatch + 400)).toContain("log.error");
|
|
83
|
+
});
|
|
84
|
+
});
|
package/src/two-phase-boot.ts
CHANGED
|
@@ -396,9 +396,37 @@ export async function buildFullApp(
|
|
|
396
396
|
const log = getAppLogger();
|
|
397
397
|
const { config, serviceSlug, mountRegistry, components, loaded, app: minimalApp } = minimalResult;
|
|
398
398
|
|
|
399
|
-
// Client asset building (expensive: Vite build)
|
|
400
|
-
|
|
401
|
-
|
|
399
|
+
// Client asset building (expensive: Vite build).
|
|
400
|
+
//
|
|
401
|
+
// ISOLATED, because a PRESENTATION failure must not take out BACKGROUND WORK.
|
|
402
|
+
// This was awaited unguarded, so a throw here aborted buildFullApp before
|
|
403
|
+
// startScheduledJobs and mountPgBoss ran, and the caller's catch logged
|
|
404
|
+
// "Failed to build full app, continuing with minimal app" — which reads as a
|
|
405
|
+
// UI-only degradation while having silently killed every scheduled job and
|
|
406
|
+
// every pg-boss handler on the service.
|
|
407
|
+
//
|
|
408
|
+
// Measured 2026-07-30: a stale @teamscala/portal-ui artifact still importing
|
|
409
|
+
// the retired @teamgbg scope threw ENOENT on a missing theme CSS file in
|
|
410
|
+
// scala-agents. Zero "scheduledJobs DONE" lines appeared for ~20 minutes
|
|
411
|
+
// across repeated boots, the pane-outbox drain never mounted, and the
|
|
412
|
+
// operator's WhatsApp assistant went silent — because of a stylesheet. The
|
|
413
|
+
// service stayed `online` and healthy throughout, so nothing surfaced it.
|
|
414
|
+
//
|
|
415
|
+
// The steps after this one do not depend on client assets: jobs and pg-boss
|
|
416
|
+
// need the registry and the DB, not a bundle. Ordering them behind a Vite
|
|
417
|
+
// build was incidental, so the coupling was never intended.
|
|
418
|
+
try {
|
|
419
|
+
await buildClientAssets(serviceSlug);
|
|
420
|
+
log.info(`[two-phase-boot:${serviceSlug}] phase-2 step: buildClientAssets DONE`);
|
|
421
|
+
} catch (error) {
|
|
422
|
+
// Loud but non-fatal: the tombstone manifest written by buildClientAssets
|
|
423
|
+
// already makes SSR serve the prior bundle, so degrading here costs the
|
|
424
|
+
// new bundle, never the background work.
|
|
425
|
+
log.error(
|
|
426
|
+
`[two-phase-boot:${serviceSlug}] phase-2 step: buildClientAssets FAILED — continuing to scheduled jobs and pg-boss (a client-asset failure must not stop background work)`,
|
|
427
|
+
{ error: error instanceof Error ? error.message : String(error) },
|
|
428
|
+
);
|
|
429
|
+
}
|
|
402
430
|
|
|
403
431
|
// Scheduled jobs
|
|
404
432
|
startScheduledJobs(config, mountRegistry, serviceSlug);
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @system server-boot
|
|
3
|
-
* @status handwritten
|
|
4
|
-
* @edit edit directly
|
|
5
|
-
*
|
|
6
|
-
* The `scheduled-browser-tests` pg-boss handler — the single scheduled-test
|
|
7
|
-
* home for the platform's regression suite (open migration
|
|
8
|
-
* `test-execution-consolidation`). Spawns `scala-tools test run --existing`
|
|
9
|
-
* on a cadence so every automated test execution flows through the one
|
|
10
|
-
* registry-driven harness, emitting the queryable `test.run.*` event pair
|
|
11
|
-
* (event-log-is-the-only-durable-event-log) that the deleted quartet of
|
|
12
|
-
* interval `guard_rule` rows never did (2026-06-29: 4 interval guards each
|
|
13
|
-
* autonomously drove headless Chrome against live UI services on 15/30-min
|
|
14
|
-
* timers with no central schedule/run-log).
|
|
15
|
-
*
|
|
16
|
-
* `--existing` runs every ACTIVE `test_vector` row across all services (the
|
|
17
|
-
* registry suite — both auto-generated and hand-authored vectors); `--no-boot`
|
|
18
|
-
* leaves service supervision to PM2 so a DOWN service fails its own health
|
|
19
|
-
* vector (the intended signal) instead of the scheduler booting a duplicate.
|
|
20
|
-
* `--source scheduled:scheduled-browser-tests` stamps the run's invoker block
|
|
21
|
-
* so a scheduled run is distinguishable from a manual agent run in the event
|
|
22
|
-
* log (`SELECT … FROM event_log WHERE kind LIKE 'test.run.%'`).
|
|
23
|
-
*
|
|
24
|
-
* The per-service outcomes land in the `test.run.result` events sharing the
|
|
25
|
-
* spawned run's `run_id`; this handler records only the schedule-level
|
|
26
|
-
* `scheduled.test.run` "did the run fire + finish" event. Spawns via
|
|
27
|
-
* `@teamscala/os` `spawn` (the single spawn surface); `scala-tools` resolves on
|
|
28
|
-
* PATH — on the dev host that is the source-mode link (always current).
|
|
29
|
-
*
|
|
30
|
-
* Registered under slug `@teamscala/service-boot:scheduled-browser-tests`.
|
|
31
|
-
* Mirrors `bun-stable-watch` / `upstream-issue-sync` (dev-host pg-boss
|
|
32
|
-
* handlers on scala-loga).
|
|
33
|
-
*/
|
|
34
|
-
import type { ScheduledJobConfig } from "@teamscala/db-validation/registry-schemas/scheduled-job";
|
|
35
|
-
import { recordEvent } from "@teamscala/event-log/record-event";
|
|
36
|
-
import { getAppLogger } from "@teamscala/logger/app-loggers";
|
|
37
|
-
import { spawn } from "@teamscala/os/spawn";
|
|
38
|
-
import { loadRegistryConfig } from "@teamscala/db/registry/load-config";
|
|
39
|
-
import * as v from "valibot";
|
|
40
|
-
import { pgBossHandlerRegistry } from "../pg-boss-handler-registry.ts";
|
|
41
|
-
|
|
42
|
-
const SOURCE = "scheduled:scheduled-browser-tests";
|
|
43
|
-
|
|
44
|
-
// A full --existing sweep across all UI services (Chrome-driven hydration +
|
|
45
|
-
// responsive + node-diff vectors, paced 5s apart between browser vectors) runs
|
|
46
|
-
// in the tens of minutes; bound the subprocess so a stuck vector's 90s wall can
|
|
47
|
-
// never hang the pg-boss worker indefinitely. exitCode null (SIGKILL on
|
|
48
|
-
// timeout) is treated as a failure + recorded.
|
|
49
|
-
const DEFAULT_RUN_TIMEOUT_MS = 30 * 60 * 1000;
|
|
50
|
-
let cachedRunTimeoutMs: number | undefined;
|
|
51
|
-
async function getRunTimeoutMs(): Promise<number> {
|
|
52
|
-
if (cachedRunTimeoutMs !== undefined) return cachedRunTimeoutMs;
|
|
53
|
-
const row = await loadRegistryConfig("config", "scheduled-browser-tests", v.looseObject({}));
|
|
54
|
-
cachedRunTimeoutMs = (row?.run_timeout_ms as number | undefined) ?? DEFAULT_RUN_TIMEOUT_MS;
|
|
55
|
-
return cachedRunTimeoutMs;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
async function scheduledBrowserTests(_ctx: { config: ScheduledJobConfig }): Promise<void> {
|
|
59
|
-
const log = getAppLogger();
|
|
60
|
-
|
|
61
|
-
const { stdout, stderr, exitCode } = await spawn({
|
|
62
|
-
name: "scheduled-browser-tests",
|
|
63
|
-
command: ["scala-tools", "test", "run", "--existing", "--no-boot", "--source", SOURCE],
|
|
64
|
-
timeoutMs: await getRunTimeoutMs(),
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
const code = exitCode ?? -1;
|
|
68
|
-
const ok = exitCode === 0;
|
|
69
|
-
// Surface the run's own summary line (the CLI prints a final
|
|
70
|
-
// "N/M vectors pass across K services" footer) so the schedule outcome is
|
|
71
|
-
// visible in the loga tail without re-querying the event log.
|
|
72
|
-
const tail = (stdout || stderr || "").trim().split("\n").slice(-2).join(" | ");
|
|
73
|
-
log.info(
|
|
74
|
-
`[scheduled-browser-tests] scala-tools test run --existing exited ${code} (${ok ? "pass" : "fail"}) — ${tail}`,
|
|
75
|
-
);
|
|
76
|
-
recordEvent({
|
|
77
|
-
kind: "scheduled.test.run",
|
|
78
|
-
payload: { source: SOURCE, exit_code: code, ok },
|
|
79
|
-
});
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
pgBossHandlerRegistry.register(
|
|
83
|
-
"@teamscala/service-boot:scheduled-browser-tests",
|
|
84
|
-
scheduledBrowserTests,
|
|
85
|
-
);
|