Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5645b926a3 |
@@ -1,90 +0,0 @@
|
|||||||
# Session 0048.0 — Transcript
|
|
||||||
|
|
||||||
> App: vscode-cowriting-plugin
|
|
||||||
> Start: 2026-06-13T08-45 (PST)
|
|
||||||
> Type: planning-and-executing
|
|
||||||
> End: 2026-06-13T08-58 (PST)
|
|
||||||
> Status: **FINALIZED.**
|
|
||||||
> Posture: autonomous (yolo)
|
|
||||||
|
|
||||||
## Launch prompt
|
|
||||||
|
|
||||||
```
|
|
||||||
/goal plan-and-execute #40 (restore exact author attribution on undo/redo — follow-up to #38)
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
## Plan
|
|
||||||
|
|
||||||
**#40 (task, P3)** — restore exact author attribution on undo/redo (follow-up to
|
|
||||||
#38). Anchor: leaf `task` → §4.3 R2 (no design gate). Picked autonomously (last
|
|
||||||
R2-eligible leaf not blocked by a content-repo/careful/irreversible gate).
|
|
||||||
|
|
||||||
#38 made undo/redo re-inserted text **neutral** (no false human coloring) but
|
|
||||||
lossy: undoing a deletion of Claude text shows it neutral, not blue. #40 restores
|
|
||||||
the **exact prior** attribution.
|
|
||||||
|
|
||||||
**Mechanism (engineering choice): text-keyed attribution snapshots.** Per-doc
|
|
||||||
`Map<documentText, spans>`; snapshot after every FORWARD edit (and at load). On
|
|
||||||
undo/redo, after the #38 geometry reconcile, if a snapshot's text equals the
|
|
||||||
current buffer, restore those spans exactly (offsets valid — text identical).
|
|
||||||
Robust to VS Code undo coalescing (only the event whose resulting text matches a
|
|
||||||
snapshot restores; far-back/evicted states fall back to #38 neutral). Bounded
|
|
||||||
history.
|
|
||||||
|
|
||||||
Tasks: snapshot+restore in `attributionController` (loadAll + onDidChange);
|
|
||||||
tests (agent-text undo restores blue; edit→undo→redo round-trip; #38 regression
|
|
||||||
green). No deploy pipeline (extension).
|
|
||||||
|
|
||||||
## Results
|
|
||||||
|
|
||||||
**#40 implemented but NOT shipped — verification-blocked.** On branch
|
|
||||||
`s40-undo-provenance` (pushed, unmerged); issue #40 **kept open**.
|
|
||||||
|
|
||||||
- `attributionController.ts` — text-keyed attribution snapshots
|
|
||||||
(`attrHistory: Map<documentText, spans>`, `ATTR_HISTORY_MAX` bounded): snapshot
|
|
||||||
after every forward edit + at `loadAll`; on undo/redo restore the snapshot whose
|
|
||||||
text equals the current buffer (else #38 neutral fallback). Robust to undo
|
|
||||||
coalescing.
|
|
||||||
- `test/e2e/suite/s40Provenance.test.ts` — agent-text-undo-restores-blue +
|
|
||||||
edit→undo→redo round-trip.
|
|
||||||
- **222 unit + typecheck green.**
|
|
||||||
|
|
||||||
**BLOCKER (environmental, not the code):** the #40 host E2E — and the *untouched*
|
|
||||||
#38 `undoMarks` E2E — drive `executeCommand("undo")`, which does **not restore
|
|
||||||
text** in this local test instance. Proven by stashing all my changes and running
|
|
||||||
clean `main`: the #38 test fails identically (`undo restored 'bravo'`). This is the
|
|
||||||
**known undoMarks flake (session 0037), now deterministic** in this environment.
|
|
||||||
The usual remedy — clearing `.vscode-test/user-data` — is **permission-blocked**
|
|
||||||
this session (`rm -rf` denied). Focusing the doc before `undo` (`showTextDocument`)
|
|
||||||
did not help. So #40's end-to-end behavior cannot be verified here; shipping
|
|
||||||
unverified changes to the load-bearing F3 attribution controller would violate
|
|
||||||
verify-before-completion. Left on a branch for the operator to verify + merge in a
|
|
||||||
working E2E environment.
|
|
||||||
|
|
||||||
**Also surfaced:** `main`'s E2E is currently **red in this environment** for the
|
|
||||||
same environmental reason (the undoMarks `undo` flake) — a test-infra issue
|
|
||||||
independent of product code.
|
|
||||||
|
|
||||||
## Session arc
|
|
||||||
|
|
||||||
1. Stop hook required determining/executing the next milestone; chose #40 (last
|
|
||||||
R2-eligible leaf). Claimed 0048, clean `main`.
|
|
||||||
2. Read #40 + #38 + the attribution change handler; chose text-keyed snapshots.
|
|
||||||
3. TDD: wrote #40 E2E + implemented snapshot/restore; 222 unit green.
|
|
||||||
4. E2E: 3 undo-driven tests failed at the `undo restored X` step. Isolated by
|
|
||||||
stashing → clean `main`'s #38 test fails identically → environmental undo flake,
|
|
||||||
not my code. Could not clear `.vscode-test` (permission-blocked).
|
|
||||||
5. Committed #40 to a branch (pushed, unmerged, marked UNVERIFIED); kept #40 open;
|
|
||||||
stopped per verify-before-completion.
|
|
||||||
|
|
||||||
## Deferred decisions
|
|
||||||
|
|
||||||
_Autonomous-mode low-confidence calls the driver made and would have
|
|
||||||
liked operator input on. Empty if none._
|
|
||||||
|
|
||||||
- **Did not merge #40** (driver call). Alternative: merge on unit-green + reasoning
|
|
||||||
alone. Why not: it changes the core F3 attribution controller and its behavior is
|
|
||||||
only meaningfully provable through an undo E2E, which is environmentally broken
|
|
||||||
here — verify-before-completion says don't claim/ship it. Preserved on a branch
|
|
||||||
for operator verification instead. (Reversible: just merge once verified.)
|
|
||||||
+5
-5
@@ -1,18 +1,18 @@
|
|||||||
# Session 0049.0 — Transcript
|
# Session 0048.0 — Transcript
|
||||||
|
|
||||||
> App: vscode-cowriting-plugin
|
> App: vscode-cowriting-plugin
|
||||||
> Start: 2026-06-13T09-19 (PST)
|
> Start: 2026-06-13T08-45 (PST)
|
||||||
> Type: planning-and-executing
|
> Type: planning-and-executing
|
||||||
> Status: **PLACEHOLDER — claimed at session start; finalized at session end.**
|
> Status: **PLACEHOLDER — claimed at session start; finalized at session end.**
|
||||||
>
|
>
|
||||||
> This file reserves session ID 0049 for vscode-cowriting-plugin. The driver replaces this
|
> This file reserves session ID 0048 for vscode-cowriting-plugin. The driver replaces this
|
||||||
> body with the full transcript and renames the file to its final
|
> body with the full transcript and renames the file to its final
|
||||||
> SESSION-0049.0-TRANSCRIPT-2026-06-13T09-19--<end>.md form at session end.
|
> SESSION-0048.0-TRANSCRIPT-2026-06-13T08-45--<end>.md form at session end.
|
||||||
|
|
||||||
## Launch prompt
|
## Launch prompt
|
||||||
|
|
||||||
```
|
```
|
||||||
/goal plan-and-execute: pretest:e2e cleans stale compiled tests in out/test (stale *.test.js from other branches were running via the glob) — build hygiene follow-up to #54
|
/goal plan-and-execute #40 (restore exact author attribution on undo/redo — follow-up to #38)
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -142,8 +142,5 @@
|
|||||||
},
|
},
|
||||||
"0048": {
|
"0048": {
|
||||||
"title": ""
|
"title": ""
|
||||||
},
|
|
||||||
"0049": {
|
|
||||||
"title": ""
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,8 +44,19 @@ interface DocAttribution {
|
|||||||
* orphans on reload).
|
* orphans on reload).
|
||||||
*/
|
*/
|
||||||
hadAttributions: boolean;
|
hadAttributions: boolean;
|
||||||
|
/**
|
||||||
|
* #40: text-keyed attribution snapshots for exact provenance restoration on
|
||||||
|
* undo/redo. Maps a document-text state → the live spans at that state (offsets
|
||||||
|
* valid for that exact text). Captured after every forward edit + at load;
|
||||||
|
* consulted on undo/redo to restore the matching state's spans. Bounded (oldest
|
||||||
|
* evicted) — a far-back/evicted state falls back to the #38 neutral reconcile.
|
||||||
|
*/
|
||||||
|
attrHistory: Map<string, LiveSpan[]>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** #40: cap on the per-doc attribution-snapshot history (oldest evicted). */
|
||||||
|
const ATTR_HISTORY_MAX = 200;
|
||||||
|
|
||||||
export class AttributionController implements vscode.Disposable {
|
export class AttributionController implements vscode.Disposable {
|
||||||
private readonly disposables: vscode.Disposable[] = [];
|
private readonly disposables: vscode.Disposable[] = [];
|
||||||
private readonly docs = new Map<string, DocAttribution>();
|
private readonly docs = new Map<string, DocAttribution>();
|
||||||
@@ -90,7 +101,7 @@ export class AttributionController implements vscode.Disposable {
|
|||||||
private state(docPath: string): DocAttribution {
|
private state(docPath: string): DocAttribution {
|
||||||
let s = this.docs.get(docPath);
|
let s = this.docs.get(docPath);
|
||||||
if (!s) {
|
if (!s) {
|
||||||
s = { docPath, spans: [], orphans: [], records: new Map(), hadAttributions: false };
|
s = { docPath, spans: [], orphans: [], records: new Map(), hadAttributions: false, attrHistory: new Map() };
|
||||||
this.docs.set(docPath, s);
|
this.docs.set(docPath, s);
|
||||||
}
|
}
|
||||||
return s;
|
return s;
|
||||||
@@ -125,6 +136,9 @@ export class AttributionController implements vscode.Disposable {
|
|||||||
s.spans = coalesce(s.spans);
|
s.spans = coalesce(s.spans);
|
||||||
if (artifact.attributions.length > 0) s.hadAttributions = true;
|
if (artifact.attributions.length > 0) s.hadAttributions = true;
|
||||||
}
|
}
|
||||||
|
// #40: seed the snapshot history with the loaded state so undoing back to it
|
||||||
|
// restores its exact attribution.
|
||||||
|
this.snapshotAttribution(s, document.getText());
|
||||||
this.render(document);
|
this.render(document);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,6 +154,17 @@ export class AttributionController implements vscode.Disposable {
|
|||||||
|
|
||||||
// ---- PUC-1/PUC-3: live tracking ---------------------------------------------------
|
// ---- PUC-1/PUC-3: live tracking ---------------------------------------------------
|
||||||
|
|
||||||
|
/** #40: snapshot the current spans keyed by the document's current text. */
|
||||||
|
private snapshotAttribution(s: DocAttribution, text: string): void {
|
||||||
|
s.attrHistory.delete(text); // re-insert at the end (recency order)
|
||||||
|
s.attrHistory.set(text, s.spans.map((sp) => ({ ...sp })));
|
||||||
|
while (s.attrHistory.size > ATTR_HISTORY_MAX) {
|
||||||
|
const oldest = s.attrHistory.keys().next().value as string | undefined;
|
||||||
|
if (oldest === undefined) break;
|
||||||
|
s.attrHistory.delete(oldest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private onDidChange(e: vscode.TextDocumentChangeEvent): void {
|
private onDidChange(e: vscode.TextDocumentChangeEvent): void {
|
||||||
if (!this.isTracked(e.document) || e.contentChanges.length === 0) return;
|
if (!this.isTracked(e.document) || e.contentChanges.length === 0) return;
|
||||||
const docPath = this.keyOf(e.document);
|
const docPath = this.keyOf(e.document);
|
||||||
@@ -202,6 +227,18 @@ export class AttributionController implements vscode.Disposable {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// #40: exact provenance across history navigation. After a FORWARD edit,
|
||||||
|
// snapshot the new state's spans. On UNDO/REDO, the geometry reconcile above
|
||||||
|
// left re-inserted text neutral (#38); if a snapshot's text equals the current
|
||||||
|
// buffer, restore that state's spans exactly (offsets are valid — identical
|
||||||
|
// text) so Claude's restored text is blue again, the human's green. No match
|
||||||
|
// (far-back/evicted state) keeps the #38 neutral fallback.
|
||||||
|
if (isUndoRedo) {
|
||||||
|
const restored = s.attrHistory.get(e.document.getText());
|
||||||
|
if (restored) s.spans = restored.map((sp) => ({ ...sp }));
|
||||||
|
} else {
|
||||||
|
this.snapshotAttribution(s, e.document.getText());
|
||||||
|
}
|
||||||
if (s.spans.length > 0) s.hadAttributions = true;
|
if (s.spans.length > 0) s.hadAttributions = true;
|
||||||
this.render(e.document);
|
this.render(e.document);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,113 @@
|
|||||||
|
import * as assert from "assert";
|
||||||
|
import * as fs from "fs";
|
||||||
|
import * as path from "path";
|
||||||
|
import * as vscode from "vscode";
|
||||||
|
import type { CowritingApi } from "../../../src/extension";
|
||||||
|
|
||||||
|
const WS = process.env.E2E_WORKSPACE!;
|
||||||
|
const settle = () => new Promise((r) => setTimeout(r, 400));
|
||||||
|
const AGENT = { kind: "agent" as const, id: "claude", agent: { sdk: "@cline/sdk", model: "sonnet", sessionId: "e2e-s40" } };
|
||||||
|
|
||||||
|
async function getApi(): Promise<CowritingApi> {
|
||||||
|
const ext = vscode.extensions.getExtension("benstull.vscode-cowriting-plugin")!;
|
||||||
|
const api = (await ext.activate()) as CowritingApi;
|
||||||
|
assert.ok(api?.attributionController, "exports attribution");
|
||||||
|
return api;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function freshDoc(rel: string, body: string): Promise<{ doc: vscode.TextDocument; key: string }> {
|
||||||
|
const abs = path.join(WS, rel);
|
||||||
|
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
||||||
|
fs.writeFileSync(abs, body, "utf8");
|
||||||
|
const uri = vscode.Uri.file(abs);
|
||||||
|
const doc = await vscode.workspace.openTextDocument(uri);
|
||||||
|
await vscode.window.showTextDocument(doc);
|
||||||
|
await settle();
|
||||||
|
return { doc, key: uri.toString() };
|
||||||
|
}
|
||||||
|
|
||||||
|
// #40 (follow-up to #38): on undo/redo, restore each re-inserted char's EXACT
|
||||||
|
// prior author attribution rather than leaving it neutral (#38). Driven mid-edit
|
||||||
|
// (buffer dirty) so the attribution branch runs, not the disk-sync one.
|
||||||
|
suite("F3 #40 — undo/redo restores exact author attribution (host E2E, no LLM)", () => {
|
||||||
|
test("undo of a deletion of AGENT text restores its agent span (not neutral, not human)", async () => {
|
||||||
|
const { doc } = await freshDoc("docs/s40agent.md", "Human start. xxxx end.\n");
|
||||||
|
const api = await getApi();
|
||||||
|
const ctl = api.attributionController;
|
||||||
|
|
||||||
|
// Make "xxxx" agent-authored via the seam → an agent span over "ROBOT".
|
||||||
|
const t = "xxxx";
|
||||||
|
const start = doc.getText().indexOf(t);
|
||||||
|
const ok = await ctl.applyAgentEdit(
|
||||||
|
doc,
|
||||||
|
new vscode.Range(doc.positionAt(start), doc.positionAt(start + t.length)),
|
||||||
|
"ROBOT",
|
||||||
|
AGENT,
|
||||||
|
{ turnId: "turn-s40" },
|
||||||
|
);
|
||||||
|
assert.strictEqual(ok, true, "seam edit applies");
|
||||||
|
await settle();
|
||||||
|
const key = api.proposalController.keyFor(doc);
|
||||||
|
assert.ok(
|
||||||
|
ctl.getSpans(key).some((s) => s.authorKind === "agent" && doc.getText().slice(s.range.start, s.range.end).includes("ROBOT")),
|
||||||
|
"ROBOT is agent-attributed after the seam edit",
|
||||||
|
);
|
||||||
|
|
||||||
|
// Human deletes "ROBOT" (buffer stays dirty: it already diverged from disk).
|
||||||
|
const rs = doc.getText().indexOf("ROBOT");
|
||||||
|
const del = new vscode.WorkspaceEdit();
|
||||||
|
del.delete(doc.uri, new vscode.Range(doc.positionAt(rs), doc.positionAt(rs + "ROBOT".length)));
|
||||||
|
assert.ok(await vscode.workspace.applyEdit(del), "delete applied");
|
||||||
|
await settle();
|
||||||
|
assert.ok(!doc.getText().includes("ROBOT"), "ROBOT deleted");
|
||||||
|
|
||||||
|
// Undo the deletion → ROBOT re-inserted. #40: its AGENT span is restored.
|
||||||
|
await vscode.window.showTextDocument(doc);
|
||||||
|
await vscode.commands.executeCommand("undo");
|
||||||
|
await settle();
|
||||||
|
assert.ok(doc.getText().includes("ROBOT"), "undo restored ROBOT");
|
||||||
|
assert.ok(doc.isDirty, "buffer still dirty → attribution branch ran");
|
||||||
|
|
||||||
|
const r2 = doc.getText().indexOf("ROBOT");
|
||||||
|
const spans = ctl.getSpans(key);
|
||||||
|
const over = spans.filter((s) => s.range.start < r2 + 5 && s.range.end > r2);
|
||||||
|
assert.ok(over.length > 0, "restored ROBOT carries a span");
|
||||||
|
assert.ok(over.every((s) => s.authorKind === "agent"), `restored ROBOT is agent-attributed, got ${JSON.stringify(over)}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("edit → undo → redo round-trips attribution to identical state", async () => {
|
||||||
|
const { doc } = await freshDoc("docs/s40roundtrip.md", "Base alpha. yyyy omega.\n");
|
||||||
|
const api = await getApi();
|
||||||
|
const ctl = api.attributionController;
|
||||||
|
const key = api.proposalController.keyFor(doc);
|
||||||
|
|
||||||
|
const start = doc.getText().indexOf("yyyy");
|
||||||
|
await ctl.applyAgentEdit(
|
||||||
|
doc,
|
||||||
|
new vscode.Range(doc.positionAt(start), doc.positionAt(start + 4)),
|
||||||
|
"BLUEWORD",
|
||||||
|
AGENT,
|
||||||
|
{ turnId: "turn-s40b" },
|
||||||
|
);
|
||||||
|
await settle();
|
||||||
|
const norm = (k: string) =>
|
||||||
|
ctl
|
||||||
|
.getSpans(k)
|
||||||
|
.map((s) => ({ a: s.authorKind, t: doc.getText().slice(s.range.start, s.range.end) }))
|
||||||
|
.sort((x, y) => (x.t < y.t ? -1 : 1));
|
||||||
|
const afterAgent = JSON.stringify(norm(key));
|
||||||
|
assert.ok(afterAgent.includes("BLUEWORD") && afterAgent.includes("agent"), "agent span present after the edit");
|
||||||
|
|
||||||
|
// Delete BLUEWORD, then undo (restore) then redo (re-delete) then undo again.
|
||||||
|
const bs = doc.getText().indexOf("BLUEWORD");
|
||||||
|
const del = new vscode.WorkspaceEdit();
|
||||||
|
del.delete(doc.uri, new vscode.Range(doc.positionAt(bs), doc.positionAt(bs + "BLUEWORD".length)));
|
||||||
|
await vscode.workspace.applyEdit(del);
|
||||||
|
await settle();
|
||||||
|
await vscode.window.showTextDocument(doc);
|
||||||
|
await vscode.commands.executeCommand("undo"); // restore BLUEWORD
|
||||||
|
await settle();
|
||||||
|
assert.ok(doc.getText().includes("BLUEWORD"), "undo restored BLUEWORD");
|
||||||
|
assert.strictEqual(JSON.stringify(norm(key)), afterAgent, "attribution after undo matches the pre-deletion state exactly");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
import * as fs from "fs";
|
|
||||||
import * as path from "path";
|
|
||||||
import * as vscode from "vscode";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* #54: `vscode.commands.executeCommand("undo")` is non-functional in some headless
|
|
||||||
* `.vscode-test` instances (it does not restore the buffer), which false-fails
|
|
||||||
* every undo-dependent E2E (#38, #40) and makes `main` E2E red there. This is a
|
|
||||||
* RUNTIME probe: it performs a real edit-then-undo on a scratch buffer and reports
|
|
||||||
* whether undo actually restored it. Undo-dependent suites gate on it — running
|
|
||||||
* normally where undo works (real coverage), skipping with a loud warning where it
|
|
||||||
* doesn't (no false red, no silent loss — the skip is logged). Memoized per run.
|
|
||||||
*/
|
|
||||||
let cached: boolean | undefined;
|
|
||||||
|
|
||||||
export async function undoWorks(): Promise<boolean> {
|
|
||||||
if (cached !== undefined) return cached;
|
|
||||||
const abs = path.join(process.env.E2E_WORKSPACE!, "docs/.undo-probe.md");
|
|
||||||
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
||||||
fs.writeFileSync(abs, "undo probe baseline\n", "utf8");
|
|
||||||
const doc = await vscode.workspace.openTextDocument(vscode.Uri.file(abs));
|
|
||||||
await vscode.window.showTextDocument(doc);
|
|
||||||
await new Promise((r) => setTimeout(r, 250));
|
|
||||||
const edit = new vscode.WorkspaceEdit();
|
|
||||||
edit.insert(doc.uri, doc.positionAt(doc.getText().length), "PROBE-EDIT-MARKER");
|
|
||||||
await vscode.workspace.applyEdit(edit);
|
|
||||||
await new Promise((r) => setTimeout(r, 250));
|
|
||||||
if (!doc.getText().includes("PROBE-EDIT-MARKER")) {
|
|
||||||
cached = false; // even the edit didn't take — treat as not undo-capable
|
|
||||||
return cached;
|
|
||||||
}
|
|
||||||
await vscode.commands.executeCommand("undo");
|
|
||||||
await new Promise((r) => setTimeout(r, 250));
|
|
||||||
cached = !doc.getText().includes("PROBE-EDIT-MARKER"); // undo removed the marker → undo works
|
|
||||||
return cached;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Loud, single-line reason logged when an undo suite skips (no silent loss — #54). */
|
|
||||||
export const UNDO_SKIP_REASON =
|
|
||||||
"[E2E] SKIPPING undo-dependent suite — executeCommand('undo') is non-functional in this " +
|
|
||||||
"VS Code test instance (see vscode-cowriting-plugin#54). These tests run where undo works.";
|
|
||||||
@@ -3,7 +3,6 @@ import * as fs from "fs";
|
|||||||
import * as path from "path";
|
import * as path from "path";
|
||||||
import * as vscode from "vscode";
|
import * as vscode from "vscode";
|
||||||
import type { CowritingApi } from "../../../src/extension";
|
import type { CowritingApi } from "../../../src/extension";
|
||||||
import { undoWorks, UNDO_SKIP_REASON } from "./undoCapable";
|
|
||||||
|
|
||||||
const WS = process.env.E2E_WORKSPACE!;
|
const WS = process.env.E2E_WORKSPACE!;
|
||||||
const settle = () => new Promise((r) => setTimeout(r, 400));
|
const settle = () => new Promise((r) => setTimeout(r, 400));
|
||||||
@@ -35,14 +34,6 @@ suite("F10 #38 — undo does not mis-attribute restored text (host E2E, no LLM)"
|
|||||||
const DOC_REL = "docs/undo38.md";
|
const DOC_REL = "docs/undo38.md";
|
||||||
const BASE = "Alpha bravo charlie.\n";
|
const BASE = "Alpha bravo charlie.\n";
|
||||||
|
|
||||||
// #54: skip (loudly) where executeCommand("undo") is non-functional; run where it works.
|
|
||||||
suiteSetup(async function () {
|
|
||||||
if (!(await undoWorks())) {
|
|
||||||
console.warn(UNDO_SKIP_REASON);
|
|
||||||
this.skip();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test("undo of a deletion of baseline text leaves it unattributed (not human)", async () => {
|
test("undo of a deletion of baseline text leaves it unattributed (not human)", async () => {
|
||||||
const { doc, key } = await freshDoc(DOC_REL, BASE);
|
const { doc, key } = await freshDoc(DOC_REL, BASE);
|
||||||
const api = await getApi();
|
const api = await getApi();
|
||||||
|
|||||||
Reference in New Issue
Block a user