@crewhaus/branch-history 0.1.0 → 0.1.2
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 +7 -12
- package/src/index.test.ts +46 -0
- package/src/index.ts +6 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crewhaus/branch-history",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Branch + diff helpers over checkpoint-store — time-travel utilities for the GRPH target",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -12,14 +12,14 @@
|
|
|
12
12
|
"test": "bun test src"
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"@crewhaus/checkpoint-store": "0.
|
|
16
|
-
"@crewhaus/errors": "0.
|
|
15
|
+
"@crewhaus/checkpoint-store": "0.1.2",
|
|
16
|
+
"@crewhaus/errors": "0.1.2"
|
|
17
17
|
},
|
|
18
18
|
"license": "Apache-2.0",
|
|
19
19
|
"author": {
|
|
20
20
|
"name": "Max Meier",
|
|
21
|
-
"email": "max@
|
|
22
|
-
"url": "https://
|
|
21
|
+
"email": "max@crewhaus.ai",
|
|
22
|
+
"url": "https://crewhaus.ai"
|
|
23
23
|
},
|
|
24
24
|
"repository": {
|
|
25
25
|
"type": "git",
|
|
@@ -31,12 +31,7 @@
|
|
|
31
31
|
"url": "https://github.com/crewhaus/factory/issues"
|
|
32
32
|
},
|
|
33
33
|
"publishConfig": {
|
|
34
|
-
"access": "
|
|
34
|
+
"access": "public"
|
|
35
35
|
},
|
|
36
|
-
"files": [
|
|
37
|
-
"src",
|
|
38
|
-
"README.md",
|
|
39
|
-
"LICENSE",
|
|
40
|
-
"NOTICE"
|
|
41
|
-
]
|
|
36
|
+
"files": ["src", "README.md", "LICENSE", "NOTICE"]
|
|
42
37
|
}
|
package/src/index.test.ts
CHANGED
|
@@ -76,4 +76,50 @@ describe("diff", () => {
|
|
|
76
76
|
expect(d[0]?.kind).toBe("same");
|
|
77
77
|
expect(d[1]?.kind).toBe("only-a");
|
|
78
78
|
});
|
|
79
|
+
|
|
80
|
+
test("only-b reported when run B is longer than run A", async () => {
|
|
81
|
+
const a = newGraphRunId();
|
|
82
|
+
const b = newGraphRunId();
|
|
83
|
+
await store.save({ graphRunId: a, nodeName: "x", state: 0 });
|
|
84
|
+
await store.save({ graphRunId: b, nodeName: "x", state: 0 });
|
|
85
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
86
|
+
const extra = await store.save({ graphRunId: b, nodeName: "y", state: 1 });
|
|
87
|
+
const d = await diff(store, a, b);
|
|
88
|
+
expect(d.length).toBe(2);
|
|
89
|
+
expect(d[0]?.kind).toBe("same");
|
|
90
|
+
expect(d[1]?.kind).toBe("only-b");
|
|
91
|
+
// The only-b row carries B's checkpoint metadata and no `a` side.
|
|
92
|
+
expect(d[1]?.a).toBeUndefined();
|
|
93
|
+
expect(d[1]?.b?.checkpointId).toBe(extra.id);
|
|
94
|
+
expect(d[1]?.b?.nodeName).toBe("y");
|
|
95
|
+
expect(typeof d[1]?.b?.stateHash).toBe("string");
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe("stateHash (via diff)", () => {
|
|
100
|
+
// Regression: a checkpoint whose `state` serializes to `undefined`
|
|
101
|
+
// (e.g. `state: undefined`) once crashed `diff` with a TypeError from
|
|
102
|
+
// `createHash().update(undefined)`. It must now hash to a stable sentinel.
|
|
103
|
+
test("checkpoints with undefined state diff without throwing", async () => {
|
|
104
|
+
const a = newGraphRunId();
|
|
105
|
+
const b = newGraphRunId();
|
|
106
|
+
await store.save({ graphRunId: a, nodeName: "x", state: undefined });
|
|
107
|
+
await store.save({ graphRunId: b, nodeName: "x", state: undefined });
|
|
108
|
+
const d = await diff(store, a, b);
|
|
109
|
+
expect(d.length).toBe(1);
|
|
110
|
+
// Two undefined states hash identically, so the row is `same`.
|
|
111
|
+
expect(d[0]?.kind).toBe("same");
|
|
112
|
+
expect(d[0]?.a?.stateHash).toBe(d[0]?.b?.stateHash);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("undefined state differs from a defined state", async () => {
|
|
116
|
+
const a = newGraphRunId();
|
|
117
|
+
const b = newGraphRunId();
|
|
118
|
+
await store.save({ graphRunId: a, nodeName: "x", state: undefined });
|
|
119
|
+
await store.save({ graphRunId: b, nodeName: "x", state: { v: 1 } });
|
|
120
|
+
const d = await diff(store, a, b);
|
|
121
|
+
expect(d.length).toBe(1);
|
|
122
|
+
expect(d[0]?.kind).toBe("state-mismatch");
|
|
123
|
+
expect(d[0]?.a?.stateHash).not.toBe(d[0]?.b?.stateHash);
|
|
124
|
+
});
|
|
79
125
|
});
|
package/src/index.ts
CHANGED
|
@@ -45,7 +45,12 @@ export type BranchAtResult = {
|
|
|
45
45
|
};
|
|
46
46
|
|
|
47
47
|
function stateHash(c: Checkpoint): string {
|
|
48
|
-
|
|
48
|
+
// `JSON.stringify` returns `undefined` for `state === undefined` (and for
|
|
49
|
+
// values that serialize to nothing, e.g. functions/symbols). Feeding that
|
|
50
|
+
// straight into `createHash().update()` throws a TypeError, so coalesce to a
|
|
51
|
+
// stable sentinel — every other state serializes unchanged, preserving hashes.
|
|
52
|
+
const serialized = JSON.stringify(c.state) ?? "undefined";
|
|
53
|
+
return createHash("sha256").update(serialized).digest("hex").slice(0, 16);
|
|
49
54
|
}
|
|
50
55
|
|
|
51
56
|
export async function branchAt(
|