block-in-file 1.0.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.
- package/- +3 -0
- package/.beads/README.md +85 -0
- package/.beads/config.yaml +67 -0
- package/.beads/interactions.jsonl +0 -0
- package/.beads/issues.jsonl +23 -0
- package/.beads/metadata.json +4 -0
- package/.git-blame-ignore-revs +2 -0
- package/.gitattributes +3 -0
- package/.prettierrc.json +5 -0
- package/AGENTS.md +40 -0
- package/README.md +122 -0
- package/block-in-file.ts +150 -0
- package/content +10 -0
- package/deno.json +14 -0
- package/deno.lock +1084 -0
- package/doc/PLAN-envsubst.md +200 -0
- package/doc/PLAN-restructure.md +114 -0
- package/package.json +44 -0
- package/src/attributes.ts +161 -0
- package/src/backup.ts +180 -0
- package/src/block-parser.ts +170 -0
- package/src/block-remover.ts +128 -0
- package/src/conflict-detection.ts +179 -0
- package/src/defaults.ts +23 -0
- package/src/envsubst.ts +59 -0
- package/src/file-processor.ts +378 -0
- package/src/index.ts +5 -0
- package/src/input.ts +69 -0
- package/src/mode-handler.ts +39 -0
- package/src/output.ts +107 -0
- package/src/plugins/.beads/.local_version +1 -0
- package/src/plugins/.beads/issues.jsonl +21 -0
- package/src/plugins/.beads/metadata.json +4 -0
- package/src/plugins/config.ts +282 -0
- package/src/plugins/diff.ts +109 -0
- package/src/plugins/io.ts +72 -0
- package/src/plugins/logger.ts +41 -0
- package/src/tags/tag-merger.ts +31 -0
- package/src/tags/tag-mode.ts +1 -0
- package/src/tags/tag.ts +36 -0
- package/src/tags/tags.ts +4 -0
- package/src/tags/types.ts +4 -0
- package/src/timestamp.ts +39 -0
- package/src/types.ts +32 -0
- package/src/validation.ts +11 -0
- package/test/additive-cli.test.ts +109 -0
- package/test/additive.test.ts +233 -0
- package/test/attributes-integration.test.ts +161 -0
- package/test/attributes.test.ts +100 -0
- package/test/backup.test.ts +386 -0
- package/test/block-in-file.test.ts +235 -0
- package/test/block-parser.test.ts +221 -0
- package/test/block-remover.test.ts +209 -0
- package/test/cli.test.ts +254 -0
- package/test/defaults.test.ts +38 -0
- package/test/envsubst-edge-cases.test.ts +116 -0
- package/test/envsubst-integration.test.ts +78 -0
- package/test/envsubst.test.ts +184 -0
- package/test/input.test.ts +86 -0
- package/test/mode.test.ts +193 -0
- package/test/output.test.ts +44 -0
- package/test/tag-merger.test.ts +176 -0
- package/test/tags.test.ts +116 -0
- package/test/timestamp-integration.test.ts +209 -0
- package/test/timestamp.test.ts +76 -0
- package/tsconfig.json +16 -0
- package/vitest.config.ts +8 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
|
|
3
|
+
function get<T>(source: Partial<T>, defaults: Partial<T>, ...keys: (keyof T)[]) {
|
|
4
|
+
if (keys.length === 1) {
|
|
5
|
+
const key = keys[0];
|
|
6
|
+
const o = source[key];
|
|
7
|
+
return (o !== undefined ? o : defaults?.[key]) as unknown;
|
|
8
|
+
}
|
|
9
|
+
const o = Array.from({ length: keys.length });
|
|
10
|
+
for (const i in keys) {
|
|
11
|
+
o[i] = get(source, defaults, keys[i]);
|
|
12
|
+
}
|
|
13
|
+
return o;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function createOpt(
|
|
17
|
+
value: boolean | 0 | 1 | "file" | "block",
|
|
18
|
+
target: "file" | "block",
|
|
19
|
+
): { create: boolean } | undefined {
|
|
20
|
+
return value === 1 || value === true || value === target ? { create: true } : undefined;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe("input utilities", () => {
|
|
24
|
+
describe("get", () => {
|
|
25
|
+
const source: Partial<{ a: number; b: number; c: number }> = { a: 1, b: 2 };
|
|
26
|
+
const defaults: Partial<{ a: number; b: number; c: number }> = { a: 10, b: 20, c: 30 };
|
|
27
|
+
|
|
28
|
+
it.each([
|
|
29
|
+
{ key: "a" as const, expected: 1 },
|
|
30
|
+
{ key: "b" as const, expected: 2 },
|
|
31
|
+
])("returns source value for key $key", ({ key, expected }) => {
|
|
32
|
+
expect(get(source, defaults, key)).toBe(expected);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("falls back to defaults when source value missing", () => {
|
|
36
|
+
expect(get(source, defaults, "c")).toBe(30);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("returns undefined when neither has value", () => {
|
|
40
|
+
expect(get(source, defaults, "d" as keyof typeof source)).toBeUndefined();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("handles multiple keys, returning array", () => {
|
|
44
|
+
const result = get(source, defaults, "a", "b", "c");
|
|
45
|
+
expect(result).toEqual([1, 2, 30]);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it.each([
|
|
49
|
+
{ source: {}, expected: 10 },
|
|
50
|
+
{ source: source, expected: 1 },
|
|
51
|
+
])("handles source value for key 'a'", ({ source: testSource, expected }) => {
|
|
52
|
+
expect(get(testSource, defaults, "a")).toBe(expected);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("handles empty defaults", () => {
|
|
56
|
+
expect(get(source, {}, "a")).toBe(1);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("prefers explicit undefined in source over default", () => {
|
|
60
|
+
const sourceWithUndefined: Partial<{ a: number; b: number; c: number }> = { a: undefined };
|
|
61
|
+
expect(get(sourceWithUndefined, defaults, "a")).toBe(10);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe("createOpt", () => {
|
|
66
|
+
it.each([
|
|
67
|
+
{ value: true as const, target: "file" as const, expected: { create: true } },
|
|
68
|
+
{ value: true as const, target: "block" as const, expected: { create: true } },
|
|
69
|
+
{ value: 1 as const, target: "file" as const, expected: { create: true } },
|
|
70
|
+
{ value: 1 as const, target: "block" as const, expected: { create: true } },
|
|
71
|
+
{ value: "file" as const, target: "file" as const, expected: { create: true } },
|
|
72
|
+
{ value: "block" as const, target: "block" as const, expected: { create: true } },
|
|
73
|
+
])("returns create:true when value=$value, target=$target", ({ value, target, expected }) => {
|
|
74
|
+
expect(createOpt(value, target)).toEqual(expected);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it.each([
|
|
78
|
+
{ value: "file" as const, target: "block" as const },
|
|
79
|
+
{ value: "block" as const, target: "file" as const },
|
|
80
|
+
{ value: false as const, target: "file" as const },
|
|
81
|
+
{ value: 0 as const, target: "file" as const },
|
|
82
|
+
])("returns undefined when value=$value, target=$target", ({ value, target }) => {
|
|
83
|
+
expect(createOpt(value, target)).toBeUndefined();
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
});
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import * as fs from "node:fs/promises";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import * as os from "node:os";
|
|
5
|
+
import { execSync } from "node:child_process";
|
|
6
|
+
|
|
7
|
+
describe("Mode functionality", () => {
|
|
8
|
+
let tempDir: string;
|
|
9
|
+
|
|
10
|
+
beforeEach(async () => {
|
|
11
|
+
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "blockinfile-mode-test-"));
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
afterEach(async () => {
|
|
15
|
+
await fs.rm(tempDir, { recursive: true, force: true });
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
function runBlockInFile(args: string, input?: string): string {
|
|
19
|
+
const cwd = path.resolve(import.meta.dirname!, "..");
|
|
20
|
+
const cmd = `npx tsx block-in-file.ts ${args}`;
|
|
21
|
+
return execSync(cmd, {
|
|
22
|
+
cwd,
|
|
23
|
+
input,
|
|
24
|
+
encoding: "utf-8",
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe("mode=ensure (idempotent)", () => {
|
|
29
|
+
it("should create block when missing", async () => {
|
|
30
|
+
const targetFile = path.join(tempDir, "target.txt");
|
|
31
|
+
const input = "new block content";
|
|
32
|
+
|
|
33
|
+
const _result = runBlockInFile(`--mode ensure ${targetFile}`, input);
|
|
34
|
+
const content = await fs.readFile(targetFile, "utf-8");
|
|
35
|
+
|
|
36
|
+
expect(content).toContain(input);
|
|
37
|
+
expect(content).toContain("# blockinfile start");
|
|
38
|
+
expect(content).toContain("# blockinfile end");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("should update block when content differs", async () => {
|
|
42
|
+
const targetFile = path.join(tempDir, "target.txt");
|
|
43
|
+
const initialContent = "initial content";
|
|
44
|
+
const updatedContent = "updated content";
|
|
45
|
+
|
|
46
|
+
await fs.writeFile(targetFile, `# blockinfile start\n${initialContent}\n# blockinfile end\n`);
|
|
47
|
+
|
|
48
|
+
const _result1 = runBlockInFile(`--mode ensure ${targetFile}`, updatedContent);
|
|
49
|
+
const content1 = await fs.readFile(targetFile, "utf-8");
|
|
50
|
+
expect(content1).toContain(updatedContent);
|
|
51
|
+
expect(content1).not.toContain(initialContent);
|
|
52
|
+
|
|
53
|
+
const _result2 = runBlockInFile(`--mode ensure ${targetFile}`, updatedContent);
|
|
54
|
+
const content2 = await fs.readFile(targetFile, "utf-8");
|
|
55
|
+
|
|
56
|
+
expect(content2).toContain(updatedContent);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("should skip when content unchanged (idempotent)", async () => {
|
|
60
|
+
const targetFile = path.join(tempDir, "target.txt");
|
|
61
|
+
const input = "test content";
|
|
62
|
+
|
|
63
|
+
await fs.writeFile(targetFile, `# blockinfile start\n${input}\n# blockinfile end\n`);
|
|
64
|
+
|
|
65
|
+
const _result = runBlockInFile(`--mode ensure --debug ${targetFile}`, input);
|
|
66
|
+
const content = await fs.readFile(targetFile, "utf-8");
|
|
67
|
+
|
|
68
|
+
expect(content).toContain(input);
|
|
69
|
+
expect(_result).toContain("no changes needed");
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe("mode=only (create if missing)", () => {
|
|
74
|
+
it("should create block when missing", async () => {
|
|
75
|
+
const targetFile = path.join(tempDir, "target.txt");
|
|
76
|
+
const input = "new block content";
|
|
77
|
+
|
|
78
|
+
const _result = runBlockInFile(`--mode only ${targetFile}`, input);
|
|
79
|
+
const content = await fs.readFile(targetFile, "utf-8");
|
|
80
|
+
|
|
81
|
+
expect(content).toContain(input);
|
|
82
|
+
expect(content).toContain("# blockinfile start");
|
|
83
|
+
expect(content).toContain("# blockinfile end");
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("should skip when block already exists", async () => {
|
|
87
|
+
const targetFile = path.join(tempDir, "target.txt");
|
|
88
|
+
const initialContent = "initial content";
|
|
89
|
+
const differentContent = "different content";
|
|
90
|
+
|
|
91
|
+
await fs.writeFile(targetFile, `# blockinfile start\n${initialContent}\n# blockinfile end\n`);
|
|
92
|
+
|
|
93
|
+
const _result = runBlockInFile(`--mode only --debug ${targetFile}`, differentContent);
|
|
94
|
+
const content = await fs.readFile(targetFile, "utf-8");
|
|
95
|
+
|
|
96
|
+
expect(content).toContain(initialContent);
|
|
97
|
+
expect(content).not.toContain(differentContent);
|
|
98
|
+
expect(_result).toContain("block already exists");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("should skip even if block content differs", async () => {
|
|
102
|
+
const targetFile = path.join(tempDir, "target.txt");
|
|
103
|
+
const initialContent = "version 1";
|
|
104
|
+
const newContent = "version 2";
|
|
105
|
+
|
|
106
|
+
await fs.writeFile(targetFile, `# blockinfile start\n${initialContent}\n# blockinfile end\n`);
|
|
107
|
+
|
|
108
|
+
const _result = runBlockInFile(`--mode only --debug ${targetFile}`, newContent);
|
|
109
|
+
const content = await fs.readFile(targetFile, "utf-8");
|
|
110
|
+
|
|
111
|
+
expect(content).toContain(initialContent);
|
|
112
|
+
expect(content).not.toContain(newContent);
|
|
113
|
+
expect(_result).toContain("block already exists");
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("should create when file exists but block missing", async () => {
|
|
117
|
+
const targetFile = path.join(tempDir, "target.txt");
|
|
118
|
+
const input = "block content";
|
|
119
|
+
|
|
120
|
+
await fs.writeFile(targetFile, "some existing content\n");
|
|
121
|
+
|
|
122
|
+
runBlockInFile(`--mode only ${targetFile}`, input);
|
|
123
|
+
const content = await fs.readFile(targetFile, "utf-8");
|
|
124
|
+
|
|
125
|
+
expect(content).toContain(input);
|
|
126
|
+
expect(content).toContain("some existing content");
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
describe("mode=none (legacy behavior)", () => {
|
|
131
|
+
it("should always attempt to insert/update block", async () => {
|
|
132
|
+
const targetFile = path.join(tempDir, "target.txt");
|
|
133
|
+
const content1 = "first content";
|
|
134
|
+
const content2 = "second content";
|
|
135
|
+
|
|
136
|
+
await fs.writeFile(targetFile, `# blockinfile start\n${content1}\n# blockinfile end\n`);
|
|
137
|
+
|
|
138
|
+
runBlockInFile(`--mode none ${targetFile}`, content2);
|
|
139
|
+
const fileContent = await fs.readFile(targetFile, "utf-8");
|
|
140
|
+
|
|
141
|
+
expect(fileContent).toContain(content2);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("should be the default mode", async () => {
|
|
145
|
+
const targetFile = path.join(tempDir, "target.txt");
|
|
146
|
+
const input = "default behavior test";
|
|
147
|
+
|
|
148
|
+
runBlockInFile(`${targetFile}`, input);
|
|
149
|
+
const content = await fs.readFile(targetFile, "utf-8");
|
|
150
|
+
|
|
151
|
+
expect(content).toContain(input);
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
describe("mode with multiple files", () => {
|
|
156
|
+
it("should process all files correctly in ensure mode", async () => {
|
|
157
|
+
const file1 = path.join(tempDir, "file1.txt");
|
|
158
|
+
const file2 = path.join(tempDir, "file2.txt");
|
|
159
|
+
const file3 = path.join(tempDir, "file3.txt");
|
|
160
|
+
const input = "shared content";
|
|
161
|
+
|
|
162
|
+
await fs.writeFile(file1, `# blockinfile start\n${input}\n# blockinfile end\n`);
|
|
163
|
+
|
|
164
|
+
runBlockInFile(`--mode ensure ${file1} ${file2} ${file3}`, input);
|
|
165
|
+
|
|
166
|
+
const content1 = await fs.readFile(file1, "utf-8");
|
|
167
|
+
const content2 = await fs.readFile(file2, "utf-8");
|
|
168
|
+
const content3 = await fs.readFile(file3, "utf-8");
|
|
169
|
+
|
|
170
|
+
expect(content1).toContain(input);
|
|
171
|
+
expect(content2).toContain(input);
|
|
172
|
+
expect(content3).toContain(input);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("should skip existing blocks in only mode across multiple files", async () => {
|
|
176
|
+
const file1 = path.join(tempDir, "file1.txt");
|
|
177
|
+
const file2 = path.join(tempDir, "file2.txt");
|
|
178
|
+
const input1 = "original content";
|
|
179
|
+
const input2 = "new content";
|
|
180
|
+
|
|
181
|
+
await fs.writeFile(file1, `# blockinfile start\n${input1}\n# blockinfile end\n`);
|
|
182
|
+
|
|
183
|
+
runBlockInFile(`--debug --mode only ${file1} ${file2}`, input2);
|
|
184
|
+
|
|
185
|
+
const content1 = await fs.readFile(file1, "utf-8");
|
|
186
|
+
const content2 = await fs.readFile(file2, "utf-8");
|
|
187
|
+
|
|
188
|
+
expect(content1).toContain(input1);
|
|
189
|
+
expect(content1).not.toContain(input2);
|
|
190
|
+
expect(content2).toContain(input2);
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { formatOutputs, generateDiff } from "../src/output.ts";
|
|
3
|
+
|
|
4
|
+
describe("output utilities", () => {
|
|
5
|
+
describe("formatOutputs", () => {
|
|
6
|
+
it.each([
|
|
7
|
+
{ outputs: ["line1", "line2", "line3"], dos: false, expected: "line1\nline2\nline3\n" },
|
|
8
|
+
{ outputs: ["line1", "line2", "line3"], dos: true, expected: "line1\r\nline2\r\nline3\r\n" },
|
|
9
|
+
{ outputs: ["line1", "line2"], dos: false, expected: "line1\nline2\n" },
|
|
10
|
+
{ outputs: ["line1", "line2", ""], dos: false, expected: "line1\nline2\n" },
|
|
11
|
+
{ outputs: [], dos: false, expected: "" },
|
|
12
|
+
{ outputs: ["only line"], dos: false, expected: "only line\n" },
|
|
13
|
+
{ outputs: ["line1", "", "line3"], dos: false, expected: "line1\n\nline3\n" },
|
|
14
|
+
])("handles outputs: $outputs", ({ outputs, dos, expected }) => {
|
|
15
|
+
const result = formatOutputs(outputs, dos);
|
|
16
|
+
expect(result).toBe(expected);
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe("generateDiff", () => {
|
|
21
|
+
it("shows no changes for identical content", () => {
|
|
22
|
+
const content = "line1\nline2\n";
|
|
23
|
+
const diff = generateDiff(content, content, "test.txt");
|
|
24
|
+
expect(diff).toContain("--- test.txt");
|
|
25
|
+
expect(diff).toContain("+++ test.txt");
|
|
26
|
+
expect(diff).not.toContain("-line");
|
|
27
|
+
expect(diff).not.toContain("+line");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it.each([
|
|
31
|
+
{ original: "line1\nline2\n", modified: "line1\nNEW\nline2\n", expected: "+NEW" },
|
|
32
|
+
{ original: "line1\nOLD\nline2\n", modified: "line1\nline2\n", expected: "-OLD" },
|
|
33
|
+
{ original: "line1\nOLD\nline2\n", modified: "line1\nNEW\nline2\n", expectedContains: ["-OLD", "+NEW"] },
|
|
34
|
+
])("handles diff: $modified", ({ original, modified, expected, expectedContains }) => {
|
|
35
|
+
const diff = generateDiff(original, modified, "test.txt");
|
|
36
|
+
if (expected) {
|
|
37
|
+
expect(diff).toContain(expected);
|
|
38
|
+
}
|
|
39
|
+
if (expectedContains) {
|
|
40
|
+
expectedContains.forEach(str => expect(diff).toContain(str));
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
});
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { mergeTags, replaceTags, applyTagMode } from "../src/tags/tag-merger.ts";
|
|
3
|
+
|
|
4
|
+
describe("mergeTags", () => {
|
|
5
|
+
it("should merge new tags into existing tags", () => {
|
|
6
|
+
const existingTags = [{ name: "tag1", value: "value1" }];
|
|
7
|
+
const newTags = [{ name: "tag2", value: "value2" }];
|
|
8
|
+
const result = mergeTags(existingTags, newTags);
|
|
9
|
+
expect(result).toEqual([
|
|
10
|
+
{ name: "tag1", value: "value1" },
|
|
11
|
+
{ name: "tag2", value: "value2" },
|
|
12
|
+
]);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("should update existing tags with new values", () => {
|
|
16
|
+
const existingTags = [{ name: "tag1", value: "value1" }];
|
|
17
|
+
const newTags = [{ name: "tag1", value: "newvalue" }];
|
|
18
|
+
const result = mergeTags(existingTags, newTags);
|
|
19
|
+
expect(result).toEqual([{ name: "tag1", value: "newvalue" }]);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("should preserve tags not being updated", () => {
|
|
23
|
+
const existingTags = [
|
|
24
|
+
{ name: "tag1", value: "value1" },
|
|
25
|
+
{ name: "tag2", value: "value2" },
|
|
26
|
+
];
|
|
27
|
+
const newTags = [{ name: "tag1", value: "newvalue" }];
|
|
28
|
+
const result = mergeTags(existingTags, newTags);
|
|
29
|
+
expect(result).toEqual([
|
|
30
|
+
{ name: "tag1", value: "newvalue" },
|
|
31
|
+
{ name: "tag2", value: "value2" },
|
|
32
|
+
]);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("should handle empty existing tags", () => {
|
|
36
|
+
const existingTags: any[] = [];
|
|
37
|
+
const newTags = [{ name: "tag1", value: "value1" }];
|
|
38
|
+
const result = mergeTags(existingTags, newTags);
|
|
39
|
+
expect(result).toEqual([{ name: "tag1", value: "value1" }]);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("should handle empty new tags", () => {
|
|
43
|
+
const existingTags = [{ name: "tag1", value: "value1" }];
|
|
44
|
+
const newTags: any[] = [];
|
|
45
|
+
const result = mergeTags(existingTags, newTags);
|
|
46
|
+
expect(result).toEqual([{ name: "tag1", value: "value1" }]);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("should merge timestamp tags", () => {
|
|
50
|
+
const existingTags = [{ name: "timestamp", value: "1234567890" }];
|
|
51
|
+
const newTags = [{ name: "timestamp", value: "9876543210" }];
|
|
52
|
+
const result = mergeTags(existingTags, newTags);
|
|
53
|
+
expect(result).toEqual([{ name: "timestamp", value: "9876543210" }]);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe("replaceTags", () => {
|
|
58
|
+
it("should replace all existing tags with new tags", () => {
|
|
59
|
+
const existingTags = [
|
|
60
|
+
{ name: "tag1", value: "value1" },
|
|
61
|
+
{ name: "tag2", value: "value2" },
|
|
62
|
+
];
|
|
63
|
+
const newTags = [{ name: "tag3", value: "value3" }];
|
|
64
|
+
const result = replaceTags(existingTags, newTags);
|
|
65
|
+
expect(result).toEqual([{ name: "tag3", value: "value3" }]);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("should return new tags when existing tags are empty", () => {
|
|
69
|
+
const existingTags: any[] = [];
|
|
70
|
+
const newTags = [{ name: "tag1", value: "value1" }];
|
|
71
|
+
const result = replaceTags(existingTags, newTags);
|
|
72
|
+
expect(result).toEqual([{ name: "tag1", value: "value1" }]);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("should return empty array when both are empty", () => {
|
|
76
|
+
const existingTags: any[] = [];
|
|
77
|
+
const newTags: any[] = [];
|
|
78
|
+
const result = replaceTags(existingTags, newTags);
|
|
79
|
+
expect(result).toEqual([]);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("should clear all tags when new tags is empty", () => {
|
|
83
|
+
const existingTags = [
|
|
84
|
+
{ name: "tag1", value: "value1" },
|
|
85
|
+
{ name: "tag2", value: "value2" },
|
|
86
|
+
];
|
|
87
|
+
const newTags: any[] = [];
|
|
88
|
+
const result = replaceTags(existingTags, newTags);
|
|
89
|
+
expect(result).toEqual([]);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe("applyTagMode", () => {
|
|
94
|
+
it("should apply merge mode by default", () => {
|
|
95
|
+
const existingTags = [{ name: "tag1", value: "value1" }];
|
|
96
|
+
const newTags = [{ name: "tag2", value: "value2" }];
|
|
97
|
+
const result = applyTagMode(existingTags, newTags, "merge");
|
|
98
|
+
expect(result).toEqual([
|
|
99
|
+
{ name: "tag1", value: "value1" },
|
|
100
|
+
{ name: "tag2", value: "value2" },
|
|
101
|
+
]);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("should apply merge mode and preserve existing tags", () => {
|
|
105
|
+
const existingTags = [
|
|
106
|
+
{ name: "tag1", value: "value1" },
|
|
107
|
+
{ name: "tag2", value: "value2" },
|
|
108
|
+
{ name: "anchor-bof", value: "100" },
|
|
109
|
+
];
|
|
110
|
+
const newTags = [{ name: "timestamp", value: "1234567890" }];
|
|
111
|
+
const result = applyTagMode(existingTags, newTags, "merge");
|
|
112
|
+
expect(result).toEqual([
|
|
113
|
+
{ name: "tag1", value: "value1" },
|
|
114
|
+
{ name: "tag2", value: "value2" },
|
|
115
|
+
{ name: "anchor-bof", value: "100" },
|
|
116
|
+
{ name: "timestamp", value: "1234567890" },
|
|
117
|
+
]);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("should apply merge mode and update existing tags", () => {
|
|
121
|
+
const existingTags = [
|
|
122
|
+
{ name: "tag1", value: "value1" },
|
|
123
|
+
{ name: "timestamp", value: "1234567890" },
|
|
124
|
+
];
|
|
125
|
+
const newTags = [{ name: "timestamp", value: "9876543210" }];
|
|
126
|
+
const result = applyTagMode(existingTags, newTags, "merge");
|
|
127
|
+
expect(result).toEqual([
|
|
128
|
+
{ name: "tag1", value: "value1" },
|
|
129
|
+
{ name: "timestamp", value: "9876543210" },
|
|
130
|
+
]);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("should apply replace mode", () => {
|
|
134
|
+
const existingTags = [
|
|
135
|
+
{ name: "tag1", value: "value1" },
|
|
136
|
+
{ name: "tag2", value: "value2" },
|
|
137
|
+
];
|
|
138
|
+
const newTags = [{ name: "tag3", value: "value3" }];
|
|
139
|
+
const result = applyTagMode(existingTags, newTags, "replace");
|
|
140
|
+
expect(result).toEqual([{ name: "tag3", value: "value3" }]);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("should apply replace mode and clear all existing tags", () => {
|
|
144
|
+
const existingTags = [
|
|
145
|
+
{ name: "tag1", value: "value1" },
|
|
146
|
+
{ name: "timestamp", value: "1234567890" },
|
|
147
|
+
{ name: "anchor-bof", value: "100" },
|
|
148
|
+
];
|
|
149
|
+
const newTags = [{ name: "timestamp", value: "9876543210" }];
|
|
150
|
+
const result = applyTagMode(existingTags, newTags, "replace");
|
|
151
|
+
expect(result).toEqual([{ name: "timestamp", value: "9876543210" }]);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("should handle empty new tags with merge mode", () => {
|
|
155
|
+
const existingTags = [
|
|
156
|
+
{ name: "tag1", value: "value1" },
|
|
157
|
+
{ name: "tag2", value: "value2" },
|
|
158
|
+
];
|
|
159
|
+
const newTags: any[] = [];
|
|
160
|
+
const result = applyTagMode(existingTags, newTags, "merge");
|
|
161
|
+
expect(result).toEqual([
|
|
162
|
+
{ name: "tag1", value: "value1" },
|
|
163
|
+
{ name: "tag2", value: "value2" },
|
|
164
|
+
]);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("should handle empty new tags with replace mode", () => {
|
|
168
|
+
const existingTags = [
|
|
169
|
+
{ name: "tag1", value: "value1" },
|
|
170
|
+
{ name: "tag2", value: "value2" },
|
|
171
|
+
];
|
|
172
|
+
const newTags: any[] = [];
|
|
173
|
+
const result = applyTagMode(existingTags, newTags, "replace");
|
|
174
|
+
expect(result).toEqual([]);
|
|
175
|
+
});
|
|
176
|
+
});
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
parseTags,
|
|
4
|
+
generateTag,
|
|
5
|
+
removeTags,
|
|
6
|
+
addTags,
|
|
7
|
+
stripTagsForMatching,
|
|
8
|
+
} from "../src/tags/tag.ts";
|
|
9
|
+
|
|
10
|
+
describe("tag parsing", () => {
|
|
11
|
+
it.each([
|
|
12
|
+
{ line: "# block start [mytag]", expected: [{ name: "mytag", value: "" }] },
|
|
13
|
+
{ line: "# block start [mytag:value]", expected: [{ name: "mytag", value: "value" }] },
|
|
14
|
+
{ line: "# block start [timestamp:1770765014846000000]", expected: [{ name: "timestamp", value: "1770765014846000000" }] },
|
|
15
|
+
{ line: "# block start [tag-name_1:value_test]", expected: [{ name: "tag-name_1", value: "value_test" }] },
|
|
16
|
+
{ line: "# block-in-file start [tag:value]", expected: [{ name: "tag", value: "value" }] },
|
|
17
|
+
])("should parse tags from $line", ({ line, expected }) => {
|
|
18
|
+
const tags = parseTags(line);
|
|
19
|
+
expect(tags).toEqual(expected);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("should parse multiple tags", () => {
|
|
23
|
+
const line = "# block start [tag1:value1] [tag2:value2]";
|
|
24
|
+
const tags = parseTags(line);
|
|
25
|
+
expect(tags).toEqual([
|
|
26
|
+
{ name: "tag1", value: "value1" },
|
|
27
|
+
{ name: "tag2", value: "value2" },
|
|
28
|
+
]);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("should parse tags with complex values", () => {
|
|
32
|
+
const line = "# block start [anchor-bof:100] [timestamp:1234567890]";
|
|
33
|
+
const tags = parseTags(line);
|
|
34
|
+
expect(tags).toEqual([
|
|
35
|
+
{ name: "anchor-bof", value: "100" },
|
|
36
|
+
{ name: "timestamp", value: "1234567890" },
|
|
37
|
+
]);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("should return empty array when no tags found", () => {
|
|
41
|
+
const line = "# block start";
|
|
42
|
+
const tags = parseTags(line);
|
|
43
|
+
expect(tags).toEqual([]);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe("tag generation", () => {
|
|
48
|
+
it.each([
|
|
49
|
+
{ name: "mytag", value: "", expected: "[mytag]" },
|
|
50
|
+
{ name: "mytag", value: "value", expected: "[mytag:value]" },
|
|
51
|
+
{ name: "timestamp", value: "1234567890", expected: "[timestamp:1234567890]" },
|
|
52
|
+
])("should generate tag: $expected", ({ name, value, expected }) => {
|
|
53
|
+
const tag = generateTag(name, value);
|
|
54
|
+
expect(tag).toBe(expected);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("should generate multiple tags by concatenation", () => {
|
|
58
|
+
const tag1 = generateTag("tag1", "value1");
|
|
59
|
+
const tag2 = generateTag("tag2", "value2");
|
|
60
|
+
const combined = `${tag1} ${tag2}`;
|
|
61
|
+
expect(combined).toBe("[tag1:value1] [tag2:value2]");
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe("tag removal", () => {
|
|
66
|
+
it.each([
|
|
67
|
+
{ line: "# block start [mytag:value]", expected: "# block start" },
|
|
68
|
+
{ line: "# block start [tag1:value1] [tag2:value2]", expected: "# block start" },
|
|
69
|
+
{ line: "# block start", expected: "# block start" },
|
|
70
|
+
{ line: "# block start [mytag]", expected: "# block start" },
|
|
71
|
+
{ line: "# block start [tag:value] ", expected: "# block start" },
|
|
72
|
+
])("should remove tags from: $line", ({ line, expected }) => {
|
|
73
|
+
const result = removeTags(line);
|
|
74
|
+
expect(result).toBe(expected);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe("tag adding", () => {
|
|
79
|
+
it.each([
|
|
80
|
+
{ line: "# block start", tags: [{ name: "mytag", value: "value" }], expected: "# block start [mytag:value]" },
|
|
81
|
+
{ line: "# block start", tags: [{ name: "mytag", value: "" }], expected: "# block start [mytag]" },
|
|
82
|
+
])("should add tags to: $line", ({ line, tags, expected }) => {
|
|
83
|
+
const result = addTags(line, tags);
|
|
84
|
+
expect(result).toBe(expected);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("should add multiple tags to a line", () => {
|
|
88
|
+
const line = "# block start";
|
|
89
|
+
const tags = [
|
|
90
|
+
{ name: "tag1", value: "value1" },
|
|
91
|
+
{ name: "tag2", value: "value2" },
|
|
92
|
+
];
|
|
93
|
+
const result = addTags(line, tags);
|
|
94
|
+
expect(result).toBe("# block start [tag1:value1] [tag2:value2]");
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("should not add tags if array is empty", () => {
|
|
98
|
+
const line = "# block start";
|
|
99
|
+
const tags: any[] = [];
|
|
100
|
+
const result = addTags(line, tags);
|
|
101
|
+
expect(result).toBe("# block start");
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe("tag stripping for matching", () => {
|
|
106
|
+
it.each([
|
|
107
|
+
{ line: "# block start [tag:value]", expected: "# block start" },
|
|
108
|
+
{ line: "# block start [tag1:value1] [tag2:value2]", expected: "# block start" },
|
|
109
|
+
{ line: "# block start", expected: "# block start" },
|
|
110
|
+
{ line: "# block-in-file start [timestamp:1234567890]", expected: "# block-in-file start" },
|
|
111
|
+
{ line: " # block start [tag:value] ", expected: "# block start" },
|
|
112
|
+
])("should strip tags from: $line", ({ line, expected }) => {
|
|
113
|
+
const result = stripTagsForMatching(line);
|
|
114
|
+
expect(result).toBe(expected);
|
|
115
|
+
});
|
|
116
|
+
});
|