create-qpq-app 0.1.19 → 0.1.21

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.
Files changed (112) hide show
  1. package/lib/commonjs/steps/006_applyDomain.js +3 -8
  2. package/lib/commonjs/steps/006_applyDomain.js.map +1 -1
  3. package/lib/commonjs/steps/013_printNextSteps.js +1 -1
  4. package/lib/commonjs/steps/013_printNextSteps.js.map +1 -1
  5. package/lib/esm/steps/006_applyDomain.js +3 -8
  6. package/lib/esm/steps/006_applyDomain.js.map +1 -1
  7. package/lib/esm/steps/013_printNextSteps.js +1 -1
  8. package/lib/esm/steps/013_printNextSteps.js.map +1 -1
  9. package/package.json +2 -2
  10. package/template/README.md +6 -3
  11. package/template/apps/qpqjs/account.qpq.ts +5 -13
  12. package/template/apps/qpqjs/bootstrap.qpq.ts +29 -14
  13. package/template/apps/qpqjs/deploy.config.json +0 -1
  14. package/template/apps/qpqjs/packages/constants/src/domain.ts +3 -1
  15. package/template/apps/qpqjs/packages/service-utils/src/defineQpqjsService.ts +15 -9
  16. package/template/apps/qpqjs/services/design/views/src/components/LandingPage/components/Infrastructure.tsx +1 -2
  17. package/template/apps/qpqjs/services/shell/service/src/infrastructure.ts +1 -4
  18. package/template/apps/qpqjs/services/test/models/package.json +2 -1
  19. package/template/apps/qpqjs/services/test/models/src/EchoRequest.ts +8 -0
  20. package/template/apps/qpqjs/services/test/models/src/EchoResponse.ts +9 -0
  21. package/template/apps/qpqjs/services/test/models/src/SmokeRun.ts +11 -9
  22. package/template/apps/qpqjs/services/test/models/src/SmokeRunStarted.ts +8 -0
  23. package/template/apps/qpqjs/services/test/models/src/SmokeRunSummary.ts +10 -6
  24. package/template/apps/qpqjs/services/test/models/src/SmokeRunWithSummary.ts +9 -5
  25. package/template/apps/qpqjs/services/test/models/src/SmokeTestResult.ts +11 -9
  26. package/template/apps/qpqjs/services/test/models/src/index.ts +3 -0
  27. package/template/apps/qpqjs/services/test/service/src/echo/config/defineEcho.ts +9 -0
  28. package/template/apps/qpqjs/services/test/service/src/echo/controller/echo.ts +32 -0
  29. package/template/apps/qpqjs/services/test/service/src/echo/controller/index.ts +1 -0
  30. package/template/apps/qpqjs/services/test/service/src/infrastructure.ts +3 -0
  31. package/template/apps/qpqjs/services/test/service/src/smoke/config/defineSmoke.ts +83 -18
  32. package/template/apps/qpqjs/services/test/service/src/smoke/constants/index.ts +1 -0
  33. package/template/apps/qpqjs/services/test/service/src/smoke/constants/smokeEventDoc.ts +27 -0
  34. package/template/apps/qpqjs/services/test/service/src/smoke/constants/smokeRunQueue.ts +4 -4
  35. package/template/apps/qpqjs/services/test/service/src/smoke/controller/askRunSmokeTests.ts +5 -1
  36. package/template/apps/qpqjs/services/test/service/src/smoke/data/askRecordSmokeTestResult.ts +32 -0
  37. package/template/apps/qpqjs/services/test/service/src/smoke/data/askSetSmokeRunOutcome.ts +24 -0
  38. package/template/apps/qpqjs/services/test/service/src/smoke/data/index.ts +2 -0
  39. package/template/apps/qpqjs/services/test/service/src/smoke/eventDoc/index.ts +1 -0
  40. package/template/apps/qpqjs/services/test/service/src/smoke/eventDoc/smokeProbeDocDefinition.ts +74 -0
  41. package/template/apps/qpqjs/services/test/service/src/smoke/index.ts +1 -0
  42. package/template/apps/qpqjs/services/test/service/src/smoke/logic/smokeRun/askExecuteSmokeTest.ts +97 -0
  43. package/template/apps/qpqjs/services/test/service/src/smoke/logic/smokeRun/askStartSmokeRun.ts +18 -10
  44. package/template/apps/qpqjs/services/test/service/src/smoke/logic/smokeRun/index.ts +1 -1
  45. package/template/apps/qpqjs/services/test/service/src/smoke/models/SmokeEventDocAppendQueueEvent.ts +15 -0
  46. package/template/apps/qpqjs/services/test/service/src/smoke/models/SmokeEventDocMark.ts +8 -0
  47. package/template/apps/qpqjs/services/test/service/src/smoke/models/SmokeTestRequestedQueueEvent.ts +12 -0
  48. package/template/apps/qpqjs/services/test/service/src/smoke/models/index.ts +3 -1
  49. package/template/apps/qpqjs/services/test/service/src/smoke/queue/index.ts +2 -1
  50. package/template/apps/qpqjs/services/test/service/src/smoke/queue/onSmokeEventDocAppend.ts +64 -0
  51. package/template/apps/qpqjs/services/test/service/src/smoke/queue/onSmokeTestRequested.ts +17 -0
  52. package/template/apps/qpqjs/services/test/service/src/smoke/tests/eventDoc/askAssertContiguousEventIds.ts +18 -0
  53. package/template/apps/qpqjs/services/test/service/src/smoke/tests/eventDoc/askAssertSmokeProbeDocState.ts +32 -0
  54. package/template/apps/qpqjs/services/test/service/src/smoke/tests/eventDoc/askAwaitSmokeEventDocLog.ts +35 -0
  55. package/template/apps/qpqjs/services/test/service/src/smoke/tests/eventDoc/askRunEventDocConcurrentAppendTest.ts +82 -0
  56. package/template/apps/qpqjs/services/test/service/src/smoke/tests/eventDoc/askRunEventDocInterleavedBatchAppendTest.ts +110 -0
  57. package/template/apps/qpqjs/services/test/service/src/smoke/tests/eventDoc/index.ts +5 -0
  58. package/template/apps/qpqjs/services/test/service/src/smoke/tests/index.ts +2 -0
  59. package/template/apps/qpqjs/services/test/service/src/smoke/tests/openApi/askRunEchoRoundTripTest.ts +45 -0
  60. package/template/apps/qpqjs/services/test/service/src/smoke/tests/openApi/askRunOpenApiDocumentTest.ts +42 -0
  61. package/template/apps/qpqjs/services/test/service/src/smoke/tests/openApi/index.ts +2 -0
  62. package/template/apps/qpqjs/services/test/service/src/smoke/tests/smokeTestRegistry.ts +18 -2
  63. package/template/apps/todo/deploy.config.json +0 -1
  64. package/template/apps/todo/packages/constants/src/domain.ts +2 -1
  65. package/template/apps/todo/packages/service-utils/src/defineTodoService.ts +5 -6
  66. package/template/apps/todo/services/shell/service/src/infrastructure.ts +1 -3
  67. package/template/docusaurus/docs/actions/core/key-value-store/ask-key-value-store-upsert-with-retry.md +2 -2
  68. package/template/docusaurus/docs/actions/core/key-value-store/ask-key-value-store-upsert.md +2 -1
  69. package/template/docusaurus/docs/actions/features/event-doc/ask-event-doc-append-server-event.md +1 -1
  70. package/template/docusaurus/docs/actions/features/event-doc/ask-event-doc-create.md +1 -1
  71. package/template/docusaurus/docs/actions/features/event-doc/ask-event-doc-event-append.md +18 -12
  72. package/template/docusaurus/docs/actions/features/event-doc/ask-event-doc-event-list.md +4 -4
  73. package/template/docusaurus/docs/actions/features/event-doc/ask-event-doc-event-write.md +6 -6
  74. package/template/docusaurus/docs/actions/features/event-doc/ask-event-doc-get-by-id.md +2 -2
  75. package/template/docusaurus/docs/actions/features/event-doc/ask-event-doc-get-draft.md +2 -2
  76. package/template/docusaurus/docs/actions/webserver/dns/ask-dns-list.md +9 -8
  77. package/template/docusaurus/docs/actions/webserver/dns/ask-dns-resolve-hosts.md +42 -0
  78. package/template/docusaurus/docs/actions/webserver/open-api/_category_.json +1 -0
  79. package/template/docusaurus/docs/actions/webserver/open-api/ask-open-api-get-document.md +41 -0
  80. package/template/docusaurus/docs/config/config-aws/account-github-oidc-provider.md +31 -0
  81. package/template/docusaurus/docs/config/config-aws/domain-certificate.md +16 -32
  82. package/template/docusaurus/docs/config/config-aws/email-sender-allow-list.md +7 -14
  83. package/template/docusaurus/docs/config/config-aws/github-deploy-role.md +68 -0
  84. package/template/docusaurus/docs/config/core/user-directory.md +2 -3
  85. package/template/docusaurus/docs/config/features/admin-settings.md +2 -6
  86. package/template/docusaurus/docs/config/features/event-doc-summary.md +2 -2
  87. package/template/docusaurus/docs/config/features/open-api-routes.md +82 -0
  88. package/template/docusaurus/docs/config/features/tenanted-web-socket-queue.md +2 -3
  89. package/template/docusaurus/docs/config/features/web-socket-queue.md +1 -5
  90. package/template/docusaurus/docs/config/webserver/api.md +6 -9
  91. package/template/docusaurus/docs/config/webserver/cache.md +1 -1
  92. package/template/docusaurus/docs/config/webserver/dns.md +25 -17
  93. package/template/docusaurus/docs/config/webserver/domain-proxy.md +1 -5
  94. package/template/docusaurus/docs/config/webserver/email-sender.md +9 -15
  95. package/template/docusaurus/docs/config/webserver/route.md +1 -0
  96. package/template/docusaurus/docs/config/webserver/seo.md +1 -1
  97. package/template/docusaurus/docs/config/webserver/web-entry.md +0 -5
  98. package/template/docusaurus/docs/config/webserver/websocket.md +3 -7
  99. package/template/docusaurus/docs/domains.md +76 -0
  100. package/template/docusaurus/docs/use-cases.md +0 -6
  101. package/template/docusaurus/docusaurus.config.ts +38 -14
  102. package/template/docusaurus/sidebars.ts +5 -5
  103. package/template/docusaurus/src/components/HomepageFeatures/index.tsx +2 -2
  104. package/template/scripts/deployedSmoke.mjs +82 -23
  105. package/template/scripts/smoke/runEdgeChecks.mjs +152 -0
  106. package/template/scripts/smoke/runSmokeRun.mjs +1 -1
  107. package/template/apps/qpqjs/services/test/service/src/smoke/logic/smokeRun/askExecuteSmokeRun.ts +0 -83
  108. package/template/apps/qpqjs/services/test/service/src/smoke/models/SmokeRunRequestedQueueEvent.ts +0 -9
  109. package/template/apps/qpqjs/services/test/service/src/smoke/queue/onSmokeRunRequested.ts +0 -15
  110. package/template/docusaurus/docs/actions/webserver/open-api-spec/_category_.json +0 -1
  111. package/template/docusaurus/docs/actions/webserver/open-api-spec/ask-get-open-api-spec.md +0 -43
  112. package/template/docusaurus/docs/config/webserver/open-api.md +0 -36
