@lenne.tech/cli 1.26.1 → 1.28.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/build/commands/dev/doctor.js +27 -1
- package/build/commands/dev/down.js +34 -10
- package/build/commands/dev/status.js +23 -4
- package/build/commands/dev/test.js +206 -119
- package/build/commands/dev/up.js +90 -50
- package/build/commands/ticket/list.js +78 -0
- package/build/commands/ticket/start.js +141 -0
- package/build/commands/ticket/stop.js +166 -0
- package/build/commands/ticket/switch.js +70 -0
- package/build/commands/ticket/test.js +80 -0
- package/build/commands/ticket/ticket.js +36 -0
- package/build/lib/dev-env-bridge.js +6 -6
- package/build/lib/dev-identity.js +36 -0
- package/build/lib/dev-migrate-helper.js +16 -6
- package/build/lib/dev-patches.js +71 -0
- package/build/lib/dev-process.js +154 -0
- package/build/lib/dev-project.js +34 -1
- package/build/lib/dev-state.js +105 -7
- package/build/lib/dev-test-session.js +430 -0
- package/build/lib/dev-ticket.js +343 -0
- package/docs/lt-dev-ticket-workflow.html +603 -0
- package/docs/lt-dev-ticket-workflow.pdf +0 -0
- package/package.json +32 -1
|
@@ -12,8 +12,10 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
12
12
|
const child_process_1 = require("child_process");
|
|
13
13
|
const caddy_1 = require("../../lib/caddy");
|
|
14
14
|
const dev_process_1 = require("../../lib/dev-process");
|
|
15
|
+
const dev_project_1 = require("../../lib/dev-project");
|
|
15
16
|
const dev_service_1 = require("../../lib/dev-service");
|
|
16
17
|
const dev_state_1 = require("../../lib/dev-state");
|
|
18
|
+
const dev_ticket_1 = require("../../lib/dev-ticket");
|
|
17
19
|
/**
|
|
18
20
|
* Diagnose Caddy / CA / DNS / port issues for `lt dev`.
|
|
19
21
|
*
|
|
@@ -30,7 +32,7 @@ const DoctorCommand = {
|
|
|
30
32
|
hidden: false,
|
|
31
33
|
name: 'doctor',
|
|
32
34
|
run: (toolbox) => __awaiter(void 0, void 0, void 0, function* () {
|
|
33
|
-
const { parameters, print: { colors, info }, } = toolbox;
|
|
35
|
+
const { filesystem, parameters, print: { colors, info }, } = toolbox;
|
|
34
36
|
info('');
|
|
35
37
|
info(colors.bold('lt dev doctor'));
|
|
36
38
|
info(colors.dim('─'.repeat(60)));
|
|
@@ -105,6 +107,30 @@ const DoctorCommand = {
|
|
|
105
107
|
const reg = (0, dev_state_1.loadRegistry)();
|
|
106
108
|
const count = Object.keys(reg.projects).length;
|
|
107
109
|
line('OK', colors.green, `registry: ${count} project(s) at ${dev_state_1.paths.registry}`);
|
|
110
|
+
// 7. Project-level (only when run inside a project): is a DB-wiping
|
|
111
|
+
// Playwright global-setup ticket/shard-safe? WARN (never auto-edit) if a
|
|
112
|
+
// bespoke allow-list would reject the per-ticket/shard `<base>-<id>-test`
|
|
113
|
+
// DBs that `lt ticket` / `lt dev test --shard` create.
|
|
114
|
+
const layout = (0, dev_project_1.resolveLayout)(filesystem.cwd(), filesystem);
|
|
115
|
+
if (layout.apiDir || layout.appDir) {
|
|
116
|
+
const gs = (0, dev_ticket_1.checkGlobalSetupTicketSafe)(layout);
|
|
117
|
+
if (gs.file && gs.hasDbReset && !gs.ticketSafe) {
|
|
118
|
+
line('WARN', colors.yellow, 'global-setup allow-list rejects per-ticket/shard test DBs — `lt ticket` / `--shard` E2E cannot reset its DB');
|
|
119
|
+
line('WARN', colors.yellow, ` ${gs.file}: widen isAllowedDb → /^<base>-(?:[a-z0-9-]+-)?test(?:-\\d+)?$/ (svl is the reference)`);
|
|
120
|
+
}
|
|
121
|
+
else if (gs.file && gs.hasDbReset) {
|
|
122
|
+
line('OK', colors.green, 'global-setup allow-list is ticket + shard safe');
|
|
123
|
+
}
|
|
124
|
+
// 8. Slug ↔ path: is this project's slug registered to a DIFFERENT checkout?
|
|
125
|
+
// Two clones of the same project (same package.json "name") share the
|
|
126
|
+
// slug → Caddy block / ports / DB and collide. Surface it proactively.
|
|
127
|
+
const { identity } = (0, dev_ticket_1.resolveDevIdentity)(layout);
|
|
128
|
+
const conflict = (0, dev_state_1.detectSlugConflict)(identity.slug, layout.root);
|
|
129
|
+
if (conflict) {
|
|
130
|
+
line('WARN', colors.yellow, `slug "${identity.slug}" is also registered to another checkout${conflict.otherSessionAlive ? ' (currently RUNNING)' : ''}: ${conflict.otherPath}`);
|
|
131
|
+
line('WARN', colors.yellow, ' two clones of the same project collide on URLs/ports/DB — rename one package.json "name", or run only one.');
|
|
132
|
+
}
|
|
133
|
+
}
|
|
108
134
|
info('');
|
|
109
135
|
if (fails > 0)
|
|
110
136
|
info(colors.red(`✗ ${fails} fail(s) — see above`));
|
|
@@ -11,10 +11,11 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
12
|
const caddy_1 = require("../../lib/caddy");
|
|
13
13
|
const dev_env_bridge_1 = require("../../lib/dev-env-bridge");
|
|
14
|
-
const dev_identity_1 = require("../../lib/dev-identity");
|
|
15
14
|
const dev_process_1 = require("../../lib/dev-process");
|
|
16
15
|
const dev_project_1 = require("../../lib/dev-project");
|
|
17
16
|
const dev_state_1 = require("../../lib/dev-state");
|
|
17
|
+
const dev_test_session_1 = require("../../lib/dev-test-session");
|
|
18
|
+
const dev_ticket_1 = require("../../lib/dev-ticket");
|
|
18
19
|
/**
|
|
19
20
|
* Stop the processes started by `lt dev up` and remove the project's
|
|
20
21
|
* Caddy block.
|
|
@@ -32,7 +33,9 @@ const DownCommand = {
|
|
|
32
33
|
run: (toolbox) => __awaiter(void 0, void 0, void 0, function* () {
|
|
33
34
|
const { filesystem, parameters, print: { colors, info, success, warning }, } = toolbox;
|
|
34
35
|
const layout = (0, dev_project_1.resolveLayout)(filesystem.cwd(), filesystem);
|
|
35
|
-
|
|
36
|
+
// Ticket-aware: in a ticket worktree the slug / Caddy block / test stack are
|
|
37
|
+
// suffixed (`<slug>-<id>`), so resolve the same identity `up` used.
|
|
38
|
+
const { identity } = (0, dev_ticket_1.resolveDevIdentity)(layout, { ticket: parameters.options.ticket });
|
|
36
39
|
const session = (0, dev_state_1.loadSession)(layout.root);
|
|
37
40
|
const stopped = [];
|
|
38
41
|
if (session) {
|
|
@@ -53,19 +56,40 @@ const DownCommand = {
|
|
|
53
56
|
else {
|
|
54
57
|
info(colors.dim('No running processes registered for this project.'));
|
|
55
58
|
}
|
|
56
|
-
//
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
59
|
+
// Don't clobber another checkout: when this slug is registered to a DIFFERENT
|
|
60
|
+
// checkout (two clones of the same project share a package.json "name" → slug),
|
|
61
|
+
// the Caddy block + registration belong to IT — stop only OUR processes
|
|
62
|
+
// (above) and leave its routing intact. Otherwise remove the block as usual.
|
|
63
|
+
const conflict = (0, dev_state_1.detectSlugConflict)(identity.slug, layout.root);
|
|
64
|
+
if (conflict) {
|
|
65
|
+
warning(`Slug "${identity.slug}" is registered to another checkout — leaving its Caddy block + registration untouched:`);
|
|
66
|
+
info(colors.dim(` ${conflict.otherPath}`));
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
const removed = (0, caddy_1.removeProjectBlock)(identity.slug);
|
|
70
|
+
if (removed) {
|
|
71
|
+
const r = yield (0, caddy_1.reloadCaddy)();
|
|
72
|
+
if (r.ok)
|
|
73
|
+
success(`Removed Caddy block for "${identity.slug}".`);
|
|
74
|
+
else
|
|
75
|
+
warning(`Removed Caddy block but reload failed: ${r.stderr.split('\n')[0]}`);
|
|
76
|
+
}
|
|
64
77
|
}
|
|
65
78
|
// Clear ENV bridge so subsequent test runs without `lt dev up`
|
|
66
79
|
// do not pick up stale URLs.
|
|
67
80
|
if ((0, dev_env_bridge_1.clearEnvBridge)(layout.root))
|
|
68
81
|
info(colors.dim('Removed .lt-dev/.env bridge.'));
|
|
82
|
+
// Also tear down any isolated test stack (`lt dev test`) for this project,
|
|
83
|
+
// so `lt dev down` always leaves a clean slate.
|
|
84
|
+
if ((0, dev_test_session_1.hasTestSession)(layout.root)) {
|
|
85
|
+
const { stopped: testStopped } = yield (0, dev_test_session_1.tearDownTestSession)(layout, identity, {
|
|
86
|
+
dim: colors.dim,
|
|
87
|
+
info,
|
|
88
|
+
warn: warning,
|
|
89
|
+
});
|
|
90
|
+
if (testStopped.length > 0)
|
|
91
|
+
success(`Stopped test stack: ${testStopped.join(', ')}`);
|
|
92
|
+
}
|
|
69
93
|
if (stopped.length > 0)
|
|
70
94
|
success(`Stopped: ${stopped.join(', ')}`);
|
|
71
95
|
if (!parameters.options.fromGluegunMenu)
|
|
@@ -10,10 +10,10 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
10
10
|
};
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
12
|
const caddy_1 = require("../../lib/caddy");
|
|
13
|
-
const dev_identity_1 = require("../../lib/dev-identity");
|
|
14
13
|
const dev_process_1 = require("../../lib/dev-process");
|
|
15
14
|
const dev_project_1 = require("../../lib/dev-project");
|
|
16
15
|
const dev_state_1 = require("../../lib/dev-state");
|
|
16
|
+
const dev_ticket_1 = require("../../lib/dev-ticket");
|
|
17
17
|
/**
|
|
18
18
|
* Show what is running.
|
|
19
19
|
*
|
|
@@ -26,7 +26,7 @@ const StatusCommand = {
|
|
|
26
26
|
hidden: false,
|
|
27
27
|
name: 'status',
|
|
28
28
|
run: (toolbox) => __awaiter(void 0, void 0, void 0, function* () {
|
|
29
|
-
var _a, _b;
|
|
29
|
+
var _a, _b, _c, _d;
|
|
30
30
|
const { filesystem, parameters, print: { colors, info, warning }, } = toolbox;
|
|
31
31
|
const all = Boolean(parameters.options.all);
|
|
32
32
|
const reg = (0, dev_state_1.loadRegistry)();
|
|
@@ -56,10 +56,11 @@ const StatusCommand = {
|
|
|
56
56
|
return 'dev status: all';
|
|
57
57
|
}
|
|
58
58
|
const layout = (0, dev_project_1.resolveLayout)(filesystem.cwd(), filesystem);
|
|
59
|
-
|
|
59
|
+
// Ticket-aware: a ticket worktree reports its OWN suffixed stack.
|
|
60
|
+
const { identity, ticket } = (0, dev_ticket_1.resolveDevIdentity)(layout, { ticket: parameters.options.ticket });
|
|
60
61
|
const entry = reg.projects[identity.slug];
|
|
61
62
|
info('');
|
|
62
|
-
info(colors.bold(`lt dev status: ${identity.slug}`));
|
|
63
|
+
info(colors.bold(`lt dev status: ${identity.slug}`) + (ticket ? colors.dim(` (ticket ${ticket})`) : ''));
|
|
63
64
|
info(colors.dim('─'.repeat(60)));
|
|
64
65
|
if (!entry) {
|
|
65
66
|
warning('Not registered. Run `lt dev init` first.');
|
|
@@ -144,6 +145,24 @@ const StatusCommand = {
|
|
|
144
145
|
}
|
|
145
146
|
}
|
|
146
147
|
}
|
|
148
|
+
// Isolated test stack (`lt dev test`), if one is up / left with --keep.
|
|
149
|
+
const testSession = (0, dev_state_1.loadSession)(layout.root, dev_state_1.TEST_SESSION_FILE);
|
|
150
|
+
if (testSession) {
|
|
151
|
+
const testEntry = reg.projects[`${identity.slug}-test`];
|
|
152
|
+
info('');
|
|
153
|
+
info(colors.bold(' Isolated test stack (lt dev test)'));
|
|
154
|
+
if (testEntry) {
|
|
155
|
+
for (const [sub, host] of Object.entries(testEntry.subdomains))
|
|
156
|
+
info(` ${sub.padEnd(6)} https://${host}`);
|
|
157
|
+
if (testEntry.dbName)
|
|
158
|
+
info(` db mongodb://127.0.0.1/${testEntry.dbName}`);
|
|
159
|
+
}
|
|
160
|
+
const apiAlive = testSession.pids.api ? (0, dev_state_1.isPidAlive)(testSession.pids.api) : false;
|
|
161
|
+
const appAlive = testSession.pids.app ? (0, dev_state_1.isPidAlive)(testSession.pids.app) : false;
|
|
162
|
+
info(` api: ${apiAlive ? colors.green('running') : colors.red('dead')} (pid ${(_c = testSession.pids.api) !== null && _c !== void 0 ? _c : '-'})`);
|
|
163
|
+
info(` app: ${appAlive ? colors.green('running') : colors.red('dead')} (pid ${(_d = testSession.pids.app) !== null && _d !== void 0 ? _d : '-'})`);
|
|
164
|
+
info(colors.dim(' stop: lt dev test down'));
|
|
165
|
+
}
|
|
147
166
|
info('');
|
|
148
167
|
if (!parameters.options.fromGluegunMenu)
|
|
149
168
|
process.exit();
|
|
@@ -9,45 +9,44 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
9
9
|
});
|
|
10
10
|
};
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
-
const child_process_1 = require("child_process");
|
|
13
12
|
const fs_1 = require("fs");
|
|
14
13
|
const caddy_1 = require("../../lib/caddy");
|
|
15
14
|
const dev_env_bridge_1 = require("../../lib/dev-env-bridge");
|
|
16
|
-
const
|
|
15
|
+
const dev_process_1 = require("../../lib/dev-process");
|
|
17
16
|
const dev_project_1 = require("../../lib/dev-project");
|
|
18
|
-
const
|
|
17
|
+
const dev_test_session_1 = require("../../lib/dev-test-session");
|
|
18
|
+
const dev_ticket_1 = require("../../lib/dev-ticket");
|
|
19
19
|
/**
|
|
20
20
|
* One-shot E2E convenience wrapper.
|
|
21
21
|
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
22
|
+
* App mode (default) runs the Playwright suite against a fully ISOLATED test
|
|
23
|
+
* stack (own URLs / ports / Caddy block / dedicated `<…>-test` database) that
|
|
24
|
+
* runs parallel to — and never touches — the developer's `lt dev up` session.
|
|
25
|
+
* Playwright's global-setup resets that dedicated DB once before the first test.
|
|
26
|
+
* The stack is torn down automatically when the run finishes (residue-free).
|
|
26
27
|
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
* 2. Wait for the App URL to respond (best-effort, ~30s timeout).
|
|
30
|
-
* 3. Inherit the env from `<root>/.lt-dev/.env` (so VS Code IDE-style
|
|
31
|
-
* runners get the same setup) and spawn the test command in the
|
|
32
|
-
* App project.
|
|
33
|
-
* 4. With `--teardown` (default false): run `lt dev down` after.
|
|
28
|
+
* API mode (`--api`) runs the standalone API test suite (`pnpm test:e2e` in the
|
|
29
|
+
* API), which already isolates itself on its own DB — no stack is brought up.
|
|
34
30
|
*
|
|
35
31
|
* Usage:
|
|
36
|
-
* lt dev test
|
|
37
|
-
* lt dev test --
|
|
38
|
-
* lt dev test
|
|
39
|
-
* lt dev test --
|
|
40
|
-
* lt dev test --
|
|
41
|
-
* lt dev test --
|
|
32
|
+
* lt dev test # isolated Playwright E2E (auto teardown)
|
|
33
|
+
* lt dev test --keep # leave the test stack up afterwards (debug)
|
|
34
|
+
* lt dev test down # tear the test stack down (residue-free)
|
|
35
|
+
* lt dev test --api # run API tests instead (already isolated)
|
|
36
|
+
* lt dev test --debug # PWDEBUG=1 + headed
|
|
37
|
+
* lt dev test --shard # shard across 2 isolated stacks (default — stable)
|
|
38
|
+
* lt dev test --shard N # shard across N isolated stacks (parallel)
|
|
39
|
+
* lt dev test --shard auto # size N from this machine's CPU + RAM
|
|
40
|
+
* lt dev test -- <args> # forward args to Playwright
|
|
42
41
|
*/
|
|
43
42
|
const TestCommand = {
|
|
44
43
|
alias: ['t'],
|
|
45
|
-
description: '
|
|
44
|
+
description: 'Run E2E tests in an isolated, parallel test stack (auto teardown)',
|
|
46
45
|
hidden: false,
|
|
47
46
|
name: 'test',
|
|
48
47
|
run: (toolbox) => __awaiter(void 0, void 0, void 0, function* () {
|
|
49
48
|
var _a;
|
|
50
|
-
const { filesystem, parameters, print: { colors, error, info, success }, } = toolbox;
|
|
49
|
+
const { filesystem, parameters, print: { colors, error, info, success, warning }, } = toolbox;
|
|
51
50
|
const layout = (0, dev_project_1.resolveLayout)(filesystem.cwd(), filesystem);
|
|
52
51
|
if (!layout.apiDir && !layout.appDir) {
|
|
53
52
|
error('No API or App project detected at this path.');
|
|
@@ -55,74 +54,201 @@ const TestCommand = {
|
|
|
55
54
|
process.exit(1);
|
|
56
55
|
return 'dev test: not a project';
|
|
57
56
|
}
|
|
57
|
+
// Ticket-aware: a ticket worktree tests its OWN isolated stack + DB
|
|
58
|
+
// (`<slug>-<id>-test`), so resolve the ticket identity + pass the ticket dev
|
|
59
|
+
// DB so the test DB is derived per ticket (never shared between tickets).
|
|
60
|
+
const { dbName: devDbName, identity } = (0, dev_ticket_1.resolveDevIdentity)(layout, { ticket: parameters.options.ticket });
|
|
61
|
+
const log = { dim: colors.dim, info, warn: warning };
|
|
62
|
+
// Sub-command: `lt dev test down` — tear the test stack(s) down + exit.
|
|
63
|
+
// Reclaims both the unsharded stack and any leftover sharded stacks.
|
|
64
|
+
if (parameters.first === 'down') {
|
|
65
|
+
const { stopped } = yield (0, dev_test_session_1.tearDownAllTestSessions)(layout, identity, log);
|
|
66
|
+
if (stopped.length > 0)
|
|
67
|
+
success(`Test stack down: ${stopped.join(', ')}`);
|
|
68
|
+
else
|
|
69
|
+
info(colors.dim('No test stack was running.'));
|
|
70
|
+
if (!parameters.options.fromGluegunMenu)
|
|
71
|
+
process.exit();
|
|
72
|
+
return 'dev test: down';
|
|
73
|
+
}
|
|
58
74
|
const apiMode = Boolean(parameters.options.api);
|
|
59
|
-
const
|
|
75
|
+
const keep = Boolean(parameters.options.keep) || parameters.options.teardown === false;
|
|
60
76
|
const debug = Boolean(parameters.options.debug);
|
|
61
|
-
// Forwarded args after `--` (gluegun puts them in parameters.array).
|
|
62
77
|
const forwarded = parameters.array || [];
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
78
|
+
const pnpmBin = process.env.LT_PNPM_BIN || 'pnpm';
|
|
79
|
+
// `--shard N` → run the suite split across N fully-isolated stacks in
|
|
80
|
+
// parallel. A bare `--shard` defaults to 2 — the stable sweet spot for a
|
|
81
|
+
// heavy built-SSR suite (N>=3 over-subscribes the perf cores → flaky; see
|
|
82
|
+
// autoShardCount). `--shard auto` instead sizes N from this machine's CPU+RAM.
|
|
83
|
+
const SHARD_DEFAULT = 2;
|
|
84
|
+
const shardRaw = (_a = parameters.options.shard) !== null && _a !== void 0 ? _a : parameters.options.shards;
|
|
85
|
+
let shardTotal;
|
|
86
|
+
if (shardRaw !== undefined) {
|
|
87
|
+
if (shardRaw === true)
|
|
88
|
+
shardTotal = SHARD_DEFAULT;
|
|
89
|
+
else if (String(shardRaw).toLowerCase() === 'auto')
|
|
90
|
+
shardTotal = (0, dev_test_session_1.autoShardCount)();
|
|
91
|
+
else
|
|
92
|
+
shardTotal = Math.floor(Number(shardRaw));
|
|
69
93
|
}
|
|
70
|
-
|
|
71
|
-
//
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
94
|
+
// -------------------------------------------------------------------------
|
|
95
|
+
// API mode — standalone, already isolated on its own DB. No stack needed.
|
|
96
|
+
// -------------------------------------------------------------------------
|
|
97
|
+
if (apiMode) {
|
|
98
|
+
if (!layout.apiDir) {
|
|
99
|
+
error('No API project in this layout.');
|
|
75
100
|
if (!parameters.options.fromGluegunMenu)
|
|
76
101
|
process.exit(1);
|
|
77
|
-
return 'dev test:
|
|
102
|
+
return 'dev test: no api';
|
|
78
103
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
104
|
+
info(colors.bold(`Running API tests for "${identity.slug}" (isolated DB)`));
|
|
105
|
+
const code = yield (0, dev_process_1.runChildInherit)(pnpmBin, ['run', 'test:e2e', ...forwarded], {
|
|
106
|
+
cwd: layout.apiDir,
|
|
107
|
+
env: process.env,
|
|
108
|
+
});
|
|
109
|
+
if (code === 0)
|
|
110
|
+
success('API tests passed.');
|
|
111
|
+
else
|
|
112
|
+
error(`API tests failed (exit ${code}).`);
|
|
113
|
+
if (!parameters.options.fromGluegunMenu)
|
|
114
|
+
process.exit(code !== null && code !== void 0 ? code : 1);
|
|
115
|
+
return `dev test: api exit=${code}`;
|
|
116
|
+
}
|
|
117
|
+
// -------------------------------------------------------------------------
|
|
118
|
+
// App mode — isolated Playwright stack.
|
|
119
|
+
// -------------------------------------------------------------------------
|
|
120
|
+
if (!layout.appDir) {
|
|
121
|
+
error('No App project in this layout.');
|
|
122
|
+
if (!parameters.options.fromGluegunMenu)
|
|
123
|
+
process.exit(1);
|
|
124
|
+
return 'dev test: no app';
|
|
125
|
+
}
|
|
126
|
+
if (!(yield (0, caddy_1.caddyAvailable)())) {
|
|
127
|
+
error('caddy is not installed. Run `lt dev install` first.');
|
|
128
|
+
if (!parameters.options.fromGluegunMenu)
|
|
129
|
+
process.exit(1);
|
|
130
|
+
return 'dev test: caddy missing';
|
|
131
|
+
}
|
|
132
|
+
if (!(yield (0, caddy_1.caddyDaemonRunning)())) {
|
|
133
|
+
error('caddy daemon not running. Run `lt dev install` first.');
|
|
134
|
+
if (!parameters.options.fromGluegunMenu)
|
|
135
|
+
process.exit(1);
|
|
136
|
+
return 'dev test: caddy daemon down';
|
|
137
|
+
}
|
|
138
|
+
// Pre-flight (#3): if the project's playwright.config is not env-aware
|
|
139
|
+
// (hardcoded baseURL or an unguarded `webServer`), the suite would IGNORE
|
|
140
|
+
// the isolated stack we are about to build and hit localhost:3001 instead —
|
|
141
|
+
// wasting a full build on a stack nothing uses. Abort early with the fix,
|
|
142
|
+
// unless `--force`.
|
|
143
|
+
const unpatched = (0, dev_project_1.appNeedsPortPatch)(layout.appDir).some((f) => f.endsWith('playwright.config.ts'));
|
|
144
|
+
if (unpatched && !parameters.options.force) {
|
|
145
|
+
error("This project's playwright.config.ts is not env-aware (hardcoded baseURL or unguarded webServer).");
|
|
146
|
+
error('`lt dev test` would build an isolated stack the suite then IGNORES (tests would hit localhost:3001).');
|
|
147
|
+
info(colors.dim('Fix: run `lt dev init` to make baseURL env-aware + guard the webServer — or pass --force.'));
|
|
148
|
+
if (!parameters.options.fromGluegunMenu)
|
|
149
|
+
process.exit(1);
|
|
150
|
+
return 'dev test: playwright.config not env-aware (run lt dev init or --force)';
|
|
151
|
+
}
|
|
152
|
+
// -------------------------------------------------------------------------
|
|
153
|
+
// Sharded mode (`--shard N`) — N fully-isolated stacks + parallel
|
|
154
|
+
// `--shard=i/N`, the local CI-parity matrix. Each shard has its own DB so
|
|
155
|
+
// there is no cross-shard contention. Auto-teardown of ALL shards (or
|
|
156
|
+
// `--keep` them for `lt dev test down`).
|
|
157
|
+
// -------------------------------------------------------------------------
|
|
158
|
+
if (shardTotal !== undefined && shardTotal > 1) {
|
|
159
|
+
let allTornDown = false;
|
|
160
|
+
const teardownAll = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
161
|
+
if (allTornDown || keep)
|
|
162
|
+
return;
|
|
163
|
+
allTornDown = true;
|
|
164
|
+
yield (0, dev_test_session_1.tearDownAllTestSessions)(layout, identity, log, { silent: true });
|
|
165
|
+
});
|
|
166
|
+
const onShardSignal = () => {
|
|
167
|
+
teardownAll()
|
|
168
|
+
.catch(() => undefined)
|
|
169
|
+
.finally(() => process.exit(130));
|
|
170
|
+
};
|
|
171
|
+
process.on('SIGINT', onShardSignal);
|
|
172
|
+
process.on('SIGTERM', onShardSignal);
|
|
173
|
+
let shardExit = 1;
|
|
174
|
+
try {
|
|
175
|
+
info('');
|
|
176
|
+
info(colors.bold(`Running isolated Playwright E2E for "${identity.slug}" sharded across ${shardTotal} stacks`));
|
|
177
|
+
shardExit = yield (0, dev_test_session_1.runShardedTestSession)(layout, identity, log, {
|
|
178
|
+
devDbName,
|
|
179
|
+
forwarded,
|
|
180
|
+
pnpmBin,
|
|
181
|
+
total: shardTotal,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
catch (e) {
|
|
185
|
+
error(`Failed to run sharded E2E: ${e.message}`);
|
|
186
|
+
shardExit = 1;
|
|
84
187
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
info(colors.dim('Project not running — invoking `lt dev up` first ...'));
|
|
92
|
-
const upResult = yield runChild('lt', ['dev', 'up'], { cwd: layout.root, env: process.env, inherit: true });
|
|
93
|
-
if (upResult !== 0) {
|
|
94
|
-
error(`lt dev up failed (exit ${upResult}).`);
|
|
95
|
-
if (!parameters.options.fromGluegunMenu)
|
|
96
|
-
process.exit(upResult !== null && upResult !== void 0 ? upResult : 1);
|
|
97
|
-
return 'dev test: up failed';
|
|
188
|
+
finally {
|
|
189
|
+
process.off('SIGINT', onShardSignal);
|
|
190
|
+
process.off('SIGTERM', onShardSignal);
|
|
191
|
+
if (keep) {
|
|
192
|
+
info('');
|
|
193
|
+
info(colors.dim('Test stacks left running (--keep). Stop them with: `lt dev test down`.'));
|
|
98
194
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
if (appHost) {
|
|
102
|
-
info(colors.dim(`Waiting for https://${appHost} to respond ...`));
|
|
103
|
-
yield waitForUrl(`https://${appHost}`, 30000);
|
|
195
|
+
else {
|
|
196
|
+
yield teardownAll();
|
|
104
197
|
}
|
|
105
198
|
}
|
|
199
|
+
if (shardExit === 0)
|
|
200
|
+
success('Tests passed (all shards).');
|
|
201
|
+
else
|
|
202
|
+
error(`Tests failed (exit ${shardExit}).`);
|
|
203
|
+
if (!parameters.options.fromGluegunMenu)
|
|
204
|
+
process.exit(shardExit !== null && shardExit !== void 0 ? shardExit : 1);
|
|
205
|
+
return `dev test: sharded exit=${shardExit}`;
|
|
106
206
|
}
|
|
107
|
-
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
207
|
+
let tornDown = false;
|
|
208
|
+
const teardown = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
209
|
+
if (tornDown || keep)
|
|
210
|
+
return;
|
|
211
|
+
tornDown = true;
|
|
212
|
+
yield (0, dev_test_session_1.tearDownTestSession)(layout, identity, log);
|
|
213
|
+
});
|
|
214
|
+
// Residue-free teardown even on Ctrl-C / kill.
|
|
215
|
+
const onSignal = () => {
|
|
216
|
+
teardown()
|
|
217
|
+
.catch(() => undefined)
|
|
218
|
+
.finally(() => process.exit(130));
|
|
219
|
+
};
|
|
220
|
+
process.on('SIGINT', onSignal);
|
|
221
|
+
process.on('SIGTERM', onSignal);
|
|
222
|
+
let exitCode = 1;
|
|
223
|
+
try {
|
|
224
|
+
const ctx = yield (0, dev_test_session_1.bringUpTestSession)(layout, identity, log, { devDbName });
|
|
225
|
+
const env = Object.assign(Object.assign(Object.assign({}, process.env), readBridgeEnv(layout.root)), {
|
|
226
|
+
// Playwright global-setup resets THIS db (allow-listed) before the suite.
|
|
227
|
+
MONGO_URI: `mongodb://127.0.0.1/${ctx.dbName}` });
|
|
228
|
+
if (debug) {
|
|
229
|
+
env.PWDEBUG = '1';
|
|
230
|
+
env.HEADED = '1';
|
|
231
|
+
}
|
|
232
|
+
info('');
|
|
233
|
+
info(colors.bold(`Running isolated Playwright E2E for "${identity.slug}"`));
|
|
234
|
+
info(colors.dim(` app: ${ctx.appUrl} db: ${ctx.dbName}`));
|
|
123
235
|
info('');
|
|
124
|
-
|
|
125
|
-
|
|
236
|
+
exitCode = yield (0, dev_process_1.runChildInherit)(pnpmBin, ['run', 'test:e2e', ...forwarded], { cwd: layout.appDir, env });
|
|
237
|
+
}
|
|
238
|
+
catch (e) {
|
|
239
|
+
error(`Failed to run isolated E2E: ${e.message}`);
|
|
240
|
+
exitCode = 1;
|
|
241
|
+
}
|
|
242
|
+
finally {
|
|
243
|
+
process.off('SIGINT', onSignal);
|
|
244
|
+
process.off('SIGTERM', onSignal);
|
|
245
|
+
if (keep) {
|
|
246
|
+
info('');
|
|
247
|
+
info(colors.dim('Test stack left running (--keep). Stop it with: `lt dev test down`.'));
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
yield teardown();
|
|
251
|
+
}
|
|
126
252
|
}
|
|
127
253
|
if (exitCode === 0)
|
|
128
254
|
success('Tests passed.');
|
|
@@ -134,11 +260,11 @@ const TestCommand = {
|
|
|
134
260
|
}),
|
|
135
261
|
};
|
|
136
262
|
/**
|
|
137
|
-
* Read the
|
|
138
|
-
* Returns an empty object if
|
|
263
|
+
* Read the test ENV bridge (`.lt-dev/.env.test`) as a key/value map.
|
|
264
|
+
* Returns an empty object if missing.
|
|
139
265
|
*/
|
|
140
266
|
function readBridgeEnv(root) {
|
|
141
|
-
const file = (0, dev_env_bridge_1.envBridgePath)(root);
|
|
267
|
+
const file = (0, dev_env_bridge_1.envBridgePath)(root, '.env.test');
|
|
142
268
|
if (!(0, fs_1.existsSync)(file))
|
|
143
269
|
return {};
|
|
144
270
|
const out = {};
|
|
@@ -149,43 +275,4 @@ function readBridgeEnv(root) {
|
|
|
149
275
|
}
|
|
150
276
|
return out;
|
|
151
277
|
}
|
|
152
|
-
/** Spawn a child synchronously (waits for exit), inheriting stdio when requested. */
|
|
153
|
-
function runChild(cmd, args, opts) {
|
|
154
|
-
return new Promise((resolve) => {
|
|
155
|
-
const child = (0, child_process_1.spawn)(cmd, args, {
|
|
156
|
-
cwd: opts.cwd,
|
|
157
|
-
env: opts.env,
|
|
158
|
-
stdio: opts.inherit ? 'inherit' : 'pipe',
|
|
159
|
-
});
|
|
160
|
-
child.on('error', () => resolve(1));
|
|
161
|
-
child.on('close', (code) => resolve(code));
|
|
162
|
-
});
|
|
163
|
-
}
|
|
164
|
-
/** Poll a URL until it responds or timeout elapses. */
|
|
165
|
-
function waitForUrl(url, timeoutMs) {
|
|
166
|
-
const start = Date.now();
|
|
167
|
-
return new Promise((resolve) => {
|
|
168
|
-
const tick = () => {
|
|
169
|
-
var _a;
|
|
170
|
-
const child = (0, child_process_1.spawn)('curl', ['-sk', '-o', '/dev/null', '-w', '%{http_code}', '--max-time', '2', url], {
|
|
171
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
172
|
-
});
|
|
173
|
-
let status = '';
|
|
174
|
-
(_a = child.stdout) === null || _a === void 0 ? void 0 : _a.on('data', (b) => (status += String(b)));
|
|
175
|
-
child.on('close', () => {
|
|
176
|
-
if (/^[1-5]\d\d$/.test(status.trim()))
|
|
177
|
-
return resolve(true);
|
|
178
|
-
if (Date.now() - start > timeoutMs)
|
|
179
|
-
return resolve(false);
|
|
180
|
-
setTimeout(tick, 500);
|
|
181
|
-
});
|
|
182
|
-
child.on('error', () => {
|
|
183
|
-
if (Date.now() - start > timeoutMs)
|
|
184
|
-
return resolve(false);
|
|
185
|
-
setTimeout(tick, 500);
|
|
186
|
-
});
|
|
187
|
-
};
|
|
188
|
-
tick();
|
|
189
|
-
});
|
|
190
|
-
}
|
|
191
278
|
module.exports = TestCommand;
|