@linkedclaw/cli 0.1.3 → 0.1.5

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.
@@ -0,0 +1,141 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { reduceRunState } from "../src/commands/converge.js";
3
+ import type { CommonsLogEvent, RunWorkspace } from "../src/converge/types.js";
4
+
5
+ const FAKE_WS: RunWorkspace = {
6
+ runId: "clg_run00000000",
7
+ sourceDebateId: "dbt_source001",
8
+ paAgentId: "agt_pa001",
9
+ targetCorpus: "/corpus",
10
+ stagingDir: "/corpus/converged/staging/clg_run00000000",
11
+ };
12
+
13
+ const SAMPLE_EVENTS: CommonsLogEvent[] = [
14
+ {
15
+ seq: 1,
16
+ event_type: "run_started",
17
+ payload: {},
18
+ appended_at: "2026-04-30T10:00:00Z",
19
+ signed_by: "sys",
20
+ },
21
+ {
22
+ seq: 2,
23
+ event_type: "sub_debate_dispatched",
24
+ payload: { crux_id: "crux_001", sub_debate_id: "dbt_sub001" },
25
+ appended_at: "2026-04-30T10:01:00Z",
26
+ signed_by: "sys",
27
+ },
28
+ {
29
+ seq: 3,
30
+ event_type: "sub_debate_dispatched",
31
+ payload: { crux_id: "crux_002", sub_debate_id: "dbt_sub002" },
32
+ appended_at: "2026-04-30T10:02:00Z",
33
+ signed_by: "sys",
34
+ },
35
+ {
36
+ seq: 4,
37
+ event_type: "sub_debate_outcome_observed",
38
+ payload: { crux_id: "crux_001", outcome: "already_aligned", bilateral_mandate_intact: true },
39
+ appended_at: "2026-04-30T10:05:00Z",
40
+ signed_by: "sys",
41
+ },
42
+ ];
43
+
44
+ describe("reduceRunState", () => {
45
+ it("extracts started_at from run_started event", () => {
46
+ const summary = reduceRunState(FAKE_WS, SAMPLE_EVENTS);
47
+ expect(summary.started_at).toBe("2026-04-30T10:00:00Z");
48
+ });
49
+
50
+ it("owner_b_accepted is false when no event emitted", () => {
51
+ const summary = reduceRunState(FAKE_WS, SAMPLE_EVENTS);
52
+ expect(summary.owner_b_accepted).toBe(false);
53
+ });
54
+
55
+ it("owner_b_accepted flips to true when event present", () => {
56
+ const events: CommonsLogEvent[] = [
57
+ ...SAMPLE_EVENTS,
58
+ {
59
+ seq: 5,
60
+ event_type: "owner_b_accepted",
61
+ payload: {},
62
+ appended_at: "2026-04-30T10:06:00Z",
63
+ signed_by: "owner_b",
64
+ },
65
+ ];
66
+ const summary = reduceRunState(FAKE_WS, events);
67
+ expect(summary.owner_b_accepted).toBe(true);
68
+ });
69
+
70
+ it("builds two crux entries from dispatched events", () => {
71
+ const summary = reduceRunState(FAKE_WS, SAMPLE_EVENTS);
72
+ expect(summary.cruxes).toHaveLength(2);
73
+ });
74
+
75
+ it("crux_001 has outcome=already_aligned and bilateral_mandate_intact=true", () => {
76
+ const summary = reduceRunState(FAKE_WS, SAMPLE_EVENTS);
77
+ const c1 = summary.cruxes.find((c) => c.crux_id === "crux_001");
78
+ expect(c1).toBeDefined();
79
+ expect(c1!.outcome).toBe("already_aligned");
80
+ expect(c1!.bilateral_mandate_intact).toBe(true);
81
+ expect(c1!.latest_sub_debate_id).toBe("dbt_sub001");
82
+ });
83
+
84
+ it("crux_002 has null outcome (in-progress)", () => {
85
+ const summary = reduceRunState(FAKE_WS, SAMPLE_EVENTS);
86
+ const c2 = summary.cruxes.find((c) => c.crux_id === "crux_002");
87
+ expect(c2).toBeDefined();
88
+ expect(c2!.outcome).toBeNull();
89
+ expect(c2!.bilateral_mandate_intact).toBeNull();
90
+ });
91
+
92
+ it("terminal_emitted is false without terminal event", () => {
93
+ const summary = reduceRunState(FAKE_WS, SAMPLE_EVENTS);
94
+ expect(summary.terminal_emitted).toBe(false);
95
+ });
96
+
97
+ it("terminal_emitted is true on convergence_map event", () => {
98
+ const events: CommonsLogEvent[] = [
99
+ ...SAMPLE_EVENTS,
100
+ {
101
+ seq: 6,
102
+ event_type: "convergence_map",
103
+ payload: {},
104
+ appended_at: "2026-04-30T11:00:00Z",
105
+ signed_by: "sys",
106
+ },
107
+ ];
108
+ const summary = reduceRunState(FAKE_WS, events);
109
+ expect(summary.terminal_emitted).toBe(true);
110
+ });
111
+
112
+ it("re-dispatch appends to sub_debate_chain and updates latest_sub_debate_id", () => {
113
+ const events: CommonsLogEvent[] = [
114
+ ...SAMPLE_EVENTS,
115
+ {
116
+ seq: 5,
117
+ event_type: "sub_debate_dispatched",
118
+ payload: { crux_id: "crux_001", sub_debate_id: "dbt_sub001b" },
119
+ appended_at: "2026-04-30T10:07:00Z",
120
+ signed_by: "sys",
121
+ },
122
+ ];
123
+ const summary = reduceRunState(FAKE_WS, events);
124
+ const c1 = summary.cruxes.find((c) => c.crux_id === "crux_001")!;
125
+ expect(c1.latest_sub_debate_id).toBe("dbt_sub001b");
126
+ expect(c1.sub_debate_chain).toEqual(["dbt_sub001", "dbt_sub001b"]);
127
+ });
128
+
129
+ it("empty event list yields empty cruxes and null started_at", () => {
130
+ const summary = reduceRunState(FAKE_WS, []);
131
+ expect(summary.cruxes).toHaveLength(0);
132
+ expect(summary.started_at).toBeNull();
133
+ expect(summary.terminal_emitted).toBe(false);
134
+ });
135
+
136
+ it("run_id and source_debate_id come from workspace", () => {
137
+ const summary = reduceRunState(FAKE_WS, SAMPLE_EVENTS);
138
+ expect(summary.run_id).toBe(FAKE_WS.runId);
139
+ expect(summary.source_debate_id).toBe(FAKE_WS.sourceDebateId);
140
+ });
141
+ });
@@ -0,0 +1,92 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
+ import { mkdtempSync, rmSync, mkdirSync } from "node:fs";
3
+ import { join, resolve } from "node:path";
4
+ import { tmpdir } from "node:os";
5
+ import { resolveWorkspace, writeRunMeta, readRunMeta } from "../src/converge/workspace.js";
6
+ import type { RunMeta } from "../src/converge/types.js";
7
+
8
+ const SAMPLE_META: RunMeta = {
9
+ run_id: "clg_aabbccdd11223344",
10
+ source_debate_id: "dbt_001",
11
+ pa_agent_id: "agt_pa001",
12
+ target_corpus: "/corpus/root",
13
+ owner_role: "a",
14
+ };
15
+
16
+ let tmp: string;
17
+
18
+ beforeEach(() => {
19
+ tmp = mkdtempSync(join(tmpdir(), "lc-ws-test-"));
20
+ });
21
+
22
+ afterEach(() => {
23
+ rmSync(tmp, { recursive: true, force: true });
24
+ });
25
+
26
+ describe("resolveWorkspace", () => {
27
+ it("returns workspace from --staging-dir with .run-meta.yaml", async () => {
28
+ writeRunMeta(tmp, SAMPLE_META);
29
+ const ws = await resolveWorkspace({ stagingDir: tmp });
30
+ expect(ws.runId).toBe(SAMPLE_META.run_id);
31
+ expect(ws.sourceDebateId).toBe(SAMPLE_META.source_debate_id);
32
+ expect(ws.stagingDir).toBe(resolve(tmp));
33
+ });
34
+
35
+ it("searches upward from cwd and finds meta", async () => {
36
+ writeRunMeta(tmp, SAMPLE_META);
37
+ const subdir = join(tmp, "sub", "dir");
38
+ mkdirSync(subdir, { recursive: true });
39
+ const ws = await resolveWorkspace({ cwd: subdir });
40
+ expect(ws.runId).toBe(SAMPLE_META.run_id);
41
+ expect(ws.stagingDir).toBe(tmp);
42
+ });
43
+
44
+ it("throws run_id_mismatch when --run-id conflicts with meta", async () => {
45
+ writeRunMeta(tmp, SAMPLE_META);
46
+ await expect(
47
+ resolveWorkspace({ stagingDir: tmp, runId: "clg_different00000000" }),
48
+ ).rejects.toMatchObject({ code: "run_id_mismatch" });
49
+ });
50
+
51
+ it("succeeds when --run-id matches meta", async () => {
52
+ writeRunMeta(tmp, SAMPLE_META);
53
+ const ws = await resolveWorkspace({ stagingDir: tmp, runId: SAMPLE_META.run_id });
54
+ expect(ws.runId).toBe(SAMPLE_META.run_id);
55
+ });
56
+
57
+ it("throws meta_not_found when staging-dir has no meta", async () => {
58
+ await expect(resolveWorkspace({ stagingDir: tmp })).rejects.toMatchObject({
59
+ code: "meta_not_found",
60
+ });
61
+ });
62
+
63
+ it("throws meta_not_found with guidance when --run-id given but no meta found", async () => {
64
+ const empty = mkdtempSync(join(tmpdir(), "lc-empty-"));
65
+ try {
66
+ await expect(
67
+ resolveWorkspace({ runId: "clg_xyz", cwd: empty }),
68
+ ).rejects.toMatchObject({ code: "meta_not_found" });
69
+ } finally {
70
+ rmSync(empty, { recursive: true, force: true });
71
+ }
72
+ });
73
+
74
+ it("returns absolute paths even when stagingDir is relative concept (resolved from cwd)", async () => {
75
+ writeRunMeta(tmp, SAMPLE_META);
76
+ const ws = await resolveWorkspace({ stagingDir: tmp });
77
+ expect(ws.stagingDir).toMatch(/^\//);
78
+ });
79
+ });
80
+
81
+ describe("readRunMeta / writeRunMeta roundtrip", () => {
82
+ it("roundtrips all fields correctly", () => {
83
+ writeRunMeta(tmp, SAMPLE_META);
84
+ const loaded = readRunMeta(tmp);
85
+ expect(loaded).toEqual(SAMPLE_META);
86
+ });
87
+
88
+ it("returns null when file does not exist", () => {
89
+ const result = readRunMeta(join(tmp, "nonexistent"));
90
+ expect(result).toBeNull();
91
+ });
92
+ });