@@ -0,0 +1,32 @@
1
+ import { AskResponse } from 'quidproquo';
2
+ import { askEventDocDocumentStateLatest } from 'quidproquo-features';
3
+
4
+ import { SmokeProbeDocState } from '../../eventDoc/smokeProbeDocDefinition';
5
+ import { askSmokeAssert } from '../askSmokeAssert';
6
+
7
+ // The folded document, read the way the render/references routes read it: snapshot
8
+ // seeded (whatever the stream projector has stored so far) plus the gap, through the
9
+ // collection's REGISTERED fold. Proves the dynamic-functions registration resolves in
10
+ // the deployed runtime and that the fold saw every event: the numbers it holds are
11
+ // exactly `expectedNumbers`, no extras, none missing, none twice.
12
+ export function* askAssertSmokeProbeDocState(
13
+ docId: string,
14
+ expectedNumbers: number[]
15
+ ): AskResponse<void> {
16
+ const latest = yield* askEventDocDocumentStateLatest(docId, {
17
+ consistentRead: true,
18
+ });
19
+ const numbers =
20
+ (latest?.state as SmokeProbeDocState | undefined)?.numbers ?? [];
21
+
22
+ const sorted = [...numbers].sort((a, b) => a - b);
23
+ const expected = [...expectedNumbers].sort((a, b) => a - b);
24
+ const same =
25
+ sorted.length === expected.length &&
26
+ sorted.every((value, index) => value === expected[index]);
27
+
28
+ yield* askSmokeAssert(
29
+ same,
30
+ `folded state holds ${numbers.length} numbers, expected ${expected.length} (1..${expected.length}); got [${sorted.join(', ')}]`
31
+ );
32
+ }
@@ -0,0 +1,35 @@
1
+ import { askDelay, AskResponse } from 'quidproquo';
2
+ import { askEventDocEventListAll, EventDocEvent } from 'quidproquo-features';
3
+
4
+ import { askSmokeAssert } from '../askSmokeAssert';
5
+
6
+ const POLL_ATTEMPTS = 20;
7
+ const POLL_INTERVAL_MS = 2000;
8
+
9
+ // Poll a probe doc's log until it holds `expectedLength` events (INIT_STATE included),
10
+ // consistently read so a just-landed append is never missed. Fails the test if the
11
+ // writers have not all landed in time - a stuck writer (one that lost the slot race
12
+ // past its retry cap) shows up here as a short log. Assumes the store context.
13
+ export function* askAwaitSmokeEventDocLog(
14
+ docId: string,
15
+ expectedLength: number
16
+ ): AskResponse<EventDocEvent[]> {
17
+ let events: EventDocEvent[] = [];
18
+
19
+ for (let attempt = 0; attempt < POLL_ATTEMPTS; attempt += 1) {
20
+ events = yield* askEventDocEventListAll(docId, { consistentRead: true });
21
+
22
+ if (events.length >= expectedLength) {
23
+ break;
24
+ }
25
+
26
+ yield* askDelay(POLL_INTERVAL_MS);
27
+ }
28
+
29
+ yield* askSmokeAssert(
30
+ events.length === expectedLength,
31
+ `expected ${expectedLength} events on doc [${docId}], found ${events.length}`
32
+ );
33
+
34
+ return events;
35
+ }
@@ -0,0 +1,82 @@
1
+ import { askQueueSendMessages, AskResponse } from 'quidproquo';
2
+ import {
3
+ askEventDocCreate,
4
+ askEventDocProvideStore,
5
+ askEventDocSoftDelete,
6
+ } from 'quidproquo-features';
7
+
8
+ import {
9
+ SMOKE_EVENT_DOC_ACTOR,
10
+ SMOKE_EVENT_DOC_APPEND_MESSAGE_TYPE,
11
+ SMOKE_EVENT_DOC_APPEND_QUEUE,
12
+ SMOKE_EVENT_DOC_MARK_EVENT,
13
+ SMOKE_EVENT_DOC_SCHEMA_VERSION,
14
+ SMOKE_EVENT_DOC_STORE_OPTIONS,
15
+ } from '../../constants/smokeEventDoc';
16
+ import { SMOKE_PROBE_DOC_WINDOW } from '../../eventDoc/smokeProbeDocDefinition';
17
+ import { SmokeEventDocAppendPayload } from '../../models/SmokeEventDocAppendQueueEvent';
18
+ import { SmokeEventDocMark } from '../../models/SmokeEventDocMark';
19
+ import { askSmokeAssert } from '../askSmokeAssert';
20
+ import { askAssertContiguousEventIds } from './askAssertContiguousEventIds';
21
+ import { askAssertSmokeProbeDocState } from './askAssertSmokeProbeDocState';
22
+ import { askAwaitSmokeEventDocLog } from './askAwaitSmokeEventDocLog';
23
+
24
+ // One writer per number the document can hold, so the folded window is exactly full.
25
+ const WRITERS = SMOKE_PROBE_DOC_WINDOW;
26
+
27
+ // N writers, each its own queue invocation, each appending ONE number (1..N) to the
28
+ // same fresh document at the same moment. The conditional-write append loop has to
29
+ // serialise them onto consecutive slots: afterwards the log is INIT_STATE plus one mark
30
+ // per writer, ids 0..N with no hole and no duplicate, and the document folded through
31
+ // the registered definition holds exactly the numbers 1..N.
32
+ function* askRunInStore(runId: string): AskResponse<void> {
33
+ const doc = yield* askEventDocCreate(
34
+ `smoke-${runId}`,
35
+ `smoke-${runId}`,
36
+ SMOKE_EVENT_DOC_ACTOR
37
+ );
38
+
39
+ const numbers = Array.from({ length: WRITERS }, (_, index) => index + 1);
40
+
41
+ const messages = numbers.map((value) => ({
42
+ type: SMOKE_EVENT_DOC_APPEND_MESSAGE_TYPE,
43
+ payload: {
44
+ docId: doc.id,
45
+ runId,
46
+ writerId: value,
47
+ values: [value],
48
+ } satisfies SmokeEventDocAppendPayload,
49
+ }));
50
+
51
+ yield* askQueueSendMessages(SMOKE_EVENT_DOC_APPEND_QUEUE, ...messages);
52
+
53
+ const events = yield* askAwaitSmokeEventDocLog(doc.id, WRITERS + 1);
54
+
55
+ yield* askAssertContiguousEventIds(events);
56
+
57
+ const marks = events
58
+ .filter((event) => event.type === SMOKE_EVENT_DOC_MARK_EVENT)
59
+ .map((event) => event.payload.data as SmokeEventDocMark);
60
+
61
+ yield* askSmokeAssert(
62
+ marks.every((mark) => mark.runId === runId),
63
+ "a mark from another run is on this run's document"
64
+ );
65
+
66
+ yield* askAssertSmokeProbeDocState(doc.id, numbers);
67
+
68
+ yield* askEventDocSoftDelete(
69
+ doc.id,
70
+ SMOKE_EVENT_DOC_ACTOR.userId,
71
+ SMOKE_EVENT_DOC_SCHEMA_VERSION
72
+ );
73
+ }
74
+
75
+ export function* askRunEventDocConcurrentAppendTest(
76
+ runId: string
77
+ ): AskResponse<void> {
78
+ yield* askEventDocProvideStore(
79
+ SMOKE_EVENT_DOC_STORE_OPTIONS,
80
+ askRunInStore(runId)
81
+ );
82
+ }
@@ -0,0 +1,110 @@
1
+ import { askQueueSendMessages, AskResponse } from 'quidproquo';
2
+ import {
3
+ askEventDocCreate,
4
+ askEventDocProvideStore,
5
+ askEventDocSoftDelete,
6
+ } from 'quidproquo-features';
7
+
8
+ import {
9
+ SMOKE_EVENT_DOC_ACTOR,
10
+ SMOKE_EVENT_DOC_APPEND_MESSAGE_TYPE,
11
+ SMOKE_EVENT_DOC_APPEND_QUEUE,
12
+ SMOKE_EVENT_DOC_MARK_EVENT,
13
+ SMOKE_EVENT_DOC_SCHEMA_VERSION,
14
+ SMOKE_EVENT_DOC_STORE_OPTIONS,
15
+ } from '../../constants/smokeEventDoc';
16
+ import { SmokeEventDocAppendPayload } from '../../models/SmokeEventDocAppendQueueEvent';
17
+ import { SmokeEventDocMark } from '../../models/SmokeEventDocMark';
18
+ import { askSmokeAssert } from '../askSmokeAssert';
19
+ import { askAssertContiguousEventIds } from './askAssertContiguousEventIds';
20
+ import { askAssertSmokeProbeDocState } from './askAssertSmokeProbeDocState';
21
+ import { askAwaitSmokeEventDocLog } from './askAwaitSmokeEventDocLog';
22
+
23
+ const BATCH_WRITERS = 4;
24
+ const BATCH_SIZE = 5;
25
+ const SINGLE_WRITERS = 6;
26
+
27
+ // Batch writers (one transactional write of BATCH_SIZE consecutive slots each) racing
28
+ // single writers on the same fresh document. Every writer lands, the log is contiguous,
29
+ // each batch's events sit in one unbroken run in input order (a batch that lost its
30
+ // slots was re-laid whole above whoever beat it, never split around them), and the
31
+ // document folded through the registered definition holds exactly the numbers sent.
32
+ function* askRunInStore(runId: string): AskResponse<void> {
33
+ const doc = yield* askEventDocCreate(
34
+ `smoke-batch-${runId}`,
35
+ `smoke-batch-${runId}`,
36
+ SMOKE_EVENT_DOC_ACTOR
37
+ );
38
+
39
+ // Numbers are dealt out in send order: a batch writer takes the next BATCH_SIZE, a
40
+ // single writer the next one. The two kinds alternate so neither gets a head start.
41
+ let nextNumber = 1;
42
+ const takeNumbers = (count: number): number[] =>
43
+ Array.from({ length: count }, () => nextNumber++);
44
+
45
+ const messages = Array.from(
46
+ { length: BATCH_WRITERS + SINGLE_WRITERS },
47
+ (_, index) => {
48
+ const isBatch = index < BATCH_WRITERS * 2 && index % 2 === 0;
49
+ return {
50
+ type: SMOKE_EVENT_DOC_APPEND_MESSAGE_TYPE,
51
+ payload: {
52
+ docId: doc.id,
53
+ runId,
54
+ writerId: index + 1,
55
+ values: takeNumbers(isBatch ? BATCH_SIZE : 1),
56
+ } satisfies SmokeEventDocAppendPayload,
57
+ };
58
+ }
59
+ );
60
+
61
+ const expectedNumbers = messages.flatMap((message) => message.payload.values);
62
+
63
+ yield* askQueueSendMessages(SMOKE_EVENT_DOC_APPEND_QUEUE, ...messages);
64
+
65
+ const events = yield* askAwaitSmokeEventDocLog(
66
+ doc.id,
67
+ expectedNumbers.length + 1
68
+ );
69
+
70
+ yield* askAssertContiguousEventIds(events);
71
+
72
+ const marked = events
73
+ .filter((event) => event.type === SMOKE_EVENT_DOC_MARK_EVENT)
74
+ .map((event) => ({
75
+ eventId: event.payload.metadata.eventId,
76
+ mark: event.payload.data as SmokeEventDocMark,
77
+ }));
78
+
79
+ for (const message of messages.filter((m) => m.payload.values.length > 1)) {
80
+ const { writerId, values } = message.payload;
81
+ const run = marked.filter((entry) => entry.mark.writerId === writerId);
82
+ const consecutive = run.every(
83
+ (entry, index) =>
84
+ entry.mark.value === values[index] &&
85
+ (index === 0 || entry.eventId === run[index - 1].eventId + 1)
86
+ );
87
+
88
+ yield* askSmokeAssert(
89
+ run.length === values.length && consecutive,
90
+ `batch writer ${writerId} did not land as one consecutive run in order: ids [${run.map((entry) => entry.eventId).join(', ')}]`
91
+ );
92
+ }
93
+
94
+ yield* askAssertSmokeProbeDocState(doc.id, expectedNumbers);
95
+
96
+ yield* askEventDocSoftDelete(
97
+ doc.id,
98
+ SMOKE_EVENT_DOC_ACTOR.userId,
99
+ SMOKE_EVENT_DOC_SCHEMA_VERSION
100
+ );
101
+ }
102
+
103
+ export function* askRunEventDocInterleavedBatchAppendTest(
104
+ runId: string
105
+ ): AskResponse<void> {
106
+ yield* askEventDocProvideStore(
107
+ SMOKE_EVENT_DOC_STORE_OPTIONS,
108
+ askRunInStore(runId)
109
+ );
110
+ }
@@ -0,0 +1,5 @@
1
+ export * from './askAssertContiguousEventIds';
2
+ export * from './askAssertSmokeProbeDocState';
3
+ export * from './askAwaitSmokeEventDocLog';
4
+ export * from './askRunEventDocConcurrentAppendTest';
5
+ export * from './askRunEventDocInterleavedBatchAppendTest';
@@ -1,8 +1,10 @@
1
1
  export * from './askSmokeAssert';
