@stardeck-customer-apps/testing 0.2.0 → 0.3.1

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/dist/index.d.mts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { PGlite } from '@electric-sql/pglite';
2
+ import { ModuleSchemaOp, ModuleDataPort, IdentityClient } from '@stardeck-customer-apps/core';
2
3
 
3
4
  /**
4
5
  * The user shape returned by project-auth's `getSession()`. Mirrors the
@@ -140,6 +141,52 @@ interface TestApp {
140
141
  */
141
142
  declare function createTestApp(options?: TestAppOptions): Promise<TestApp>;
142
143
 
144
+ /** The minimal slice of a module definition the harness needs to stand it up. */
145
+ interface MountableModule {
146
+ schema: ModuleSchemaOp[];
147
+ }
148
+ interface ModuleSeedDeps {
149
+ /** Raw-SQL data port over the harness PGlite — what the module engine reads/writes through. */
150
+ data: ModuleDataPort;
151
+ /** Identity client backed by the simulated platform identity directory. */
152
+ identities: IdentityClient;
153
+ }
154
+ interface CreateModuleAppOptions {
155
+ /** Modules whose install schema is rendered to DDL and applied to the test DB. */
156
+ modules: MountableModule[];
157
+ /**
158
+ * Seed run once after the schema is applied, and again on every `reset()`.
159
+ * Receives the same data port + identity client the module engine uses, so a
160
+ * module's own `seed*` function drops straight in.
161
+ */
162
+ seed?: (deps: ModuleSeedDeps) => Promise<void>;
163
+ /** Allow real outbound network (default false). */
164
+ allowNetwork?: boolean;
165
+ }
166
+ interface ModuleApp extends ModuleSeedDeps {
167
+ /** The underlying harness app (db, identities inspector, callRoute, asUser, …). */
168
+ app: TestApp;
169
+ /** Reset to a clean slate: drop + re-apply the module schema, clear the sim, re-run the seed. */
170
+ reset(): Promise<void>;
171
+ /** Tear down (restore global fetch, close the db). */
172
+ close(): Promise<void>;
173
+ }
174
+ /**
175
+ * Stand a capability module up on the in-process platform simulator — the **L2**
176
+ * harness. It renders the module's install schema to real DDL (the SAME renderer
177
+ * the control plane uses, drift-tested in apps/web), applies it to PGlite, wires a
178
+ * raw-SQL data port over it, and a real `integrations-sdk` identity client talking
179
+ * to the simulated identity directory (so scoping + governance behave as in prod).
180
+ *
181
+ * That's a near-end-to-end test of a module's runtime — schema + data port +
182
+ * identity integration + engine — minus the HTTP/RSC shell (those belong to a
183
+ * real-deploy smoke). The same seams back the local dev playground.
184
+ *
185
+ * Adding a module costs one co-located test file calling this with its own
186
+ * definition + seed; there is no per-module harness to maintain.
187
+ */
188
+ declare function createModuleApp(options: CreateModuleAppOptions): Promise<ModuleApp>;
189
+
143
190
  type RouteHandler<Req extends Request, P> = (request: Req, context: {
144
191
  params: Promise<P>;
145
192
  }) => Promise<Response> | Response;
