@superblocksteam/sdk 2.0.119-next.0 → 2.0.119

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 (85) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/cli-replacement/dev-s3-restore.test.mjs +0 -116
  3. package/dist/cli-replacement/dev-s3-restore.test.mjs.map +1 -1
  4. package/dist/cli-replacement/dev-startup-git-before-dbfs-order.test.mjs +0 -1
  5. package/dist/cli-replacement/dev-startup-git-before-dbfs-order.test.mjs.map +1 -1
  6. package/dist/cli-replacement/dev.d.mts +0 -6
  7. package/dist/cli-replacement/dev.d.mts.map +1 -1
  8. package/dist/cli-replacement/dev.mjs +6 -77
  9. package/dist/cli-replacement/dev.mjs.map +1 -1
  10. package/dist/client.d.ts +6 -67
  11. package/dist/client.d.ts.map +1 -1
  12. package/dist/client.js +0 -94
  13. package/dist/client.js.map +1 -1
  14. package/dist/dev-utils/dev-server-persist.test.mjs +17 -117
  15. package/dist/dev-utils/dev-server-persist.test.mjs.map +1 -1
  16. package/dist/dev-utils/dev-server.d.mts +1 -19
  17. package/dist/dev-utils/dev-server.d.mts.map +1 -1
  18. package/dist/dev-utils/dev-server.mjs +28 -296
  19. package/dist/dev-utils/dev-server.mjs.map +1 -1
  20. package/dist/flag.d.ts +1 -1
  21. package/dist/flag.d.ts.map +1 -1
  22. package/dist/flag.js +3 -3
  23. package/dist/flag.js.map +1 -1
  24. package/dist/index.d.ts +3 -4
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +2 -3
  27. package/dist/index.js.map +1 -1
  28. package/dist/sdk.d.ts +1 -8
  29. package/dist/sdk.d.ts.map +1 -1
  30. package/dist/sdk.js +1 -24
  31. package/dist/sdk.js.map +1 -1
  32. package/dist/sdk.test.js +1 -102
  33. package/dist/sdk.test.js.map +1 -1
  34. package/dist/telemetry/index.d.ts +1 -2
  35. package/dist/telemetry/index.d.ts.map +1 -1
  36. package/dist/telemetry/index.js +1 -8
  37. package/dist/telemetry/index.js.map +1 -1
  38. package/dist/telemetry/logging.d.ts +2 -1
  39. package/dist/telemetry/logging.d.ts.map +1 -1
  40. package/dist/telemetry/logging.js +1 -1
  41. package/dist/telemetry/logging.js.map +1 -1
  42. package/dist/types/common.d.ts +1 -1
  43. package/dist/types/common.d.ts.map +1 -1
  44. package/dist/version-control.d.mts.map +1 -1
  45. package/dist/version-control.mjs +19 -14
  46. package/dist/version-control.mjs.map +1 -1
  47. package/eslint.config.js +0 -6
  48. package/package.json +8 -8
  49. package/src/cli-replacement/dev-s3-restore.test.mts +0 -156
  50. package/src/cli-replacement/dev-startup-git-before-dbfs-order.test.mts +0 -1
  51. package/src/cli-replacement/dev.mts +14 -104
  52. package/src/client.ts +6 -189
  53. package/src/dev-utils/dev-server-persist.test.mts +19 -170
  54. package/src/dev-utils/dev-server.mts +32 -366
  55. package/src/flag.ts +4 -4
  56. package/src/index.ts +1 -19
  57. package/src/sdk.test.ts +6 -145
  58. package/src/sdk.ts +0 -36
  59. package/src/telemetry/index.ts +1 -9
  60. package/src/telemetry/logging.ts +2 -2
  61. package/src/types/common.ts +1 -1
  62. package/src/version-control.mts +30 -11
  63. package/test/version-control.test.mts +2 -0
  64. package/tsconfig.tsbuildinfo +1 -1
  65. package/turbo.json +0 -1
  66. package/dist/dev-utils/dedupe-async.d.ts +0 -16
  67. package/dist/dev-utils/dedupe-async.d.ts.map +0 -1
  68. package/dist/dev-utils/dedupe-async.js +0 -27
  69. package/dist/dev-utils/dedupe-async.js.map +0 -1
  70. package/dist/dev-utils/dedupe-async.test.d.ts +0 -2
  71. package/dist/dev-utils/dedupe-async.test.d.ts.map +0 -1
  72. package/dist/dev-utils/dedupe-async.test.js +0 -120
  73. package/dist/dev-utils/dedupe-async.test.js.map +0 -1
  74. package/dist/dev-utils/dev-server-metrics.d.mts +0 -95
  75. package/dist/dev-utils/dev-server-metrics.d.mts.map +0 -1
  76. package/dist/dev-utils/dev-server-metrics.mjs +0 -193
  77. package/dist/dev-utils/dev-server-metrics.mjs.map +0 -1
  78. package/dist/telemetry/memory-metrics.d.ts +0 -3
  79. package/dist/telemetry/memory-metrics.d.ts.map +0 -1
  80. package/dist/telemetry/memory-metrics.js +0 -59
  81. package/dist/telemetry/memory-metrics.js.map +0 -1
  82. package/src/dev-utils/dedupe-async.test.ts +0 -151
  83. package/src/dev-utils/dedupe-async.ts +0 -45
  84. package/src/dev-utils/dev-server-metrics.mts +0 -252
  85. package/src/telemetry/memory-metrics.ts +0 -90