2
2
  export * from './crossService';
3
3
  export * from './eventBus';
4
+ export * from './eventDoc';
4
5
  export * from './keyValueStore';
5
6
  export * from './noop';
7
+ export * from './openApi';
6
8
  export * from './parameter';
7
9
  export * from './secret';
8
10
  export * from './SmokeTestDefinition';
@@ -0,0 +1,45 @@
1
+ import {
2
+ askNetworkRequest,
3
+ askOpenApiGetDocument,
4
+ AskResponse,
5
+ } from 'quidproquo';
6
+
7
+ import { EchoRequest, EchoResponse } from '@qpqjs/test-models';
8
+
9
+ import { askSmokeAssert } from '../askSmokeAssert';
10
+
11
+ // The public echo route over real HTTP, from the document's own servers entry,
12
+ // so the same test also proves that entry is a reachable base for this
13
+ // environment (the deployed api domain, or the dev server's mount path).
14
+ // The runId is the echoed value, so the response is provably this run's.
15
+ export function* askRunEchoRoundTripTest(runId: string): AskResponse<void> {
16
+ const document = yield* askOpenApiGetDocument();
17
+ const baseUrl = document.servers[0]?.url;
18
+ yield* askSmokeAssert(!!baseUrl, 'document lists no servers to call');
19
+
20
+ const valid: EchoRequest = { bodyValue: runId };
21
+ const ok = yield* askNetworkRequest<EchoRequest, EchoResponse>(
22
+ 'POST',
23
+ `${baseUrl}/v1/echo/${runId}`,
24
+ { body: valid }
25
+ );
26
+ yield* askSmokeAssert(
27
+ ok.status === 200,
28
+ `echo returned [${ok.status}] for a valid body`
29
+ );
30
+ yield* askSmokeAssert(
31
+ ok.data.pathValue === runId && ok.data.bodyValue === runId,
32
+ `echo returned [${JSON.stringify(ok.data)}], expected both values to be [${runId}]`
33
+ );
34
+
35
+ // An empty bodyValue fails the min(1) on the schema; the route must 422, not 200
36
+ const invalid = yield* askNetworkRequest<EchoRequest, unknown>(
37
+ 'POST',
38
+ `${baseUrl}/v1/echo/${runId}`,
39
+ { body: { bodyValue: '' } }
40
+ );
41
+ yield* askSmokeAssert(
42
+ invalid.status === 422,
43
+ `echo returned [${invalid.status}] for an invalid body, expected 422`
44
+ );
45
+ }
@@ -0,0 +1,42 @@
1
+ import { askOpenApiGetDocument, AskResponse } from 'quidproquo';
2
+
3
+ import { askSmokeAssert } from '../askSmokeAssert';
4
+
5
+ // The generated document, built in-process from the deployed config. Proves the
6
+ // openApi processor is registered on this runtime and that the route schemas
7
+ // survived the trip from config into the bundle. No network involved.
8
+ export function* askRunOpenApiDocumentTest(): AskResponse<void> {
9
+ const document = yield* askOpenApiGetDocument();
10
+
11
+ const echo = document.paths['/v1/echo/{pathValue}']?.post;
12
+ yield* askSmokeAssert(!!echo, 'echo route is missing from the document');
13
+ yield* askSmokeAssert(
14
+ !!echo?.requestBody?.content['application/json'].schema.properties,
15
+ 'echo route has no request body schema'
16
+ );
17
+ yield* askSmokeAssert(
18
+ echo?.parameters?.some((p) => p.in === 'path' && p.name === 'pathValue') ===
19
+ true,
20
+ 'echo route has no pathValue path parameter'
21
+ );
22
+ yield* askSmokeAssert(
23
+ !!echo?.responses['422'],
24
+ 'echo route does not document its 422'
25
+ );
26
+
27
+ const smokeRun = document.paths['/smoke/run/{runId}']?.get;
28
+ yield* askSmokeAssert(
29
+ !!smokeRun?.responses['200'].content?.['application/json'].schema,
30
+ 'smoke run route has no response schema'
31
+ );
32
+
33
+ yield* askSmokeAssert(
34
+ !document.paths['/v1/docs'] && !document.paths['/v1/docs/openapi.json'],
35
+ 'docs routes should be hidden from the document'
36
+ );
37
+
38
+ yield* askSmokeAssert(
39
+ document.servers.length > 0,
40
+ 'document lists no servers'
41
+ );
42
+ }
@@ -0,0 +1,2 @@
1
+ export * from './askRunEchoRoundTripTest';
2
+ export * from './askRunOpenApiDocumentTest';
@@ -1,16 +1,22 @@
1
1
  import { askRunCrossServiceKeyValueStoreTest } from './crossService/askRunCrossServiceKeyValueStoreTest';
