playthrough 0.3.1 → 0.4.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.
@@ -1,11 +1,12 @@
1
1
  #!/usr/bin/env -S node --disable-warning=MODULE_TYPELESS_PACKAGE_JSON
2
- import { A as problemsFinishing, C as Refused, D as inOrderJoined, E as conversation, M as list, N as quote, O as lastSeen, P as time, T as allNotes, _ as onPage, b as readingsOn, g as hashOf, j as roundInProgress, k as notesToAnswer, m as firstLine } from "./reporter-BEodHYRI.js";
3
- import { D as pausedAt, E as repo, S as giveBackLeft, T as setupIn, _ as keepRunning, a as failedAt, c as makeFlowsFolder, d as walkFork, f as whereItStopped, g as shownAt, h as placeIn, i as called, l as runFlows, m as compareWithEarlier, o as howToWriteFlows, p as trySetup, s as isFlow, t as ALONGSIDE, v as serves, y as stopApp } from "./flows-BcISV1M-.js";
2
+ import { A as list, C as conversation, D as problemsFinishing, E as notesToAnswer, M as time, O as roundInProgress, S as allNotes, T as lastSeen, _ as readingsOn, b as Refused, j as quote, k as toAccountFor, p as firstLine, w as inOrderJoined } from "./reporter-DE8aueAO.js";
3
+ import { D as setupIn, O as repo, _ as placeIn, a as failedAt, b as serves, c as isFlow, f as walkFork, g as compareWithEarlier, h as writtenNow, i as called, k as pausedAt, l as makeFlowsFolder, m as trySetup, o as flowsOf, p as whereItStopped, s as howToWriteFlows, t as ALONGSIDE, u as runFlows, v as shownAt, w as giveBackLeft, x as stopApp, y as keepRunning } from "./flows-BkG0TgHA.js";
4
4
  import { t as read } from "./read-Bg9XVTtn.js";
5
5
  import { createRequire } from "node:module";
