evolved-monkey 0.1.0
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/README.md +22 -0
- package/dist/src/cli.d.ts +15 -0
- package/dist/src/cli.d.ts.map +1 -0
- package/dist/src/cli.js +63 -0
- package/dist/src/cli.js.map +1 -0
- package/dist/src/commands/challenge-init.d.ts +15 -0
- package/dist/src/commands/challenge-init.d.ts.map +1 -0
- package/dist/src/commands/challenge-init.js +98 -0
- package/dist/src/commands/challenge-init.js.map +1 -0
- package/dist/src/commands/challenge-submit.d.ts +21 -0
- package/dist/src/commands/challenge-submit.d.ts.map +1 -0
- package/dist/src/commands/challenge-submit.js +52 -0
- package/dist/src/commands/challenge-submit.js.map +1 -0
- package/dist/src/commands/challenge-validate.d.ts +13 -0
- package/dist/src/commands/challenge-validate.d.ts.map +1 -0
- package/dist/src/commands/challenge-validate.js +28 -0
- package/dist/src/commands/challenge-validate.js.map +1 -0
- package/dist/src/constants.d.ts +6 -0
- package/dist/src/constants.d.ts.map +1 -0
- package/dist/src/constants.js +12 -0
- package/dist/src/constants.js.map +1 -0
- package/dist/src/index.d.ts +3 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +20 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/lib/args.d.ts +3 -0
- package/dist/src/lib/args.d.ts.map +1 -0
- package/dist/src/lib/args.js +24 -0
- package/dist/src/lib/args.js.map +1 -0
- package/dist/src/lib/backend-client.d.ts +10 -0
- package/dist/src/lib/backend-client.d.ts.map +1 -0
- package/dist/src/lib/backend-client.js +86 -0
- package/dist/src/lib/backend-client.js.map +1 -0
- package/dist/src/lib/cli-error.d.ts +12 -0
- package/dist/src/lib/cli-error.d.ts.map +1 -0
- package/dist/src/lib/cli-error.js +13 -0
- package/dist/src/lib/cli-error.js.map +1 -0
- package/dist/src/lib/fs-utils.d.ts +6 -0
- package/dist/src/lib/fs-utils.d.ts.map +1 -0
- package/dist/src/lib/fs-utils.js +39 -0
- package/dist/src/lib/fs-utils.js.map +1 -0
- package/dist/src/lib/tarball.d.ts +7 -0
- package/dist/src/lib/tarball.d.ts.map +1 -0
- package/dist/src/lib/tarball.js +23 -0
- package/dist/src/lib/tarball.js.map +1 -0
- package/dist/src/lib/workspace.d.ts +21 -0
- package/dist/src/lib/workspace.d.ts.map +1 -0
- package/dist/src/lib/workspace.js +107 -0
- package/dist/src/lib/workspace.js.map +1 -0
- package/dist/src/types.d.ts +39 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.js +2 -0
- package/dist/src/types.js.map +1 -0
- package/dist/tests/helpers/test-utils.d.ts +5 -0
- package/dist/tests/helpers/test-utils.d.ts.map +1 -0
- package/dist/tests/helpers/test-utils.js +26 -0
- package/dist/tests/helpers/test-utils.js.map +1 -0
- package/dist/tests/integration/submit-flow.spec.d.ts +2 -0
- package/dist/tests/integration/submit-flow.spec.d.ts.map +1 -0
- package/dist/tests/integration/submit-flow.spec.js +172 -0
- package/dist/tests/integration/submit-flow.spec.js.map +1 -0
- package/dist/tests/run-tests.d.ts +2 -0
- package/dist/tests/run-tests.d.ts.map +1 -0
- package/dist/tests/run-tests.js +33 -0
- package/dist/tests/run-tests.js.map +1 -0
- package/dist/tests/unit/cli-commands.spec.d.ts +2 -0
- package/dist/tests/unit/cli-commands.spec.d.ts.map +1 -0
- package/dist/tests/unit/cli-commands.spec.js +73 -0
- package/dist/tests/unit/cli-commands.spec.js.map +1 -0
- package/docs/01.md +39 -0
- package/package.json +22 -0
- package/src/cli.ts +86 -0
- package/src/commands/challenge-init.ts +162 -0
- package/src/commands/challenge-submit.ts +101 -0
- package/src/commands/challenge-validate.ts +48 -0
- package/src/constants.ts +13 -0
- package/src/index.ts +21 -0
- package/src/lib/args.ts +31 -0
- package/src/lib/backend-client.ts +129 -0
- package/src/lib/cli-error.ts +19 -0
- package/src/lib/fs-utils.ts +47 -0
- package/src/lib/tarball.ts +42 -0
- package/src/lib/workspace.ts +168 -0
- package/src/types.ts +45 -0
- package/tests/fixtures/invalid-workspace/challenge.config.json +11 -0
- package/tests/fixtures/invalid-workspace/starter/package.json +5 -0
- package/tests/fixtures/invalid-workspace/starter/src/index.js +3 -0
- package/tests/fixtures/valid-workspace/challenge.config.json +11 -0
- package/tests/fixtures/valid-workspace/starter/package.json +9 -0
- package/tests/fixtures/valid-workspace/starter/src/index.js +3 -0
- package/tests/fixtures/valid-workspace/tests/package.json +8 -0
- package/tests/fixtures/valid-workspace/tests/spec/basic.test.js +6 -0
- package/tests/helpers/test-utils.ts +32 -0
- package/tests/integration/submit-flow.spec.ts +207 -0
- package/tests/run-tests.ts +42 -0
- package/tests/snapshots/init-result.json +5 -0
- package/tests/unit/cli-commands.spec.ts +105 -0
- package/tsconfig.json +20 -0
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import http from "node:http";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import type { AddressInfo } from "node:net";
|
|
6
|
+
|
|
7
|
+
import { runCli } from "../../src/cli.js";
|
|
8
|
+
import { createTempDir } from "../helpers/test-utils.js";
|
|
9
|
+
|
|
10
|
+
function createTestBackendServer() {
|
|
11
|
+
const uploadStats = {
|
|
12
|
+
starterBytes: 0,
|
|
13
|
+
starterUploads: 0,
|
|
14
|
+
testsBytes: 0,
|
|
15
|
+
testsUploads: 0,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
let server: http.Server | undefined;
|
|
19
|
+
|
|
20
|
+
function sendJson(response: http.ServerResponse, statusCode: number, body: unknown): void {
|
|
21
|
+
response.writeHead(statusCode, { "content-type": "application/json" });
|
|
22
|
+
response.end(JSON.stringify(body));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function readBody(request: http.IncomingMessage): Promise<Buffer> {
|
|
26
|
+
return new Promise((resolve, reject) => {
|
|
27
|
+
const chunks: Buffer[] = [];
|
|
28
|
+
request.on("data", (chunk: Buffer) => chunks.push(chunk));
|
|
29
|
+
request.on("end", () => resolve(Buffer.concat(chunks)));
|
|
30
|
+
request.on("error", reject);
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function handler(request: http.IncomingMessage, response: http.ServerResponse) {
|
|
35
|
+
const host = request.headers.host;
|
|
36
|
+
const baseUrl = `http://${host}`;
|
|
37
|
+
const pathname = new URL(request.url ?? "/", baseUrl).pathname;
|
|
38
|
+
|
|
39
|
+
if (request.method === "POST" && pathname === "/v1/creator/challenges/init") {
|
|
40
|
+
const bodyBuffer = await readBody(request);
|
|
41
|
+
const payload = JSON.parse(bodyBuffer.toString("utf8"));
|
|
42
|
+
|
|
43
|
+
sendJson(response, 201, {
|
|
44
|
+
data: {
|
|
45
|
+
challenge: {
|
|
46
|
+
id: "challenge-integration-id",
|
|
47
|
+
slug: payload.slug,
|
|
48
|
+
title: payload.title,
|
|
49
|
+
type: "EXPRESS_NODE",
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
success: true,
|
|
53
|
+
});
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const starterUploadMatch = pathname.match(
|
|
58
|
+
/^\/v1\/creator\/challenges\/([^/]+)\/artifacts\/starter\/upload-url$/,
|
|
59
|
+
);
|
|
60
|
+
if (request.method === "POST" && starterUploadMatch) {
|
|
61
|
+
const slug = decodeURIComponent(starterUploadMatch[1] ?? "");
|
|
62
|
+
|
|
63
|
+
sendJson(response, 200, {
|
|
64
|
+
data: {
|
|
65
|
+
challengeId: "challenge-integration-id",
|
|
66
|
+
challengeSlug: slug,
|
|
67
|
+
upload: {
|
|
68
|
+
expiresInSeconds: 900,
|
|
69
|
+
headers: { "content-type": "application/gzip" },
|
|
70
|
+
key: "challenges/challenge-integration-id/starter/mock.tar.gz",
|
|
71
|
+
method: "PUT",
|
|
72
|
+
url: `${baseUrl}/upload/starter`,
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
success: true,
|
|
76
|
+
});
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const testsUploadMatch = pathname.match(
|
|
81
|
+
/^\/v1\/creator\/challenges\/([^/]+)\/artifacts\/tests\/upload-url$/,
|
|
82
|
+
);
|
|
83
|
+
if (request.method === "POST" && testsUploadMatch) {
|
|
84
|
+
const slug = decodeURIComponent(testsUploadMatch[1] ?? "");
|
|
85
|
+
|
|
86
|
+
sendJson(response, 200, {
|
|
87
|
+
data: {
|
|
88
|
+
challengeId: "challenge-integration-id",
|
|
89
|
+
challengeSlug: slug,
|
|
90
|
+
upload: {
|
|
91
|
+
expiresInSeconds: 900,
|
|
92
|
+
headers: { "content-type": "application/gzip" },
|
|
93
|
+
key: "challenges/challenge-integration-id/tests/mock.tar.gz",
|
|
94
|
+
method: "PUT",
|
|
95
|
+
url: `${baseUrl}/upload/tests`,
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
success: true,
|
|
99
|
+
});
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (request.method === "PUT" && pathname === "/upload/starter") {
|
|
104
|
+
const bodyBuffer = await readBody(request);
|
|
105
|
+
uploadStats.starterUploads += 1;
|
|
106
|
+
uploadStats.starterBytes += bodyBuffer.byteLength;
|
|
107
|
+
response.writeHead(200);
|
|
108
|
+
response.end();
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (request.method === "PUT" && pathname === "/upload/tests") {
|
|
113
|
+
const bodyBuffer = await readBody(request);
|
|
114
|
+
uploadStats.testsUploads += 1;
|
|
115
|
+
uploadStats.testsBytes += bodyBuffer.byteLength;
|
|
116
|
+
response.writeHead(200);
|
|
117
|
+
response.end();
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
response.writeHead(404);
|
|
122
|
+
response.end();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
async start() {
|
|
127
|
+
server = http.createServer((request, response) => {
|
|
128
|
+
void handler(request, response);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
await new Promise<void>((resolve) => {
|
|
132
|
+
server?.listen(0, "127.0.0.1", resolve);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
const address = server?.address();
|
|
136
|
+
|
|
137
|
+
if (!address || typeof address === "string") {
|
|
138
|
+
throw new Error("Unable to read test server address.");
|
|
139
|
+
}
|
|
140
|
+
const info = address as AddressInfo;
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
baseUrl: `http://127.0.0.1:${info.port}`,
|
|
144
|
+
};
|
|
145
|
+
},
|
|
146
|
+
async stop() {
|
|
147
|
+
if (!server) {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
await new Promise<void>((resolve) => server?.close(() => resolve()));
|
|
152
|
+
server = undefined;
|
|
153
|
+
},
|
|
154
|
+
uploadStats,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export async function runSubmitFlowIntegrationTests() {
|
|
159
|
+
const tempRoot = await createTempDir("em-cli-integration-");
|
|
160
|
+
const server = createTestBackendServer();
|
|
161
|
+
|
|
162
|
+
try {
|
|
163
|
+
const { baseUrl } = await server.start();
|
|
164
|
+
const workspaceRelative = "integration-workspace";
|
|
165
|
+
const workspaceAbsolute = path.join(tempRoot, workspaceRelative);
|
|
166
|
+
|
|
167
|
+
await runCli(
|
|
168
|
+
[
|
|
169
|
+
"challenge",
|
|
170
|
+
"init",
|
|
171
|
+
"--slug",
|
|
172
|
+
"integration-challenge",
|
|
173
|
+
"--title",
|
|
174
|
+
"Integration Challenge",
|
|
175
|
+
"--description",
|
|
176
|
+
"Create a deterministic express middleware that validates query params and returns JSON.",
|
|
177
|
+
"--constraints",
|
|
178
|
+
"Use only express and built-in Node modules. Keep behavior deterministic and offline.",
|
|
179
|
+
"--backend",
|
|
180
|
+
baseUrl,
|
|
181
|
+
"--dir",
|
|
182
|
+
workspaceRelative,
|
|
183
|
+
],
|
|
184
|
+
{
|
|
185
|
+
cwd: tempRoot,
|
|
186
|
+
log: () => {},
|
|
187
|
+
},
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
await runCli(
|
|
191
|
+
["challenge", "submit", "--backend", baseUrl, "--dir", workspaceAbsolute],
|
|
192
|
+
{
|
|
193
|
+
cwd: tempRoot,
|
|
194
|
+
log: () => {},
|
|
195
|
+
packageDirectory: async () => Buffer.from("fake-tar-gz-bytes"),
|
|
196
|
+
},
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
assert.equal(server.uploadStats.starterUploads, 1);
|
|
200
|
+
assert.equal(server.uploadStats.testsUploads, 1);
|
|
201
|
+
assert.equal(server.uploadStats.starterBytes > 0, true);
|
|
202
|
+
assert.equal(server.uploadStats.testsBytes > 0, true);
|
|
203
|
+
} finally {
|
|
204
|
+
await server.stop();
|
|
205
|
+
await fs.rm(tempRoot, { force: true, recursive: true });
|
|
206
|
+
}
|
|
207
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { runSubmitFlowIntegrationTests } from "./integration/submit-flow.spec.js";
|
|
2
|
+
import { runCliCommandsUnitTests } from "./unit/cli-commands.spec.js";
|
|
3
|
+
|
|
4
|
+
type TestEntry = {
|
|
5
|
+
name: string;
|
|
6
|
+
run: () => Promise<void>;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
const tests: TestEntry[] = [
|
|
10
|
+
{
|
|
11
|
+
name: "unit: cli-commands",
|
|
12
|
+
run: runCliCommandsUnitTests,
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
name: "integration: submit-flow",
|
|
16
|
+
run: runSubmitFlowIntegrationTests,
|
|
17
|
+
},
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
async function main() {
|
|
21
|
+
let failed = 0;
|
|
22
|
+
|
|
23
|
+
for (const test of tests) {
|
|
24
|
+
try {
|
|
25
|
+
await test.run();
|
|
26
|
+
console.log(`[PASS] ${test.name}`);
|
|
27
|
+
} catch (error: unknown) {
|
|
28
|
+
failed += 1;
|
|
29
|
+
console.error(`[FAIL] ${test.name}`);
|
|
30
|
+
console.error(error);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (failed > 0) {
|
|
35
|
+
process.exit(1);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
console.log(`All tests passed (${tests.length}).`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
void main();
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
import { runChallengeInit } from "../../src/commands/challenge-init.js";
|
|
6
|
+
import { runChallengeValidate } from "../../src/commands/challenge-validate.js";
|
|
7
|
+
import { readJsonFile } from "../../src/lib/fs-utils.js";
|
|
8
|
+
import type {
|
|
9
|
+
ArtifactUploadResponse,
|
|
10
|
+
BackendClientContract,
|
|
11
|
+
ChallengeDraftPayload,
|
|
12
|
+
} from "../../src/types.js";
|
|
13
|
+
import {
|
|
14
|
+
copyDir,
|
|
15
|
+
createTempDir,
|
|
16
|
+
resolveFixturePath,
|
|
17
|
+
resolveSnapshotPath,
|
|
18
|
+
} from "../helpers/test-utils.js";
|
|
19
|
+
|
|
20
|
+
class FakeBackendClient implements BackendClientContract {
|
|
21
|
+
public async createChallengeDraft(input: ChallengeDraftPayload) {
|
|
22
|
+
return {
|
|
23
|
+
id: "challenge-snapshot-id",
|
|
24
|
+
slug: input.slug,
|
|
25
|
+
title: input.title,
|
|
26
|
+
type: "EXPRESS_NODE" as const,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
public async requestStarterUpload(_slug: string): Promise<ArtifactUploadResponse> {
|
|
31
|
+
throw new Error("Not used in this unit test.");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
public async requestTestsUpload(_slug: string): Promise<ArtifactUploadResponse> {
|
|
35
|
+
throw new Error("Not used in this unit test.");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
public async uploadArtifact(_upload: ArtifactUploadResponse["upload"], _fileBuffer: Buffer) {
|
|
39
|
+
throw new Error("Not used in this unit test.");
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function runCliCommandsUnitTests() {
|
|
44
|
+
const tempRoot = await createTempDir("em-cli-unit-");
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const initResult = await runChallengeInit({
|
|
48
|
+
backendClient: new FakeBackendClient(),
|
|
49
|
+
cwd: tempRoot,
|
|
50
|
+
flags: {
|
|
51
|
+
constraints:
|
|
52
|
+
"Use only express and built-in APIs. Keep runtime deterministic and avoid external services.",
|
|
53
|
+
description:
|
|
54
|
+
"Build a middleware that validates incoming headers and returns clear JSON error responses.",
|
|
55
|
+
slug: "snapshot-challenge",
|
|
56
|
+
title: "Snapshot Challenge",
|
|
57
|
+
},
|
|
58
|
+
log: () => {},
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const normalizedInitResult = {
|
|
62
|
+
...initResult,
|
|
63
|
+
workspaceDir: "<workspace>",
|
|
64
|
+
};
|
|
65
|
+
const snapshotPath = resolveSnapshotPath("init-result.json");
|
|
66
|
+
const expectedSnapshot = JSON.parse(await fs.readFile(snapshotPath, "utf8")) as {
|
|
67
|
+
challengeId: string;
|
|
68
|
+
slug: string;
|
|
69
|
+
workspaceDir: string;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
assert.deepEqual(normalizedInitResult, expectedSnapshot);
|
|
73
|
+
|
|
74
|
+
const configPath = path.join(initResult.workspaceDir, "challenge.config.json");
|
|
75
|
+
const config = await readJsonFile<{
|
|
76
|
+
slug: string;
|
|
77
|
+
type: string;
|
|
78
|
+
}>(configPath);
|
|
79
|
+
assert.equal(config.slug, "snapshot-challenge");
|
|
80
|
+
assert.equal(config.type, "EXPRESS_NODE");
|
|
81
|
+
|
|
82
|
+
const validResult = await runChallengeValidate({
|
|
83
|
+
cwd: initResult.workspaceDir,
|
|
84
|
+
flags: {},
|
|
85
|
+
log: () => {},
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
assert.equal(validResult.ok, true);
|
|
89
|
+
|
|
90
|
+
const invalidFixtureSource = resolveFixturePath("invalid-workspace");
|
|
91
|
+
const invalidFixtureTarget = path.join(tempRoot, "invalid-workspace");
|
|
92
|
+
await copyDir(invalidFixtureSource, invalidFixtureTarget);
|
|
93
|
+
|
|
94
|
+
const invalidResult = await runChallengeValidate({
|
|
95
|
+
cwd: invalidFixtureTarget,
|
|
96
|
+
flags: {},
|
|
97
|
+
log: () => {},
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
assert.equal(invalidResult.ok, false);
|
|
101
|
+
assert.equal(invalidResult.errors.length > 0, true);
|
|
102
|
+
} finally {
|
|
103
|
+
await fs.rm(tempRoot, { force: true, recursive: true });
|
|
104
|
+
}
|
|
105
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"lib": ["ES2022", "DOM"],
|
|
7
|
+
"types": ["node"],
|
|
8
|
+
"rootDir": ".",
|
|
9
|
+
"outDir": "./dist",
|
|
10
|
+
"sourceMap": true,
|
|
11
|
+
"declaration": true,
|
|
12
|
+
"declarationMap": true,
|
|
13
|
+
"strict": true,
|
|
14
|
+
"verbatimModuleSyntax": true,
|
|
15
|
+
"isolatedModules": true,
|
|
16
|
+
"skipLibCheck": true
|
|
17
|
+
},
|
|
18
|
+
"include": ["src/**/*.ts", "tests/**/*.ts"],
|
|
19
|
+
"exclude": ["dist", "node_modules"]
|
|
20
|
+
}
|