2
2
  import { askRunCrossServiceStorageDriveTest } from './crossService/askRunCrossServiceStorageDriveTest';
3
3
  import { askRunEventBusTest } from './eventBus/askRunEventBusTest';
4
+ import { askRunEventDocConcurrentAppendTest } from './eventDoc/askRunEventDocConcurrentAppendTest';
5
+ import { askRunEventDocInterleavedBatchAppendTest } from './eventDoc/askRunEventDocInterleavedBatchAppendTest';
4
6
  import { askRunKeyValueStoreTest } from './keyValueStore/askRunKeyValueStoreTest';
5
7
  import { askRunNoopTest } from './noop/askRunNoopTest';
8
+ import { askRunEchoRoundTripTest } from './openApi/askRunEchoRoundTripTest';
9
+ import { askRunOpenApiDocumentTest } from './openApi/askRunOpenApiDocumentTest';
6
10
  import { askRunParameterTest } from './parameter/askRunParameterTest';
7
11
  import { askRunScheduleTest } from './schedule/askRunScheduleTest';
8
12
  import { askRunSecretTest } from './secret/askRunSecretTest';
9
13
  import { askRunStorageDriveTest } from './storageDrive/askRunStorageDriveTest';
10
14
  import { SmokeTestDefinition } from './SmokeTestDefinition';
