|
1 | | -import { mkdirSync, rmSync, writeFileSync } from "node:fs"; |
| 1 | +import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; |
2 | 2 | import { join } from "node:path"; |
3 | 3 | import test from "ava"; |
4 | 4 | import { Text } from "ink"; |
5 | 5 | import { render } from "ink-testing-library"; |
6 | 6 | import { useKeyInput } from "../components/index.js"; |
7 | 7 | import { loadTrainingData } from "../lib/data.js"; |
8 | 8 | import { streamPreview } from "./chat.js"; |
| 9 | +import { DataExportCommand } from "./data/export.js"; |
9 | 10 | import { DataImportCommand } from "./data/import.js"; |
10 | 11 | import { DataListCommand } from "./data/list.js"; |
11 | 12 | import { DataValidateCommand } from "./data/validate.js"; |
@@ -60,6 +61,19 @@ function writeExamples(lines: object[]) { |
60 | 61 | ); |
61 | 62 | } |
62 | 63 |
|
| 64 | +function writeEvalExamples(lines: object[]) { |
| 65 | + writeFileSync( |
| 66 | + join(DATA_DIR, "valid.jsonl"), |
| 67 | + `${lines.map((l) => JSON.stringify(l)).join("\n")}\n`, |
| 68 | + ); |
| 69 | +} |
| 70 | + |
| 71 | +const settle = () => new Promise((resolve) => setTimeout(resolve, 60)); |
| 72 | + |
| 73 | +function userContent(example: { messages: { role: string; content: string }[] }) { |
| 74 | + return example.messages.find((m) => m.role === "user")?.content; |
| 75 | +} |
| 76 | + |
63 | 77 | function example(userInput: string) { |
64 | 78 | return { |
65 | 79 | messages: [ |
@@ -293,3 +307,82 @@ test("streamPreview clips a single very long line by characters", (t) => { |
293 | 307 | t.true(truncated); |
294 | 308 | t.is(text.length, 2000); |
295 | 309 | }); |
| 310 | + |
| 311 | +// ── data list edits the set it was opened on ────────────────────────── |
| 312 | + |
| 313 | +test.serial( |
| 314 | + "DataListCommand with --eval edits valid.jsonl and leaves train.jsonl alone", |
| 315 | + async (t) => { |
| 316 | + // Regression: the edit path called updateTrainingExample/loadTrainingData |
| 317 | + // without isEval, so editing a validation example overwrote the training |
| 318 | + // example at the same index instead. |
| 319 | + const originalTTY = process.stdin.isTTY; |
| 320 | + try { |
| 321 | + setupProject(); |
| 322 | + writeExamples([example("train-one")]); |
| 323 | + writeEvalExamples([example("valid-one")]); |
| 324 | + process.stdin.isTTY = true; |
| 325 | + |
| 326 | + const instance = render(<DataListCommand isEval />); |
| 327 | + await settle(); |
| 328 | + instance.stdin.write("e"); // enter edit mode |
| 329 | + await settle(); |
| 330 | + instance.stdin.write("\r"); // submit user input unchanged |
| 331 | + await settle(); |
| 332 | + instance.stdin.write("\r"); // submit assistant output unchanged |
| 333 | + await settle(); |
| 334 | + instance.unmount(); |
| 335 | + |
| 336 | + t.is(userContent(loadTrainingData(false)[0]), "train-one"); |
| 337 | + t.is(userContent(loadTrainingData(true)[0]), "valid-one"); |
| 338 | + t.is(loadTrainingData(false).length, 1); |
| 339 | + t.is(loadTrainingData(true).length, 1); |
| 340 | + } finally { |
| 341 | + process.stdin.isTTY = originalTTY; |
| 342 | + teardown(); |
| 343 | + } |
| 344 | + }, |
| 345 | +); |
| 346 | + |
| 347 | +// ── data export honours --eval ──────────────────────────────────────── |
| 348 | + |
| 349 | +test.serial("DataExportCommand exports training data by default", async (t) => { |
| 350 | + try { |
| 351 | + setupProject(); |
| 352 | + writeExamples([example("train-one"), example("train-two")]); |
| 353 | + writeEvalExamples([example("valid-one")]); |
| 354 | + |
| 355 | + await renderCommand( |
| 356 | + <DataExportCommand file="out.jsonl" yes />, |
| 357 | + "Export complete!", |
| 358 | + ); |
| 359 | + |
| 360 | + const written = readFileSync(join(TEST_DIR, "out.jsonl"), "utf-8").trim(); |
| 361 | + t.is(written.split("\n").length, 2); |
| 362 | + t.true(written.includes("train-one")); |
| 363 | + t.false(written.includes("valid-one")); |
| 364 | + } finally { |
| 365 | + teardown(); |
| 366 | + } |
| 367 | +}); |
| 368 | + |
| 369 | +test.serial("DataExportCommand with --eval exports the validation set", async (t) => { |
| 370 | + try { |
| 371 | + setupProject(); |
| 372 | + writeExamples([example("train-one"), example("train-two")]); |
| 373 | + writeEvalExamples([example("valid-one")]); |
| 374 | + |
| 375 | + const output = await renderCommand( |
| 376 | + <DataExportCommand file="out.jsonl" yes isEval />, |
| 377 | + "Export complete!", |
| 378 | + ); |
| 379 | + |
| 380 | + t.true(output.includes("Export Validation Data")); |
| 381 | + const written = readFileSync(join(TEST_DIR, "out.jsonl"), "utf-8").trim(); |
| 382 | + t.is(written.split("\n").length, 1); |
| 383 | + t.true(written.includes("valid-one")); |
| 384 | + t.false(written.includes("train-one")); |
| 385 | + } finally { |
| 386 | + teardown(); |
| 387 | + } |
| 388 | +}); |
0 commit comments