@teamscala/service-boot 0.23.319 → 0.23.321
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 +526 -526
- package/src/agreement-gate.d.ts +19 -0
- package/src/agreement-gate.ts +0 -12
- 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-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-router.d.ts +377 -0
- package/src/fn-routers/task-sync-router.test.ts +2 -14
- package/src/fn-routers/task-sync-router.ts +1075 -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/singleton-tools-currency-watch.ts +11 -114
- 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/install-logging.ts +0 -3
- 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/resource-not-found.ts +0 -10
- 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-close.test.ts +0 -44
- package/src/fn-routers/task-sync-close.ts +0 -82
- package/src/fn-routers/task-sync-hook-lifecycle.test.ts +0 -204
- package/src/fn-routers/task-sync-lane.test.ts +0 -36
- package/src/fn-routers/task-sync-rename.test.ts +0 -53
- package/src/fn-routers/task-sync-rename.ts +0 -111
- 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
|
@@ -1,204 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @system service-boot
|
|
3
|
-
* @status handwritten
|
|
4
|
-
* @edit edit directly
|
|
5
|
-
*
|
|
6
|
-
* Lifecycle round-trip for the Claude Code per-item task projection:
|
|
7
|
-
* `upsertFromTaskHookHandler` (the extracted body of the upsertFromTaskHook
|
|
8
|
-
* ORPC handler) must carry a task through created -> in_progress -> completed ->
|
|
9
|
-
* deleted against an in-memory mock prisma, so `work_items` sees every state a
|
|
10
|
-
* task reaches (`task-projection-converges`). A projection that cannot express
|
|
11
|
-
* completion — or deletion — is an append-only log, and the operator cannot see
|
|
12
|
-
* what a lane is doing. The matcher covering TaskCreate|TaskUpdate|TaskDelete is
|
|
13
|
-
* what routes all three tool events here; these tests assert the body applies
|
|
14
|
-
* each one. Same mock-prisma shape as claim-orchestrator-session.test.ts.
|
|
15
|
-
*/
|
|
16
|
-
import { afterEach, beforeEach, describe, expect, it, mock } from "bun:test";
|
|
17
|
-
|
|
18
|
-
const recordEventMock = mock((_event: unknown) => {});
|
|
19
|
-
mock.module("@teamscala/event-log/record-event", () => ({ recordEvent: recordEventMock }));
|
|
20
|
-
mock.module("@teamscala/logger/app-loggers", () => ({
|
|
21
|
-
getAppLogger: () => ({
|
|
22
|
-
info: () => {},
|
|
23
|
-
warn: () => {},
|
|
24
|
-
error: () => {},
|
|
25
|
-
}),
|
|
26
|
-
}));
|
|
27
|
-
|
|
28
|
-
const { upsertFromTaskHookHandler } = await import("./task-sync-router.ts");
|
|
29
|
-
|
|
30
|
-
/** A lane's parent task, returned by the mocked resolveLaneTask findFirst. */
|
|
31
|
-
const LANE_TASK = {
|
|
32
|
-
id: "task-1",
|
|
33
|
-
kind: "task",
|
|
34
|
-
parent_id: null,
|
|
35
|
-
organisation_id: "org-1",
|
|
36
|
-
title: "Test Lane",
|
|
37
|
-
status: "active",
|
|
38
|
-
completed: false,
|
|
39
|
-
position: null,
|
|
40
|
-
kind_data: { lane: { session: "ses-1", pane: "tmux:%1" } },
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
function makePrisma() {
|
|
44
|
-
// In-memory subtask store, so the four states act on ONE evolving row.
|
|
45
|
-
const subtasks: Array<Record<string, unknown>> = [];
|
|
46
|
-
const findFirst = mock(async (args: { where: Record<string, unknown> }) => {
|
|
47
|
-
// resolveLaneTask asks for kind='task' + status active/archived; everything
|
|
48
|
-
// else (subtask lookups) returns null.
|
|
49
|
-
if (args.where?.kind === "task") return { ...LANE_TASK };
|
|
50
|
-
return null;
|
|
51
|
-
});
|
|
52
|
-
const findMany = mock(async (args: { where: Record<string, unknown> }) => {
|
|
53
|
-
const pid = args.where?.parent_id;
|
|
54
|
-
return subtasks.filter((s) => s.parent_id === pid);
|
|
55
|
-
});
|
|
56
|
-
const create = mock(async (args: { data: Record<string, unknown> }) => {
|
|
57
|
-
const row = { id: `s-${subtasks.length + 1}`, ...args.data };
|
|
58
|
-
subtasks.push(row);
|
|
59
|
-
return row;
|
|
60
|
-
});
|
|
61
|
-
const update = mock(async (args: { where: { id: string }; data: Record<string, unknown> }) => {
|
|
62
|
-
const row = subtasks.find((s) => s.id === args.where.id);
|
|
63
|
-
if (row) Object.assign(row, args.data);
|
|
64
|
-
return row ?? null;
|
|
65
|
-
});
|
|
66
|
-
const deleteMany = mock(async (args: { where: { id: { in: string[] } } }) => {
|
|
67
|
-
// Real Prisma form: where.id.in (a bare array on the scalar id is a
|
|
68
|
-
// validation error). Mirrors the canonical deleteMany in reconcileTaskList.
|
|
69
|
-
const ids = args.where?.id?.in ?? [];
|
|
70
|
-
let count = 0;
|
|
71
|
-
for (let i = subtasks.length - 1; i >= 0; i--) {
|
|
72
|
-
if (ids.includes(String(subtasks[i]?.id))) {
|
|
73
|
-
subtasks.splice(i, 1);
|
|
74
|
-
count++;
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
return { count };
|
|
78
|
-
});
|
|
79
|
-
return {
|
|
80
|
-
store: subtasks,
|
|
81
|
-
prisma: { work_items: { findFirst, findMany, create, update, deleteMany } },
|
|
82
|
-
mocks: { findFirst, findMany, create, update, deleteMany },
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/** A lane context bound to a mock prisma, so each call site passes only the event. */
|
|
87
|
-
function bindCtx(prisma: unknown, session = "ses-1") {
|
|
88
|
-
const headers = new Headers();
|
|
89
|
-
headers.set("x-lane-session", session);
|
|
90
|
-
headers.set("x-lane-pane", "tmux:%1");
|
|
91
|
-
return { headers, prisma, user: undefined };
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/** makePrisma + a `hook(evt)` convenience bound to that prisma's session. */
|
|
95
|
-
function makeFixture() {
|
|
96
|
-
const fixture = makePrisma();
|
|
97
|
-
const hook = (evt: unknown) => upsertFromTaskHookHandler(evt, bindCtx(fixture.prisma));
|
|
98
|
-
return { ...fixture, hook };
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
describe("upsertFromTaskHookHandler — full lifecycle round-trip", () => {
|
|
102
|
-
beforeEach(() => recordEventMock.mockClear());
|
|
103
|
-
|
|
104
|
-
it("created -> in_progress -> completed -> deleted all write through to one subtask", async () => {
|
|
105
|
-
const { hook, store, mocks } = makeFixture();
|
|
106
|
-
|
|
107
|
-
// 1. CREATED — PostToolUse:TaskCreate. The harness assigns the task id in
|
|
108
|
-
// tool_response.task.id; the projection must stamp it as the mark so every
|
|
109
|
-
// later state is mark-addressable.
|
|
110
|
-
const created = await hook({
|
|
111
|
-
tool_name: "TaskCreate",
|
|
112
|
-
hook_event_name: "PostToolUse",
|
|
113
|
-
tool_input: { subject: "Ship the fix" },
|
|
114
|
-
tool_response: { task: { id: "42" } },
|
|
115
|
-
});
|
|
116
|
-
expect(created).toEqual({ applied: true, op: "create", taskId: "42" });
|
|
117
|
-
expect(mocks.create).toHaveBeenCalledTimes(1);
|
|
118
|
-
const row = store[0];
|
|
119
|
-
expect(row.title).toBe("Ship the fix");
|
|
120
|
-
expect(row.status).toBe("todo");
|
|
121
|
-
expect((row.kind_data as { task_sync: { mark: string } }).task_sync.mark).toBe("42");
|
|
122
|
-
|
|
123
|
-
// 2. IN_PROGRESS — PostToolUse:TaskUpdate with status in_progress. Matched
|
|
124
|
-
// by the mark stamped at creation; the projection expresses it, it does
|
|
125
|
-
// not append a new row.
|
|
126
|
-
mocks.create.mockClear();
|
|
127
|
-
const inProgress = await hook({
|
|
128
|
-
tool_name: "TaskUpdate",
|
|
129
|
-
hook_event_name: "PostToolUse",
|
|
130
|
-
tool_input: { taskId: "42", status: "in_progress" },
|
|
131
|
-
});
|
|
132
|
-
expect(inProgress).toEqual({ applied: true, op: "update", taskId: "42" });
|
|
133
|
-
expect(mocks.create).not.toHaveBeenCalled();
|
|
134
|
-
expect(mocks.update).toHaveBeenCalledTimes(1);
|
|
135
|
-
expect(row.status).toBe("in-progress");
|
|
136
|
-
expect(row.completed).toBe(false);
|
|
137
|
-
expect(store).toHaveLength(1);
|
|
138
|
-
|
|
139
|
-
// 3. COMPLETED — PostToolUse:TaskUpdate with status completed. A projection
|
|
140
|
-
// that cannot express completion is an append-only log.
|
|
141
|
-
mocks.update.mockClear();
|
|
142
|
-
const done = await hook({
|
|
143
|
-
tool_name: "TaskUpdate",
|
|
144
|
-
hook_event_name: "PostToolUse",
|
|
145
|
-
tool_input: { taskId: "42", status: "completed" },
|
|
146
|
-
});
|
|
147
|
-
expect(done).toEqual({ applied: true, op: "update", taskId: "42" });
|
|
148
|
-
expect(mocks.update).toHaveBeenCalledTimes(1);
|
|
149
|
-
expect(row.status).toBe("done");
|
|
150
|
-
expect(row.completed).toBe(true);
|
|
151
|
-
expect(store).toHaveLength(1);
|
|
152
|
-
|
|
153
|
-
// 4. DELETED — PostToolUse:TaskDelete. The row leaves the projection; the
|
|
154
|
-
// list empties, which is the half a create-only projection can never do.
|
|
155
|
-
const deleted = await hook({
|
|
156
|
-
tool_name: "TaskDelete",
|
|
157
|
-
hook_event_name: "PostToolUse",
|
|
158
|
-
tool_input: { taskId: "42" },
|
|
159
|
-
});
|
|
160
|
-
expect(deleted).toEqual({ applied: true, op: "delete", taskId: "42" });
|
|
161
|
-
expect(mocks.deleteMany).toHaveBeenCalledTimes(1);
|
|
162
|
-
expect(store).toHaveLength(0);
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
it("stamps the mark on a title-matched update so a later rename is mark-addressable", async () => {
|
|
166
|
-
// A row written with NO mark (pre-mark history, or a healed row) is only
|
|
167
|
-
// reachable by title. The first title-match must stamp the mark so the row
|
|
168
|
-
// never orphans on a later rename (the 2026-07-28 stranded-subtask class).
|
|
169
|
-
const { hook, store } = makeFixture();
|
|
170
|
-
store.push({
|
|
171
|
-
id: "legacy-1",
|
|
172
|
-
parent_id: "task-1",
|
|
173
|
-
title: "Old title",
|
|
174
|
-
status: "todo",
|
|
175
|
-
completed: false,
|
|
176
|
-
kind_data: {},
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
await hook({
|
|
180
|
-
tool_name: "TaskUpdate",
|
|
181
|
-
hook_event_name: "PostToolUse",
|
|
182
|
-
tool_input: { taskId: "99", subject: "Old title", status: "in_progress" },
|
|
183
|
-
});
|
|
184
|
-
expect((store[0].kind_data as { task_sync: { mark: string } }).task_sync.mark).toBe("99");
|
|
185
|
-
expect(store[0].status).toBe("in-progress");
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
it("is idempotent on a retried TaskCreate (no duplicate subtask)", async () => {
|
|
189
|
-
// The hook curls with --retry, so one TaskCreate can arrive twice. A
|
|
190
|
-
// projection that duplicates on retry is not a projection.
|
|
191
|
-
const { hook, store } = makeFixture();
|
|
192
|
-
const evt = {
|
|
193
|
-
tool_name: "TaskCreate",
|
|
194
|
-
hook_event_name: "PostToolUse",
|
|
195
|
-
tool_input: { subject: "Once only" },
|
|
196
|
-
tool_response: { task: { id: "7" } },
|
|
197
|
-
};
|
|
198
|
-
await hook(evt);
|
|
199
|
-
const second = await hook(evt);
|
|
200
|
-
// The retried create matches the stored mark and is discarded, not doubled.
|
|
201
|
-
expect(second.applied).toBe(false);
|
|
202
|
-
expect(store.filter((s) => s.title === "Once only")).toHaveLength(1);
|
|
203
|
-
});
|
|
204
|
-
});
|
|
@@ -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,53 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @system service-boot
|
|
3
|
-
* @status handwritten
|
|
4
|
-
* @edit directly
|
|
5
|
-
*
|
|
6
|
-
* Unit tests for the rename back-write leg: renameOutcome (the pure
|
|
7
|
-
* no-task ⇒ not-updated decision) + source-contract assertions that the handler
|
|
8
|
-
* resolves the lane task BY SESSION (never pane) and updates its title. Covers
|
|
9
|
-
* `what-must-have-a-test` for the new procedure + the `lane-identity-is-cli-
|
|
10
|
-
* session-id` invariant it rides on.
|
|
11
|
-
*/
|
|
12
|
-
import { describe, it, expect } from "bun:test";
|
|
13
|
-
import { renameOutcome } from "./task-sync-rename.ts";
|
|
14
|
-
|
|
15
|
-
describe("renameOutcome (no verdict from no data)", () => {
|
|
16
|
-
it("returns not-updated with a null task_id when no task is bound to the session", () => {
|
|
17
|
-
// A rename of a tab whose lane has no resolvable task updates nothing —
|
|
18
|
-
// never an error, never a fake id. updated:false is the signal the caller
|
|
19
|
-
// treats as best-effort (the window was already renamed; this only missed
|
|
20
|
-
// the DB echo, which is the rare archived-and-pruned case).
|
|
21
|
-
expect(renameOutcome(null)).toEqual({ task_id: null, updated: false });
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
it("returns the bound task id + updated:true when a task resolves", () => {
|
|
25
|
-
expect(renameOutcome({ id: "task-9" })).toEqual({ task_id: "task-9", updated: true });
|
|
26
|
-
});
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
describe("renameLaneTask resolves BY SESSION and writes the title (source contract)", () => {
|
|
30
|
-
const renameSrc = Bun.file(new URL("./task-sync-rename.ts", import.meta.url).pathname);
|
|
31
|
-
|
|
32
|
-
it("resolves the lane task by lane_session (the CLI session id), never by pane", async () => {
|
|
33
|
-
// `lane-identity-is-cli-session-id`: binding is by session because a pane
|
|
34
|
-
// outlives the session inside it. A pane-keyed rename would let a dead
|
|
35
|
-
// session's task answer for a live lane.
|
|
36
|
-
const text = await renameSrc.text();
|
|
37
|
-
expect(text).toContain("resolveLaneTask(prisma, lane_session)");
|
|
38
|
-
// No pane matching anywhere in the resolution path.
|
|
39
|
-
expect(text).not.toContain("lane.pane");
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it("updates the resolved task's title by id", async () => {
|
|
43
|
-
const text = await renameSrc.text();
|
|
44
|
-
expect(text).toContain('where: { id: task.id }');
|
|
45
|
-
expect(text).toContain("data: { title }");
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
it("requires lane_session + title at the signature (incomplete rename unconstructible)", async () => {
|
|
49
|
-
const text = await renameSrc.text();
|
|
50
|
-
expect(text).toContain('lane_session: v.pipe(v.string()');
|
|
51
|
-
expect(text).toContain('title: v.pipe(v.string()');
|
|
52
|
-
});
|
|
53
|
-
});
|
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @system service-boot
|
|
3
|
-
* @status handwritten
|
|
4
|
-
* @edit edit directly
|
|
5
|
-
*
|
|
6
|
-
* Lane RENAME back-write for task-sync: a tab rename EMITS to update the bound
|
|
7
|
-
* work_items task's title in the DB, by CLI session id. This is the DB half of
|
|
8
|
-
* `tab-rename-emits-back-to-task-title`: the operator's tab bar is how they see
|
|
9
|
-
* what is being worked on (`lane-labels-and-task-lists`), so the tmux window
|
|
10
|
-
* name and the task title must never diverge silently. Renaming a tab from
|
|
11
|
-
* `scala-tools session rename` (or the lane renaming itself through the same
|
|
12
|
-
* surface) updates the task title so the project board agrees with the tab bar.
|
|
13
|
-
*
|
|
14
|
-
* THE EMISSION, not a poll (`actions-are-rows-then-emissions`): a caller renames
|
|
15
|
-
* the window (a row/state change) and that change emits the title update — the
|
|
16
|
-
* task-sync owner reacts by updating the row. State lives in the row, never in a
|
|
17
|
-
* return value a caller may never receive.
|
|
18
|
-
*
|
|
19
|
-
* Bound by CLI SESSION ID, never pane id (`lane-identity-is-cli-session-id`): a
|
|
20
|
-
* pane OUTLIVES the session inside it, so matching on a pane would let a dead
|
|
21
|
-
* session's task answer for a live lane. The session is the stable identity a
|
|
22
|
-
* rename resolves to (the daemon resolves the pane's tab → its stored session id
|
|
23
|
-
* and passes that here), so the rename lands on the lane that owns the tab.
|
|
24
|
-
*
|
|
25
|
-
* Split out of task-sync-router.ts for the same reason task-sync-brief/lane/
|
|
26
|
-
* assign exist: the router sits at its 1000-line commit gate, so every cohesive
|
|
27
|
-
* task-sync unit that can live in a sibling does. This owns the rename handler +
|
|
28
|
-
* its pure outcome decision.
|
|
29
|
-
*
|
|
30
|
-
* CAPABILITY HARVEST (capability-harvest-on-split): APP-LOCAL, not a capability
|
|
31
|
-
* candidate. Bound to the work_items schema and to task-sync's own
|
|
32
|
-
* kind_data.lane.session convention; no unrelated app could install this
|
|
33
|
-
* unmodified.
|
|
34
|
-
*/
|
|
35
|
-
|
|
36
|
-
import * as v from "valibot";
|
|
37
|
-
import type { Prisma } from "./task-sync-router.ts";
|
|
38
|
-
import { authed, type Context } from "./auth-router.ts";
|
|
39
|
-
import { resolveLaneTask } from "./task-sync-lane.ts";
|
|
40
|
-
import { getAppLogger } from "@teamscala/logger/app-loggers";
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* renameLaneTask ORPC input. `lane_session` is the lane's CLI-native session id
|
|
44
|
-
* (kind_data.lane.session) — the stable identity a pane rename resolves to.
|
|
45
|
-
* `title` is the new task title (the renamed tab's label). Both required:
|
|
46
|
-
* oRPC rejects a call missing either at the signature
|
|
47
|
-
* (`prevention-over-detection`), so a malformed rename is a 400, never a silent
|
|
48
|
-
* no-op that leaves the title stale.
|
|
49
|
-
*/
|
|
50
|
-
export const renameLaneTaskInput = v.object({
|
|
51
|
-
lane_session: v.pipe(v.string(), v.minLength(1, "lane_session is required")),
|
|
52
|
-
title: v.pipe(v.string(), v.minLength(1, "title is required")),
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* The rename outcome from a resolved lane task — pure so the "no task bound ⇒
|
|
57
|
-
* not updated, never an error" decision is unit-testable with no DB. A rename
|
|
58
|
-
* whose lane has no task is best-effort (returns updated:false): every spawn
|
|
59
|
-
* now mints a task, so this is the rare case (a tab whose task was archived and
|
|
60
|
-
* pruned), not a path the operator hits. Failing loudly here would refuse a
|
|
61
|
-
* rename of the WINDOW for a DB miss, which couples two surfaces that must not
|
|
62
|
-
* be — the tmux rename already succeeded before this is called.
|
|
63
|
-
*/
|
|
64
|
-
export function renameOutcome(task: { id: string } | null): {
|
|
65
|
-
task_id: string | null;
|
|
66
|
-
updated: boolean;
|
|
67
|
-
} {
|
|
68
|
-
if (!task) return { task_id: null, updated: false };
|
|
69
|
-
return { task_id: task.id, updated: true };
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* renameLaneTask ORPC handler. The session-picker daemon calls this from
|
|
74
|
-
* `/rpc/rename` after it renames the tmux window: it resolves the pane's tab to
|
|
75
|
-
* its session id and posts it here. Same transport as bindLane/readTaskList:
|
|
76
|
-
* page-gate-exempt (fn.taskSync), localhost super-admin. Resolves the lane's
|
|
77
|
-
* task BY SESSION (never pane) and updates its title. Idempotent: renaming to
|
|
78
|
-
* the same title writes the same row.
|
|
79
|
-
*
|
|
80
|
-
* An archived parent is matched too (resolveLaneTask falls back to the archived
|
|
81
|
-
* lane task for the session), so a rename of a tab whose run closed still lands
|
|
82
|
-
* on its task — the title is the title regardless of run state.
|
|
83
|
-
*/
|
|
84
|
-
export const renameLaneTaskHandler = authed
|
|
85
|
-
.input(renameLaneTaskInput)
|
|
86
|
-
.handler(async ({ input, context }) => {
|
|
87
|
-
const ctx = context as Context;
|
|
88
|
-
const prisma = ctx.prisma as Prisma;
|
|
89
|
-
const { lane_session, title } = input;
|
|
90
|
-
const task = await resolveLaneTask(prisma, lane_session);
|
|
91
|
-
const outcome = renameOutcome(task);
|
|
92
|
-
if (!task) return outcome;
|
|
93
|
-
try {
|
|
94
|
-
await prisma.work_items.update({
|
|
95
|
-
where: { id: task.id },
|
|
96
|
-
data: { title },
|
|
97
|
-
select: { id: true },
|
|
98
|
-
});
|
|
99
|
-
} catch (err) {
|
|
100
|
-
// The window was already renamed by the caller; a DB update failure must
|
|
101
|
-
// not surface as an error that implies the rename failed. Report
|
|
102
|
-
// not-updated + log, so the two surfaces stay decoupled
|
|
103
|
-
// (`a-component-may-not-report-a-state-it-has-not-verified`).
|
|
104
|
-
getAppLogger().warn("[task-sync] renameLaneTask: title update failed", {
|
|
105
|
-
taskId: task.id,
|
|
106
|
-
error: err instanceof Error ? err.message : String(err),
|
|
107
|
-
});
|
|
108
|
-
return { task_id: task.id, updated: false };
|
|
109
|
-
}
|
|
110
|
-
return outcome;
|
|
111
|
-
});
|
|
@@ -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
|
-
});
|