@durable-streams/server-conformance-tests 0.1.2 → 0.1.4

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/cli.cjs CHANGED
@@ -86,14 +86,19 @@ function getTestRunnerPath() {
86
86
  if ((0, node_fs.existsSync)(runnerInDist)) return runnerInDist;
87
87
  return runnerInSrc;
88
88
  }
89
+ function findVitestBinary() {
90
+ const possiblePaths = [
91
+ (0, node_path.join)(__dirname$1, `..`, `node_modules`, `.bin`, `vitest`),
92
+ (0, node_path.join)(__dirname$1, `..`, `..`, `..`, `..`, `.bin`, `vitest`),
93
+ (0, node_path.join)(__dirname$1, `..`, `..`, `..`, `node_modules`, `.bin`, `vitest`)
94
+ ];
95
+ for (const vitestPath of possiblePaths) if ((0, node_fs.existsSync)(vitestPath)) return vitestPath;
96
+ return `vitest`;
97
+ }
89
98
  function runTests(baseUrl) {
90
99
  return new Promise((resolvePromise) => {
91
100
  const runnerPath = getTestRunnerPath();
92
- const vitestBin = (0, node_path.join)(__dirname$1, `..`, `node_modules`, `.bin`, `vitest`);
93
- const vitestBinAlt = (0, node_path.join)(__dirname$1, `..`, `..`, `..`, `node_modules`, `.bin`, `vitest`);
94
- let vitestPath = `vitest`;
95
- if ((0, node_fs.existsSync)(vitestBin)) vitestPath = vitestBin;
96
- else if ((0, node_fs.existsSync)(vitestBinAlt)) vitestPath = vitestBinAlt;
101
+ const vitestPath = findVitestBinary();
97
102
  const args = [
98
103
  `run`,
99
104
  runnerPath,
@@ -130,11 +135,7 @@ async function runWatch(baseUrl, watchPaths) {
130
135
  const DEBOUNCE_MS = 300;
131
136
  const spawnTests = () => {
132
137
  const runnerPath = getTestRunnerPath();
133
- const vitestBin = (0, node_path.join)(__dirname$1, `..`, `node_modules`, `.bin`, `vitest`);
134
- const vitestBinAlt = (0, node_path.join)(__dirname$1, `..`, `..`, `..`, `node_modules`, `.bin`, `vitest`);
135
- let vitestPath = `vitest`;
136
- if ((0, node_fs.existsSync)(vitestBin)) vitestPath = vitestBin;
137
- else if ((0, node_fs.existsSync)(vitestBinAlt)) vitestPath = vitestBinAlt;
138
+ const vitestPath = findVitestBinary();
138
139
  const args = [
139
140
  `run`,
140
141
  runnerPath,
package/dist/cli.js CHANGED
@@ -84,14 +84,19 @@ function getTestRunnerPath() {
84
84
  if (existsSync(runnerInDist)) return runnerInDist;
85
85
  return runnerInSrc;
86
86
  }
87
+ function findVitestBinary() {
88
+ const possiblePaths = [
89
+ join(__dirname, `..`, `node_modules`, `.bin`, `vitest`),
90
+ join(__dirname, `..`, `..`, `..`, `..`, `.bin`, `vitest`),
91
+ join(__dirname, `..`, `..`, `..`, `node_modules`, `.bin`, `vitest`)
92
+ ];
93
+ for (const vitestPath of possiblePaths) if (existsSync(vitestPath)) return vitestPath;
94
+ return `vitest`;
95
+ }
87
96
  function runTests(baseUrl) {
88
97
  return new Promise((resolvePromise) => {
89
98
  const runnerPath = getTestRunnerPath();
90
- const vitestBin = join(__dirname, `..`, `node_modules`, `.bin`, `vitest`);
91
- const vitestBinAlt = join(__dirname, `..`, `..`, `..`, `node_modules`, `.bin`, `vitest`);
92
- let vitestPath = `vitest`;
93
- if (existsSync(vitestBin)) vitestPath = vitestBin;
94
- else if (existsSync(vitestBinAlt)) vitestPath = vitestBinAlt;
99
+ const vitestPath = findVitestBinary();
95
100
  const args = [
96
101
  `run`,
97
102
  runnerPath,
@@ -128,11 +133,7 @@ async function runWatch(baseUrl, watchPaths) {
128
133
  const DEBOUNCE_MS = 300;
129
134
  const spawnTests = () => {
130
135
  const runnerPath = getTestRunnerPath();
131
- const vitestBin = join(__dirname, `..`, `node_modules`, `.bin`, `vitest`);
132
- const vitestBinAlt = join(__dirname, `..`, `..`, `..`, `node_modules`, `.bin`, `vitest`);
133
- let vitestPath = `vitest`;
134
- if (existsSync(vitestBin)) vitestPath = vitestBin;
135
- else if (existsSync(vitestBinAlt)) vitestPath = vitestBinAlt;
136
+ const vitestPath = findVitestBinary();
136
137
  const args = [
137
138
  `run`,
138
139
  runnerPath,
package/dist/index.cjs CHANGED
@@ -1,3 +1,3 @@
1
- const require_src = require('./src-mPjxiipV.cjs');
1
+ const require_src = require('./src-DK3GDgwo.cjs');
2
2
 
3
3
  exports.runConformanceTests = require_src.runConformanceTests
package/dist/index.js CHANGED
@@ -1,3 +1,3 @@
1
- import { runConformanceTests } from "./src-DRIMnUPk.js";
1
+ import { runConformanceTests } from "./src-DcbQ_SIQ.js";
2
2
 
3
3
  export { runConformanceTests };
@@ -1212,6 +1212,153 @@ function runConformanceTests(options) {
1212
1212
  if (expiresHeader) (0, vitest.expect)(expiresHeader).toBeDefined();
1213
1213
  });
1214
1214
  });
1215
+ (0, vitest.describe)(`TTL Expiration Behavior`, () => {
1216
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
1217
+ const uniquePath = (prefix) => `/v1/stream/${prefix}-${Date.now()}-${Math.random().toString(36).slice(2)}`;
1218
+ vitest.test.concurrent(`should return 404 on HEAD after TTL expires`, async () => {
1219
+ const streamPath = uniquePath(`ttl-expire-head`);
1220
+ const createResponse = await fetch(`${getBaseUrl()}${streamPath}`, {
1221
+ method: `PUT`,
1222
+ headers: {
1223
+ "Content-Type": `text/plain`,
1224
+ "Stream-TTL": `1`
1225
+ }
1226
+ });
1227
+ (0, vitest.expect)(createResponse.status).toBe(201);
1228
+ const headBefore = await fetch(`${getBaseUrl()}${streamPath}`, { method: `HEAD` });
1229
+ (0, vitest.expect)(headBefore.status).toBe(200);
1230
+ await sleep(1500);
1231
+ const headAfter = await fetch(`${getBaseUrl()}${streamPath}`, { method: `HEAD` });
1232
+ (0, vitest.expect)(headAfter.status).toBe(404);
1233
+ });
1234
+ vitest.test.concurrent(`should return 404 on GET after TTL expires`, async () => {
1235
+ const streamPath = uniquePath(`ttl-expire-get`);
1236
+ const createResponse = await fetch(`${getBaseUrl()}${streamPath}`, {
1237
+ method: `PUT`,
1238
+ headers: {
1239
+ "Content-Type": `text/plain`,
1240
+ "Stream-TTL": `1`
1241
+ },
1242
+ body: `test data`
1243
+ });
1244
+ (0, vitest.expect)(createResponse.status).toBe(201);
1245
+ const getBefore = await fetch(`${getBaseUrl()}${streamPath}`, { method: `GET` });
1246
+ (0, vitest.expect)(getBefore.status).toBe(200);
1247
+ await sleep(1500);
1248
+ const getAfter = await fetch(`${getBaseUrl()}${streamPath}`, { method: `GET` });
1249
+ (0, vitest.expect)(getAfter.status).toBe(404);
1250
+ });
1251
+ vitest.test.concurrent(`should return 404 on POST append after TTL expires`, async () => {
1252
+ const streamPath = uniquePath(`ttl-expire-post`);
1253
+ const createResponse = await fetch(`${getBaseUrl()}${streamPath}`, {
1254
+ method: `PUT`,
1255
+ headers: {
1256
+ "Content-Type": `text/plain`,
1257
+ "Stream-TTL": `1`
1258
+ }
1259
+ });
1260
+ (0, vitest.expect)(createResponse.status).toBe(201);
1261
+ const postBefore = await fetch(`${getBaseUrl()}${streamPath}`, {
1262
+ method: `POST`,
1263
+ headers: { "Content-Type": `text/plain` },
1264
+ body: `appended data`
1265
+ });
1266
+ (0, vitest.expect)([200, 204]).toContain(postBefore.status);
1267
+ await sleep(1500);
1268
+ const postAfter = await fetch(`${getBaseUrl()}${streamPath}`, {
1269
+ method: `POST`,
1270
+ headers: { "Content-Type": `text/plain` },
1271
+ body: `more data`
1272
+ });
1273
+ (0, vitest.expect)(postAfter.status).toBe(404);
1274
+ });
1275
+ vitest.test.concurrent(`should return 404 on HEAD after Expires-At passes`, async () => {
1276
+ const streamPath = uniquePath(`expires-at-head`);
1277
+ const expiresAt = new Date(Date.now() + 1e3).toISOString();
1278
+ const createResponse = await fetch(`${getBaseUrl()}${streamPath}`, {
1279
+ method: `PUT`,
1280
+ headers: {
1281
+ "Content-Type": `text/plain`,
1282
+ "Stream-Expires-At": expiresAt
1283
+ }
1284
+ });
1285
+ (0, vitest.expect)(createResponse.status).toBe(201);
1286
+ const headBefore = await fetch(`${getBaseUrl()}${streamPath}`, { method: `HEAD` });
1287
+ (0, vitest.expect)(headBefore.status).toBe(200);
1288
+ await sleep(1500);
1289
+ const headAfter = await fetch(`${getBaseUrl()}${streamPath}`, { method: `HEAD` });
1290
+ (0, vitest.expect)(headAfter.status).toBe(404);
1291
+ });
1292
+ vitest.test.concurrent(`should return 404 on GET after Expires-At passes`, async () => {
1293
+ const streamPath = uniquePath(`expires-at-get`);
1294
+ const expiresAt = new Date(Date.now() + 1e3).toISOString();
1295
+ const createResponse = await fetch(`${getBaseUrl()}${streamPath}`, {
1296
+ method: `PUT`,
1297
+ headers: {
1298
+ "Content-Type": `text/plain`,
1299
+ "Stream-Expires-At": expiresAt
1300
+ },
1301
+ body: `test data`
1302
+ });
1303
+ (0, vitest.expect)(createResponse.status).toBe(201);
1304
+ const getBefore = await fetch(`${getBaseUrl()}${streamPath}`, { method: `GET` });
1305
+ (0, vitest.expect)(getBefore.status).toBe(200);
1306
+ await sleep(1500);
1307
+ const getAfter = await fetch(`${getBaseUrl()}${streamPath}`, { method: `GET` });
1308
+ (0, vitest.expect)(getAfter.status).toBe(404);
1309
+ });
1310
+ vitest.test.concurrent(`should return 404 on POST append after Expires-At passes`, async () => {
1311
+ const streamPath = uniquePath(`expires-at-post`);
1312
+ const expiresAt = new Date(Date.now() + 1e3).toISOString();
1313
+ const createResponse = await fetch(`${getBaseUrl()}${streamPath}`, {
1314
+ method: `PUT`,
1315
+ headers: {
1316
+ "Content-Type": `text/plain`,
1317
+ "Stream-Expires-At": expiresAt
1318
+ }
1319
+ });
1320
+ (0, vitest.expect)(createResponse.status).toBe(201);
1321
+ const postBefore = await fetch(`${getBaseUrl()}${streamPath}`, {
1322
+ method: `POST`,
1323
+ headers: { "Content-Type": `text/plain` },
1324
+ body: `appended data`
1325
+ });
1326
+ (0, vitest.expect)([200, 204]).toContain(postBefore.status);
1327
+ await sleep(1500);
1328
+ const postAfter = await fetch(`${getBaseUrl()}${streamPath}`, {
1329
+ method: `POST`,
1330
+ headers: { "Content-Type": `text/plain` },
1331
+ body: `more data`
1332
+ });
1333
+ (0, vitest.expect)(postAfter.status).toBe(404);
1334
+ });
1335
+ vitest.test.concurrent(`should allow recreating stream after TTL expires`, async () => {
1336
+ const streamPath = uniquePath(`ttl-recreate`);
1337
+ const createResponse = await fetch(`${getBaseUrl()}${streamPath}`, {
1338
+ method: `PUT`,
1339
+ headers: {
1340
+ "Content-Type": `text/plain`,
1341
+ "Stream-TTL": `1`
1342
+ },
1343
+ body: `original data`
1344
+ });
1345
+ (0, vitest.expect)(createResponse.status).toBe(201);
1346
+ await sleep(1500);
1347
+ const recreateResponse = await fetch(`${getBaseUrl()}${streamPath}`, {
1348
+ method: `PUT`,
1349
+ headers: {
1350
+ "Content-Type": `application/json`,
1351
+ "Stream-TTL": `3600`
1352
+ },
1353
+ body: `["new data"]`
1354
+ });
1355
+ (0, vitest.expect)(recreateResponse.status).toBe(201);
1356
+ const getResponse = await fetch(`${getBaseUrl()}${streamPath}`, { method: `GET` });
1357
+ (0, vitest.expect)(getResponse.status).toBe(200);
1358
+ const body = await getResponse.text();
1359
+ (0, vitest.expect)(body).toContain(`new data`);
1360
+ });
1361
+ });
1215
1362
  (0, vitest.describe)(`Caching and ETag`, () => {
1216
1363
  (0, vitest.test)(`should generate ETag on GET responses`, async () => {
1217
1364
  const streamPath = `/v1/stream/etag-generate-test-${Date.now()}`;
@@ -1210,6 +1210,153 @@ function runConformanceTests(options) {
1210
1210
  if (expiresHeader) expect(expiresHeader).toBeDefined();
1211
1211
  });
1212
1212
  });
1213
+ describe(`TTL Expiration Behavior`, () => {
1214
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
1215
+ const uniquePath = (prefix) => `/v1/stream/${prefix}-${Date.now()}-${Math.random().toString(36).slice(2)}`;
1216
+ test.concurrent(`should return 404 on HEAD after TTL expires`, async () => {
1217
+ const streamPath = uniquePath(`ttl-expire-head`);
1218
+ const createResponse = await fetch(`${getBaseUrl()}${streamPath}`, {
1219
+ method: `PUT`,
1220
+ headers: {
1221
+ "Content-Type": `text/plain`,
1222
+ "Stream-TTL": `1`
1223
+ }
1224
+ });
1225
+ expect(createResponse.status).toBe(201);
1226
+ const headBefore = await fetch(`${getBaseUrl()}${streamPath}`, { method: `HEAD` });
1227
+ expect(headBefore.status).toBe(200);
1228
+ await sleep(1500);
1229
+ const headAfter = await fetch(`${getBaseUrl()}${streamPath}`, { method: `HEAD` });
1230
+ expect(headAfter.status).toBe(404);
1231
+ });
1232
+ test.concurrent(`should return 404 on GET after TTL expires`, async () => {
1233
+ const streamPath = uniquePath(`ttl-expire-get`);
1234
+ const createResponse = await fetch(`${getBaseUrl()}${streamPath}`, {
1235
+ method: `PUT`,
1236
+ headers: {
1237
+ "Content-Type": `text/plain`,
1238
+ "Stream-TTL": `1`
1239
+ },
1240
+ body: `test data`
1241
+ });
1242
+ expect(createResponse.status).toBe(201);
1243
+ const getBefore = await fetch(`${getBaseUrl()}${streamPath}`, { method: `GET` });
1244
+ expect(getBefore.status).toBe(200);
1245
+ await sleep(1500);
1246
+ const getAfter = await fetch(`${getBaseUrl()}${streamPath}`, { method: `GET` });
1247
+ expect(getAfter.status).toBe(404);
1248
+ });
1249
+ test.concurrent(`should return 404 on POST append after TTL expires`, async () => {
1250
+ const streamPath = uniquePath(`ttl-expire-post`);
1251
+ const createResponse = await fetch(`${getBaseUrl()}${streamPath}`, {
1252
+ method: `PUT`,
1253
+ headers: {
1254
+ "Content-Type": `text/plain`,
1255
+ "Stream-TTL": `1`
1256
+ }
1257
+ });
1258
+ expect(createResponse.status).toBe(201);
1259
+ const postBefore = await fetch(`${getBaseUrl()}${streamPath}`, {
1260
+ method: `POST`,
1261
+ headers: { "Content-Type": `text/plain` },
1262
+ body: `appended data`
1263
+ });
1264
+ expect([200, 204]).toContain(postBefore.status);
1265
+ await sleep(1500);
1266
+ const postAfter = await fetch(`${getBaseUrl()}${streamPath}`, {
1267
+ method: `POST`,
1268
+ headers: { "Content-Type": `text/plain` },
1269
+ body: `more data`
1270
+ });
1271
+ expect(postAfter.status).toBe(404);
1272
+ });
1273
+ test.concurrent(`should return 404 on HEAD after Expires-At passes`, async () => {
1274
+ const streamPath = uniquePath(`expires-at-head`);
1275
+ const expiresAt = new Date(Date.now() + 1e3).toISOString();
1276
+ const createResponse = await fetch(`${getBaseUrl()}${streamPath}`, {
1277
+ method: `PUT`,
1278
+ headers: {
1279
+ "Content-Type": `text/plain`,
1280
+ "Stream-Expires-At": expiresAt
1281
+ }
1282
+ });
1283
+ expect(createResponse.status).toBe(201);
1284
+ const headBefore = await fetch(`${getBaseUrl()}${streamPath}`, { method: `HEAD` });
1285
+ expect(headBefore.status).toBe(200);
1286
+ await sleep(1500);
1287
+ const headAfter = await fetch(`${getBaseUrl()}${streamPath}`, { method: `HEAD` });
1288
+ expect(headAfter.status).toBe(404);
1289
+ });
1290
+ test.concurrent(`should return 404 on GET after Expires-At passes`, async () => {
1291
+ const streamPath = uniquePath(`expires-at-get`);
1292
+ const expiresAt = new Date(Date.now() + 1e3).toISOString();
1293
+ const createResponse = await fetch(`${getBaseUrl()}${streamPath}`, {
1294
+ method: `PUT`,
1295
+ headers: {
1296
+ "Content-Type": `text/plain`,
1297
+ "Stream-Expires-At": expiresAt
1298
+ },
1299
+ body: `test data`
1300
+ });
1301
+ expect(createResponse.status).toBe(201);
1302
+ const getBefore = await fetch(`${getBaseUrl()}${streamPath}`, { method: `GET` });
1303
+ expect(getBefore.status).toBe(200);
1304
+ await sleep(1500);
1305
+ const getAfter = await fetch(`${getBaseUrl()}${streamPath}`, { method: `GET` });
1306
+ expect(getAfter.status).toBe(404);
1307
+ });
1308
+ test.concurrent(`should return 404 on POST append after Expires-At passes`, async () => {
1309
+ const streamPath = uniquePath(`expires-at-post`);
1310
+ const expiresAt = new Date(Date.now() + 1e3).toISOString();
1311
+ const createResponse = await fetch(`${getBaseUrl()}${streamPath}`, {
1312
+ method: `PUT`,
1313
+ headers: {
1314
+ "Content-Type": `text/plain`,
1315
+ "Stream-Expires-At": expiresAt
1316
+ }
1317
+ });
1318
+ expect(createResponse.status).toBe(201);
1319
+ const postBefore = await fetch(`${getBaseUrl()}${streamPath}`, {
1320
+ method: `POST`,
1321
+ headers: { "Content-Type": `text/plain` },
1322
+ body: `appended data`
1323
+ });
1324
+ expect([200, 204]).toContain(postBefore.status);
1325
+ await sleep(1500);
1326
+ const postAfter = await fetch(`${getBaseUrl()}${streamPath}`, {
1327
+ method: `POST`,
1328
+ headers: { "Content-Type": `text/plain` },
1329
+ body: `more data`
1330
+ });
1331
+ expect(postAfter.status).toBe(404);
1332
+ });
1333
+ test.concurrent(`should allow recreating stream after TTL expires`, async () => {
1334
+ const streamPath = uniquePath(`ttl-recreate`);
1335
+ const createResponse = await fetch(`${getBaseUrl()}${streamPath}`, {
1336
+ method: `PUT`,
1337
+ headers: {
1338
+ "Content-Type": `text/plain`,
1339
+ "Stream-TTL": `1`
1340
+ },
1341
+ body: `original data`
1342
+ });
1343
+ expect(createResponse.status).toBe(201);
1344
+ await sleep(1500);
1345
+ const recreateResponse = await fetch(`${getBaseUrl()}${streamPath}`, {
1346
+ method: `PUT`,
1347
+ headers: {
1348
+ "Content-Type": `application/json`,
1349
+ "Stream-TTL": `3600`
1350
+ },
1351
+ body: `["new data"]`
1352
+ });
1353
+ expect(recreateResponse.status).toBe(201);
1354
+ const getResponse = await fetch(`${getBaseUrl()}${streamPath}`, { method: `GET` });
1355
+ expect(getResponse.status).toBe(200);
1356
+ const body = await getResponse.text();
1357
+ expect(body).toContain(`new data`);
1358
+ });
1359
+ });
1213
1360
  describe(`Caching and ETag`, () => {
1214
1361
  test(`should generate ETag on GET responses`, async () => {
1215
1362
  const streamPath = `/v1/stream/etag-generate-test-${Date.now()}`;
@@ -1,5 +1,5 @@
1
1
  "use strict";
2
- const require_src = require('./src-mPjxiipV.cjs');
2
+ const require_src = require('./src-DK3GDgwo.cjs');
3
3
 
4
4
  //#region src/test-runner.ts
5
5
  const baseUrl = process.env.CONFORMANCE_TEST_URL;
@@ -1,4 +1,4 @@
1
- import { runConformanceTests } from "./src-DRIMnUPk.js";
1
+ import { runConformanceTests } from "./src-DcbQ_SIQ.js";
2
2
 
3
3
  //#region src/test-runner.ts
4
4
  const baseUrl = process.env.CONFORMANCE_TEST_URL;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@durable-streams/server-conformance-tests",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Conformance test suite for Durable Streams server implementations",
5
5
  "author": "Durable Stream contributors",
6
6
  "license": "Apache-2.0",
@@ -23,6 +23,7 @@
23
23
  "module": "./dist/index.js",
24
24
  "types": "./dist/index.d.ts",
25
25
  "bin": {
26
+ "server-conformance-tests": "./dist/cli.js",
26
27
  "durable-streams-server-conformance": "./dist/cli.js",
27
28
  "durable-streams-server-conformance-dev": "./bin/conformance-dev.mjs"
28
29
  },
@@ -47,7 +48,7 @@
47
48
  ],
48
49
  "dependencies": {
49
50
  "fast-check": "^4.4.0",
50
- "vitest": "^3.2.4",
51
+ "vitest": "^4.0.0",
51
52
  "@durable-streams/client": "0.1.2"
52
53
  },
53
54
  "devDependencies": {
package/src/cli.ts CHANGED
@@ -131,28 +131,31 @@ function getTestRunnerPath(): string {
131
131
  return runnerInSrc
132
132
  }
133
133
 
134
+ // Find vitest binary in various possible locations
135
+ function findVitestBinary(): string {
136
+ const possiblePaths = [
137
+ // Package's own node_modules (bundled dependency)
138
+ join(__dirname, `..`, `node_modules`, `.bin`, `vitest`),
139
+ // Hoisted location for scoped packages (@scope/package-name/dist -> node_modules/.bin)
140
+ join(__dirname, `..`, `..`, `..`, `..`, `.bin`, `vitest`),
141
+ // Monorepo root node_modules (for development)
142
+ join(__dirname, `..`, `..`, `..`, `node_modules`, `.bin`, `vitest`),
143
+ ]
144
+
145
+ for (const vitestPath of possiblePaths) {
146
+ if (existsSync(vitestPath)) {
147
+ return vitestPath
148
+ }
149
+ }
150
+
151
+ // Fallback to vitest in PATH
152
+ return `vitest`
153
+ }
154
+
134
155
  function runTests(baseUrl: string): Promise<number> {
135
156
  return new Promise((resolvePromise) => {
136
157
  const runnerPath = getTestRunnerPath()
137
-
138
- // Find vitest binary
139
- const vitestBin = join(__dirname, `..`, `node_modules`, `.bin`, `vitest`)
140
- const vitestBinAlt = join(
141
- __dirname,
142
- `..`,
143
- `..`,
144
- `..`,
145
- `node_modules`,
146
- `.bin`,
147
- `vitest`
148
- )
149
-
150
- let vitestPath = `vitest`
151
- if (existsSync(vitestBin)) {
152
- vitestPath = vitestBin
153
- } else if (existsSync(vitestBinAlt)) {
154
- vitestPath = vitestBinAlt
155
- }
158
+ const vitestPath = findVitestBinary()
156
159
 
157
160
  const args = [
158
161
  `run`,
@@ -199,25 +202,7 @@ async function runWatch(
199
202
 
200
203
  const spawnTests = (): ChildProcess => {
201
204
  const runnerPath = getTestRunnerPath()
202
-
203
- // Find vitest binary
204
- const vitestBin = join(__dirname, `..`, `node_modules`, `.bin`, `vitest`)
205
- const vitestBinAlt = join(
206
- __dirname,
207
- `..`,
208
- `..`,
209
- `..`,
210
- `node_modules`,
211
- `.bin`,
212
- `vitest`
213
- )
214
-
215
- let vitestPath = `vitest`
216
- if (existsSync(vitestBin)) {
217
- vitestPath = vitestBin
218
- } else if (existsSync(vitestBinAlt)) {
219
- vitestPath = vitestBinAlt
220
- }
205
+ const vitestPath = findVitestBinary()
221
206
 
222
207
  const args = [
223
208
  `run`,
package/src/index.ts CHANGED
@@ -1898,6 +1898,260 @@ export function runConformanceTests(options: ConformanceTestOptions): void {
1898
1898
  })
1899
1899
  })
1900
1900
 
1901
+ // ============================================================================
1902
+ // TTL Expiration Behavior Tests
1903
+ // ============================================================================
1904
+
1905
+ describe(`TTL Expiration Behavior`, () => {
1906
+ // Helper function to wait for a specified duration
1907
+ const sleep = (ms: number) =>
1908
+ new Promise((resolve) => setTimeout(resolve, ms))
1909
+
1910
+ // Helper to generate unique stream paths for concurrent tests
1911
+ const uniquePath = (prefix: string) =>
1912
+ `/v1/stream/${prefix}-${Date.now()}-${Math.random().toString(36).slice(2)}`
1913
+
1914
+ // Run tests concurrently to avoid 6x 1.5s wait time
1915
+ test.concurrent(`should return 404 on HEAD after TTL expires`, async () => {
1916
+ const streamPath = uniquePath(`ttl-expire-head`)
1917
+
1918
+ // Create stream with 1 second TTL
1919
+ const createResponse = await fetch(`${getBaseUrl()}${streamPath}`, {
1920
+ method: `PUT`,
1921
+ headers: {
1922
+ "Content-Type": `text/plain`,
1923
+ "Stream-TTL": `1`,
1924
+ },
1925
+ })
1926
+ expect(createResponse.status).toBe(201)
1927
+
1928
+ // Verify stream exists immediately
1929
+ const headBefore = await fetch(`${getBaseUrl()}${streamPath}`, {
1930
+ method: `HEAD`,
1931
+ })
1932
+ expect(headBefore.status).toBe(200)
1933
+
1934
+ // Wait for TTL to expire (1 second + buffer)
1935
+ await sleep(1500)
1936
+
1937
+ // Stream should no longer exist
1938
+ const headAfter = await fetch(`${getBaseUrl()}${streamPath}`, {
1939
+ method: `HEAD`,
1940
+ })
1941
+ expect(headAfter.status).toBe(404)
1942
+ })
1943
+
1944
+ test.concurrent(`should return 404 on GET after TTL expires`, async () => {
1945
+ const streamPath = uniquePath(`ttl-expire-get`)
1946
+
1947
+ // Create stream with 1 second TTL and some data
1948
+ const createResponse = await fetch(`${getBaseUrl()}${streamPath}`, {
1949
+ method: `PUT`,
1950
+ headers: {
1951
+ "Content-Type": `text/plain`,
1952
+ "Stream-TTL": `1`,
1953
+ },
1954
+ body: `test data`,
1955
+ })
1956
+ expect(createResponse.status).toBe(201)
1957
+
1958
+ // Verify stream is readable immediately
1959
+ const getBefore = await fetch(`${getBaseUrl()}${streamPath}`, {
1960
+ method: `GET`,
1961
+ })
1962
+ expect(getBefore.status).toBe(200)
1963
+
1964
+ // Wait for TTL to expire
1965
+ await sleep(1500)
1966
+
1967
+ // Stream should no longer exist
1968
+ const getAfter = await fetch(`${getBaseUrl()}${streamPath}`, {
1969
+ method: `GET`,
1970
+ })
1971
+ expect(getAfter.status).toBe(404)
1972
+ })
1973
+
1974
+ test.concurrent(
1975
+ `should return 404 on POST append after TTL expires`,
1976
+ async () => {
1977
+ const streamPath = uniquePath(`ttl-expire-post`)
1978
+
1979
+ // Create stream with 1 second TTL
1980
+ const createResponse = await fetch(`${getBaseUrl()}${streamPath}`, {
1981
+ method: `PUT`,
1982
+ headers: {
1983
+ "Content-Type": `text/plain`,
1984
+ "Stream-TTL": `1`,
1985
+ },
1986
+ })
1987
+ expect(createResponse.status).toBe(201)
1988
+
1989
+ // Verify append works immediately
1990
+ const postBefore = await fetch(`${getBaseUrl()}${streamPath}`, {
1991
+ method: `POST`,
1992
+ headers: { "Content-Type": `text/plain` },
1993
+ body: `appended data`,
1994
+ })
1995
+ expect([200, 204]).toContain(postBefore.status)
1996
+
1997
+ // Wait for TTL to expire
1998
+ await sleep(1500)
1999
+
2000
+ // Append should fail - stream no longer exists
2001
+ const postAfter = await fetch(`${getBaseUrl()}${streamPath}`, {
2002
+ method: `POST`,
2003
+ headers: { "Content-Type": `text/plain` },
2004
+ body: `more data`,
2005
+ })
2006
+ expect(postAfter.status).toBe(404)
2007
+ }
2008
+ )
2009
+
2010
+ test.concurrent(
2011
+ `should return 404 on HEAD after Expires-At passes`,
2012
+ async () => {
2013
+ const streamPath = uniquePath(`expires-at-head`)
2014
+
2015
+ // Create stream that expires in 1 second
2016
+ const expiresAt = new Date(Date.now() + 1000).toISOString()
2017
+ const createResponse = await fetch(`${getBaseUrl()}${streamPath}`, {
2018
+ method: `PUT`,
2019
+ headers: {
2020
+ "Content-Type": `text/plain`,
2021
+ "Stream-Expires-At": expiresAt,
2022
+ },
2023
+ })
2024
+ expect(createResponse.status).toBe(201)
2025
+
2026
+ // Verify stream exists immediately
2027
+ const headBefore = await fetch(`${getBaseUrl()}${streamPath}`, {
2028
+ method: `HEAD`,
2029
+ })
2030
+ expect(headBefore.status).toBe(200)
2031
+
2032
+ // Wait for expiry time to pass
2033
+ await sleep(1500)
2034
+
2035
+ // Stream should no longer exist
2036
+ const headAfter = await fetch(`${getBaseUrl()}${streamPath}`, {
2037
+ method: `HEAD`,
2038
+ })
2039
+ expect(headAfter.status).toBe(404)
2040
+ }
2041
+ )
2042
+
2043
+ test.concurrent(
2044
+ `should return 404 on GET after Expires-At passes`,
2045
+ async () => {
2046
+ const streamPath = uniquePath(`expires-at-get`)
2047
+
2048
+ // Create stream that expires in 1 second
2049
+ const expiresAt = new Date(Date.now() + 1000).toISOString()
2050
+ const createResponse = await fetch(`${getBaseUrl()}${streamPath}`, {
2051
+ method: `PUT`,
2052
+ headers: {
2053
+ "Content-Type": `text/plain`,
2054
+ "Stream-Expires-At": expiresAt,
2055
+ },
2056
+ body: `test data`,
2057
+ })
2058
+ expect(createResponse.status).toBe(201)
2059
+
2060
+ // Verify stream is readable immediately
2061
+ const getBefore = await fetch(`${getBaseUrl()}${streamPath}`, {
2062
+ method: `GET`,
2063
+ })
2064
+ expect(getBefore.status).toBe(200)
2065
+
2066
+ // Wait for expiry time to pass
2067
+ await sleep(1500)
2068
+
2069
+ // Stream should no longer exist
2070
+ const getAfter = await fetch(`${getBaseUrl()}${streamPath}`, {
2071
+ method: `GET`,
2072
+ })
2073
+ expect(getAfter.status).toBe(404)
2074
+ }
2075
+ )
2076
+
2077
+ test.concurrent(
2078
+ `should return 404 on POST append after Expires-At passes`,
2079
+ async () => {
2080
+ const streamPath = uniquePath(`expires-at-post`)
2081
+
2082
+ // Create stream that expires in 1 second
2083
+ const expiresAt = new Date(Date.now() + 1000).toISOString()
2084
+ const createResponse = await fetch(`${getBaseUrl()}${streamPath}`, {
2085
+ method: `PUT`,
2086
+ headers: {
2087
+ "Content-Type": `text/plain`,
2088
+ "Stream-Expires-At": expiresAt,
2089
+ },
2090
+ })
2091
+ expect(createResponse.status).toBe(201)
2092
+
2093
+ // Verify append works immediately
2094
+ const postBefore = await fetch(`${getBaseUrl()}${streamPath}`, {
2095
+ method: `POST`,
2096
+ headers: { "Content-Type": `text/plain` },
2097
+ body: `appended data`,
2098
+ })
2099
+ expect([200, 204]).toContain(postBefore.status)
2100
+
2101
+ // Wait for expiry time to pass
2102
+ await sleep(1500)
2103
+
2104
+ // Append should fail - stream no longer exists
2105
+ const postAfter = await fetch(`${getBaseUrl()}${streamPath}`, {
2106
+ method: `POST`,
2107
+ headers: { "Content-Type": `text/plain` },
2108
+ body: `more data`,
2109
+ })
2110
+ expect(postAfter.status).toBe(404)
2111
+ }
2112
+ )
2113
+
2114
+ test.concurrent(
2115
+ `should allow recreating stream after TTL expires`,
2116
+ async () => {
2117
+ const streamPath = uniquePath(`ttl-recreate`)
2118
+
2119
+ // Create stream with 1 second TTL
2120
+ const createResponse = await fetch(`${getBaseUrl()}${streamPath}`, {
2121
+ method: `PUT`,
2122
+ headers: {
2123
+ "Content-Type": `text/plain`,
2124
+ "Stream-TTL": `1`,
2125
+ },
2126
+ body: `original data`,
2127
+ })
2128
+ expect(createResponse.status).toBe(201)
2129
+
2130
+ // Wait for TTL to expire
2131
+ await sleep(1500)
2132
+
2133
+ // Recreate stream with different config - should succeed (201)
2134
+ const recreateResponse = await fetch(`${getBaseUrl()}${streamPath}`, {
2135
+ method: `PUT`,
2136
+ headers: {
2137
+ "Content-Type": `application/json`,
2138
+ "Stream-TTL": `3600`,
2139
+ },
2140
+ body: `["new data"]`,
2141
+ })
2142
+ expect(recreateResponse.status).toBe(201)
2143
+
2144
+ // Verify the new stream is accessible
2145
+ const getResponse = await fetch(`${getBaseUrl()}${streamPath}`, {
2146
+ method: `GET`,
2147
+ })
2148
+ expect(getResponse.status).toBe(200)
2149
+ const body = await getResponse.text()
2150
+ expect(body).toContain(`new data`)
2151
+ }
2152
+ )
2153
+ })
2154
+
1901
2155
  // ============================================================================
1902
2156
  // Caching and ETag Tests
1903
2157
  // ============================================================================