@openparachute/vault 0.4.7-rc.1 → 0.4.8-rc.4
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 +44 -10
- package/core/src/connection-pragmas.test.ts +232 -0
- package/core/src/core.test.ts +257 -0
- package/core/src/cursor.test.ts +160 -0
- package/core/src/cursor.ts +272 -0
- package/core/src/mcp.ts +51 -7
- package/core/src/notes.ts +164 -2
- package/core/src/portable-md.test.ts +247 -0
- package/core/src/portable-md.ts +118 -1
- package/core/src/schema.ts +98 -2
- package/core/src/store.ts +11 -1
- package/core/src/types.ts +32 -0
- package/package.json +1 -1
- package/src/auth-status.ts +4 -0
- package/src/auto-transcribe.test.ts +116 -0
- package/src/auto-transcribe.ts +48 -0
- package/src/cli.ts +151 -50
- package/src/config.test.ts +26 -0
- package/src/config.ts +53 -1
- package/src/db.ts +15 -2
- package/src/export-watch.test.ts +99 -0
- package/src/mcp-install-interactive.test.ts +23 -2
- package/src/mcp-install-interactive.ts +21 -2
- package/src/mcp-install.test.ts +40 -0
- package/src/mcp-tools.ts +17 -1
- package/src/module-config.ts +70 -14
- package/src/module-manifest.test.ts +93 -0
- package/src/module-manifest.ts +94 -0
- package/src/routes.ts +267 -50
- package/src/scribe-discovery.test.ts +77 -0
- package/src/scribe-discovery.ts +91 -0
- package/src/scribe-env.test.ts +66 -1
- package/src/scribe-env.ts +42 -1
- package/src/self-register.test.ts +380 -0
- package/src/self-register.ts +234 -0
- package/src/server.ts +46 -11
- package/src/transcript-note.test.ts +171 -0
- package/src/transcript-note.ts +189 -0
- package/src/transcription-registry.ts +22 -0
- package/src/transcription-worker.test.ts +250 -0
- package/src/transcription-worker.ts +186 -27
- package/src/vault.test.ts +347 -0
package/src/vault.test.ts
CHANGED
|
@@ -1575,6 +1575,177 @@ describe("HTTP /notes", async () => {
|
|
|
1575
1575
|
expect(body.map((n) => n.content)).toEqual(["modified"]);
|
|
1576
1576
|
});
|
|
1577
1577
|
|
|
1578
|
+
// ---- Cursor pagination (vault#313) ----
|
|
1579
|
+
//
|
|
1580
|
+
// Opaque cursors for since-last-checked agent loops. The full engine
|
|
1581
|
+
// semantics live in core.test.ts; these tests pin the HTTP plumbing:
|
|
1582
|
+
// wrapped {notes, next_cursor} envelope when ?cursor= is set, structured
|
|
1583
|
+
// 400s on bad cursor, and end-to-end resume across calls.
|
|
1584
|
+
test("GET /notes?cursor=... returns {notes, next_cursor} envelope", async () => {
|
|
1585
|
+
await store.createNote("a", { id: "cur-rest-a" });
|
|
1586
|
+
await store.createNote("b", { id: "cur-rest-b" });
|
|
1587
|
+
db.prepare("UPDATE notes SET updated_at = ? WHERE id = ?")
|
|
1588
|
+
.run("2026-04-01T00:00:00.000Z", "cur-rest-a");
|
|
1589
|
+
db.prepare("UPDATE notes SET updated_at = ? WHERE id = ?")
|
|
1590
|
+
.run("2026-04-02T00:00:00.000Z", "cur-rest-b");
|
|
1591
|
+
|
|
1592
|
+
// Mint a starting cursor via the store (we don't expose a "first cursor"
|
|
1593
|
+
// endpoint — the first call's response carries the cursor). Simulate
|
|
1594
|
+
// the first call by querying without a cursor and reading
|
|
1595
|
+
// next_cursor from a follow-up call shape.
|
|
1596
|
+
const seed = await store.queryNotesPaged({});
|
|
1597
|
+
const cursor = seed.next_cursor;
|
|
1598
|
+
|
|
1599
|
+
// No new writes after seed → second call is empty but still returns
|
|
1600
|
+
// an envelope.
|
|
1601
|
+
const res = await handleNotes(
|
|
1602
|
+
mkReq("GET", `/notes?cursor=${encodeURIComponent(cursor)}`),
|
|
1603
|
+
store,
|
|
1604
|
+
"",
|
|
1605
|
+
);
|
|
1606
|
+
expect(res.status).toBe(200);
|
|
1607
|
+
const body = await res.json() as any;
|
|
1608
|
+
expect(body).toHaveProperty("notes");
|
|
1609
|
+
expect(body).toHaveProperty("next_cursor");
|
|
1610
|
+
expect(Array.isArray(body.notes)).toBe(true);
|
|
1611
|
+
expect(body.notes).toHaveLength(0);
|
|
1612
|
+
expect(typeof body.next_cursor).toBe("string");
|
|
1613
|
+
});
|
|
1614
|
+
|
|
1615
|
+
test("GET /notes (no cursor) returns legacy flat-array shape", async () => {
|
|
1616
|
+
await store.createNote("a", { id: "noCur-a" });
|
|
1617
|
+
const res = await handleNotes(mkReq("GET", "/notes"), store, "");
|
|
1618
|
+
const body = await res.json();
|
|
1619
|
+
expect(Array.isArray(body)).toBe(true);
|
|
1620
|
+
});
|
|
1621
|
+
|
|
1622
|
+
test("GET /notes?cursor=<stale> returns 400 cursor_query_mismatch", async () => {
|
|
1623
|
+
await store.createNote("a", { tags: ["x"], id: "cm-a" });
|
|
1624
|
+
await store.createNote("b", { tags: ["y"], id: "cm-b" });
|
|
1625
|
+
const seed = await store.queryNotesPaged({ tags: ["x"] });
|
|
1626
|
+
|
|
1627
|
+
// Reuse on a different tag — engine raises cursor_query_mismatch.
|
|
1628
|
+
const res = await handleNotes(
|
|
1629
|
+
mkReq("GET", `/notes?tag=y&cursor=${encodeURIComponent(seed.next_cursor)}`),
|
|
1630
|
+
store,
|
|
1631
|
+
"",
|
|
1632
|
+
);
|
|
1633
|
+
expect(res.status).toBe(400);
|
|
1634
|
+
const body = await res.json() as any;
|
|
1635
|
+
expect(body.code).toBe("cursor_query_mismatch");
|
|
1636
|
+
});
|
|
1637
|
+
|
|
1638
|
+
test("GET /notes?cursor=<garbage> returns 400 cursor_invalid", async () => {
|
|
1639
|
+
const res = await handleNotes(
|
|
1640
|
+
mkReq("GET", `/notes?cursor=not-a-real-cursor`),
|
|
1641
|
+
store,
|
|
1642
|
+
"",
|
|
1643
|
+
);
|
|
1644
|
+
expect(res.status).toBe(400);
|
|
1645
|
+
const body = await res.json() as any;
|
|
1646
|
+
expect(body.code).toBe("cursor_invalid");
|
|
1647
|
+
});
|
|
1648
|
+
|
|
1649
|
+
test("GET /notes?cursor=...&near[note_id]=x rejects with INVALID_QUERY", async () => {
|
|
1650
|
+
const a = await store.createNote("anchor", { id: "n1" });
|
|
1651
|
+
const seed = await store.queryNotesPaged({});
|
|
1652
|
+
const res = await handleNotes(
|
|
1653
|
+
mkReq("GET", `/notes?cursor=${encodeURIComponent(seed.next_cursor)}&near[note_id]=${a.id}`),
|
|
1654
|
+
store,
|
|
1655
|
+
"",
|
|
1656
|
+
);
|
|
1657
|
+
expect(res.status).toBe(400);
|
|
1658
|
+
const body = await res.json() as any;
|
|
1659
|
+
expect(body.code).toBe("INVALID_QUERY");
|
|
1660
|
+
});
|
|
1661
|
+
|
|
1662
|
+
test("GET /notes?cursor=...&search=... rejects with INVALID_QUERY (vault#355 reviewer)", async () => {
|
|
1663
|
+
// REST used to silently drop the cursor and route into the FTS branch.
|
|
1664
|
+
// MCP rejects this combo explicitly at core/src/mcp.ts — REST now does
|
|
1665
|
+
// the same. Surface parity, no silent corruption.
|
|
1666
|
+
await store.createNote("the quick brown fox", { id: "s1" });
|
|
1667
|
+
const seed = await store.queryNotesPaged({});
|
|
1668
|
+
const res = await handleNotes(
|
|
1669
|
+
mkReq("GET", `/notes?cursor=${encodeURIComponent(seed.next_cursor)}&search=fox`),
|
|
1670
|
+
store,
|
|
1671
|
+
"",
|
|
1672
|
+
);
|
|
1673
|
+
expect(res.status).toBe(400);
|
|
1674
|
+
const body = await res.json() as any;
|
|
1675
|
+
expect(body.code).toBe("INVALID_QUERY");
|
|
1676
|
+
});
|
|
1677
|
+
|
|
1678
|
+
test("GET /notes?cursor=...&sort=desc rejects with INVALID_QUERY", async () => {
|
|
1679
|
+
// Descending iteration with a watermark cursor would skip newly-written
|
|
1680
|
+
// rows. queryNotesPaged surfaces this as a QueryError; the REST handler
|
|
1681
|
+
// catches and translates to 400. Asserts the surface parity with MCP
|
|
1682
|
+
// even though the guard sits at the core layer.
|
|
1683
|
+
await store.createNote("a", { id: "sd-a" });
|
|
1684
|
+
const seed = await store.queryNotesPaged({});
|
|
1685
|
+
const res = await handleNotes(
|
|
1686
|
+
mkReq("GET", `/notes?cursor=${encodeURIComponent(seed.next_cursor)}&sort=desc`),
|
|
1687
|
+
store,
|
|
1688
|
+
"",
|
|
1689
|
+
);
|
|
1690
|
+
expect(res.status).toBe(400);
|
|
1691
|
+
const body = await res.json() as any;
|
|
1692
|
+
expect(body.code).toBe("INVALID_QUERY");
|
|
1693
|
+
});
|
|
1694
|
+
|
|
1695
|
+
test("GET /notes?cursor=...&order_by=... rejects with INVALID_QUERY", async () => {
|
|
1696
|
+
// Cursor pagination forces order by updated_at; order_by is mutually
|
|
1697
|
+
// exclusive. Same surface-parity assertion as the sort=desc test.
|
|
1698
|
+
await store.createNote("a", { id: "ob-a" });
|
|
1699
|
+
const seed = await store.queryNotesPaged({});
|
|
1700
|
+
const res = await handleNotes(
|
|
1701
|
+
mkReq("GET", `/notes?cursor=${encodeURIComponent(seed.next_cursor)}&order_by=created_at`),
|
|
1702
|
+
store,
|
|
1703
|
+
"",
|
|
1704
|
+
);
|
|
1705
|
+
expect(res.status).toBe(400);
|
|
1706
|
+
const body = await res.json() as any;
|
|
1707
|
+
expect(body.code).toBe("INVALID_QUERY");
|
|
1708
|
+
});
|
|
1709
|
+
|
|
1710
|
+
test("GET /notes?search=fox (without cursor) still works after the cursor+search guard", async () => {
|
|
1711
|
+
// Sanity check: the cursor+search guard must not regress plain FTS.
|
|
1712
|
+
await store.createNote("the quick brown fox", { id: "ss-a" });
|
|
1713
|
+
const res = await handleNotes(mkReq("GET", "/notes?search=fox"), store, "");
|
|
1714
|
+
expect(res.status).toBe(200);
|
|
1715
|
+
const body = await res.json() as any[];
|
|
1716
|
+
expect(body).toHaveLength(1);
|
|
1717
|
+
expect(body[0].id).toBe("ss-a");
|
|
1718
|
+
});
|
|
1719
|
+
|
|
1720
|
+
test("GET /notes?cursor=... resumes correctly across calls (end-to-end)", async () => {
|
|
1721
|
+
// Three notes spread across distinct updated_at watermarks. First
|
|
1722
|
+
// call returns the first batch, second call (with cursor) returns
|
|
1723
|
+
// only the note written after the cursor was minted.
|
|
1724
|
+
const a = await store.createNote("first", { id: "e2e-a" });
|
|
1725
|
+
db.prepare("UPDATE notes SET updated_at = ? WHERE id = ?")
|
|
1726
|
+
.run("2026-04-01T00:00:00.000Z", a.id);
|
|
1727
|
+
const b = await store.createNote("second", { id: "e2e-b" });
|
|
1728
|
+
db.prepare("UPDATE notes SET updated_at = ? WHERE id = ?")
|
|
1729
|
+
.run("2026-04-02T00:00:00.000Z", b.id);
|
|
1730
|
+
|
|
1731
|
+
const seed = await store.queryNotesPaged({});
|
|
1732
|
+
expect(seed.notes.map((n) => n.id).sort()).toEqual(["e2e-a", "e2e-b"]);
|
|
1733
|
+
|
|
1734
|
+
// Write a third note that lands AFTER the cursor's watermark.
|
|
1735
|
+
const c = await store.createNote("third", { id: "e2e-c" });
|
|
1736
|
+
db.prepare("UPDATE notes SET updated_at = ? WHERE id = ?")
|
|
1737
|
+
.run("2026-04-03T00:00:00.000Z", c.id);
|
|
1738
|
+
|
|
1739
|
+
const res = await handleNotes(
|
|
1740
|
+
mkReq("GET", `/notes?cursor=${encodeURIComponent(seed.next_cursor)}&include_content=true`),
|
|
1741
|
+
store,
|
|
1742
|
+
"",
|
|
1743
|
+
);
|
|
1744
|
+
expect(res.status).toBe(200);
|
|
1745
|
+
const body = await res.json() as any;
|
|
1746
|
+
expect(body.notes.map((n: any) => n.id)).toEqual(["e2e-c"]);
|
|
1747
|
+
});
|
|
1748
|
+
|
|
1578
1749
|
test("GET /notes?has_links=false returns only orphaned notes", async () => {
|
|
1579
1750
|
const a = await store.createNote("src", { id: "qa" });
|
|
1580
1751
|
const b = await store.createNote("tgt", { id: "qb" });
|
|
@@ -2543,6 +2714,182 @@ describe("HTTP /notes", async () => {
|
|
|
2543
2714
|
expect(body.map((n) => n.content)).toEqual(["new"]);
|
|
2544
2715
|
});
|
|
2545
2716
|
});
|
|
2717
|
+
|
|
2718
|
+
// -------------------------------------------------------------------------
|
|
2719
|
+
// POST /notes/:id/retry-transcription — vault#353 design Q5.
|
|
2720
|
+
//
|
|
2721
|
+
// The endpoint re-enqueues a failed transcript by flipping the matching
|
|
2722
|
+
// attachment back to `transcribe_status: pending`. The worker (or sweep)
|
|
2723
|
+
// picks it up. These tests exercise the request shape + error branches;
|
|
2724
|
+
// the actual re-transcription is covered in transcription-worker.test.ts's
|
|
2725
|
+
// auto-origin section.
|
|
2726
|
+
// -------------------------------------------------------------------------
|
|
2727
|
+
describe("POST /notes/:id/retry-transcription", async () => {
|
|
2728
|
+
async function seedFailedTranscript(opts: {
|
|
2729
|
+
audioPath?: string;
|
|
2730
|
+
noteId?: string;
|
|
2731
|
+
transcriptId?: string;
|
|
2732
|
+
omitAttachmentId?: boolean;
|
|
2733
|
+
} = {}): Promise<{ owner: { id: string }; attachmentId: string; transcriptId: string; audioPath: string }> {
|
|
2734
|
+
const audioPath = opts.audioPath ?? `${opts.noteId ?? "src"}/voice.webm`;
|
|
2735
|
+
const ownerId = opts.noteId ?? "src-note";
|
|
2736
|
+
const owner = await store.createNote("# Voice\n", { id: ownerId });
|
|
2737
|
+
const att = await store.addAttachment(owner.id, audioPath, "audio/webm", {
|
|
2738
|
+
transcribe_status: "failed",
|
|
2739
|
+
transcribe_origin: "auto",
|
|
2740
|
+
transcribe_error: "no transcription provider configured",
|
|
2741
|
+
});
|
|
2742
|
+
// Seed the failed transcript note exactly as the worker would have.
|
|
2743
|
+
const transcriptMeta: Record<string, unknown> = {
|
|
2744
|
+
transcript_of: audioPath,
|
|
2745
|
+
transcript_status: "failed",
|
|
2746
|
+
transcript_error: "missing_provider: no transcription provider configured",
|
|
2747
|
+
};
|
|
2748
|
+
if (!opts.omitAttachmentId) transcriptMeta.transcript_attachment_id = att.id;
|
|
2749
|
+
await store.createNote("", {
|
|
2750
|
+
id: opts.transcriptId ?? "transcript-1",
|
|
2751
|
+
path: `${audioPath}.transcript`,
|
|
2752
|
+
tags: ["transcript", "capture"],
|
|
2753
|
+
metadata: transcriptMeta,
|
|
2754
|
+
});
|
|
2755
|
+
// Audio file present on disk so the retry doesn't 404 on audio_missing.
|
|
2756
|
+
const assetsRoot = join(tmpDir, "assets");
|
|
2757
|
+
mkdirSync(join(assetsRoot, audioPath.split("/").slice(0, -1).join("/")), { recursive: true });
|
|
2758
|
+
writeFileSync(join(assetsRoot, audioPath), Buffer.from([1, 2, 3]));
|
|
2759
|
+
process.env.ASSETS_DIR = assetsRoot;
|
|
2760
|
+
return { owner: { id: ownerId }, attachmentId: att.id, transcriptId: opts.transcriptId ?? "transcript-1", audioPath };
|
|
2761
|
+
}
|
|
2762
|
+
|
|
2763
|
+
test("happy path: flips attachment to pending and returns 202", async () => {
|
|
2764
|
+
const { attachmentId, transcriptId } = await seedFailedTranscript();
|
|
2765
|
+
const res = await handleNotes(
|
|
2766
|
+
mkReq("POST", `/notes/${transcriptId}/retry-transcription`),
|
|
2767
|
+
store,
|
|
2768
|
+
`/${transcriptId}/retry-transcription`,
|
|
2769
|
+
"default",
|
|
2770
|
+
);
|
|
2771
|
+
expect(res.status).toBe(202);
|
|
2772
|
+
const body = await res.json() as any;
|
|
2773
|
+
expect(body.status).toBe("queued");
|
|
2774
|
+
expect(body.attachment_id).toBe(attachmentId);
|
|
2775
|
+
expect(body.transcript_note_id).toBe(transcriptId);
|
|
2776
|
+
|
|
2777
|
+
// Attachment metadata reset.
|
|
2778
|
+
const att = await store.getAttachment(attachmentId);
|
|
2779
|
+
expect(att?.metadata?.transcribe_status).toBe("pending");
|
|
2780
|
+
expect(att?.metadata?.transcribe_origin).toBe("auto");
|
|
2781
|
+
// Stale failure state cleared.
|
|
2782
|
+
expect(att?.metadata?.transcribe_error).toBeUndefined();
|
|
2783
|
+
expect(att?.metadata?.transcribe_attempts).toBeUndefined();
|
|
2784
|
+
|
|
2785
|
+
delete process.env.ASSETS_DIR;
|
|
2786
|
+
});
|
|
2787
|
+
|
|
2788
|
+
test("404 when transcript note doesn't exist", async () => {
|
|
2789
|
+
const res = await handleNotes(
|
|
2790
|
+
mkReq("POST", "/notes/no-such-id/retry-transcription"),
|
|
2791
|
+
store,
|
|
2792
|
+
"/no-such-id/retry-transcription",
|
|
2793
|
+
"default",
|
|
2794
|
+
);
|
|
2795
|
+
expect(res.status).toBe(404);
|
|
2796
|
+
});
|
|
2797
|
+
|
|
2798
|
+
test("400 invalid_target when target is not a transcript note", async () => {
|
|
2799
|
+
await store.createNote("regular note", { id: "regular" });
|
|
2800
|
+
const res = await handleNotes(
|
|
2801
|
+
mkReq("POST", "/notes/regular/retry-transcription"),
|
|
2802
|
+
store,
|
|
2803
|
+
"/regular/retry-transcription",
|
|
2804
|
+
"default",
|
|
2805
|
+
);
|
|
2806
|
+
expect(res.status).toBe(400);
|
|
2807
|
+
const body = await res.json() as any;
|
|
2808
|
+
expect(body.error).toBe("invalid_target");
|
|
2809
|
+
});
|
|
2810
|
+
|
|
2811
|
+
test("400 not_failed when transcript already succeeded", async () => {
|
|
2812
|
+
const audioPath = "memo/done.webm";
|
|
2813
|
+
const owner = await store.createNote("voice", { id: "src-done" });
|
|
2814
|
+
const att = await store.addAttachment(owner.id, audioPath, "audio/webm");
|
|
2815
|
+
await store.createNote("the spoken words", {
|
|
2816
|
+
id: "transcript-done",
|
|
2817
|
+
path: `${audioPath}.transcript`,
|
|
2818
|
+
metadata: {
|
|
2819
|
+
transcript_of: audioPath,
|
|
2820
|
+
transcript_attachment_id: att.id,
|
|
2821
|
+
transcript_status: "complete",
|
|
2822
|
+
},
|
|
2823
|
+
});
|
|
2824
|
+
const res = await handleNotes(
|
|
2825
|
+
mkReq("POST", "/notes/transcript-done/retry-transcription"),
|
|
2826
|
+
store,
|
|
2827
|
+
"/transcript-done/retry-transcription",
|
|
2828
|
+
"default",
|
|
2829
|
+
);
|
|
2830
|
+
expect(res.status).toBe(400);
|
|
2831
|
+
const body = await res.json() as any;
|
|
2832
|
+
expect(body.error).toBe("not_failed");
|
|
2833
|
+
expect(body.transcript_status).toBe("complete");
|
|
2834
|
+
});
|
|
2835
|
+
|
|
2836
|
+
test("400 missing_attachment_id when frontmatter lacks the id", async () => {
|
|
2837
|
+
await seedFailedTranscript({
|
|
2838
|
+
transcriptId: "transcript-no-id",
|
|
2839
|
+
noteId: "src-no-id",
|
|
2840
|
+
omitAttachmentId: true,
|
|
2841
|
+
});
|
|
2842
|
+
const res = await handleNotes(
|
|
2843
|
+
mkReq("POST", "/notes/transcript-no-id/retry-transcription"),
|
|
2844
|
+
store,
|
|
2845
|
+
"/transcript-no-id/retry-transcription",
|
|
2846
|
+
"default",
|
|
2847
|
+
);
|
|
2848
|
+
expect(res.status).toBe(400);
|
|
2849
|
+
const body = await res.json() as any;
|
|
2850
|
+
expect(body.error).toBe("missing_attachment_id");
|
|
2851
|
+
delete process.env.ASSETS_DIR;
|
|
2852
|
+
});
|
|
2853
|
+
|
|
2854
|
+
test("404 attachment_missing when the attachment row no longer exists", async () => {
|
|
2855
|
+
const owner = await store.createNote("voice", { id: "src-stale" });
|
|
2856
|
+
await store.createNote("", {
|
|
2857
|
+
id: "transcript-stale",
|
|
2858
|
+
path: "memo/stale.webm.transcript",
|
|
2859
|
+
metadata: {
|
|
2860
|
+
transcript_of: "memo/stale.webm",
|
|
2861
|
+
transcript_attachment_id: "deleted-attachment-id",
|
|
2862
|
+
transcript_status: "failed",
|
|
2863
|
+
transcript_error: "missing_provider",
|
|
2864
|
+
},
|
|
2865
|
+
});
|
|
2866
|
+
const res = await handleNotes(
|
|
2867
|
+
mkReq("POST", "/notes/transcript-stale/retry-transcription"),
|
|
2868
|
+
store,
|
|
2869
|
+
"/transcript-stale/retry-transcription",
|
|
2870
|
+
"default",
|
|
2871
|
+
);
|
|
2872
|
+
expect(res.status).toBe(404);
|
|
2873
|
+
const body = await res.json() as any;
|
|
2874
|
+
expect(body.error).toBe("attachment_missing");
|
|
2875
|
+
});
|
|
2876
|
+
|
|
2877
|
+
test("405 on GET (must POST)", async () => {
|
|
2878
|
+
await seedFailedTranscript({
|
|
2879
|
+
transcriptId: "transcript-405",
|
|
2880
|
+
noteId: "src-405",
|
|
2881
|
+
audioPath: "memo/405.webm",
|
|
2882
|
+
});
|
|
2883
|
+
const res = await handleNotes(
|
|
2884
|
+
mkReq("GET", "/notes/transcript-405/retry-transcription"),
|
|
2885
|
+
store,
|
|
2886
|
+
"/transcript-405/retry-transcription",
|
|
2887
|
+
"default",
|
|
2888
|
+
);
|
|
2889
|
+
expect(res.status).toBe(405);
|
|
2890
|
+
delete process.env.ASSETS_DIR;
|
|
2891
|
+
});
|
|
2892
|
+
});
|
|
2546
2893
|
});
|
|
2547
2894
|
|
|
2548
2895
|
describe("HTTP GET /notes?format=graph", async () => {
|