11
15
 
12
- // Every smoke test a run executes, in order. A test's id in the run record is
13
- // its 1-based position here, so append rather than reorder where possible.
16
+ // Every smoke test a run executes. They run in parallel, one queue message
17
+ // each, so nothing here may depend on another test having finished. A test's
18
+ // id in the run record is its 1-based position here, so append rather than
19
+ // reorder where possible; `name` is the key the queue message carries.
14
20
  // Adding a test: one folder with its askRun<Name>Test story, one line here.
15
21
  export const smokeTestRegistry: SmokeTestDefinition[] = [
16
22
  { name: 'noop', askRun: askRunNoopTest },
@@ -28,4 +34,14 @@ export const smokeTestRegistry: SmokeTestDefinition[] = [
28
34
  askRun: askRunCrossServiceStorageDriveTest,
29
35
  },
30
36
  { name: 'schedule', askRun: askRunScheduleTest },
37
+ { name: 'openApiDocument', askRun: askRunOpenApiDocumentTest },
38
+ { name: 'echoRoundTrip', askRun: askRunEchoRoundTripTest },
39
+ {
40
+ name: 'eventDocConcurrentAppend',
41
+ askRun: askRunEventDocConcurrentAppendTest,
42
+ },
43
+ {
44
+ name: 'eventDocInterleavedBatchAppend',
45
+ askRun: askRunEventDocInterleavedBatchAppendTest,
46
+ },
31
47
  ];
