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
@@ -9,11 +9,22 @@ import {
9
9
  QPQConfig,
10
10
  QpqFunctionRuntime,
11
11
  } from 'quidproquo';
12
+ import { defineEventDoc } from 'quidproquo-features';
12
13
 
14
+ import { z } from 'zod/v4';
13
15
  import { SMOKE_PROBE_DRIVE, SMOKE_PROBE_STORE } from '@qpqjs/constants';
14
- import { SmokeProbeRecord } from '@qpqjs/test-models';
16
+ import {
17
+ SmokeProbeRecord,
18
+ SmokeRunStartedSchema,
19
+ SmokeRunWithSummarySchema,
20
+ } from '@qpqjs/test-models';
15
21
 
16
22
  import { SMOKE_RUNS_STORE } from '../constants/SMOKE_RUNS_STORE';
23
+ import {
24
+ SMOKE_EVENT_DOC_APPEND_MESSAGE_TYPE,
25
+ SMOKE_EVENT_DOC_APPEND_QUEUE,
26
+ SMOKE_EVENT_DOC_BASE_PATH,
27
+ } from '../constants/smokeEventDoc';
17
28
  import {
18
29
  SMOKE_PROBE_EVENT_BUS,
19
30
  SMOKE_PROBE_EVENT_QUEUE,
@@ -24,8 +35,9 @@ import {
24
35
  } from '../constants/smokeProbe';
25
36
  import {
26
37
  SMOKE_RUN_QUEUE,
27
- SMOKE_RUN_REQUESTED_MESSAGE_TYPE,
38
+ SMOKE_TEST_REQUESTED_MESSAGE_TYPE,
28
39
  } from '../constants/smokeRunQueue';
40
+ import { smokeProbeDocDefinition } from '../eventDoc/smokeProbeDocDefinition';
29
41
 
30
42
  // Everything the smoke feature needs: the run store, the queue runs execute
31
43
  // on, and the two routes. Runtimes are located relative to this file, so the
@@ -46,15 +58,17 @@ export const defineSmoke = (): QPQConfig => {
46
58
  ];
47
59
 
48
60
  return [
49
- // One record per run, keyed by runId; the queue entry updates it as tests
50
- // complete and the GET route polls it.
61
+ // One record per run, keyed by runId; the queue workers each write their
62
+ // own test's entry as it completes and the GET route polls it.
51
63
  defineKeyValueStore(SMOKE_RUNS_STORE, 'runId'),
52
64
 
65
+ // One message per test per run. Not FIFO and no concurrency cap, so the
66
+ // tests of a run fan out and execute in parallel.
53
67
  defineQueue(SMOKE_RUN_QUEUE, {
54
- [SMOKE_RUN_REQUESTED_MESSAGE_TYPE]: {
68
+ [SMOKE_TEST_REQUESTED_MESSAGE_TYPE]: {
55
69
  basePath: __dirname,
56
- relativePath: '../queue/onSmokeRunRequested',
57
- functionName: 'onSmokeRunRequested',
70
+ relativePath: '../queue/onSmokeTestRequested',
71
+ functionName: 'onSmokeTestRequested',
58
72
  },
59
73
  }),
60
74
 
@@ -85,18 +99,69 @@ export const defineSmoke = (): QPQConfig => {
85
99
  { eventBusSubscriptions: [SMOKE_PROBE_EVENT_BUS] }
86
100
  ),
87
101
 
88
- defineRoute('POST', '/smoke/run', {
89
- basePath: __dirname,
90
- relativePath: '../controller/askRunSmokeTests',
91
- functionName: 'askRunSmokeTests',
92
- actionProcessors: githubOidcAuth,
102
+ // The event-doc probe collection: a full registered collection (stores, stream
103
+ // projector, dynamic functions, routes) so the smoke tests exercise event docs the
104
+ // way a real service does. NOTE the routes mount with no auth - the test service
105
+ // has no user directory - so the collection is world-writable; it holds nothing
106
+ // but throwaway probe docs. The queue fans a test's writers out one invocation
107
+ // each, so their appends race across lambdas.
108
+ defineEventDoc(
109
+ smokeProbeDocDefinition,
110
+ {
111
+ basePath: __dirname,
112
+ relativePath: '../eventDoc/smokeProbeDocDefinition',
113
+ functionName: 'smokeProbeDocDefinition',
114
+ },
115
+ { basePath: SMOKE_EVENT_DOC_BASE_PATH }
116
+ ),
117
+ defineQueue(SMOKE_EVENT_DOC_APPEND_QUEUE, {
118
+ [SMOKE_EVENT_DOC_APPEND_MESSAGE_TYPE]: {
119
+ basePath: __dirname,
120
+ relativePath: '../queue/onSmokeEventDocAppend',
121
+ functionName: 'onSmokeEventDocAppend',
122
+ },
93
123
  }),
94
124
 
95
- defineRoute('GET', '/smoke/run/{runId}', {
96
- basePath: __dirname,
97
- relativePath: '../controller/askGetSmokeRun',
98
- functionName: 'askGetSmokeRun',
99
- actionProcessors: githubOidcAuth,
100
- }),
125
+ // The zod models double as the routes' published contract, flattened to
126
+ // JSON Schema here for the OpenAPI document at /v1/docs.
127
+ defineRoute(
128
+ 'POST',
129
+ '/smoke/run',
130
+ {
131
+ basePath: __dirname,
132
+ relativePath: '../controller/askRunSmokeTests',
133
+ functionName: 'askRunSmokeTests',
134
+ actionProcessors: githubOidcAuth,
135
+ },
136
+ {
137
+ schema: {
138
+ summary: 'Start a smoke run',
139
+ description:
140
+ 'Queues every registered smoke test and returns the run id to poll. Requires a GitHub Actions OIDC token for this repo.',
141
+ tags: ['smoke'],
142
+ responseJsonSchema: z.toJSONSchema(SmokeRunStartedSchema),
143
+ },
144
+ }
145
+ ),
146
+
147
+ defineRoute(
148
+ 'GET',
149
+ '/smoke/run/{runId}',
150
+ {
151
+ basePath: __dirname,
152
+ relativePath: '../controller/askGetSmokeRun',
153
+ functionName: 'askGetSmokeRun',
154
+ actionProcessors: githubOidcAuth,
155
+ },
156
+ {
157
+ schema: {
158
+ summary: 'Get a smoke run',
159
+ description:
160
+ 'The stored run plus pass/fail counts. Poll until status leaves running.',
161
+ tags: ['smoke'],
162
+ responseJsonSchema: z.toJSONSchema(SmokeRunWithSummarySchema),
163
+ },
164
+ }
165
+ ),
101
166
  ];
102
167
  };
@@ -1,4 +1,5 @@
1
1
  export * from './githubOidc';
2
2
  export * from './SMOKE_RUNS_STORE';
3
+ export * from './smokeEventDoc';
3
4
  export * from './smokeProbe';
4
5
  export * from './smokeRunQueue';
@@ -0,0 +1,27 @@
1
+ import { EventDocEventActor, EventDocStoreOptions } from 'quidproquo-features';
2
+
3
+ // The throwaway event-doc collection the smoke tests write to, registered in full via
4
+ // defineEventDoc (see ../eventDoc/smokeProbeDocDefinition): stores, stream projector,
5
+ // dynamic functions and routes, exactly as a real service's collection.
6
+ export const SMOKE_EVENT_DOC_STORE = 'smokeEventDocs';
7
+ export const SMOKE_EVENT_DOC_TYPE = 'smokeProbeDoc';
8
+ export const SMOKE_EVENT_DOC_BASE_PATH = '/smoke/probe-docs' as const;
9
+
10
+ export const SMOKE_EVENT_DOC_STORE_OPTIONS: EventDocStoreOptions = {
11
+ storeName: SMOKE_EVENT_DOC_STORE,
12
+ type: SMOKE_EVENT_DOC_TYPE,
13
+ };
14
+
15
+ // The queue the tests fan their writers out on: one message per writer, each landing in
16
+ // its own invocation, so the appends race across lambdas rather than inside one.
17
+ export const SMOKE_EVENT_DOC_APPEND_QUEUE = 'smokeEventDocAppends';
18
+ export const SMOKE_EVENT_DOC_APPEND_MESSAGE_TYPE = 'smokeEventDocAppend';
19
+
20
+ // The one event type the probe docs ever carry; its data is a SmokeEventDocMark.
21
+ export const SMOKE_EVENT_DOC_MARK_EVENT = 'SMOKE_MARK';
22
+ export const SMOKE_EVENT_DOC_SCHEMA_VERSION = 1;
23
+
24
+ export const SMOKE_EVENT_DOC_ACTOR: EventDocEventActor = {
25
+ userId: 'smoke',
26
+ userDisplayName: 'Smoke run',
27
+ };
@@ -1,6 +1,6 @@
1
1
  // The queue a smoke run is executed from, and the one message type on it.
2
- // POST /smoke/run creates the run record and sends this message; the queue
3
- // entry picks it up and runs the registered tests, so the request returns at
4
- // once and polling shows real progress.
2
+ // POST /smoke/run creates the run record and sends one message per registered
3
+ // test; the queue entry runs that single test, so the tests execute in
4
+ // parallel, the request returns at once, and polling shows real progress.
5
5
  export const SMOKE_RUN_QUEUE = 'smokeRuns';
6
- export const SMOKE_RUN_REQUESTED_MESSAGE_TYPE = 'smokeRunRequested';
6
+ export const SMOKE_TEST_REQUESTED_MESSAGE_TYPE = 'smokeTestRequested';
@@ -5,6 +5,8 @@ import {
5
5
  qpqWebServerUtils,
6
6
  } from 'quidproquo';
7
7
 
8
+ import { SmokeRunStarted } from '@qpqjs/test-models';
9
+
8
10
  import { askStartSmokeRun } from '../logic/smokeRun/askStartSmokeRun';
9
11
 
10
12
  // POST /smoke/run. Auth happened in the route preamble via the GitHub OIDC
@@ -15,5 +17,7 @@ export function* askRunSmokeTests(
15
17
  ): AskResponse<HTTPEventResponse> {
16
18
  const smokeRun = yield* askStartSmokeRun();
17
19
 
18
- return qpqWebServerUtils.toJsonEventResponse({ runId: smokeRun.runId });
20
+ const started: SmokeRunStarted = { runId: smokeRun.runId };
21
+
22
+ return qpqWebServerUtils.toJsonEventResponse(started);
19
23
  }
@@ -0,0 +1,32 @@
1
+ import {
2
+ askKeyValueStoreUpdate,
3
+ AskResponse,
4
+ KvsAdvancedDataType,
5
+ kvsSet,
6
+ kvsUpdate,
7
+ } from 'quidproquo';
8
+
9
+ import { SmokeRun, SmokeTestResult } from '@qpqjs/test-models';
10
+
11
+ import { SMOKE_RUNS_STORE } from '../constants/SMOKE_RUNS_STORE';
12
+
13
+ // Writes one test's entry into the run record, touching nothing else, so the
14
+ // tests running in parallel cannot clobber each other the way a whole-record
15
+ // upsert would. Returns the record as it stands after this write: updates to
16
+ // one item are serialized by the store, so the returned `tests` include every
17
+ // result recorded before this one.
18
+ export function* askRecordSmokeTestResult(
19
+ runId: string,
20
+ testIndex: number,
21
+ result: SmokeTestResult
22
+ ): AskResponse<SmokeRun> {
23
+ // A completed entry carries no nulls, but the KVS value type cannot express
24
+ // that the model's Nullable timestamps are filled in by now.
25
+ const value = result as unknown as KvsAdvancedDataType;
26
+
27
+ return yield* askKeyValueStoreUpdate<SmokeRun>(
28
+ SMOKE_RUNS_STORE,
29
+ kvsUpdate([kvsSet(['tests', testIndex], value)]),
30
+ runId
31
+ );
32
+ }
@@ -0,0 +1,24 @@
1
+ import {
2
+ askKeyValueStoreUpdate,
3
+ AskResponse,
4
+ kvsSet,
5
+ kvsUpdate,
6
+ } from 'quidproquo';
7
+
8
+ import { SmokeRun, SmokeRunStatus } from '@qpqjs/test-models';
9
+
10
+ import { SMOKE_RUNS_STORE } from '../constants/SMOKE_RUNS_STORE';
11
+
12
+ // Stamps the run's terminal status. Only the status and finish time are
13
+ // written, so it cannot undo a test result that landed in the meantime.
14
+ export function* askSetSmokeRunOutcome(
15
+ runId: string,
16
+ status: SmokeRunStatus,
17
+ finishedAt: string
18
+ ): AskResponse<SmokeRun> {
19
+ return yield* askKeyValueStoreUpdate<SmokeRun>(
20
+ SMOKE_RUNS_STORE,
21
+ kvsUpdate([kvsSet('status', status), kvsSet('finishedAt', finishedAt)]),
22
+ runId
23
+ );
24
+ }
@@ -1,2 +1,4 @@
1
1
  export * from './askGetSmokeRunById';
2
+ export * from './askRecordSmokeTestResult';
2
3
  export * from './askSaveSmokeRun';
4
+ export * from './askSetSmokeRunOutcome';
@@ -0,0 +1 @@
1
+ export * from './smokeProbeDocDefinition';
@@ -0,0 +1,74 @@
1
+ import { AskResponse, Effect, QpqReducer } from 'quidproquo';
2
+ import {
3
+ askApplyEventDocEvent,
4
+ buildEventDocFoldReducer,
5
+ createEventDocDefinition,
6
+ createEventDocInitialDocumentState,
7
+ EventDocDocument,
8
+ EventDocEvent,
9
+ EventDocEventPayload,
10
+ } from 'quidproquo-features';
11
+
12
+ import {
13
+ SMOKE_EVENT_DOC_MARK_EVENT,
14
+ SMOKE_EVENT_DOC_STORE,
15
+ SMOKE_EVENT_DOC_TYPE,
16
+ } from '../constants/smokeEventDoc';
17
+ import { SmokeEventDocMark } from '../models/SmokeEventDocMark';
18
+
19
+ // How many numbers the document keeps: a rolling window, so the fold is a real
20
+ // reducer (it drops as well as adds) rather than an append.
21
+ export const SMOKE_PROBE_DOC_WINDOW = 100;
22
+
23
+ // The probe doc type the event-doc smoke tests write to: one event (a mark carrying a
24
+ // number) folded into the last SMOKE_PROBE_DOC_WINDOW numbers, in log order. Small on
25
+ // purpose - it exists so the deployed runtime exercises a REGISTERED collection end to
26
+ // end: the append gate, the stream projector's snapshot fold, and the state reads that
27
+ // resolve through the dynamic-functions registration all run this definition.
28
+ export type SmokeProbeDocState = EventDocDocument & {
29
+ numbers: number[];
30
+ };
31
+
32
+ const createInitialSmokeProbeDocState = (): SmokeProbeDocState => ({
33
+ ...createEventDocInitialDocumentState(1),
34
+ numbers: [],
35
+ });
36
+
37
+ type SmokeProbeDocEffects = Effect<
38
+ typeof SMOKE_EVENT_DOC_MARK_EVENT,
39
+ EventDocEventPayload<SmokeEventDocMark>
40
+ >;
41
+
42
+ const smokeProbeDocFoldReducer = buildEventDocFoldReducer<
43
+ SmokeProbeDocState,
44
+ SmokeProbeDocEffects
45
+ >(createInitialSmokeProbeDocState, {
46
+ [SMOKE_EVENT_DOC_MARK_EVENT]: (state, payload) => ({
47
+ ...state,
48
+ numbers: [...state.numbers, payload.data.value].slice(
49
+ -SMOKE_PROBE_DOC_WINDOW
50
+ ),
51
+ }),
52
+ }) as QpqReducer<SmokeProbeDocState, EventDocEvent>;
53
+
54
+ function* askSmokeProbeDocMark(mark: SmokeEventDocMark): AskResponse<void> {
55
+ yield* askApplyEventDocEvent(SMOKE_EVENT_DOC_MARK_EVENT, mark);
56
+ }
57
+
58
+ export const smokeProbeDocDefinition = createEventDocDefinition({
59
+ storeName: SMOKE_EVENT_DOC_STORE,
60
+ type: SMOKE_EVENT_DOC_TYPE,
61
+ schemaVersion: 1,
62
+ versions: [
63
+ {
64
+ version: 1,
65
+ views: {
66
+ document: {
67
+ foldReducer: smokeProbeDocFoldReducer,
68
+ createInitialViewState: createInitialSmokeProbeDocState,
69
+ },
70
+ },
71
+ },
72
+ ],
73
+ api: { askSmokeProbeDocMark },
74
+ });
@@ -2,6 +2,7 @@ export * from './config';
2
2
  export * from './constants';
3
3
  export * from './controller';
4
4
  export * from './data';
5
+ export * from './eventDoc';
5
6
  export * from './logic';
6
7
  export * from './models';
7
8
  export * from './processors';
@@ -0,0 +1,97 @@
1
+ import {
2
+ askCatch,
3
+ askDateNow,
4
+ AskResponse,
5
+ askThrowError,
6
+ ErrorTypeEnum,
7
+ } from 'quidproquo';
8
+
9
+ import {
10
+ SmokeRun,
11
+ SmokeRunStatus,
12
+ SmokeTestResult,
13
+ SmokeTestStatus,
14
+ } from '@qpqjs/test-models';
15
+
16
+ import { askGetSmokeRunById } from '../../data/askGetSmokeRunById';
17
+ import { askRecordSmokeTestResult } from '../../data/askRecordSmokeTestResult';
18
+ import { askSetSmokeRunOutcome } from '../../data/askSetSmokeRunOutcome';
19
+ import { SmokeTestDefinition } from '../../tests/SmokeTestDefinition';
20
+ import { smokeTestRegistry } from '../../tests/smokeTestRegistry';
21
+
22
+ // Runs one registered test, returning its completed result entry.
23
+ function* askRunSmokeTest(
24
+ test: SmokeTestDefinition,
25
+ pending: SmokeTestResult,
26
+ runId: string
27
+ ): AskResponse<SmokeTestResult> {
28
+ const startedAt = yield* askDateNow();
29
+ const outcome = yield* askCatch(test.askRun(runId));
30
+ const finishedAt = yield* askDateNow();
31
+
32
+ return {
33
+ ...pending,
34
+ status: outcome.success ? SmokeTestStatus.passed : SmokeTestStatus.failed,
35
+ message: outcome.success ? 'passed' : outcome.error.errorText,
36
+ startedAt,
37
+ finishedAt,
38
+ };
39
+ }
40
+
41
+ const isTerminal = (result: SmokeTestResult): boolean =>
42
+ result.status === SmokeTestStatus.passed ||
43
+ result.status === SmokeTestStatus.failed;
44
+
45
+ // Executes one registered test for an existing run (created by
46
+ // askStartSmokeRun) and records its result. Every test of a run executes
47
+ // concurrently through its own queue message, so nobody owns the run's
48
+ // finish: the write of each result returns the record as it stands, and
49
+ // because writes to one record serialize, exactly one worker (whichever
50
+ // lands last) sees every test terminal. That worker stamps the run's outcome.
51
+ export function* askExecuteSmokeTest(
52
+ runId: string,
53
+ testName: string
54
+ ): AskResponse<SmokeRun> {
55
+ const test = smokeTestRegistry.find((t) => t.name === testName);
56
+ if (!test) {
57
+ return yield* askThrowError(
58
+ ErrorTypeEnum.NotFound,
59
+ `no smoke test [${testName}] registered`
60
+ );
61
+ }
62
+
63
+ const smokeRun = yield* askGetSmokeRunById(runId);
64
+ if (!smokeRun) {
65
+ return yield* askThrowError(
66
+ ErrorTypeEnum.NotFound,
67
+ `no smoke run [${runId}] to execute`
68
+ );
69
+ }
70
+
71
+ const testIndex = smokeRun.tests.findIndex((t) => t.name === testName);
72
+ if (testIndex < 0) {
73
+ return yield* askThrowError(
74
+ ErrorTypeEnum.NotFound,
75
+ `smoke run [${runId}] has no entry for test [${testName}]`
76
+ );
77
+ }
78
+
79
+ const result = yield* askRunSmokeTest(test, smokeRun.tests[testIndex], runId);
80
+
81
+ const recorded = yield* askRecordSmokeTestResult(runId, testIndex, result);
82
+
83
+ if (!recorded.tests.every(isTerminal)) {
84
+ return recorded;
85
+ }
86
+
87
+ const finishedAt = yield* askDateNow();
88
+ const anyFailed = recorded.tests.some(
89
+ (t) => t.status !== SmokeTestStatus.passed
90
+ );
91
+
92
+ return yield* askSetSmokeRunOutcome(
93
+ runId,
94
+ anyFailed ? SmokeRunStatus.failed : SmokeRunStatus.passed,
95
+ finishedAt
96
+ );
97
+ }
@@ -15,10 +15,10 @@ import {
15
15
 
16
16
  import {
17
17
  SMOKE_RUN_QUEUE,
18
- SMOKE_RUN_REQUESTED_MESSAGE_TYPE,
18
+ SMOKE_TEST_REQUESTED_MESSAGE_TYPE,
19
19
  } from '../../constants/smokeRunQueue';
20
20
  import { askSaveSmokeRun } from '../../data/askSaveSmokeRun';
21
- import { SmokeRunRequestedPayload } from '../../models/SmokeRunRequestedQueueEvent';
21
+ import { SmokeTestRequestedPayload } from '../../models/SmokeTestRequestedQueueEvent';
22
22
  import { SmokeTestDefinition } from '../../tests/SmokeTestDefinition';
23
23
  import { smokeTestRegistry } from '../../tests/smokeTestRegistry';
24
24
 
@@ -34,9 +34,18 @@ const createPendingResult = (
34
34
  finishedAt: null,
35
35
  });
36
36
 
37
- // Creates the run record with every registered test pending, then hands the
38
- // run to the queue. The record exists before the message is sent so a poll
39
- // that races the queue still finds it.
37
+ const createTestRequestedMessage = (
38
+ runId: string,
39
+ test: SmokeTestDefinition
40
+ ): QueueMessage<SmokeTestRequestedPayload> => ({
41
+ type: SMOKE_TEST_REQUESTED_MESSAGE_TYPE,
42
+ payload: { runId, testName: test.name },
43
+ });
44
+
45
+ // Creates the run record with every registered test pending, then sends one
46
+ // queue message per test so they execute in parallel. The record exists
47
+ // before the messages are sent so a poll (or a worker) that races the queue
48
+ // still finds it.
40
49
  export function* askStartSmokeRun(): AskResponse<SmokeRun> {
41
50
  const runId = yield* askNewGuid();
42
51
  const startedAt = yield* askDateNow();
@@ -51,12 +60,11 @@ export function* askStartSmokeRun(): AskResponse<SmokeRun> {
51
60
 
52
61
  yield* askSaveSmokeRun(smokeRun);
53
62
 
54
- const message: QueueMessage<SmokeRunRequestedPayload> = {
55
- type: SMOKE_RUN_REQUESTED_MESSAGE_TYPE,
56
- payload: { runId },
57
- };
63
+ const messages = smokeTestRegistry.map((test) =>
64
+ createTestRequestedMessage(runId, test)
65
+ );
58
66
 
59
- yield* askQueueSendMessages(SMOKE_RUN_QUEUE, message);
67
+ yield* askQueueSendMessages(SMOKE_RUN_QUEUE, ...messages);
60
68
 
61
69
  return smokeRun;
62
70
  }
@@ -1,4 +1,4 @@
1
- export * from './askExecuteSmokeRun';
1
+ export * from './askExecuteSmokeTest';
2
2
  export * from './askFindSmokeRun';
3
3
  export * from './askStartSmokeRun';
4
4
  export * from './summarizeSmokeRun';
@@ -0,0 +1,15 @@
1
+ import { QueueEvent, QueueMessage } from 'quidproquo';
2
+
3
+ // One writer's instruction: append one mark per value to `docId` for `runId`. A single
4
+ // value goes through the single server append, several through the batch append, so
5
+ // the tests exercise both paths against the same log.
6
+ export type SmokeEventDocAppendPayload = {
7
+ docId: string;
8
+ runId: string;
9
+ writerId: number;
10
+ values: number[];
11
+ };
12
+
13
+ export type SmokeEventDocAppendQueueEvent = QueueEvent<
14
+ QueueMessage<SmokeEventDocAppendPayload>
15
+ >;
@@ -0,0 +1,8 @@
1
+ // The data on every event a smoke writer appends: the number it contributes to the
2
+ // document, and which run and writer produced it. The document folds the numbers;
3
+ // the tests read them back to prove every writer's every event landed exactly once.
4
+ export type SmokeEventDocMark = {
5
+ value: number;
6
+ runId: string;
7
+ writerId: number;
8
+ };
@@ -0,0 +1,12 @@
1
+ import { QueueEvent, QueueMessage } from 'quidproquo';
2
+
3
+ // One registered test of one run. `testName` is the registry's stable key,
4
+ // never the positional id.
5
+ export type SmokeTestRequestedPayload = {
6
+ runId: string;
7
+ testName: string;
8
+ };
9
+
10
+ export type SmokeTestRequestedQueueEvent = QueueEvent<
11
+ QueueMessage<SmokeTestRequestedPayload>
12
+ >;
@@ -1,3 +1,5 @@
1
1
  export * from './GithubOidcClaims';
2
+ export * from './SmokeEventDocAppendQueueEvent';
3
+ export * from './SmokeEventDocMark';
2
4
  export * from './SmokeProbeEventQueueEvent';
3
- export * from './SmokeRunRequestedQueueEvent';
5
+ export * from './SmokeTestRequestedQueueEvent';
@@ -1,2 +1,3 @@
1
+ export * from './onSmokeEventDocAppend';
1
2
  export * from './onSmokeProbeEvent';
2
- export * from './onSmokeRunRequested';
3
+ export * from './onSmokeTestRequested';
@@ -0,0 +1,64 @@
1
+ import { AskResponse, QueueEventResponse } from 'quidproquo';
2
+ import {
3
+ askEventDocAppendServerEvent,
4
+ askEventDocAppendServerEvents,
5
+ askEventDocProvideStore,
6
+ EventDocServerEventInput,
7
+ } from 'quidproquo-features';
8
+
9
+ import {
10
+ SMOKE_EVENT_DOC_ACTOR,
11
+ SMOKE_EVENT_DOC_MARK_EVENT,
12
+ SMOKE_EVENT_DOC_SCHEMA_VERSION,
13
+ SMOKE_EVENT_DOC_STORE_OPTIONS,
14
+ } from '../constants/smokeEventDoc';
15
+ import { SmokeEventDocAppendQueueEvent } from '../models/SmokeEventDocAppendQueueEvent';
16
+ import { SmokeEventDocMark } from '../models/SmokeEventDocMark';
17
+
18
+ // One writer in the event-doc concurrency tests. Every message is its own invocation,
19
+ // so the writers of a test race each other for the log's next slots for real - the
20
+ // conditional-write loop in the append stories is what has to keep them contiguous.
21
+ function* askAppendMarks(
22
+ docId: string,
23
+ runId: string,
24
+ writerId: number,
25
+ values: number[]
26
+ ): AskResponse<void> {
27
+ const mark = (value: number): SmokeEventDocMark => ({
28
+ value,
29
+ runId,
30
+ writerId,
31
+ });
32
+
33
+ if (values.length === 1) {
34
+ yield* askEventDocAppendServerEvent(
35
+ docId,
36
+ SMOKE_EVENT_DOC_MARK_EVENT,
37
+ mark(values[0]),
38
+ SMOKE_EVENT_DOC_SCHEMA_VERSION,
39
+ SMOKE_EVENT_DOC_ACTOR
40
+ );
41
+ return;
42
+ }
43
+
44
+ const inputs: EventDocServerEventInput[] = values.map((value) => ({
45
+ type: SMOKE_EVENT_DOC_MARK_EVENT,
46
+ data: mark(value),
47
+ version: SMOKE_EVENT_DOC_SCHEMA_VERSION,
48
+ }));
49
+
50
+ yield* askEventDocAppendServerEvents(docId, inputs, SMOKE_EVENT_DOC_ACTOR);
51
+ }
52
+
53
+ export function* onSmokeEventDocAppend(
54
+ event: SmokeEventDocAppendQueueEvent
55
+ ): AskResponse<QueueEventResponse> {
56
+ const { docId, runId, writerId, values } = event.message.payload;
57
+
58
+ yield* askEventDocProvideStore(
59
+ SMOKE_EVENT_DOC_STORE_OPTIONS,
60
+ askAppendMarks(docId, runId, writerId, values)
61
+ );
62
+
63
+ return true;
64
+ }
@@ -0,0 +1,17 @@
1
+ import { AskResponse, QueueEventResponse } from 'quidproquo';
2
+
3
+ import { askExecuteSmokeTest } from '../logic/smokeRun/askExecuteSmokeTest';
4
+ import { SmokeTestRequestedQueueEvent } from '../models/SmokeTestRequestedQueueEvent';
5
+
6
+ // Queue entry for one test of a smoke run: executes it against the run record
7
+ // askStartSmokeRun created and records its result. A throw here (e.g. the
8
+ // record is missing) fails the message so the platform can redeliver it.
9
+ export function* onSmokeTestRequested(
10
+ event: SmokeTestRequestedQueueEvent
11
+ ): AskResponse<QueueEventResponse> {
12
+ const { runId, testName } = event.message.payload;
13
+
14
+ yield* askExecuteSmokeTest(runId, testName);
15
+
16
+ return true;
17
+ }
@@ -0,0 +1,18 @@
1
+ import { AskResponse } from 'quidproquo';
2
+ import { EventDocEvent } from 'quidproquo-features';
3
+
4
+ import { askSmokeAssert } from '../askSmokeAssert';
5
+
6
+ // The whole point of numeric event ids: an ascending list read of the log is exactly
7
+ // 0, 1, 2, ... with no hole and no repeat, whatever order the writers raced in.
8
+ export function* askAssertContiguousEventIds(
9
+ events: EventDocEvent[]
10
+ ): AskResponse<void> {
11
+ const ids = events.map((event) => event.payload.metadata.eventId);
12
+ const expected = ids.map((_, index) => index);
13
+
14
+ yield* askSmokeAssert(
15
+ ids.every((id, index) => id === expected[index]),
16
+ `event ids are not contiguous from 0: [${ids.join(', ')}]`
17
+ );
18
+ }