@solidxai/core 0.1.13-beta.4 → 0.1.13-beta.6
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/settings.local.json +16 -0
- package/CHANGELOG.md +1053 -0
- package/CLAUDE.md +26 -0
- package/dist/commands/run-tests.command.d.ts +7 -1
- package/dist/commands/run-tests.command.d.ts.map +1 -1
- package/dist/commands/run-tests.command.js +49 -3
- package/dist/commands/run-tests.command.js.map +1 -1
- package/dist/constants/chatter-message.constants.d.ts +1 -0
- package/dist/constants/chatter-message.constants.d.ts.map +1 -1
- package/dist/constants/chatter-message.constants.js +3 -1
- package/dist/constants/chatter-message.constants.js.map +1 -1
- package/dist/constants/user.constants.d.ts +2 -0
- package/dist/constants/user.constants.d.ts.map +1 -0
- package/dist/constants/user.constants.js +5 -0
- package/dist/constants/user.constants.js.map +1 -0
- package/dist/controllers/chatter-message.controller.d.ts +5 -0
- package/dist/controllers/chatter-message.controller.d.ts.map +1 -1
- package/dist/controllers/chatter-message.controller.js +14 -0
- package/dist/controllers/chatter-message.controller.js.map +1 -1
- package/dist/entities/user.entity.d.ts.map +1 -1
- package/dist/entities/user.entity.js +36 -0
- package/dist/entities/user.entity.js.map +1 -1
- package/dist/seeders/seed-data/solid-core-metadata.json +12 -7
- package/dist/services/chatter-message.service.d.ts +10 -0
- package/dist/services/chatter-message.service.d.ts.map +1 -1
- package/dist/services/chatter-message.service.js +44 -3
- package/dist/services/chatter-message.service.js.map +1 -1
- package/dist/services/crud.service.d.ts +2 -0
- package/dist/services/crud.service.d.ts.map +1 -1
- package/dist/services/crud.service.js +13 -1
- package/dist/services/crud.service.js.map +1 -1
- package/dist/services/settings/default-settings-provider.service.d.ts +40 -0
- package/dist/services/settings/default-settings-provider.service.d.ts.map +1 -1
- package/dist/services/settings/default-settings-provider.service.js +22 -0
- package/dist/services/settings/default-settings-provider.service.js.map +1 -1
- package/dist/testing/adapters/ui/playwright-adapter.d.ts +5 -0
- package/dist/testing/adapters/ui/playwright-adapter.d.ts.map +1 -1
- package/dist/testing/adapters/ui/playwright-adapter.js +17 -1
- package/dist/testing/adapters/ui/playwright-adapter.js.map +1 -1
- package/dist/testing/adapters/ui/ui.types.d.ts +2 -0
- package/dist/testing/adapters/ui/ui.types.d.ts.map +1 -1
- package/dist/testing/adapters/ui/ui.types.js.map +1 -1
- package/dist/testing/steps/ui/actions.step.d.ts.map +1 -1
- package/dist/testing/steps/ui/actions.step.js +6 -2
- package/dist/testing/steps/ui/actions.step.js.map +1 -1
- package/dist/testing/steps/ui/assertions.step.d.ts.map +1 -1
- package/dist/testing/steps/ui/assertions.step.js +11 -3
- package/dist/testing/steps/ui/assertions.step.js.map +1 -1
- package/dist/testing/steps/ui/form.step.d.ts.map +1 -1
- package/dist/testing/steps/ui/form.step.js +9 -3
- package/dist/testing/steps/ui/form.step.js.map +1 -1
- package/dist/testing/steps/ui/manual.step.js +1 -1
- package/dist/testing/steps/ui/manual.step.js.map +1 -1
- package/dist/testing/steps/ui/navigation.step.d.ts.map +1 -1
- package/dist/testing/steps/ui/navigation.step.js +3 -1
- package/dist/testing/steps/ui/navigation.step.js.map +1 -1
- package/package.json +1 -1
- package/src/commands/run-tests.command.ts +47 -1
- package/src/constants/chatter-message.constants.ts +12 -0
- package/src/constants/user.constants.ts +13 -0
- package/src/controllers/chatter-message.controller.ts +8 -0
- package/src/entities/user.entity.ts +53 -36
- package/src/seeders/seed-data/solid-core-metadata.json +12 -7
- package/src/services/1.js +6 -0
- package/src/services/chatter-message.service.ts +86 -4
- package/src/services/crud.service.ts +31 -1
- package/src/services/settings/default-settings-provider.service.ts +22 -0
- package/src/testing/README.md +64 -2
- package/src/testing/adapters/ui/playwright-adapter.ts +25 -0
- package/src/testing/adapters/ui/ui.types.ts +2 -0
- package/src/testing/steps/ui/actions.step.ts +8 -4
- package/src/testing/steps/ui/assertions.step.ts +18 -5
- package/src/testing/steps/ui/form.step.ts +16 -6
- package/src/testing/steps/ui/manual.step.ts +1 -1
- package/src/testing/steps/ui/navigation.step.ts +4 -2
- package/dist-tests/api/authenticate.spec.js +0 -119
- package/dist-tests/api/authenticate.spec.js.map +0 -1
- package/dist-tests/api/crud-service.findOne.cityMaster.spec.js +0 -97
- package/dist-tests/api/crud-service.findOne.cityMaster.spec.js.map +0 -1
- package/dist-tests/api/ping.spec.js +0 -21
- package/dist-tests/api/ping.spec.js.map +0 -1
- package/dist-tests/helpers/auth.js +0 -41
- package/dist-tests/helpers/auth.js.map +0 -1
- package/dist-tests/helpers/env.js +0 -11
- package/dist-tests/helpers/env.js.map +0 -1
- package/docs/agent-builder/notebook-lm-prompt-for-agent-builder.md +0 -136
- package/docs/agent-builder/team-ready-note-agent-builder.md +0 -305
- package/docs/agent-hub-grooming.md +0 -301
- package/docs/dashboards/AGENTIC_DASHBOARD_IMPLEMENTATION_PLAN.md +0 -438
- package/docs/dashboards/dashboard-curl-smoke-tests.txt +0 -146
- package/docs/dashboards/delete-legacy-dashboard-metadata.sql +0 -172
- package/docs/datasource-introspection-ddl-analysis.md +0 -326
- package/docs/datasource-introspection-implementation-plan.md +0 -306
- package/docs/grouping-enhancements.md +0 -89
- package/docs/java-spring/README.md +0 -3
- package/docs/java-spring/solid-core-module-deep-dive-report.md +0 -1317
- package/docs/module-package-import-handoff.md +0 -691
- package/docs/seed-changes.md +0 -65
- package/docs/test-data-workflow.md +0 -200
- package/docs/type-declaration-import-issue.md +0 -24
- package/docs/workflow/business-automation-example-notes.md +0 -309
- package/docs/workflow/control-flow-node-addition-sop.md +0 -324
- package/docs/workflow/data-engineering-pipeline-example-notes.md +0 -330
- package/docs/workflow/foreach-example-notes.md +0 -187
- package/docs/workflow/hello-world-example-notes.md +0 -271
- package/docs/workflow/kestra-concepts-plugins-blueprints.md +0 -315
- package/docs/workflow/loop-until-example-notes.md +0 -198
- package/docs/workflow/microservices-and-apis-example-notes.md +0 -264
- package/docs/workflow/samples.md +0 -25
- package/docs/workflow/what-is-kestra.md +0 -79
- package/docs/workflow/workflow-core-module-handoff-summary.md +0 -191
- package/docs/workflow/workflow-module-brd.md +0 -185
- package/docs/workflow/workflow-module-domain-model.md +0 -252
- package/docs/workflow/workflow-module-metadata-dsl.md +0 -302
- package/docs/workflow/workflow-module-technical-spec.md +0 -293
- package/docs/workflow/workflow-node-type-contracts.md +0 -635
- package/docs/workflow/workflow-node-ui-metadata-contract.md +0 -1262
- package/docs/workflow/workflow-solidx-model-field-spec.md +0 -284
|
@@ -6141,7 +6141,7 @@
|
|
|
6141
6141
|
"type": "int",
|
|
6142
6142
|
"ormType": "integer",
|
|
6143
6143
|
"isSystem": false,
|
|
6144
|
-
"defaultValue": 1,
|
|
6144
|
+
"defaultValue": "1",
|
|
6145
6145
|
"min": null,
|
|
6146
6146
|
"max": null,
|
|
6147
6147
|
"required": true,
|
|
@@ -6162,7 +6162,7 @@
|
|
|
6162
6162
|
"type": "int",
|
|
6163
6163
|
"ormType": "integer",
|
|
6164
6164
|
"isSystem": false,
|
|
6165
|
-
"defaultValue": 0,
|
|
6165
|
+
"defaultValue": "0",
|
|
6166
6166
|
"min": null,
|
|
6167
6167
|
"max": null,
|
|
6168
6168
|
"required": false,
|
|
@@ -7367,7 +7367,7 @@
|
|
|
7367
7367
|
"type": "boolean",
|
|
7368
7368
|
"ormType": "boolean",
|
|
7369
7369
|
"isSystem": false,
|
|
7370
|
-
"defaultValue": false,
|
|
7370
|
+
"defaultValue": "false",
|
|
7371
7371
|
"required": false,
|
|
7372
7372
|
"unique": false,
|
|
7373
7373
|
"index": true,
|
|
@@ -7868,6 +7868,7 @@
|
|
|
7868
7868
|
"ChatterMessageController.findMany",
|
|
7869
7869
|
"ChatterMessageController.markCompleted",
|
|
7870
7870
|
"ChatterMessageController.updateCustomNoteMessage",
|
|
7871
|
+
"ChatterMessageController.getMentionableUsers",
|
|
7871
7872
|
"ListOfValuesController.findMany",
|
|
7872
7873
|
"ListOfValuesController.findOne",
|
|
7873
7874
|
"SettingController.getAllSettings",
|
|
@@ -10433,7 +10434,8 @@
|
|
|
10433
10434
|
"attrs": {
|
|
10434
10435
|
"name": "user",
|
|
10435
10436
|
"isSearchable": true,
|
|
10436
|
-
"searchField": "user.fullName"
|
|
10437
|
+
"searchField": "user.fullName",
|
|
10438
|
+
"coModelFieldToDisplay": "fullName"
|
|
10437
10439
|
}
|
|
10438
10440
|
}
|
|
10439
10441
|
]
|
|
@@ -10517,7 +10519,8 @@
|
|
|
10517
10519
|
{
|
|
10518
10520
|
"type": "field",
|
|
10519
10521
|
"attrs": {
|
|
10520
|
-
"name": "user"
|
|
10522
|
+
"name": "user",
|
|
10523
|
+
"coModelFieldToDisplay": "fullName"
|
|
10521
10524
|
}
|
|
10522
10525
|
},
|
|
10523
10526
|
{
|
|
@@ -14855,7 +14858,8 @@
|
|
|
14855
14858
|
"name": "user",
|
|
14856
14859
|
"label": "User",
|
|
14857
14860
|
"isSearchable": true,
|
|
14858
|
-
"searchField": "user.fullName"
|
|
14861
|
+
"searchField": "user.fullName",
|
|
14862
|
+
"coModelFieldToDisplay": "fullName"
|
|
14859
14863
|
}
|
|
14860
14864
|
},
|
|
14861
14865
|
{
|
|
@@ -17077,7 +17081,8 @@
|
|
|
17077
17081
|
"name": "user",
|
|
17078
17082
|
"label": "User",
|
|
17079
17083
|
"isSearchable": true,
|
|
17080
|
-
"searchField": "user.fullName"
|
|
17084
|
+
"searchField": "user.fullName",
|
|
17085
|
+
"coModelFieldToDisplay": "fullName"
|
|
17081
17086
|
}
|
|
17082
17087
|
}
|
|
17083
17088
|
]
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
1. Do i need to create a storeStreams method for aws service too?
|
|
2
|
+
- Handle later
|
|
3
|
+
2. queues handling -> if queues is enabled by default, i.e triggerExport(exportTransactionEntity.id).
|
|
4
|
+
- startExport should either return the data or return the transaction id
|
|
5
|
+
3. How to handle scenarios wherein, nested related exist.(do i need to only get the userkey)
|
|
6
|
+
- show the userKey
|
|
@@ -5,7 +5,7 @@ import { InjectEntityManager } from '@nestjs/typeorm';
|
|
|
5
5
|
import { Brackets, EntityManager, EntityMetadata, In } from 'typeorm';
|
|
6
6
|
|
|
7
7
|
import { classify } from '@angular-devkit/core/src/utils/strings';
|
|
8
|
-
import { CHATTER_MESSAGE_STATUS, CHATTER_MESSAGE_SUBTYPE, CHATTER_MESSAGE_TYPE } from 'src/constants/chatter-message.constants';
|
|
8
|
+
import { CHATTER_MESSAGE_STATUS, CHATTER_MESSAGE_SUBTYPE, CHATTER_MESSAGE_TYPE, CHATTER_MESSAGE_USER_FIELDS } from 'src/constants/chatter-message.constants';
|
|
9
9
|
import { ERROR_MESSAGES } from 'src/constants/error-messages';
|
|
10
10
|
import { PostChatterMessageDto } from 'src/dtos/post-chatter-message.dto';
|
|
11
11
|
import { UpdateChatterNoteMessageDto } from 'src/dtos/update-chatter-note-message.dto';
|
|
@@ -18,9 +18,11 @@ import { FieldMetadataRepository } from 'src/repository/field-metadata.repositor
|
|
|
18
18
|
import { MediaRepository } from 'src/repository/media.repository';
|
|
19
19
|
import { ModelMetadataRepository } from 'src/repository/model-metadata.repository';
|
|
20
20
|
import { CRUDService } from 'src/services/crud.service';
|
|
21
|
+
import { BasicFilterDto } from '../dtos/basic-filters.dto';
|
|
21
22
|
import { MediaStorageProviderType } from '../dtos/create-media-storage-provider-metadata.dto';
|
|
22
23
|
import { ChatterMessageDetails } from '../entities/chatter-message-details.entity';
|
|
23
24
|
import { ChatterMessage } from '../entities/chatter-message.entity';
|
|
25
|
+
import { User } from '../entities/user.entity';
|
|
24
26
|
import { getMediaStorageProvider } from './mediaStorageProviders';
|
|
25
27
|
import { RequestContextService } from './request-context.service';
|
|
26
28
|
import { Logger } from '@nestjs/common';
|
|
@@ -101,6 +103,44 @@ export class ChatterMessageService extends CRUDService<ChatterMessage> {
|
|
|
101
103
|
chatterMessage.updatedBy = resolvedUserId;
|
|
102
104
|
}
|
|
103
105
|
|
|
106
|
+
/**
|
|
107
|
+
* Reduce a message's hydrated `user` relation to CHATTER_MESSAGE_USER_FIELDS.
|
|
108
|
+
*
|
|
109
|
+
* `getChatterMessages` restricts the columns in the query itself, which is preferable.
|
|
110
|
+
* The generic CRUD paths below build their query from find-options and hand `populate`
|
|
111
|
+
* straight to TypeORM, so they are trimmed after the fact instead. (`createdBy` /
|
|
112
|
+
* `updatedBy` need nothing here - CRUDService.handlePopulateUserIdFields already selects
|
|
113
|
+
* only USER_SUMMARY_FIELDS when a caller populates them.)
|
|
114
|
+
*/
|
|
115
|
+
private trimMessageUser<M extends ChatterMessage>(message: M): M {
|
|
116
|
+
if (message?.user) {
|
|
117
|
+
message.user = this.toUserSummary(message.user) as User;
|
|
118
|
+
}
|
|
119
|
+
return message;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private trimMessageUsers(messages: ChatterMessage[] | undefined) {
|
|
123
|
+
messages?.forEach(message => this.trimMessageUser(message));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* `GET /chatter-message?populate[]=user` would otherwise be a way around the column
|
|
128
|
+
* allowlist applied by getChatterMessages.
|
|
129
|
+
*/
|
|
130
|
+
async find(basicFilterDto: BasicFilterDto, solidRequestContext: any = {}): Promise<any> {
|
|
131
|
+
const result = await super.find(basicFilterDto, solidRequestContext);
|
|
132
|
+
this.trimMessageUsers(result?.records);
|
|
133
|
+
// A grouped find (populateGroup) nests its entities one level deeper.
|
|
134
|
+
for (const groupRecord of result?.groupRecords ?? []) {
|
|
135
|
+
this.trimMessageUsers(groupRecord?.groupData?.records);
|
|
136
|
+
}
|
|
137
|
+
return result;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async findOne(id: number, query: any = {}, solidRequestContext: any = {}) {
|
|
141
|
+
return this.trimMessageUser(await super.findOne(id, query, solidRequestContext));
|
|
142
|
+
}
|
|
143
|
+
|
|
104
144
|
private isEditableCustomNoteMessage(message: ChatterMessage): boolean {
|
|
105
145
|
if (message.messageType !== CHATTER_MESSAGE_TYPE.CUSTOM) {
|
|
106
146
|
return false;
|
|
@@ -188,6 +228,31 @@ export class ChatterMessageService extends CRUDService<ChatterMessage> {
|
|
|
188
228
|
}
|
|
189
229
|
}
|
|
190
230
|
|
|
231
|
+
// Deliberately bypasses UserRepository's security-rule filtering: the mention picker
|
|
232
|
+
// needs id/username/fullName for any active user regardless of the caller's row-level
|
|
233
|
+
// access to the User model, same as the getChatterMessages one-to-many lookup below.
|
|
234
|
+
async getMentionableUsers(search?: string, limit: number = 8): Promise<Array<{ id: number; username: string; fullName: string }>> {
|
|
235
|
+
const normalizedLimit = Number.isInteger(limit) && limit > 0 && limit <= 50 ? limit : 8;
|
|
236
|
+
const userRepository = this.entityManager.getRepository(User);
|
|
237
|
+
|
|
238
|
+
const qb = userRepository
|
|
239
|
+
.createQueryBuilder('user')
|
|
240
|
+
.select(['user.id', 'user.username', 'user.fullName'])
|
|
241
|
+
.where('user.active = :active', { active: true });
|
|
242
|
+
|
|
243
|
+
const trimmedSearch = (search ?? '').trim();
|
|
244
|
+
if (trimmedSearch) {
|
|
245
|
+
qb.andWhere('(LOWER(user.username) LIKE :search OR LOWER(user.fullName) LIKE :search)', {
|
|
246
|
+
search: `%${trimmedSearch.toLowerCase()}%`,
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
qb.orderBy('user.username', 'ASC').take(normalizedLimit);
|
|
251
|
+
|
|
252
|
+
const users = await qb.getMany();
|
|
253
|
+
return users.map(user => ({ id: user.id, username: user.username, fullName: user.fullName }));
|
|
254
|
+
}
|
|
255
|
+
|
|
191
256
|
async markCompleted(id: number) {
|
|
192
257
|
const activeUser = this.requestContextService.getActiveUser();
|
|
193
258
|
if (!activeUser) {
|
|
@@ -306,7 +371,8 @@ export class ChatterMessageService extends CRUDService<ChatterMessage> {
|
|
|
306
371
|
|
|
307
372
|
await this.publishChatterMentionNotifications(savedMessage, model);
|
|
308
373
|
|
|
309
|
-
return
|
|
374
|
+
// The `user` relation was loaded for the ownership check above; don't return all of it.
|
|
375
|
+
return this.trimMessageUser(savedMessage);
|
|
310
376
|
}
|
|
311
377
|
|
|
312
378
|
async postMessage(postDto: PostChatterMessageDto, files: Express.Multer.File[] = []) {
|
|
@@ -891,16 +957,25 @@ export class ChatterMessageService extends CRUDService<ChatterMessage> {
|
|
|
891
957
|
qb.where(orConditions.join(' OR '), parameters);
|
|
892
958
|
}));
|
|
893
959
|
|
|
894
|
-
const relations = ['chatterMessageDetails'
|
|
960
|
+
const relations = ['chatterMessageDetails'];
|
|
895
961
|
if (populate && populate.length > 0) {
|
|
896
962
|
const normalizedPopulate = this.crudHelperService.normalize(populate);
|
|
897
|
-
|
|
963
|
+
// SECURITY: 'user' is joined below with an explicit column allowlist. A client-supplied
|
|
964
|
+
// populate[]=user must not be able to turn it back into a full-entity join (the chatter
|
|
965
|
+
// panel does send exactly that).
|
|
966
|
+
relations.push(...normalizedPopulate.filter(rel => rel !== 'user' && !relations.includes(rel)));
|
|
898
967
|
}
|
|
899
968
|
|
|
900
969
|
relations.forEach(relation => {
|
|
901
970
|
qb.leftJoinAndSelect(`entity.${relation}`, relation);
|
|
902
971
|
});
|
|
903
972
|
|
|
973
|
+
// Only the author's id and display name leave this endpoint - see CHATTER_MESSAGE_USER_FIELDS.
|
|
974
|
+
// This join must stay before applyFilters below: it lets filters[user][fullName] reuse this
|
|
975
|
+
// alias instead of adding a second join of its own.
|
|
976
|
+
qb.leftJoin('entity.user', 'user');
|
|
977
|
+
qb.addSelect(CHATTER_MESSAGE_USER_FIELDS.map(field => `user.${field}`));
|
|
978
|
+
|
|
904
979
|
if (filters) {
|
|
905
980
|
qb.andWhere(new Brackets(whereQb => {
|
|
906
981
|
this.crudHelperService.applyFilters(whereQb, filters, 'entity', qb);
|
|
@@ -914,6 +989,13 @@ export class ChatterMessageService extends CRUDService<ChatterMessage> {
|
|
|
914
989
|
const [entities, count] = await qb.getManyAndCount();
|
|
915
990
|
this.logHeapUsed('getChatterMessages-entitiesLoaded');
|
|
916
991
|
|
|
992
|
+
// The join above only selects the allowlisted columns, but TypeORM still hydrates the
|
|
993
|
+
// author with `new User()`, which leaves every initialiser-backed property (active,
|
|
994
|
+
// forcePasswordChange, ...) sitting on the instance holding its default rather than a
|
|
995
|
+
// value read from the database. Replace it with a plain object of exactly the
|
|
996
|
+
// allowlisted keys so the response cannot report fabricated defaults as data.
|
|
997
|
+
this.trimMessageUsers(entities);
|
|
998
|
+
|
|
917
999
|
// Convert date strings in message details to ISO format for consistent handling on the frontend
|
|
918
1000
|
const DATE_FIELD_TYPES = ['date', 'datetime', 'time'];
|
|
919
1001
|
for (const entity of entities) {
|
|
@@ -7,6 +7,7 @@ import { SolidBaseRepository } from "../repository/solid-base.repository";
|
|
|
7
7
|
import { SettingService } from "./setting.service";
|
|
8
8
|
import { ERROR_MESSAGES } from "src/constants/error-messages";
|
|
9
9
|
import { SUCCESS_MESSAGES } from "src/constants/success-messages";
|
|
10
|
+
import { USER_SUMMARY_FIELDS } from "src/constants/user.constants";
|
|
10
11
|
import { EntityManager, FindOptionsWhere, In, IsNull, Not, QueryFailedError, SelectQueryBuilder } from "typeorm";
|
|
11
12
|
import { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity";
|
|
12
13
|
import { BasicFilterDto } from "../dtos/basic-filters.dto";
|
|
@@ -573,6 +574,29 @@ private async prepareManyToManyAuditSnapshot(entity: T,id: number,modelSingularN
|
|
|
573
574
|
return this.crudHelperService.pagedResponse(offset, limit, count, entities);
|
|
574
575
|
}
|
|
575
576
|
|
|
577
|
+
/**
|
|
578
|
+
* Reduce a user to USER_SUMMARY_FIELDS as a plain object.
|
|
579
|
+
*
|
|
580
|
+
* Returning the hydrated entity is not good enough. A partially selected `User` is still
|
|
581
|
+
* built with `new User()`, so every property that has a TypeScript field initializer
|
|
582
|
+
* (`active`, `forcePasswordChange`, `lastLoginProvider`, `failedLoginAttempts`,
|
|
583
|
+
* `isAllowedToGenerateApiKeys`) is present on the instance holding its *default* rather
|
|
584
|
+
* than the value in the database - `active: true` for a user who may well be inactive.
|
|
585
|
+
* Serialization does not help: those defaults are real own properties. Copying the
|
|
586
|
+
* allowlist into a fresh object is what guarantees the response contains only columns
|
|
587
|
+
* actually read from the database.
|
|
588
|
+
*/
|
|
589
|
+
protected toUserSummary(user: unknown): Partial<User> | null {
|
|
590
|
+
if (!user) {
|
|
591
|
+
return null;
|
|
592
|
+
}
|
|
593
|
+
const summary: Partial<User> = {};
|
|
594
|
+
for (const field of USER_SUMMARY_FIELDS) {
|
|
595
|
+
summary[field] = user[field] as never;
|
|
596
|
+
}
|
|
597
|
+
return summary;
|
|
598
|
+
}
|
|
599
|
+
|
|
576
600
|
// entities is an array of T
|
|
577
601
|
// T can contain createdBy and updatedBy fields
|
|
578
602
|
// We need to populate the createdBy and updatedBy fields with the User entity
|
|
@@ -584,9 +608,15 @@ private async prepareManyToManyAuditSnapshot(entity: T,id: number,modelSingularN
|
|
|
584
608
|
if (userId) {
|
|
585
609
|
const user = await userRepository.findOne({
|
|
586
610
|
where: { id: userId },
|
|
611
|
+
// SECURITY: a populated createdBy/updatedBy is only ever displayed as
|
|
612
|
+
// "who did this", so it must not ship the author's email, mobile, role
|
|
613
|
+
// assignments or API keys - let alone the credential columns, which
|
|
614
|
+
// survive here whenever User is subclassed (class-transformer resolves
|
|
615
|
+
// @Exclude() per class and does not inherit it).
|
|
616
|
+
select: USER_SUMMARY_FIELDS as unknown as (keyof User)[],
|
|
587
617
|
});
|
|
588
618
|
// @ts-ignore
|
|
589
|
-
entity[userFieldPath] = user;
|
|
619
|
+
entity[userFieldPath] = this.toUserSummary(user);
|
|
590
620
|
}
|
|
591
621
|
}
|
|
592
622
|
}
|
|
@@ -1474,6 +1474,28 @@ const getSolidCoreSettings = (isProd: boolean) =>
|
|
|
1474
1474
|
value: process.env.COMMON_META_WHATSAPP_APP_SECRET,
|
|
1475
1475
|
level: SettingLevel.SystemEnv,
|
|
1476
1476
|
},
|
|
1477
|
+
{
|
|
1478
|
+
moduleName: "solid-core",
|
|
1479
|
+
key: "uiTestDefaultTimeoutMs",
|
|
1480
|
+
value: 30000,
|
|
1481
|
+
level: SettingLevel.SystemAdminEditable,
|
|
1482
|
+
label: "UI Test Default Timeout (ms)",
|
|
1483
|
+
group: "testing-settings",
|
|
1484
|
+
sortOrder: 10,
|
|
1485
|
+
controlType: "numeric",
|
|
1486
|
+
helpText: "How long UI test steps wait for an element before failing. Raise this if tests time out on slow screens. Overridden by the --ui-timeout-ms flag and by a per-step timeoutMs.",
|
|
1487
|
+
},
|
|
1488
|
+
{
|
|
1489
|
+
moduleName: "solid-core",
|
|
1490
|
+
key: "uiTestNavigationTimeoutMs",
|
|
1491
|
+
value: 30000,
|
|
1492
|
+
level: SettingLevel.SystemAdminEditable,
|
|
1493
|
+
label: "UI Test Navigation Timeout (ms)",
|
|
1494
|
+
group: "testing-settings",
|
|
1495
|
+
sortOrder: 20,
|
|
1496
|
+
controlType: "numeric",
|
|
1497
|
+
helpText: "How long the ui.goto step waits for a page to load before failing. Falls back to the UI test default timeout.",
|
|
1498
|
+
},
|
|
1477
1499
|
] as const satisfies SettingDefinition[];
|
|
1478
1500
|
|
|
1479
1501
|
// 2.
|
package/src/testing/README.md
CHANGED
|
@@ -75,6 +75,48 @@ General step fields:
|
|
|
75
75
|
- `timeoutMs` (optional): per-step timeout override
|
|
76
76
|
- `spec` (optional): used by `test.spec` to identify the spec
|
|
77
77
|
|
|
78
|
+
## Timeouts
|
|
79
|
+
There are two independent timeout layers. They are easy to confuse, so note which one you want:
|
|
80
|
+
|
|
81
|
+
**1. Engine timeouts** — `scenario.timeoutMs` and the top-level `step.timeoutMs`.
|
|
82
|
+
These wrap execution in a `Promise.race` (`core/timeout.ts`). They are a hard upper bound
|
|
83
|
+
on wall-clock time and do **not** cancel the underlying browser call, so the failure message
|
|
84
|
+
only tells you the scenario or op that overran.
|
|
85
|
+
|
|
86
|
+
**2. UI (Playwright) timeouts** — the run-wide default plus per-step `with.timeoutMs`.
|
|
87
|
+
This is what governs how long `ui.expectVisible`, `ui.click`, `ui.fill` etc. wait for a
|
|
88
|
+
selector. Playwright cancels the operation itself, so the error names the selector and the
|
|
89
|
+
state it was waiting for. **If your UI steps are timing out, this is the knob to turn.**
|
|
90
|
+
|
|
91
|
+
Resolution order for UI steps: `with.timeoutMs` → `--ui-timeout-ms` → `UI_TIMEOUT_MS` →
|
|
92
|
+
`uiTestDefaultTimeoutMs` setting → `30000`.
|
|
93
|
+
`ui.goto` uses the navigation timeout instead: `with.timeoutMs` → `--ui-navigation-timeout-ms`
|
|
94
|
+
→ `UI_NAVIGATION_TIMEOUT_MS` → `uiTestNavigationTimeoutMs` setting → the resolved UI timeout.
|
|
95
|
+
|
|
96
|
+
Raise the default for a whole run:
|
|
97
|
+
```bash
|
|
98
|
+
solid test run --module <module-name> --ui-timeout-ms 60000
|
|
99
|
+
# or
|
|
100
|
+
UI_TIMEOUT_MS=60000 solid test run --module <module-name>
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Or set the installation-wide default under **Settings → Testing Settings** in the admin UI
|
|
104
|
+
(`uiTestDefaultTimeoutMs` / `uiTestNavigationTimeoutMs`). Those rows are created by `solid seed`;
|
|
105
|
+
until then the built-in `30000` applies. Flags and env vars still win, so CI keeps per-run control.
|
|
106
|
+
|
|
107
|
+
Or bump only the one slow step, leaving the rest of the suite strict:
|
|
108
|
+
```json
|
|
109
|
+
{
|
|
110
|
+
"then": {
|
|
111
|
+
"op": "ui.expectVisible",
|
|
112
|
+
"with": { "selector": "#report-grid", "timeoutMs": 90000 }
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Caution: a scenario-level `timeoutMs` lower than the UI timeout will fire first and mask the
|
|
118
|
+
more useful Playwright error. Keep `defaults.timeoutMs` above your UI timeout.
|
|
119
|
+
|
|
78
120
|
## Interpolation
|
|
79
121
|
Available tokens:
|
|
80
122
|
- `${env:NAME}` (environment variables)
|
|
@@ -198,6 +240,7 @@ Description: Navigates the browser to a URL.
|
|
|
198
240
|
|
|
199
241
|
Options in `with`:
|
|
200
242
|
- `url` (required)
|
|
243
|
+
- `timeoutMs` (optional, overrides the run-wide UI navigation timeout)
|
|
201
244
|
|
|
202
245
|
### **Op: `ui.expectUrl`**
|
|
203
246
|
Description: Asserts the current page URL.
|
|
@@ -212,6 +255,7 @@ Description: Fills an input or editable element.
|
|
|
212
255
|
Options in `with`:
|
|
213
256
|
- `selector` (required)
|
|
214
257
|
- `value` (required)
|
|
258
|
+
- `timeoutMs` (optional, overrides the run-wide UI timeout)
|
|
215
259
|
|
|
216
260
|
### **Op: `ui.select`**
|
|
217
261
|
Description: Selects an option in a select element.
|
|
@@ -219,12 +263,14 @@ Description: Selects an option in a select element.
|
|
|
219
263
|
Options in `with`:
|
|
220
264
|
- `selector` (required)
|
|
221
265
|
- `value` (required)
|
|
266
|
+
- `timeoutMs` (optional, overrides the run-wide UI timeout)
|
|
222
267
|
|
|
223
268
|
### **Op: `ui.click`**
|
|
224
269
|
Description: Clicks an element located by selector.
|
|
225
270
|
|
|
226
271
|
Options in `with`:
|
|
227
272
|
- `selector` (required)
|
|
273
|
+
- `timeoutMs` (optional, overrides the run-wide UI timeout)
|
|
228
274
|
|
|
229
275
|
### **Op: `ui.press`**
|
|
230
276
|
Description: Presses a keyboard key on a focused element.
|
|
@@ -232,6 +278,7 @@ Description: Presses a keyboard key on a focused element.
|
|
|
232
278
|
Options in `with`:
|
|
233
279
|
- `selector` (required)
|
|
234
280
|
- `key` (required)
|
|
281
|
+
- `timeoutMs` (optional, overrides the run-wide UI timeout)
|
|
235
282
|
|
|
236
283
|
### **Op: `ui.waitForManual`**
|
|
237
284
|
Description: Pauses a headed Playwright run so a human can interact with the live browser, then resumes when Enter is pressed in the terminal.
|
|
@@ -242,7 +289,7 @@ Options in `with`:
|
|
|
242
289
|
- `waitForSelector` (optional, waits for a visible selector after resume)
|
|
243
290
|
- `waitForUrlEquals` (optional, waits until the current URL exactly matches)
|
|
244
291
|
- `waitForUrlContains` (optional, waits until the current URL contains a substring)
|
|
245
|
-
- `timeoutMs` (optional, timeout for post-resume waits)
|
|
292
|
+
- `timeoutMs` (optional, timeout for post-resume waits; defaults to the run-wide UI timeout)
|
|
246
293
|
- `bringToFront` (optional, defaults to `true`)
|
|
247
294
|
|
|
248
295
|
Notes:
|
|
@@ -269,6 +316,7 @@ Description: Waits for an element to be visible.
|
|
|
269
316
|
|
|
270
317
|
Options in `with`:
|
|
271
318
|
- `selector` (required)
|
|
319
|
+
- `timeoutMs` (optional, overrides the run-wide UI timeout)
|
|
272
320
|
|
|
273
321
|
### **Op: `ui.expectText`**
|
|
274
322
|
Description: Asserts the text content of an element.
|
|
@@ -277,6 +325,7 @@ Options in `with`:
|
|
|
277
325
|
- `selector` (required)
|
|
278
326
|
- `equals` (optional)
|
|
279
327
|
- `contains` (optional)
|
|
328
|
+
- `timeoutMs` (optional, overrides the run-wide UI timeout)
|
|
280
329
|
|
|
281
330
|
### **Op: `assert.equals`**
|
|
282
331
|
Description: Asserts strict equality between two values.
|
|
@@ -388,7 +437,12 @@ await runFromMetadata({
|
|
|
388
437
|
includeTags: ["smoke"],
|
|
389
438
|
defaults: { timeoutMs: 30_000, retries: 1 },
|
|
390
439
|
api: { baseUrl: "https://api.example.com" },
|
|
391
|
-
ui: {
|
|
440
|
+
ui: {
|
|
441
|
+
baseUrl: "https://app.example.com",
|
|
442
|
+
headless: true,
|
|
443
|
+
defaultTimeoutMs: 30_000,
|
|
444
|
+
navigationTimeoutMs: 45_000
|
|
445
|
+
}
|
|
392
446
|
});
|
|
393
447
|
```
|
|
394
448
|
|
|
@@ -409,6 +463,14 @@ Lightweight existing-database workflow:
|
|
|
409
463
|
|
|
410
464
|
For human-assisted OTP or third-party verification flows, use `ui.waitForManual` in a headed run so the browser remains interactive while the scenario is paused.
|
|
411
465
|
|
|
466
|
+
Common `test run` flags (all pass through solidctl unchanged):
|
|
467
|
+
- `--ui-base-url <url>` / `--api-base-url <url>`
|
|
468
|
+
- `--headless [true|false]`
|
|
469
|
+
- `--ui-timeout-ms <number>` — default wait for every UI step (see [Timeouts](#timeouts))
|
|
470
|
+
- `--ui-navigation-timeout-ms <number>` — default wait for `ui.goto`
|
|
471
|
+
- `--timeout-ms <number>` / `--retries <number>` — scenario-level guard and retry count
|
|
472
|
+
- `--scenario-ids <ids>` / `--include-tags <tags>` / `--skip-scenario-ids <ids>`
|
|
473
|
+
|
|
412
474
|
## Add A New Step (SOP)
|
|
413
475
|
1. Create a new `*.step.ts` in the right domain folder.
|
|
414
476
|
2. Implement a `registerXSteps(registry)` function and `registry.register("op.name", handler)`.
|
|
@@ -6,9 +6,13 @@ function isAbsoluteUrl(url: string): boolean {
|
|
|
6
6
|
return /^https?:\/\//i.test(url);
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
+
export const DEFAULT_UI_TIMEOUT_MS = 30_000;
|
|
10
|
+
|
|
9
11
|
export class PlaywrightAdapter {
|
|
10
12
|
private readonly baseUrl?: string;
|
|
11
13
|
private readonly headless: boolean;
|
|
14
|
+
private readonly defaultTimeoutMs: number;
|
|
15
|
+
private readonly navigationTimeoutMs: number;
|
|
12
16
|
private browser?: Browser;
|
|
13
17
|
private context?: BrowserContext;
|
|
14
18
|
public page?: Page;
|
|
@@ -16,16 +20,37 @@ export class PlaywrightAdapter {
|
|
|
16
20
|
constructor(opts?: PlaywrightAdapterOptions) {
|
|
17
21
|
this.baseUrl = opts?.baseUrl;
|
|
18
22
|
this.headless = opts?.headless ?? true;
|
|
23
|
+
this.defaultTimeoutMs = opts?.defaultTimeoutMs ?? DEFAULT_UI_TIMEOUT_MS;
|
|
24
|
+
this.navigationTimeoutMs =
|
|
25
|
+
opts?.navigationTimeoutMs ?? this.defaultTimeoutMs;
|
|
19
26
|
}
|
|
20
27
|
|
|
21
28
|
isHeadless(): boolean {
|
|
22
29
|
return this.headless;
|
|
23
30
|
}
|
|
24
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Resolves the timeout for a single Playwright call: an explicit per-step
|
|
34
|
+
* value wins, otherwise the run-wide default applies.
|
|
35
|
+
*/
|
|
36
|
+
resolveTimeout(stepTimeoutMs?: number): number {
|
|
37
|
+
return typeof stepTimeoutMs === 'number' && Number.isFinite(stepTimeoutMs)
|
|
38
|
+
? stepTimeoutMs
|
|
39
|
+
: this.defaultTimeoutMs;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
resolveNavigationTimeout(stepTimeoutMs?: number): number {
|
|
43
|
+
return typeof stepTimeoutMs === 'number' && Number.isFinite(stepTimeoutMs)
|
|
44
|
+
? stepTimeoutMs
|
|
45
|
+
: this.navigationTimeoutMs;
|
|
46
|
+
}
|
|
47
|
+
|
|
25
48
|
async start(): Promise<void> {
|
|
26
49
|
const { chromium } = await import('playwright');
|
|
27
50
|
this.browser = await chromium.launch({ headless: this.headless });
|
|
28
51
|
this.context = await this.browser.newContext();
|
|
52
|
+
this.context.setDefaultTimeout(this.defaultTimeoutMs);
|
|
53
|
+
this.context.setDefaultNavigationTimeout(this.navigationTimeoutMs);
|
|
29
54
|
this.page = await this.context.newPage();
|
|
30
55
|
}
|
|
31
56
|
|
|
@@ -2,8 +2,8 @@ import type { TestContext } from "../../contracts/runtime-context.types";
|
|
|
2
2
|
import type { OpStep } from "../../contracts/testing-metadata.types";
|
|
3
3
|
import { StepRegistry } from "../../core/step-registry";
|
|
4
4
|
|
|
5
|
-
type ClickInput = { selector: string };
|
|
6
|
-
type PressInput = { selector: string; key: string };
|
|
5
|
+
type ClickInput = { selector: string; timeoutMs?: number };
|
|
6
|
+
type PressInput = { selector: string; key: string; timeoutMs?: number };
|
|
7
7
|
|
|
8
8
|
function requirePage(ctx: TestContext, op: string) {
|
|
9
9
|
if (!ctx.ui || !ctx.ui.page) {
|
|
@@ -19,7 +19,9 @@ export function registerActionSteps(registry: StepRegistry): void {
|
|
|
19
19
|
if (!input.selector) {
|
|
20
20
|
throw new Error('Missing "selector" in step.with for op "ui.click"');
|
|
21
21
|
}
|
|
22
|
-
await page.click(input.selector
|
|
22
|
+
await page.click(input.selector, {
|
|
23
|
+
timeout: ctx.ui?.resolveTimeout(input.timeoutMs),
|
|
24
|
+
});
|
|
23
25
|
});
|
|
24
26
|
|
|
25
27
|
registry.register("ui.press", async (ctx: TestContext, step: OpStep) => {
|
|
@@ -31,6 +33,8 @@ export function registerActionSteps(registry: StepRegistry): void {
|
|
|
31
33
|
if (!input.key) {
|
|
32
34
|
throw new Error('Missing "key" in step.with for op "ui.press"');
|
|
33
35
|
}
|
|
34
|
-
await page.press(input.selector, input.key
|
|
36
|
+
await page.press(input.selector, input.key, {
|
|
37
|
+
timeout: ctx.ui?.resolveTimeout(input.timeoutMs),
|
|
38
|
+
});
|
|
35
39
|
});
|
|
36
40
|
}
|
|
@@ -2,8 +2,13 @@ import type { TestContext } from "../../contracts/runtime-context.types";
|
|
|
2
2
|
import type { OpStep } from "../../contracts/testing-metadata.types";
|
|
3
3
|
import { StepRegistry } from "../../core/step-registry";
|
|
4
4
|
|
|
5
|
-
type VisibleInput = { selector: string };
|
|
6
|
-
type ExpectTextInput = {
|
|
5
|
+
type VisibleInput = { selector: string; timeoutMs?: number };
|
|
6
|
+
type ExpectTextInput = {
|
|
7
|
+
selector: string;
|
|
8
|
+
equals?: string;
|
|
9
|
+
contains?: string;
|
|
10
|
+
timeoutMs?: number;
|
|
11
|
+
};
|
|
7
12
|
|
|
8
13
|
function requirePage(ctx: TestContext, op: string) {
|
|
9
14
|
if (!ctx.ui || !ctx.ui.page) {
|
|
@@ -19,7 +24,10 @@ export function registerAssertionSteps(registry: StepRegistry): void {
|
|
|
19
24
|
if (!input.selector) {
|
|
20
25
|
throw new Error('Missing "selector" in step.with for op "ui.expectVisible"');
|
|
21
26
|
}
|
|
22
|
-
await page.waitForSelector(input.selector, {
|
|
27
|
+
await page.waitForSelector(input.selector, {
|
|
28
|
+
state: "visible",
|
|
29
|
+
timeout: ctx.ui?.resolveTimeout(input.timeoutMs),
|
|
30
|
+
});
|
|
23
31
|
});
|
|
24
32
|
|
|
25
33
|
registry.register("ui.expectHidden", async (ctx: TestContext, step: OpStep) => {
|
|
@@ -28,7 +36,10 @@ export function registerAssertionSteps(registry: StepRegistry): void {
|
|
|
28
36
|
if (!input.selector) {
|
|
29
37
|
throw new Error('Missing "selector" in step.with for op "ui.expectHidden"');
|
|
30
38
|
}
|
|
31
|
-
await page.waitForSelector(input.selector, {
|
|
39
|
+
await page.waitForSelector(input.selector, {
|
|
40
|
+
state: "hidden",
|
|
41
|
+
timeout: ctx.ui?.resolveTimeout(input.timeoutMs),
|
|
42
|
+
});
|
|
32
43
|
});
|
|
33
44
|
|
|
34
45
|
registry.register("ui.expectText", async (ctx: TestContext, step: OpStep) => {
|
|
@@ -38,7 +49,9 @@ export function registerAssertionSteps(registry: StepRegistry): void {
|
|
|
38
49
|
throw new Error('Missing "selector" in step.with for op "ui.expectText"');
|
|
39
50
|
}
|
|
40
51
|
|
|
41
|
-
const text = await page
|
|
52
|
+
const text = await page
|
|
53
|
+
.locator(input.selector)
|
|
54
|
+
.innerText({ timeout: ctx.ui?.resolveTimeout(input.timeoutMs) });
|
|
42
55
|
if (input.equals !== undefined) {
|
|
43
56
|
if (text !== input.equals) {
|
|
44
57
|
throw new Error(
|
|
@@ -4,9 +4,13 @@ import type { TestContext } from "../../contracts/runtime-context.types";
|
|
|
4
4
|
import type { OpStep } from "../../contracts/testing-metadata.types";
|
|
5
5
|
import { StepRegistry } from "../../core/step-registry";
|
|
6
6
|
|
|
7
|
-
type FillInput = { selector: string; value: string };
|
|
8
|
-
type SelectInput = { selector: string; value: string };
|
|
9
|
-
type UploadFileInput = {
|
|
7
|
+
type FillInput = { selector: string; value: string; timeoutMs?: number };
|
|
8
|
+
type SelectInput = { selector: string; value: string; timeoutMs?: number };
|
|
9
|
+
type UploadFileInput = {
|
|
10
|
+
selector: string;
|
|
11
|
+
filePath: string | string[];
|
|
12
|
+
timeoutMs?: number;
|
|
13
|
+
};
|
|
10
14
|
|
|
11
15
|
function requirePage(ctx: TestContext, op: string) {
|
|
12
16
|
if (!ctx.ui || !ctx.ui.page) {
|
|
@@ -25,7 +29,9 @@ export function registerFormSteps(registry: StepRegistry): void {
|
|
|
25
29
|
if (input.value === undefined) {
|
|
26
30
|
throw new Error('Missing "value" in step.with for op "ui.fill"');
|
|
27
31
|
}
|
|
28
|
-
await page.fill(input.selector, String(input.value)
|
|
32
|
+
await page.fill(input.selector, String(input.value), {
|
|
33
|
+
timeout: ctx.ui?.resolveTimeout(input.timeoutMs),
|
|
34
|
+
});
|
|
29
35
|
});
|
|
30
36
|
|
|
31
37
|
registry.register("ui.select", async (ctx: TestContext, step: OpStep) => {
|
|
@@ -37,7 +43,9 @@ export function registerFormSteps(registry: StepRegistry): void {
|
|
|
37
43
|
if (input.value === undefined) {
|
|
38
44
|
throw new Error('Missing "value" in step.with for op "ui.select"');
|
|
39
45
|
}
|
|
40
|
-
await page.selectOption(input.selector, String(input.value)
|
|
46
|
+
await page.selectOption(input.selector, String(input.value), {
|
|
47
|
+
timeout: ctx.ui?.resolveTimeout(input.timeoutMs),
|
|
48
|
+
});
|
|
41
49
|
});
|
|
42
50
|
|
|
43
51
|
registry.register("ui.uploadFile", async (ctx: TestContext, step: OpStep) => {
|
|
@@ -53,6 +61,8 @@ export function registerFormSteps(registry: StepRegistry): void {
|
|
|
53
61
|
const resolved = Array.isArray(input.filePath)
|
|
54
62
|
? input.filePath.map((p) => path.resolve(cwd, String(p)))
|
|
55
63
|
: path.resolve(cwd, String(input.filePath));
|
|
56
|
-
await page.setInputFiles(input.selector, resolved
|
|
64
|
+
await page.setInputFiles(input.selector, resolved, {
|
|
65
|
+
timeout: ctx.ui?.resolveTimeout(input.timeoutMs),
|
|
66
|
+
});
|
|
57
67
|
});
|
|
58
68
|
}
|
|
@@ -28,7 +28,7 @@ export function registerManualSteps(registry: StepRegistry): void {
|
|
|
28
28
|
const inputConfig = (step.with ?? {}) as WaitForManualInput;
|
|
29
29
|
const message = inputConfig.message?.trim() || "Manual interaction required.";
|
|
30
30
|
const prompt = inputConfig.prompt?.trim() || "Press Enter to continue...";
|
|
31
|
-
const timeoutMs = inputConfig.timeoutMs;
|
|
31
|
+
const timeoutMs = ctx.ui?.resolveTimeout(inputConfig.timeoutMs);
|
|
32
32
|
const bringToFront = inputConfig.bringToFront ?? true;
|
|
33
33
|
|
|
34
34
|
if (ctx.ui?.isHeadless()) {
|