@@ -183,4 +230,4 @@ declare const TEST_ENV_DEFAULTS: {
183
230
  /** Default location of the DDL snapshot written by `generate-types`. */
184
231
  declare const DEFAULT_SCHEMA_PATH = "./src/generated/data-store-schema.sql";
185
232
 
186
- export { CONTROL_PLANE_TEST_URL, type CallRouteOptions, type CapturedEmail, type CapturedIdentity, type CapturedIdentityLink, DATA_STORE_TEST_HOST, DEFAULT_SCHEMA_PATH, type SessionTokens, TEST_ENV_DEFAULTS, type TestApp, type TestAppOptions, type TestDirectory, type TestInbox, type TestUser, WORKFLOW_NAME_PREFIX, callRoute, createTestApp, describeWorkflow, parseWorkflowName };
233
+ export { CONTROL_PLANE_TEST_URL, type CallRouteOptions, type CapturedEmail, type CapturedIdentity, type CapturedIdentityLink, type CreateModuleAppOptions, DATA_STORE_TEST_HOST, DEFAULT_SCHEMA_PATH, type ModuleApp, type ModuleSeedDeps, type MountableModule, type SessionTokens, TEST_ENV_DEFAULTS, type TestApp, type TestAppOptions, type TestDirectory, type TestInbox, type TestUser, WORKFLOW_NAME_PREFIX, callRoute, createModuleApp, createTestApp, describeWorkflow, parseWorkflowName };
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { PGlite } from '@electric-sql/pglite';
2
+ import { ModuleSchemaOp, ModuleDataPort, IdentityClient } from '@stardeck-customer-apps/core';
2
3
 
3
4
  /**
4
5
  * The user shape returned by project-auth's `getSession()`. Mirrors the
@@ -140,6 +141,52 @@ interface TestApp {
140
141
  */
141
142
  declare function createTestApp(options?: TestAppOptions): Promise<TestApp>;
142
143
 
144
+ /** The minimal slice of a module definition the harness needs to stand it up. */
145
+ interface MountableModule {
146
+ schema: ModuleSchemaOp[];
147
+ }
148
+ interface ModuleSeedDeps {
149
+ /** Raw-SQL data port over the harness PGlite — what the module engine reads/writes through. */
150
+ data: ModuleDataPort;
151
+ /** Identity client backed by the simulated platform identity directory. */
152
+ identities: IdentityClient;
153
+ }
154
+ interface CreateModuleAppOptions {
155
+ /** Modules whose install schema is rendered to DDL and applied to the test DB. */
156
+ modules: MountableModule[];
157
+ /**
158
+ * Seed run once after the schema is applied, and again on every `reset()`.
159
+ * Receives the same data port + identity client the module engine uses, so a
160
+ * module's own `seed*` function drops straight in.
161
+ */
162
+ seed?: (deps: ModuleSeedDeps) => Promise<void>;
163
+ /** Allow real outbound network (default false). */
164
+ allowNetwork?: boolean;
165
+ }
166
+ interface ModuleApp extends ModuleSeedDeps {
167
+ /** The underlying harness app (db, identities inspector, callRoute, asUser, …). */
168
+ app: TestApp;
169
+ /** Reset to a clean slate: drop + re-apply the module schema, clear the sim, re-run the seed. */
170
+ reset(): Promise<void>;
171
+ /** Tear down (restore global fetch, close the db). */
172
+ close(): Promise<void>;
173
+ }
174
+ /**
175
+ * Stand a capability module up on the in-process platform simulator — the **L2**
176
+ * harness. It renders the module's install schema to real DDL (the SAME renderer
177
+ * the control plane uses, drift-tested in apps/web), applies it to PGlite, wires a
178
+ * raw-SQL data port over it, and a real `integrations-sdk` identity client talking
179
+ * to the simulated identity directory (so scoping + governance behave as in prod).
180
+ *
181
+ * That's a near-end-to-end test of a module's runtime — schema + data port +
182
+ * identity integration + engine — minus the HTTP/RSC shell (those belong to a
183
+ * real-deploy smoke). The same seams back the local dev playground.
184
+ *
185
+ * Adding a module costs one co-located test file calling this with its own
186
+ * definition + seed; there is no per-module harness to maintain.
187
+ */
188
+ declare function createModuleApp(options: CreateModuleAppOptions): Promise<ModuleApp>;
189
+
143
190
  type RouteHandler<Req extends Request, P> = (request: Req, context: {
144
191
  params: Promise<P>;
145
192
  }) => Promise<Response> | Response;
@@ -183,4 +230,4 @@ declare const TEST_ENV_DEFAULTS: {
183
230
  /** Default location of the DDL snapshot written by `generate-types`. */
184
231
  declare const DEFAULT_SCHEMA_PATH = "./src/generated/data-store-schema.sql";
185
232
 
186
- export { CONTROL_PLANE_TEST_URL, type CallRouteOptions, type CapturedEmail, type CapturedIdentity, type CapturedIdentityLink, DATA_STORE_TEST_HOST, DEFAULT_SCHEMA_PATH, type SessionTokens, TEST_ENV_DEFAULTS, type TestApp, type TestAppOptions, type TestDirectory, type TestInbox, type TestUser, WORKFLOW_NAME_PREFIX, callRoute, createTestApp, describeWorkflow, parseWorkflowName };
233
+ export { CONTROL_PLANE_TEST_URL, type CallRouteOptions, type CapturedEmail, type CapturedIdentity, type CapturedIdentityLink, type CreateModuleAppOptions, DATA_STORE_TEST_HOST, DEFAULT_SCHEMA_PATH, type ModuleApp, type ModuleSeedDeps, type MountableModule, type SessionTokens, TEST_ENV_DEFAULTS, type TestApp, type TestAppOptions, type TestDirectory, type TestInbox, type TestUser, WORKFLOW_NAME_PREFIX, callRoute, createModuleApp, createTestApp, describeWorkflow, parseWorkflowName };
package/dist/index.js CHANGED
@@ -36,6 +36,7 @@ __export(index_exports, {
36
36
  TEST_ENV_DEFAULTS: () => TEST_ENV_DEFAULTS,
37
37
  WORKFLOW_NAME_PREFIX: () => WORKFLOW_NAME_PREFIX,
38
38
  callRoute: () => callRoute,
39
+ createModuleApp: () => createModuleApp,
39
40
  createTestApp: () => createTestApp,
40
41
  describeWorkflow: () => describeWorkflow,
41
42
  parseWorkflowName: () => parseWorkflowName
@@ -98,6 +99,7 @@ var state = globalSingleton("state", () => ({
98
99
  emailCounter: 0,
99
100
  identities: /* @__PURE__ */ new Map(),
100
101
  identityLinks: [],
102
+ identityMemory: [],
101
103
  allowNetwork: false
102
104
  }));
103
105
  function requireDb() {
@@ -755,16 +757,108 @@ async function handleAttachLink(identityId, request) {
755
757
  state.identityLinks.push(link);
756
758
  return success({ link });
757
759
  }
760
+ function memoryDto(m) {
761
+ return {
762
+ id: m.id,
763
+ source: m.source,
764
+ kind: m.kind,
765
+ content: m.content,
766
+ metadata: m.metadata,
767
+ createdAt: m.createdAt
768
+ };
769
+ }
770
+ async function handleResolve(request) {
771
+ const body = await readBody(request);
772
+ const type = body.type;
773
+ if (type !== "person" && type !== "account") {
774
+ return failure("type must be 'person' or 'account'");
775
+ }
776
+ if (type === "account") {
777
+ return failure(
778
+ "resolveOrCreate is for channel-linked persons; an account is not resolvable by link \u2014 create it explicitly instead.",
779
+ 409
780
+ );
781
+ }
782
+ const link = body.link;
783
+ const kind = link?.kind;
784
+ const externalId = link?.externalId;
785
+ if (typeof kind !== "string" || !LINK_KINDS.has(kind)) {
786
+ return failure(`link.kind must be one of: ${[...LINK_KINDS].join(", ")}`);
787
+ }
788
+ if (typeof externalId !== "string" || !externalId) {
789
+ return failure("link.externalId is required");
790
+ }
791
+ const existingLink = state.identityLinks.find(
792
+ (l) => l.kind === kind && l.externalId === externalId
793
+ );
794
+ if (existingLink) {
795
+ const identity2 = state.identities.get(existingLink.identityId);
796
+ if (identity2) return success({ identity: identity2, created: false });
797
+ }
798
+ const identity = {
799
+ id: import_node_crypto3.default.randomUUID(),
800
+ type,
801
+ parentId: null,
802
+ displayName: body.displayName ?? null,
803
+ profile: body.profile ?? {},
804
+ status: "active",
805
+ mergedIntoId: null,
806
+ externalRef: null,
807
+ createdAt: now(),
808
+ updatedAt: now()
809
+ };
810
+ state.identities.set(identity.id, identity);
811
+ state.identityLinks.push({
812
+ id: import_node_crypto3.default.randomUUID(),
813
+ identityId: identity.id,
814
+ kind,
815
+ externalId,
816
+ verified: true,
817
+ createdAt: now()
818
+ });
819
+ return success({ identity, created: true });
820
+ }
821
+ async function handleWriteMemory(identityId, request) {
822
+ if (!state.identities.get(identityId)) return failure("Identity not found", 404);
823
+ const body = await readBody(request);
824
+ if (typeof body.content !== "string" || !body.content) return failure("content is required");
825
+ const entry = {
826
+ id: import_node_crypto3.default.randomUUID(),
827
+ identityId,
828
+ source: typeof body.source === "string" ? body.source : "app",
829
+ kind: typeof body.kind === "string" ? body.kind : "fact",
830
+ content: body.content,
831
+ metadata: body.metadata ?? {},
832
+ createdAt: now()
833
+ };
834
+ state.identityMemory.push(entry);
835
+ return success({ memory: memoryDto(entry) });
836
+ }
837
+ function handleListMemory(identityId, request) {
838
+ if (!state.identities.get(identityId)) return failure("Identity not found", 404);
839
+ let rows = state.identityMemory.filter((m) => m.identityId === identityId).reverse();
840
+ const limitParam = new URL(request.url).searchParams.get("limit");
841
+ if (limitParam) rows = rows.slice(0, Math.max(1, parseInt(limitParam, 10) || 0));
842
+ return success({ memories: rows.map(memoryDto) });
843
+ }
758
844
  async function handleIdentitiesRequest(request, subPath) {
759
845
  const method = request.method;
760
846
  if (subPath === "" || subPath === "/") {
761
847
  if (method === "GET") return handleList(request);
762
848
  if (method === "POST") return handleCreate(request);
763
849
  }
850
+ if (subPath === "/resolve" && method === "POST") {
851
+ return handleResolve(request);
852
+ }
764
853
  const linksMatch = subPath.match(/^\/([^/]+)\/links$/);
765
854
  if (linksMatch && method === "POST") {
766
855
  return handleAttachLink(linksMatch[1], request);
767
856
  }
857
+ const memoryMatch = subPath.match(/^\/([^/]+)\/memory$/);
858
+ if (memoryMatch) {
859
+ if (method === "GET") return handleListMemory(memoryMatch[1], request);
860
+ if (method === "POST") return handleWriteMemory(memoryMatch[1], request);
861
+ }
768
862
  const singleMatch = subPath.match(/^\/([^/]+)$/);
769
863
  if (singleMatch) {
770
864
  if (method === "GET") return handleGet(singleMatch[1]);
@@ -964,6 +1058,7 @@ async function createTestApp(options = {}) {
964
1058
  state.emailCounter = 0;
965
1059
  state.identities.clear();
966
1060
  state.identityLinks = [];
1061
+ state.identityMemory = [];
967
1062
  },
968
1063
  async close() {
969
1064
  state.db = null;
@@ -973,6 +1068,7 @@ async function createTestApp(options = {}) {
973
1068
  state.emails = [];
974
1069
  state.identities.clear();
975
1070
  state.identityLinks = [];
1071
+ state.identityMemory = [];
976
1072
  uninstallFetchRouter();
977
1073
  await db.close();
978
1074
  }
@@ -980,6 +1076,39 @@ async function createTestApp(options = {}) {
980
1076
  return app;
981
1077
  }
982
1078
 
1079
+ // src/module-app.ts
1080
+ var import_core = require("@stardeck-customer-apps/core");
1081
+ async function createModuleApp(options) {
1082
+ const schemaSql = options.modules.map((m) => (0, import_core.renderSchemaOpsToSql)(m.schema)).join("\n\n");
1083
+ const app = await createTestApp({ schemaSql, allowNetwork: options.allowNetwork });
1084
+ const sql = { query: (text, params) => app.db.query(text, params ?? []) };
1085
+ const data = (0, import_core.makeSqlPort)(sql);
1086
+ const { createIntegrationsClient } = await import("@stardeck-customer-apps/integrations-sdk");
1087
+ const identities = createIntegrationsClient({
1088
+ controlPlaneUrl: TEST_ENV_DEFAULTS.CONTROL_PLANE_URL,
1089
+ organizationId: TEST_ENV_DEFAULTS.ORGANIZATION_ID,
1090
+ projectId: TEST_ENV_DEFAULTS.PROJECT_ID,
1091
+ deploymentId: TEST_ENV_DEFAULTS.DEPLOYMENT_ID,
1092
+ deploymentSecret: TEST_ENV_DEFAULTS.DEPLOYMENT_SECRET
1093
+ }).identities;
1094
+ const runSeed = async () => {
1095
+ if (options.seed) await options.seed({ data, identities });
1096
+ };
1097
+ await runSeed();
1098
+ return {
1099
+ app,
1100
+ data,
1101
+ identities,
1102
+ async reset() {
1103
+ await app.reset();
1104
+ await runSeed();
1105
+ },
1106
+ async close() {
1107
+ await app.close();
1108
+ }
1109
+ };
1110
+ }
1111
+
983
1112
  // src/next/headers-shim.ts
984
1113
  var import_node_async_hooks = require("async_hooks");
985
1114
  var requestScopeStorage = globalSingleton(
@@ -1077,6 +1206,7 @@ function parseWorkflowName(describeTitle) {
1077
1206
  TEST_ENV_DEFAULTS,
1078
1207
  WORKFLOW_NAME_PREFIX,
1079
1208
  callRoute,
1209
+ createModuleApp,
1080
1210
  createTestApp,
1081
1211
  describeWorkflow,
1082
1212
  parseWorkflowName
package/dist/index.mjs CHANGED
@@ -54,6 +54,7 @@ var state = globalSingleton("state", () => ({
54
54
  emailCounter: 0,
55
55
  identities: /* @__PURE__ */ new Map(),
56
56
  identityLinks: [],
57
+ identityMemory: [],
57
58
  allowNetwork: false
58
59
  }));
59
60
  function requireDb() {
@@ -711,16 +712,108 @@ async function handleAttachLink(identityId, request) {
711
712
  state.identityLinks.push(link);
712
713
  return success({ link });
713
714
  }
715
+ function memoryDto(m) {
716
+ return {
717
+ id: m.id,
718
+ source: m.source,
719
+ kind: m.kind,
720
+ content: m.content,
721
+ metadata: m.metadata,
722
+ createdAt: m.createdAt
723
+ };
724
+ }
725
+ async function handleResolve(request) {
726
+ const body = await readBody(request);
727
+ const type = body.type;
728
+ if (type !== "person" && type !== "account") {
729
+ return failure("type must be 'person' or 'account'");
730
+ }
731
+ if (type === "account") {
732
+ return failure(
733
+ "resolveOrCreate is for channel-linked persons; an account is not resolvable by link \u2014 create it explicitly instead.",
734
+ 409
735
+ );
736
+ }
737
+ const link = body.link;
738
+ const kind = link?.kind;
739
+ const externalId = link?.externalId;
740
+ if (typeof kind !== "string" || !LINK_KINDS.has(kind)) {
741
+ return failure(`link.kind must be one of: ${[...LINK_KINDS].join(", ")}`);
742
+ }
743
+ if (typeof externalId !== "string" || !externalId) {
744
+ return failure("link.externalId is required");
745
+ }
746
+ const existingLink = state.identityLinks.find(
747
+ (l) => l.kind === kind && l.externalId === externalId
748
+ );
749
+ if (existingLink) {
750
+ const identity2 = state.identities.get(existingLink.identityId);
751
+ if (identity2) return success({ identity: identity2, created: false });
752
+ }
753
+ const identity = {
754
+ id: crypto3.randomUUID(),
755
+ type,
756
+ parentId: null,
757
+ displayName: body.displayName ?? null,
758
+ profile: body.profile ?? {},
759
+ status: "active",
760
+ mergedIntoId: null,
761
+ externalRef: null,
762
+ createdAt: now(),
763
+ updatedAt: now()
764
+ };
765
+ state.identities.set(identity.id, identity);
766
+ state.identityLinks.push({
767
+ id: crypto3.randomUUID(),
768
+ identityId: identity.id,
769
+ kind,
770
+ externalId,
771
+ verified: true,
772
+ createdAt: now()
773
+ });
774
+ return success({ identity, created: true });
775
+ }
776
+ async function handleWriteMemory(identityId, request) {
777
+ if (!state.identities.get(identityId)) return failure("Identity not found", 404);
778
+ const body = await readBody(request);
779
+ if (typeof body.content !== "string" || !body.content) return failure("content is required");
780
+ const entry = {
781
+ id: crypto3.randomUUID(),
782
+ identityId,
783
+ source: typeof body.source === "string" ? body.source : "app",
784
+ kind: typeof body.kind === "string" ? body.kind : "fact",
785
+ content: body.content,
786
+ metadata: body.metadata ?? {},
787
+ createdAt: now()
788
+ };
789
+ state.identityMemory.push(entry);
790
+ return success({ memory: memoryDto(entry) });
791
+ }
792
+ function handleListMemory(identityId, request) {
793
+ if (!state.identities.get(identityId)) return failure("Identity not found", 404);
794
+ let rows = state.identityMemory.filter((m) => m.identityId === identityId).reverse();
795
+ const limitParam = new URL(request.url).searchParams.get("limit");
796
+ if (limitParam) rows = rows.slice(0, Math.max(1, parseInt(limitParam, 10) || 0));
797
+ return success({ memories: rows.map(memoryDto) });
798
+ }
714
799
  async function handleIdentitiesRequest(request, subPath) {
715
800
  const method = request.method;
716
801
  if (subPath === "" || subPath === "/") {
717
802
  if (method === "GET") return handleList(request);
718
803
  if (method === "POST") return handleCreate(request);
719
804
  }
805
+ if (subPath === "/resolve" && method === "POST") {
806
+ return handleResolve(request);
807
+ }
720
808
  const linksMatch = subPath.match(/^\/([^/]+)\/links$/);
721
809
  if (linksMatch && method === "POST") {
722
810
  return handleAttachLink(linksMatch[1], request);
723
811
  }
812
+ const memoryMatch = subPath.match(/^\/([^/]+)\/memory$/);
813
+ if (memoryMatch) {
814
+ if (method === "GET") return handleListMemory(memoryMatch[1], request);
815
+ if (method === "POST") return handleWriteMemory(memoryMatch[1], request);
816
+ }
724
817
  const singleMatch = subPath.match(/^\/([^/]+)$/);
725
818
  if (singleMatch) {
726
819
  if (method === "GET") return handleGet(singleMatch[1]);
@@ -920,6 +1013,7 @@ async function createTestApp(options = {}) {
920
1013
  state.emailCounter = 0;
921
1014
  state.identities.clear();
922
1015
  state.identityLinks = [];
1016
+ state.identityMemory = [];
923
1017
  },
924
1018
  async close() {
925
1019
  state.db = null;
@@ -929,6 +1023,7 @@ async function createTestApp(options = {}) {
929
1023
  state.emails = [];
930
1024
  state.identities.clear();
931
1025
  state.identityLinks = [];
1026
+ state.identityMemory = [];
932
1027
  uninstallFetchRouter();
933
1028
  await db.close();
934
1029
  }
@@ -936,6 +1031,42 @@ async function createTestApp(options = {}) {
936
1031
  return app;
937
1032
  }
938
1033
 
1034
+ // src/module-app.ts
1035
+ import {
1036
+ makeSqlPort,
1037
+ renderSchemaOpsToSql
1038
+ } from "@stardeck-customer-apps/core";
1039
+ async function createModuleApp(options) {
1040
+ const schemaSql = options.modules.map((m) => renderSchemaOpsToSql(m.schema)).join("\n\n");
1041
+ const app = await createTestApp({ schemaSql, allowNetwork: options.allowNetwork });
1042
+ const sql = { query: (text, params) => app.db.query(text, params ?? []) };
1043
+ const data = makeSqlPort(sql);
1044
+ const { createIntegrationsClient } = await import("@stardeck-customer-apps/integrations-sdk");
1045
+ const identities = createIntegrationsClient({
1046
+ controlPlaneUrl: TEST_ENV_DEFAULTS.CONTROL_PLANE_URL,
1047
+ organizationId: TEST_ENV_DEFAULTS.ORGANIZATION_ID,
1048
+ projectId: TEST_ENV_DEFAULTS.PROJECT_ID,
1049
+ deploymentId: TEST_ENV_DEFAULTS.DEPLOYMENT_ID,
1050
+ deploymentSecret: TEST_ENV_DEFAULTS.DEPLOYMENT_SECRET
1051
+ }).identities;
1052
+ const runSeed = async () => {
1053
+ if (options.seed) await options.seed({ data, identities });
1054
+ };
1055
+ await runSeed();
1056
+ return {
1057
+ app,
1058
+ data,
1059
+ identities,
1060
+ async reset() {
1061
+ await app.reset();
1062
+ await runSeed();
1063
+ },
1064
+ async close() {
1065
+ await app.close();
1066
+ }
1067
+ };
1068
+ }
1069
+
939
1070
  // src/next/headers-shim.ts
940
1071
  import { AsyncLocalStorage } from "async_hooks";
941
1072
  var requestScopeStorage = globalSingleton(
@@ -1032,6 +1163,7 @@ export {
1032
1163
  TEST_ENV_DEFAULTS,
1033
1164
  WORKFLOW_NAME_PREFIX,
1034
1165
  callRoute,
1166
+ createModuleApp,
1035
1167
  createTestApp,
1036
1168
  describeWorkflow,
1037
1169
  parseWorkflowName
@@ -46,6 +46,7 @@ var state = globalSingleton("state", () => ({
46
46
  emailCounter: 0,
47
47
  identities: /* @__PURE__ */ new Map(),
48
48
  identityLinks: [],
49
+ identityMemory: [],
49
50
  allowNetwork: false
50
51
  }));
51
52
 
@@ -19,6 +19,7 @@ var state = globalSingleton("state", () => ({
19
19
  emailCounter: 0,
20
20
  identities: /* @__PURE__ */ new Map(),
21
21
  identityLinks: [],
22
+ identityMemory: [],
22
23
  allowNetwork: false
23
24
  }));
24
25
 
package/dist/setup.js CHANGED
@@ -40,6 +40,7 @@ var state = globalSingleton("state", () => ({
40
40
  emailCounter: 0,
41
41
  identities: /* @__PURE__ */ new Map(),
42
42
  identityLinks: [],
43
+ identityMemory: [],
43
44
  allowNetwork: false
44
45
  }));
45
46
  function requireDb() {
@@ -687,16 +688,108 @@ async function handleAttachLink(identityId, request) {
687
688
  state.identityLinks.push(link);
688
689
  return success({ link });
689
690
  }
691
+ function memoryDto(m) {
692
+ return {
693
+ id: m.id,
694
+ source: m.source,
695
+ kind: m.kind,
696
+ content: m.content,
697
+ metadata: m.metadata,
698
+ createdAt: m.createdAt
699
+ };
700
+ }
701
+ async function handleResolve(request) {
702
+ const body = await readBody(request);
703
+ const type = body.type;
704
+ if (type !== "person" && type !== "account") {
705
+ return failure("type must be 'person' or 'account'");
706
+ }
707
+ if (type === "account") {
708
+ return failure(
709
+ "resolveOrCreate is for channel-linked persons; an account is not resolvable by link \u2014 create it explicitly instead.",
710
+ 409
711
+ );
712
+ }
713
+ const link = body.link;
714
+ const kind = link?.kind;
715
+ const externalId = link?.externalId;
716
+ if (typeof kind !== "string" || !LINK_KINDS.has(kind)) {
717
+ return failure(`link.kind must be one of: ${[...LINK_KINDS].join(", ")}`);
718
+ }
719
+ if (typeof externalId !== "string" || !externalId) {
720
+ return failure("link.externalId is required");
721
+ }
722
+ const existingLink = state.identityLinks.find(
723
+ (l) => l.kind === kind && l.externalId === externalId
724
+ );
725
+ if (existingLink) {
726
+ const identity2 = state.identities.get(existingLink.identityId);
727
+ if (identity2) return success({ identity: identity2, created: false });
728
+ }
729
+ const identity = {
730
+ id: import_node_crypto3.default.randomUUID(),
731
+ type,
732
+ parentId: null,
733
+ displayName: body.displayName ?? null,
734
+ profile: body.profile ?? {},
735
+ status: "active",
736
+ mergedIntoId: null,
737
+ externalRef: null,
738
+ createdAt: now(),
739
+ updatedAt: now()
740
+ };
741
+ state.identities.set(identity.id, identity);
742
+ state.identityLinks.push({
743
+ id: import_node_crypto3.default.randomUUID(),
744
+ identityId: identity.id,
745
+ kind,
746
+ externalId,
747
+ verified: true,
748
+ createdAt: now()
749
+ });
750
+ return success({ identity, created: true });
751
+ }
752
+ async function handleWriteMemory(identityId, request) {
753
+ if (!state.identities.get(identityId)) return failure("Identity not found", 404);
754
+ const body = await readBody(request);
755
+ if (typeof body.content !== "string" || !body.content) return failure("content is required");
756
+ const entry = {
757
+ id: import_node_crypto3.default.randomUUID(),
758
+ identityId,
759
+ source: typeof body.source === "string" ? body.source : "app",
760
+ kind: typeof body.kind === "string" ? body.kind : "fact",
761
+ content: body.content,
762
+ metadata: body.metadata ?? {},
763
+ createdAt: now()
764
+ };
765
+ state.identityMemory.push(entry);
766
+ return success({ memory: memoryDto(entry) });
767
+ }
768
+ function handleListMemory(identityId, request) {
769
+ if (!state.identities.get(identityId)) return failure("Identity not found", 404);
770
+ let rows = state.identityMemory.filter((m) => m.identityId === identityId).reverse();
771
+ const limitParam = new URL(request.url).searchParams.get("limit");
772
+ if (limitParam) rows = rows.slice(0, Math.max(1, parseInt(limitParam, 10) || 0));
773
+ return success({ memories: rows.map(memoryDto) });
774
+ }
690
775
  async function handleIdentitiesRequest(request, subPath) {
691
776
  const method = request.method;
692
777
  if (subPath === "" || subPath === "/") {
693
778
  if (method === "GET") return handleList(request);
694
779
  if (method === "POST") return handleCreate(request);
695
780
  }
781
+ if (subPath === "/resolve" && method === "POST") {
782
+ return handleResolve(request);
783
+ }
696
784
  const linksMatch = subPath.match(/^\/([^/]+)\/links$/);
697
785
  if (linksMatch && method === "POST") {
698
786
  return handleAttachLink(linksMatch[1], request);
699
787
  }
788
+ const memoryMatch = subPath.match(/^\/([^/]+)\/memory$/);
789
+ if (memoryMatch) {
790
+ if (method === "GET") return handleListMemory(memoryMatch[1], request);
791
+ if (method === "POST") return handleWriteMemory(memoryMatch[1], request);
792
+ }
700
793
  const singleMatch = subPath.match(/^\/([^/]+)$/);
701
794
  if (singleMatch) {
702
795
  if (method === "GET") return handleGet(singleMatch[1]);
package/dist/setup.mjs CHANGED
@@ -16,6 +16,7 @@ var state = globalSingleton("state", () => ({
16
16
  emailCounter: 0,
17
17
  identities: /* @__PURE__ */ new Map(),
18
18
  identityLinks: [],
19
+ identityMemory: [],
19
20
  allowNetwork: false
20
21
  }));
21
22
  function requireDb() {
@@ -663,16 +664,108 @@ async function handleAttachLink(identityId, request) {
663
664
  state.identityLinks.push(link);
664
665
  return success({ link });
665
666
  }
667
+ function memoryDto(m) {
668
+ return {
669
+ id: m.id,
670
+ source: m.source,
671
+ kind: m.kind,
672
+ content: m.content,
673
+ metadata: m.metadata,
674
+ createdAt: m.createdAt
675
+ };
676
+ }
677
+ async function handleResolve(request) {
678
+ const body = await readBody(request);
679
+ const type = body.type;
680
+ if (type !== "person" && type !== "account") {
681
+ return failure("type must be 'person' or 'account'");
682
+ }
683
+ if (type === "account") {
684
+ return failure(
685
+ "resolveOrCreate is for channel-linked persons; an account is not resolvable by link \u2014 create it explicitly instead.",
686
+ 409
687
+ );
688
+ }
689
+ const link = body.link;
690
+ const kind = link?.kind;
691
+ const externalId = link?.externalId;
692
+ if (typeof kind !== "string" || !LINK_KINDS.has(kind)) {
693
+ return failure(`link.kind must be one of: ${[...LINK_KINDS].join(", ")}`);
694
+ }
695
+ if (typeof externalId !== "string" || !externalId) {
696
+ return failure("link.externalId is required");
697
+ }
698
+ const existingLink = state.identityLinks.find(
699
+ (l) => l.kind === kind && l.externalId === externalId
700
+ );
701
+ if (existingLink) {
702
+ const identity2 = state.identities.get(existingLink.identityId);
703
+ if (identity2) return success({ identity: identity2, created: false });
704
+ }
705
+ const identity = {
706
+ id: crypto3.randomUUID(),
707
+ type,
708
+ parentId: null,
709
+ displayName: body.displayName ?? null,
710
+ profile: body.profile ?? {},
711
+ status: "active",
712
+ mergedIntoId: null,
713
+ externalRef: null,
714
+ createdAt: now(),
715
+ updatedAt: now()
716
+ };
717
+ state.identities.set(identity.id, identity);
718
+ state.identityLinks.push({
719
+ id: crypto3.randomUUID(),
720
+ identityId: identity.id,
721
+ kind,
722
+ externalId,
723
+ verified: true,
724
+ createdAt: now()
725
+ });
726
+ return success({ identity, created: true });
727
+ }
728
+ async function handleWriteMemory(identityId, request) {
729
+ if (!state.identities.get(identityId)) return failure("Identity not found", 404);
730
+ const body = await readBody(request);
731
+ if (typeof body.content !== "string" || !body.content) return failure("content is required");
732
+ const entry = {
733
+ id: crypto3.randomUUID(),
734
+ identityId,
735
+ source: typeof body.source === "string" ? body.source : "app",
736
+ kind: typeof body.kind === "string" ? body.kind : "fact",
737
+ content: body.content,
738
+ metadata: body.metadata ?? {},
739
+ createdAt: now()
740
+ };
741
+ state.identityMemory.push(entry);
742
+ return success({ memory: memoryDto(entry) });
743
+ }
744
+ function handleListMemory(identityId, request) {
745
+ if (!state.identities.get(identityId)) return failure("Identity not found", 404);
746
+ let rows = state.identityMemory.filter((m) => m.identityId === identityId).reverse();
747
+ const limitParam = new URL(request.url).searchParams.get("limit");
748
+ if (limitParam) rows = rows.slice(0, Math.max(1, parseInt(limitParam, 10) || 0));
749
+ return success({ memories: rows.map(memoryDto) });
750
+ }
666
751
  async function handleIdentitiesRequest(request, subPath) {
667
752
  const method = request.method;
668
753
  if (subPath === "" || subPath === "/") {
669
754
  if (method === "GET") return handleList(request);
670
755
  if (method === "POST") return handleCreate(request);
671
756
  }
757
+ if (subPath === "/resolve" && method === "POST") {
758
+ return handleResolve(request);
759
+ }
672
760
  const linksMatch = subPath.match(/^\/([^/]+)\/links$/);
673
761
  if (linksMatch && method === "POST") {
674
762
  return handleAttachLink(linksMatch[1], request);
675
763
  }
764
+ const memoryMatch = subPath.match(/^\/([^/]+)\/memory$/);
765
+ if (memoryMatch) {
766
+ if (method === "GET") return handleListMemory(memoryMatch[1], request);
767
+ if (method === "POST") return handleWriteMemory(memoryMatch[1], request);
768
+ }
676
769
  const singleMatch = subPath.match(/^\/([^/]+)$/);
677
770
  if (singleMatch) {
678
771
  if (method === "GET") return handleGet(singleMatch[1]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stardeck-customer-apps/testing",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "description": "Vitest test harness for Stardeck customer apps — in-process Postgres (PGlite) plus a control-plane simulator so the real Stardeck SDKs run unmodified in tests",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -62,13 +62,18 @@
62
62
  "author": "Stardeck",
63
63
  "license": "MIT",
64
64
  "dependencies": {
65
- "@electric-sql/pglite": "^0.3.0"
65
+ "@electric-sql/pglite": "^0.3.0",
66
+ "@stardeck-customer-apps/core": "*"
66
67
  },
67
68
  "peerDependencies": {
69
+ "@stardeck-customer-apps/integrations-sdk": ">=1.6.0",
68
70
  "next": "^14.0.0 || ^15.0.0 || ^16.0.0",
69
71
  "vitest": ">=2.0.0"
70
72
  },
71
73
  "peerDependenciesMeta": {
74
+ "@stardeck-customer-apps/integrations-sdk": {
75
+ "optional": true
76
+ },
72
77
  "next": {
73
78
  "optional": true
74
79
  }