6
6
  import { copyFileSync, cpSync, existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
7
7
  import { execFileSync } from "node:child_process";
8
8
  import { readFile, readdir, stat, writeFile } from "node:fs/promises";
9
+ import { createHash } from "node:crypto";
9
10
  import { constants, networkInterfaces, tmpdir, userInfo } from "node:os";
10
11
  import { dirname, extname, join, normalize, resolve, sep } from "node:path";
11
12
  import { chromium, request } from "@playwright/test";
@@ -21,9 +22,27 @@ if (major < 24) {
21
22
  // src/before.ts
22
23
  function keepBefore(repo, id) {
23
24
  if (!attempt(() => git(repo, ["rev-parse", "--is-inside-work-tree"]))) return void 0;
25
+ const head = headOf(repo);
26
+ if (!head) return void 0;
24
27
  try {
25
- const head = headOf(repo);
26
- return keep(repo, id, treeNow(repo), head ? [head] : []);
28
+ const commit = git(repo, [
29
+ "commit-tree",
30
+ "--no-gpg-sign",
31
+ "-p",
32
+ head,
33
+ "-m",
34
+ `Before, of the review ${id}: the app as it was, with what wasn't committed`,
35
+ treeNow(repo)
36
+ ], {
37
+ ...process.env,
38
+ ...PLAYTHROUGH
39
+ });
40
+ git(repo, [
41
+ "update-ref",
42
+ `refs/playthrough/${id}/before`,
43
+ commit
44
+ ]);
45
+ return commit;
27
46
  } catch (error) {
28
47
  throw new Refused([
29
48
  "Before can't be kept as a commit, because git says:",
@@ -32,179 +51,82 @@ function keepBefore(repo, id) {
32
51
  ]);
33
52
  }
34
53
  }
35
- const keptWithOursNow = (repo, id, commit) => keep(repo, id, treeNow(repo, {
36
- commit,
37
- paths: ["playthrough"]
38
- }), git(repo, [
39
- "rev-list",
40
- "--parents",
41
- "--max-count=1",
42
- commit
43
- ]).split(" ").slice(1));
44
- function keep(repo, id, tree, parents) {
45
- const commit = git(repo, [
46
- "commit-tree",
47
- "--no-gpg-sign",
48
- ...parents.flatMap((parent) => ["-p", parent]),
49
- "-m",
50
- `Before, of the review ${id}: the app as it was, with what wasn't committed`,
51
- tree
52
- ], {
53
- ...process.env,
54
- ...PLAYTHROUGH
55
- });
56
- git(repo, [
57
- "update-ref",
58
- `refs/playthrough/${id}/before`,
59
- commit
60
- ]);
61
- return commit;
62
- }
63
- function treeNow(repo, onto) {
54
+ function treeNow(repo) {
64
55
  const folder = mkdtempSync(join(tmpdir(), "playthrough-"));
65
56
  try {
66
57
  const index = join(folder, "index");
67
- const env = {
58
+ attempt(() => copyFileSync(resolve(repo.root, git(repo, [
59
+ "rev-parse",
60
+ "--git-path",
61
+ "index"
62
+ ])), index));
63
+ git(repo, [
64
+ "add",
65
+ "--all",
66
+ ":/"
67
+ ], {
68
68
  ...process.env,
69
69
  GIT_INDEX_FILE: index
70
- };
71
- if (onto) {
72
- git(repo, ["read-tree", onto.commit], env);
73
- git(repo, [
74
- "rm",
75
- "-r",
76
- "--cached",
77
- "--force",
78
- "--quiet",
79
- "--ignore-unmatch",
80
- "--",
81
- ...onto.paths
82
- ], env);
83
- git(repo, [
84
- "add",
85
- "--all",
86
- "--",
87
- ...onto.paths
88
- ], env);
89
- } else {
90
- attempt(() => copyFileSync(resolve(repo.root, git(repo, [
91
- "rev-parse",
92
- "--git-path",
93
- "index"
94
- ])), index));
95
- git(repo, [
96
- "add",
97
- "--all",
98
- ":/"
99
- ], env);
100
- }
101
- return git(repo, ["write-tree"], env);
70
+ });
71
+ return git(repo, ["write-tree"], {
72
+ ...process.env,
73
+ GIT_INDEX_FILE: index
74
+ });
102
75
  } finally {
103
76
  rmSync(folder, { recursive: true });
104
77
  }
105
78
  }
106
- const changedSince = (repo, commit, paths) => attempt(() => git(repo, [
107
- "diff",
108
- "--quiet",
109
- `${commit}^{tree}`,
110
- treeNow(repo),
111
- "--",
112
- ...paths
113
- ])) === void 0;
114
79
  const PLAYTHROUGH = {
115
80
  GIT_AUTHOR_NAME: "Playthrough",
116
81
  GIT_AUTHOR_EMAIL: "playthrough@localhost",
117
82
  GIT_COMMITTER_NAME: "Playthrough",
118
83
  GIT_COMMITTER_EMAIL: "playthrough@localhost"
119
84
  };
120
- async function recordOnBefore(repo, id, number, flows) {
121
- const before = (await repo.reviews.get(id)).rounds.find((round) => round.number === 0);
122
- if (!before) return;
123
- const later = flows.filter((name) => !before.flows.some((flow) => flow.name === name) && !before.cantStart?.includes(name));
124
- if (!later.length) return;
125
- const [are, they, have] = later.length > 1 ? [
126
- "are",
127
- "they're",
128
- "have"
129
- ] : [
130
- "is",
131
- "it's",
132
- "has"
133
- ];
134
- const named = called(later);
135
- await onBefore(repo, id, later, `${named} ${are} new, so ${they} recorded on Before too`, `Round ${number} wasn't recorded`, "Record again.", (why) => `${named} ${have} no Before: ${why}.`);
136
- }
137
- async function changedOnBefore(repo, review) {
138
- const before = review.rounds.find((round) => round.number === 0);
139
- if (!before) return [];
140
- const handedOver = review.rounds.some((round) => round.number > 0 && round.finished);
141
- const flows = [...before.flows.filter((flow) => !handedOver || flow.stopped).map((flow) => flow.name), ...before.cantStart ?? []];
142
- const ran = await ranOnBefore(repo, review.id);
143
- return flows.filter((name) => !ran[name] || hashOf(join(repo.flows(review.id), ran[name].file)) !== ran[name].hash);
144
- }
145
- const ranOnBefore = (repo, id) => readFile(repo.ranOnBefore(id), "utf8").then(JSON.parse, () => ({}));
146
- async function noteRanOnBefore(repo, id, written, gone = []) {
147
- const ran = {
148
- ...await ranOnBefore(repo, id),
149
- ...written
150
- };
151
- for (const name of gone) delete ran[name];
152
- await writeFile(repo.ranOnBefore(id), JSON.stringify(ran));
153
- }
154
- async function tryBeforeAgain(repo, id, flows, again) {
155
- const [has, they, them] = flows.length > 1 ? [
156
- "have",
157
- "they're",
158
- "them"
159
- ] : [
160
- "has",
161
- "it's",
162
- "it"
163
- ];
164
- const named = called(flows);
165
- await onBefore(repo, id, flows, `${named} ${has} changed since ${flows.length > 1 ? "they" : "it"} last ran on Before, so ${they} recorded on it again, in place of what it has of ${them}`, "Before wasn't recorded again", again, (why) => `Before can't record ${named} again: ${why}.`);
166
- }
167
- async function recordBeforeAgain(repo, id, command, again) {
85
+ const handedOver = (review, flow) => review.rounds.some((round) => round.number > 0 && round.finished && round.flows.some((kept) => kept.name === flow));
86
+ async function recordOnBefore(repo, id, number, { recorded, written }) {
168
87
  const review = await repo.reviews.get(id);
169
88
  const before = review.rounds.find((round) => round.number === 0);
170
- if (!before?.commit || review.rounds.some((round) => round.number > 0 && round.finished) || !changedSince(repo, before.commit, ["playthrough"])) return false;
171
- await trySetup(repo, command);
172
- return onBefore(repo, id, [...before.flows.map((flow) => flow.name), ...before.cantStart ?? []], `playthrough/ has changed since Before was recorded, so Before is recorded again on it, in place of what it has`, "Before wasn't recorded again", again, (why) => `Before stays as it was recorded, on playthrough/ as it was then: ${why}.`, true);
173
- }
174
- async function onBefore(repo, id, flows, doing, unrecorded, again, cant, whole = false) {
175
- const before = (await repo.reviews.get(id)).rounds.find((round) => round.number === 0);
176
- const unchanged = !!before.commit && !changedSince(repo, before.commit, [":/", ":(exclude)playthrough"]);
177
- const why = !before.commit ? "Before wasn't kept as a commit, so the app as it was can't be put back" : !unchanged && !headOf(repo) ? UNBORN : void 0;
178
- if (why) {
179
- console.log(`\n${cant(why)}`);
180
- return false;
181
- }
182
- const later = flows;
89
+ if (!before) return;
90
+ const ran = await readFile(repo.ranOnBefore(id), "utf8").then(JSON.parse, () => ({}));
91
+ const setup = await writtenNow(repo);
92
+ const follows = Object.fromEntries(recorded.map(({ flow, doneOn }) => [flow.name, createHash("sha256").update(JSON.stringify([
93
+ written[flow.name]?.hash,
94
+ setup,
95
+ doneOn ?? {}
96
+ ])).digest("hex")]));
97
+ const on = [...before.flows.map((flow) => flow.name), ...before.cantStart ?? []];
98
+ const flows = recorded.filter(({ flow }) => !on.includes(flow.name) || !handedOver(review, flow.name) && ran[flow.name] !== follows[flow.name]);
99
+ const gone = on.filter((name) => !handedOver(review, name) && !recorded.some(({ flow }) => flow.name === name));
100
+ if (!flows.length && !gone.length) return;
101
+ const names = flows.map(({ flow }) => flow.name);
102
+ const unrecorded = `Round ${number} wasn't recorded`;
103
+ const why = !before.commit ? "Before wasn't kept as a commit, so the app as it was can't be put back" : !headOf(repo) ? UNBORN : void 0;
104
+ if (why) return void (names.length && console.log(`\n${called(names)} ${names.length > 1 ? "have" : "has"} no Before: ${why}.`));
183
105
  const taken = await repo.reviews.files(id, 0);
184
- const run = () => runFlows(repo, id, 0, {
185
- later: {
186
- flows: later,
187
- taken
188
- },
189
- first: () => true
190
- }).catch((error) => {
106
+ const doneInRound = Object.fromEntries(flows.map(({ flow, doneOn }) => [flow.name, doneOn ?? {}]));
107
+ const run = () => runFlows(repo, id, 0, { before: {
108
+ flows: names,
109
+ taken,
110
+ doneInRound
111
+ } }).catch((error) => {
191
112
  if (error instanceof Refused) return {
192
113
  recorded: [],
193
- failed: later.map((flow) => ({ flow })),
114
+ failed: names.map((flow) => ({ flow })),
194
115
  written: {}
195
116
  };
196
117
  throw error;
197
118
  });
198
- let ran;
199
- if (unchanged) {
200
- console.log(`\n${doing}, on the app as it is: nothing has changed since Before.`);
201
- ran = await run();
202
- } else {
119
+ let kept = {
120
+ recorded: [],
121
+ failed: [],
122
+ written: {}
123
+ };
124
+ if (names.length) {
203
125
  const { url } = await repo.app();
204
126
  await stopApp(url);
205
127
  const shared = await serves(url);
206
- console.log(`\n${doing}. Your work is set aside with git stash meanwhile${shared ? `, and what serves ${url} serves the app as it was` : ""}.`);
207
- ran = await asItWas(repo, unrecorded, before.commit, shared, async () => {
128
+ console.log(`\nYour work is set aside with git stash while ${names.length > 1 ? "they record" : "it records"} on Before${shared ? `, and what serves ${url} serves the app as it was` : ""}.`);
129
+ kept = await asItWas(repo, unrecorded, before.commit, shared, async () => {
208
130
  if (shared) await caughtUp(url);
209
131
  try {
210
132
  return await run();
@@ -213,21 +135,17 @@ async function onBefore(repo, id, flows, doing, unrecorded, again, cant, whole =
213
135
  }
214
136
  });
215
137
  if (shared) await caughtUp(url);
138
+ refuseIfBroken(kept.failed, unrecorded, "Record again.");
216
139
  }
217
- refuseIfBroken(ran.failed, unrecorded, again);
218
- const commit = whole ? keptWithOursNow(repo, id, before.commit) : void 0;
219
- const gone = later.filter((name) => !ran.recorded.some(({ flow }) => flow.name === name) && !ran.failed.some(({ flow }) => flow === name));
220
140
  await repo.reviews.addToBefore(id, {
221
- flows: ran.recorded.map(({ flow }) => flow),
222
- cantStart: cantStart(ran.recorded, ran.failed),
223
- files: ran.recorded.flatMap(({ files }) => files),
224
- ...commit && { commit },
141
+ flows: kept.recorded.map(({ flow }) => flow),
142
+ cantStart: cantStart(kept.recorded, kept.failed),
143
+ files: kept.recorded.flatMap(({ files }) => files),
225
144
  gone
226
145
  });
227
- await noteRanOnBefore(repo, id, ran.written, gone);
228
- if (gone.length) console.log(`\n${gone.length > 3 ? `${gone.length} of them` : list(gone.map(quote))} ${gone.length > 1 ? "aren't flows" : "isn't a flow"} any more, so Before lets ${gone.length > 1 ? "them" : "it"} go.`);
229
- for (const line of howFar(repo, id, ran.recorded, ran.failed, "the app as it was")) console.log(line);
230
- return true;
146
+ for (const name of names) ran[name] = follows[name];
147
+ for (const name of gone) delete ran[name];
148
+ await writeFile(repo.ranOnBefore(id), JSON.stringify(ran));
231
149
  }
232
150
  function refuseIfBroken(failed, unrecorded, again) {
233
151
  const broken = failed.filter((failure) => failure.broken);
@@ -238,15 +156,6 @@ function refuseIfBroken(failed, unrecorded, again) {
238
156
  ]);
239
157
  }
240
158
  const cantStart = (recorded, failed) => failed.map(({ flow }) => flow).filter((flow) => !recorded.some((kept) => kept.flow.name === flow));
241
- function howFar(repo, id, recorded, failed, app) {
242
- const lines = failed.map(({ flow, at, why, on }) => {
243
- const because = why ? `, where Playwright says “${why}”` : "";
244
- return !recorded.some((kept) => kept.flow.name === flow) ? `${quote(flow)} can't start on ${app}${because}, so it has no Before.` : `${quote(flow)} ${at ? `stops at ${quote(at)}${onPage(on)}${because}` : "stops after its last step"}, so its Before goes as far as that.`;
245
- });
246
- const where = existsSync(repo.failed(id)) ? ` ${whereItStopped(repo, id, failed)}` : "";
247
- if (lines.length) lines.push(`If that's the app as it was, that's Before. If it's the flow's own mistake, fix the flow and run playthrough start ${id} again: it records a flow changed since on Before again.${where}`);
248
- return lines;
249
- }
250
159
  const SIGNALS = [
251
160
  "SIGINT",
252
161
  "SIGTERM",
@@ -1517,19 +1426,14 @@ async function record(repo, id) {
1517
1426
  const review = await repo.reviews.get(id);
1518
1427
  if (review.accepted) throw new Refused(["This review is accepted, so nothing more can be recorded."]);
1519
1428
  const number = review.rounds.length ? roundInProgress(review) : 1;
1520
- const onBefore = !!review.rounds.find((round) => round.number === 0)?.commit;
1521
- const first = (flow) => !onBefore && !review.rounds.some((round) => round.number !== number && round.flows.some((kept) => kept.name === flow));
1522
- if (number === 1 && !await recordBeforeAgain(repo, id, `playthrough record ${id}`, "Record again.")) {
1523
- const again = await changedOnBefore(repo, review);
1524
- if (again.length) await tryBeforeAgain(repo, id, again, "Record again.");
1525
- }
1526
- const { recorded, failed } = await runFlows(repo, id, number, { first });
1429
+ const ran = await runFlows(repo, id, number, { first: (flow) => !handedOver(review, flow) });
1430
+ const { recorded, failed } = ran;
1527
1431
  if (failed.length) throw new Refused([
1528
1432
  `\nRound ${number} wasn't recorded, because ${failed.length > 1 ? "these flows" : "a flow"} failed:`,
1529
1433
  ...failed.map(failedAt),
1530
1434
  `Fix ${failed.length > 1 ? "them" : "it"} and record again, which runs ${failed.length > 1 ? "them" : "it"} first. ${whereItStopped(repo, id, failed)}`
1531
1435
  ]);
1532
- await recordOnBefore(repo, id, number, recorded.map(({ flow }) => flow.name));
1436
+ await recordOnBefore(repo, id, number, ran);
1533
1437
  const saved = review.rounds.find((round) => round.number === number);
1534
1438
  const joined = inOrderJoined(review);
1535
1439
  const place = (name) => joined.includes(name) ? joined.indexOf(name) : joined.length;
@@ -1565,11 +1469,30 @@ async function record(repo, id) {
1565
1469
  const left = problemsFinishing(await repo.reviews.get(id), number);
1566
1470
  console.log(`\nRecorded round ${number}: ${flows.length > 3 ? called(flows.map((flow) => flow.name)) : list(flows.map((flow) => `${quote(flow.name)} (${Math.round(flow.length)}s)`))}.`);
1567
1471
  const since = comparisons.find((comparison) => comparison.since === number - 1);
1568
- if (since) console.log(`\n${changed(since.changes.length, number - 1)}`);
1472
+ const named = toAccountFor(since);
1473
+ if (since) console.log(`\n${changed(named.length, number - 1)}`);
1474
+ const pictured = since?.changes.filter((change) => change.pictureOnly) ?? [];
1475
+ if (pictured.length) {
1476
+ const [parts, look, them, need] = pictured.length > 1 ? [
1477
+ `${pictured.length} parts of the page`,
1478
+ "look",
1479
+ "them",
1480
+ "they need"
1481
+ ] : [
1482
+ "One part of the page",
1483
+ "looks",
1484
+ "it",
1485
+ "it needs"
1486
+ ];
1487
+ console.log(`${parts} only ${look} different in the pictures, with nothing read there changed${pictured.length > 3 ? "" : `: ${list(pictured.map((change) => quote(change.words)))}`}. The person sees ${them} on the recording, and ${need} nothing from you, unless it's what you changed for a note: then say so in “accounts”, and it shows in your answer.`);
1488
+ }
1569
1489
  if (since && !since.changes.length && replies.some((reply) => reply.on && !reply.on.app && !reply.watch)) console.log(`Nothing at rest shows an answer that moves, like an animation: if yours does, add "watch": "<the title of the step it's seen in>" to its reply in ${file}, and the note plays that step.`);
1570
1490
  if (walked.length) console.log(`\n${walked.map(saidOf).join("\n")}`);
1571
1491
  if (left.length) console.log(`\nStill to do, in ${file}:\n${left.map((problem) => ` ${problem}`).join("\n")}`);
1572
- if (number === 1 && !saved && since?.changes.length) console.log(`\nSay why each change is there in “accounts”, by its id: { "line": "<why, in your words>" }, { "note": "<the id of the note it was made for>" }, or { "notReal": "<why nothing really changed>" }.`);
1492
+ if (number === 1 && !saved && named.length) console.log(`\nSay why each change is there in “accounts”, by its id: { "line": "<why, in your words>" }, { "note": "<the id of the note it was made for>" }, or { "notReal": "<why nothing really changed>" }.`);
1493
+ const wrote = number === 1 && !saved && recorded.find((kept) => kept.wrote);
1494
+ if (wrote && !(await setupIn(repo.setup)).after) console.log(`
1495
+ ${quote(wrote.flow.name)} sends the app ${wrote.wrote}, which changes what it holds, and ${existsSync(repo.setup) ? "playthrough/setup.ts has no after, so what each flow makes stays in the app" : "there's no playthrough/setup.ts, so each flow starts on what the ones before it left, and what they make stays in the app"}: one for each flow recorded, three for a new one. Tell the person, with the step that would fix it; run playthrough alone for how setup.ts makes what each flow starts from, and gives it back.`);
1573
1496
  if (unsaid.length) {
1574
1497
  const [are, each] = unsaid.length > 1 ? ["are", "each"] : ["is", "it"];
1575
1498
  console.log(`\n${list(unsaid.map((flow) => quote(flow.name)))} ${are} new to the person. In ${file}, ${each} can have a line at its start, as “line” in its flow under “flows”: what it starts from, or the choice it shows, in a sentence of up to 140 characters.`);
@@ -1621,42 +1544,30 @@ Write its flows before you change anything. They show what was asked, so write t
1621
1544
 
1622
1545
  ${howToWriteFlows(repo, id)}
1623
1546
 
1624
- Then run playthrough start ${id} again. It records Before: each flow on the app as it is, as far as the app can take it. If the app is new, there's no Before: go straight to the work.`);
1547
+ Then run playthrough start ${id} again. It tries the app's setup, and keeps the app as it is as Before, to record each flow on with round 1, so the person can compare. If the app is new, there's no Before: go straight to the work.`);
1625
1548
  }
1626
1549
  const review = await repo.reviews.get(id);
1627
- if (title && title !== review.title) throw new Refused([`There's already a review called “${id}”, of “${review.title}”. Give this one another name, or record that one's Before: playthrough start ${id}`]);
1550
+ if (title && title !== review.title) throw new Refused([`There's already a review called “${id}”, of “${review.title}”. Give this one another name, or carry that one on: playthrough start ${id}`]);
1628
1551
  const last = review.rounds.at(-1);
1629
- review.rounds.find((round) => round.number === 0);
1630
- if (last?.number === 0 && await recordBeforeAgain(repo, id, `playthrough start ${id}`, "Run this again.")) return;
1631
- const changed = await changedOnBefore(repo, review);
1632
- if (changed.length) return tryBeforeAgain(repo, id, changed, "Run this again.");
1633
- if (last?.number === 0) throw new Refused([`Before is recorded already. Change the app, then record round 1: playthrough record ${id}`]);
1552
+ if (last?.number === 0) throw new Refused([`Before is kept already. Change the app, then record round 1: playthrough record ${id}`]);
1634
1553
  if (last) throw new Refused([`Before is how the app was before the work, and ${id} has round ${last.number} already. Record the round being worked on: playthrough record ${id}`]);
1554
+ await flowsOf(repo, id);
1555
+ await trySetup(repo, `playthrough start ${id}`);
1635
1556
  const commit = keepBefore(repo, id);
1636
- const { recorded, failed, written } = await runFlows(repo, id, 0, { first: () => true });
1637
- refuseIfBroken(failed, "Before wasn't recorded", "Run this again.");
1638
- if (!recorded.length) return console.log(`\nNone of the flows can start on the app as it is, so there's no Before. If one should, fix it and run this again. Otherwise change the app, then record round 1: playthrough record ${id}`);
1639
- for (const { files } of recorded) for (const { name, bytes } of files) await repo.reviews.saveFile(id, 0, name, bytes);
1640
- const flows = recorded.map(({ flow }) => flow);
1641
- await repo.reviews.saveRound(id, {
1642
- number: 0,
1643
- by: agent(),
1644
- flows,
1645
- replies: [],
1646
- accounts: {},
1647
- cantStart: cantStart(recorded, failed),
1648
- ...commit && { commit }
1649
- });
1650
- await repo.reviews.finish(id, 0);
1651
- await noteRanOnBefore(repo, id, written);
1652
- console.log(`\nRecorded Before: ${called(flows.map((flow) => flow.name))}.`);
1653
- for (const line of howFar(repo, id, recorded, failed, "the app as it is")) console.log(line);
1654
- if (!commit) console.log(`This folder isn't in git, so Before isn't kept as a commit, and a flow added later can't be recorded on it.`);
1655
- const wrote = recorded.find((kept) => kept.wrote);
1656
- const { after } = await setupIn(repo.setup);
1657
- if (wrote && !after) console.log(`
1658
- ${quote(wrote.flow.name)} sends the app ${wrote.wrote}, which changes what it holds, and ${existsSync(repo.setup) ? "playthrough/setup.ts has no after, so what each flow makes stays in the app" : "there's no playthrough/setup.ts, so each flow starts on what the ones before it left, and what they make stays in the app"}: one for each flow recorded, two for a new one. Tell the person, with the step that would fix it; run playthrough alone for how setup.ts makes what each flow starts from, and gives it back.`);
1557
+ if (commit) {
1558
+ await repo.reviews.saveRound(id, {
1559
+ number: 0,
1560
+ by: agent(),
1561
+ flows: [],
1562
+ replies: [],
1563
+ accounts: {},
1564
+ commit
1565
+ });
1566
+ await repo.reviews.finish(id, 0);
1567
+ }
1659
1568
  console.log(`
1569
+ ${commit ? "Kept Before: the app as it is now. Each flow is recorded on it with round 1, as far as the app as it was takes it, for the person to compare with." : "This folder isn't in git, or has no commits yet, so there's no Before: the app as it is can't be put back once you've changed it."}
1570
+
1660
1571
  Now change the app. Keep each flow's name, and each step's while it does the same job: they're matched across rounds by name. Once the work is done, add a flow for each choice you made that the person can see and didn't ask for. Then record round 1: playthrough record ${id}`);
1661
1572
  }
1662
1573
  // src/noted.ts
@@ -1,4 +1,4 @@
1
- import { u as waitsFor } from "./flows-BcISV1M-.js";
1
+ import { d as waitsFor } from "./flows-BkG0TgHA.js";
2
2
  import { availableParallelism } from "node:os";
3
3
  import { defineConfig } from "@playwright/test";
4
4
  import { fileURLToPath } from "node:url";
@@ -1,7 +1,7 @@
1
1
  import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { z } from "zod";
3
- import { join, relative } from "node:path";
4
3
  import { createHash } from "node:crypto";
4
+ import { join, relative } from "node:path";
5
5
  // ../review/src/model.ts
6
6
  const text = (error) => z.string({ error }).refine((value) => value.trim() !== "", { error });
7
7
  const seconds$1 = (error) => z.number({ error }).min(0, { error });
@@ -68,14 +68,15 @@ const Flow = z.strictObject({
68
68
  steps: z.array(Step),
69
69
  recording: fileName("needs its recording: the name of a file in the round"),
70
70
  line: text("needs words in its line, or no line").optional().describe("The agent's, at the flow's start: what it starts from, or the choice it shows"),
71
- stopped: text("needs the step it stopped at").optional().describe("On Before, the step the flow couldn't take there: it's kept as far as that, and tried again by playthrough start"),
71
+ stopped: text("needs the step it stopped at").optional().describe("On Before, the step the flow couldn't take there, or that the app as it was had something else for: it's kept as far as that"),
72
72
  person: text("needs the name of who it acts as, or no person").optional().describe("Who the flow is done as, as its file or the app's setup names them: the name on the recorded pointer, handed to you when you take over"),
73
73
  unsteady: z.array(text("needs the ids of the changes it makes by itself")).optional().describe("Changes found between two recordings of the same app, like a time the server stamps: left out of every comparison, in every flow")
74
74
  });
75
75
  const Change = z.strictObject({
76
76
  id: text("needs an id").describe("Its thing and its kind of change, so it keeps its id in every round while it's the same change"),
77
77
  words: text("needs its words").describe("What changed, for the agent: “Download CSV and Cancel have swapped places”"),
78
- seen: z.array(Place, { error: "needs where it's seen, a list" }).min(1, { error: "needs where it's seen: each flow, at its moment there" }).describe("Each flow it's seen in, at its moment there; a flow that's gone, with no moment")
78
+ seen: z.array(Place, { error: "needs where it's seen, a list" }).min(1, { error: "needs where it's seen: each flow, at its moment there" }).describe("Each flow it's seen in, at its moment there; a flow that's gone, with no moment"),
79
+ pictureOnly: z.literal(true).optional().describe("Seen only in the pictures, with nothing read there changed: shown to the person, who can see what it is, and not put to the agent, who can't see more of it than Playthrough did")
79
80
  });
80
81
  const Comparison = z.strictObject({
81
82
  round: z.number({ error: "needs its round" }).int().min(1, { error: "needs its round, after Before" }),
@@ -318,6 +319,7 @@ function problemsWithComparison(review, comparison) {
318
319
  }
319
320
  return problems;
320
321
  }
322
+ const toAccountFor = (comparison) => (comparison?.changes ?? []).filter((change) => !change.pictureOnly);
321
323
  function problemsFinishing(review, number) {
322
324
  if (review.accepted) return ["This review is accepted, so nothing more can be recorded."];
323
325
  const round = review.rounds.find((round) => round.number === number);
@@ -325,7 +327,7 @@ function problemsFinishing(review, number) {
325
327
  const at = `Round ${number}`;
326
328
  const shape = problemsWithShape(Round, round, at);
327
329
  if (shape.length) return shape;
328
- if (!round.flows.length) return [`${at} has no flows yet. Record them first.`];
330
+ if (number > 0 && !round.flows.length) return [`${at} has no flows yet. Record them first.`];
329
331
  const problems = [];
330
332
  const notes = allNotes(review);
331
333
  for (const id of notesToAnswer(review, number)) {
@@ -338,7 +340,7 @@ function problemsFinishing(review, number) {
338
340
  const comparisons = review.comparisons.filter((comparison) => comparison.round === number);
339
341
  if (comparisons.length) for (const earlier of review.rounds.filter((earlier) => earlier.number < number && !comparisons.some((comparison) => comparison.since === earlier.number))) problems.push(`${at} isn't compared with ${roundName(earlier.number)} yet, so it can't be handed over. Record it again.`);
340
342
  const changes = new Map(comparisons.flatMap((comparison) => comparison.changes.map((change) => [change.id, change])));
341
- for (const change of comparisons.find((comparison) => comparison.since === number - 1)?.changes ?? []) if (!round.accounts[change.id]) problems.push(`${at}: say why ${change.id} is there (${change.words}${where(change)}): its line, the id of the note it was made for, or why nothing really changed.`);
343
+ for (const change of toAccountFor(comparisons.find((comparison) => comparison.since === number - 1))) if (!round.accounts[change.id]) problems.push(`${at}: say why ${change.id} is there (${change.words}${where(change)}): its line, the id of the note it was made for, or why nothing really changed.`);
342
344
  for (const [id, account] of Object.entries(round.accounts)) {
343
345
  const change = changes.get(id);
344
346
  if (!change) problems.push(`${at} says something about the change ${id}, which isn't one of this round's changes.`);
@@ -591,10 +593,7 @@ var Reviews = class {
591
593
  if (problems.length) throw new Refused(problems);
592
594
  const before = review.rounds.find((round) => round.number === 0);
593
595
  for (const { name, bytes } of added.files) await this.storage.write(`${id}/rounds/0/files/${name}`, bytes);
594
- await this.#writeJson(`${id}/rounds/0/round.json`, {
595
- ...withInBefore(before, added.flows, added.cantStart, added.gone),
596
- ...added.commit && { commit: added.commit }
597
- });
596
+ await this.#writeJson(`${id}/rounds/0/round.json`, withInBefore(before, added.flows, added.cantStart, added.gone));
598
597
  });
599
598
  }
600
599
  async saveComparison(id, comparison) {
@@ -733,8 +732,7 @@ const PERSON = "playthrough person";
733
732
  const HANDS = "playthrough hands";
734
733
  const ACTIONS = "playthrough actions";
735
734
  const WROTE = "playthrough wrote";
736
- const UNMET = "playthrough unmet";
737
- const STOPPED_ON = "playthrough stopped on";
735
+ const DONE_ON = "playthrough done on";
738
736
  const HOLD = 1500;
739
737
  const readingsOf = (flow) => `readings/${flow.recording}`;
740
738
  async function readingsOn(reviews, review, round, flow) {
@@ -752,11 +750,6 @@ function firstLine(said) {
752
750
  if (found.length) line = `${line} ${found.slice(0, 2).join(", ")}${found.length > 2 ? ", …" : ""}`;
753
751
  return line.length > 300 ? `${line.slice(0, 300)}…` : line;
754
752
  }
755
- const pathOf = (address) => {
756
- const { pathname, search } = new URL(address);
757
- return pathname + search;
758
- };
759
- const onPage = (on) => on ? `, on ${on}` : "";
760
753
  const BROKEN = /Page crashed|Target crashed|Target page, context or browser has been closed|net::ERR_CONNECTION_REFUSED/;
761
754
  const hashOf = (file) => {
762
755
  try {
@@ -795,14 +788,14 @@ var Flows = class {
795
788
  const steps = result.steps.filter((step) => step.category === "test.step");
796
789
  const person = result.attachments.find((attachment) => attachment.name === PERSON)?.body?.toString();
797
790
  const wrote = result.attachments.find((attachment) => attachment.name === WROTE)?.body?.toString();
791
+ const doneOn = result.attachments.find((attachment) => attachment.name === DONE_ON)?.body?.toString();
798
792
  const as = (made) => {
799
793
  if (person) made.flow.person = person;
800
794
  for (const step of made.flow.steps) if (step.person === made.flow.person) delete step.person;
801
795
  if (wrote) made.wrote = wrote;
796
+ if (doneOn) made.doneOn = JSON.parse(doneOn);
802
797
  return made;
803
798
  };
804
- const unmet = Number(result.attachments.find((attachment) => attachment.name === "playthrough unmet")?.body?.toString() ?? 0);
805
- const yet = unmet ? `, though ${unmet > 1 ? `${unmet} things it expects aren't` : "1 thing it expects isn't"} so yet` : "";
806
799
  const went = (how) => console.log(` ${quote(test.title)}: ${how}`);
807
800
  if (result.status === "passed") {
808
801
  if (!recording) return;
@@ -811,20 +804,18 @@ var Flows = class {
811
804
  ...where,
812
805
  recorded
813
806
  });
814
- return went(`to its end, ${Math.round(recorded.flow.length)}s${yet}`);
807
+ return went(`to its end, ${Math.round(recorded.flow.length)}s`);
815
808
  }
816
809
  const stop = steps.at(-1);
817
810
  const kept = stop?.error ? steps.slice(0, -1) : steps;
818
811
  const said = (stop?.error ?? result.error)?.message ?? "";
819
812
  const broken = BROKEN.test(said);
820
813
  const why = firstLine(said);
821
- const on = result.attachments.find((attachment) => attachment.name === STOPPED_ON)?.body?.toString();
822
814
  const failed = {
823
815
  flow: test.title,
824
816
  ...stop?.error && { at: stop.title },
825
817
  ...why && { why },
826
- ...broken && { broken },
827
- ...stop?.error && on && { on }
818
+ ...broken && { broken }
828
819
  };
829
820
  if (!this.#before || broken || !recording || !stop || !kept.length) {
830
821
  this.#ran.push({
@@ -841,7 +832,7 @@ var Flows = class {
841
832
  failed,
842
833
  recorded
843
834
  });
844
- went(`${stop.error ? `stops at ${quote(stop.title)}${onPage(failed.on)}` : "stops after its last step"}${yet}`);
835
+ went(stop.error ? `stops at ${quote(stop.title)}` : "stops after its last step");
845
836
  }
846
837
  onError(error) {
847
838
  console.log(`\n${error.message ?? error.value}`);
@@ -927,4 +918,4 @@ var Flows = class {
927
918
  }
928
919
  };
929
920
  const seconds = (ms) => Math.round(Math.max(0, ms)) / 1e3;
930
- export { problemsFinishing as A, Refused as C, inOrderJoined as D, conversation as E, list as M, quote as N, lastSeen as O, time as P, keepAssets as S, allNotes as T, onPage as _, HANDS as a, readingsOn as b, PICTURE as c, STOPPED_ON as d, UNMET as f, hashOf as g, glimpsesOf as h, GLIMPSES as i, roundInProgress as j, notesToAnswer as k, READINGS as l, firstLine as m, ASSETS as n, HOLD as o, WROTE as p, Flows as r, PERSON as s, ACTIONS as t, RECORDING as u, pathOf as v, Reviews as w, seconds as x, readingsOf as y };
921
+ export { list as A, conversation as C, problemsFinishing as D, notesToAnswer as E, time as M, roundInProgress as O, allNotes as S, lastSeen as T, readingsOn as _, GLIMPSES as a, Refused as b, PERSON as c, RECORDING as d, WROTE as f, readingsOf as g, hashOf as h, Flows as i, quote as j, toAccountFor as k, PICTURE as l, glimpsesOf as m, ASSETS as n, HANDS as o, firstLine as p, DONE_ON as r, HOLD as s, ACTIONS as t, READINGS as u, seconds as v, inOrderJoined as w, Reviews as x, keepAssets as y };
package/src/reporter.js CHANGED
@@ -1 +1 @@
1
- export { t as ACTIONS, n as ASSETS, i as GLIMPSES, a as HANDS, o as HOLD, s as PERSON, c as PICTURE, l as READINGS, u as RECORDING, d as STOPPED_ON, f as UNMET, p as WROTE, r as default, m as firstLine, h as glimpsesOf, g as hashOf, _ as onPage, v as pathOf, y as readingsOf, b as readingsOn, x as seconds } from "./reporter-BEodHYRI.js";
1
+ export { t as ACTIONS, n as ASSETS, r as DONE_ON, a as GLIMPSES, o as HANDS, s as HOLD, c as PERSON, l as PICTURE, u as READINGS, d as RECORDING, f as WROTE, i as default, p as firstLine, m as glimpsesOf, h as hashOf, g as readingsOf, _ as readingsOn, v as seconds } from "./reporter-DE8aueAO.js";