@@ -1,6 +1,5 @@
1
1
  {
2
2
  "prefix": "todo",
3
- "domain": "todo.quidproquojs.com",
4
3
  "environments": {
5
4
  "development": {
6
5
  "platform": "docker"
@@ -1 +1,2 @@
1
- export const TODO_DOMAIN = 'todo.quidproquojs.com';
1
+ // Root domains the app is served on, primary first.
2
+ export const TODO_DOMAINS = ['todo.quidproquojs.com'];
@@ -22,7 +22,7 @@ import {
22
22
 
23
23
  import { execSync } from 'child_process';
24
24
  import {
25
- TODO_DOMAIN,
25
+ TODO_DOMAINS,
26
26
  TODO_USER_DIRECTORY,
27
27
  TodoServiceEnum,
28
28
  todoServiceNames,
@@ -52,8 +52,7 @@ export const defineTodoService = (
52
52
  configRoot: string,
53
53
  apiBuildPath: string,
54
54
  apiLayers: ApiLayer[] = [],
55
- lambdaMemoryInMiB = 1024,
56
- domainName = TODO_DOMAIN
55
+ lambdaMemoryInMiB = 1024
57
56
  ): QPQConfig => [
58
57
  defineApplicationModule(
59
58
  modulePrefix,
@@ -66,7 +65,7 @@ export const defineTodoService = (
66
65
 
67
66
  defineApplicationVersion(`${getVersionTag()}-${new Date().toISOString()}`),
68
67
 
69
- defineDns(domainName),
68
+ defineDns(TODO_DOMAINS),
70
69
 
71
70
  defineAdminUserDirectory({
72
71
  owner: {
@@ -99,7 +98,7 @@ export const defineTodoService = (
99
98
  sharedSingletons: ['chakra', 'zod'],
100
99
  }),
101
100
 
102
- defineApi('api', domainName),
101
+ defineApi('api'),
103
102
  defineDefaultRouteOptions('api', {
104
103
  allowedOrigins: [
105
104
  'http://localhost:3080',
@@ -128,7 +127,7 @@ export const defineTodoService = (
128
127
  }
129
128
  ),
130
129
 
131
- defineAdminSettings(TodoServiceEnum.Admin, TODO_DOMAIN, {
130
+ defineAdminSettings(TodoServiceEnum.Admin, {
132
131
  services: todoServiceNames,
133
132
  coldStorageAfterDays: 90,
134
133
  }),
@@ -6,7 +6,7 @@ import {
6
6
  import { defineDevServerOptions } from 'quidproquo-dev-server/config';
7
7
  import { defineDynamicRoutes } from 'quidproquo-features';
8
8
 
9
- import { TODO_DOMAIN, TodoServiceEnum } from '@todo/constants';
9
+ import { TodoServiceEnum } from '@todo/constants';
10
10
  import { defineTodoService } from '@todo/service-utils';
11
11
 
12
12
  import * as dynamicRoutes from './entry/controller';
@@ -23,7 +23,6 @@ const webEntryOptions: QPQConfigAdvancedWebEntrySettings = {
23
23
  cacheSettingsName: 'default',
24
24
  domain: {
25
25
  onRootDomain: true,
26
- rootDomain: TODO_DOMAIN,
27
26
  },
28
27
  storageDrive: {
29
28
  sourceStorageDrive: websiteWebEntryStorageDriveName,
@@ -40,7 +39,6 @@ const mfeHostWebEntryOptions: QPQConfigAdvancedWebEntrySettings = {
40
39
  domain: {
41
40
  subDomainName: 'views',
42
41
  onRootDomain: true,
43
- rootDomain: TODO_DOMAIN,
44
42
  },
45
43
  storageDrive: {
46
44
  sourceStorageDrive: mfeHostWebEntryStorageDriveName,
@@ -7,7 +7,7 @@ description: Upsert a record into a key-value store, retrying automatically on t
7
7
 
8
8
  Writes a record with [askKeyValueStoreUpsert](./ask-key-value-store-upsert.md), automatically retrying when the store is temporarily unavailable. Use it for writes that must succeed through transient DynamoDB throttling rather than failing the story on the first hiccup.
9
9
 
10
- - **Built from:** wraps [askKeyValueStoreUpsert](./ask-key-value-store-upsert.md) in `askRetry`, retrying only on `KeyValueStoreUpsertErrorTypeEnum.ServiceUnavailable`. It is a helper story, not a distinct action.
10
+ - **Built from:** wraps [askKeyValueStoreUpsert](./ask-key-value-store-upsert.md) in `askRetry`, retrying on `KeyValueStoreUpsertErrorTypeEnum.ServiceUnavailable` and `KeyValueStoreUpsertErrorTypeEnum.WriteContention`. It is a helper story, not a distinct action.
11
11
 
12
12
  ```typescript
13
13
  import { askKeyValueStoreUpsertWithRetry } from 'quidproquo-core';
@@ -54,7 +54,7 @@ Extends `KeyValueStoreUpsertOptions` (`ttlInSeconds`, `ifNotExists`, `scope`) wi
54
54
 
55
55
  ## Notes
56
56
 
57
- - Only `ServiceUnavailable` (throttling / transient DynamoDB errors) is retried. A `Conflict` from an `ifNotExists` write is **not** retried — that's a genuine key collision, not a transient fault. If every attempt fails, the last error is re-thrown.
57
+ - `ServiceUnavailable` (throttling / transient DynamoDB errors) and `WriteContention` (another write momentarily holding the item) are retried. A `Conflict` from an `ifNotExists` write is **not** retried — that's a genuine key collision, not a transient fault. If every attempt fails, the last error is re-thrown.
58
58
  - Errors that escape the retries surface the same `KeyValueStoreUpsertErrorTypeEnum` members as the plain upsert; catch them with `askCatch`.
59
59
 
60
60
  ## Related
@@ -59,7 +59,8 @@ function* askKeyValueStoreUpsert<KvsItem>(
59
59
  | --- | --- |
60
60
  | `KeyValueStoreUpsertErrorTypeEnum.ServiceUnavailable` | DynamoDB internal error or throttling. |
61
61
  | `KeyValueStoreUpsertErrorTypeEnum.ResourceNotFound` | The underlying table does not exist. |
62
- | `KeyValueStoreUpsertErrorTypeEnum.Conflict` | A conditional (`ifNotExists`) write lost to an existing item. Namespaced — not the generic `ErrorTypeEnum.Conflict` — so retry logic can target the write race specifically without also catching domain-level conflicts. |
62
+ | `KeyValueStoreUpsertErrorTypeEnum.Conflict` | A conditional (`ifNotExists`) write lost to an existing item — final, the slot is taken. Namespaced — not the generic `ErrorTypeEnum.Conflict` — so retry logic can target the write race specifically without also catching domain-level conflicts. |
63
+ | `KeyValueStoreUpsertErrorTypeEnum.WriteContention` | A conditional (`ifNotExists`) write lost to another write (a transaction) holding the item right now — transient, the item may still be absent once it settles. |
63
64
  | `KeyValueStoreUpsertErrorTypeEnum.InvalidScope` | The `scope` option is malformed (empty, `.`, over 128 characters, or containing path separators, `..`, `@`, or null bytes), the store's partition key is not string-typed, or the partition-key value contains the reserved `@@QPQSCOPE@@` delimiter (reserved on string-pk stores, so unscoped calls reject it too). |
64
65
  | `KeyValueStoreUpsertErrorTypeEnum.StoreNotFound` | The key value store is not declared in the qpq config (misconfiguration, e.g. a wrong name or a missing `defineKeyValueStore`). |
65
66
 
@@ -54,7 +54,7 @@ function* askEventDocAppendServerEvent<T>(
54
54
 
55
55
  ## Notes
56
56
 
57
- - This is a thin envelope-building wrapper over [askEventDocEventAppend](./ask-event-doc-event-append.md), so the same caveat applies: the event is written unconditionally, and version/lifecycle validation is decided later at fold time, not here. A server event that a validator would reject is written but silently skipped by every fold.
57
+ - This is a thin envelope-building wrapper over [askEventDocEventAppend](./ask-event-doc-event-append.md), called with `{ validate: false }`: it still claims the next contiguous `eventId` with a conditional write and retries under slot contention, but it skips the pre-write state resolve and `validateEvent` check, trusting server code to author valid events. Version/lifecycle validation is decided later at fold time instead a server event that a validator would reject is written but silently skipped by every fold.
58
58
  - A fresh `clientMessageId` is generated on every call, so this path does not participate in client retry dedup — each call is a distinct intended event.
59
59
 
60
60
  ## Related
@@ -82,7 +82,7 @@ function* askEventDocSeedInitState(
82
82
  | `name` | `string` | The document's name. |
83
83
  | `actor` | `EventDocEventActor` | Who is creating the document. |
84
84
 
85
- **Returns** `EventDocEvent` — the written `INIT_STATE` event, with a freshly minted sortable `payload.metadata.eventId` and `version === 1`.
85
+ **Returns** `EventDocEvent` — the written `INIT_STATE` event, with `payload.metadata.eventId === 0` (the log's opening position) and `version === 1`.
86
86
 
87
87
  Use `askEventDocCreate` unless you are building a custom create flow that needs the raw event; `askEventDocSeedInitState` alone writes the log but does **not** derive or persist the summary record.
88
88