@ultimat3/cli 23.0.0 → 24.0.0
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/CLAUDE.md +15 -7
- package/README.md +3 -3
- package/package.json +28 -28
- package/src/affected.ts +27 -35
- package/src/api-registration.ts +89 -0
- package/src/app-artifacts.ts +1 -1
- package/src/app-auth.ts +4 -27
- package/src/app-config-load.ts +51 -0
- package/src/app-env.ts +5 -11
- package/src/app-manifest.ts +2 -8
- package/src/app-mcp.ts +10 -23
- package/src/app-permissions-borrowed.ts +129 -0
- package/src/app-permissions-site.ts +24 -0
- package/src/app-permissions.ts +7 -17
- package/src/budgets.ts +4 -35
- package/src/cdp-shot-driver.ts +3 -1
- package/src/cmd-auth-spec.ts +16 -0
- package/src/cmd-auth.ts +65 -0
- package/src/cmd-build.ts +64 -15
- package/src/cmd-db-backfill.ts +25 -12
- package/src/cmd-db.ts +6 -5
- package/src/cmd-deploy-helm.ts +7 -6
- package/src/cmd-deploy.ts +4 -4
- package/src/cmd-dev.ts +9 -8
- package/src/cmd-doctor-spec.ts +2 -9
- package/src/cmd-doctor.ts +41 -14
- package/src/cmd-env.ts +26 -2
- package/src/cmd-generate.ts +79 -32
- package/src/cmd-help.ts +19 -6
- package/src/cmd-i18n.ts +10 -9
- package/src/cmd-jobs.ts +18 -6
- package/src/cmd-manifest-spec.ts +1 -1
- package/src/cmd-manifest.ts +6 -25
- package/src/cmd-mcp.ts +46 -15
- package/src/cmd-planned.ts +15 -8
- package/src/cmd-routes.ts +12 -2
- package/src/cmd-secrets.ts +47 -6
- package/src/cmd-shot-matrix.ts +1 -3
- package/src/cmd-shot.ts +12 -3
- package/src/cmd-tasks.ts +2 -2
- package/src/cmd-test-spec.ts +1 -7
- package/src/cmd-test.ts +13 -29
- package/src/cmd-verify.ts +4 -4
- package/src/db-subscribes.ts +12 -4
- package/src/dev-dashboard-guard.ts +76 -0
- package/src/dev-dashboard.ts +51 -29
- package/src/dev-lock.ts +4 -7
- package/src/dispatch.ts +3 -6
- package/src/doctor-app-url.ts +39 -0
- package/src/doctor-auth.ts +95 -0
- package/src/drift-replica-identity.ts +42 -0
- package/src/error-catalog.ts +6 -9
- package/src/error-codes.ts +23 -3
- package/src/error-unthrown.ts +29 -11
- package/src/errors.ts +54 -83
- package/src/framework-schema-apply.ts +78 -0
- package/src/framework-schema-stamp.ts +96 -0
- package/src/framework-schema.ts +13 -2
- package/src/generate-files.ts +4 -3
- package/src/generate-kinds.ts +33 -1
- package/src/generate-shadow.ts +46 -0
- package/src/generate-write.ts +47 -13
- package/src/gh-target.ts +12 -7
- package/src/i18n-audit.ts +4 -1
- package/src/index.ts +1 -3
- package/src/island-bundle.ts +3 -15
- package/src/jobs-drain.ts +82 -15
- package/src/jobs-report.ts +4 -4
- package/src/loopback-host.ts +13 -0
- package/src/manifest-staleness.ts +62 -0
- package/src/mcp-errors.ts +12 -3
- package/src/mcp-host.ts +16 -9
- package/src/mcp-test-run.ts +38 -0
- package/src/messages.ts +2 -0
- package/src/metrics-endpoint.ts +25 -1
- package/src/output.ts +6 -0
- package/src/page-navigation.ts +7 -23
- package/src/page-speculation.ts +5 -20
- package/src/prerender-out.ts +34 -7
- package/src/prerender.ts +8 -5
- package/src/pwa-artifacts.ts +26 -94
- package/src/registry.ts +2 -0
- package/src/revalidated-response.ts +4 -3
- package/src/role-replicator.ts +16 -0
- package/src/role-start.ts +9 -4
- package/src/role-sync.ts +55 -20
- package/src/runtime-assets.ts +11 -7
- package/src/runtime-bindings.ts +4 -4
- package/src/runtime-cache.ts +38 -47
- package/src/runtime-idempotency-scope.ts +23 -0
- package/src/runtime-isr.ts +36 -0
- package/src/runtime-jobs.ts +8 -33
- package/src/runtime-mfa-warning.ts +29 -0
- package/src/runtime-notify-retention.ts +10 -46
- package/src/runtime-overrides.ts +1 -1
- package/src/runtime-purge.ts +38 -16
- package/src/runtime-queue.ts +31 -10
- package/src/runtime-realtime.ts +9 -45
- package/src/runtime-render.ts +62 -35
- package/src/runtime-services.ts +20 -17
- package/src/runtime-storage.ts +10 -5
- package/src/schema-diff.ts +16 -0
- package/src/schema-drift.ts +45 -15
- package/src/seo-routes.ts +80 -9
- package/src/serve-boot.ts +34 -6
- package/src/serve-drain.ts +9 -33
- package/src/serve-env.ts +9 -6
- package/src/serve-web.ts +12 -7
- package/src/shot-locale.ts +5 -23
- package/src/site-assets.ts +3 -5
- package/src/site-config.ts +9 -48
- package/src/site-seo.ts +1 -1
- package/src/sitemap-lastmod.ts +5 -0
- package/src/solid-loader.ts +12 -3
- package/src/source-files.ts +6 -0
- package/src/stored-object-headers.ts +43 -0
- package/src/sync-url.ts +2 -2
- package/src/tasks-facts.ts +3 -29
- package/src/templates/action.ts +3 -0
- package/src/templates/naming.ts +28 -0
- package/src/templates/route.ts +3 -6
- package/src/templates/scaffold-app.ts +6 -2
- package/src/templates/scaffold-container-compose.ts +10 -1
- package/src/templates/scaffold-container.ts +4 -2
- package/src/templates/scaffold-db-client.ts +4 -2
- package/src/templates/scaffold-helm-fences.ts +76 -0
- package/src/templates/scaffold-helm-templates.ts +32 -7
- package/src/templates/scaffold-helm.ts +31 -1
- package/src/templates/scaffold-repo.ts +9 -1
- package/src/templates/scaffold-shell.ts +39 -36
- package/src/test-counts.ts +2 -6
- package/src/test-ignores.ts +25 -0
- package/src/test-passes.ts +0 -15
- package/src/test-select.ts +5 -14
- package/src/test-shards.ts +41 -93
- package/src/test-workers.ts +26 -4
- package/src/theme-boot.ts +6 -31
- package/src/trusted-hops-error.ts +17 -0
- package/src/unthrown-codes.ts +26 -0
- package/src/verify-checks.ts +17 -46
- package/src/verify-deadline.ts +130 -33
- package/src/verify-merge.ts +60 -27
- package/src/verify-part-step.ts +90 -0
- package/src/verify-run.ts +29 -8
- package/src/verify-shard.ts +13 -2
- package/src/verify-stalled.ts +152 -0
- package/src/verify-tests.ts +5 -2
- package/src/build-errors.ts +0 -54
- package/src/verify-errors.ts +0 -58
package/src/templates/route.ts
CHANGED
|
@@ -33,12 +33,9 @@ const OFFLINE: Record<Surface, string> = { site: 'precache', app: 'runtime' };
|
|
|
33
33
|
/**
|
|
34
34
|
* Structured, not a literal string: the route and the test that pins it read the same fact.
|
|
35
35
|
*
|
|
36
|
-
* `
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
* was therefore a budget that reads as declared, is never weighed, and passes silently the moment
|
|
40
|
-
* a `build-stats.json` row exists: the false green `budgets.ts`'s own header forbids. Scaffolding
|
|
41
|
-
* only what the build measures is the half of "delete it or thread it" that `x new` can act on.
|
|
36
|
+
* `js` is the one budget a route can declare: it is the one the build weighs. `lcp` was scaffolded
|
|
37
|
+
* here once and then deleted from `RouteBudget` itself — nothing in the framework observes a
|
|
38
|
+
* paint, so it read as declared and was never weighed.
|
|
42
39
|
*/
|
|
43
40
|
const BUDGET: Record<Surface, { readonly js: string }> = {
|
|
44
41
|
site: { js: '0kb' },
|
|
@@ -16,6 +16,10 @@ import { rolesFiles } from './scaffold-roles';
|
|
|
16
16
|
import { shellFiles } from './scaffold-shell';
|
|
17
17
|
import { siteFiles } from './scaffold-site';
|
|
18
18
|
|
|
19
|
+
// `typecheck` is the ROOT program, as in every workspace `x new` writes: a type extension declared in
|
|
20
|
+
// one workspace (`PermissionRegistry` in `app/*/policy.ts`) is in force only where its file is in
|
|
21
|
+
// the program, and the app's own `tsconfig.json` passed in `apps/admin` what the gate refuses.
|
|
22
|
+
//
|
|
19
23
|
// The one dependency this manifest names, and it is not decoration: every page below reads its
|
|
20
24
|
// strings through `@<app>/i18n`'s `useT()`, so the surface that renders a string DEPENDS on the
|
|
21
25
|
// module that registers the catalogs. An undeclared workspace dependency resolves through the root
|
|
@@ -36,7 +40,7 @@ const webPackage = (app: NameSet, example: boolean): string => {
|
|
|
36
40
|
"./*.tsx": "./*.tsx"
|
|
37
41
|
},
|
|
38
42
|
"scripts": {
|
|
39
|
-
"typecheck": "tsc --noEmit -p tsconfig.json"
|
|
43
|
+
"typecheck": "tsc --noEmit -p ../../tsconfig.json"
|
|
40
44
|
},
|
|
41
45
|
"dependencies": {${db}
|
|
42
46
|
"@${app.kebab}/i18n": "0.0.0"
|
|
@@ -260,7 +264,7 @@ const adminPackage = (app: NameSet): string => `{
|
|
|
260
264
|
"./*.tsx": "./*.tsx"
|
|
261
265
|
},
|
|
262
266
|
"scripts": {
|
|
263
|
-
"typecheck": "tsc --noEmit -p tsconfig.json"
|
|
267
|
+
"typecheck": "tsc --noEmit -p ../../tsconfig.json"
|
|
264
268
|
},
|
|
265
269
|
"dependencies": {
|
|
266
270
|
"@${app.kebab}/db": "0.0.0",
|
|
@@ -25,6 +25,11 @@ export const composeProdFile = (
|
|
|
25
25
|
#
|
|
26
26
|
# docker compose --env-file ${PROD_ENV_FILE} -f docker/docker-compose.prod.yml up -d
|
|
27
27
|
#
|
|
28
|
+
# Two origins the file cannot know, so ${PROD_ENV_FILE} states them: SYNC_URL, where a page dials
|
|
29
|
+
# the socket (ws://<host>:3001/_x/sync), and APP_URL, where the pages are served
|
|
30
|
+
# (http://<host>:3000) — the sync node admits a socket from APP_URL's origin and no other once
|
|
31
|
+
# declared; undeclared, from the origin it was reached on.
|
|
32
|
+
#
|
|
28
33
|
# A published host port has exactly one binder, so \`web\` and \`sync\` run at 1 here. Compose is one
|
|
29
34
|
# box; horizontal scaling of those two belongs to an orchestrator — \`docker/helm\`, beside this
|
|
30
35
|
# file, is the chart \`x deploy --method helm\` installs. To scale them on one box anyway, drop
|
|
@@ -129,7 +134,11 @@ services:
|
|
|
129
134
|
|
|
130
135
|
sync:
|
|
131
136
|
<<: *image
|
|
132
|
-
|
|
137
|
+
# The page is :3000 and this socket :3001 — two origins — and the node admits an upgrade from
|
|
138
|
+
# APP_URL's origin and no other once declared (scheme, host and port, exactly); undeclared, from
|
|
139
|
+
# the origin it was reached on. Unset, every websocket is 403 X_SOCKET_ORIGIN_REFUSED, so it is
|
|
140
|
+
# required: the same value web builds its links from.
|
|
141
|
+
environment: [ROLE=sync, 'APP_URL=\${APP_URL:?set APP_URL=http://<host>:3000 — the origin the pages are served on, see wiki/Deployment.md}']
|
|
133
142
|
depends_on:
|
|
134
143
|
db: { condition: service_healthy }
|
|
135
144
|
migrate: { condition: service_completed_successfully }
|
|
@@ -30,14 +30,16 @@ const dockerfile = (
|
|
|
30
30
|
# install refuses. \`bunfig.toml\` survives because its \`[install]\` section is input to the install,
|
|
31
31
|
# and so does a \`.tgz\`: a \`file:\` tarball dependency (a vendored or private package) is resolved
|
|
32
32
|
# from the file itself, and without it the frozen install cannot find what the lockfile names.
|
|
33
|
-
|
|
33
|
+
# The tag names the series for the reader; the digest is what builds — a tag is a pointer its
|
|
34
|
+
# publisher can move. Dependabot's docker ecosystem moves the digest together with the tag.
|
|
35
|
+
FROM oven/bun:1.4-alpine@sha256:d888c0ae6c86d7866ff10c5aafdd9077b36aee6455b33dd270fb93c0dd5cef6f AS manifests
|
|
34
36
|
WORKDIR /app
|
|
35
37
|
COPY . .
|
|
36
38
|
RUN find . ! -type d ! -name package.json ! -name bunfig.toml ! -name '*.tgz' -delete \\
|
|
37
39
|
&& find . -type d -empty -delete
|
|
38
40
|
|
|
39
41
|
# ---------- deps: runtime dependencies only, cached on the lockfile and the manifests ----------
|
|
40
|
-
FROM oven/bun:1.4-alpine AS deps
|
|
42
|
+
FROM oven/bun:1.4-alpine@sha256:d888c0ae6c86d7866ff10c5aafdd9077b36aee6455b33dd270fb93c0dd5cef6f AS deps
|
|
41
43
|
WORKDIR /app
|
|
42
44
|
COPY --from=manifests /app ./
|
|
43
45
|
COPY bun.lock ./
|
|
@@ -74,7 +74,7 @@ export const dbClientSource = (
|
|
|
74
74
|
// same two lines: its import, and \`<tableName>: <entity>,\` in the set.
|
|
75
75
|
|
|
76
76
|
${sortedImports([
|
|
77
|
-
"import {
|
|
77
|
+
"import { storeMode } from '@ultimat3/core';",
|
|
78
78
|
"import { type Driver, database, memoryDriver, postgresDriver } from '@ultimat3/entity';",
|
|
79
79
|
...entries.map(handleImportLine),
|
|
80
80
|
])}
|
|
@@ -86,9 +86,11 @@ ${sortedImports([
|
|
|
86
86
|
* \`postgresDriver()\` takes no connection: it resolves \`@ultimat3/db\`'s process client, which is
|
|
87
87
|
* the embedded PGlite under \`x dev\` and the \`DATABASE_URL\` pool in a container. \`bun test\`
|
|
88
88
|
* installs no client, so a test gets the in-memory driver — the same contract, no database.
|
|
89
|
+
* \`storeMode\` is core's one answer to that question, shared with every other seam that has a
|
|
90
|
+
* memory and a database store.
|
|
89
91
|
*/
|
|
90
92
|
export const selectDriver = (env: Readonly<Record<string, string | undefined>>): Driver =>
|
|
91
|
-
|
|
93
|
+
storeMode(env) === 'memory' ? memoryDriver() : postgresDriver();
|
|
92
94
|
|
|
93
95
|
export const driver = selectDriver(Bun.env);
|
|
94
96
|
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// What a compromised pod of the scaffolded chart can reach: its `/tmp`, bounded, and its ports,
|
|
2
|
+
// fenced by one NetworkPolicy per role. The framework chart's own (`docker/helm/`), mirrored —
|
|
3
|
+
// `scaffold-helm-parity.test.ts` holds the two to each other.
|
|
4
|
+
|
|
5
|
+
import type { GeneratedFile, NameSet } from './naming';
|
|
6
|
+
|
|
7
|
+
const volumes = (app: NameSet): string => `{{/*
|
|
8
|
+
The writable scratch every pod mounts at /tmp — the root filesystem is read-only. Bounded, because
|
|
9
|
+
an emptyDir with no sizeLimit is node disk: one role filling /tmp evicts its NEIGHBOURS for disk
|
|
10
|
+
pressure, where with a limit the kubelet evicts that pod alone.
|
|
11
|
+
*/}}
|
|
12
|
+
{{- define "${app.kebab}.tmpVolume" -}}
|
|
13
|
+
- name: tmp
|
|
14
|
+
emptyDir:
|
|
15
|
+
sizeLimit: {{ required "tmp.sizeLimit is unset — set it in your values (e.g. 512Mi): an unbounded /tmp is node disk" (default dict .Values.tmp).sizeLimit | quote }}
|
|
16
|
+
{{- end -}}
|
|
17
|
+
`;
|
|
18
|
+
|
|
19
|
+
const networkPolicy = (app: NameSet): string => `{{/*
|
|
20
|
+
One NetworkPolicy per enabled role: \`http\` (web, sync) from networkPolicy.httpFrom, \`metrics\`
|
|
21
|
+
(every role) from networkPolicy.metricsFrom, nothing else. A \`from\` that renders empty is omitted,
|
|
22
|
+
which Kubernetes reads as "any source". Egress is filtered only when networkPolicy.egress lists
|
|
23
|
+
rules — the database, NATS and object store are your addresses, so list DNS too. Serving roles
|
|
24
|
+
only: the migrate Job is a pre-install hook that runs before this exists, and opens no port.
|
|
25
|
+
*/}}
|
|
26
|
+
{{- if .Values.networkPolicy.enabled }}
|
|
27
|
+
{{- range $role, $cfg := .Values.roles }}
|
|
28
|
+
{{- if $cfg.enabled }}
|
|
29
|
+
---
|
|
30
|
+
apiVersion: networking.k8s.io/v1
|
|
31
|
+
kind: NetworkPolicy
|
|
32
|
+
metadata:
|
|
33
|
+
name: {{ include "${app.kebab}.fullname" $ }}-{{ $role }}
|
|
34
|
+
labels:
|
|
35
|
+
{{- include "${app.kebab}.labels" $ | nindent 4 }}
|
|
36
|
+
app.kubernetes.io/component: {{ $role }}
|
|
37
|
+
spec:
|
|
38
|
+
podSelector:
|
|
39
|
+
matchLabels:
|
|
40
|
+
app.kubernetes.io/instance: {{ $.Release.Name }}
|
|
41
|
+
app.kubernetes.io/component: {{ $role }}
|
|
42
|
+
policyTypes:
|
|
43
|
+
- Ingress
|
|
44
|
+
{{- if $.Values.networkPolicy.egress }}
|
|
45
|
+
- Egress
|
|
46
|
+
{{- end }}
|
|
47
|
+
ingress:
|
|
48
|
+
{{- if $cfg.port }}
|
|
49
|
+
- ports:
|
|
50
|
+
- port: http
|
|
51
|
+
protocol: TCP
|
|
52
|
+
{{- with $.Values.networkPolicy.httpFrom }}
|
|
53
|
+
from: {{- toYaml . | nindent 8 }}
|
|
54
|
+
{{- end }}
|
|
55
|
+
{{- end }}
|
|
56
|
+
- ports:
|
|
57
|
+
- port: metrics
|
|
58
|
+
protocol: TCP
|
|
59
|
+
{{- with $.Values.networkPolicy.metricsFrom }}
|
|
60
|
+
from: {{- toYaml . | nindent 8 }}
|
|
61
|
+
{{- end }}
|
|
62
|
+
{{- with $.Values.networkPolicy.egress }}
|
|
63
|
+
egress: {{- toYaml . | nindent 4 }}
|
|
64
|
+
{{- end }}
|
|
65
|
+
{{- end }}
|
|
66
|
+
{{- end }}
|
|
67
|
+
{{- end }}
|
|
68
|
+
`;
|
|
69
|
+
|
|
70
|
+
/** The bounded /tmp helper and the per-role NetworkPolicy, as two files of the chart's templates. */
|
|
71
|
+
export function helmFenceFiles(app: NameSet): readonly GeneratedFile[] {
|
|
72
|
+
return [
|
|
73
|
+
{ path: 'docker/helm/templates/_volumes.tpl', contents: volumes(app) },
|
|
74
|
+
{ path: 'docker/helm/templates/networkpolicy.yaml', contents: networkPolicy(app) },
|
|
75
|
+
];
|
|
76
|
+
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// Split from scaffold-helm.ts, which holds the chart's INPUTS (Chart.yaml, values.yaml).
|
|
4
4
|
|
|
5
5
|
import type { GeneratedFile, NameSet } from './naming';
|
|
6
|
+
import { helmFenceFiles } from './scaffold-helm-fences';
|
|
6
7
|
|
|
7
8
|
const helpers = (
|
|
8
9
|
app: NameSet,
|
|
@@ -79,13 +80,28 @@ nobody bound. Derived here rather than stated twice in values.yaml, where the tw
|
|
|
79
80
|
- name: METRICS_PORT
|
|
80
81
|
value: {{ $root.Values.metricsPort | quote }}
|
|
81
82
|
{{- end }}
|
|
83
|
+
{{- /*
|
|
84
|
+
The sync node's page origin, from the one place this chart already knows it. Undeclared, a node
|
|
85
|
+
reached over plain http behind this Ingress admits BOTH spellings of its host — the plain-http
|
|
86
|
+
page included. env.APP_URL wins when set; an APP_URL kept in the Secret is shadowed for this
|
|
87
|
+
role, so pages served from another host than the Ingress's must say so in env.
|
|
88
|
+
*/}}
|
|
89
|
+
{{- if and (eq $role "sync") $root.Values.ingress.enabled (not (hasKey $root.Values.env "APP_URL")) }}
|
|
90
|
+
- name: APP_URL
|
|
91
|
+
value: {{ printf "%s://%s" (ternary "https" "http" $root.Values.ingress.tls) $root.Values.ingress.host | quote }}
|
|
92
|
+
{{- end }}
|
|
82
93
|
{{- range $key, $value := $root.Values.env }}
|
|
83
94
|
- name: {{ $key }}
|
|
84
95
|
value: {{ $value | quote }}
|
|
85
96
|
{{- end }}
|
|
97
|
+
{{- /*
|
|
98
|
+
The Secret a role reads, per role: roles.<role>.existingSecret (or migrate.existingSecret) names a
|
|
99
|
+
narrower one than the release-wide existingSecret, so the migrate Job's schema-owning credential
|
|
100
|
+
need not sit on a web pod. Both unset is a render error, not a pod with no configuration.
|
|
101
|
+
*/}}
|
|
86
102
|
envFrom:
|
|
87
103
|
- secretRef:
|
|
88
|
-
name: {{ $root.Values.existingSecret }}
|
|
104
|
+
name: {{ required (printf "no Secret for role %s — set existingSecret, or roles.%s.existingSecret, in your values" $role $role) (default $root.Values.existingSecret $cfg.existingSecret) }}
|
|
89
105
|
{{- if or $cfg.port $scraped }}
|
|
90
106
|
ports:
|
|
91
107
|
{{- if $cfg.port }}
|
|
@@ -100,8 +116,10 @@ nobody bound. Derived here rather than stated twice in values.yaml, where the tw
|
|
|
100
116
|
{{/*
|
|
101
117
|
Probes follow the role, because the roles do not agree on what they open. web and sync serve
|
|
102
118
|
HTTP and get both. worker, scheduler and replicator open no HTTP socket at all — the scrape
|
|
103
|
-
listener is their only port — so they take liveness on it
|
|
104
|
-
|
|
119
|
+
listener is their only port — so they take liveness on it. Readiness for replicator alone, on
|
|
120
|
+
/readyz of that listener: 503 while the replication stream is down, so the pod shows 0/1 instead
|
|
121
|
+
of a healthy pod feeding nothing. Their headless Service publishes not-ready addresses, so a
|
|
122
|
+
not-ready pod is still scraped.
|
|
105
123
|
|
|
106
124
|
Every one of them takes a startupProbe too, because no listener opens early: the server builds
|
|
107
125
|
the app's islands before any role binds a port, and a liveness probe counting from container
|
|
@@ -134,6 +152,11 @@ nobody bound. Derived here rather than stated twice in values.yaml, where the tw
|
|
|
134
152
|
httpGet: { path: /metrics, port: metrics }
|
|
135
153
|
periodSeconds: 5
|
|
136
154
|
failureThreshold: 30
|
|
155
|
+
{{- if eq $role "replicator" }}
|
|
156
|
+
readinessProbe:
|
|
157
|
+
httpGet: { path: '/readyz?deep=1', port: metrics }
|
|
158
|
+
periodSeconds: 10
|
|
159
|
+
{{- end }}
|
|
137
160
|
livenessProbe:
|
|
138
161
|
httpGet: { path: /metrics, port: metrics }
|
|
139
162
|
periodSeconds: 15
|
|
@@ -194,8 +217,7 @@ spec:
|
|
|
194
217
|
containers:
|
|
195
218
|
{{- include "${app.kebab}.container" (dict "role" $role "cfg" $cfg "root" $) | nindent 8 }}
|
|
196
219
|
volumes:
|
|
197
|
-
-
|
|
198
|
-
emptyDir: {}
|
|
220
|
+
{{- include "${app.kebab}.tmpVolume" $ | nindent 8 }}
|
|
199
221
|
{{- end }}
|
|
200
222
|
{{- end }}
|
|
201
223
|
`;
|
|
@@ -219,6 +241,9 @@ spec:
|
|
|
219
241
|
type: ClusterIP
|
|
220
242
|
{{- if not $cfg.port }}
|
|
221
243
|
clusterIP: None
|
|
244
|
+
# Scraped whether or not ready: a replicator whose stream is down (readiness 503) is the pod
|
|
245
|
+
# whose metrics matter most, and nothing routes to these addresses anyway.
|
|
246
|
+
publishNotReadyAddresses: true
|
|
222
247
|
{{- end }}
|
|
223
248
|
selector:
|
|
224
249
|
app.kubernetes.io/instance: {{ $.Release.Name }}
|
|
@@ -267,8 +292,7 @@ spec:
|
|
|
267
292
|
containers:
|
|
268
293
|
{{- include "${app.kebab}.container" (dict "role" "migrate" "cfg" .Values.migrate "root" .) | nindent 8 }}
|
|
269
294
|
volumes:
|
|
270
|
-
-
|
|
271
|
-
emptyDir: {}
|
|
295
|
+
{{- include "${app.kebab}.tmpVolume" . | nindent 8 }}
|
|
272
296
|
{{- end }}
|
|
273
297
|
`;
|
|
274
298
|
|
|
@@ -382,5 +406,6 @@ export function helmTemplateFiles(app: NameSet): readonly GeneratedFile[] {
|
|
|
382
406
|
{ path: 'docker/helm/templates/migrate-job.yaml', contents: migrateJob(app) },
|
|
383
407
|
{ path: 'docker/helm/templates/ingress.yaml', contents: ingress(app) },
|
|
384
408
|
{ path: 'docker/helm/templates/hpa.yaml', contents: hpa(app) },
|
|
409
|
+
...helmFenceFiles(app),
|
|
385
410
|
];
|
|
386
411
|
}
|
|
@@ -31,11 +31,41 @@ image:
|
|
|
31
31
|
|
|
32
32
|
# Non-secret configuration. Secrets come from an existing Secret, never from this file:
|
|
33
33
|
# kubectl create secret generic ${app.kebab}-secrets --from-literal=DATABASE_URL=...
|
|
34
|
+
#
|
|
35
|
+
# APP_URL — the pages' public origin — is what the sync node admits a websocket from. With the
|
|
36
|
+
# ingress below enabled the chart SETS it on the sync role from ingress.host (https when
|
|
37
|
+
# ingress.tls), because an undeclared node behind a TLS ingress also admits the plain-http page.
|
|
38
|
+
# Set it here when the pages are served from another host or port than that (a CDN domain, a
|
|
39
|
+
# SYNC_URL of its own): this value wins, and without it every websocket from those pages is
|
|
40
|
+
# 403 X_SOCKET_ORIGIN_REFUSED.
|
|
34
41
|
env:
|
|
35
42
|
NODE_ENV: production
|
|
36
43
|
|
|
37
44
|
existingSecret: ${app.kebab}-secrets # DATABASE_URL, NATS_URL, S3_*, AUTH_SECRET, ULTIMATE_CURSOR_SECRET
|
|
38
45
|
|
|
46
|
+
# The release-wide Secret above is every role's DEFAULT: \`existingSecret\` under a role
|
|
47
|
+
# (roles.web.existingSecret, migrate.existingSecret) names that role's own, so the migrate Job's
|
|
48
|
+
# schema-owning DATABASE_URL need not sit on a web pod.
|
|
49
|
+
|
|
50
|
+
# The /tmp every pod mounts (the root filesystem is read-only), bounded: past this the kubelet
|
|
51
|
+
# evicts the pod that filled it, never its neighbours for node disk pressure.
|
|
52
|
+
tmp:
|
|
53
|
+
sizeLimit: 512Mi
|
|
54
|
+
|
|
55
|
+
# One NetworkPolicy per serving role (templates/networkpolicy.yaml): \`http\` from httpFrom,
|
|
56
|
+
# \`metrics\` from metricsFrom, nothing else. Inert on a CNI that does not enforce NetworkPolicy.
|
|
57
|
+
# httpFrom empty = any source; narrow it to your ingress controller's namespace, e.g.
|
|
58
|
+
# [{ namespaceSelector: { matchLabels: { kubernetes.io/metadata.name: ingress-nginx } } }]
|
|
59
|
+
# metricsFrom default: any pod in the cluster and no address outside it
|
|
60
|
+
# egress OFF while empty; a list turns on egress filtering with exactly those rules —
|
|
61
|
+
# the database, NATS, the object store and DNS are your addresses, so list all of them
|
|
62
|
+
networkPolicy:
|
|
63
|
+
enabled: true
|
|
64
|
+
httpFrom: []
|
|
65
|
+
metricsFrom:
|
|
66
|
+
- namespaceSelector: {}
|
|
67
|
+
egress: []
|
|
68
|
+
|
|
39
69
|
# The scrape listener every serving role opens, on its own port and never the app's: the ingress
|
|
40
70
|
# routes / to web, so /metrics beside /healthz on 3000 is /metrics on the internet. This is
|
|
41
71
|
# METRICS_PORT in the container; move one and the other follows.
|
|
@@ -153,7 +183,7 @@ ingress:
|
|
|
153
183
|
`;
|
|
154
184
|
|
|
155
185
|
/**
|
|
156
|
-
* The chart for a new app:
|
|
186
|
+
* The chart for a new app: six kinds of object, all of them core Kubernetes. A ServiceMonitor and
|
|
157
187
|
* a PodDisruptionBudget are deliberately absent — the first needs a CRD `helm install` fails on
|
|
158
188
|
* where no Prometheus operator is installed, and both are cluster policy rather than app topology.
|
|
159
189
|
*/
|
|
@@ -41,6 +41,14 @@ const BIOME_VERSION = '2.5.8';
|
|
|
41
41
|
*/
|
|
42
42
|
const TYPESCRIPT_VERSION = '^7.0.2';
|
|
43
43
|
|
|
44
|
+
/**
|
|
45
|
+
* The Solid runtime every island and `@ultimat3/ui` component renders with, exact for the reason
|
|
46
|
+
* `BIOME_VERSION` is: the CLI compiles islands with `babel-preset-solid ^1.9.15`, and a scaffold
|
|
47
|
+
* that installed an older runtime than the compiler targets drifted as a buried literal (1.9.14
|
|
48
|
+
* beside the repository's 1.9.15). `scaffold-repo.test.ts` holds it equal to the root manifest's.
|
|
49
|
+
*/
|
|
50
|
+
const SOLID_VERSION = '1.9.15';
|
|
51
|
+
|
|
44
52
|
// `engines.bun` is `REQUIRED_BUN`, the floor the SHIPPED `x` enforces
|
|
45
53
|
// (`packages/cli/src/app-root.ts`), and not a second literal. It was one: `>=1.3.0`, a whole minor
|
|
46
54
|
// BELOW the CLI the app then runs, so on a box with Bun 1.3.x `bun install` succeeded and the very
|
|
@@ -99,7 +107,7 @@ const rootPackage = (app: NameSet, version: string): string => `{
|
|
|
99
107
|
"@ultimat3/schema": "^${version}",
|
|
100
108
|
"@ultimat3/seo": "^${version}",
|
|
101
109
|
"@ultimat3/ui": "^${version}",
|
|
102
|
-
"solid-js": "
|
|
110
|
+
"solid-js": "${SOLID_VERSION}"
|
|
103
111
|
},
|
|
104
112
|
"engines": {
|
|
105
113
|
"bun": ">=${REQUIRED_BUN}"
|
|
@@ -272,17 +272,11 @@ const toggleIsland =
|
|
|
272
272
|
// browser downloads. Named by SPECIFIER from the page, never by import:
|
|
273
273
|
// const ThemeSwitch = island({ src: '../../shared/theme-toggle.island.tsx', props: ['locale'] });
|
|
274
274
|
//
|
|
275
|
-
// The control itself is the catalog's \`ThemeToggle\`; this file is the runtime it needs
|
|
276
|
-
//
|
|
275
|
+
// The control itself is the catalog's \`ThemeToggle\`; this file is only the runtime it needs. It
|
|
276
|
+
// reads the browser on its own: the theme the boot stamped on \`<html>\`, then \`theme.defaultMode\`
|
|
277
|
+
// (stamped beside it), then the OS.
|
|
277
278
|
|
|
278
|
-
import {
|
|
279
|
-
browserThemeEnv,
|
|
280
|
-
setSolidRuntime,
|
|
281
|
-
THEME_ATTRIBUTE,
|
|
282
|
-
type ThemeEnv,
|
|
283
|
-
ThemeToggle,
|
|
284
|
-
UiProvider,
|
|
285
|
-
} from '@ultimat3/ui';
|
|
279
|
+
import { setSolidRuntime, ThemeToggle, UiProvider } from '@ultimat3/ui';
|
|
286
280
|
import {
|
|
287
281
|
createContext,
|
|
288
282
|
createEffect,
|
|
@@ -298,17 +292,6 @@ export interface ThemeToggleIslandProps {
|
|
|
298
292
|
readonly locale: string;
|
|
299
293
|
}
|
|
300
294
|
|
|
301
|
-
/**
|
|
302
|
-
* With no stored choice the catalog's \`resolveTheme\` asks the OS — but the document already wears
|
|
303
|
-
* the theme the framework's boot script stamped from \`theme.defaultMode\` (dark, in this app), so
|
|
304
|
-
* a toggle that asked the OS would show one theme while the page wore another. The boot's verdict
|
|
305
|
-
* is on \`<html>\` before this runs: read it once, and let a stored choice win as it does there.
|
|
306
|
-
*/
|
|
307
|
-
export const bootedEnv = (): ThemeEnv => {
|
|
308
|
-
const booted = document.documentElement.getAttribute(THEME_ATTRIBUTE) === 'dark';
|
|
309
|
-
return { ...browserThemeEnv(), prefersDark: () => booted };
|
|
310
|
-
};
|
|
311
|
-
|
|
312
295
|
/**
|
|
313
296
|
* The one export the hydration runtime calls. \`setSolidRuntime\` comes FIRST and is not optional:
|
|
314
297
|
* \`@ultimat3/ui\` imports types from solid-js and never a runtime, so the reactive graph a
|
|
@@ -318,12 +301,11 @@ export const bootedEnv = (): ThemeEnv => {
|
|
|
318
301
|
*/
|
|
319
302
|
export function mount(el: HTMLElement, props: ThemeToggleIslandProps): void {
|
|
320
303
|
setSolidRuntime({ createContext, useContext, createSignal, createMemo, createEffect, onCleanup });
|
|
321
|
-
const env = bootedEnv();
|
|
322
304
|
el.textContent = '';
|
|
323
305
|
render(
|
|
324
306
|
() => (
|
|
325
307
|
<UiProvider locale={props.locale}>
|
|
326
|
-
<ThemeToggle mode="toggle"
|
|
308
|
+
<ThemeToggle mode="toggle" />
|
|
327
309
|
</UiProvider>
|
|
328
310
|
),
|
|
329
311
|
el,
|
|
@@ -353,15 +335,13 @@ export const themeToggleStates = defineIslandStates({
|
|
|
353
335
|
|
|
354
336
|
const toggleTest =
|
|
355
337
|
(): string => `// The toggle the browser actually runs: built with the same \`buildIslands\` as \`x build\`, mounted
|
|
356
|
-
// against a DOM small enough to read, and clicked. What it pins
|
|
357
|
-
//
|
|
358
|
-
// key the boot script reads back on the next load.
|
|
338
|
+
// against a DOM small enough to read, and clicked. What it pins: the boot's verdict on \`<html>\` is
|
|
339
|
+
// what the control starts from, and a click writes the key the boot script reads back next load.
|
|
359
340
|
|
|
360
341
|
import { join } from 'node:path';
|
|
361
342
|
import { buildIslands } from '@ultimat3/cli';
|
|
362
343
|
import { describeIslandState, expect, test } from '@ultimat3/testing';
|
|
363
|
-
import { THEME_ATTRIBUTE, THEME_STORAGE_KEY } from '@ultimat3/ui';
|
|
364
|
-
import { bootedEnv } from './theme-toggle.island';
|
|
344
|
+
import { THEME_ATTRIBUTE, THEME_DEFAULT_ATTRIBUTE, THEME_STORAGE_KEY } from '@ultimat3/ui';
|
|
365
345
|
import { themeToggleStates } from './theme-toggle.island.states';
|
|
366
346
|
|
|
367
347
|
/** What the island reads through \`browserThemeEnv\`: storage and the OS query, both fakes. */
|
|
@@ -371,11 +351,19 @@ const localStorage = {
|
|
|
371
351
|
setItem: (key: string, value: string): void => void stored.set(key, value),
|
|
372
352
|
removeItem: (key: string): void => void stored.delete(key),
|
|
373
353
|
};
|
|
354
|
+
/** One OS query, shared by every \`matchMedia\` call: \`flipOs\` changes it and tells the listeners. */
|
|
355
|
+
const os = { dark: false, listeners: new Set<() => void>() };
|
|
374
356
|
const matchMedia = (): Record<string, unknown> => ({
|
|
375
|
-
matches:
|
|
376
|
-
|
|
377
|
-
|
|
357
|
+
get matches(): boolean {
|
|
358
|
+
return os.dark;
|
|
359
|
+
},
|
|
360
|
+
addEventListener: (_type: string, listener: () => void) => void os.listeners.add(listener),
|
|
361
|
+
removeEventListener: (_type: string, listener: () => void) => void os.listeners.delete(listener),
|
|
378
362
|
});
|
|
363
|
+
const flipOs = (dark: boolean): void => {
|
|
364
|
+
os.dark = dark;
|
|
365
|
+
for (const listener of os.listeners) listener();
|
|
366
|
+
};
|
|
379
367
|
|
|
380
368
|
// The state \`x shot --island theme-toggle\` photographs is the state this mounts: one manifest,
|
|
381
369
|
// so the picture and the assertion cannot be of two different components.
|
|
@@ -404,12 +392,27 @@ describeIslandState(themeToggleStates, 'default', island, (mounted) => {
|
|
|
404
392
|
expect(mounted().documentElement.getAttribute(THEME_ATTRIBUTE)).toBe('dark');
|
|
405
393
|
});
|
|
406
394
|
|
|
407
|
-
test('
|
|
408
|
-
// The fake
|
|
395
|
+
test('with nothing stored, a click flips the theme the boot stamped — not the OS', () => {
|
|
396
|
+
// The OS fake says light. The boot stamped this app's \`defaultMode\`, dark, so the visible
|
|
397
|
+
// theme is dark and the first click must go light; a toggle that asked the OS would write the
|
|
398
|
+
// dark already on screen and look dead.
|
|
399
|
+
stored.clear();
|
|
400
|
+
mounted().documentElement.setAttribute(THEME_DEFAULT_ATTRIBUTE, 'dark');
|
|
409
401
|
mounted().documentElement.setAttribute(THEME_ATTRIBUTE, 'dark');
|
|
410
|
-
expect(
|
|
411
|
-
|
|
412
|
-
expect(
|
|
402
|
+
expect(mounted().fire('button', 'click')).toBe(true);
|
|
403
|
+
expect(stored.get(THEME_STORAGE_KEY)).toBe('light');
|
|
404
|
+
expect(mounted().documentElement.getAttribute(THEME_ATTRIBUTE)).toBe('light');
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
test("an OS flip is followed while nothing is stored and the app's default is 'system'", () => {
|
|
408
|
+
stored.clear();
|
|
409
|
+
mounted().documentElement.setAttribute(THEME_DEFAULT_ATTRIBUTE, 'system');
|
|
410
|
+
flipOs(true);
|
|
411
|
+
expect(mounted().documentElement.getAttribute(THEME_ATTRIBUTE)).toBe('dark');
|
|
412
|
+
// A fixed default is the app's own opinion: the OS does not override it.
|
|
413
|
+
mounted().documentElement.setAttribute(THEME_DEFAULT_ATTRIBUTE, 'dark');
|
|
414
|
+
flipOs(false);
|
|
415
|
+
expect(mounted().documentElement.getAttribute(THEME_ATTRIBUTE)).toBe('dark');
|
|
413
416
|
});
|
|
414
417
|
});
|
|
415
418
|
`;
|
package/src/test-counts.ts
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
import type { ExecResult } from './exec';
|
|
6
6
|
import { execOutput } from './exec';
|
|
7
7
|
import { parseBunTest } from './mcp-test-output';
|
|
8
|
+
// One reader of bun's `N errors` line, shared with MCP `tests.run` — never a second regex.
|
|
9
|
+
import { errorsIn } from './mcp-test-run';
|
|
8
10
|
|
|
9
11
|
export interface TestCounts {
|
|
10
12
|
/** Tests that executed — passed plus failed. A failed test is a test that ran. */
|
|
@@ -20,12 +22,6 @@ export interface TestCounts {
|
|
|
20
22
|
readonly errors?: number;
|
|
21
23
|
}
|
|
22
24
|
|
|
23
|
-
/** Bun's own `N error` / `N errors` summary line: the last one, as `parseBunTest` reads the rest. */
|
|
24
|
-
const errorsIn = (output: string): number => {
|
|
25
|
-
const last = [...output.matchAll(/^\s*(\d+)\s+errors?\s*$/gm)].at(-1);
|
|
26
|
-
return last === undefined ? 0 : Number.parseInt(last[1] ?? '0', 10);
|
|
27
|
-
};
|
|
28
|
-
|
|
29
25
|
/**
|
|
30
26
|
* Summed across every process the step spawned, because a step is one line in the gate whether it
|
|
31
27
|
* ran on one worker or eight.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// The ONE policy for which paths are never a test the gate runs here, projected twice: the path
|
|
2
|
+
// predicate `discoverTests` filters by and the `--path-ignore-patterns` the serial steps pass to
|
|
3
|
+
// `bun test`. A leaf, so `test-select.ts` and `verify-tests.ts` (which import each other) share it.
|
|
4
|
+
|
|
5
|
+
/** Build output and installed packages: never a test, at ANY depth — a workspace's `dist/` too. */
|
|
6
|
+
const IGNORED_ANYWHERE = ['dist', 'node_modules'] as const;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Nested projects with their own gate (`examples/`, `dummy/`, run by
|
|
10
|
+
* `scripts/reference-app-gate.ts`) and the root `build/` — skipped AT THE ROOT only. Matched at any
|
|
11
|
+
* depth they swallowed an app's own slice: `x g resource build` lost 14 of its 16 test files and
|
|
12
|
+
* `x g resource example` its `examples/` page tests, from `x test` and the gate alike.
|
|
13
|
+
*/
|
|
14
|
+
const IGNORED_AT_ROOT = ['build', 'examples', 'dummy'] as const;
|
|
15
|
+
|
|
16
|
+
/** Whether a root-relative POSIX path is excluded from every test selection. */
|
|
17
|
+
export const isIgnoredTestPath = (path: string): boolean =>
|
|
18
|
+
IGNORED_ANYWHERE.some((dir) => `/${path}`.includes(`/${dir}/`)) ||
|
|
19
|
+
IGNORED_AT_ROOT.some((dir) => path.startsWith(`${dir}/`));
|
|
20
|
+
|
|
21
|
+
/** The same policy as `bun test --path-ignore-patterns` globs, relative to the run's root. */
|
|
22
|
+
export const IGNORED_TEST_PATTERNS: readonly string[] = [
|
|
23
|
+
...IGNORED_ANYWHERE.map((dir) => `**/${dir}/**`),
|
|
24
|
+
...IGNORED_AT_ROOT.map((dir) => `${dir}/**`),
|
|
25
|
+
];
|
package/src/test-passes.ts
CHANGED
|
@@ -24,8 +24,6 @@ export interface PassInput {
|
|
|
24
24
|
readonly workers: number;
|
|
25
25
|
/** The positional, when there was one. A pass over one type keeps it; the split never adds one. */
|
|
26
26
|
readonly type?: TestType;
|
|
27
|
-
/** Set for a `--worker I` rerun, which is one process by construction. */
|
|
28
|
-
readonly shard?: number;
|
|
29
27
|
}
|
|
30
28
|
|
|
31
29
|
const widthFor = (files: readonly TestFile[], workers: number): number =>
|
|
@@ -45,22 +43,9 @@ const widthFor = (files: readonly TestFile[], workers: number): number =>
|
|
|
45
43
|
*
|
|
46
44
|
* Each serial type is its own pass rather than one pass over both, because the pass's `type` is
|
|
47
45
|
* what its failure reproduces with: `x test live --workers 1` selects exactly the files that ran.
|
|
48
|
-
*
|
|
49
|
-
* A `--worker I` rerun is left whole: it is a single `bun test --isolate --shard=i/N` process, so
|
|
50
|
-
* nothing inside it runs beside anything else, and splitting it would make shard i of the rerun a
|
|
51
|
-
* different set of files from shard i of the run it reproduces.
|
|
52
46
|
*/
|
|
53
47
|
export function testPasses(input: PassInput): readonly TestPass[] {
|
|
54
48
|
const serialTypes = new Set<TestType>(SERIAL_TYPES);
|
|
55
|
-
if (input.shard !== undefined) {
|
|
56
|
-
return [
|
|
57
|
-
{
|
|
58
|
-
files: input.files,
|
|
59
|
-
workers: widthFor(input.files, input.workers),
|
|
60
|
-
...(input.type === undefined ? {} : { type: input.type }),
|
|
61
|
-
},
|
|
62
|
-
];
|
|
63
|
-
}
|
|
64
49
|
const shared = input.files.filter((file) => !serialTypes.has(ownerOf(file.path)));
|
|
65
50
|
const passes: TestPass[] = [];
|
|
66
51
|
// Cheapest first, and the widest first: the pool run is most of the corpus and most of the
|
package/src/test-select.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { nearestName, renderFixShellArg } from '@ultimat3/core';
|
|
|
9
9
|
import { BadFlagError } from './errors';
|
|
10
10
|
import type { ParsedArgs } from './parse';
|
|
11
11
|
import { flagString } from './parse';
|
|
12
|
+
import { isIgnoredTestPath } from './test-ignores';
|
|
12
13
|
import type { TestType } from './verify-tests';
|
|
13
14
|
import { ownerOf, TEST_TYPES } from './verify-tests';
|
|
14
15
|
|
|
@@ -27,20 +28,10 @@ const TEST_GLOB = '**/*.test.{ts,tsx}';
|
|
|
27
28
|
|
|
28
29
|
/**
|
|
29
30
|
* The root `test` script's ignore list, kept identical so `x test` and `bun run test` see one
|
|
30
|
-
* suite
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
* `dummy/` and `build/` complete the list `verify-tests.ts` already excluded (`NEVER_A_TEST`). The
|
|
35
|
-
* comment above claimed the two agreed and they did not: `x test unit` discovered 464 files where
|
|
36
|
-
* the gate's `unit` step ran 441, so the gate's own test steps — which now select through this
|
|
37
|
-
* function — would have started running a nested demo app's suite on the framework's gate.
|
|
38
|
-
*
|
|
39
|
-
* Both directories are gated where they belong, by `scripts/reference-app-gate.ts` running
|
|
40
|
-
* `x verify` inside each app — see `NEVER_A_TEST` for why that is the only place they run.
|
|
31
|
+
* suite — `test-ignores.ts` holds it, and the serial steps' argv projects the same policy. `e2e/`
|
|
32
|
+
* is NOT on it: an opt-in suite that the gate runs but `x test` silently drops is a suite nobody
|
|
33
|
+
* runs until CI says so.
|
|
41
34
|
*/
|
|
42
|
-
const IGNORED = ['/dist/', '/build/', '/node_modules/', '/examples/', '/dummy/'];
|
|
43
|
-
|
|
44
35
|
/**
|
|
45
36
|
* File size stands in for duration: cheap to read, and it correlates far better than file count.
|
|
46
37
|
* `type`, when given, narrows to exactly the files verify-tests.ts would run for that suite — one
|
|
@@ -58,7 +49,7 @@ export async function discoverTests(
|
|
|
58
49
|
const files: TestFile[] = [];
|
|
59
50
|
for await (const found of new Bun.Glob(TEST_GLOB).scan({ cwd: root, absolute: false })) {
|
|
60
51
|
const path = found.split('\\').join('/');
|
|
61
|
-
if (
|
|
52
|
+
if (isIgnoredTestPath(path)) continue;
|
|
62
53
|
if (filters !== undefined && !filters.some((part) => path.includes(part))) continue;
|
|
63
54
|
if (type !== undefined && !belongsToType(path, type)) continue;
|
|
64
55
|
files.push({ path, bytes: Bun.file(join(root, path)).size });
|