@rubytech/create-realagent 1.0.828 → 1.0.829
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/package.json +1 -1
- package/payload/platform/neo4j/schema.cypher +2 -1
- package/payload/platform/package.json +2 -2
- package/payload/platform/plugins/admin/hooks/__tests__/archive-ingest-surface-gate.test.sh +39 -54
- package/payload/platform/plugins/admin/hooks/archive-ingest-surface-gate.sh +26 -58
- package/payload/platform/plugins/admin/skills/onboarding/SKILL.md +2 -2
- package/payload/platform/plugins/docs/references/plugins-guide.md +1 -1
- package/payload/platform/plugins/memory/PLUGIN.md +4 -4
- package/payload/platform/plugins/memory/mcp/dist/index.js +18 -218
- package/payload/platform/plugins/memory/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/schema-validator.test.js +103 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/schema-validator.test.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/llm-classifier.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/llm-classifier.js +30 -20
- package/payload/platform/plugins/memory/mcp/dist/lib/llm-classifier.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/schema-validator.d.ts +16 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/schema-validator.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/schema-validator.js +12 -3
- package/payload/platform/plugins/memory/mcp/dist/lib/schema-validator.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-archive-write.test.js +2 -138
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-archive-write.test.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-ingest.test.js +10 -5
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-ingest.test.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/profile-update-personfields-open.test.d.ts +2 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/profile-update-personfields-open.test.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/profile-update-personfields-open.test.js +148 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/profile-update-personfields-open.test.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-archive-write.d.ts +1 -64
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-archive-write.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-archive-write.js +6 -336
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-archive-write.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.d.ts +7 -11
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.js +1 -11
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/profile-update.d.ts +21 -17
- package/payload/platform/plugins/memory/mcp/dist/tools/profile-update.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/profile-update.js +77 -37
- package/payload/platform/plugins/memory/mcp/dist/tools/profile-update.js.map +1 -1
- package/payload/platform/plugins/memory/references/schema-base.md +2 -0
- package/payload/platform/plugins/memory/skills/document-ingest/SKILL.md +54 -4
- package/payload/platform/plugins/whatsapp/PLUGIN.md +1 -1
- package/payload/platform/scripts/seed-neo4j.sh +15 -14
- package/payload/platform/templates/specialists/agents/database-operator.md +9 -15
- package/payload/server/chunk-CUSH3UXP.js +2305 -0
- package/payload/server/chunk-IWNDVGKT.js +10077 -0
- package/payload/server/chunk-KC7NUABI.js +654 -0
- package/payload/server/chunk-WUVXPZIV.js +1116 -0
- package/payload/server/client-pool-3TM3SRIA.js +32 -0
- package/payload/server/cloudflare-task-tracker-4NIODMGL.js +19 -0
- package/payload/server/maxy-edge.js +3 -3
- package/payload/server/neo4j-migrations-XTQ4WEV6.js +428 -0
- package/payload/server/server.js +6 -6
- package/payload/platform/plugins/whatsapp-import/PLUGIN.md +0 -48
- package/payload/platform/plugins/whatsapp-import/bin/ingest.mjs +0 -617
- package/payload/platform/plugins/whatsapp-import/bin/whatsapp-ingest.sh +0 -98
- package/payload/platform/plugins/whatsapp-import/lib/src/__tests__/delta-append.test.ts +0 -163
- package/payload/platform/plugins/whatsapp-import/lib/src/__tests__/parse-export-lrm.test.ts +0 -83
- package/payload/platform/plugins/whatsapp-import/lib/src/__tests__/parse-export.test.ts +0 -678
- package/payload/platform/plugins/whatsapp-import/lib/src/__tests__/sessionize.test.ts +0 -91
- package/payload/platform/plugins/whatsapp-import/lib/src/__tests__/to-classifier-input.test.ts +0 -59
- package/payload/platform/plugins/whatsapp-import/lib/src/delta-cursor.ts +0 -54
- package/payload/platform/plugins/whatsapp-import/lib/src/derive-keys.ts +0 -82
- package/payload/platform/plugins/whatsapp-import/lib/src/index.ts +0 -22
- package/payload/platform/plugins/whatsapp-import/lib/src/parse-export.ts +0 -471
- package/payload/platform/plugins/whatsapp-import/lib/src/sessionize.ts +0 -81
- package/payload/platform/plugins/whatsapp-import/lib/src/to-classifier-input.ts +0 -48
- package/payload/platform/plugins/whatsapp-import/lib/tsconfig.json +0 -9
- package/payload/platform/plugins/whatsapp-import/lib/vitest.config.ts +0 -9
- package/payload/platform/plugins/whatsapp-import/skills/whatsapp-import/SKILL.md +0 -124
- package/payload/platform/plugins/whatsapp-import/skills/whatsapp-import/references/conversation-archive-shape.md +0 -143
- package/payload/platform/plugins/whatsapp-import/skills/whatsapp-import/references/export-parse.md +0 -109
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# =============================================================================
|
|
3
|
-
# whatsapp-ingest.sh — single deterministic Bash entry for WhatsApp archive
|
|
4
|
-
# ingestion (Task 891 — chunked :ConversationArchive shape).
|
|
5
|
-
#
|
|
6
|
-
# Pipeline: parse → bind canonical sender set → derive conversationIdentity →
|
|
7
|
-
# look up prior :ConversationArchive (delta cursor) → sessionize delta at
|
|
8
|
-
# gap-hours boundary → classify each session via Haiku (mode='chat') →
|
|
9
|
-
# memory-ingest with parentLabel='ConversationArchive'.
|
|
10
|
-
#
|
|
11
|
-
# Usage:
|
|
12
|
-
# bash whatsapp-ingest.sh <archive.zip|dir|_chat.txt>
|
|
13
|
-
# --owner-element-id <id>
|
|
14
|
-
# --participant-person-ids <csv>
|
|
15
|
-
# --scope <admin|public>
|
|
16
|
-
# [--session-gap-hours <N>] (default 12)
|
|
17
|
-
# [--account-id <accountId>]
|
|
18
|
-
# [--timezone <iana-zone>]
|
|
19
|
-
# [--date-format <DD/MM/YY|MM/DD/YY|DD/MM/YYYY|MM/DD/YYYY>]
|
|
20
|
-
#
|
|
21
|
-
# `--owner-element-id` + `--participant-person-ids` form the closed sender
|
|
22
|
-
# set; any parsed senderName outside that set LOUD-FAILs with `parser-miss`
|
|
23
|
-
# and exits non-zero. `--filter` and `--subject-person-id` are gone — chunked
|
|
24
|
-
# Section:Conversation rows bound the operator surface naturally.
|
|
25
|
-
#
|
|
26
|
-
# Exit 0 + JSON summary on stdout on success.
|
|
27
|
-
# Exit !0 + one [whatsapp-import] FAIL line on stderr on failure.
|
|
28
|
-
# =============================================================================
|
|
29
|
-
|
|
30
|
-
set -euo pipefail
|
|
31
|
-
|
|
32
|
-
arg_fail() {
|
|
33
|
-
local reason="$1"
|
|
34
|
-
echo "[whatsapp-import] FAIL phase=argv reason=\"${reason}\"" >&2
|
|
35
|
-
exit 1
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
39
|
-
PLATFORM_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
|
|
40
|
-
INGEST_MJS="$SCRIPT_DIR/ingest.mjs"
|
|
41
|
-
|
|
42
|
-
if [ ! -f "$INGEST_MJS" ]; then
|
|
43
|
-
arg_fail "ingest.mjs not found at $INGEST_MJS — run from a built install"
|
|
44
|
-
fi
|
|
45
|
-
|
|
46
|
-
ARCHIVE=""
|
|
47
|
-
HAS_OWNER=0
|
|
48
|
-
OWNER_VAL=""
|
|
49
|
-
HAS_PARTICIPANTS=0
|
|
50
|
-
PARTICIPANTS_VAL=""
|
|
51
|
-
HAS_SCOPE=0
|
|
52
|
-
SCOPE_VAL=""
|
|
53
|
-
|
|
54
|
-
ARGS=("$@")
|
|
55
|
-
i=0
|
|
56
|
-
while [ $i -lt ${#ARGS[@]} ]; do
|
|
57
|
-
a="${ARGS[$i]}"
|
|
58
|
-
case "$a" in
|
|
59
|
-
--owner-element-id) HAS_OWNER=1; OWNER_VAL="${ARGS[$((i + 1))]:-}"; i=$((i + 2)); continue ;;
|
|
60
|
-
--participant-person-ids) HAS_PARTICIPANTS=1; PARTICIPANTS_VAL="${ARGS[$((i + 1))]:-}"; i=$((i + 2)); continue ;;
|
|
61
|
-
--scope) HAS_SCOPE=1; SCOPE_VAL="${ARGS[$((i + 1))]:-}"; i=$((i + 2)); continue ;;
|
|
62
|
-
--session-gap-hours|--account-id|--timezone|--date-format) i=$((i + 2)); continue ;;
|
|
63
|
-
--*) i=$((i + 2)); continue ;;
|
|
64
|
-
*)
|
|
65
|
-
if [ -z "$ARCHIVE" ]; then ARCHIVE="$a"; fi
|
|
66
|
-
i=$((i + 1))
|
|
67
|
-
continue
|
|
68
|
-
;;
|
|
69
|
-
esac
|
|
70
|
-
done
|
|
71
|
-
|
|
72
|
-
[ -n "$ARCHIVE" ] || arg_fail "missing positional <archive>"
|
|
73
|
-
[ "$HAS_OWNER" -eq 1 ] && [ -n "$OWNER_VAL" ] || arg_fail "missing --owner-element-id (or empty value)"
|
|
74
|
-
[ "$HAS_PARTICIPANTS" -eq 1 ] && [ -n "$PARTICIPANTS_VAL" ] || arg_fail "missing --participant-person-ids (csv of operator-confirmed :Person/:AdminUser elementIds, owner excluded)"
|
|
75
|
-
[ "$HAS_SCOPE" -eq 1 ] && [ -n "$SCOPE_VAL" ] || arg_fail "missing --scope (or empty value)"
|
|
76
|
-
case "$SCOPE_VAL" in
|
|
77
|
-
admin|public) : ;;
|
|
78
|
-
*) arg_fail "invalid --scope \"$SCOPE_VAL\" (admin|public)" ;;
|
|
79
|
-
esac
|
|
80
|
-
|
|
81
|
-
if [ -z "${NEO4J_PASSWORD:-}" ]; then
|
|
82
|
-
NEO4J_PASSWORD_FILE="$PLATFORM_ROOT/config/.neo4j-password"
|
|
83
|
-
if [ -f "$NEO4J_PASSWORD_FILE" ]; then
|
|
84
|
-
NEO4J_PASSWORD="$(cat "$NEO4J_PASSWORD_FILE")"
|
|
85
|
-
export NEO4J_PASSWORD
|
|
86
|
-
else
|
|
87
|
-
arg_fail "NEO4J_PASSWORD not in env and $NEO4J_PASSWORD_FILE not found"
|
|
88
|
-
fi
|
|
89
|
-
fi
|
|
90
|
-
|
|
91
|
-
if [ -z "${NEO4J_URI:-}" ]; then
|
|
92
|
-
arg_fail "NEO4J_URI not set (no default — set in env)"
|
|
93
|
-
fi
|
|
94
|
-
|
|
95
|
-
export NEO4J_USER="${NEO4J_USER:-neo4j}"
|
|
96
|
-
export MAXY_PLATFORM_ROOT="$PLATFORM_ROOT"
|
|
97
|
-
|
|
98
|
-
exec node "$INGEST_MJS" "$@"
|
|
@@ -1,163 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { findDeltaCursor } from "../delta-cursor.js";
|
|
3
|
-
import {
|
|
4
|
-
deriveConversationIdentity,
|
|
5
|
-
deriveMessageContentHash,
|
|
6
|
-
normaliseSenderName,
|
|
7
|
-
} from "../derive-keys.js";
|
|
8
|
-
import type { ParsedLine } from "../parse-export.js";
|
|
9
|
-
|
|
10
|
-
function mk(dateSent: string, senderName: string, body: string): ParsedLine {
|
|
11
|
-
return { dateSent, senderName, body, sequenceIndex: 0 };
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
describe("deriveConversationIdentity", () => {
|
|
15
|
-
it("is stable across participant order (sorted internally)", () => {
|
|
16
|
-
const a = deriveConversationIdentity({
|
|
17
|
-
accountId: "acct-1",
|
|
18
|
-
participantElementIds: ["1:abc:1", "1:abc:2"],
|
|
19
|
-
});
|
|
20
|
-
const b = deriveConversationIdentity({
|
|
21
|
-
accountId: "acct-1",
|
|
22
|
-
participantElementIds: ["1:abc:2", "1:abc:1"],
|
|
23
|
-
});
|
|
24
|
-
expect(a).toBe(b);
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
it("changes when accountId differs", () => {
|
|
28
|
-
const a = deriveConversationIdentity({
|
|
29
|
-
accountId: "acct-1",
|
|
30
|
-
participantElementIds: ["1:abc:1", "1:abc:2"],
|
|
31
|
-
});
|
|
32
|
-
const b = deriveConversationIdentity({
|
|
33
|
-
accountId: "acct-2",
|
|
34
|
-
participantElementIds: ["1:abc:1", "1:abc:2"],
|
|
35
|
-
});
|
|
36
|
-
expect(a).not.toBe(b);
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it("rejects empty participant array", () => {
|
|
40
|
-
expect(() =>
|
|
41
|
-
deriveConversationIdentity({
|
|
42
|
-
accountId: "acct-1",
|
|
43
|
-
participantElementIds: [],
|
|
44
|
-
}),
|
|
45
|
-
).toThrow(/non-empty/);
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
it("rejects empty accountId", () => {
|
|
49
|
-
expect(() =>
|
|
50
|
-
deriveConversationIdentity({
|
|
51
|
-
accountId: "",
|
|
52
|
-
participantElementIds: ["1:abc:1"],
|
|
53
|
-
}),
|
|
54
|
-
).toThrow(/accountId/);
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it("treats DM and group identically (same formula, different array length)", () => {
|
|
58
|
-
const dm = deriveConversationIdentity({
|
|
59
|
-
accountId: "acct-1",
|
|
60
|
-
participantElementIds: ["1:a:1", "1:a:2"],
|
|
61
|
-
});
|
|
62
|
-
const group = deriveConversationIdentity({
|
|
63
|
-
accountId: "acct-1",
|
|
64
|
-
participantElementIds: ["1:a:1", "1:a:2", "1:a:3"],
|
|
65
|
-
});
|
|
66
|
-
expect(dm).not.toBe(group);
|
|
67
|
-
// Both must hex-decode to a sha256 (64 hex chars).
|
|
68
|
-
expect(dm).toMatch(/^[a-f0-9]{64}$/);
|
|
69
|
-
expect(group).toMatch(/^[a-f0-9]{64}$/);
|
|
70
|
-
});
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
describe("deriveMessageContentHash", () => {
|
|
74
|
-
it("is content-only — no archive bytes contribute", () => {
|
|
75
|
-
const a = deriveMessageContentHash({
|
|
76
|
-
dateSent: "2026-03-14T10:00:00+00:00",
|
|
77
|
-
senderName: "Joel",
|
|
78
|
-
body: "hi",
|
|
79
|
-
});
|
|
80
|
-
const b = deriveMessageContentHash({
|
|
81
|
-
dateSent: "2026-03-14T10:00:00+00:00",
|
|
82
|
-
senderName: "Joel",
|
|
83
|
-
body: "hi",
|
|
84
|
-
});
|
|
85
|
-
expect(a).toBe(b);
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
it("changes when any field changes", () => {
|
|
89
|
-
const base = {
|
|
90
|
-
dateSent: "2026-03-14T10:00:00+00:00",
|
|
91
|
-
senderName: "Joel",
|
|
92
|
-
body: "hi",
|
|
93
|
-
};
|
|
94
|
-
expect(deriveMessageContentHash(base)).not.toBe(
|
|
95
|
-
deriveMessageContentHash({ ...base, body: "hello" }),
|
|
96
|
-
);
|
|
97
|
-
expect(deriveMessageContentHash(base)).not.toBe(
|
|
98
|
-
deriveMessageContentHash({ ...base, senderName: "Adam" }),
|
|
99
|
-
);
|
|
100
|
-
expect(deriveMessageContentHash(base)).not.toBe(
|
|
101
|
-
deriveMessageContentHash({ ...base, dateSent: "2026-03-14T10:00:01+00:00" }),
|
|
102
|
-
);
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
it("collapses NFKC-equivalent sender names to one hash", () => {
|
|
106
|
-
const a = deriveMessageContentHash({
|
|
107
|
-
dateSent: "2026-03-14T10:00:00+00:00",
|
|
108
|
-
senderName: " ADAM Mackay ",
|
|
109
|
-
body: "hi",
|
|
110
|
-
});
|
|
111
|
-
const b = deriveMessageContentHash({
|
|
112
|
-
dateSent: "2026-03-14T10:00:00+00:00",
|
|
113
|
-
senderName: "adam mackay",
|
|
114
|
-
body: "hi",
|
|
115
|
-
});
|
|
116
|
-
expect(a).toBe(b);
|
|
117
|
-
});
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
describe("normaliseSenderName", () => {
|
|
121
|
-
it("returns NFKC-trim-lower form", () => {
|
|
122
|
-
expect(normaliseSenderName(" Adam Mackay ")).toBe("adam mackay");
|
|
123
|
-
});
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
describe("findDeltaCursor", () => {
|
|
127
|
-
const lines: ParsedLine[] = [
|
|
128
|
-
mk("2026-03-14T10:00:00+00:00", "Joel", "first"),
|
|
129
|
-
mk("2026-03-14T10:01:00+00:00", "Adam", "second"),
|
|
130
|
-
mk("2026-03-14T10:02:00+00:00", "Joel", "third"),
|
|
131
|
-
];
|
|
132
|
-
const hashFor = (l: ParsedLine) =>
|
|
133
|
-
deriveMessageContentHash({
|
|
134
|
-
dateSent: l.dateSent,
|
|
135
|
-
senderName: l.senderName,
|
|
136
|
-
body: l.body,
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
it("returns 'found' with deltaStart = cursor + 1 when the hash matches a prior line", () => {
|
|
140
|
-
const result = findDeltaCursor(lines, hashFor(lines[0]));
|
|
141
|
-
expect(result).toEqual({ kind: "found", deltaStart: 1 });
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
it("returns 'empty' when the cursor is the last line (no new messages)", () => {
|
|
145
|
-
const result = findDeltaCursor(lines, hashFor(lines[2]));
|
|
146
|
-
expect(result).toEqual({ kind: "empty" });
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
it("returns 'missing' when no parsed line matches (LOUD-FAIL upstream)", () => {
|
|
150
|
-
const result = findDeltaCursor(lines, "deadbeef".repeat(8));
|
|
151
|
-
expect(result).toEqual({ kind: "missing" });
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
it("returns 'missing' for an empty parsed sequence (cursor cannot exist)", () => {
|
|
155
|
-
const result = findDeltaCursor([], hashFor(lines[0]));
|
|
156
|
-
expect(result).toEqual({ kind: "missing" });
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
it("rejects empty cursor hash", () => {
|
|
160
|
-
expect(() => findDeltaCursor(lines, "")).toThrow(/non-empty/);
|
|
161
|
-
expect(() => findDeltaCursor(lines, " ")).toThrow(/non-empty/);
|
|
162
|
-
});
|
|
163
|
-
});
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
-
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
-
import { tmpdir } from "node:os";
|
|
4
|
-
import { join } from "node:path";
|
|
5
|
-
import { parseExport } from "../parse-export.js";
|
|
6
|
-
|
|
7
|
-
// Task 887 — bidi-strip regression. Some WhatsApp exports prefix every
|
|
8
|
-
// timestamp header with U+200E (LEFT-TO-RIGHT MARK) or U+200F (RTL MARK).
|
|
9
|
-
// Pre-fix `decodeAndNormalise` left those bytes in place; the timestamp
|
|
10
|
-
// regex (`^\[(\d{2})\/...`) failed; the LRM-prefixed line was glued onto
|
|
11
|
-
// the previous body as a continuation; the next clean header parsed its
|
|
12
|
-
// senderName off the polluted body — leaking 23 :Person nodes per import
|
|
13
|
-
// in the Adam Mackay archive. The fix strips U+200E/U+200F before
|
|
14
|
-
// tokenisation; this test reproduces the failure shape.
|
|
15
|
-
|
|
16
|
-
let workDir: string;
|
|
17
|
-
|
|
18
|
-
beforeEach(() => {
|
|
19
|
-
workDir = mkdtempSync(join(tmpdir(), "whatsapp-export-lrm-"));
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
afterEach(() => {
|
|
23
|
-
rmSync(workDir, { recursive: true, force: true });
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
function writeChat(name: string, content: string): string {
|
|
27
|
-
const filePath = join(workDir, name);
|
|
28
|
-
writeFileSync(filePath, content);
|
|
29
|
-
return filePath;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
describe("parseExport — bidi-strip (Task 887)", () => {
|
|
33
|
-
it("strips U+200E from timestamp headers and parses each row independently", () => {
|
|
34
|
-
const LRM = "";
|
|
35
|
-
const filePath = writeChat(
|
|
36
|
-
"_chat.txt",
|
|
37
|
-
[
|
|
38
|
-
`${LRM}[04/02/26, 11:52:16] Adam Mackay: hi`,
|
|
39
|
-
`${LRM}[04/02/26, 11:52:30] Joel Smalley: hey`,
|
|
40
|
-
"",
|
|
41
|
-
].join("\n"),
|
|
42
|
-
);
|
|
43
|
-
|
|
44
|
-
const result = parseExport({
|
|
45
|
-
filePath,
|
|
46
|
-
accountId: "acct-887",
|
|
47
|
-
timezone: "Europe/London",
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
expect(result.parsedLines).toHaveLength(2);
|
|
51
|
-
expect(result.parsedLines.map((l) => l.senderName).sort()).toEqual([
|
|
52
|
-
"Adam Mackay",
|
|
53
|
-
"Joel Smalley",
|
|
54
|
-
]);
|
|
55
|
-
for (const line of result.parsedLines) {
|
|
56
|
-
expect(line.senderName).not.toContain("\n");
|
|
57
|
-
expect(line.senderName).not.toContain("[");
|
|
58
|
-
expect(line.senderName).not.toContain(LRM);
|
|
59
|
-
}
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it("strips U+200F (RLM) on the timestamp line", () => {
|
|
63
|
-
const RLM = "";
|
|
64
|
-
const filePath = writeChat(
|
|
65
|
-
"_chat.txt",
|
|
66
|
-
[
|
|
67
|
-
`${RLM}[14/03/26, 10:15:23] Joel: Hello`,
|
|
68
|
-
`${RLM}[14/03/26, 10:16:01] Sarah: Hi back`,
|
|
69
|
-
"",
|
|
70
|
-
].join("\n"),
|
|
71
|
-
);
|
|
72
|
-
|
|
73
|
-
const result = parseExport({
|
|
74
|
-
filePath,
|
|
75
|
-
accountId: "acct-887",
|
|
76
|
-
timezone: "Europe/London",
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
expect(result.parsedLines).toHaveLength(2);
|
|
80
|
-
expect(result.parsedLines[0].senderName).toBe("Joel");
|
|
81
|
-
expect(result.parsedLines[1].senderName).toBe("Sarah");
|
|
82
|
-
});
|
|
83
|
-
});
|