Repository navigation
feat(tools): guarded write CAS core with per-path FIFO chain (S4a, #1375) #1405
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
easonLiangWorldedtech
wants to merge
40
commits into
Zoo-Code-Org:main
Choose a base branch
from
easonLiangWorldedtech:feat/guarded-write-s4a
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+3,990
−155
Open
Changes from all commits
Commits
Show all changes
40 commits
Select commit
Hold shift + click to select a range
a37dd24
feat(file-safety): atomic text publish primitive + safeWriteJson refa…
easonliang28 3dd8700
feat(file-safety): add version token for the guarded-write path (A1, …
easonliang28 ba1332e
docs(file-safety): correct ino precision bounds in version token (A1,…
easonliang28 6813013
fix(file-safety): derive the version token from exact BigInt stats (A…
easonliang28 588b95f
feat(task): per-task file observation registry (A2, #1375)
easonliang28 7a25fc0
feat(tools): guarded write CAS core with per-path FIFO chain (S4a, #1…
easonliang28 56ce4bf
fix(fws): use doUnmock + resetModules in safeWriteJson test cleanup
easonliang28 be894d9
chore(ci): empty commit — re-trigger CI and the CodeRabbit current-he…
easonliang28 58bb5ce
fix(tools): address CodeRabbit review feedback (PR 1405)
easonliang28 bb93d62
merge(upstream): take main into feat/guarded-write-s4a
4d4d511
fix(file-safety): run verifyBeforeCommit before the backup rename, an…
1ce0421
test(file-safety): cover a staging-file fsync failure
968078d
chore: trigger a fresh review pass at this head
a2c364d
docs(task): state the cross-process limit of the observation check
9f8a857
fix(utils): refuse to publish a settings export through a symlink
cb72ac7
test(config): expect the export write to refuse symlink targets
8cb5920
fix(file-safety): keep the target mode, clean the staging dir, correc…
2810c8f
fix(services): honour the creation mask for new targets and keep the …
006389d
fix(services): stop safeWriteText from resolving an already-guarded p…
2dd792c
fix(file-safety): publish credential writes without following a plant…
51f0cea
test(file-safety): pin that a partial DACL dump is unlinked after a f…
8e5061e
fix(task): clear the per-task observation registry on disposal
bacb9d3
fix(mcp): refuse a symlinked target for project-scoped settings writes
fc94f62
fix(mcp,task): derive the symlink policy from the selected source; ma…
171a26e
fix(utils): refuse a symlinked ancestor for credential-bearing safeWr…
17c736e
fix(utils): keep safeWriteJson publishing no-follow by default, and s…
f1b2ae4
test(tools): cover the omitted kind argument in guardedWrite, and say…
17c53e0
fix(tools): stop a queued guarded write once its task has been aborte…
509208e
test(file-safety): cover parent-directory creation and its failure paths
df0ce3a
Merge org main into the guarded-write CAS core
55d69c1
style: format the MCP hub spec the way the repository formatter wants it
a37f284
test(mcp,task): make two test names say what their assertions check, …
5e66c2f
test(read-file): check that both stats around a read ask for the bigi…
ca77e38
test(guarded-write,safe-write): stop names and describe blocks claimi…
a54e21a
style: format the safe-write JSON module the way the repository forma…
1d205cd
docs(safe-write-json): say the lock and publication decision once, an…
777df3c
docs(safe-write-json): state the default symlink contract as it is im…
3ee59e5
fix(tools): re-observe the published version and capture observations…
e56d189
style: format the four remaining files this PR touches
ea730f0
test(guarded-write): drop orphaned comment fragment in the FIFO chain…
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,90 @@ | ||
| import { describe, it, expect, vi } from "vitest" | ||
|
|
||
| import { ObservationRegistry } from "../observationRegistry" | ||
|
|
||
| describe("ObservationRegistry", () => { | ||
| it("observe → get returns the recorded version and observedAt", () => { | ||
| const reg = new ObservationRegistry() | ||
| reg.observe("/a/b/c.ts", "1:2:300:4000000000:5000000000") | ||
|
|
||
| const obs = reg.get("/a/b/c.ts") | ||
| expect(obs).toBeDefined() | ||
| expect(obs!.version).toBe("1:2:300:4000000000:5000000000") | ||
| expect(typeof obs!.observedAt).toBe("number") | ||
| }) | ||
|
|
||
| it("re-observe replaces the entry with a fresh observedAt", () => { | ||
| vi.useFakeTimers() | ||
| const reg = new ObservationRegistry() | ||
| reg.observe("/a/b/c.ts", "v1") | ||
| const first = reg.get("/a/b/c.ts")! | ||
| expect(first.version).toBe("v1") | ||
|
|
||
| vi.advanceTimersByTime(50) | ||
| reg.observe("/a/b/c.ts", "v2") | ||
| const second = reg.get("/a/b/c.ts")! | ||
| expect(second.version).toBe("v2") | ||
| expect(second.observedAt).toBeGreaterThan(first.observedAt) | ||
|
|
||
| vi.useRealTimers() | ||
| }) | ||
|
|
||
| it("has returns true for observed paths, false otherwise", () => { | ||
| const reg = new ObservationRegistry() | ||
| reg.observe("/x.ts", "t1") | ||
| expect(reg.has("/x.ts")).toBe(true) | ||
| expect(reg.has("/y.ts")).toBe(false) | ||
| }) | ||
|
|
||
| it("size reflects the number of observed entries", () => { | ||
| const reg = new ObservationRegistry() | ||
| expect(reg.size).toBe(0) | ||
| reg.observe("/a.ts", "t1") | ||
| reg.observe("/b.ts", "t2") | ||
| expect(reg.size).toBe(2) | ||
| }) | ||
|
|
||
| it("clear removes all entries and resets size to 0", () => { | ||
| const reg = new ObservationRegistry() | ||
| reg.observe("/a.ts", "t1") | ||
| reg.observe("/b.ts", "t2") | ||
| reg.clear() | ||
| expect(reg.size).toBe(0) | ||
| expect(reg.get("/a.ts")).toBeUndefined() | ||
| expect(reg.has("/b.ts")).toBe(false) | ||
| }) | ||
|
|
||
| it("get on empty registry returns undefined", () => { | ||
| const reg = new ObservationRegistry() | ||
| expect(reg.get("/any.ts")).toBeUndefined() | ||
| }) | ||
|
|
||
| it("separate instances are independent — observing in one does not appear in the other", () => { | ||
| const regA = new ObservationRegistry() | ||
| const regB = new ObservationRegistry() | ||
| regA.observe("/shared.ts", "v1") | ||
| expect(regA.get("/shared.ts")).toBeDefined() | ||
| expect(regB.get("/shared.ts")).toBeUndefined() | ||
| regB.observe("/shared.ts", "v2") | ||
| expect(regA.get("/shared.ts")!.version).toBe("v1") | ||
| expect(regB.get("/shared.ts")!.version).toBe("v2") | ||
| }) | ||
| }) | ||
|
|
||
| describe("close() - disposal is terminal", () => { | ||
| it("drops every observation and refuses later ones", () => { | ||
| const registry = new ObservationRegistry() | ||
| registry.observe("/workspace/a.ts", "v-a") | ||
| expect(registry.size).toBe(1) | ||
|
|
||
| registry.close() | ||
|
|
||
| expect(registry.size).toBe(0) | ||
| expect(registry.isClosed).toBe(true) | ||
|
|
||
| // A read that was in flight when the task was disposed must not repopulate it. | ||
| registry.observe("/workspace/late.ts", "v-late") | ||
| expect(registry.get("/workspace/late.ts")).toBeUndefined() | ||
| expect(registry.size).toBe(0) | ||
| }) | ||
| }) |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.