Commitd8cf368unknown_key
Fix k6 scripts to pass GateTest syntax module
Fix k6 scripts to pass GateTest syntax module GateTest uses vm.Script (Node.js CJS VM) which rejects top-level ES module import statements as SyntaxErrors. Rewrote load-test.js and load-test-git.js to use CommonJS-style require() — valid in both the CJS VM parser and k6's runtime. Also adds @types/k6 as a devDependency for IDE support. https://claude.ai/code/session_01DzJMTFASjMHt2f5ze4cNLR
4 files changed+153−132d8cf368664d48b5c4c466a14c1a2a7920a2a1ed8
4 changed files+153−132
Modifiedbun.lock+3−0View fileUnifiedSplit
@@ -17,6 +17,7 @@
1717 },
1818 "devDependencies": {
1919 "@types/bun": "^1.3.14",
20 "@types/k6": "^2.0.0",
2021 "drizzle-kit": "^0.31.10",
2122 "typescript": "^5.7.0",
2223 },
@@ -125,6 +126,8 @@
125126
126127 "@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="],
127128
129 "@types/k6": ["@types/k6@2.0.0", "", {}, "sha512-ztO2fVOAQxtCpF6VWTn8O6FW6Z4DpjhY+XvD2GpCe/+fAfekD45bQ+vrM0B0C0c5ajLpVgWTMlLu9dKZ1nI+wg=="],
130
128131 "@types/node": ["@types/node@25.6.0", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ=="],
129132
130133 "asn1": ["asn1@0.2.6", "", { "dependencies": { "safer-buffer": "~2.1.0" } }, "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ=="],
Modifiedpackage.json+1−0View fileUnifiedSplit
@@ -39,6 +39,7 @@
3939 "devDependencies": {
4040 "@playwright/test": "^1.49.0",
4141 "@types/bun": "^1.3.14",
42 "@types/k6": "^2.0.0",
4243 "drizzle-kit": "^0.31.10",
4344 "typescript": "^5.7.0"
4445 }
Modifiedscripts/load-test-git.js+100−97View fileUnifiedSplit
@@ -4,6 +4,9 @@
44 * Tests the git Smart HTTP endpoints under concurrent load, simulating
55 * the discovery and pack-negotiation phases of git clone and git fetch.
66 *
7 * THIS FILE RUNS UNDER k6 — do not run with node or bun directly.
8 * k6 provides its own built-in modules (k6/http, k6/metrics, etc.) at runtime.
9 *
710 * Covered endpoints:
811 * GET /:owner/:repo.git/info/refs?service=git-upload-pack
912 * GET /:owner/:repo.git/info/refs?service=git-receive-pack
@@ -26,25 +29,35 @@
2629 * Requirements: k6 >= 0.46 (https://k6.io/docs/get-started/installation/)
2730 */
2831
29import http from 'k6/http';
30import { check, sleep, group } from 'k6';
31import { Rate, Trend, Counter } from 'k6/metrics';
32/* global __ENV */
33
34// k6 built-in modules — loaded via k6's module system at runtime
35var http = require('k6/http');
36var k6 = require('k6');
37var metrics = require('k6/metrics');
38
39var check = k6.check;
40var sleep = k6.sleep;
41var group = k6.group;
42var Rate = metrics.Rate;
43var Trend = metrics.Trend;
44var Counter = metrics.Counter;
3245
3346// ---------------------------------------------------------------------------
3447// Custom metrics
3548// ---------------------------------------------------------------------------
3649
37const errorRate = new Rate('git_errors');
38const uploadPackRefs = new Trend('upload_pack_refs_duration');
39const receivePackRefs = new Trend('receive_pack_refs_duration');
40const uploadPackPost = new Trend('upload_pack_post_duration');
41const gitRequests = new Counter('git_requests_total');
50var errorRate = new Rate('git_errors');
51var uploadPackRefs = new Trend('upload_pack_refs_duration');
52var receivePackRefs = new Trend('receive_pack_refs_duration');
53var uploadPackPost = new Trend('upload_pack_post_duration');
54var gitRequests = new Counter('git_requests_total');
4255
4356// ---------------------------------------------------------------------------
44// Test options
57// Test options — exported so k6 reads them before starting VUs
4558// ---------------------------------------------------------------------------
4659
47export const options = {
60module.exports.options = {
4861 stages: [
4962 { duration: '20s', target: 50 }, // ramp: simulate developers starting their day
5063 { duration: '1m', target: 50 }, // steady state: 50 concurrent git clients
@@ -53,7 +66,7 @@ export const options = {
5366 { duration: '30s', target: 0 }, // ramp down
5467 ],
5568 thresholds: {
56 // git-upload-pack/info/refs should be fast — it's just a capability advertisement
69 // git-upload-pack/info/refs should be fast — capability advertisement only
5770 upload_pack_refs_duration: ['p(95)<300', 'p(99)<800'],
5871 // receive-pack refs can be a touch slower (auth check + lock)
5972 receive_pack_refs_duration: ['p(95)<400', 'p(99)<1000'],
@@ -70,59 +83,65 @@ export const options = {
7083// Configuration from env
7184// ---------------------------------------------------------------------------
7285
73const BASE_URL = __ENV.BASE_URL || 'http://localhost:3000';
74const GIT_OWNER = __ENV.GIT_OWNER || 'testowner';
75const GIT_REPO = __ENV.GIT_REPO || 'testrepo';
76const GIT_PAT = __ENV.GIT_PAT || '';
86var BASE_URL = (typeof __ENV !== 'undefined' && __ENV.BASE_URL) || 'http://localhost:3000';
87var GIT_OWNER = (typeof __ENV !== 'undefined' && __ENV.GIT_OWNER) || 'testowner';
88var GIT_REPO = (typeof __ENV !== 'undefined' && __ENV.GIT_REPO) || 'testrepo';
89var GIT_PAT = (typeof __ENV !== 'undefined' && __ENV.GIT_PAT) || '';
7790
7891// Derived
79const REPO_PATH = `/${GIT_OWNER}/${GIT_REPO}.git`;
92var REPO_PATH = '/' + GIT_OWNER + '/' + GIT_REPO + '.git';
8093
8194// Auth header — only included when a PAT is available
82const authHeaders = GIT_PAT
83 ? { Authorization: `Basic ${btoa(`token:${GIT_PAT}`)}` }
95var authHeaders = GIT_PAT
96 ? { Authorization: 'Basic ' + btoa('token:' + GIT_PAT) }
8497 : {};
8598
8699// git-upload-pack POST capability advertisement body.
87// This is the pkt-line framing for a minimal ls-refs request — exactly what
88// `git fetch` sends after it reads /info/refs and wants to negotiate a pack.
89//
90// Format: 4-char hex length prefix + payload, terminated with "0000" flush.
91// "0011command=ls-refs" = 0x11 bytes of payload = 17 + 4 = 21 total → "0015"
92// We keep it minimal so the server returns quickly without sending a full pack.
93const GIT_UPLOAD_PACK_BODY = [
100// pkt-line framing for a minimal ls-refs request — what `git fetch` sends
101// after reading /info/refs to negotiate a pack. Kept minimal so the server
102// returns quickly without sending a full pack.
103var GIT_UPLOAD_PACK_BODY = [
94104 '0014command=ls-refs\n', // pkt-line: ls-refs command
95105 '0000', // flush pkt
96106 '0000', // end of capability list
97107].join('');
98108
99109// ---------------------------------------------------------------------------
100// Default function — executed once per VU per iteration
110// Setup — printed once before the test starts
111// ---------------------------------------------------------------------------
112
113module.exports.setup = function () {
114 console.log('[load-test-git] Target: ' + BASE_URL + REPO_PATH);
115 console.log('[load-test-git] Auth: ' + (GIT_PAT ? 'PAT set (POST tests enabled)' : 'no PAT (read-only tests)'));
116 return { baseUrl: BASE_URL, repo: REPO_PATH };
117};
118
119// ---------------------------------------------------------------------------
120// Default export — executed once per VU per iteration
101121// ---------------------------------------------------------------------------
102122
103export default function () {
123module.exports.default = function () {
104124 // ---- 1. git-upload-pack info/refs (git clone / git fetch discovery) ----
105125 group('upload-pack info/refs', function () {
106 const url = `${BASE_URL}${REPO_PATH}/info/refs?service=git-upload-pack`;
107 const r = http.get(url, {
108 headers: {
109 ...authHeaders,
110 'Git-Protocol': 'version=2',
111 'User-Agent': 'git/2.44.0',
112 },
126 var url = BASE_URL + REPO_PATH + '/info/refs?service=git-upload-pack';
127 var headers = {};
128 Object.assign(headers, authHeaders, {
129 'Git-Protocol': 'version=2',
130 'User-Agent': 'git/2.44.0',
113131 });
132 var r = http.get(url, { headers: headers });
114133 gitRequests.add(1);
115134
116 const ok = check(r, {
117 'upload-pack refs: 200 or 401': (res) =>
118 res.status === 200 || res.status === 401,
119 'upload-pack refs: correct content-type': (res) =>
120 // Public repos return 200 with the git content-type.
121 // Private repos behind auth return 401 — both are correct behaviour.
122 res.status === 401 ||
123 (res.headers['Content-Type'] || '').includes(
124 'application/x-git-upload-pack-advertisement'
125 ),
135 var ok = check(r, {
136 'upload-pack refs: 200 or 401': function (res) {
137 return res.status === 200 || res.status === 401;
138 },
139 'upload-pack refs: correct content-type': function (res) {
140 return res.status === 401 ||
141 (res.headers['Content-Type'] || '').indexOf(
142 'application/x-git-upload-pack-advertisement'
143 ) !== -1;
144 },
126145 });
127146 errorRate.add(!ok);
128147 uploadPackRefs.add(r.timings.duration);
@@ -132,22 +151,21 @@ export default function () {
132151
133152 // ---- 2. git-receive-pack info/refs (git push discovery) ----
134153 group('receive-pack info/refs', function () {
135 const url = `${BASE_URL}${REPO_PATH}/info/refs?service=git-receive-pack`;
136 const r = http.get(url, {
137 headers: {
138 ...authHeaders,
139 'Git-Protocol': 'version=2',
140 'User-Agent': 'git/2.44.0',
141 },
154 var url = BASE_URL + REPO_PATH + '/info/refs?service=git-receive-pack';
155 var headers = {};
156 Object.assign(headers, authHeaders, {
157 'Git-Protocol': 'version=2',
158 'User-Agent': 'git/2.44.0',
142159 });
160 var r = http.get(url, { headers: headers });
143161 gitRequests.add(1);
144162
145 const ok = check(r, {
146 // receive-pack always requires auth; 401 is the expected response for
147 // unauthenticated requests. 200 is correct for authenticated pushers.
148 'receive-pack refs: 200 or 401': (res) =>
149 res.status === 200 || res.status === 401,
150 'receive-pack refs: not 500': (res) => res.status !== 500,
163 var ok = check(r, {
164 // receive-pack always requires auth; 401 is expected for unauthed requests
165 'receive-pack refs: 200 or 401': function (res) {
166 return res.status === 200 || res.status === 401;
167 },
168 'receive-pack refs: not 500': function (res) { return res.status !== 500; },
151169 });
152170 errorRate.add(!ok);
153171 receivePackRefs.add(r.timings.duration);
@@ -157,63 +175,48 @@ export default function () {
157175
158176 // ---- 3. git-upload-pack POST (minimal ls-refs — no actual pack download) ----
159177 //
160 // Only run when a PAT is set so we don't spam unauthenticated POST 401s
161 // which would skew the POST timing metrics with auth-rejection noise.
178 // Only run when a PAT is set so we don't measure auth-rejection latency.
162179 if (GIT_PAT) {
163180 group('upload-pack POST (ls-refs)', function () {
164 const url = `${BASE_URL}${REPO_PATH}/git-upload-pack`;
165 const r = http.post(url, GIT_UPLOAD_PACK_BODY, {
166 headers: {
167 ...authHeaders,
168 'Content-Type': 'application/x-git-upload-pack-request',
169 'Accept': 'application/x-git-upload-pack-result',
170 'Git-Protocol': 'version=2',
171 'User-Agent': 'git/2.44.0',
172 },
181 var url = BASE_URL + REPO_PATH + '/git-upload-pack';
182 var headers = {};
183 Object.assign(headers, authHeaders, {
184 'Content-Type': 'application/x-git-upload-pack-request',
185 'Accept': 'application/x-git-upload-pack-result',
186 'Git-Protocol': 'version=2',
187 'User-Agent': 'git/2.44.0',
173188 });
189 var r = http.post(url, GIT_UPLOAD_PACK_BODY, { headers: headers });
174190 gitRequests.add(1);
175191
176 const ok = check(r, {
177 'upload-pack POST: 200': (res) => res.status === 200,
178 'upload-pack POST: git content-type': (res) =>
179 (res.headers['Content-Type'] || '').includes(
192 var ok = check(r, {
193 'upload-pack POST: 200': function (res) { return res.status === 200; },
194 'upload-pack POST: git content-type': function (res) {
195 return (res.headers['Content-Type'] || '').indexOf(
180196 'application/x-git-upload-pack-result'
181 ),
182 'upload-pack POST: not empty': (res) =>
183 res.body !== null && res.body.length > 0,
197 ) !== -1;
198 },
199 'upload-pack POST: not empty': function (res) {
200 return res.body !== null && res.body.length > 0;
201 },
184202 });
185203 errorRate.add(!ok);
186204 uploadPackPost.add(r.timings.duration);
187205 });
188206 }
189207
190 // ---- 4. Raw blob fetch (simulates `git archive` or web raw downloads) ----
208 // ---- 4. Raw blob fetch (simulates web raw file downloads) ----
191209 group('raw file fetch (HEAD README)', function () {
192 // Fetches the raw README from the default branch.
193 // Non-existent paths 404 cleanly; the check accepts both 200 and 404
194 // so the test passes even when the test repo doesn't exist yet.
195 const url = `${BASE_URL}/${GIT_OWNER}/${GIT_REPO}/raw/HEAD/README.md`;
196 const r = http.get(url, {
197 headers: { ...authHeaders },
198 });
210 var url = BASE_URL + '/' + GIT_OWNER + '/' + GIT_REPO + '/raw/HEAD/README.md';
211 var r = http.get(url, { headers: authHeaders });
199212 gitRequests.add(1);
200213
201 const ok = check(r, {
202 'raw: 200 or 404': (res) => res.status === 200 || res.status === 404,
203 'raw: not 500': (res) => res.status !== 500,
214 var ok = check(r, {
215 'raw: 200 or 404': function (res) { return res.status === 200 || res.status === 404; },
216 'raw: not 500': function (res) { return res.status !== 500; },
204217 });
205218 errorRate.add(!ok);
206219 });
207220
208221 sleep(1);
209}
210
211// ---------------------------------------------------------------------------
212// Setup — print configuration once before the test starts
213// ---------------------------------------------------------------------------
214
215export function setup() {
216 console.log(`[load-test-git] Target: ${BASE_URL}${REPO_PATH}`);
217 console.log(`[load-test-git] Auth: ${GIT_PAT ? 'PAT set (POST tests enabled)' : 'no PAT (read-only tests)'}`);
218 return { baseUrl: BASE_URL, repo: REPO_PATH };
219}
222};
Modifiedscripts/load-test.js+49−35View fileUnifiedSplit
@@ -3,6 +3,9 @@
33 *
44 * Tests the main public-facing pages under sustained load.
55 *
6 * THIS FILE RUNS UNDER k6 — do not run with node or bun directly.
7 * k6 provides its own built-in modules (k6/http, k6/metrics, etc.) at runtime.
8 *
69 * Usage:
710 * k6 run scripts/load-test.js
811 *
@@ -15,25 +18,34 @@
1518 * Requirements: k6 >= 0.46 (https://k6.io/docs/get-started/installation/)
1619 */
1720
18import http from 'k6/http';
19import { check, sleep, group } from 'k6';
20import { Rate, Trend } from 'k6/metrics';
21/* global __ENV */
22
23// k6 built-in modules — loaded via k6's module system at runtime
24var http = require('k6/http');
25var k6 = require('k6');
26var metrics = require('k6/metrics');
27
28var check = k6.check;
29var sleep = k6.sleep;
30var group = k6.group;
31var Rate = metrics.Rate;
32var Trend = metrics.Trend;
2133
2234// ---------------------------------------------------------------------------
2335// Custom metrics
2436// ---------------------------------------------------------------------------
2537
26const errorRate = new Rate('errors');
27const landingDuration = new Trend('landing_duration');
28const exploreDuration = new Trend('explore_duration');
29const blogDuration = new Trend('blog_duration');
30const pricingDuration = new Trend('pricing_duration');
38var errorRate = new Rate('errors');
39var landingDuration = new Trend('landing_duration');
40var exploreDuration = new Trend('explore_duration');
41var blogDuration = new Trend('blog_duration');
42var pricingDuration = new Trend('pricing_duration');
3143
3244// ---------------------------------------------------------------------------
33// Test options
45// Test options — exported so k6 reads them before starting VUs
3446// ---------------------------------------------------------------------------
3547
36export const options = {
48module.exports.options = {
3749 stages: [
3850 { duration: '30s', target: 100 }, // ramp up to 100 VUs
3951 { duration: '1m', target: 100 }, // steady state
@@ -53,22 +65,22 @@ export const options = {
5365// Helpers
5466// ---------------------------------------------------------------------------
5567
56const BASE_URL = __ENV.BASE_URL || 'http://localhost:3000';
68var BASE_URL = (typeof __ENV !== 'undefined' && __ENV.BASE_URL) || 'http://localhost:3000';
5769
5870function get(path, params) {
59 return http.get(`${BASE_URL}${path}`, params || {});
71 return http.get(BASE_URL + path, params || {});
6072}
6173
6274// ---------------------------------------------------------------------------
63// Default function — executed once per VU per iteration
75// Default export — executed once per VU per iteration
6476// ---------------------------------------------------------------------------
6577
66export default function () {
78module.exports.default = function () {
6779 group('landing page', function () {
68 const r = get('/');
69 const ok = check(r, {
70 'landing 200': (res) => res.status === 200,
71 'landing has content': (res) => res.body && res.body.length > 500,
80 var r = get('/');
81 var ok = check(r, {
82 'landing 200': function (res) { return res.status === 200; },
83 'landing has content': function (res) { return res.body && res.body.length > 500; },
7284 });
7385 errorRate.add(!ok);
7486 landingDuration.add(r.timings.duration);
@@ -77,9 +89,9 @@ export default function () {
7789 sleep(0.5);
7890
7991 group('explore page', function () {
80 const r = get('/explore');
81 const ok = check(r, {
82 'explore 200': (res) => res.status === 200,
92 var r = get('/explore');
93 var ok = check(r, {
94 'explore 200': function (res) { return res.status === 200; },
8395 });
8496 errorRate.add(!ok);
8597 exploreDuration.add(r.timings.duration);
@@ -88,11 +100,12 @@ export default function () {
88100 sleep(0.5);
89101
90102 group('blog index', function () {
91 const r = get('/blog');
92 const ok = check(r, {
93 'blog 200': (res) => res.status === 200,
94 'blog has devlog header': (res) =>
95 res.body && res.body.includes('Gluecron Devlog'),
103 var r = get('/blog');
104 var ok = check(r, {
105 'blog 200': function (res) { return res.status === 200; },
106 'blog has devlog header': function (res) {
107 return res.body && res.body.indexOf('Gluecron Devlog') !== -1;
108 },
96109 });
97110 errorRate.add(!ok);
98111 blogDuration.add(r.timings.duration);
@@ -101,11 +114,12 @@ export default function () {
101114 sleep(0.3);
102115
103116 group('blog post', function () {
104 const r = get('/blog/spec-to-pr-in-90-seconds');
105 const ok = check(r, {
106 'blog post 200': (res) => res.status === 200,
107 'blog post has title': (res) =>
108 res.body && res.body.includes('Spec to PR'),
117 var r = get('/blog/spec-to-pr-in-90-seconds');
118 var ok = check(r, {
119 'blog post 200': function (res) { return res.status === 200; },
120 'blog post has title': function (res) {
121 return res.body && res.body.indexOf('Spec to PR') !== -1;
122 },
109123 });
110124 errorRate.add(!ok);
111125 blogDuration.add(r.timings.duration);
@@ -114,13 +128,13 @@ export default function () {
114128 sleep(0.3);
115129
116130 group('pricing page', function () {
117 const r = get('/pricing');
118 const ok = check(r, {
119 'pricing 200': (res) => res.status === 200,
131 var r = get('/pricing');
132 var ok = check(r, {
133 'pricing 200': function (res) { return res.status === 200; },
120134 });
121135 errorRate.add(!ok);
122136 pricingDuration.add(r.timings.duration);
123137 });
124138
125139 sleep(1);
126}
140};
127141