@@ -26,7 +26,6 @@ import {
26
26
  AiServiceFeatureFlags,
27
27
  SnapshotManager,
28
28
  isSdkApiTemplate,
29
- stripResolvedFromLockfile,
30
29
  } from "@superblocksteam/vite-plugin-file-sync/ai-service";
31
30
  import type { DraftInterface } from "@superblocksteam/vite-plugin-file-sync/draft-interface";
32
31
  import { createGitService } from "@superblocksteam/vite-plugin-file-sync/git-service";
@@ -296,7 +295,7 @@ async function installPackages(cwd: string, logger: Logger) {
296
295
  logger.info("Package installation completed successfully");
297
296
  logger.info(stdout);
298
297
  } catch (error) {
299
- logger.error("Error during package installation", getErrorMeta(error));
298
+ logger.error(`Error during package installation: ${error}`);
300
299
  throw error;
301
300
  }
302
301
  }
@@ -352,13 +351,6 @@ export async function dev(options: {
352
351
  /** Pre-existing HTTP server from warm standby mode (avoids port gap on transition). */
353
352
  existingServer?: HttpServer;
354
353
 
355
- /**
356
- * Wall-clock timestamp (Date.now()) when the warm-standby /_sb_activate
357
- * POST was accepted. Forwarded to createDevServer so the warm-activation
358
- * handoff histogram can be recorded.
359
- */
360
- warmActivationStart?: number;
361
-
362
354
  /** Force package installation when the caller has detected dependency drift. */
363
355
  forcePackageInstall?: boolean;
364
356
 
@@ -391,26 +383,8 @@ export async function dev(options: {
391
383
  let snapshotManager: SnapshotManager | undefined;
392
384
  let gitUserName: string | undefined;
393
385
  let gitUserEmail: string | undefined;
394
- // In-flight install handle. We launch the install while the upload/restart
395
- // decisions are still being evaluated and join at every gate that depends
396
- // on post-install state. See the install block and `joinPackageInstall`.
397
- let packageInstallPromise: Promise<void> | undefined;
398
386
  const tracer = getTracer();
399
387
  const logger = getLogger(options.logger);
400
- // Joins the in-flight install at any step that depends on a settled
401
- // node_modules / lockfile state. Clears the handle so later joins are
402
- // no-ops. The rejection (if any) propagates so the caller's step can
403
- // abort cleanly. Defined at outer scope so the post-sync `createDevServer`
404
- // gate and the abort handler can call it too.
405
- const joinPackageInstall = async (reason: string): Promise<void> => {
406
- if (!packageInstallPromise) {
407
- return;
408
- }
409
- logger.info(`Waiting for background package install (${reason})…`);
410
- const promise = packageInstallPromise;
411
- packageInstallPromise = undefined;
412
- await promise;
413
- };
414
388
  const skipAutoUpgrade = autoUpgradeMode === DevServerAutoUpgradeMode.SKIP;
415
389
  const skipCliUpgrade =
416
390
  skipAutoUpgrade ||
@@ -830,16 +804,6 @@ export async function dev(options: {
830
804
  logger.info("[dev-startup] Skipping download, already in sync");
831
805
  }
832
806
 
833
- // Unconditional lockfile sanitation: strip cross-registry
834
- // `resolved` URLs from any lockfile on disk regardless of whether
835
- // install runs next. The lockfile here is whatever survived the
836
- // DBFS path (downloadFirst overwrite, prior boot, brownfield
837
- // import); npm honors `resolved` verbatim and would bypass the
838
- // active registry. `integrity` is preserved, so genuine
839
- // cross-registry tarball drift surfaces as EINTEGRITY. No-op when
840
- // there's no lockfile or no `resolved` entries.
841
- await stripResolvedFromLockfile(cwd);
842
-
843
807
  let hasCliUpdated = false;
844
808
  let upgradePromises: Promise<void>[] = [];
845
809
  const forceUpgrade =
@@ -953,42 +917,17 @@ export async function dev(options: {
953
917
  upgradePromises.length > 0 ||
954
918
  forcePackageInstall
955
919
  ) {
956
- // Launch the install while the upload/restart decisions below
957
- // are still being evaluated. The synchronous joins at upload,
958
- // CLI restart, and pre-Vite-startup all observe and surface
959
- // any rejection; the `.catch` backstop only fires if every
960
- // join was skipped (rare — would mean no upload, no restart,
961
- // and a re-throw before reaching createDevServer).
962
- logger.info("Starting package install in background…");
963
- packageInstallPromise = tracer.startActiveSpan(
964
- "installPackages",
965
- async (span) => {
966
- try {
967
- // Upgrade global CLI and local packages in parallel - improves performance
968
- await Promise.all([
969
- ...upgradePromises,
970
- installPackages(cwd, logger),
971
- ]);
972
- } finally {
973
- span.end();
974
- }
975
- },
976
- );
977
- // Backstop: assigning `.catch` to a separate (discarded) promise
978
- // keeps `packageInstallPromise` itself rejecting, so the joins
979
- // can still observe and abort. Without this, an install that
980
- // fails before any join fires becomes an unhandled rejection.
981
- const installApplicationId = applicationConfig.id;
982
- const installUpgradeCount = upgradePromises.length;
983
- packageInstallPromise.catch((err) => {
984
- logger.error(
985
- // errorId is encoded into the message body because the
986
- // logger.error contract limits structured attributes to
987
- // `{ error: { kind, message, stack } }`. The id stays
988
- // grep-able for Datadog/Sentry alert rules.
989
- `Background package install failed [errorId=DEV_SERVER_BG_INSTALL_FAILED applicationId=${installApplicationId} cwd=${cwd} upgradePromiseCount=${installUpgradeCount}]`,
990
- getErrorMeta(err),
991
- );
920
+ logger.info("Installing packages…");
921
+ await tracer.startActiveSpan("installPackages", async (span) => {
922
+ try {
923
+ // Upgrade global CLI and local packages in parallel - improves performance
924
+ await Promise.all([
925
+ ...upgradePromises,
926
+ installPackages(cwd, logger),
927
+ ]);
928
+ } finally {
929
+ span.end();
930
+ }
992
931
  });
993
932
  } else {
994
933
  logger.info(
@@ -999,9 +938,6 @@ export async function dev(options: {
999
938
  const shouldUploadPackageState =
1000
939
  hasPackageChanged || forcePackageInstall;
1001
940
  if (shouldUploadPackageState || uploadFirst) {
1002
- // Upload serializes the post-install lockfile + node_modules
1003
- // tree to DBFS, so it must observe a quiesced install.
1004
- await joinPackageInstall("before upload");
1005
941
  logger.info(
1006
942
  `Uploading local files to branch '${activeDbfsBranchName}' on server before starting`,
1007
943
  );
@@ -1016,9 +952,6 @@ export async function dev(options: {
1016
952
  }
1017
953
 
1018
954
  if (hasCliUpdated) {
1019
- // Exiting mid-install would leave a half-written lockfile that
1020
- // the next boot would have to recover from.
1021
- await joinPackageInstall("before CLI restart");
1022
955
  try {
1023
956
  logger.info("Releasing lock before restarting the dev server");
1024
957
  await aiService?.removeIntegrationCache();
@@ -1034,9 +967,9 @@ export async function dev(options: {
1034
967
  }
1035
968
  });
1036
969
  } catch (error: any) {
970
+ const msg = error instanceof Error ? error.message : String(error);
1037
971
  logger.error(
1038
- "[dev-server] Startup failed during sync/lock/setup (exiting with code 1)",
1039
- getErrorMeta(error),
972
+ `[dev-server] Startup failed during sync/lock/setup (exiting with code 1): ${msg}`,
1040
973
  );
1041
974
  try {
1042
975
  await aiService?.removeIntegrationCache();
@@ -1050,15 +983,6 @@ export async function dev(options: {
1050
983
  logger.info("Skipping directory sync");
1051
984
  }
1052
985
 
1053
- // Drain the install before Vite starts. Vite's createServer eagerly
1054
- // scans `node_modules` for `optimizeDeps` pre-bundling; if that scan
1055
- // observes a mid-install state (npm moves per-package directories via
1056
- // rename, so a reader can race the rename), the resulting `.vite/deps`
1057
- // cache embeds partial state that survives across reloads. Awaiting
1058
- // here costs at most the install's remaining wall time — the upload
1059
- // and CLI-restart joins above usually drain it first.
1060
- await joinPackageInstall("before Vite startup");
1061
-
1062
986
  const activateRuntimeGitService = async (): Promise<
1063
987
  GitService | undefined
1064
988
  > => {
@@ -1174,7 +1098,6 @@ export async function dev(options: {
1174
1098
  sdk,
1175
1099
  superblocksBaseUrl: tokenConfig.superblocksBaseUrl,
1176
1100
  existingServer: options.existingServer,
1177
- warmActivationStart: options.warmActivationStart,
1178
1101
  // TODO: Remove this cast — build the options object to match
1179
1102
  // CreateDevServerOptions directly so new required fields cause a
1180
1103
  // compile error instead of silently passing undefined.
@@ -1194,19 +1117,6 @@ export async function dev(options: {
1194
1117
  logger.warn(`Error stopping auth hot-reload server: ${error}`);
1195
1118
  });
1196
1119
  }
1197
- // Drain any straggler install before tear-down. By this point the
1198
- // pre-Vite join has already run on the success path, so usually
1199
- // `packageInstallPromise` is undefined and this is a no-op; if abort
1200
- // races createDevServer (or fires during the inner sync block on
1201
- // some error path), draining here keeps the spawned npm child from
1202
- // being SIGKILL'd mid-rename. Errors only get logged — we are tearing
1203
- // down anyway.
1204
- joinPackageInstall("during abort").catch((error: unknown) => {
1205
- logger.warn(
1206
- "Error draining background install during abort",
1207
- getErrorMeta(error),
1208
- );
1209
- });
1210
1120
  // Clean up AI service cache (must happen before app switches directories)
1211
1121
  aiService?.removeIntegrationCache().catch((error: any) => {
1212
1122
  logger.warn(`Error removing integration cache: ${error}`);
package/src/client.ts CHANGED
@@ -2796,71 +2796,21 @@ export interface UsageRecordRow {
2796
2796
  source: string;
2797
2797
  }
2798
2798
 
2799
- export interface UserCreditLimit {
2800
- amountLimitCents: number | null;
2801
- creditLimit: number | null;
2802
- id: string;
2803
- orgId: string;
2804
- updatedAt: string;
2805
- updatedBy: string | null;
2806
- userId: string | null;
2807
- }
2808
-
2809
- export type OrgCreditLimit = Omit<UserCreditLimit, "userId">;
2810
-
2811
- export interface BillingUsageLimitsResponse {
2812
- limits: UserCreditLimit[];
2813
- orgLimit: OrgCreditLimit | null;
2814
- }
2815
-
2816
- type BillingUsageLimitValue =
2817
- | {
2818
- amountLimitCents: number;
2819
- creditLimit?: never;
2820
- }
2821
- | {
2822
- amountLimitCents?: never;
2823
- creditLimit: number;
2824
- };
2825
-
2826
- export type UpdateUserBillingUsageLimitRequest = BillingUsageLimitValue & {
2827
- userId: string | null;
2828
- };
2829
-
2830
- export type UpdateOrgBillingUsageLimitRequest = BillingUsageLimitValue;
2831
-
2832
2799
  export interface PlanSummary {
2833
2800
  billingInterval: "annual" | "monthly";
2834
- builderSeatsAssigned?: number;
2835
- builderSeatsTotal?: number;
2836
- contractEnd?: string | null;
2837
- contractStart?: string | null;
2838
- contractType?: string | null;
2839
2801
  currentPlanCredits: number;
2840
2802
  currentUsageCredits: number;
2841
2803
  cycleStart: string | null;
2842
2804
  cycleEnd: string | null;
2843
2805
  creditsPerSeat: number | null;
2844
- deployedAppsAdditional: number;
2845
- deployedAppPriceAnnual: number | null;
2846
- deployedAppPriceMonthly: number | null;
2847
- deployedAppsIncluded: number;
2848
- deployedAppsLimit: number;
2849
- deployedAppsUsed: number;
2850
- deployedAppCostCents?: number | null;
2851
- dollarCommitAmountCents?: number | null;
2852
- dollarCommitUsedCents?: number | null;
2853
- overageAppsEnabled?: boolean;
2854
- overageCreditsEnabled?: boolean;
2855
- overageSeatsEnabled?: boolean;
2856
2806
  pricePerSeatAnnual: number | null;
2857
2807
  pricePerSeatMonthly: number | null;
2858
- seatsUnlimited?: boolean;
2859
- subscriptionStatus: string | null;
2860
- topupCreditsPurchased: number;
2861
- topupCreditsUsed: number;
2862
- usageResetPeriodLimit: number;
2863
- usageResetPeriodType: "annual" | "contract_term" | "monthly";
2808
+ deployedAppsUsed: number;
2809
+ deployedAppsLimit: number;
2810
+ deployedAppsIncluded: number;
2811
+ deployedAppsAdditional: number;
2812
+ deployedAppPriceMonthly: number | null;
2813
+ deployedAppPriceAnnual: number | null;
2864
2814
  }
2865
2815
 
2866
2816
  export async function fetchBillingUsageDaily({
@@ -2995,139 +2945,6 @@ export async function fetchBillingPlanSummary({
2995
2945
  }
2996
2946
  }
2997
2947
 
2998
- export async function fetchBillingUsageLimits({
2999
- cliVersion,
3000
- token,
3001
- superblocksBaseUrl,
3002
- }: {
3003
- cliVersion: string;
3004
- token: string;
3005
- superblocksBaseUrl: string;
3006
- }): Promise<BillingUsageLimitsResponse> {
3007
- try {
3008
- const url = new URL(
3009
- `${BASE_SERVER_API_URL_V1}/billing/user-credit-limits`,
3010
- superblocksBaseUrl,
3011
- );
3012
-
3013
- const config: AxiosRequestConfig = {
3014
- method: "get",
3015
- url: url.toString(),
3016
- headers: {
3017
- Authorization: "Bearer " + token,
3018
- [CLI_VERSION_HEADER]: cliVersion,
3019
- },
3020
- };
3021
- const response = await axios(config);
3022
- return response.data.data as BillingUsageLimitsResponse;
3023
- } catch (e: any) {
3024
- let message: string;
3025
- if (e instanceof AxiosError) {
3026
- message =
3027
- (e.response?.data?.responseMeta?.message as string) ??
3028
- JSON.stringify(e.response?.data) ??
3029
- e.response?.statusText ??
3030
- e?.message;
3031
- } else {
3032
- message = `${e?.message ? e?.message : e}`;
3033
- }
3034
- throw new Error(`Could not fetch billing usage limits: ${message}`);
3035
- }
3036
- }
3037
-
3038
- export async function updateUserBillingUsageLimit({
3039
- amountLimitCents,
3040
- cliVersion,
3041
- creditLimit,
3042
- token,
3043
- superblocksBaseUrl,
3044
- userId,
3045
- }: UpdateUserBillingUsageLimitRequest & {
3046
- cliVersion: string;
3047
- token: string;
3048
- superblocksBaseUrl: string;
3049
- }): Promise<{ success: boolean }> {
3050
- try {
3051
- const url = new URL(
3052
- `${BASE_SERVER_API_URL_V1}/billing/user-credit-limits`,
3053
- superblocksBaseUrl,
3054
- );
3055
-
3056
- const config: AxiosRequestConfig = {
3057
- method: "put",
3058
- url: url.toString(),
3059
- headers: {
3060
- Authorization: "Bearer " + token,
3061
- [CLI_VERSION_HEADER]: cliVersion,
3062
- },
3063
- data: {
3064
- userId,
3065
- ...(amountLimitCents !== undefined
3066
- ? { amountLimitCents }
3067
- : { creditLimit }),
3068
- },
3069
- };
3070
- const response = await axios(config);
3071
- return response.data.data as { success: boolean };
3072
- } catch (e: any) {
3073
- let message: string;
3074
- if (e instanceof AxiosError) {
3075
- message =
3076
- (e.response?.data?.responseMeta?.message as string) ??
3077
- JSON.stringify(e.response?.data) ??
3078
- e.response?.statusText ??
3079
- e?.message;
3080
- } else {
3081
- message = `${e?.message ? e?.message : e}`;
3082
- }
3083
- throw new Error(`Could not update user billing usage limit: ${message}`);
3084
- }
3085
- }
3086
-
3087
- export async function updateOrgBillingUsageLimit({
3088
- amountLimitCents,
3089
- cliVersion,
3090
- creditLimit,
3091
- token,
3092
- superblocksBaseUrl,
3093
- }: UpdateOrgBillingUsageLimitRequest & {
3094
- cliVersion: string;
3095
- token: string;
3096
- superblocksBaseUrl: string;
3097
- }): Promise<{ success: boolean }> {
3098
- try {
3099
- const url = new URL(
3100
- `${BASE_SERVER_API_URL_V1}/billing/user-credit-limits/org`,
3101
- superblocksBaseUrl,
3102
- );
3103
-
3104
- const config: AxiosRequestConfig = {
3105
- method: "put",
3106
- url: url.toString(),
3107
- headers: {
3108
- Authorization: "Bearer " + token,
3109
- [CLI_VERSION_HEADER]: cliVersion,
3110
- },
3111
- data:
3112
- amountLimitCents !== undefined ? { amountLimitCents } : { creditLimit },
3113
- };
3114
- const response = await axios(config);
3115
- return response.data.data as { success: boolean };
3116
- } catch (e: any) {
3117
- let message: string;
3118
- if (e instanceof AxiosError) {
3119
- message =
3120
- (e.response?.data?.responseMeta?.message as string) ??
3121
- JSON.stringify(e.response?.data) ??
3122
- e.response?.statusText ??
3123
- e?.message;
3124
- } else {
3125
- message = `${e?.message ? e?.message : e}`;
3126
- }
3127
- throw new Error(`Could not update org billing usage limit: ${message}`);
3128
- }
3129
- }
3130
-
3131
2948
  // ---------------------------------------------------------------------------
3132
2949
  // Knowledge (Facts)
3133
2950
  // ---------------------------------------------------------------------------
@@ -1,11 +1,8 @@
1
1
  import type * as NodeChildProcess from "node:child_process";
2
2
  import { EventEmitter } from "node:events";
3
- import { mkdirSync, mkdtempSync, rmSync } from "node:fs";
4
- import { tmpdir } from "node:os";
5
- import { join } from "node:path";
6
3
  import { PassThrough } from "node:stream";
7
4
 
8
- import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
5
+ import { describe, expect, it, vi } from "vitest";
9
6
 
10
7
  const { spawnMock } = vi.hoisted(() => ({
11
8
  spawnMock: vi.fn(),
@@ -25,14 +22,6 @@ vi.mock("node:child_process", async () => {
25
22
  };
26
23
  });
27
24
 
28
- // Use a static import so the slow `dev-server.mjs` module graph (vite, react
29
- // plugin, workspace deps) loads during file evaluation rather than inside each
30
- // test, where the cold-import cost easily exceeds the default 5s test timeout.
31
- import {
32
- createWorkspacePersistArchive,
33
- getSuperblocksTarExcludes,
34
- } from "./dev-server.mjs";
35
-
36
25
  function createMockProcess() {
37
26
  return Object.assign(new EventEmitter(), {
38
27
  stdin: new PassThrough(),
@@ -41,53 +30,8 @@ function createMockProcess() {
41
30
  });
42
31
  }
43
32
 
44
- describe("getSuperblocksTarExcludes", () => {
45
- let tempRoot: string;
46
-
47
- beforeAll(() => {
48
- tempRoot = mkdtempSync(join(tmpdir(), "persist-test-"));
49
- });
50
-
51
- afterAll(() => {
52
- rmSync(tempRoot, { recursive: true, force: true });
53
- });
54
-
55
- it("excludes .superblocks entirely when the directory does not exist", () => {
56
- const result = getSuperblocksTarExcludes(tempRoot);
57
- expect(result).toEqual(["--exclude", ".superblocks"]);
58
- });
59
-
60
- it("excludes subdirs not in the safelist and keeps context/drafts", () => {
61
- const sb = join(tempRoot, ".superblocks");
62
- mkdirSync(sb);
63
- mkdirSync(join(sb, "context"));
64
- mkdirSync(join(sb, "drafts"));
65
- mkdirSync(join(sb, "recordings"));
66
- mkdirSync(join(sb, "scratch"));
67
- mkdirSync(join(sb, "system-skills"));
68
-
69
- const result = getSuperblocksTarExcludes(tempRoot);
70
-
71
- // Safelisted dirs must NOT appear
72
- expect(result).not.toContain(".superblocks/context");
73
- expect(result).not.toContain(".superblocks/drafts");
74
-
75
- // Non-safelisted dirs must be excluded
76
- expect(result).toContain(".superblocks/recordings");
77
- expect(result).toContain(".superblocks/scratch");
78
- expect(result).toContain(".superblocks/system-skills");
79
-
80
- // Each excluded dir has a preceding --exclude flag
81
- for (const name of ["recordings", "scratch", "system-skills"]) {
82
- const idx = result.indexOf(`.superblocks/${name}`);
83
- expect(idx).toBeGreaterThan(0);
84
- expect(result[idx - 1]).toBe("--exclude");
85
- }
86
- });
87
- });
88
-
89
33
  describe("createWorkspacePersistArchive", () => {
90
- it("archive respects excludes", async () => {
34
+ it("excludes node_modules from the tar archive", async () => {
91
35
  const tarProc = createMockProcess();
92
36
  const zstdProc = createMockProcess();
93
37
  spawnMock.mockImplementation((command: string) => {
@@ -103,17 +47,27 @@ describe("createWorkspacePersistArchive", () => {
103
47
  return zstdProc;
104
48
  });
105
49
 
50
+ const { createWorkspacePersistArchive } = await import("./dev-server.mjs");
51
+
106
52
  await expect(createWorkspacePersistArchive("/workspace")).resolves.toEqual(
107
53
  Buffer.from("archive"),
108
54
  );
109
55
 
110
56
  const tarCall = spawnMock.mock.calls.find(([command]) => command === "tar");
111
57
  expect(tarCall).toBeDefined();
112
- const args: string[] = tarCall?.[1];
113
-
114
- // Standard excludes are always present
115
- expect(args).toContain(".git");
116
- expect(args).toContain("node_modules");
58
+ expect(tarCall?.[1]).toEqual([
59
+ "cf",
60
+ "-",
61
+ "--exclude",
62
+ ".git",
63
+ "--exclude",
64
+ "node_modules",
65
+ "--exclude",
66
+ ".superblocks",
67
+ "-C",
68
+ "/workspace",
69
+ ".",
70
+ ]);
117
71
  });
118
72
 
119
73
  it("rejects if tar fails after zstd closes successfully", async () => {
@@ -133,115 +87,10 @@ describe("createWorkspacePersistArchive", () => {
133
87
  return zstdProc;
134
88
  });
135
89
 
90
+ const { createWorkspacePersistArchive } = await import("./dev-server.mjs");
91
+
136
92
  await expect(
137
93
  createWorkspacePersistArchive("/workspace"),
138
94
  ).rejects.toThrowError(/^tar archival failed \(code 2\): tar failed$/);
139
95
  });
140
96
  });
141
-
142
- describe("createWorkspacePersistUploadHeaders", () => {
143
- it("forwards presigned upload headers and adds the archive content length", async () => {
144
- const { createWorkspacePersistUploadHeaders } =
145
- await import("./dev-server.mjs");
146
-
147
- expect(
148
- createWorkspacePersistUploadHeaders(123, {
149
- "Content-Type": "application/custom-zstd",
150
- "x-amz-server-side-encryption": "AES256",
151
- }),
152
- ).toEqual({
153
- "content-length": "123",
154
- "content-type": "application/custom-zstd",
155
- "x-amz-server-side-encryption": "AES256",
156
- });
157
- });
158
-
159
- it("normalizes presigned upload header names case-insensitively", async () => {
160
- const { createWorkspacePersistUploadHeaders } =
161
- await import("./dev-server.mjs");
162
-
163
- expect(
164
- createWorkspacePersistUploadHeaders(789, {
165
- "content-type": "application/custom-zstd",
166
- "content-length": "1",
167
- "x-amz-server-side-encryption": "AES256",
168
- }),
169
- ).toEqual({
170
- "content-length": "789",
171
- "content-type": "application/custom-zstd",
172
- "x-amz-server-side-encryption": "AES256",
173
- });
174
- });
175
-
176
- it("keeps the legacy content headers when SABS sends only a URL", async () => {
177
- const { createWorkspacePersistUploadHeaders } =
178
- await import("./dev-server.mjs");
179
-
180
- expect(createWorkspacePersistUploadHeaders(456)).toEqual({
181
- "content-length": "456",
182
- "content-type": "application/zstd",
183
- });
184
- });
185
- });
186
-
187
- describe("isWorkspacePersistUploadHeaders", () => {
188
- it("accepts undefined (legacy SABS payloads with no uploadHeaders)", async () => {
189
- const { isWorkspacePersistUploadHeaders } =
190
- await import("./dev-server.mjs");
191
-
192
- expect(isWorkspacePersistUploadHeaders(undefined)).toBe(true);
193
- });
194
-
195
- it("accepts a valid string map", async () => {
196
- const { isWorkspacePersistUploadHeaders } =
197
- await import("./dev-server.mjs");
198
-
199
- expect(
200
- isWorkspacePersistUploadHeaders({
201
- "content-type": "application/zstd",
202
- "x-amz-server-side-encryption": "AES256",
203
- }),
204
- ).toBe(true);
205
- });
206
-
207
- it("rejects null, arrays, and non-string values", async () => {
208
- const { isWorkspacePersistUploadHeaders } =
209
- await import("./dev-server.mjs");
210
-
211
- expect(isWorkspacePersistUploadHeaders(null)).toBe(false);
212
- expect(isWorkspacePersistUploadHeaders([])).toBe(false);
213
- expect(isWorkspacePersistUploadHeaders({ "x-foo": 1 })).toBe(false);
214
- });
215
-
216
- it("rejects case-insensitive duplicate keys", async () => {
217
- const { isWorkspacePersistUploadHeaders } =
218
- await import("./dev-server.mjs");
219
-
220
- expect(
221
- isWorkspacePersistUploadHeaders({
222
- "Content-Type": "application/zstd",
223
- "content-type": "application/custom-zstd",
224
- }),
225
- ).toBe(false);
226
- });
227
-
228
- it("rejects header names with illegal characters", async () => {
229
- const { isWorkspacePersistUploadHeaders } =
230
- await import("./dev-server.mjs");
231
-
232
- // Spaces are not allowed in HTTP token names.
233
- expect(isWorkspacePersistUploadHeaders({ "bad header": "value" })).toBe(
234
- false,
235
- );
236
- });
237
-
238
- it("rejects header values with illegal characters", async () => {
239
- const { isWorkspacePersistUploadHeaders } =
240
- await import("./dev-server.mjs");
241
-
242
- // CR/LF in header values is not allowed (header injection guard).
243
- expect(isWorkspacePersistUploadHeaders({ "x-foo": "bad\r\nvalue" })).toBe(
244
- false,
245
- );
246
- });
247
- });