feat: replace revision counters with domain state contracts

Use content digests, key fingerprints, binding and trust identities, immutable mutation receipts, and observation tokens instead of state-update ordinals across API, storage, Runtime, and Web contracts.

Preserve historical replay and secret material through frozen migrations; preflight uncertain removals before cutover, resume provisioning receipts, validate encrypted migration candidates, and align fresh/upgraded schemas. Include reviewed regression fixes, generated artifacts, and validation evidence.
This commit is contained in:
2026-10-10 15:55:20 +09:00
parent 72ce817de4
commit c78f7f7d80
358 changed files with 25965 additions and 12069 deletions
+1 -1
View File
@@ -6,7 +6,7 @@
"dev": "deno run -A npm:vite@8.3.2 dev",
"dev:backend": "cd ../.. && cargo run -p yoi-workspace-server --bin yoi-server -- serve --listen 127.0.0.1:8787",
"check": "deno run -A npm:@sveltejs/kit@3.0.0 sync && deno run -A npm:svelte-check@4.7.6 --tsconfig ./tsconfig.json",
"test": "deno test --allow-read=src,test,tests,../../docs,../../resources/config-schema --allow-env=LOG,VSCODE_TEXTMATE_DEBUG,NODE_ENV tests/workspace-model.test.ts tests/repository-api.test.ts tests/workspace-catalog.test.ts tests/profile-api.test.ts tests/skill-api.test.ts tests/tooltip-contract.test.ts tests/bevel-contract.test.ts tests/bevel-line-contract.test.ts tests/typography-contract.test.ts src/lib/workspace/auth/model.test.ts src/lib/workspace/protocol-validator.test.ts tests/auth-api.test.ts src/lib/workspace/api/http.test.ts src/lib/workspace/api/workers.test.ts src/lib/workspace/api/ticket-browser.test.ts src/lib/workspace/header/breadcrumb-model.test.ts src/lib/workspace/console/chat-submit.test.ts src/lib/workspace/console/composer-delivery.test.ts test/composer-history.test.ts tests/composer-paste.test.ts src/lib/workspace/console/composer-command.test.ts src/lib/workspace/console/composer-draft.test.ts src/lib/workspace/console/composer-completion.test.ts src/lib/workspace/console/composer-invocation.test.ts src/lib/workspace/console/file-completions.test.ts test/console/ansi.test.ts src/lib/workspace/console/history.test.ts src/lib/workspace/console/model.test.ts src/lib/workspace/console/turn-navigation.test.ts src/lib/workspace/console/run-status.test.ts src/lib/workspace/companion/api.test.ts tests/workdir-api.test.ts tests/workdir-removal.test.ts tests/workdir-permissions.test.ts src/lib/workspace/console/tasks.test.ts test/ticket-detail-route-reuse.test.ts test/spa-shell.test.ts test/objective-pages.test.ts test/repositories/ui.test.ts src/lib/workspace/console/worker-console.ui.test.ts src/lib/workspace/settings/model.test.ts tests/runtime-connection.test.ts tests/runtime-management.test.ts tests/runtime-management-source.test.ts src/lib/workspace/sidebar/override-stack.test.ts src/lib/workspace/sidebar/route-ownership.test.ts src/lib/workspace/sidebar/workers.test.ts src/lib/workspace/sidebar/workspace-switcher.test.ts src/lib/workspace/sidebar/worker-subscription.test.ts src/lib/workspace/sidebar/worker-workdir-meta.test.ts src/lib/workspace/sidebar/worker-launch.test.ts src/lib/workspace/workers/worker-launch.test.ts tests/runtime-workers.test.ts test/sidebar/worker-actions.test.ts src/lib/workspace/tickets/merge-request-resources.test.ts src/lib/workspace/tickets/ticket-panel.test.ts test/merge-request-status.test.ts test/config-source/decodal-grammar.test.ts test/config-source/editor-state.test.ts test/config-source/fixed-schema-wrapper.test.ts test/config-source/toolchain.test.ts test/config-source/wasm-parity.test.ts test/config-source/api.test.ts tests/memory-api.test.ts test/repository-access/api.test.ts test/repository-access/loader.test.ts test/repository-access/model.test.ts test/repository-access/structure.test.ts test/repository-ssh-connection-ui.test.ts src/lib/workspace/drive/api.test.ts src/lib/workspace/drive/controller.test.ts src/lib/workspace/drive-ui/policy.test.ts",
"test": "deno test --allow-read=src,test,tests,../../docs,../../resources/config-schema --allow-env=LOG,VSCODE_TEXTMATE_DEBUG,NODE_ENV tests/workspace-model.test.ts tests/repository-api.test.ts tests/workspace-catalog.test.ts tests/profile-api.test.ts tests/skill-api.test.ts tests/tooltip-contract.test.ts tests/bevel-contract.test.ts tests/bevel-line-contract.test.ts tests/typography-contract.test.ts src/lib/workspace/auth/model.test.ts src/lib/workspace/protocol-validator.test.ts tests/auth-api.test.ts src/lib/workspace/api/http.test.ts src/lib/workspace/api/workers.test.ts src/lib/workspace/api/ticket-browser.test.ts src/lib/workspace/header/breadcrumb-model.test.ts src/lib/workspace/console/chat-submit.test.ts src/lib/workspace/console/composer-delivery.test.ts test/composer-history.test.ts tests/composer-paste.test.ts src/lib/workspace/console/composer-command.test.ts src/lib/workspace/console/composer-draft.test.ts src/lib/workspace/console/composer-completion.test.ts src/lib/workspace/console/composer-invocation.test.ts src/lib/workspace/console/file-completions.test.ts test/console/ansi.test.ts src/lib/workspace/console/history.test.ts src/lib/workspace/console/model.test.ts src/lib/workspace/console/turn-navigation.test.ts src/lib/workspace/console/run-status.test.ts src/lib/workspace/companion/api.test.ts tests/workdir-api.test.ts src/lib/workspace/settings/workdir-checkout.test.ts tests/workdir-removal.test.ts tests/workdir-permissions.test.ts src/lib/workspace/console/tasks.test.ts test/ticket-detail-route-reuse.test.ts test/spa-shell.test.ts test/objective-pages.test.ts test/repositories/ui.test.ts src/lib/workspace/console/worker-console.ui.test.ts src/lib/workspace/settings/model.test.ts tests/runtime-connection.test.ts tests/runtime-management.test.ts tests/runtime-management-source.test.ts src/lib/workspace/sidebar/override-stack.test.ts src/lib/workspace/sidebar/route-ownership.test.ts src/lib/workspace/sidebar/workers.test.ts src/lib/workspace/sidebar/workspace-switcher.test.ts src/lib/workspace/sidebar/worker-subscription.test.ts src/lib/workspace/sidebar/worker-workdir-meta.test.ts src/lib/workspace/sidebar/worker-launch.test.ts src/lib/workspace/workers/worker-launch.test.ts tests/runtime-workers.test.ts test/sidebar/worker-actions.test.ts src/lib/workspace/tickets/merge-request-resources.test.ts src/lib/workspace/tickets/ticket-panel.test.ts test/merge-request-status.test.ts test/config-source/decodal-grammar.test.ts test/config-source/editor-state.test.ts test/config-source/fixed-schema-wrapper.test.ts test/config-source/toolchain.test.ts test/config-source/wasm-parity.test.ts test/config-source/api.test.ts tests/memory-api.test.ts test/repository-access/api.test.ts test/repository-access/loader.test.ts test/repository-access/model.test.ts test/repository-access/structure.test.ts test/repository-ssh-connection-ui.test.ts src/lib/workspace/drive/api.test.ts src/lib/workspace/drive/controller.test.ts src/lib/workspace/drive-ui/policy.test.ts",
"test:component": "deno run -A npm:vitest@5.0.3 run --config vitest.config.ts",
"test:browser:empty-workspace": "deno test --config ../../tools/web-ux/deno.json --allow-env --allow-net --allow-read --allow-write --allow-run --allow-sys test/visual-review/empty-workspace.browser.test.ts",
"test:browser:drive": "deno test --config test/drive-browser/deno.json --allow-env --allow-net --allow-read --allow-write --allow-run --allow-sys test/drive-browser/drive.browser.test.ts",
@@ -1,6 +1,6 @@
// @generated by yoi-companion-openapi-typescript@1. Do not edit.
// Input: openapi/server-api.json
// Input contract: 0.1.0 (sha256:6dd3e9e7ffe5e653697addc29675545b5fb28ea856b14bafc580355f0453ec12)
// Input contract: 0.1.0 (sha256:07cfe53bfdd700cad76d00695a031255ffa76a6062110a05596fa8b64ae372c4)
// Options: companion-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output
// Output: web/workspace/src/lib/generated/companion-api.ts
// Regenerate/check: cargo run -q -p server-api --example generate_companion_api_types -- --check
+11 -8
View File
@@ -10,7 +10,10 @@ export type DriveEntry = {
parent: DriveEntryRef | null;
name: string;
kind: DriveEntryKind;
revision: string;
/**
* Request ID of the last committed mutation, or empty for the immutable root.
*/
last_mutation_id: string;
/**
* File length in bytes; absent for folders.
*/
@@ -74,9 +77,9 @@ export type DriveReadChunkQuery = {
entry_workspace_id: string;
id: string;
/**
* Bind a multi-chunk read to the same file revision.
* Require each chunk to match the same committed mutation observed in metadata.
*/
expected_revision: string;
expected_mutation_id: string;
offset: number;
length: number;
};
@@ -84,7 +87,7 @@ export type DriveReadChunkQuery = {
export type DriveDownloadQuery = {
entry_workspace_id: string;
id: string;
expected_revision: string | null;
expected_mutation_id: string | null;
};
export type DriveMutation = {
@@ -100,16 +103,16 @@ export type DriveMutation = {
} | {
"operation": "update_text";
id: DriveEntryRef;
expected_revision: string;
expected_mutation_id: string;
text: string;
content_type: string;
} | {
"operation": "relocate";
id: DriveEntryRef;
expected_revision: string;
expected_mutation_id: string;
parent: DriveEntryRef;
name: string;
} | { "operation": "delete"; id: DriveEntryRef; expected_revision: string };
} | { "operation": "delete"; id: DriveEntryRef; expected_mutation_id: string };
export type DriveMutationRequest = {
request_id: string;
@@ -133,7 +136,7 @@ export type DriveUploadQuery = {
parent_id: string | null;
name: string | null;
id: string | null;
expected_revision: string | null;
expected_mutation_id: string | null;
content_type: string;
/**
* Values above the file limit still decode as u32 so the service returns
@@ -24,7 +24,6 @@ export type WorkspaceRepositoryRecord = {
provider: string | null;
source: RepositorySource;
default_ref: string | null;
source_revision: number;
source_fingerprint: string;
observed_status: RepositoryObservedStatus;
observed_at: string | null;
@@ -35,7 +34,6 @@ export type WorkspaceRepositoryRecord = {
export type WorkspaceCreateResponse = {
workspace: WorkspaceSummary;
repository: WorkspaceRepositoryRecord | null;
config_revision: number;
request_fingerprint: string;
replayed: boolean;
};
@@ -92,9 +90,9 @@ export type WorkspaceDeletionPreflightResponse = {
workspace_id: string;
display_name: string;
/**
* Opaque persisted Workspace metadata revision used as a CAS fence.
* Observed Workspace metadata update timestamp used as a CAS fence.
*/
expected_revision: string;
expected_workspace_updated_at: string;
can_delete: boolean;
resources: WorkspaceDeletionResourceCounts;
blockers: Array<WorkspaceDeletionBlocker>;
@@ -102,7 +100,7 @@ export type WorkspaceDeletionPreflightResponse = {
export type WorkspaceDeletionRequest = {
operation_id: string;
expected_revision: string;
expected_workspace_updated_at: string;
confirmation: string;
};
@@ -155,14 +153,14 @@ export type WorkspaceMetadataSettingsResponse = {
workspace_id: string;
display_name: string;
created_at: string;
revision: string;
updated_at: string;
source: string;
diagnostics: Array<Diagnostic>;
};
export type UpdateWorkspaceMetadataRequest = {
display_name: string;
revision: string;
expected_updated_at: string;
};
export type WorkspaceMetadataMutationResponse = {
@@ -178,7 +176,6 @@ export type WorkspaceSigningIdentityPublic = {
algorithm: string;
public_key?: string;
public_key_fingerprint?: string;
revision: number;
state: WorkspaceSigningIdentityState;
created_at: string;
provisioned_at?: string;
@@ -191,7 +188,6 @@ export type WorkspacePublicIdentityBundle = {
algorithm: string;
public_key: string;
public_key_fingerprint: string;
revision: number;
};
export type WorkspaceSigningIdentityResponse = {
@@ -209,7 +205,6 @@ export type ConfigEntry = {
};
export type ConfigTreeSnapshot = {
revision: number;
digest: string;
entries: { [key in string]: ConfigEntry };
};
@@ -267,7 +262,6 @@ export type WorkspaceConfigTreeResponse = {
};
export type ConfigCommitRequest = {
base_revision: number;
base_digest: string;
changes: Array<ConfigTreeChange>;
entrypoints: Array<string>;
@@ -316,7 +310,6 @@ export type WorkspaceConfigNode = {
export type WorkspaceConfigObserveResponse = {
connection_id: string;
validator: string;
revision: number;
digest: string;
/**
* Canonical commit entrypoints from this exact metadata snapshot.
@@ -347,7 +340,6 @@ export type WorkspaceConfigCommitRequest = {
export type WorkspaceConfigCommitResponse = {
validator: string;
revision: number;
digest: string;
};
@@ -376,8 +368,6 @@ export type WorkspaceConfigGrantResponse = {
export type ProfileSettingsResponse = {
workspace_id: string;
registry_revision: string;
config_revision?: number | null;
tree_digest?: string | null;
projection_digest?: string | null;
default_profile?: string | null;
@@ -406,7 +396,6 @@ export type WorkspaceProfileSourceSummary = {
content_digest: string;
provenance: WorkspaceProfileSourceProvenance;
editable: boolean;
revision: string;
size_bytes: number;
diagnostics: Array<Diagnostic>;
};
@@ -445,7 +434,7 @@ export type RepositorySshConnectionProbeResponse = {
port: number;
trust_state: RepositorySshConnectionTrustState;
host_trust_id: string;
expected_host_trust_revision: number | null;
expected_host_key_fingerprint: string | null;
candidates: Array<RepositorySshHostKeyCandidate>;
};
@@ -453,7 +442,7 @@ export type ConfirmRepositorySshHostTrustRequest = {
operation_id: string;
runtime_id: string;
host_key: string;
expected_host_trust_revision: number | null;
expected_host_key_fingerprint: string | null;
};
export type RepositoryLogResponse = {
+209 -27
View File
@@ -15,60 +15,242 @@ export const SUBJEKTIV_API_LIMITS = {
export type DiagnosticSeverity = "info" | "warning" | "error";
export type Diagnostic = { code: string, severity: DiagnosticSeverity, message: string, };
export type Diagnostic = {
code: string;
severity: DiagnosticSeverity;
message: string;
};
export type MemoryDocumentResponse = { body_md: string, created_at: string, updated_at: string, bytes: number, record_source: string, };
export type MemoryDocumentResponse = {
body_md: string;
created_at: string;
updated_at: string;
bytes: number;
record_source: string;
};
export type MemoryCandidateKind = "preference" | "working_assumption" | "constraint" | "decision" | "open_question" | "lesson";
export type MemoryCandidateKind =
| "preference"
| "working_assumption"
| "constraint"
| "decision"
| "open_question"
| "lesson";
export type MemoryEvidenceOriginKind = "human_input" | "worker_input" | "flow_instruction" | "backend_instruction" | "model_output" | "tool_output" | "derived_summary" | "legacy_unknown";
export type MemoryEvidenceOriginKind =
| "human_input"
| "worker_input"
| "flow_instruction"
| "backend_instruction"
| "model_output"
| "tool_output"
| "derived_summary"
| "legacy_unknown";
export type MemoryEvidenceOrigin = { kind: MemoryEvidenceOriginKind, account_id?: string | null, workspace_id?: string | null, runtime_id?: string | null, worker_id?: string | null, flow_selector?: string | null, flow_definition_id?: string | null, flow_definition_revision?: number | null, };
export type MemoryEvidenceOrigin = {
kind: MemoryEvidenceOriginKind;
account_id?: string | null;
workspace_id?: string | null;
runtime_id?: string | null;
worker_id?: string | null;
flow_selector?: string | null;
flow_definition_id?: string | null;
flow_definition_fingerprint?: number | null;
};
export type MemorySourceRef = { segment_id: string, range: [number, number], };
export type MemorySourceRef = { segment_id: string; range: [number, number] };
export type MemoryStagingEvidence = { id: string, kind: string, entry_range: [number, number] | null, origin?: MemoryEvidenceOrigin | null, excerpt: string | null, summary: string | null, };
export type MemoryStagingEvidence = {
id: string;
kind: string;
entry_range: [number, number] | null;
origin?: MemoryEvidenceOrigin | null;
excerpt: string | null;
summary: string | null;
};
export type MemorySourceEvidenceRef = { session_id: string | null, segment_id: string | null, entry_range: [number, number] | null, evidence_id: string | null, origin?: MemoryEvidenceOrigin | null, evidence_kind: string | null, label: string | null, summary: string | null, };
export type MemorySourceEvidenceRef = {
session_id: string | null;
segment_id: string | null;
entry_range: [number, number] | null;
evidence_id: string | null;
origin?: MemoryEvidenceOrigin | null;
evidence_kind: string | null;
label: string | null;
summary: string | null;
};
export type MemoryStagingRecord = { schema_version: number, id: string, extract_run_id: string, source: MemorySourceRef, kind: MemoryCandidateKind, claim: string, why_useful: string, staleness: string | null, evidence: Array<MemoryStagingEvidence>, source_refs: Array<MemorySourceEvidenceRef>, };
export type MemoryStagingRecord = {
schema_version: number;
id: string;
extract_run_id: string;
source: MemorySourceRef;
kind: MemoryCandidateKind;
claim: string;
why_useful: string;
staleness: string | null;
evidence: Array<MemoryStagingEvidence>;
source_refs: Array<MemorySourceEvidenceRef>;
};
export type MemoryStagingEntry = { id: string, byte_len: number, record: MemoryStagingRecord, };
export type MemoryStagingEntry = {
id: string;
byte_len: number;
record: MemoryStagingRecord;
};
export type MemoryStagingListResponse = { limit: number, returned_count: number, total_valid_count: number, invalid_count: number, truncated: boolean, order: string, record_authority: string, items: Array<MemoryStagingEntry>, diagnostics: Array<Diagnostic>, };
export type MemoryStagingListResponse = {
limit: number;
returned_count: number;
total_valid_count: number;
invalid_count: number;
truncated: boolean;
order: string;
record_authority: string;
items: Array<MemoryStagingEntry>;
diagnostics: Array<Diagnostic>;
};
export type SubjektivSubjectState = "active" | "retired";
export type SubjektivSubjectCreateRequest = { role: string, behavior_md?: string, };
export type SubjektivSubjectCreateRequest = {
role: string;
behavior_md?: string;
};
export type SubjektivSubjectBehaviorUpdateRequest = { expected_behavior_revision: number, behavior_md: string, };
export type SubjektivSubjectBehaviorUpdateRequest = {
expected_behavior_md: string;
behavior_md: string;
};
export type SubjektivSubjectResponse = { id: string, role: string, behavior_md: string, behavior_revision: number, state: SubjektivSubjectState, store_revision: number, created_at: string, updated_at: string, current_worker?: | import("./worker-launch-api").WorkerLaunchWorkerSummary | null, };
export type SubjektivSubjectResponse = {
id: string;
role: string;
behavior_md: string;
state: SubjektivSubjectState;
memory_fingerprint: string;
created_at: string;
updated_at: string;
current_worker?:
| import("./worker-launch-api").WorkerLaunchWorkerSummary
| null;
};
export type SubjektivSubjectListResponse = { limit: number, items: Array<SubjektivSubjectResponse>, next_cursor?: string | null, has_more: boolean, };
export type SubjektivSubjectListResponse = {
limit: number;
items: Array<SubjektivSubjectResponse>;
next_cursor?: string | null;
has_more: boolean;
};
export type SubjektivMemoryState = "active" | "resolved" | "retracted";
export type SubjektivMemoryRevisionRef = { memory_id: string, revision: number, };
export type SubjektivMemoryChangeRef = { memory_id: string; change_id: string };
export type SubjektivResidentSurfaceAvailability = "ungenerated" | "stale" | "failed" | "ready";
export type SubjektivResidentSurfaceAvailability =
| "ungenerated"
| "stale"
| "failed"
| "ready";
export type SubjektivResidentSurfaceSnapshot = { snapshot_id: string, body_md: string, memory_refs: Array<SubjektivMemoryRevisionRef>, built_from_store_revision: number, created_at: string, };
export type SubjektivResidentSurfaceSnapshot = {
snapshot_id: string;
body_md: string;
memory_refs: Array<SubjektivMemoryChangeRef>;
built_from_memory_fingerprint: string;
created_at: string;
};
export type SubjektivResidentSurfaceResponse = { subject_id: string, availability: SubjektivResidentSurfaceAvailability, snapshot?: SubjektivResidentSurfaceSnapshot | null, };
export type SubjektivResidentSurfaceResponse = {
subject_id: string;
availability: SubjektivResidentSurfaceAvailability;
snapshot?: SubjektivResidentSurfaceSnapshot | null;
};
export type SubjektivMemoryQueryItem = { id: string, revision: number, kind: MemoryCandidateKind, state: SubjektivMemoryState, claim: string, excerpt: string, updated_at: string, };
export type SubjektivMemoryQueryItem = {
id: string;
change_id: string;
kind: MemoryCandidateKind;
state: SubjektivMemoryState;
claim: string;
excerpt: string;
updated_at: string;
};
export type SubjektivMemoryQueryResponse = { items: Array<SubjektivMemoryQueryItem>, next_cursor?: string | null, has_more: boolean, };
export type SubjektivMemoryQueryResponse = {
items: Array<SubjektivMemoryQueryItem>;
next_cursor?: string | null;
has_more: boolean;
};
export type SubjektivMemoryEvidence = { id: string, kind: string, entry_range?: [number, number] | null, origin?: MemoryEvidenceOrigin | null, excerpt?: string | null, summary?: string | null, };
export type SubjektivMemoryEvidence = {
id: string;
kind: string;
entry_range?: [number, number] | null;
origin?: MemoryEvidenceOrigin | null;
excerpt?: string | null;
summary?: string | null;
};
export type SubjektivMemorySourceEvidenceRef = { session_id?: string | null, segment_id?: string | null, entry_range?: [number, number] | null, evidence_id?: string | null, origin?: MemoryEvidenceOrigin | null, evidence_kind?: string | null, label?: string | null, summary?: string | null, };
export type SubjektivMemorySourceEvidenceRef = {
session_id?: string | null;
segment_id?: string | null;
entry_range?: [number, number] | null;
evidence_id?: string | null;
origin?: MemoryEvidenceOrigin | null;
evidence_kind?: string | null;
label?: string | null;
summary?: string | null;
};
export type SubjektivMemoryEvidenceCandidate = { candidate_id: string, evidence: Array<SubjektivMemoryEvidence>, evidence_total: number, evidence_truncated: boolean, source_refs: Array<SubjektivMemorySourceEvidenceRef>, source_refs_total: number, source_refs_truncated: boolean, };
export type SubjektivMemoryEvidenceCandidate = {
candidate_id: string;
evidence: Array<SubjektivMemoryEvidence>;
evidence_total: number;
evidence_truncated: boolean;
source_refs: Array<SubjektivMemorySourceEvidenceRef>;
source_refs_total: number;
source_refs_truncated: boolean;
};
export type SubjektivMemoryReadResponse = { memory_id: string, revision: number, current_revision: number, kind: MemoryCandidateKind, state: SubjektivMemoryState, claim: string, body_md: string, why_useful: string, staleness?: string | null, change_reason: string, created_at: string, updated_at: string, body_offset: number, body_byte_offset: number, body_next_offset?: number | null, body_next_byte_offset?: number | null, body_truncated: boolean, source_candidate_ids: Array<string>, source_candidates: Array<SubjektivMemoryEvidenceCandidate>, derived_from: Array<SubjektivMemoryRevisionRef>, evidence_next_cursor?: string | null, evidence_has_more: boolean, };
export type SubjektivMemoryReadResponse = {
memory_id: string;
change_id: string;
current_change_id: string;
kind: MemoryCandidateKind;
state: SubjektivMemoryState;
claim: string;
body_md: string;
why_useful: string;
staleness?: string | null;
change_reason: string;
created_at: string;
updated_at: string;
body_offset: number;
body_byte_offset: number;
body_next_offset?: number | null;
body_next_byte_offset?: number | null;
body_truncated: boolean;
source_candidate_ids: Array<string>;
source_candidates: Array<SubjektivMemoryEvidenceCandidate>;
derived_from: Array<SubjektivMemoryChangeRef>;
evidence_next_cursor?: string | null;
evidence_has_more: boolean;
};
export type SubjektivMemoryRevisionItem = { revision: number, kind: MemoryCandidateKind, state: SubjektivMemoryState, claim: string, change_reason: string, updated_at: string, };
export type SubjektivMemoryChangeItem = {
change_id: string;
kind: MemoryCandidateKind;
state: SubjektivMemoryState;
claim: string;
change_reason: string;
updated_at: string;
};
export type SubjektivMemoryListRevisionsResponse = { memory_id: string, current_revision: number, items: Array<SubjektivMemoryRevisionItem>, next_cursor?: string | null, has_more: boolean, };
export type SubjektivMemoryListChangesResponse = {
memory_id: string;
current_change_id: string;
items: Array<SubjektivMemoryChangeItem>;
next_cursor?: string | null;
has_more: boolean;
};
File diff suppressed because one or more lines are too long
+7 -7
View File
@@ -115,7 +115,7 @@ export type CommandEvent = { "kind": "started", command_id: string, tool_call_id
export type CompactionLifecycleState = "running" | "done" | "failed" | "interrupted";
export type CompactionLifecycle = { schema_version: number, compaction_id: string, revision: number, internal_worker?: InternalWorkerRef | null, state: CompactionLifecycleState,
export type CompactionLifecycle = { schema_version: number, compaction_id: string, internal_worker?: InternalWorkerRef | null, state: CompactionLifecycleState,
/**
* Milliseconds since the Unix epoch.
*/
@@ -240,7 +240,7 @@ export type PendingSubmissionSummary = { submission_id: string,
*/
preview?: string | null, accepted_at_ms: number, segment_count: number, byte_len: number, };
export type PendingSubmissionsSnapshot = { revision: number, notification_count: number,
export type PendingSubmissionsSnapshot = { notification_count: number,
/**
* Ordered display-only previews of waiting notifications, each bounded like
* PendingSubmissionSummary::preview. Older runtimes omit this field.
@@ -281,7 +281,7 @@ export type InternalWorkerKind = "sub_worker" | { "service": { kind: string, } }
export type InternalWorkerRef = { session_id: string, name: string, parent_session_id?: string | null, kind: InternalWorkerKind, };
export type InternalWorkerSnapshot = { worker: InternalWorkerRef, revision: number, session: SessionSnapshot,
export type InternalWorkerSnapshot = { worker: InternalWorkerRef, session: SessionSnapshot,
/**
* Public execution metadata for this child. Older snapshots and service
* workers may omit it; clients must never substitute the parent's values.
@@ -416,9 +416,9 @@ export type SubscriptionFramePayload = { "frame": "request", "message": Subscrip
export type SubscriptionFrame = { protocol_version: number, } & ({ "frame": "request", "message": SubscriptionRequest } | { "frame": "response", "message": SubscriptionResponse } | { "frame": "event", "message": SubscriptionEvent } | { "frame": "worker_protocol", "message": SubscriptionWorkerProtocolMethod });
export type Method = { "method": "submit", "params": { submission_request_id: string, input: Array<Segment>, } } | { "method": "notify", "params": { notification_request_id: string, message: string, } } | { "method": "worker_event", "params": WorkerEvent } | { "method": "list_pending_submissions" } | { "method": "cancel_pending_submission", "params": { submission_id: string, expected_revision: number, } } | { "method": "clear_pending_submissions", "params": { expected_revision: number, } } | { "method": "continue_pending", "params": { expected_revision: number, expected_head_id: string, } } | { "method": "resume", "params": { command: WorkerCommandEnvelope, } } | { "method": "cancel", "params": { command: WorkerCommandEnvelope, } } | { "method": "pause", "params": { command: WorkerCommandEnvelope, } } | { "method": "compact", "params": { command: WorkerCommandEnvelope, } } | { "method": "list_rewind_targets" } | { "method": "rewind_to", "params": { target: RewindTargetId, expected_head_entries: number, } } | { "method": "shutdown", "params": { command: WorkerCommandEnvelope, } } | { "method": "list_completions", "params": { kind: CompletionKind, prefix: string,
export type Method = { "method": "submit", "params": { submission_request_id: string, input: Array<Segment>, } } | { "method": "notify", "params": { notification_request_id: string, message: string, } } | { "method": "worker_event", "params": WorkerEvent } | { "method": "list_pending_submissions" } | { "method": "cancel_pending_submission", "params": { submission_id: string, } } | { "method": "clear_pending_submissions", "params": { expected_submission_ids: Array<string>, } } | { "method": "continue_pending", "params": { expected_head_id: string, } } | { "method": "resume", "params": { command: WorkerCommandEnvelope, } } | { "method": "cancel", "params": { command: WorkerCommandEnvelope, } } | { "method": "pause", "params": { command: WorkerCommandEnvelope, } } | { "method": "compact", "params": { command: WorkerCommandEnvelope, } } | { "method": "list_rewind_targets" } | { "method": "rewind_to", "params": { target: RewindTargetId, expected_head_entries: number, } } | { "method": "shutdown", "params": { command: WorkerCommandEnvelope, } } | { "method": "list_completions", "params": { kind: CompletionKind, prefix: string,
/**
* Client query generation. Echoed verbatim so identical-prefix ABA
* Client query identity. Echoed verbatim so identical-prefix ABA
* responses cannot cross edits, target switches, or permission changes.
*/
request_id?: string | null, context?: CompletionContext | null, } } | { "method": "list_workers" } | { "method": "restore_worker", "params": { name: string, } } | { "method": "register_peer", "params": { name: string, } };
@@ -455,7 +455,7 @@ output?: string | null, disposition?: ToolResultDisposition | null, is_error: bo
*/
timestamp_ms?: number | null, input_tokens: number | null, output_tokens: number | null, cache_read_input_tokens?: number | null, } } | { "event": "context_usage", "data": { usage?: ContextUsage | null, } } | { "event": "run_end", "data": { result: RunResult, } } | { "event": "error", "data": { code: ErrorCode, message: string, } } | { "event": "snapshot", "data": { session: SessionSnapshot, greeting: Greeting,
/**
* Full revisioned live execution state. `Stopped` remains Runtime
* Full live execution state. `Stopped` remains Runtime
* catalog authority and is deliberately not represented here.
*/
state: WorkerStateSnapshot,
@@ -468,4 +468,4 @@ in_flight?: InFlightSnapshot,
* Parent-owned Internal Worker sessions visible to this client.
* Service-private Internal Workers are deliberately excluded.
*/
internal_workers?: Array<InternalWorkerSnapshot>, } } | { "event": "internal_worker", "data": { worker: InternalWorkerRef, revision: number, event: Event, } } | { "event": "internal_worker_removed", "data": { worker: InternalWorkerRef, revision: number, } } | { "event": "segment_rotated", "data": { session: SessionSnapshot, } } | { "event": "worker_state", "data": { snapshot: WorkerStateSnapshot, } } | { "event": "command_acknowledged", "data": { acknowledgement: WorkerCommandAcknowledgement, } } | { "event": "command", "data": { event: CommandEvent, } } | { "event": "completions", "data": { kind: CompletionKind, prefix: string, request_id?: string | null, context?: CompletionContext | null, entries: Array<CompletionEntry>, } } | { "event": "rewind_targets", "data": { head_entries: number, targets: Array<RewindTarget>, } } | { "event": "rewind_applied", "data": { session: SessionSnapshot, input: Array<Segment>, summary: RewindSummary, } } | { "event": "workers_listed", "data": { workers: unknown, } } | { "event": "worker_restored", "data": { result: unknown, } } | { "event": "peer_registered", "data": { result: unknown, } } | { "event": "alert", "data": Alert } | { "event": "memory_worker", "data": MemoryWorkerEvent } | { "event": "compaction_progress", "data": { compaction: InFlightCompaction | null, } } | { "event": "compact_start", "data": { lifecycle: CompactionLifecycle, } } | { "event": "compact_done", "data": { lifecycle: CompactionLifecycle, } } | { "event": "compact_failed", "data": { lifecycle: CompactionLifecycle, } } | { "event": "shutdown" };
internal_workers?: Array<InternalWorkerSnapshot>, } } | { "event": "internal_worker", "data": { worker: InternalWorkerRef, event: Event, } } | { "event": "internal_worker_removed", "data": { worker: InternalWorkerRef, } } | { "event": "segment_rotated", "data": { session: SessionSnapshot, } } | { "event": "worker_state", "data": { snapshot: WorkerStateSnapshot, } } | { "event": "command_acknowledged", "data": { acknowledgement: WorkerCommandAcknowledgement, } } | { "event": "command", "data": { event: CommandEvent, } } | { "event": "completions", "data": { kind: CompletionKind, prefix: string, request_id?: string | null, context?: CompletionContext | null, entries: Array<CompletionEntry>, } } | { "event": "rewind_targets", "data": { head_entries: number, targets: Array<RewindTarget>, } } | { "event": "rewind_applied", "data": { session: SessionSnapshot, input: Array<Segment>, summary: RewindSummary, } } | { "event": "workers_listed", "data": { workers: unknown, } } | { "event": "worker_restored", "data": { result: unknown, } } | { "event": "peer_registered", "data": { result: unknown, } } | { "event": "alert", "data": Alert } | { "event": "memory_worker", "data": MemoryWorkerEvent } | { "event": "compaction_progress", "data": { compaction: InFlightCompaction | null, } } | { "event": "compact_start", "data": { lifecycle: CompactionLifecycle, } } | { "event": "compact_done", "data": { lifecycle: CompactionLifecycle, } } | { "event": "compact_failed", "data": { lifecycle: CompactionLifecycle, } } | { "event": "shutdown" };
@@ -7,7 +7,6 @@ export type RepositorySshCredential = {
name: string;
public_key_algorithm: string;
public_key_fingerprint: string;
current_revision: number;
status: string;
created_at: string;
rotated_at: string | null;
@@ -30,7 +29,6 @@ export type GenerateRepositorySshCredentialRequest = {
export type RepositorySshPublicKey = {
credential_id: string;
current_revision: number;
public_key_algorithm: string;
public_key_fingerprint: string;
public_key: string;
@@ -38,14 +36,14 @@ export type RepositorySshPublicKey = {
export type RotateRepositorySshCredentialRequest = {
operation_id: string;
expected_revision: number;
expected_public_key_fingerprint: string;
private_key: string;
passphrase: string | null;
};
export type DeleteRepositorySshCredentialRequest = {
operation_id: string;
expected_revision: number;
expected_public_key_fingerprint: string;
};
export type RepositorySshHostTrust = {
@@ -56,7 +54,6 @@ export type RepositorySshHostTrust = {
key_algorithm: string;
host_key: string;
fingerprint: string;
current_revision: number;
created_at: string;
updated_at: string;
referenced_repositories: Array<string>;
@@ -68,12 +65,12 @@ export type PutRepositorySshHostTrustRequest = {
hostname: string;
port: number;
host_key: string;
expected_revision: number | null;
expected_fingerprint: string | null;
};
export type DeleteRepositorySshHostTrustRequest = {
operation_id: string;
expected_revision: number;
expected_fingerprint: string;
};
export type RepositoryAccessMode = "read_only" | "read_write";
@@ -87,7 +84,6 @@ export type RepositorySshAccessBinding = {
export type RepositoryAccessProjection = {
workspace_id: string;
config_revision: number;
projection_digest: string;
bindings: Array<RepositorySshAccessBinding>;
};
@@ -1,11 +1,11 @@
// @generated by yoi-repository-openapi-typescript@1. Do not edit.
// Input: openapi/server-api.json
// Input contract: 0.1.0 (sha256:6dd3e9e7ffe5e653697addc29675545b5fb28ea856b14bafc580355f0453ec12)
// Input contract: 0.1.0 (sha256:07cfe53bfdd700cad76d00695a031255ffa76a6062110a05596fa8b64ae372c4)
// Options: repository-operation-schema-closure,openapi-3.1,format:uint64=safe-number,strict-safe-integers,sorted-output
// Output: web/workspace/src/lib/generated/repository-api.ts
// Regenerate/check: cargo run -q -p server-api --example generate_repository_openapi_types -- --check
export type ConfirmRepositorySshHostTrustRequest = { expected_host_trust_revision?: number | null; host_key: string; operation_id: string; runtime_id: string; };
export type ConfirmRepositorySshHostTrustRequest = { expected_host_key_fingerprint?: string | null; host_key: string; operation_id: string; runtime_id: string; };
export type CreateWorkspaceRepositoryRequest = { default_ref?: string | null; repository_key: string; source: string; };
@@ -47,15 +47,15 @@ export type RepositorySourceKind = "local_path" | "file" | "ssh" | "https" | "in
export type RepositorySshConnectionProbeRequest = { runtime_id: string; };
export type RepositorySshConnectionProbeResponse = { candidates: Array<RepositorySshHostKeyCandidate>; expected_host_trust_revision?: number | null; host_trust_id: string; hostname: string; port: number; repository_key: string; runtime_id: string; trust_state: RepositorySshConnectionTrustState; workspace_id: string; };
export type RepositorySshConnectionProbeResponse = { candidates: Array<RepositorySshHostKeyCandidate>; expected_host_key_fingerprint?: string | null; host_trust_id: string; hostname: string; port: number; repository_key: string; runtime_id: string; trust_state: RepositorySshConnectionTrustState; workspace_id: string; };
export type RepositorySshConnectionTrustState = "untrusted" | "verified" | "changed";
export type RepositorySshHostKeyCandidate = { algorithm: string; fingerprint: string; host_key: string; };
export type RepositorySshHostTrust = { created_at: string; current_revision: number; fingerprint: string; host_key: string; host_trust_id: string; hostname: string; key_algorithm: string; port: number; referenced_repositories?: Array<string>; updated_at: string; workspace_id: string; };
export type RepositorySshHostTrust = { created_at: string; fingerprint: string; host_key: string; host_trust_id: string; hostname: string; key_algorithm: string; port: number; referenced_repositories?: Array<string>; updated_at: string; workspace_id: string; };
export type RepositorySummary = { default_selector?: string | null; diagnostics?: Array<RepositoryDiagnostic> | null; git?: GitRepositorySummary | null; kind: string; observed_at?: string | null; observed_status: RepositoryObservedStatus; provider: string; record_authority: string; repository_key: string; source: RepositorySource; source_fingerprint: string; source_revision: number; };
export type RepositorySummary = { default_selector?: string | null; diagnostics?: Array<RepositoryDiagnostic> | null; git?: GitRepositorySummary | null; kind: string; observed_at?: string | null; observed_status: RepositoryObservedStatus; provider: string; record_authority: string; repository_key: string; source: RepositorySource; source_fingerprint: string; };
export type SubscriptionWorkerAvailability = "observed" | "unavailable";
+12 -12
View File
@@ -1,6 +1,6 @@
// @generated by yoi-runtime-openapi-typescript@1. Do not edit.
// Input: openapi/server-api.json
// Input contract: 0.1.0 (sha256:6dd3e9e7ffe5e653697addc29675545b5fb28ea856b14bafc580355f0453ec12)
// Input contract: 0.1.0 (sha256:07cfe53bfdd700cad76d00695a031255ffa76a6062110a05596fa8b64ae372c4)
// Options: runtime-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output
// Output: web/workspace/src/lib/generated/runtime-api.ts
// Regenerate/check: cargo run -q -p server-api --example generate_runtime_api_types -- --check
@@ -25,13 +25,13 @@ export type CompletionEntry = { description?: string | null; invocation?: Featur
export type CompletionKind = "file" | "feature" | "feature_argument";
export type CreateRemoteRuntimeRequest = { display_name?: string | null; endpoint: string; expected_revision?: number | null; public_bundle: RuntimePublicIdentityBundle; };
export type CreateRemoteRuntimeRequest = { display_name?: string | null; endpoint: string; expected_binding_id?: string | null; public_bundle: RuntimePublicIdentityBundle; workspace_trust_id: string; };
export type Diagnostic = { code: string; message: string; severity: DiagnosticSeverity; };
export type DiagnosticSeverity = "info" | "warning" | "error";
export type ExecuteRuntimeCleanupRequest = { confirm_dirty_discard_target_ids?: Array<string>; expected_plan_digest: string; expected_plan_revision: string; workdir_target_ids?: Array<string>; worker_target_ids?: Array<string>; };
export type ExecuteRuntimeCleanupRequest = { confirm_dirty_discard_target_ids?: Array<string>; expected_plan_digest: string; workdir_target_ids?: Array<string>; worker_target_ids?: Array<string>; };
export type ExternalWorkdirPermissions = { command: boolean; read: boolean; write: boolean; };
@@ -61,17 +61,17 @@ export type PasteArtifactMediaType = "text_plain_utf8";
export type PasteArtifactRef = { artifact_id: string; availability: PasteArtifactAvailability; byte_len: number; char_count: number; created_at_ms: number; line_count: number; media_type: PasteArtifactMediaType; sha256: string; source_entry_id: string; };
export type RemoveRuntimeRequest = { expected_binding_revision: number; operation_id: string; };
export type RemoveRuntimeRequest = { expected_binding_id: string; operation_id: string; };
export type RepositoryApiError = { diagnostics?: Array<Diagnostic>; error: string; message: string; };
export type RevokeRuntimeTrustKeyRequest = { expected_revision: number; };
export type RevokeRuntimeTrustKeyRequest = { expected_binding_id: string; };
export type RuntimeCleanupExecutionResponse = { diagnostics: Array<Diagnostic>; executed_at: string; plan_after: RuntimeCleanupPlanResponse; results: Array<RuntimeCleanupExecutionResult>; runtime_id: string; workspace_id: string; };
export type RuntimeCleanupExecutionResult = { action: CleanupTargetKind; message: string; status: string; target_id: string; };
export type RuntimeCleanupPlanResponse = { diagnostics: Array<Diagnostic>; digest: string; generated_at: string; revision: string; runtime_id: string; workdirs: Array<CleanupWorkdirCandidate>; workers: Array<CleanupWorkerCandidate>; workspace_id: string; };
export type RuntimeCleanupPlanResponse = { diagnostics: Array<Diagnostic>; digest: string; generated_at: string; runtime_id: string; workdirs: Array<CleanupWorkdirCandidate>; workers: Array<CleanupWorkerCandidate>; workspace_id: string; };
export type RuntimeConfigBundleResult = { availability?: unknown; diagnostics?: Array<Diagnostic>; state: WorkerOperationState; };
@@ -81,7 +81,7 @@ export type RuntimeConnectionDisplayState = "configured" | "verified" | "unavail
export type RuntimeConnectionTestFailureKind = "authentication" | "authorization" | "network_unreachable" | "timeout" | "tls_or_transport" | "malformed_response" | "protocol_version_mismatch" | "runtime_identity_mismatch" | "configuration";
export type RuntimeConnectionTestResponse = { actual_protocol_version?: number | null; binding_revision: number; checked_at: string; connection_state: RuntimeConnectionDisplayState; diagnostics?: Array<Diagnostic>; expected_protocol_version: number; failure_kind?: RuntimeConnectionTestFailureKind | null; runtime_id: string; status: RuntimeConnectionTestStatus; verification?: RuntimeVerificationEvidenceSummary | null; workspace_id: string; };
export type RuntimeConnectionTestResponse = { actual_protocol_version?: number | null; binding_id: string; checked_at: string; connection_state: RuntimeConnectionDisplayState; diagnostics?: Array<Diagnostic>; expected_protocol_version: number; failure_kind?: RuntimeConnectionTestFailureKind | null; runtime_id: string; status: RuntimeConnectionTestStatus; verification?: RuntimeVerificationEvidenceSummary | null; workspace_id: string; };
export type RuntimeConnectionTestStatus = "compatible" | "failed";
@@ -89,7 +89,7 @@ export type RuntimeIdentityAuthority = "runtime_registry_projection" | "server_r
export type RuntimeListResponse = { diagnostics?: Array<Diagnostic>; items: Array<RuntimeSummary>; limit: number; source: string; workspace_id: string; };
export type RuntimeManagementApiError = { current_fingerprint?: string | null; current_revision?: number | null; diagnostics?: Array<Diagnostic>; error: string; message: string; };
export type RuntimeManagementApiError = { current_binding_id?: string | null; current_fingerprint?: string | null; diagnostics?: Array<Diagnostic>; error: string; message: string; };
export type RuntimeManagementSummary = { binding?: WorkspaceRuntimeBindingSummary | null; built_in: boolean; config_managed: boolean; endpoint_configured: boolean; removable: boolean; token_ref_configured: boolean; };
@@ -113,15 +113,15 @@ export type RuntimeTicketWorkerRole = "intake" | "orchestrator" | "worker" | "re
export type RuntimeTrustAuditAction = "created" | "replaced" | "reactivated" | "revoked";
export type RuntimeTrustAuditEntry = { action: RuntimeTrustAuditAction; actor_account_id: string; at: string; new_fingerprint?: string | null; old_fingerprint?: string | null; revision: number; };
export type RuntimeTrustAuditEntry = { action: RuntimeTrustAuditAction; actor_account_id: string; at: string; binding_id: string; new_fingerprint?: string | null; old_fingerprint?: string | null; };
export type RuntimeTrustKeyRevealResponse = { public_key: string; };
export type RuntimeTrustKeyState = { created_at?: string | null; fingerprint?: string | null; revision?: number | null; revoked_at?: string | null; status: RuntimeTrustKeyStatus; updated_at?: string | null; };
export type RuntimeTrustKeyState = { binding_id?: string | null; created_at?: string | null; fingerprint?: string | null; revoked_at?: string | null; status: RuntimeTrustKeyStatus; updated_at?: string | null; };
export type RuntimeTrustKeyStatus = "unconfigured" | "active" | "revoked";
export type RuntimeVerificationEvidenceSummary = { binding_revision: number; last_checked_at: string; last_outcome: RuntimeVerificationOutcome; runtime_identity_revision: number; runtime_public_key_fingerprint: string; verified_at?: string | null; workspace_identity_revision: number; workspace_key_id: string; workspace_trust_generation: number; };
export type RuntimeVerificationEvidenceSummary = { binding_id: string; last_checked_at: string; last_outcome: RuntimeVerificationOutcome; runtime_public_key_fingerprint: string; verified_at?: string | null; workspace_key_id: string; workspace_public_key_fingerprint: string; workspace_trust_id: string; };
export type RuntimeVerificationOutcome = "verified" | "challenge_issued" | "verification_failed" | "connectivity_failed";
@@ -219,7 +219,7 @@ export type WorkspaceConfigAccess = "read_only" | "read_write";
export type WorkspaceRuntimeBindingState = "configured" | "verified" | "revoked";
export type WorkspaceRuntimeBindingSummary = { connection_state: RuntimeConnectionDisplayState; revision: number; state: WorkspaceRuntimeBindingState; verification?: RuntimeVerificationEvidenceSummary | null; workspace_key_generation?: number | null; workspace_key_id?: string | null; };
export type WorkspaceRuntimeBindingSummary = { binding_id: string; connection_state: RuntimeConnectionDisplayState; state: WorkspaceRuntimeBindingState; verification?: RuntimeVerificationEvidenceSummary | null; workspace_key_id?: string | null; workspace_trust_id?: string | null; };
export type WorkspaceRuntimeDetail = { endpoint?: string | null; recent_audit?: Array<RuntimeTrustAuditEntry>; runtime: WorkspaceRuntimeResource; trust_key: RuntimeTrustKeyState; workspace_id: string; };
+1 -5
View File
@@ -33,7 +33,6 @@ export type SkillProvenance = {
kind: SkillSourceKind;
id: string;
virtual_path?: string;
revision?: number;
source_digest?: string;
tree_digest?: string;
};
@@ -42,10 +41,7 @@ export type SkillActivationStatus = "active" | "inactive";
export type SkillProjectionStatus = "valid" | "invalid";
export type SkillProjectionIdentity = {
config_revision: number;
tree_digest: string;
};
export type SkillProjectionIdentity = { tree_digest: string };
export type SkillResourceRef = {
kind: string;
@@ -1,13 +1,13 @@
// @generated by yoi-ticket-browser-openapi-typescript@1. Do not edit.
// Input: openapi/server-api.json
// Input contract: 0.1.0 (sha256:6dd3e9e7ffe5e653697addc29675545b5fb28ea856b14bafc580355f0453ec12)
// Input contract: 0.1.0 (sha256:07cfe53bfdd700cad76d00695a031255ffa76a6062110a05596fa8b64ae372c4)
// Options: ticket-objective-merge-request-browser-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output
// Output: web/workspace/src/lib/generated/ticket-api.ts
// Regenerate/check: cargo run -q -p server-api --example generate_ticket_api_types -- --check
export type BrowserAppendTicketEventRequest = { author?: string | null; body: string; role: BrowserTicketThreadRole; };
export type BrowserCloseTicketRequest = { expected_item_revision: string; expected_state: BrowserTicketWorkflowState; operation_key: string; resolution: string; };
export type BrowserCloseTicketRequest = { expected_content_digest: string; expected_state: BrowserTicketWorkflowState; operation_key: string; resolution: string; };
export type BrowserEditTicketRequest = { author?: string | null; body?: string | null; new_string?: string | null; old_string?: string | null; replace_all?: boolean; target?: BrowserTicketTargetEdit | null; title?: string | null; };
@@ -19,7 +19,7 @@ export type BrowserTicketThreadRole = "comment" | "plan" | "decision" | "impleme
export type BrowserTicketWorkflowState = "planning" | "ready" | "queued" | "inprogress" | "done" | "closed";
export type BrowserTransitionTicketStateRequest = { body?: string | null; expected_item_revision: string; expected_state: BrowserTicketWorkflowState; operation_key: string; reason: string; state: BrowserTicketWorkflowState; };
export type BrowserTransitionTicketStateRequest = { body?: string | null; expected_content_digest: string; expected_state: BrowserTicketWorkflowState; operation_key: string; reason: string; state: BrowserTicketWorkflowState; };
export type CancelTicketImplementationRequest = { assignment_id: string; operation_id: string; reason: string; };
@@ -51,7 +51,7 @@ export type MergeRequestReviewSubject = { merge_request_id: string; subject_ref:
export type MergeRequestState = "open" | "merged" | "closed";
export type MergeRequestThreadEvent = { created_at: string; event_id: string; kind: "review_requested"; requested_by: MergeRequestWorkerIdentity; reviewer: MergeRequestWorkerIdentity; sequence: number; subject_ref: string; ticket_item_revision: string; ticket_merge_request_subjects: Array<MergeRequestReviewSubject>; } | { body: string; created_at: string; decision: ReviewDecision; event_id: string; findings: Array<ReviewFinding>; kind: "review"; request_event_id: string; reviewer: MergeRequestWorkerIdentity; sequence: number; subject_ref: string; ticket_item_revision: string; ticket_merge_request_subjects: Array<MergeRequestReviewSubject>; } | { created_at: string; event_id: string; kind: "review_revoked"; reason: string; review_event_id: string; revoked_by: MergeRequestWorkerIdentity; sequence: number; subject_ref: string; } | { created_at: string; event_id: string; kind: "review_cancelled"; reason: string; request_event_id: string; sequence: number; subject_ref: string; } | { author: MergeRequestWorkerIdentity; body: string; created_at: string; event_id: string; kind: "comment"; sequence: number; } | { approval_event_id: string; approved_source_ref: string; created_at: string; event_id: string; kind: "merge"; merged_by: MergeRequestWorkerIdentity; operation_id: string; resolution: ConflictResolution; sequence: number; strategy: MergeStrategy; target_ref_after: string; target_ref_before: string; };
export type MergeRequestThreadEvent = { created_at: string; event_id: string; kind: "review_requested"; requested_by: MergeRequestWorkerIdentity; reviewer: MergeRequestWorkerIdentity; sequence: number; subject_ref: string; ticket_content_digest: string; ticket_merge_request_subjects: Array<MergeRequestReviewSubject>; } | { body: string; created_at: string; decision: ReviewDecision; event_id: string; findings: Array<ReviewFinding>; kind: "review"; request_event_id: string; reviewer: MergeRequestWorkerIdentity; sequence: number; subject_ref: string; ticket_content_digest: string; ticket_merge_request_subjects: Array<MergeRequestReviewSubject>; } | { created_at: string; event_id: string; kind: "review_revoked"; reason: string; review_event_id: string; revoked_by: MergeRequestWorkerIdentity; sequence: number; subject_ref: string; } | { created_at: string; event_id: string; kind: "review_cancelled"; reason: string; request_event_id: string; sequence: number; subject_ref: string; } | { author: MergeRequestWorkerIdentity; body: string; created_at: string; event_id: string; kind: "comment"; sequence: number; } | { approval_event_id: string; approved_source_ref: string; created_at: string; event_id: string; kind: "merge"; merged_by: MergeRequestWorkerIdentity; operation_id: string; resolution: ConflictResolution; sequence: number; strategy: MergeStrategy; target_ref_after: string; target_ref_before: string; };
export type MergeRequestWorkerIdentity = { runtime_id: string; worker_id: string; };
@@ -59,7 +59,7 @@ export type MergeStrategy = "fast_forward" | "merge";
export type NewTicket = { assignee?: string | null; author?: string | null; body: MarkdownText; kind: string; labels: Array<string>; priority: string; queued_at?: string | null; queued_by?: string | null; readiness?: string | null; risk_flags: Array<string>; slug?: string | null; targets?: Array<TicketTarget>; title: string; workflow_state?: TicketWorkflowState | null; };
export type ObjectiveDetail = { body: string; body_truncated: boolean; created_at?: string | null; event_page: QueryPage; events: Array<ObjectiveEventDetail>; id: string; linked_ticket_summaries: Array<ObjectiveLinkedTicketSummary>; linked_tickets: Array<string>; record_source: string; resource_key: string; resources: Array<ObjectiveResourceSummary>; revision: string; state: string; title: string; updated_at?: string | null; };
export type ObjectiveDetail = { body: string; body_truncated: boolean; created_at?: string | null; event_page: QueryPage; events: Array<ObjectiveEventDetail>; id: string; linked_ticket_summaries: Array<ObjectiveLinkedTicketSummary>; linked_tickets: Array<string>; record_source: string; resource_key: string; resources: Array<ObjectiveResourceSummary>; state: string; title: string; updated_at?: string | null; };
export type ObjectiveEventDetail = { body?: string | null; created_at: string; event_ref: string; kind: string; };
@@ -91,7 +91,7 @@ export type TicketAssignmentRole = "orchestrator" | "worker" | "owner" | "contri
export type TicketAssignmentSummary = { assignment_id: string; runtime_id: string; worker_id: string; worker_resource_key?: string | null; };
export type TicketDetail = { action_eligibility: TicketActionEligibility; artifact_count: number; artifacts: Array<string>; assignment_diagnostics: Array<string>; assignments: Array<TicketRoleAssignmentSummary>; body: string; body_truncated: boolean; created_at?: string | null; current_worker?: TicketAssignmentSummary | null; event_count: number; event_page: QueryPage; events: Array<TicketEventDetail>; evidence: TicketEvidenceSummary; id: string; implementation_reports: Array<TicketEvidenceEvent>; item_revision: string; linked_objectives: Array<ObjectiveLinkSummary>; merge_request?: TicketMergeRequestSummary | null; merge_requests: Array<TicketMergeRequestSummary>; priority: string; queued_at?: string | null; queued_by?: string | null; readiness?: string | null; record_source: string; relations: TicketDetailRelationView; resolution?: string | null; resource_key: string; risk_flags: Array<string>; state: string; targets: Array<TicketTarget>; title: string; updated_at?: string | null; };
export type TicketDetail = { action_eligibility: TicketActionEligibility; artifact_count: number; artifacts: Array<string>; assignment_diagnostics: Array<string>; assignments: Array<TicketRoleAssignmentSummary>; body: string; body_truncated: boolean; content_digest: string; created_at?: string | null; current_worker?: TicketAssignmentSummary | null; event_count: number; event_page: QueryPage; events: Array<TicketEventDetail>; evidence: TicketEvidenceSummary; id: string; implementation_reports: Array<TicketEvidenceEvent>; linked_objectives: Array<ObjectiveLinkSummary>; merge_request?: TicketMergeRequestSummary | null; merge_requests: Array<TicketMergeRequestSummary>; priority: string; queued_at?: string | null; queued_by?: string | null; readiness?: string | null; record_source: string; relations: TicketDetailRelationView; resolution?: string | null; resource_key: string; risk_flags: Array<string>; state: string; targets: Array<TicketTarget>; title: string; updated_at?: string | null; };
export type TicketDetailDerivedRelation = { at: string; author: string; forward_kind: string; inverse_kind: string; note?: string | null; source_resource_key?: string | null; source_ticket: string; };
@@ -1,6 +1,6 @@
// @generated by yoi-workdir-openapi-typescript@1. Do not edit.
// Input: openapi/server-api.json
// Input contract: 0.1.0 (sha256:6dd3e9e7ffe5e653697addc29675545b5fb28ea856b14bafc580355f0453ec12)
// Input contract: 0.1.0 (sha256:07cfe53bfdd700cad76d00695a031255ffa76a6062110a05596fa8b64ae372c4)
// Options: workdir-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output
// Output: web/workspace/src/lib/generated/workdir-api.ts
// Regenerate/check: cargo run -q -p server-api --example generate_workdir_api_types -- --check
@@ -1,6 +1,6 @@
// @generated by yoi-worker-openapi-typescript@1. Do not edit.
// Input: openapi/server-api.json
// Input contract: 0.1.0 (sha256:6dd3e9e7ffe5e653697addc29675545b5fb28ea856b14bafc580355f0453ec12)
// Input contract: 0.1.0 (sha256:07cfe53bfdd700cad76d00695a031255ffa76a6062110a05596fa8b64ae372c4)
// Options: worker-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output
// Output: web/workspace/src/lib/generated/worker-launch-api.ts
// Regenerate/check: cargo run -q -p server-api --example generate_worker_launch_api_types -- --check
@@ -93,7 +93,6 @@ Deno.test("loadWorkspaceSkillCatalog fetches lightweight catalog", async () => {
JSON.stringify({
authority: "workspace-config-skills-v1",
projection: {
config_revision: 7,
tree_digest: "tree-digest",
},
entries: [{
@@ -105,7 +104,6 @@ Deno.test("loadWorkspaceSkillCatalog fetches lightweight catalog", async () => {
kind: "workspace",
id: "workspace:triage-errors",
virtual_path: "skills/triage-errors/SKILL.md",
revision: 7,
source_digest: "source-digest",
tree_digest: "tree-digest",
},
@@ -36,7 +36,6 @@ export function parseRepositorySshCredential(
"name",
"public_key_algorithm",
"public_key_fingerprint",
"current_revision",
"status",
"created_at",
"rotated_at",
@@ -47,7 +46,6 @@ export function parseRepositorySshCredential(
readString(record, "name", path);
readString(record, "public_key_algorithm", path);
readString(record, "public_key_fingerprint", path);
readRevision(record, "current_revision", path);
readString(record, "status", path);
readString(record, "created_at", path);
readNullableString(record, "rotated_at", path);
@@ -61,13 +59,11 @@ export function parseRepositorySshPublicKey(
): RepositorySshPublicKey {
const record = readRecord(value, path, [
"credential_id",
"current_revision",
"public_key_algorithm",
"public_key_fingerprint",
"public_key",
]);
readString(record, "credential_id", path);
readRevision(record, "current_revision", path);
readString(record, "public_key_algorithm", path);
readString(record, "public_key_fingerprint", path);
readString(record, "public_key", path);
@@ -94,7 +90,6 @@ export function parseRepositorySshHostTrust(
"key_algorithm",
"host_key",
"fingerprint",
"current_revision",
"created_at",
"updated_at",
"referenced_repositories",
@@ -112,7 +107,6 @@ export function parseRepositorySshHostTrust(
readString(record, "key_algorithm", path);
readString(record, "host_key", path);
readString(record, "fingerprint", path);
readRevision(record, "current_revision", path);
readString(record, "created_at", path);
readString(record, "updated_at", path);
readStringArray(record, "referenced_repositories", path);
@@ -125,12 +119,10 @@ export function parseRepositoryAccessProjection(
const path = "access_projection";
const record = readRecord(value, path, [
"workspace_id",
"config_revision",
"projection_digest",
"bindings",
]);
readString(record, "workspace_id", path);
readRevision(record, "config_revision", path);
readString(record, "projection_digest", path);
const bindings = readArray(record.bindings, `${path}.bindings`);
bindings.forEach((binding, index) => {
@@ -248,18 +240,3 @@ function readInteger(
}
return value;
}
function readRevision(
record: Record<string, unknown>,
key: string,
path: string,
): number {
const revision = readInteger(record, key, path);
if (revision < 0) {
throw new RepositoryAccessSchemaError(
`${path}.${key}`,
"a non-negative safe integer",
);
}
return revision;
}
@@ -8,7 +8,7 @@ import type {
const RESPONSE_KEYS = [
"workspace_id",
"runtime_id",
"binding_revision",
"binding_id",
"connection_state",
"verification",
"checked_at",
@@ -107,15 +107,15 @@ export function parseRuntimeConnectionTestResponse(
) {
return null;
}
const bindingRevision = value.binding_revision;
const bindingId = value.binding_id;
const connectionState = parseConnectionState(value.connection_state);
const verification = parseVerificationEvidence(value.verification);
if (
!isSafeRevision(bindingRevision) ||
!isBoundedString(bindingId) ||
connectionState === null ||
(value.verification !== null && verification === null) ||
(verification !== null &&
verification.binding_revision !== bindingRevision) ||
verification.binding_id !== bindingId) ||
(connectionState === "verified" && value.status !== "compatible") ||
(connectionState === "verified" && verification !== null &&
(verification.last_outcome !== "verified" ||
@@ -126,7 +126,7 @@ export function parseRuntimeConnectionTestResponse(
return {
workspace_id: value.workspace_id,
runtime_id: value.runtime_id,
binding_revision: bindingRevision,
binding_id: bindingId,
connection_state: connectionState,
verification,
checked_at: value.checked_at,
@@ -147,10 +147,6 @@ function parseConnectionState(
: null;
}
function isSafeRevision(value: unknown): value is number {
return Number.isSafeInteger(value) && (value as number) >= 0;
}
function parseVerificationEvidence(
value: unknown,
): RuntimeVerificationEvidenceSummary | null {
@@ -159,12 +155,11 @@ function parseVerificationEvidence(
"verified_at",
"last_checked_at",
"last_outcome",
"binding_revision",
"binding_id",
"workspace_key_id",
"workspace_identity_revision",
"workspace_trust_generation",
"workspace_public_key_fingerprint",
"workspace_trust_id",
"runtime_public_key_fingerprint",
"runtime_identity_revision",
] as const;
if (!isRecord(value) || !hasExactKeys(value, keys)) return null;
if (
@@ -177,12 +172,11 @@ function parseVerificationEvidence(
value.last_outcome !== "challenge_issued" &&
value.last_outcome !== "verification_failed" &&
value.last_outcome !== "connectivity_failed") ||
!isSafeRevision(value.binding_revision) ||
!isBoundedString(value.binding_id) ||
!isBoundedString(value.workspace_key_id, 128) ||
!isSafeRevision(value.workspace_identity_revision) ||
!isSafeRevision(value.workspace_trust_generation) ||
!isBoundedString(value.runtime_public_key_fingerprint, 128) ||
!isSafeRevision(value.runtime_identity_revision)
!isBoundedString(value.workspace_public_key_fingerprint) ||
!isBoundedString(value.workspace_trust_id) ||
!isBoundedString(value.runtime_public_key_fingerprint, 128)
) {
return null;
}
@@ -190,12 +184,11 @@ function parseVerificationEvidence(
verified_at: value.verified_at,
last_checked_at: value.last_checked_at,
last_outcome: value.last_outcome,
binding_revision: value.binding_revision,
binding_id: value.binding_id,
workspace_key_id: value.workspace_key_id,
workspace_identity_revision: value.workspace_identity_revision,
workspace_trust_generation: value.workspace_trust_generation,
workspace_public_key_fingerprint: value.workspace_public_key_fingerprint,
workspace_trust_id: value.workspace_trust_id,
runtime_public_key_fingerprint: value.runtime_public_key_fingerprint,
runtime_identity_revision: value.runtime_identity_revision,
};
}
@@ -27,16 +27,20 @@ import type {
} from "#lib/generated/runtime-api.ts";
import { workspaceApiPath } from "./http.ts";
export type WorkspaceRuntimeList = Omit<WorkspaceRuntimeListResponse, "diagnostics"> & {
diagnostics: Diagnostic[];
};
export type ValidatedWorkspaceRuntimeDetail = Omit<
WorkspaceRuntimeDetail,
"recent_audit"
> & {
recent_audit: RuntimeTrustAuditEntry[];
};
export type RuntimeTrustConflictKind = "stale_revision" | "fingerprint_in_use";
export type WorkspaceRuntimeList =
& Omit<WorkspaceRuntimeListResponse, "diagnostics">
& {
diagnostics: Diagnostic[];
};
export type ValidatedWorkspaceRuntimeDetail =
& Omit<
WorkspaceRuntimeDetail,
"recent_audit"
>
& {
recent_audit: RuntimeTrustAuditEntry[];
};
export type RuntimeTrustConflictKind = "stale_binding" | "fingerprint_in_use";
export type RuntimeTrustConflictResponse = RuntimeManagementApiError & {
error: RuntimeTrustConflictKind;
};
@@ -83,7 +87,7 @@ const AUDIT_ACTIONS = new Set<RuntimeTrustAuditAction>([
"revoked",
]);
const CONFLICT_KINDS = new Set<RuntimeTrustConflictKind>([
"stale_revision",
"stale_binding",
"fingerprint_in_use",
]);
const BINDING_STATES = new Set<WorkspaceRuntimeBindingState>([
@@ -226,8 +230,8 @@ function safeInteger(value: unknown, path: string, minimum = 0): number {
return value;
}
function safeRevision(value: unknown, path: string): number {
return safeInteger(value, path, 1);
function boundedIdentity(value: unknown, path: string): string {
return boundedString(value, path, LIMITS.idBytes);
}
function optionalNullableString(
@@ -240,20 +244,20 @@ function optionalNullableString(
return boundedString(value, path, maxBytes, allowEmpty);
}
function optionalRevision(
function optionalIdentity(
value: unknown,
path: string,
): number | undefined {
): string | undefined {
if (value === undefined || value === null) return undefined;
return safeRevision(value, path);
return boundedIdentity(value, path);
}
function optionalNullableRevision(
function optionalNullableIdentity(
value: unknown,
path: string,
): number | null | undefined {
): string | null | undefined {
if (value === undefined || value === null) return value;
return safeRevision(value, path);
return boundedIdentity(value, path);
}
function timestamp(value: unknown, path: string): string {
@@ -333,12 +337,11 @@ function runtimeVerification(
"verified_at",
"last_checked_at",
"last_outcome",
"binding_revision",
"binding_id",
"workspace_key_id",
"workspace_identity_revision",
"workspace_trust_generation",
"workspace_public_key_fingerprint",
"workspace_trust_id",
"runtime_public_key_fingerprint",
"runtime_identity_revision",
],
[],
path,
@@ -366,32 +369,28 @@ function runtimeVerification(
128,
),
last_outcome: lastOutcome,
binding_revision: safeRevision(
item.binding_revision,
`${path}.binding_revision`,
binding_id: boundedIdentity(
item.binding_id,
`${path}.binding_id`,
),
workspace_key_id: boundedString(
item.workspace_key_id,
`${path}.workspace_key_id`,
LIMITS.idBytes,
),
workspace_identity_revision: safeRevision(
item.workspace_identity_revision,
`${path}.workspace_identity_revision`,
workspace_public_key_fingerprint: boundedIdentity(
item.workspace_public_key_fingerprint,
`${path}.workspace_public_key_fingerprint`,
),
workspace_trust_generation: safeRevision(
item.workspace_trust_generation,
`${path}.workspace_trust_generation`,
workspace_trust_id: boundedIdentity(
item.workspace_trust_id,
`${path}.workspace_trust_id`,
),
runtime_public_key_fingerprint: boundedString(
item.runtime_public_key_fingerprint,
`${path}.runtime_public_key_fingerprint`,
LIMITS.fingerprintBytes,
),
runtime_identity_revision: safeRevision(
item.runtime_identity_revision,
`${path}.runtime_identity_revision`,
),
};
}
@@ -403,8 +402,8 @@ function runtimeBinding(
const item = object(value, path);
exactKeys(
item,
["state", "connection_state", "revision"],
["workspace_key_id", "workspace_key_generation", "verification"],
["state", "connection_state", "binding_id"],
["workspace_key_id", "workspace_trust_id", "verification"],
path,
);
const workspaceKeyId = optionalNullableString(
@@ -412,15 +411,15 @@ function runtimeBinding(
`${path}.workspace_key_id`,
LIMITS.idBytes,
);
const workspaceKeyGeneration = optionalNullableRevision(
item.workspace_key_generation,
`${path}.workspace_key_generation`,
const workspaceTrustId = optionalNullableIdentity(
item.workspace_trust_id,
`${path}.workspace_trust_id`,
);
const state = enumValue(item.state, `${path}.state`, BINDING_STATES);
if (
requiresWorkspaceIdentity &&
state !== "revoked" &&
(workspaceKeyId == null || workspaceKeyGeneration == null)
(workspaceKeyId == null || workspaceTrustId == null)
) {
return fail(path, "requires Workspace signing key identity metadata");
}
@@ -429,14 +428,14 @@ function runtimeBinding(
`${path}.connection_state`,
CONNECTION_STATES,
);
const revision = safeRevision(item.revision, `${path}.revision`);
const binding_id = boundedIdentity(item.binding_id, `${path}.binding_id`);
const verification = item.verification === undefined
? undefined
: runtimeVerification(item.verification, `${path}.verification`);
if (
verification !== undefined && verification.binding_revision !== revision
verification !== undefined && verification.binding_id !== binding_id
) {
return fail(path, "verification must match the current binding revision");
return fail(path, "verification must match the current binding binding_id");
}
if (
requiresWorkspaceIdentity &&
@@ -453,13 +452,13 @@ function runtimeBinding(
return {
state,
connection_state: connectionState,
revision,
binding_id,
...(workspaceKeyId === undefined
? {}
: { workspace_key_id: workspaceKeyId }),
...(workspaceKeyGeneration === undefined
...(workspaceTrustId === undefined
? {}
: { workspace_key_generation: workspaceKeyGeneration }),
: { workspace_trust_id: workspaceTrustId }),
...(verification === undefined ? {} : { verification }),
};
}
@@ -566,7 +565,7 @@ function trustKey(value: unknown, path: string): RuntimeTrustKeyState {
exactKeys(
item,
["status"],
["fingerprint", "revision", "created_at", "updated_at", "revoked_at"],
["fingerprint", "binding_id", "created_at", "updated_at", "revoked_at"],
path,
);
const result: RuntimeTrustKeyState = {
@@ -576,7 +575,7 @@ function trustKey(value: unknown, path: string): RuntimeTrustKeyState {
`${path}.fingerprint`,
LIMITS.fingerprintBytes,
),
revision: optionalNullableRevision(item.revision, `${path}.revision`),
binding_id: optionalNullableIdentity(item.binding_id, `${path}.binding_id`),
created_at: optionalNullableTimestamp(
item.created_at,
`${path}.created_at`,
@@ -594,12 +593,12 @@ function trustKey(value: unknown, path: string): RuntimeTrustKeyState {
const hasBinding = result.status !== "unconfigured";
if (
hasBinding &&
(result.fingerprint == null || result.revision == null ||
(result.fingerprint == null || result.binding_id == null ||
result.created_at == null || result.updated_at == null)
) {
fail(
path,
"must include fingerprint, revision, created_at, and updated_at",
"must include fingerprint, binding_id, created_at, and updated_at",
);
}
if (
@@ -623,7 +622,7 @@ function auditEntry(value: unknown, path: string): RuntimeTrustAuditEntry {
const item = object(value, path);
exactKeys(
item,
["action", "actor_account_id", "revision", "at"],
["action", "actor_account_id", "binding_id", "at"],
["old_fingerprint", "new_fingerprint"],
path,
);
@@ -644,7 +643,7 @@ function auditEntry(value: unknown, path: string): RuntimeTrustAuditEntry {
`${path}.new_fingerprint`,
LIMITS.fingerprintBytes,
),
revision: safeRevision(item.revision, `${path}.revision`),
binding_id: boundedIdentity(item.binding_id, `${path}.binding_id`),
at: timestamp(item.at, `${path}.at`),
};
}
@@ -764,7 +763,7 @@ export function parseRuntimeTrustConflict(
exactKeys(
response,
["error", "message"],
["current_revision", "current_fingerprint"],
["current_binding_id", "current_fingerprint"],
"Runtime trust conflict",
);
return {
@@ -778,9 +777,9 @@ export function parseRuntimeTrustConflict(
"Runtime trust conflict.message",
LIMITS.conflictMessageBytes,
),
current_revision: optionalRevision(
response.current_revision,
"Runtime trust conflict.current_revision",
current_binding_id: optionalIdentity(
response.current_binding_id,
"Runtime trust conflict.current_binding_id",
),
current_fingerprint: optionalNullableString(
response.current_fingerprint,
@@ -867,16 +866,6 @@ export function parseRuntimeRemovalOperationResponse(
};
}
function revisionForJson(revision: number | null): number | null {
if (revision === null) return null;
if (!Number.isSafeInteger(revision) || revision < 1) {
throw new RuntimeTrustRequestError(
"Runtime trust revision is not a safe integer",
);
}
return revision;
}
async function readBoundedJson(response: Response): Promise<unknown> {
const contentLength = response.headers.get("content-length");
if (contentLength !== null) {
@@ -959,22 +948,25 @@ function requestErrorFrom(
}
}
function throwRuntimeTrustConflict(payload: unknown): never {
let conflict: RuntimeTrustConflictResponse;
try {
conflict = parseRuntimeTrustConflict(payload);
} catch {
throw new RuntimeTrustRequestError(
"Runtime trust conflict response was invalid",
);
}
throw new RuntimeTrustConflictError(conflict);
}
async function finishMutation(
response: Response,
workspaceId: string,
runtimeId: string,
): Promise<WorkspaceRuntimeDetail> {
const payload = await readBoundedJson(response);
if (response.status === 409) {
try {
throw new RuntimeTrustConflictError(parseRuntimeTrustConflict(payload));
} catch (error) {
if (error instanceof RuntimeTrustConflictError) throw error;
throw new RuntimeTrustRequestError(
"Runtime trust conflict response was invalid",
);
}
}
if (response.status === 409) throwRuntimeTrustConflict(payload);
if (!response.ok) throw requestErrorFrom(payload, response.status);
let detail: WorkspaceRuntimeDetail;
try {
@@ -998,15 +990,17 @@ export async function createRemoteRuntime(
request: CreateRemoteRuntimeRequest,
fetchImpl: typeof fetch = fetch,
): Promise<WorkspaceRuntimeResource> {
const enrollmentId = boundedIdentity(request.workspace_trust_id, "workspace_trust_id");
const response = await fetchImpl(
workspaceApiPath(workspaceId, "/runtimes"),
{
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(request),
body: JSON.stringify({ ...request, workspace_trust_id: enrollmentId }),
},
);
const payload = await readBoundedJson(response);
if (response.status === 409) throwRuntimeTrustConflict(payload);
if (!response.ok) throw requestErrorFrom(payload, response.status);
const runtime = runtimeResource(payload, "Runtime create response");
if (runtime.runtime_id !== request.public_bundle.identity_id) {
@@ -1173,7 +1167,10 @@ export async function revokeRuntimeTrustKey(
method: "DELETE",
headers: { "content-type": "application/json" },
body: JSON.stringify({
expected_revision: revisionForJson(request.expected_revision),
expected_binding_id: boundedIdentity(
request.expected_binding_id,
"expected_binding_id",
),
}),
},
);
@@ -406,7 +406,6 @@ export function parseRuntimeCleanupPlan(
"workspace_id",
"runtime_id",
"generated_at",
"revision",
"digest",
"workers",
"workdirs",
@@ -428,7 +427,6 @@ export function parseRuntimeCleanupPlan(
item.generated_at,
"Runtime cleanup plan.generated_at",
),
revision: boundedString(item.revision, "Runtime cleanup plan.revision"),
digest: boundedString(item.digest, "Runtime cleanup plan.digest"),
workers: array(item.workers, "Runtime cleanup plan.workers", MAX_CANDIDATES)
.map(
@@ -151,7 +151,7 @@ function ticketDetailFixture() {
},
id: "ticket-id",
implementation_reports: [],
item_revision: "revision-1",
content_digest: "a".repeat(64),
linked_objectives: [],
merge_request: null,
merge_requests: [],
@@ -410,7 +410,7 @@ Deno.test("Ticket Browser parser validates the canonical Merge Request thread",
event_id: "event-1",
sequence: 1,
subject_ref: "abc123",
ticket_item_revision: "ticket-revision",
ticket_content_digest: "b".repeat(64),
ticket_merge_request_subjects: [
{ merge_request_id: "mr-1", subject_ref: "abc123" },
],
@@ -737,7 +737,7 @@ export function parseTicketDetail(value: unknown): TicketDetail {
"evidence",
"id",
"implementation_reports",
"item_revision",
"content_digest",
"linked_objectives",
"merge_request",
"merge_requests",
@@ -792,7 +792,7 @@ export function parseTicketDetail(value: unknown): TicketDetail {
`${label}.implementation_reports`,
parseTicketEvidenceEvent,
),
item_revision: string(item.item_revision, `${label}.item_revision`),
content_digest: string(item.content_digest, `${label}.content_digest`),
linked_objectives: array(
item.linked_objectives,
`${label}.linked_objectives`,
@@ -986,7 +986,6 @@ export function parseObjectiveDetail(value: unknown): ObjectiveDetail {
"record_source",
"resource_key",
"resources",
"revision",
"state",
"title",
"updated_at",
@@ -1011,7 +1010,6 @@ export function parseObjectiveDetail(value: unknown): ObjectiveDetail {
`${label}.resources`,
parseObjectiveResource,
),
revision: string(item.revision, `${label}.revision`),
state: string(item.state, `${label}.state`),
title: string(item.title, `${label}.title`),
updated_at: optionalNullableString(item.updated_at, `${label}.updated_at`),
@@ -1138,7 +1136,7 @@ function parseMergeRequestThreadEvent(
"requested_by",
"reviewer",
"subject_ref",
"ticket_item_revision",
"ticket_content_digest",
"ticket_merge_request_subjects",
],
label,
@@ -1152,9 +1150,9 @@ function parseMergeRequestThreadEvent(
),
reviewer: parseMergeRequestWorker(item.reviewer, `${label}.reviewer`),
subject_ref: string(item.subject_ref, `${label}.subject_ref`),
ticket_item_revision: string(
item.ticket_item_revision,
`${label}.ticket_item_revision`,
ticket_content_digest: string(
item.ticket_content_digest,
`${label}.ticket_content_digest`,
),
ticket_merge_request_subjects: array(
item.ticket_merge_request_subjects,
@@ -1172,7 +1170,7 @@ function parseMergeRequestThreadEvent(
"request_event_id",
"reviewer",
"subject_ref",
"ticket_item_revision",
"ticket_content_digest",
"ticket_merge_request_subjects",
], label);
const decision = string(item.decision, `${label}.decision`);
@@ -1191,9 +1189,9 @@ function parseMergeRequestThreadEvent(
),
reviewer: parseMergeRequestWorker(item.reviewer, `${label}.reviewer`),
subject_ref: string(item.subject_ref, `${label}.subject_ref`),
ticket_item_revision: string(
item.ticket_item_revision,
`${label}.ticket_item_revision`,
ticket_content_digest: string(
item.ticket_content_digest,
`${label}.ticket_content_digest`,
),
ticket_merge_request_subjects: array(
item.ticket_merge_request_subjects,
@@ -14,7 +14,6 @@ export const currentInitialRepository = {
provider: "git",
source: { kind: "https", uri: "https://example.test/current.git" },
default_ref: "main",
source_revision: 2,
source_fingerprint: "sha256:current",
observed_status: "unverified",
observed_at: null,
@@ -26,7 +25,6 @@ export function creationResponse(repository: unknown = null, replayed = false) {
return {
workspace: createdWorkspace,
repository,
config_revision: 1,
request_fingerprint: "sha256:creation",
replayed,
};
@@ -258,7 +258,6 @@ function repositorySummary(value: unknown, path: string): RepositorySummary {
"kind",
"provider",
"source",
"source_revision",
"source_fingerprint",
"observed_status",
"observed_at",
@@ -291,10 +290,6 @@ function repositorySummary(value: unknown, path: string): RepositorySummary {
kind: repositoryString(item.kind, `${path}.kind`),
provider: repositoryString(item.provider, `${path}.provider`),
source: repositorySource(item.source, `${path}.source`),
source_revision: repositorySourceRevision(
item.source_revision,
`${path}.source_revision`,
),
source_fingerprint: repositoryString(
item.source_fingerprint,
`${path}.source_fingerprint`,
@@ -360,7 +355,6 @@ function workspaceRepositoryRecord(
"provider",
"source",
"default_ref",
"source_revision",
"source_fingerprint",
"observed_status",
"observed_at",
@@ -383,10 +377,6 @@ function workspaceRepositoryRecord(
provider: nullableString(item.provider, `${path}.provider`),
source: repositorySource(item.source, `${path}.source`),
default_ref: nullableString(item.default_ref, `${path}.default_ref`),
source_revision: repositorySourceRevision(
item.source_revision,
`${path}.source_revision`,
),
source_fingerprint: repositoryString(
item.source_fingerprint,
`${path}.source_fingerprint`,
@@ -558,7 +548,6 @@ export function parseWorkspaceCreateResponse(
[
"workspace",
"repository",
"config_revision",
"request_fingerprint",
"replayed",
],
@@ -573,10 +562,6 @@ export function parseWorkspaceCreateResponse(
response.repository,
"workspace create response.repository",
),
config_revision: integer(
response.config_revision,
"workspace create response.config_revision",
),
request_fingerprint: string(
response.request_fingerprint,
"workspace create response.request_fingerprint",
@@ -789,7 +774,7 @@ export function parseRepositorySshConnectionProbeResponse(
"port",
"trust_state",
"host_trust_id",
"expected_host_trust_revision",
"expected_host_key_fingerprint",
"candidates",
],
"repository SSH connection probe response",
@@ -829,11 +814,10 @@ export function parseRepositorySshConnectionProbeResponse(
response.host_trust_id,
"repository SSH connection probe response.host_trust_id",
),
expected_host_trust_revision: response.expected_host_trust_revision === null
? null
: integer(
response.expected_host_trust_revision,
"repository SSH connection probe response.expected_host_trust_revision",
expected_host_key_fingerprint:
response.expected_host_key_fingerprint === null ? null : string(
response.expected_host_key_fingerprint,
"repository SSH connection probe response.expected_host_key_fingerprint",
),
candidates: array(
response.candidates,
@@ -980,7 +964,7 @@ export function parseWorkspaceDeletionPreflightResponse(
exactKeys(item, [
"workspace_id",
"display_name",
"expected_revision",
"expected_workspace_updated_at",
"can_delete",
"resources",
"blockers",
@@ -994,9 +978,9 @@ export function parseWorkspaceDeletionPreflightResponse(
item.display_name,
"Workspace deletion preflight.display_name",
),
expected_revision: deletionBoundedString(
item.expected_revision,
"Workspace deletion preflight.expected_revision",
expected_workspace_updated_at: deletionBoundedString(
item.expected_workspace_updated_at,
"Workspace deletion preflight.expected_workspace_updated_at",
WORKSPACE_DELETION_MAX_REVISION_BYTES,
),
can_delete: boolean(
@@ -1116,7 +1100,8 @@ export function parseHostListResponse(value: unknown): HostListResponse {
),
limit,
items: repositoryArray(response.items, "host list response.items").map(
(item, index) => parseHostSummary(item, `host list response.items[${index}]`),
(item, index) =>
parseHostSummary(item, `host list response.items[${index}]`),
),
source: repositoryString(response.source, "host list response.source"),
diagnostics: repositoryArray(
@@ -10,6 +10,9 @@ const toolchain = vi.hoisted(() => ({
analyze: vi.fn(),
complete: vi.fn(),
close: vi.fn(),
applyChanges: vi.fn(),
changesBetween: vi.fn(),
format: vi.fn(),
}));
vi.mock("./toolchain.ts", () => ({
ConfigSourceToolchain: class {
@@ -20,13 +23,15 @@ vi.mock("./toolchain.ts", () => ({
analyze = toolchain.analyze;
complete = toolchain.complete;
close = toolchain.close;
applyChanges = toolchain.applyChanges;
changesBetween = toolchain.changesBetween;
format = toolchain.format;
},
}));
const schemaSource = "schema body\n".repeat(6000);
const tree = {
snapshot: {
revision: 7,
digest: "sha256:tree",
entries: {
"main.dcdl": {
@@ -219,7 +224,6 @@ test("unmount during schema setup closes the toolchain", async () => {
test("builtin diagnostics show read-only paths without adding editable workspace files", async () => {
toolchain.analyze.mockResolvedValue([{
path: "$builtin/profiles/companion.dcdl",
revision: 7,
tree_digest: "sha256:tree",
kind: "constraint_violation",
span: { start_byte: 1, end_byte: 4 },
@@ -243,7 +247,6 @@ test("builtin diagnostics show read-only paths without adding editable workspace
test("unknown builtin diagnostics retain the workspace importer path", async () => {
toolchain.analyze.mockResolvedValue([{
path: "main.dcdl",
revision: 7,
tree_digest: "sha256:tree",
kind: "import",
span: { start_byte: 8, end_byte: 40 },
@@ -261,6 +264,36 @@ test("unknown builtin diagnostics retain the workspace importer path", async ()
expect(view.getByRole("navigation", { name: "Virtual configuration paths" }).textContent).not.toContain("$builtin/");
});
test.each(["resolve", "reject"] as const)(
"superseded analysis %s cannot replace the current diagnostics or status",
async (settlement) => {
let resolveOld!: (value: unknown[]) => void;
let rejectOld!: (error: Error) => void;
const oldAnalysis = new Promise<unknown[]>((resolve, reject) => {
resolveOld = resolve;
rejectOld = reject;
});
toolchain.analyze.mockReturnValueOnce(oldAnalysis);
const view = renderEditor();
await expectReady(view);
await waitFor(() => expect(toolchain.analyze).toHaveBeenCalledOnce());
await fireEvent.click(view.getByRole("button", { name: "Create local source" }));
await waitFor(() => expect(toolchain.analyze).toHaveBeenCalledTimes(2));
if (settlement === "resolve") {
resolveOld([{
path: "main.dcdl", tree_digest: tree.snapshot.digest, kind: "import", span: { start_byte: 0, end_byte: 1 },
message: "Stale diagnostic", labels: [], notes: [],
}]);
} else rejectOld(new Error("Stale analysis failure"));
await oldAnalysis.catch(() => {});
await waitFor(() => {
expect(view.container.textContent).toContain("Creating local source module.dcdl");
expect(view.container.textContent).not.toContain("Stale diagnostic");
expect(view.container.textContent).not.toContain("Stale analysis failure");
});
},
);
test("unmount during fetch does not start a toolchain", async () => {
let finishFetch!: (response: Response) => void;
fetcher.mockReturnValueOnce(
@@ -274,3 +307,70 @@ test("unmount during fetch does not start a toolchain", async () => {
await new Promise((resolve) => setTimeout(resolve, 0));
expect(toolchain.construct).not.toHaveBeenCalled();
});
test("diagnostics from another tree digest are not displayed", async () => {
toolchain.analyze.mockResolvedValue([
{
path: "main.dcdl", tree_digest: "sha256:old-tree", kind: "syntax",
span: { start_byte: 0, end_byte: 1 }, message: "Wrong tree diagnostic",
labels: [], notes: [],
},
{
path: "main.dcdl", tree_digest: tree.snapshot.digest, kind: "syntax",
span: { start_byte: 0, end_byte: 1 }, message: "Current tree diagnostic",
labels: [], notes: [],
},
]);
const view = renderEditor();
await expectReady(view);
await waitFor(() => expect(view.container.textContent).toContain("Current tree diagnostic"));
expect(view.container.textContent).not.toContain("Wrong tree diagnostic");
});
test.each([409, 422])("failed commit %s preserves the local draft and authoritative base digest for retry", async (status) => {
const change = { kind: "create", path: "module.dcdl", content_type: "decodal", content: "{}\n" };
const candidate = {
digest: "sha256:local-draft",
entries: {
...tree.snapshot.entries,
"module.dcdl": {
path: change.path,
content_type: change.content_type,
content: change.content,
content_digest: "sha256:module",
},
},
};
toolchain.applyChanges.mockResolvedValue(candidate);
toolchain.changesBetween.mockResolvedValue([change]);
toolchain.format.mockImplementation(async (source: string) => source);
const view = renderEditor();
await expectReady(view);
await fireEvent.click(view.getByRole("button", { name: "Create local source" }));
const commit = view.getByRole("button", { name: "Commit" });
expect(commit).toBeInstanceOf(HTMLButtonElement);
fetcher.mockResolvedValueOnce(new Response(
status === 409 ? "base content digest mismatch" : "candidate evaluation failed",
{ status },
));
await fireEvent.click(commit);
await waitFor(() => expect(view.container.textContent).toContain(
status === 409 ? "base content digest mismatch" : "candidate evaluation failed",
));
const expectedRequest = {
base_digest: tree.snapshot.digest, changes: [change], entrypoints: ["main.dcdl"],
};
expect(JSON.parse(fetcher.mock.calls[1][1].body)).toEqual(expectedRequest);
expect(view.container.querySelector(".cm-content")?.textContent).toContain("{}");
expect(commit).toHaveProperty("disabled", false);
expect(toolchain.setSnapshot).toHaveBeenCalledOnce();
if (status === 409) expect(view.getByRole("alert").textContent).toContain("Discard local candidate");
fetcher.mockResolvedValueOnce(new Response(JSON.stringify({ ...tree, snapshot: candidate })));
await fireEvent.click(commit);
await waitFor(() => expect(view.container.textContent).toContain("Committed formatted source tree"));
expect(JSON.parse(fetcher.mock.calls[2][1].body)).toEqual(expectedRequest);
expect(toolchain.setSnapshot).toHaveBeenLastCalledWith(candidate, tree.contract.schema_bundle);
expect(commit).toHaveProperty("disabled", true);
});
@@ -25,14 +25,13 @@
let mounted = false;
let busy = $state(false);
let workingChanges = $state<ConfigTreeChange[]>([]);
let baseRevision = $state(0);
let baseDigest = $state("");
let renamePath = $state("");
let baseSnapshot = $state.raw<WorkspaceConfigTreeResponse["snapshot"] | null>(null);
let conflict = $state(false);
let toolchain = $state.raw<ConfigSourceToolchain | null>(null);
let analysisReady = $state(false);
let analysisGeneration = 0;
let analysisRequest: AbortController | null = null;
const paths = $derived(
treeState ? Object.keys(treeState.snapshot.entries).toSorted() : [],
@@ -48,6 +47,7 @@
void reload();
return () => {
mounted = false;
analysisRequest?.abort();
toolchain?.close();
};
});
@@ -57,20 +57,23 @@
const path = selectedPath;
const value = source;
const ready = analysisReady;
const generation = ++analysisGeneration;
const digest = treeState?.snapshot.digest;
analysisRequest?.abort();
const request = new AbortController();
analysisRequest = request;
diagnostics = [];
if (!analyzer || !path || !ready) return;
const timer = setTimeout(() => {
void analyzer.analyze(path, value).then((result) => {
if (generation === analysisGeneration) diagnostics = result;
if (!request.signal.aborted) diagnostics = result.filter((diagnostic) => diagnostic.tree_digest === digest);
}).catch((error) => {
if (generation === analysisGeneration) status = `Analyze failed: ${String(error)}`;
if (!request.signal.aborted) status = `Analyze failed: ${String(error)}`;
});
}, 250);
return () => {
clearTimeout(timer);
if (generation === analysisGeneration) analysisGeneration += 1;
request.abort();
};
});
@@ -78,7 +81,7 @@
loadState = "loading";
status = "Loading source tree…";
analysisReady = false;
analysisGeneration += 1;
analysisRequest?.abort();
try {
const remote = await fetchConfigTree(workspaceId);
if (!mounted) return;
@@ -94,14 +97,13 @@
}
source = selectedPath ? treeState.snapshot.entries[selectedPath].content : "";
baseSnapshot = $state.snapshot(treeState.snapshot);
baseRevision = treeState.snapshot.revision;
baseDigest = treeState.snapshot.digest;
analysisReady = true;
workingChanges = [];
renamePath = selectedPath;
diagnostics = [];
conflict = false;
status = `Revision ${treeState.snapshot.revision} · ${treeState.snapshot.digest.slice(0, 20)}…`;
status = `Tree digest ${treeState.snapshot.digest.slice(0, 20)}…`;
loadState = "ready";
} catch (error) {
if (!mounted) return;
@@ -197,7 +199,7 @@
function recordCommitError(error: unknown) {
const message = String(error);
conflict = message.includes("conflict") || message.includes("base revision/digest mismatch");
conflict = message.includes("conflict") || message.includes("base content digest mismatch");
status = conflict ? `${message} Reload the authoritative tree before editing again.` : message;
}
@@ -211,7 +213,6 @@
return;
}
treeState = await commitConfigTree(workspaceId, {
base_revision: baseRevision,
base_digest: baseDigest,
changes: workingChanges,
entrypoints: entrypoints(),
@@ -219,12 +220,11 @@
workingChanges = [];
conflict = false;
baseSnapshot = $state.snapshot(treeState.snapshot);
baseRevision = treeState.snapshot.revision;
baseDigest = treeState.snapshot.digest;
await toolchain?.setSnapshot(treeState.snapshot, treeState.contract.schema_bundle);
source = treeState.snapshot.entries[selectedPath]?.content ?? "";
diagnostics = [];
status = `Committed formatted revision ${treeState.snapshot.revision}.`;
status = `Committed formatted source tree · ${treeState.snapshot.digest.slice(0, 20)}…`;
} catch (error) {
recordCommitError(error);
} finally {
@@ -241,7 +241,6 @@
const localChanges = [...workingChanges];
const remote = await fetchConfigTree(workspaceId);
baseSnapshot = structuredClone(remote.snapshot);
baseRevision = remote.snapshot.revision;
baseDigest = remote.snapshot.digest;
await toolchain.setSnapshot(remote.snapshot, remote.contract.schema_bundle);
try {
@@ -251,10 +250,10 @@
selectedPath = candidate.entries[selectedPath] ? selectedPath : Object.keys(candidate.entries).toSorted()[0] ?? "";
source = selectedPath ? candidate.entries[selectedPath].content : "";
conflict = false;
status = "Local changes reapplied to the latest revision. Commit to persist them.";
status = "Local changes reapplied to the latest source tree. Commit to persist them.";
} catch (error) {
conflict = true;
status = `Local changes conflict with the latest revision: ${String(error)}. Discard local changes or resolve against a fresh reload.`;
status = `Local changes conflict with the latest source tree: ${String(error)}. Discard local changes or resolve against a fresh reload.`;
}
}
@@ -241,7 +241,7 @@ function parseToolchainContract(value: unknown): ToolchainContract {
}
export function parseConfigTreeSnapshot(value: unknown): ConfigTreeSnapshot {
const item = record(value, ["revision", "digest", "entries"]);
const item = record(value, ["digest", "entries"]);
const entriesRecord = objectValue(item.entries);
const entries = Object.entries(entriesRecord);
if (entries.length > MAX_ENTRY_COUNT) throw schemaError();
@@ -253,7 +253,6 @@ export function parseConfigTreeSnapshot(value: unknown): ConfigTreeSnapshot {
parsedEntries[boundedPath] = parsed;
}
return {
revision: safeInteger(item.revision),
digest: boundedString(item.digest, MAX_DIGEST_BYTES),
entries: parsedEntries,
};
@@ -299,13 +298,13 @@ export async function fetchConfigEntry(
);
}
export async function fetchConfigRevision(
export async function fetchConfigHistory(
workspaceId: string,
revision: number,
contentDigest: string,
fetcher: typeof fetch = fetch,
): Promise<ConfigTreeSnapshot> {
return await readJson(
await fetcher(`${sourceTreeUrl(workspaceId)}/revisions/${revision}`, {
await fetcher(`${sourceTreeUrl(workspaceId)}/history/${encodeURIComponent(contentDigest)}`, {
headers: { accept: "application/json" },
}),
parseConfigTreeSnapshot,
@@ -1,5 +0,0 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { ConfigTreeChange } from "./ConfigTreeChange";
import type { VirtualPath } from "./VirtualPath";
export type ConfigCommitRequest = { base_revision: number, base_digest: string, changes: Array<ConfigTreeChange>, entrypoints: Array<VirtualPath>, };
@@ -3,4 +3,4 @@ import type { ConfigDiagnosticLabel } from "./ConfigDiagnosticLabel";
import type { ConfigSpan } from "./ConfigSpan";
import type { VirtualPath } from "./VirtualPath";
export type ConfigDiagnostic = { path: VirtualPath, revision: number, tree_digest: string, kind: string, span: ConfigSpan, message: string, labels: Array<ConfigDiagnosticLabel>, notes: Array<string>, };
export type ConfigDiagnostic = { path: VirtualPath, tree_digest: string, kind: string, span: ConfigSpan, message: string, labels: Array<ConfigDiagnosticLabel>, notes: Array<string>, };
@@ -2,4 +2,11 @@
import type { ConfigEntry } from "./ConfigEntry";
import type { VirtualPath } from "./VirtualPath";
export type ConfigTreeSnapshot = { revision: number, digest: string, entries: { [key in VirtualPath]: ConfigEntry }, };
export type ConfigTreeSnapshot = {
/**
* SHA-256 of ordered virtual paths, content types and complete source bytes.
* Identifies tree content, not an edit sequence or freshness counter.
*/
digest: string;
entries: { [key in VirtualPath]: ConfigEntry };
};
@@ -15,7 +15,7 @@ function reconnectSnapshot(text: string): Event {
session: {
entries: [],
pending_submissions: {
revision: 0,
head_id: null,
notification_count: 0,
submissions: [],
@@ -1506,7 +1506,7 @@ Deno.test("legacy compaction lifecycle events are inert", () => {
const running = {
schema_version: 2,
compaction_id: "compaction-1",
revision: 1,
internal_worker: null,
state: "running",
started_at_ms: 1_000,
@@ -1530,7 +1530,7 @@ Deno.test("legacy compaction lifecycle events are inert", () => {
data: {
lifecycle: {
...running,
revision: 2,
state: "done",
ended_at_ms: 4_000,
summary: "accepted summary",
@@ -1546,7 +1546,7 @@ Deno.test("legacy compaction lifecycle events are inert", () => {
data: {
lifecycle: {
...running,
revision: 3,
state: "failed",
ended_at_ms: 5_000,
error: "legacy failure",
@@ -1580,7 +1580,7 @@ Deno.test("snapshot restores running compaction without staged content", () => {
parent_session_id: "parent-session",
kind: { service: { kind: "compaction" } },
},
revision: 1,
session: canonicalSession([]),
status: "running",
in_flight: {
@@ -1678,7 +1678,7 @@ Deno.test("snapshot does not guess between multiple compaction services", () =>
parent_session_id: "parent-session",
kind: { service: { kind: "compaction" } },
},
revision: 1,
session: canonicalSession([]),
status: "running" as const,
in_flight: {
@@ -1737,7 +1737,7 @@ Deno.test("live compaction keeps its first service binding until terminal progre
event: "internal_worker",
data: {
worker: workerA,
revision: 1,
event: {
event: "tool_call_start",
data: { id: "read-a", name: "ReadEntry" },
@@ -1751,7 +1751,7 @@ Deno.test("live compaction keeps its first service binding until terminal progre
event: "internal_worker",
data: {
worker: workerB,
revision: 1,
event: {
event: "tool_call_start",
data: { id: "search-b", name: "SearchSessionEntries" },
@@ -1797,7 +1797,7 @@ Deno.test("compaction summary candidate stays private", () => {
event: "internal_worker",
data: {
worker,
revision: 1,
event: {
event: "tool_call_start",
data: { id: "call-1", name: "write_summary" },
@@ -1811,7 +1811,7 @@ Deno.test("compaction summary candidate stays private", () => {
event: "internal_worker",
data: {
worker,
revision: 2,
event: {
event: "tool_call_done",
data: {
@@ -1866,7 +1866,7 @@ Deno.test("runtime compaction progress nests service activity and clears it", ()
event: "internal_worker",
data: {
worker,
revision: 1,
event: {
event: "tool_call_done",
data: {
@@ -1883,7 +1883,7 @@ Deno.test("runtime compaction progress nests service activity and clears it", ()
event: "internal_worker",
data: {
worker,
revision: 2,
event: {
event: "tool_call_done",
data: {
@@ -1915,7 +1915,7 @@ Deno.test("runtime compaction progress nests service activity and clears it", ()
eventId: "compaction-service-removed",
event: {
event: "internal_worker_removed",
data: { worker, revision: 3 },
data: { worker },
} satisfies Event,
}]);
assertEquals(projection.internalWorkers, []);
@@ -1946,7 +1946,7 @@ Deno.test("snapshot excludes storage-only compaction extension records", () => {
payload: {
schema_version: 2,
compaction_id: "compaction-orphaned",
revision: 2,
internal_worker: {
session_id: "missing-service",
name: "Compaction",
@@ -2023,7 +2023,7 @@ Deno.test("back-to-back compactions do not retain prior activity", () => {
event: "internal_worker",
data: {
worker,
revision: 1,
event: {
event: "tool_call_done",
data: { id: "read-1", name: "ReadEntry", arguments: "{}" },
@@ -2035,7 +2035,7 @@ Deno.test("back-to-back compactions do not retain prior activity", () => {
eventId: "first-removed",
event: {
event: "internal_worker_removed",
data: { worker, revision: 2 },
data: { worker },
} satisfies Event,
},
{
@@ -3025,7 +3025,7 @@ Deno.test("projectConsole mirrors live TaskCreate and TaskUpdate calls", () => {
}]);
});
Deno.test("Internal Worker output stays separate and revision-fenced", () => {
Deno.test("Internal Worker output stays separate and follows parent stream arrival order", () => {
const worker = {
session_id: "child-session",
name: "research",
@@ -3039,7 +3039,7 @@ Deno.test("Internal Worker output stays separate and revision-fenced", () => {
event: "internal_worker",
data: {
worker,
revision: 2,
event: { event: "text_done", data: { text: "child output" } },
},
},
@@ -3057,12 +3057,12 @@ Deno.test("Internal Worker output stays separate and revision-fenced", () => {
event: "internal_worker",
data: {
worker,
revision: 1,
event: { event: "text_done", data: { text: "stale" } },
event: { event: "text_done", data: { text: "next output" } },
},
},
}]);
assertEquals(projection.internalWorkers[0].console.lines.length, 1);
assertEquals(projection.internalWorkers[0].console.lines.map((line) => line.body), ["child output", "next output"]);
const views = consoleWorkerViews(projection);
assertEquals(views.map((view) => [view.sessionId, view.label]), [
@@ -3095,7 +3095,7 @@ Deno.test("live-created Internal Worker receives own metadata without reconnect"
eventId: "child-snapshot",
event: {
event: "internal_worker",
data: { worker, revision: 1, event: childSnapshot },
data: { worker, event: childSnapshot },
},
},
{
@@ -3104,7 +3104,7 @@ Deno.test("live-created Internal Worker receives own metadata without reconnect"
event: "internal_worker",
data: {
worker,
revision: 2,
event: {
event: "context_usage",
data: { usage: { tokens: 14_000, source: "estimated" } },
@@ -3137,7 +3137,7 @@ Deno.test("Internal Worker snapshot output continues without entering the parent
};
snapshot.data.internal_workers = [{
worker,
revision: 4,
session: canonicalSession([]),
greeting: {
worker_name: "research",
@@ -3185,7 +3185,7 @@ Deno.test("Internal Worker snapshot output continues without entering the parent
event: "internal_worker",
data: {
worker,
revision: 5,
event: { event: "text_delta", data: { text: "ld**" } },
},
},
@@ -3216,7 +3216,7 @@ Deno.test("console Worker views expose only direct Internal Workers", () => {
parent_session_id: "parent-session",
kind: "sub_worker",
},
revision: 1,
event: {
event: "internal_worker",
data: {
@@ -3226,7 +3226,7 @@ Deno.test("console Worker views expose only direct Internal Workers", () => {
parent_session_id: "child-session",
kind: "sub_worker",
},
revision: 1,
event: {
event: "worker_state",
data: { snapshot: workerState("running") },
@@ -3248,7 +3248,7 @@ Deno.test("console Worker views expose only direct Internal Workers", () => {
parent_session_id: "parent-session",
kind: "sub_worker",
},
revision: 1,
event: {
event: "worker_state",
data: { snapshot: workerState("idle") },
@@ -3298,7 +3298,7 @@ Deno.test("parent snapshot authoritatively replaces Internal Worker projections"
parent_session_id: "parent-session",
kind: "sub_worker",
},
revision: 4,
session: canonicalSession([{
kind: "assistant_item",
ts: 1,
@@ -3343,7 +3343,7 @@ Deno.test("parent snapshot authoritatively replaces Internal Worker projections"
parent_session_id: "parent-session",
kind: "sub_worker",
},
revision: 1,
event: {
event: "worker_state",
data: { snapshot: workerState("running") },
@@ -3384,12 +3384,12 @@ Deno.test("terminal Internal Worker removal drops descendants and fences late ev
event: "internal_worker",
data: {
worker,
revision: 2,
event: {
event: "internal_worker",
data: {
worker: nestedWorker,
revision: 1,
event: { event: "text_done", data: { text: "nested" } },
},
},
@@ -3406,7 +3406,7 @@ Deno.test("terminal Internal Worker removal drops descendants and fences late ev
eventId: "removed",
event: {
event: "internal_worker_removed",
data: { worker, revision: 3 },
data: { worker },
},
}, {
eventId: "late",
@@ -3414,7 +3414,7 @@ Deno.test("terminal Internal Worker removal drops descendants and fences late ev
event: "internal_worker",
data: {
worker,
revision: 4,
event: { event: "text_done", data: { text: "must stay removed" } },
},
},
@@ -3427,7 +3427,7 @@ Deno.test("terminal Internal Worker removal drops descendants and fences late ev
assertEquals(projection.removedInternalWorkers, {});
});
Deno.test("stale Internal Worker removal cannot discard a newer projection", () => {
Deno.test("Internal Worker removal follows arrival order without a synthetic freshness counter", () => {
const worker = {
session_id: "child-session",
name: "child",
@@ -3441,21 +3441,21 @@ Deno.test("stale Internal Worker removal cannot discard a newer projection", ()
event: "internal_worker",
data: {
worker,
revision: 4,
event: { event: "text_done", data: { text: "current" } },
},
},
}]);
const projection = projector.append([{
eventId: "stale-removal",
eventId: "removal",
event: {
event: "internal_worker_removed",
data: { worker, revision: 3 },
data: { worker },
},
}]);
assertEquals(projection.internalWorkers.length, 1);
assertEquals(projection.internalWorkers[0].revision, 4);
assertEquals(projection.internalWorkers, []);
assertEquals(projection.removedInternalWorkers, { "child-session": true });
});
Deno.test("snapshot restores TaskStore state from system history", () => {
@@ -3465,7 +3465,7 @@ Deno.test("snapshot restores TaskStore state from system history", () => {
if (event.event !== "snapshot") throw new Error("snapshot fixture expected");
event.data.session = {
pending_submissions: {
revision: 0,
notification_count: 0,
head_id: null,
submissions: [],
@@ -3717,7 +3717,7 @@ Deno.test("snapshot entry replay is idempotent by identity, not text, including
const child = { session_id: "child-session", name: "reviewer", kind: "sub_worker" as const };
const snapshot = snapshotEvent("/repo") as SnapshotEvent;
snapshot.data.internal_workers = [{
worker: child, revision: 5,
worker: child,
session: { ...snapshot.data.session, entries }, status: "idle",
in_flight: { blocks: [] }, internal_workers: [],
}];
@@ -3725,13 +3725,13 @@ Deno.test("snapshot entry replay is idempotent by identity, not text, including
for (const [index, entry] of [user, assistant, system].entries()) {
events.push({ eventId: `parent-${index}`, event: {
event: "internal_worker", data: {
worker: child, revision: 6 + index,
worker: child,
event: { event: "session_entry_committed", data: { entry } },
},
} });
}
events.push({ eventId: "replayed-user", event: {
event: "internal_worker", data: { worker: child, revision: 9, event: {
event: "internal_worker", data: { worker: child, event: {
event: "user_message", data: { entry_id: user.entry_id, segments: user.segments },
} },
} });
@@ -114,7 +114,6 @@ export type ConsoleLine = {
export type InternalWorkerProjection = {
worker: InternalWorkerRef;
revision: number;
console: ConsoleProjection;
};
@@ -194,7 +193,7 @@ export type ConsoleProjection = {
lastEventId: string | null;
internalWorkers: InternalWorkerProjection[];
/** Terminal child-session fences, reset only by an authoritative snapshot. */
removedInternalWorkers: Record<string, number>;
removedInternalWorkers: Record<string, true>;
};
export type WorkerTarget = {
@@ -907,7 +906,7 @@ function projectInternalWorkerSnapshot(
if (snapshot.in_flight?.compaction) {
console = applyInFlightCompaction(console, snapshot.in_flight.compaction);
}
return { worker: snapshot.worker, revision: snapshot.revision, console };
return { worker: snapshot.worker, console };
}
const RUNTIME_COMPACTION_LINE_ID = "compaction-runtime";
@@ -1247,16 +1246,13 @@ export function applyProtocolEvent(
? next.internalWorkers[existingIndex]
: {
worker: event.data.worker,
revision: 0,
console: emptyConsoleProjection(),
};
if (event.data.revision <= existing.revision) break;
const updated: InternalWorkerProjection = {
worker: event.data.worker,
revision: event.data.revision,
console: applyProtocolEvent(existing.console, {
eventId:
`${envelope.eventId}:internal:${event.data.worker.session_id}:${event.data.revision}`,
`${envelope.eventId}:internal:${event.data.worker.session_id}`,
event: event.data.event,
observedAtMs: envelope.observedAtMs,
}),
@@ -1269,12 +1265,7 @@ export function applyProtocolEvent(
const existingIndex = next.internalWorkers.findIndex((worker) =>
worker.worker.session_id === event.data.worker.session_id
);
const existingRevision = existingIndex >= 0
? next.internalWorkers[existingIndex].revision
: 0;
if (event.data.revision <= existingRevision) break;
next.removedInternalWorkers[event.data.worker.session_id] =
event.data.revision;
next.removedInternalWorkers[event.data.worker.session_id] = true;
if (existingIndex >= 0) next.internalWorkers.splice(existingIndex, 1);
break;
}
@@ -163,7 +163,7 @@ Deno.test("new invoke and legacy snapshot reset run activity", () => {
entries: [],
greeting: { text: "", profile: "" },
state: {
revision: 0,
last_command_id: 0,
state: { kind: "idle" },
},
@@ -504,7 +504,7 @@ Deno.test("workspace Memory surfaces use read-only scoped memory APIs", async ()
subjectIndexPage.includes(">Subjects</h1>") &&
subjectIndexPage.includes("Worker connection") &&
subjectIndexPage.includes("subject.current_worker") &&
!subjectIndexPage.includes("subject.store_revision") &&
!subjectIndexPage.includes("subject.memory_fingerprint") &&
subjectIndexPage.includes('data-memory-view="subjects"'),
"Memory product entry should list explicit typed subjects",
);
@@ -513,7 +513,7 @@ Deno.test("workspace Memory surfaces use read-only scoped memory APIs", async ()
subjectPageLoad.includes("`${subjectPath}/memories`") &&
subjectPage.includes("DocumentMarkdown from") &&
subjectPage.includes("Resident context") &&
subjectPage.includes("Subject store revision") &&
subjectPage.includes("Memory fingerprint") &&
subjectPage.includes("Current Memories") &&
subjectPage.includes("surface?.availability === 'ready'") &&
subjectPage.includes("surface?.availability === 'ungenerated'") &&
@@ -524,15 +524,15 @@ Deno.test("workspace Memory surfaces use read-only scoped memory APIs", async ()
"Subject page should combine strict resident-surface and current-Memory reads without a nested vertical scroller",
);
assert(
memoryDetailLoad.includes("`${memoryPath}/revisions`") &&
memoryDetailLoad.includes("`${memoryPath}/changes`") &&
memoryDetailLoad.includes("parseSubjektivMemoryReadResponse") &&
memoryDetailPage.includes("Candidate provenance") &&
memoryDetailPage.includes("Source refs") &&
memoryDetailPage.includes("Derived from") &&
memoryDetailPage.includes("Revision history") &&
memoryDetailPage.includes("Change history") &&
memoryDetailPage.includes("DocumentMarkdown from") &&
!memoryDetailPage.includes("overflow-y"),
"Committed Memory detail should preserve bounded provenance and immutable revisions without mutation controls",
"Committed Memory detail should preserve bounded provenance and immutable changes without mutation controls",
);
});
@@ -58,8 +58,8 @@ test("A late image decode error cannot replace the newly selected image preview"
expect(revoke).toHaveBeenCalledWith("blob:http://localhost/image-1");
});
test("Refreshing metadata never silently rebases an open rename form onto another writer revision", async () => {
let revision = "1";
test("Refreshing metadata never silently rebases an open rename form onto another writer’s saved content", async () => {
let last_mutation_id = "1";
const mutations: DriveMutationRequest[] = [];
vi.stubGlobal(
"fetch",
@@ -71,7 +71,7 @@ test("Refreshing metadata never silently rebases an open rename form onto anothe
return Response.json({
code: "conflict",
classification: "not_committed",
message: "Revision conflict",
message: "Content changed",
}, { status: 409 });
}
if (url.pathname.endsWith("/list")) {
@@ -79,7 +79,7 @@ test("Refreshing metadata never silently rebases an open rename form onto anothe
}
if (url.pathname.endsWith("/read-text")) {
return Response.json({
entry: entry("2", "alpha", revision),
entry: entry("2", "alpha", last_mutation_id),
text: "new",
truncated: false,
});
@@ -88,7 +88,7 @@ test("Refreshing metadata never silently rebases an open rename form onto anothe
entry(
id,
"alpha",
id === "1" ? "1" : revision,
id === "1" ? "1" : last_mutation_id,
id === "1" ? "folder" : "file",
),
);
@@ -102,7 +102,7 @@ test("Refreshing metadata never silently rebases an open rename form onto anothe
await fireEvent.input(view.getByLabelText("Name"), {
target: { value: "my-name.txt" },
});
revision = "2";
last_mutation_id = "2";
await fireEvent.click(
view.getByRole("button", { name: "Refresh" }),
);
@@ -113,14 +113,14 @@ test("Refreshing metadata never silently rebases an open rename form onto anothe
),
).toBe(false)
);
expect(view.container.textContent).toContain("Expected revision 1.");
expect(view.container.textContent).toContain("Changes by another writer will not be overwritten.");
await fireEvent.click(
view.getByRole("button", { name: "Apply rename / move" }),
);
await waitFor(() => expect(mutations).toHaveLength(1));
expect(mutations[0].mutation).toMatchObject({
operation: "relocate",
expected_revision: "1",
expected_mutation_id: "1",
name: "my-name.txt",
id: { workspace_id: "alpha", node_id: "2" },
});
@@ -147,7 +147,7 @@ test("Folder picker blocks failed reads, filters files and self, and follows pag
return Response.json({
code: "conflict",
classification: "not_committed",
message: "Revision conflict",
message: "Content changed",
}, { status: 409 });
}
if (url.pathname.endsWith("/list")) {
@@ -209,7 +209,7 @@ test("Folder picker blocks failed reads, filters files and self, and follows pag
await waitFor(() => expect(mutations).toHaveLength(1));
expect(mutations[0].mutation).toMatchObject({
operation: "relocate",
expected_revision: "7",
expected_mutation_id: "7",
id: { workspace_id: "alpha", node_id: "2" },
parent: { workspace_id: "alpha", node_id: "5" },
});
@@ -170,13 +170,13 @@
if (!file) throw new Error('Choose a file.');
if (file.size > DRIVE_FILE_MAX_BYTES) throw new Error('Upload exceeds 16 MiB.');
requestId = await controller.upload(file, selectedForm === 'replace' ? {
operation: 'update', id: current.entry, expected_revision: current.revision, content_type: file.type || 'application/octet-stream',
operation: 'update', id: current.entry, expected_mutation_id: current.last_mutation_id, content_type: file.type || 'application/octet-stream',
} : {
operation: 'create', parent: current.entry, name: file.name, content_type: file.type || 'application/octet-stream',
});
} else if (selectedForm === 'relocate') {
if (!destination || destinationBusy || destinationError) throw new Error('Choose an available destination folder.');
requestId = await controller.mutate({ operation: 'relocate', id: current.entry, expected_revision: current.revision, parent: { workspace_id: workspaceId, node_id: parseDriveDecimal(parentId) }, name });
requestId = await controller.mutate({ operation: 'relocate', id: current.entry, expected_mutation_id: current.last_mutation_id, parent: { workspace_id: workspaceId, node_id: parseDriveDecimal(parentId) }, name });
} else {
requestId = await controller.mutate(selectedForm === 'folder' ? { operation: 'create_folder', parent: current.entry, name } : { operation: 'create_text', parent: current.entry, name, text: newText, content_type: 'text/markdown' });
}
@@ -193,7 +193,7 @@
if (!current || !canWrite || !current.parent) return;
if (!confirm(`Delete “${current.name}”? This cannot be undone. Folders must be empty; contents are never recursively deleted.`)) return;
await action(async () => {
await controller.mutate({ operation: 'delete', id: current.entry, expected_revision: current.revision });
await controller.mutate({ operation: 'delete', id: current.entry, expected_mutation_id: current.last_mutation_id });
// Stay on this stable URL; a subsequent lookup correctly reports NotFound.
if (controller.state.receipts.at(-1)?.state === 'committed') await controller.refresh();
});
@@ -283,7 +283,7 @@
<button class="primary" type="button" onclick={() => openForm('upload')}><DriveIcon name="upload" />Upload</button>
{/if}
{#if entry?.parent}
{#if entry.kind === 'file'}<a class="drive-button" href={driveDownloadUrl(workspaceId, entry.entry, entry.revision)} download={entry.name}><DriveIcon name="download" />Download</a>{/if}
{#if entry.kind === 'file'}<a class="drive-button" href={driveDownloadUrl(workspaceId, entry.entry, entry.last_mutation_id)} download={entry.name}><DriveIcon name="download" />Download</a>{/if}
{#if canEdit}<button class="primary" type="button" onclick={() => editing = !editing}><DriveIcon name="edit" />{editing ? 'View' : 'Edit text'}</button>{/if}
<details class="drive-menu" bind:open={actionsMenu}>
<summary class="icon-button" aria-label="More actions" title="More actions"><DriveIcon name="more" /></summary>
@@ -352,17 +352,17 @@
{:else if kind === 'markdown' || kind === 'text'}
{#if $controller.truncated}<p class="drive-state" role="status">Preview truncated at 64 KiB. Download the full file; partial content cannot be saved.</p>{/if}
{#if draft?.conflict}
<div class="drive-conflict" role="alert"><strong>Revision conflict — your draft is retained.</strong><p>Another writer changed this file. Refresh displays the latest content without changing your draft or its expected revision. Copy your draft before explicitly discarding it; it is never automatically retried.</p></div>
<div class="drive-conflict" role="alert"><strong>Content changed — your draft is retained.</strong><p>Another writer changed this file. Refresh displays the latest content without changing your draft or the saved content it was based on. Copy your draft before explicitly discarding it; it is never automatically retried.</p></div>
{/if}
<div class="drive-document" class:is-editing={editing && draft !== null}>
{#if editing && draft}
<div class="drive-editor">
<label>Draft (expected revision {draft.expectedRevision})
<label>Draft
<textarea value={draft.text} oninput={(event) => controller.edit(event.currentTarget.value)} readonly={denied} spellcheck="false"></textarea>
</label>
<div class="drive-toolbar">
{#if canWrite}<button class="primary" type="button" disabled={!dirty || draft.conflict || new TextEncoder().encode(draft.text).length > DRIVE_TEXT_MAX_BYTES} onclick={() => action(() => controller.save())}>Save</button>{/if}
<button type="button" disabled={unresolved} onclick={() => { if (confirm('Discard your draft and load the current Backend revision?')) void action(() => controller.discardDraft()); }}>Discard draft / load latest</button>
<button type="button" disabled={unresolved} onclick={() => { if (confirm('Discard your draft and load the latest saved content?')) void action(() => controller.discardDraft()); }}>Discard draft / load latest</button>
<span class="drive-note" role="status">{dirty ? 'Unsaved draft' : 'Saved content'} · {formatBytes(new TextEncoder().encode(draft.text).length)} / 64 KiB</span>
</div>
</div>
@@ -383,11 +383,11 @@
{/if}
</div>
{:else if kind === 'download'}
<div class="drive-empty drive-download-preview"><span class="drive-empty-icon"><DriveIcon name="download" /></span><h2>No preview available</h2><p class="drive-note">This file type is download-only. HTML, SVG and other active content are never rendered here.</p><a class="drive-button" href={driveDownloadUrl(workspaceId, entry.entry, entry.revision)} download={entry.name}>Download file</a></div>
<div class="drive-empty drive-download-preview"><span class="drive-empty-icon"><DriveIcon name="download" /></span><h2>No preview available</h2><p class="drive-note">This file type is download-only. HTML, SVG and other active content are never rendered here.</p><a class="drive-button" href={driveDownloadUrl(workspaceId, entry.entry, entry.last_mutation_id)} download={entry.name}>Download file</a></div>
{/if}
{#if entry.parent}
<details class="drive-details"><summary><DriveIcon name="info" />Details</summary>
<dl><div><dt>Updated</dt><dd>{formatDate(entry.updated_at)}</dd></div><div><dt>By</dt><dd>{entry.updated_by}</dd></div><div><dt>Revision</dt><dd>{entry.revision}</dd></div><div><dt>Latest URL</dt><dd><a href={driveHref(entry.entry)}>{driveHref(entry.entry)}</a></dd></div></dl>
<dl><div><dt>Updated</dt><dd>{formatDate(entry.updated_at)}</dd></div><div><dt>By</dt><dd>{entry.updated_by}</dd></div><div><dt>Last change request</dt><dd>{entry.last_mutation_id}</dd></div><div><dt>Latest URL</dt><dd><a href={driveHref(entry.entry)}>{driveHref(entry.entry)}</a></dd></div></dl>
<p class="drive-note">Saving replaces the current content. There is no version history.</p>
</details>
{/if}
@@ -426,7 +426,7 @@
{/if}
{#if form === 'text'}<label>Markdown<textarea bind:value={newText} disabled={pending} placeholder="Write your document…"></textarea></label>{/if}
{/if}
{#if form === 'replace' || form === 'relocate'}<p class="drive-note">Expected revision {formEntry?.revision}. Changes by another writer will not be overwritten.</p>{/if}
{#if form === 'replace' || form === 'relocate'}<p class="drive-note">Changes by another writer will not be overwritten.</p>{/if}
{#if form === 'replace'}<p class="drive-note">This replaces the current file. There is no version history.</p>{/if}
<footer class="drive-dialog-actions">
<button type="button" disabled={pending} onclick={closeForm}>Cancel form</button>
@@ -44,13 +44,13 @@ Deno.test("Drive metadata rejects cross-workspace refs and unsafe latest links",
const good = entry();
const parsed = parseDriveEntry(good, "alpha");
equal(parsed.entry, good.entry);
equal(parsed.revision, good.revision);
equal(parsed.last_mutation_id, good.last_mutation_id);
equal(parsed.latest_url, good.latest_url);
for (
const patch of [
{ entry: ref("2", "beta") },
{ parent: ref("1", "beta") },
{ revision: 4 },
{ last_mutation_id: 4 },
{ kind: "directory" },
{ size: -1 },
{ size: DRIVE_FILE_MAX_BYTES + 1 },
@@ -200,7 +200,7 @@ Deno.test("Drive client uses current routes with same-origin authentication and
assert(called);
equal(
driveDownloadUrl("alpha", ref("9007199254740993"), "9007199254740995"),
"/api/w/alpha/drive/download?entry_workspace_id=alpha&id=9007199254740993&expected_revision=9007199254740995",
"/api/w/alpha/drive/download?entry_workspace_id=alpha&id=9007199254740993&expected_mutation_id=9007199254740995",
);
});
Deno.test("Drive upload over 16 MiB is rejected before fetching", async () => {
@@ -244,7 +244,7 @@ Deno.test("Drive mutation network abort or malformed reply remains unknown after
client.mutate("alpha", "req", {
operation: "delete",
id: ref(),
expected_revision: "9007199254740993",
expected_mutation_id: "9007199254740993",
}),
(error) => {
assert(error instanceof DriveRequestError);
@@ -268,7 +268,7 @@ Deno.test("Drive typed conflict proves not committed and read responses cannot t
client.mutate("alpha", "req", {
operation: "delete",
id: ref(),
expected_revision: "1",
expected_mutation_id: "1",
}),
(error) => {
assert(error instanceof DriveRequestError);
@@ -285,7 +285,10 @@ Deno.test("Drive typed conflict proves not committed and read responses cannot t
Deno.test("Drive image preview fetches authenticated bounded binary rather than metadata URL", async () => {
const image = { ...entry(), content_type: "image/png" };
const client = createDriveClient((url, init) => {
equal(String(url), driveDownloadUrl("alpha", ref(), image.revision));
equal(
String(url),
driveDownloadUrl("alpha", ref(), image.last_mutation_id),
);
equal((init as RequestInit | undefined)?.credentials, "same-origin");
equal((init as RequestInit | undefined)?.cache, "no-store");
return Promise.resolve(new Response(new Uint8Array([0, 1, 2])));
@@ -310,7 +313,7 @@ Deno.test("Drive malformed mutation response target parent name or operation kin
mutation: {
operation: "update_text",
id: ref(),
expected_revision: "1",
expected_mutation_id: "1",
text: "abc",
content_type: "text/plain",
},
@@ -320,7 +323,7 @@ Deno.test("Drive malformed mutation response target parent name or operation kin
mutation: {
operation: "update_text",
id: ref(),
expected_revision: "1",
expected_mutation_id: "1",
text: "abc",
content_type: "text/plain",
},
@@ -330,7 +333,7 @@ Deno.test("Drive malformed mutation response target parent name or operation kin
mutation: {
operation: "relocate",
id: ref(),
expected_revision: "1",
expected_mutation_id: "1",
parent: ref("4"),
name: "moved.txt",
},
@@ -340,7 +343,7 @@ Deno.test("Drive malformed mutation response target parent name or operation kin
mutation: {
operation: "relocate",
id: ref(),
expected_revision: "1",
expected_mutation_id: "1",
parent: ref("4"),
name: "moved.txt",
},
@@ -350,7 +353,7 @@ Deno.test("Drive malformed mutation response target parent name or operation kin
mutation: {
operation: "relocate",
id: ref(),
expected_revision: "1",
expected_mutation_id: "1",
parent: ref("4"),
name: "moved.txt",
},
@@ -392,14 +395,14 @@ Deno.test("Drive malformed mutation response target parent name or operation kin
mutation: {
operation: "update_text",
id: ref(),
expected_revision: "1",
expected_mutation_id: "1",
text: "abc",
content_type: "text/plain",
},
response: null,
},
{
mutation: { operation: "delete", id: ref(), expected_revision: "1" },
mutation: { operation: "delete", id: ref(), expected_mutation_id: "1" },
response: entry(),
},
];
@@ -422,7 +425,7 @@ Deno.test("Drive malformed upload update and create identity remains unknown aft
target: {
operation: "update",
id: ref(),
expected_revision: "1",
expected_mutation_id: "1",
content_type: "text/plain",
},
response: entry("3"),
@@ -431,7 +434,7 @@ Deno.test("Drive malformed upload update and create identity remains unknown aft
target: {
operation: "update",
id: ref(),
expected_revision: "1",
expected_mutation_id: "1",
content_type: "text/plain",
},
response: entry("2", "alpha", "2", "folder"),
@@ -467,7 +470,7 @@ Deno.test("Drive malformed upload update and create identity remains unknown aft
target: {
operation: "update",
id: ref(),
expected_revision: "1",
expected_mutation_id: "1",
content_type: "text/plain",
},
response: null,
@@ -508,3 +511,22 @@ Deno.test("Drive bounded text roundtrip accepts worst-case JSON escaping without
);
equal(result.entry.size, 64 * 1024);
});
Deno.test("Drive committed request identity remains UTF-8 and URL encoded rather than numeric", () => {
const id = 'write/資料 ?"#&=+';
const value = { ...entry(), last_mutation_id: id };
equal(parseDriveEntry(value, "alpha").last_mutation_id, id);
const url = new URL(
driveDownloadUrl("alpha", value.entry, id),
"https://example.test",
);
equal(url.searchParams.get("expected_mutation_id"), id);
equal(url.searchParams.size, 3);
equal(url.hash, "");
for (
const bad of ["", "\nrequest", "\x7f", "é".repeat(65), "x".repeat(129)]
) {
throws(() => parseDriveEntry({ ...value, last_mutation_id: bad }, "alpha"));
throws(() => driveDownloadUrl("alpha", value.entry, bad));
}
});
+16 -8
View File
@@ -68,6 +68,14 @@ export function parseDriveDecimal(value: unknown): string {
) invalid();
return result;
}
function parseMutationId(value: unknown, allowRoot = false): string {
const result = string(value, 128);
if (
(!allowRoot && !result) || new TextEncoder().encode(result).length > 128 ||
/[\x00-\x1f\x7f-\x9f]/.test(result)
) invalid();
return result;
}
function hasControl(value: string, includeSpace = false): boolean {
return Array.from(value).some((char) =>
char.charCodeAt(0) <= (includeSpace ? 32 : 31)
@@ -158,7 +166,7 @@ export function parseDriveEntry(
parent,
kind: v.kind,
name: string(v.name, 255),
revision: parseDriveDecimal(v.revision),
last_mutation_id: parseMutationId(v.last_mutation_id, parent === null),
size: size as number | null,
content_type: v.content_type === null ? null : mime(v.content_type),
updated_by: string(v.updated_by),
@@ -293,7 +301,7 @@ export type DriveUploadTarget =
| {
operation: "update";
id: DriveEntryRef;
expected_revision: string;
expected_mutation_id: string;
content_type: string;
};
export type DrivePageOptions = { after?: string | null; limit?: number };
@@ -349,14 +357,14 @@ export interface DriveClient {
export function driveDownloadUrl(
workspaceId: string,
ref: DriveEntryRef,
revision?: string,
last_mutation_id?: string,
): string {
return `${base(workspaceId)}/download${
query({
...refQuery(ref, workspaceId),
expected_revision: revision === undefined
expected_mutation_id: last_mutation_id === undefined
? null
: parseDriveDecimal(revision),
: parseMutationId(last_mutation_id),
})
}`;
}
@@ -375,7 +383,7 @@ function name(value: string): string {
function validateMutation(value: DriveMutation, workspaceId: string): void {
if ("id" in value) {
parseDriveEntryRef(value.id, workspaceId);
parseDriveDecimal(value.expected_revision);
parseMutationId(value.expected_mutation_id);
}
if ("parent" in value) parseDriveEntryRef(value.parent, workspaceId);
if ("name" in value) name(value.name);
@@ -514,7 +522,7 @@ export function createDriveClient(fetchFn: typeof fetch = fetch): DriveClient {
entry.size === null || entry.size > DRIVE_RESPONSE_MAX_BYTES
) throw new DriveRequestError("limit", "not_committed");
const res = await response(
driveDownloadUrl(ws, entry.entry, entry.revision),
driveDownloadUrl(ws, entry.entry, entry.last_mutation_id),
{ signal },
);
await checked(res);
@@ -578,7 +586,7 @@ export function createDriveClient(fetchFn: typeof fetch = fetch): DriveClient {
}
: {
id: parseDriveEntryRef(target.id, ws).node_id,
expected_revision: parseDriveDecimal(target.expected_revision),
expected_mutation_id: parseMutationId(target.expected_mutation_id),
};
signal?.throwIfAborted();
const digest = await crypto.subtle.digest(
@@ -15,7 +15,7 @@ import {
export type DriveDraft = {
text: string;
baseText: string;
expectedRevision: string;
expectedMutationId: string;
contentType: string;
conflict: boolean;
};
@@ -42,7 +42,7 @@ export type DriveControllerState = {
};
type ReceiptRecord = {
receipt: DriveReceipt;
saved?: { text: string; revision: string };
saved?: { text: string; last_mutation_id: string };
};
const empty = (): DriveControllerState => ({
workspaceId: null,
@@ -208,7 +208,7 @@ export class DriveController {
if (!this.current(generation)) return;
let draft = this.drafts.get(k) ?? null;
// Clean acknowledged drafts may refresh, but dirty/conflicted drafts
// and unresolved requests must keep their original CAS revision.
// and unresolved requests must keep their original CAS last_mutation_id.
const unresolved = this.currentReceipts(k).some((receipt) =>
receipt.state === "pending" || receipt.state === "unknown"
);
@@ -225,7 +225,7 @@ export class DriveController {
draft = {
text: result.text,
baseText: result.text,
expectedRevision: result.entry.revision,
expectedMutationId: result.entry.last_mutation_id,
contentType: result.entry.content_type ?? "text/plain",
conflict: false,
};
@@ -354,11 +354,11 @@ export class DriveController {
this.client.mutate(s.ws, id, {
operation: "update_text",
id: s.entry.entry,
expected_revision: draft.expectedRevision,
expected_mutation_id: draft.expectedMutationId,
text: draft.text,
content_type: draft.contentType,
}, signal),
{ text: draft.text, revision: draft.expectedRevision },
{ text: draft.text, last_mutation_id: draft.expectedMutationId },
);
}
async mutate(mutation: DriveMutation): Promise<string> {
@@ -453,13 +453,13 @@ export class DriveController {
const draft = this.drafts.get(k);
if (
record.saved && draft && entry &&
draft.expectedRevision === record.saved.revision &&
draft.expectedMutationId === record.saved.last_mutation_id &&
entry.entry.node_id === this.value.entry?.entry.node_id
) {
const updated = {
...draft,
baseText: record.saved.text,
expectedRevision: entry.revision,
expectedMutationId: entry.last_mutation_id,
conflict: false,
};
this.drafts.set(k, updated);
@@ -110,12 +110,12 @@ Deno.test("Drive A to B to A selection rejects the first A response and late rea
equal(c.state.text, "fresh");
equal(c.state.error, null);
});
Deno.test("Drive drafts survive workspace and node switches without revision rebasing", async () => {
let revision = "9007199254740993";
Deno.test("Drive drafts survive workspace and node switches without last_mutation_id rebasing", async () => {
let last_mutation_id = "9007199254740993";
const c = controller({
readText: (ws, target) =>
Promise.resolve({
entry: entry(target.node_id, ws, revision),
entry: entry(target.node_id, ws, last_mutation_id),
text: "server",
truncated: false,
}),
@@ -124,22 +124,22 @@ Deno.test("Drive drafts survive workspace and node switches without revision reb
c.edit("local alpha");
await c.select("beta", ref("2", "beta"));
c.edit("local beta");
revision = "9007199254740999";
last_mutation_id = "9007199254740999";
await c.select("alpha", ref("3"));
await c.select("alpha", ref());
equal(c.state.draft?.text, "local alpha");
equal(c.state.draft?.expectedRevision, "9007199254740993");
equal(c.state.entry?.revision, "9007199254740999");
equal(c.state.draft?.expectedMutationId, "9007199254740993");
equal(c.state.entry?.last_mutation_id, "9007199254740999");
await c.select("beta", ref("2", "beta"));
equal(c.state.draft?.text, "local beta");
});
Deno.test("Drive conflict keeps local text and old CAS revision across switching without retry", async () => {
Deno.test("Drive conflict keeps local text and old CAS last_mutation_id across switching without retry", async () => {
let mutations = 0;
const c = controller({
mutate: (_, __, mutation) => {
++mutations;
assert("expected_revision" in mutation);
equal(mutation.expected_revision, "9007199254740993");
assert("expected_mutation_id" in mutation);
equal(mutation.expected_mutation_id, "9007199254740993");
return Promise.reject(new DriveRequestError("conflict", "not_committed"));
},
});
@@ -151,7 +151,7 @@ Deno.test("Drive conflict keeps local text and old CAS revision across switching
await c.select("alpha", ref("3"));
await c.select("alpha", ref());
equal(c.state.draft?.conflict, true);
equal(c.state.draft?.expectedRevision, "9007199254740993");
equal(c.state.draft?.expectedMutationId, "9007199254740993");
equal(mutations, 1);
});
Deno.test("Drive acknowledged save preserves edits made while the request was in flight", async () => {
@@ -168,7 +168,7 @@ Deno.test("Drive acknowledged save preserves edits made while the request was in
await saving;
equal(c.state.draft?.text, "new edit");
equal(c.state.draft?.baseText, "sent");
equal(c.state.draft?.expectedRevision, "9007199254740994");
equal(c.state.draft?.expectedMutationId, "9007199254740994");
});
Deno.test("Drive late mutation and upload success cannot affect a newly selected identity", async () => {
for (const operation of ["mutation", "upload"] as const) {
@@ -184,7 +184,7 @@ Deno.test("Drive late mutation and upload success cannot affect a newly selected
: c.upload(new Blob(["abc"]), {
operation: "update",
id: ref(),
expected_revision: "9007199254740993",
expected_mutation_id: "9007199254740993",
content_type: "text/plain",
});
await c.select("beta", ref("2", "beta"));
@@ -195,11 +195,11 @@ Deno.test("Drive late mutation and upload success cannot affect a newly selected
await sending;
equal(c.state.workspaceId, "beta");
equal(c.state.receipts, []);
equal(c.state.draft?.expectedRevision, "9007199254740993");
equal(c.state.draft?.expectedMutationId, "9007199254740993");
await c.select("alpha", ref());
equal(c.state.receipts[0].state, "unknown");
equal(c.state.draft?.text, "sent");
equal(c.state.draft?.expectedRevision, "9007199254740993");
equal(c.state.draft?.expectedMutationId, "9007199254740993");
}
});
Deno.test("Drive unknown upload reconciles by request id and never blindly retransmits", async () => {
@@ -234,7 +234,7 @@ Deno.test("Drive unknown upload reconciles by request id and never blindly retra
const target = {
operation: "update" as const,
id: ref(),
expected_revision: "9007199254740993",
expected_mutation_id: "9007199254740993",
content_type: "application/octet-stream",
};
const id = await c.upload(new Blob(["abc"]), target);
@@ -273,7 +273,7 @@ Deno.test("Drive late status response is fenced even when the original node is r
});
await checking;
equal(c.state.receipts[0].state, "unknown");
equal(c.state.draft?.expectedRevision, "9007199254740993");
equal(c.state.draft?.expectedMutationId, "9007199254740993");
equal(c.state.draft?.text, "local");
});
Deno.test("Drive truncated text has no editable draft and disposal rejects late publication", async () => {
@@ -460,7 +460,7 @@ Deno.test("Drive cancelled writes retain draft and receipt and ignore late compl
: c.upload(new Blob(["abc"]), {
operation: "update",
id: ref(),
expected_revision: "9007199254740993",
expected_mutation_id: "9007199254740993",
content_type: "text/plain",
});
equal(c.state.receipts[0].state, "pending");
@@ -470,7 +470,7 @@ Deno.test("Drive cancelled writes retain draft and receipt and ignore late compl
equal(c.state.receipts[0].requestId, id);
equal(c.state.receipts[0].state, "unknown");
equal(c.state.draft?.text, "sent draft");
equal(c.state.draft?.expectedRevision, "9007199254740993");
equal(c.state.draft?.expectedMutationId, "9007199254740993");
await rejects(() => c.save());
await c.reconcile(id);
assert(
@@ -495,7 +495,7 @@ Deno.test("Drive cancelled writes retain draft and receipt and ignore late compl
equal(await sending, id);
assert(c.state === snapshot);
equal(sends, 1);
equal(c.state.entry?.revision, "9007199254740994");
equal(c.state.entry?.last_mutation_id, "9007199254740994");
}
});
Deno.test("Drive cancellation does not abort or fence active search reads", async () => {
@@ -555,14 +555,14 @@ Deno.test("Drive concurrent writes are blocked before transmission and receipt c
c.mutate({
operation: "delete",
id: ref(),
expected_revision: "9007199254740993",
expected_mutation_id: "9007199254740993",
})
);
await rejects(() =>
c.upload(new Blob(["abc"]), {
operation: "update",
id: ref(),
expected_revision: "9007199254740993",
expected_mutation_id: "9007199254740993",
content_type: "text/plain",
})
);
@@ -574,13 +574,13 @@ Deno.test("Drive concurrent writes are blocked before transmission and receipt c
});
await saving;
});
Deno.test("Drive clean draft refresh adopts latest revision unless a request remains unresolved", async () => {
let revision = "9007199254740993";
Deno.test("Drive clean draft refresh adopts latest last_mutation_id unless a request remains unresolved", async () => {
let last_mutation_id = "9007199254740993";
let text = "abc";
const c = controller({
readText: () =>
Promise.resolve({
entry: entry("2", "alpha", revision),
entry: entry("2", "alpha", last_mutation_id),
text,
truncated: false,
}),
@@ -588,19 +588,19 @@ Deno.test("Drive clean draft refresh adopts latest revision unless a request rem
Promise.reject(new DriveRequestError("outcome_unknown", "unknown")),
});
await c.select("alpha", ref());
revision = "9007199254740994";
last_mutation_id = "9007199254740994";
text = "new";
await c.refresh();
equal(c.state.draft?.text, "new");
equal(c.state.draft?.baseText, "new");
equal(c.state.draft?.expectedRevision, revision);
equal(c.state.draft?.expectedMutationId, last_mutation_id);
await c.save();
revision = "9007199254740995";
last_mutation_id = "9007199254740995";
text = "unknown change";
await c.refresh();
equal(c.state.text, "unknown change");
equal(c.state.draft?.text, "new");
equal(c.state.draft?.expectedRevision, "9007199254740994");
equal(c.state.draft?.expectedMutationId, "9007199254740994");
equal(c.state.receipts[0].state, "unknown");
await rejects(() => c.save());
});
@@ -688,13 +688,13 @@ Deno.test("Drive cancelled late success stays unknown until explicit committed s
});
const id = await saving;
equal(c.state.receipts[0].state, "unknown");
equal(c.state.draft?.expectedRevision, "9007199254740993");
equal(c.state.draft?.expectedMutationId, "9007199254740993");
equal(c.state.draft?.text, "local");
equal(statusCalls, 0);
await rejects(() => c.save());
await c.reconcile(id);
equal(c.state.receipts[0].state, "committed");
equal(c.state.draft?.expectedRevision, "9007199254740994");
equal(c.state.draft?.expectedMutationId, "9007199254740994");
equal(c.state.draft?.baseText, "local");
});
Deno.test("Drive archived dirty draft cannot enable editing after MIME changes to active bytes", async () => {
@@ -723,14 +723,14 @@ Deno.test("Drive archived dirty draft cannot enable editing after MIME changes t
contentType = "text/plain";
await c.refresh();
equal(c.state.draft?.text, "retain me");
equal(c.state.draft?.expectedRevision, "9007199254740993");
equal(c.state.draft?.expectedMutationId, "9007199254740993");
});
Deno.test("Drive conflicted clean draft stays fixed while clean truncated reread removes editor", async () => {
let revision = "9007199254740993";
let last_mutation_id = "9007199254740993";
let truncated = false;
const readText = () =>
Promise.resolve({
entry: entry("2", "alpha", revision),
entry: entry("2", "alpha", last_mutation_id),
text: "abc",
truncated,
});
@@ -741,12 +741,12 @@ Deno.test("Drive conflicted clean draft stays fixed while clean truncated reread
});
await conflict.select("alpha", ref());
await conflict.save();
revision = "9007199254740994";
last_mutation_id = "9007199254740994";
await conflict.refresh();
equal(conflict.state.draft?.text, "abc");
equal(conflict.state.draft?.baseText, "abc");
equal(conflict.state.draft?.conflict, true);
equal(conflict.state.draft?.expectedRevision, "9007199254740993");
equal(conflict.state.draft?.expectedMutationId, "9007199254740993");
const clean = controller({ readText });
await clean.select("alpha", ref());
assert(clean.state.draft);
@@ -46,7 +46,7 @@ export function ref(id = "2", ws = "alpha"): DriveEntryRef {
export function entry(
id = "2",
ws = "alpha",
revision = "9007199254740993",
last_mutation_id = "9007199254740993",
kind: "file" | "folder" = "file",
): DriveEntry {
return {
@@ -54,7 +54,7 @@ export function entry(
parent: id === "1" ? null : ref("1", ws),
name: id === "1" ? "" : "note.txt",
kind,
revision,
last_mutation_id,
size: kind === "file" ? 3 : null,
content_type: kind === "file" ? "text/plain" : null,
updated_by: "user:test",
@@ -33,7 +33,7 @@ export function fixtureDetail(key = "T-101") {
const { workspace_action_priority: _, ...ticket } = summary;
return {
...ticket,
item_revision: "revision-1",
content_digest: "a".repeat(64),
created_at: updated(1),
readiness: null,
body: "Implementation in progress.",
@@ -155,7 +155,6 @@ export function dashboardFixture(
...detail,
body: "# Objective detail\n\nHome dashboard navigation destination.",
body_truncated: false,
revision: "revision-1",
linked_ticket_summaries: [],
resources: [],
events: [],
+41 -71
View File
@@ -18,12 +18,12 @@ import type {
MemoryStagingRecord,
SubjektivMemoryEvidence,
SubjektivMemoryEvidenceCandidate,
SubjektivMemoryListRevisionsResponse,
SubjektivMemoryListChangesResponse,
SubjektivMemoryQueryItem,
SubjektivMemoryQueryResponse,
SubjektivMemoryReadResponse,
SubjektivMemoryRevisionItem,
SubjektivMemoryRevisionRef,
SubjektivMemoryChangeItem,
SubjektivMemoryChangeRef,
SubjektivMemorySourceEvidenceRef,
SubjektivMemoryState,
SubjektivResidentSurfaceAvailability,
@@ -79,7 +79,7 @@ const memoryStates = new Set<SubjektivMemoryState>([
]);
const MAX_SUBJECTS = 100;
const MAX_CURRENT_MEMORIES = 100;
const MAX_REVISIONS = 100;
const MAX_CHANGES = 100;
const MAX_MEMORY_REFS = MEMORY_API_LIMITS.maxCollectionItems;
export const MEMORY_API_LOAD_POLICY = {
@@ -221,15 +221,7 @@ export async function updateSubjektivSubjectBehavior(
request: SubjektivSubjectBehaviorUpdateRequest,
): Promise<SubjektivSubjectResponse> {
validateSubjectBehavior(request.behavior_md);
if (
!Number.isSafeInteger(request.expected_behavior_revision) ||
request.expected_behavior_revision < 0
) {
throw new SubjektivSubjectCreateError(
"validation",
"Subject behavior revision is invalid.",
);
}
validateSubjectBehavior(request.expected_behavior_md);
let response: Response;
try {
response = await fetchFn(
@@ -494,7 +486,6 @@ function parseEvidenceOrigin(value: unknown): MemoryEvidenceOrigin {
"worker_id",
"flow_selector",
"flow_definition_id",
"flow_definition_revision",
] as const;
const record = strictRecord(
value,
@@ -528,14 +519,6 @@ function parseEvidenceOrigin(value: unknown): MemoryEvidenceOrigin {
result[key] = text;
}
}
if ("flow_definition_revision" in record) {
result.flow_definition_revision = record.flow_definition_revision === null
? null
: nonNegativeInteger(
record.flow_definition_revision,
"flow_definition_revision",
);
}
return result;
}
@@ -549,9 +532,8 @@ export function parseSubjektivSubjectResponse(
"id",
"role",
"behavior_md",
"behavior_revision",
"state",
"store_revision",
"memory_fingerprint",
"created_at",
"updated_at",
"current_worker",
@@ -573,9 +555,8 @@ export function parseSubjektivSubjectResponse(
"behavior_md",
MAX_SUBJECT_BEHAVIOR_BYTES,
),
behavior_revision: requiredNonNegativeInteger(record, "behavior_revision"),
state,
store_revision: requiredNonNegativeInteger(record, "store_revision"),
memory_fingerprint: requiredIdentifier(record, "memory_fingerprint"),
created_at: requiredString(record, "created_at"),
updated_at: requiredString(record, "updated_at"),
};
@@ -665,7 +646,7 @@ function parseResidentSurfaceSnapshot(
"snapshot_id",
"body_md",
"memory_refs",
"built_from_store_revision",
"built_from_memory_fingerprint",
"created_at",
],
"Resident surface snapshot",
@@ -681,10 +662,10 @@ function parseResidentSurfaceSnapshot(
record.memory_refs,
MAX_MEMORY_REFS,
"memory_refs",
).map(parseMemoryRevisionRef),
built_from_store_revision: requiredNonNegativeInteger(
).map(parseMemoryChangeRef),
built_from_memory_fingerprint: requiredIdentifier(
record,
"built_from_store_revision",
"built_from_memory_fingerprint",
),
created_at: requiredString(record, "created_at"),
};
@@ -716,12 +697,12 @@ export function parseSubjektivMemoryQueryResponse(
function parseMemoryQueryItem(value: unknown): SubjektivMemoryQueryItem {
const record = strictRecord(
value,
["id", "revision", "kind", "state", "claim", "excerpt", "updated_at"],
["id", "change_id", "kind", "state", "claim", "excerpt", "updated_at"],
"Current Memory item",
);
return {
id: requiredIdentifier(record, "id"),
revision: requiredPositiveInteger(record, "revision"),
change_id: requiredIdentifier(record, "change_id"),
kind: parseCandidateKind(record.kind),
state: parseMemoryState(record.state),
claim: requiredString(record, "claim"),
@@ -738,8 +719,8 @@ export function parseSubjektivMemoryReadResponse(
value,
[
"memory_id",
"revision",
"current_revision",
"change_id",
"current_change_id",
"kind",
"state",
"claim",
@@ -772,9 +753,8 @@ export function parseSubjektivMemoryReadResponse(
if (expectedMemoryId !== undefined && memoryId !== expectedMemoryId) {
invalid("Memory detail identity does not match the requested Memory");
}
const revision = requiredPositiveInteger(record, "revision");
const currentRevision = requiredPositiveInteger(record, "current_revision");
if (revision > currentRevision) invalid("revision exceeds current_revision");
const changeId = requiredIdentifier(record, "change_id");
const currentChangeId = requiredIdentifier(record, "current_change_id");
const bodyTruncated = requiredBoolean(record, "body_truncated");
const bodyNextOffset = optionalNonNegativeInteger(record, "body_next_offset");
const bodyNextByteOffset = optionalNonNegativeInteger(
@@ -814,8 +794,8 @@ export function parseSubjektivMemoryReadResponse(
assertCursorInvariant(evidenceHasMore, evidenceNextCursor, "Memory evidence");
const result: SubjektivMemoryReadResponse = {
memory_id: memoryId,
revision,
current_revision: currentRevision,
change_id: changeId,
current_change_id: currentChangeId,
kind: parseCandidateKind(record.kind),
state: parseMemoryState(record.state),
claim: requiredString(record, "claim"),
@@ -837,7 +817,7 @@ export function parseSubjektivMemoryReadResponse(
record.derived_from,
MAX_MEMORY_REFS,
"derived_from",
).map(parseMemoryRevisionRef),
).map(parseMemoryChangeRef),
evidence_has_more: evidenceHasMore,
};
if ("staleness" in record) {
@@ -984,37 +964,34 @@ function parseMemorySourceEvidenceRef(
return result;
}
export function parseSubjektivMemoryListRevisionsResponse(
export function parseSubjektivMemoryListChangesResponse(
value: unknown,
expectedMemoryId?: string,
): SubjektivMemoryListRevisionsResponse {
): SubjektivMemoryListChangesResponse {
const record = strictRecord(
value,
["memory_id", "current_revision", "items", "next_cursor", "has_more"],
"Memory revisions response",
["memory_id", "current_change_id", "items", "next_cursor", "has_more"],
"Memory changes response",
["next_cursor"],
);
const memoryId = requiredIdentifier(record, "memory_id");
if (expectedMemoryId !== undefined && memoryId !== expectedMemoryId) {
invalid("Memory revisions identity does not match the requested Memory");
invalid("Memory changes identity does not match the requested Memory");
}
const currentRevision = requiredPositiveInteger(record, "current_revision");
const items = boundedArray(record.items, MAX_REVISIONS, "items").map(
parseMemoryRevisionItem,
const currentChangeId = requiredIdentifier(record, "current_change_id");
const items = boundedArray(record.items, MAX_CHANGES, "items").map(
parseMemoryChangeItem,
);
if (items.some((item) => item.revision > currentRevision)) {
invalid("revision history contains a future revision");
}
assertUnique(
items.map((item) => String(item.revision)),
"Memory revisions",
items.map((item) => item.change_id),
"Memory changes",
);
const hasMore = requiredBoolean(record, "has_more");
const nextCursor = optionalNullableString(record, "next_cursor");
assertCursorInvariant(hasMore, nextCursor, "Memory revisions");
const result: SubjektivMemoryListRevisionsResponse = {
assertCursorInvariant(hasMore, nextCursor, "Memory changes");
const result: SubjektivMemoryListChangesResponse = {
memory_id: memoryId,
current_revision: currentRevision,
current_change_id: currentChangeId,
items,
has_more: hasMore,
};
@@ -1022,14 +999,14 @@ export function parseSubjektivMemoryListRevisionsResponse(
return result;
}
function parseMemoryRevisionItem(value: unknown): SubjektivMemoryRevisionItem {
function parseMemoryChangeItem(value: unknown): SubjektivMemoryChangeItem {
const record = strictRecord(
value,
["revision", "kind", "state", "claim", "change_reason", "updated_at"],
"Memory revision item",
["change_id", "kind", "state", "claim", "change_reason", "updated_at"],
"Memory change item",
);
return {
revision: requiredPositiveInteger(record, "revision"),
change_id: requiredIdentifier(record, "change_id"),
kind: parseCandidateKind(record.kind),
state: parseMemoryState(record.state),
claim: requiredString(record, "claim"),
@@ -1038,15 +1015,15 @@ function parseMemoryRevisionItem(value: unknown): SubjektivMemoryRevisionItem {
};
}
function parseMemoryRevisionRef(value: unknown): SubjektivMemoryRevisionRef {
function parseMemoryChangeRef(value: unknown): SubjektivMemoryChangeRef {
const record = strictRecord(
value,
["memory_id", "revision"],
"Memory revision ref",
["memory_id", "change_id"],
"Memory change ref",
);
return {
memory_id: requiredIdentifier(record, "memory_id"),
revision: requiredPositiveInteger(record, "revision"),
change_id: requiredIdentifier(record, "change_id"),
};
}
@@ -1198,13 +1175,6 @@ function requiredNonNegativeInteger(
return nonNegativeInteger(record[key], key);
}
function requiredPositiveInteger(
record: Record<string, unknown>,
key: string,
): number {
return positiveInteger(record[key], key);
}
function positiveInteger(value: unknown, label: string): number {
const result = nonNegativeInteger(value, label);
if (result === 0) invalid(`${label} must be a positive safe integer`);
@@ -48,7 +48,7 @@ Deno.test('pending submission previews accept new and legacy runtime payloads',
snapshot: { topic: 'worker_protocol', data: {
worker_id: 'worker-1',
events: [{ event: 'pending_submissions_changed', data: { pending: {
revision: 1, notification_count: 0, head_id: 'queued-1',
notification_count: 0, head_id: 'queued-1',
submissions: [{ submission_id: 'queued-1', accepted_at_ms: 1,
segment_count: 1, byte_len: 10, ...(preview === undefined ? {} : { preview }) }],
} } }],
@@ -65,7 +65,7 @@ Deno.test('pending notification previews accept new and legacy runtime payloads'
snapshot: { topic: 'worker_protocol', data: {
worker_id: 'worker-1',
events: [{ event: 'pending_submissions_changed', data: { pending: {
revision: 1, notification_count: 1, head_id: 'notification-head', submissions: [],
notification_count: 1, head_id: 'notification-head', submissions: [],
...(previews === undefined ? {} : { notification_previews: previews }),
} } }],
} },
@@ -17,7 +17,7 @@ function assert(condition: boolean, message: string): void {
const snapshot: SessionSnapshot = {
pending_submissions: {
revision: 0,
notification_count: 0,
head_id: null,
submissions: [],
@@ -113,7 +113,7 @@
nameError = nameNotice = null;
try {
const response = await updateWorkspaceMetadata(workspaceId, {
display_name: displayNameDraft, revision: workspaceMetadata.revision,
display_name: displayNameDraft, expected_updated_at: workspaceMetadata.updated_at,
});
if (disposed) return;
workspaceMetadata = response.workspace;
@@ -203,13 +203,13 @@
if (typeof value !== 'object' || value === null) return null;
const record = value as Record<string, unknown>;
if (
Object.keys(record).sort().join(',') !== 'confirmation,expected_revision,operation_id' ||
Object.keys(record).sort().join(',') !== 'confirmation,expected_workspace_updated_at,operation_id' ||
typeof record.operation_id !== 'string' || record.operation_id.length === 0 || record.operation_id.length > 128 ||
!/^[A-Za-z0-9_-]+$/.test(record.operation_id) ||
typeof record.expected_revision !== 'string' || record.expected_revision.length > 128 ||
typeof record.expected_workspace_updated_at !== 'string' || record.expected_workspace_updated_at.length > 128 ||
typeof record.confirmation !== 'string' || record.confirmation !== workspaceName || record.confirmation.length > 256
) return null;
return { operation_id: record.operation_id, expected_revision: record.expected_revision, confirmation: record.confirmation };
return { operation_id: record.operation_id, expected_workspace_updated_at: record.expected_workspace_updated_at, confirmation: record.confirmation };
} catch { return null; }
}
onMount(() => {
@@ -232,7 +232,7 @@
deletionError = null;
try {
const request = deletionRequest ?? {
operation_id: crypto.randomUUID(), expected_revision: deletionPreflight!.expected_revision, confirmation: deletionConfirmation,
operation_id: crypto.randomUUID(), expected_workspace_updated_at: deletionPreflight!.expected_workspace_updated_at, confirmation: deletionConfirmation,
};
deletionRequest = request;
sessionStorage.setItem(deletionStorageKey(), JSON.stringify(request));
@@ -285,7 +285,6 @@
<summary>Technical details</summary>
<dl>
<div><dt>Source</dt><dd><code>{workspaceMetadata.source}</code></dd></div>
<div><dt>Revision</dt><dd><code>{workspaceMetadata.revision}</code></dd></div>
</dl>
{#each diagnostics.filter((item) => item.severity === 'info') as diagnostic}<p>{diagnostic.message}</p>{/each}
</details>
@@ -316,7 +315,6 @@
<dl>
<div><dt>Key</dt><dd><code>{signingIdentity.identity.key_id}</code></dd></div>
<div><dt>Fingerprint</dt><dd><code>{signingIdentity.identity.public_key_fingerprint}</code></dd></div>
<div><dt>Revision</dt><dd><code>{signingIdentity.identity.revision}</code></dd></div>
</dl>
<label>Public identity bundle<textarea readonly rows="9" value={identityBundleText} spellcheck="false"></textarea></label>
</details>
@@ -6,7 +6,7 @@ export function metadataFixture(workspaceId = "home-owner") {
? "Workspace with a long name — international documentation and distributed development"
: "Workspace Settings Review",
created_at: "2026-01-01T00:00:00Z",
revision: "2026-01-02T00:00:00Z",
updated_at: "2026-01-02T00:00:00Z",
source: "server_db",
diagnostics: [],
};
@@ -16,7 +16,6 @@ export function identityFixture(workspaceId = "home-owner", pending = false) {
workspace_id: workspaceId,
key_id: "fixture-workspace-key",
algorithm: "ed25519",
revision: 1,
state: pending ? "pending_provisioning" : "active",
created_at: "2026-01-01T00:00:00Z",
};
@@ -37,7 +36,6 @@ export function identityFixture(workspaceId = "home-owner", pending = false) {
algorithm: identity.algorithm,
public_key: publicKey,
public_key_fingerprint: fingerprint,
revision: 1,
},
};
}
@@ -45,7 +43,7 @@ export function deletionFixture(workspaceId = "home-owner") {
return {
workspace_id: workspaceId,
display_name: metadataFixture(workspaceId).display_name,
expected_revision: metadataFixture(workspaceId).revision,
expected_workspace_updated_at: metadataFixture(workspaceId).updated_at,
can_delete: true,
blockers: [],
resources: {
@@ -70,7 +68,7 @@ export function settingsFixtureHandler() {
const current = names.get(id) ?? metadataFixture(id);
if (request.method === "PUT") {
const body = await request.json();
if (body.revision !== current.revision) {
if (body.expected_updated_at !== current.updated_at) {
return new Response(
"Workspace metadata changed. Reload the saved name.",
{ status: 409 },
@@ -79,7 +77,7 @@ export function settingsFixtureHandler() {
const next = {
...current,
display_name: String(body.display_name).trim(),
revision: String(Number.parseInt(current.revision, 10) + 1),
updated_at: new Date(Date.parse(current.updated_at) + 1).toISOString(),
};
names.set(id, next);
return Response.json({ workspace: next, diagnostics: [] });
@@ -115,7 +113,7 @@ export function settingsFixtureHandler() {
...(names.has(deletion[1])
? {
display_name: names.get(deletion[1])!.display_name,
expected_revision: names.get(deletion[1])!.revision,
expected_workspace_updated_at: names.get(deletion[1])!.updated_at,
}
: {}),
});
@@ -49,8 +49,8 @@ export const SETTINGS_SECTIONS: readonly SettingsSection[] = [
"Edit the Server-owned virtual Decodal source tree through one native/WASM toolchain contract.",
bullets: [
"Virtual paths and imports resolve inside the committed Workspace tree, never from browser or Server host paths.",
"Browser analysis is advisory; Server evaluation is required before an atomic revision commit.",
"Profile launch data is projected from this active revision; remaining Skill, Prompt, and Plugin consumers migrate in their follow-up cutovers.",
"Browser analysis is advisory; Server evaluation is required before an atomic content-digest CAS commit.",
"Profile, Skill, Prompt, and Plugin data are projected from the active immutable source tree.",
],
},
{
@@ -82,11 +82,11 @@ export const SETTINGS_SECTIONS: readonly SettingsSection[] = [
label: "Profile Sources",
status: "editable",
summary:
"Inspect the Profile launch projection derived from the active Workspace configuration revision.",
"Inspect the Profile launch projection derived from the active Workspace configuration tree.",
bullets: [
"Selectors are source-qualified (builtin:* or project:*); raw profile source paths, archive content, archive digests, resource handles, and runtime tokens are not exposed.",
"Profile declarations and sources are edited only through the shared Workspace configuration editor and evaluate-before-commit contract.",
"Launch candidates and Profile archives carry the same active config revision, tree digest, and projection digest.",
"Launch candidates and Profile archives carry the same active tree digest and projection digest.",
],
},
{
@@ -146,7 +146,7 @@ export function parseWorkspaceMetadataSettingsResponse(
"workspace_id",
"display_name",
"created_at",
"revision",
"updated_at",
"source",
"diagnostics",
],
@@ -157,7 +157,7 @@ export function parseWorkspaceMetadataSettingsResponse(
workspace_id: stringValue(item.workspace_id, "Workspace metadata"),
display_name: stringValue(item.display_name, "Workspace metadata"),
created_at: stringValue(item.created_at, "Workspace metadata"),
revision: stringValue(item.revision, "Workspace metadata"),
updated_at: stringValue(item.updated_at, "Workspace metadata"),
source: stringValue(item.source, "Workspace metadata"),
diagnostics: arrayValue(
item.diagnostics,
@@ -237,7 +237,6 @@ function parseWorkspaceProfileSourceSummary(
"content_digest",
"provenance",
"editable",
"revision",
"size_bytes",
"diagnostics",
],
@@ -269,7 +268,6 @@ function parseWorkspaceProfileSourceSummary(
content_digest: stringValue(item.content_digest, "Profile source catalog"),
provenance: provenance as WorkspaceProfileSourceProvenance,
editable: booleanValue(item.editable, "Profile source catalog"),
revision: stringValue(item.revision, "Profile source catalog"),
size_bytes: sizeBytes,
diagnostics: arrayValue(
item.diagnostics,
@@ -285,14 +283,12 @@ export function parseProfileSettingsResponse(
const item = record(value, "Profile settings");
exactKeys(
item,
["workspace_id", "registry_revision", "profiles", "sources", "diagnostics"],
["config_revision", "tree_digest", "projection_digest", "default_profile"],
["workspace_id", "profiles", "sources", "diagnostics"],
["tree_digest", "projection_digest", "default_profile"],
"Profile settings",
);
return {
workspace_id: stringValue(item.workspace_id, "Profile settings"),
registry_revision: stringValue(item.registry_revision, "Profile settings"),
config_revision: optionalRevision(item.config_revision, "Profile settings"),
tree_digest: optionalString(item.tree_digest, "Profile settings"),
projection_digest: optionalString(
item.projection_digest,
@@ -330,7 +326,7 @@ export function parseWorkspaceSigningIdentityResponse(
const identityItem = record(item.identity, "Workspace signing identity");
exactKeys(
identityItem,
["workspace_id", "key_id", "algorithm", "revision", "state", "created_at"],
["workspace_id", "key_id", "algorithm", "state", "created_at"],
["public_key", "public_key_fingerprint", "provisioned_at"],
"Workspace signing identity",
);
@@ -345,16 +341,6 @@ export function parseWorkspaceSigningIdentityResponse(
502,
);
}
const revision = optionalRevision(
identityItem.revision,
"Workspace signing identity",
);
if (revision === undefined || revision === null || revision < 1) {
throw new ProfileApiError(
"Workspace signing identity returned an invalid response.",
502,
);
}
const publicKey = optionalBoundedString(
identityItem.public_key,
"Workspace signing identity",
@@ -392,7 +378,6 @@ export function parseWorkspaceSigningIdentityResponse(
...(fingerprint === undefined || fingerprint === null
? {}
: { public_key_fingerprint: fingerprint }),
revision,
state: state as WorkspaceSigningIdentityState,
created_at: boundedStringValue(
identityItem.created_at,
@@ -419,24 +404,10 @@ export function parseWorkspaceSigningIdentityResponse(
"algorithm",
"public_key",
"public_key_fingerprint",
"revision",
],
[],
"Workspace public identity bundle",
);
const bundleRevision = optionalRevision(
bundle.revision,
"Workspace public identity bundle",
);
if (
bundleRevision === undefined || bundleRevision === null ||
bundleRevision < 1
) {
throw new ProfileApiError(
"Workspace public identity bundle returned an invalid response.",
502,
);
}
publicBundle = {
workspace_id: boundedStringValue(
bundle.workspace_id,
@@ -468,7 +439,6 @@ export function parseWorkspaceSigningIdentityResponse(
"Workspace public identity bundle",
128,
),
revision: bundleRevision,
};
}
if (
@@ -478,8 +448,7 @@ export function parseWorkspaceSigningIdentityResponse(
publicBundle.key_id !== identity.key_id ||
publicBundle.algorithm !== identity.algorithm ||
publicBundle.public_key !== identity.public_key ||
publicBundle.public_key_fingerprint !== identity.public_key_fingerprint ||
publicBundle.revision !== identity.revision
publicBundle.public_key_fingerprint !== identity.public_key_fingerprint
)
) {
throw new ProfileApiError(
@@ -1,5 +1,5 @@
import type { WorkingDirectorySummary } from "../sidebar/types.ts";
import { formatCurrentWorkdirRevision } from "./workdir-revision.ts";
import { formatCurrentWorkdirCheckout } from "./workdir-checkout.ts";
declare const Deno: {
test(name: string, fn: () => Promise<void> | void): void;
@@ -32,7 +32,7 @@ function workdir(
Deno.test("Git detached Workdir shows only its current ref", () => {
assertEquals(
formatCurrentWorkdirRevision(
formatCurrentWorkdirCheckout(
workdir(null, "0123456789abcdef0123456789abcdef01234567"),
"git",
),
@@ -42,7 +42,7 @@ Deno.test("Git detached Workdir shows only its current ref", () => {
Deno.test("Git Workdir with a selector shows selector at current ref", () => {
assertEquals(
formatCurrentWorkdirRevision(
formatCurrentWorkdirCheckout(
workdir("feature/current", "fedcba9876543210fedcba9876543210fedcba98"),
"git",
),
@@ -52,7 +52,7 @@ Deno.test("Git Workdir with a selector shows selector at current ref", () => {
Deno.test("non-Git Workdir does not receive Git hash formatting", () => {
assertEquals(
formatCurrentWorkdirRevision(
formatCurrentWorkdirCheckout(
workdir("snapshot", "revision-value"),
"archive",
),
@@ -1,6 +1,6 @@
import type { WorkingDirectorySummary } from "../sidebar/types.ts";
export function formatCurrentWorkdirRevision(
export function formatCurrentWorkdirCheckout(
workdir: WorkingDirectorySummary,
repositoryProvider: string | null | undefined,
): string {
@@ -239,7 +239,7 @@ export async function deleteSidebarWorker(
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
expected_plan_revision: plan.revision,
expected_plan_digest: plan.digest,
worker_target_ids: [candidate.target_id],
workdir_target_ids: [],
@@ -138,10 +138,11 @@ describe("shared catalog observation refresh", () => {
latest = state
);
mock.handlers.onFrame(snapshot());
const before = latest.observationVersion;
const before = latest.catalogRequest;
const old = pending.at(-1)!;
mock.handlers.onFrame(snapshot("idle", "generation-2"));
expect(latest.observationVersion).toBeGreaterThan(before);
expect(latest.catalogRequest).not.toBe(before);
expect(before?.aborted).toBe(true);
expect(latest.workers[0].lifecycleState).toBe("idle");
pending.at(-1)!(response("idle", "generation-2"));
await settle();
@@ -163,9 +164,10 @@ describe("shared catalog observation refresh", () => {
);
mock.handlers.onFrame(snapshot());
await settle();
const before = latest.observationVersion;
const before = latest.catalogRequest;
await refreshWorkspaceWorkers("team");
expect(latest.observationVersion).toBeGreaterThan(before);
expect(latest.catalogRequest).not.toBe(before);
expect(before?.aborted).toBe(true);
expect(fetchMock.mock.calls.every(([url]) => !url.endsWith("/restore")))
.toBe(true);
});
@@ -369,7 +369,7 @@ test("sidebar Delete cannot proceed to execution when its cleanup plan arrives a
workspace_id: "stop-test",
runtime_id: "runtime-a",
generated_at: "2026-10-06T00:00:00Z",
revision: "old-plan",
digest: "old-digest",
workdirs: [],
diagnostics: [],
@@ -34,8 +34,8 @@ export type WorkspaceWorkersState = {
workers: SidebarWorker[];
/** Full catalog metadata; null until fetched. Never substitutes sidebar display state. */
catalogWorkers: Worker[] | null;
/** Changes immediately when an observation invalidates dependent cleanup plans. */
observationVersion: number;
/** Actual catalog GET identity; changes before dependent cleanup plans can settle. */
catalogRequest: AbortSignal | null;
catalogRefreshing: boolean;
};
@@ -108,7 +108,7 @@ export function workspaceWorkersStore(
loading: true,
workers: [],
catalogWorkers: null,
observationVersion: 0,
catalogRequest: null,
catalogRefreshing: true,
},
(set) => {
@@ -117,7 +117,7 @@ export function workspaceWorkersStore(
loading: false,
workers: [],
catalogWorkers: [],
observationVersion: 0,
catalogRequest: null,
catalogRefreshing: false,
});
return;
@@ -130,9 +130,7 @@ export function workspaceWorkersStore(
let loading = true;
let disposed = false;
let catalogWorkers: Worker[] | null = null;
let observationVersion = 0;
let catalogRefreshing = true;
let refreshSequence = 0;
let catalogAbort: AbortController | null = null;
const publish = (): boolean => {
if (disposed) return false;
@@ -144,7 +142,7 @@ export function workspaceWorkersStore(
loading,
workers,
catalogWorkers,
observationVersion,
catalogRequest: catalogAbort?.signal ?? null,
catalogRefreshing,
});
return true;
@@ -160,7 +158,7 @@ export function workspaceWorkersStore(
loading: false,
workers,
catalogWorkers,
observationVersion,
catalogRequest: catalogAbort?.signal ?? null,
catalogRefreshing,
});
return false;
@@ -168,8 +166,6 @@ export function workspaceWorkersStore(
};
const refreshCatalog = async () => {
if (disposed) return;
const sequence = ++refreshSequence;
const version = ++observationVersion;
catalogAbort?.abort();
const abort = new AbortController();
catalogAbort = abort;
@@ -183,8 +179,7 @@ export function workspaceWorkersStore(
{ diagnosticLabel: "Worker API", maxResponseBytes: 8 * 1024 * 1024 },
);
if (
disposed || sequence !== refreshSequence ||
version !== observationVersion
disposed || abort.signal.aborted || catalogAbort !== abort
) return;
if (!result.data || result.data.workspace_id !== workspaceId) {
reportWorkerFailure(
+4 -17
View File
@@ -167,14 +167,10 @@ function parseCatalogEntry(
function parseProjection(value: unknown): SkillProjectionIdentity {
const record = strictObject(
value,
["config_revision", "tree_digest"],
["tree_digest"],
"Skill projection identity",
);
return {
config_revision: safeInteger(
record.config_revision,
"Skill config revision",
),
tree_digest: boundedString(
record.tree_digest,
"Skill tree digest",
@@ -205,14 +201,12 @@ function parseProvenance(
"kind",
"id",
"virtual_path",
"revision",
"source_digest",
"tree_digest",
],
"Skill provenance",
[
"virtual_path",
"revision",
"source_digest",
"tree_digest",
],
@@ -239,9 +233,6 @@ function parseProvenance(
"Skill provenance tree digest",
SKILL_API_LIMITS.maxDigestBytes,
);
const revision = record.revision === undefined
? undefined
: safeInteger(record.revision, "Skill provenance revision");
if (
!id.startsWith(`${kind}:`) || virtualPath === undefined ||
@@ -250,17 +241,14 @@ function parseProvenance(
throw contractError("invalid Skill provenance");
}
if (kind === "builtin") {
if (revision !== undefined || treeDigest !== undefined) {
if (treeDigest !== undefined) {
throw contractError("invalid built-in Skill provenance");
}
} else {
if (revision === undefined || treeDigest === undefined) {
if (treeDigest === undefined) {
throw contractError("incomplete Workspace Skill provenance");
}
if (
revision !== projection.config_revision ||
treeDigest !== projection.tree_digest
) {
if (treeDigest !== projection.tree_digest) {
throw contractError("stale Workspace Skill projection");
}
}
@@ -269,7 +257,6 @@ function parseProvenance(
kind,
id,
virtual_path: virtualPath,
revision,
source_digest: sourceDigest,
tree_digest: treeDigest,
};
@@ -228,7 +228,7 @@
<details class="disclosure">
<summary>Technical details</summary>
<dl class="technical-values">
<div><dt>Revision</dt><dd><code>rev_01J8WZ7C8AV6P3Y2N0M4KQ9B1F</code></dd></div>
<div><dt>Observed</dt><dd><time datetime="2026-10-09T14:32:00Z">2026-10-09 14:32 UTC</time></dd></div>
<BevelLine as="div" direction="x" length="100%" decorative />
<div><dt>Digest</dt><dd><code>sha256:2f8d9bb168afb7b19f447463f3fbe8b4728d8e87a64eaf1afbea1ca4d4c41f21</code></dd></div>
</dl>
@@ -45,7 +45,7 @@
const workspaceId = subjectWorkspaceId;
const subjectId = subject.id;
const request = {
expected_behavior_revision: subject.behavior_revision,
expected_behavior_md: subject.behavior_md,
behavior_md: behaviorDraft,
};
const isCurrent = () => generation === routeGeneration &&
@@ -94,11 +94,12 @@
return workspaceRoute(data.workspaceId, '/memory');
}
function memoryHref(memoryId: string): string {
return workspaceRoute(
function memoryHref(memoryId: string, changeId?: string): string {
const path = workspaceRoute(
data.workspaceId,
`/memory/${encodeURIComponent(data.subjectId)}/${encodeURIComponent(memoryId)}`,
);
return changeId ? `${path}?change_id=${encodeURIComponent(changeId)}` : path;
}
function memoryPageHref(cursor?: string | null): string {
@@ -192,8 +193,8 @@
<dl>
<div><dt>Subject ID</dt><dd><code>{subject.id}</code></dd></div>
<div>
<dt>Subject store revision</dt>
<dd>{subject.store_revision}<small>Internal change number for committed Memories; not a Memory count or content-quality score.</small></dd>
<dt>Memory fingerprint</dt>
<dd>{subject.memory_fingerprint}<small>Content condition for current committed Memories; not a Memory count or content-quality score.</small></dd>
</div>
<div><dt>Last changed</dt><dd><time datetime={subject.updated_at}>{formatDate(subject.updated_at)}</time></dd></div>
</dl>
@@ -218,7 +219,7 @@
<button type="button" class="behavior-edit" onclick={beginBehaviorEdit}>Edit</button>
{/if}
</header>
<p class="behavior-help">The Host injects this text verbatim for connected Workers, separately from generated Memory. Saving confirms storage only. Connected Workers check for the latest revision before later model requests; this page does not report that application.</p>
<p class="behavior-help">The Host injects this text verbatim for connected Workers, separately from generated Memory. Saving confirms storage only. Connected Workers check for the latest text before later model requests; this page does not report that application.</p>
{#if editingBehavior}
<form class="behavior-form" aria-busy={behaviorSaving} onsubmit={(event) => { event.preventDefault(); void saveBehavior(); }}>
<label>
@@ -237,7 +238,7 @@
{:else}
<div class="memory-state" role="status"><strong>No behavior is set.</strong><p>Connected Workers receive an explicit empty behavior document.</p></div>
{/if}
{#if behaviorSaveConfirmed}<p class="behavior-saved" role="status">Behavior storage confirmed at revision {subject.behavior_revision}. This page does not confirm application by a connected Worker.</p>{/if}
{#if behaviorSaveConfirmed}<p class="behavior-saved" role="status">Behavior storage confirmed. This page does not confirm application by a connected Worker.</p>{/if}
</section>
{/if}
@@ -273,18 +274,18 @@
<dl>
<div><dt>Snapshot ID</dt><dd><code>{snapshot.snapshot_id}</code></dd></div>
<div>
<dt>Generated from Subject revision</dt>
<dd>{snapshot.built_from_store_revision}<small>The Subject change number used for this surface, distinct from each Memory’s revision.</small></dd>
<dt>Generated from Memory fingerprint</dt>
<dd>{snapshot.built_from_memory_fingerprint}<small>The content condition used to generate this surface.</small></dd>
</div>
<div>
<dt>Referenced Memory revisions</dt>
<dt>Referenced Memory changes</dt>
<dd>
{#if snapshot.memory_refs.length === 0}
None
{:else}
<ul class="reference-list">
{#each snapshot.memory_refs as reference (`${reference.memory_id}:${reference.revision}`)}
<li><a href={memoryHref(reference.memory_id)}><code>{reference.memory_id}</code> · revision {reference.revision}</a></li>
{#each snapshot.memory_refs as reference (`${reference.memory_id}:${reference.change_id}`)}
<li><a href={memoryHref(reference.memory_id, reference.change_id)}><code>{reference.memory_id}</code> · change {reference.change_id}</a></li>
{/each}
</ul>
{/if}
@@ -347,7 +348,7 @@
</div>
<dl>
<div><dt>Memory ID</dt><dd><code title={memory.id}>{memory.id}</code></dd></div>
<div><dt>Memory revision</dt><dd>{memory.revision}</dd></div>
<div><dt>Memory change</dt><dd>{memory.change_id}</dd></div>
<div><dt>Last changed</dt><dd><time datetime={memory.updated_at}>{formatDate(memory.updated_at)}</time></dd></div>
</dl>
</a>
@@ -3,7 +3,7 @@
import { formatDate, workspaceRoute } from '#lib/workspace/api/http.ts';
import type {
MemoryEvidenceOrigin,
SubjektivMemoryRevisionRef,
SubjektivMemoryChangeRef,
SubjektivMemorySourceEvidenceRef,
} from '#lib/generated/memory-api.ts';
import type { PageProps } from './$types';
@@ -11,7 +11,7 @@
let { data }: PageProps = $props();
const memory = $derived(data.memory.data);
const revisions = $derived(data.revisions.data?.items ?? []);
const changes = $derived(data.changes.data?.items ?? []);
function subjectHref(): string {
return workspaceRoute(data.workspaceId, `/memory/${encodeURIComponent(data.subjectId)}`);
@@ -24,26 +24,26 @@
);
}
function revisionHref(revision: number): string {
return `${memoryPath()}?revision=${revision}`;
function changeHref(changeId: string): string {
return `${memoryPath()}?change_id=${encodeURIComponent(changeId)}`;
}
function revisionPageHref(cursor?: string | null): string {
function changePageHref(cursor?: string | null): string {
const query = new URLSearchParams();
if (memory) query.set('revision', String(memory.revision));
if (cursor) query.set('revision_cursor', cursor);
if (memory) query.set('change_id', String(memory.change_id));
if (cursor) query.set('change_cursor', cursor);
const encoded = query.toString();
return encoded ? `${memoryPath()}?${encoded}` : memoryPath();
}
function derivedHref(reference: SubjektivMemoryRevisionRef): string {
return `${memoryPath(reference.memory_id)}?revision=${reference.revision}`;
function derivedHref(reference: SubjektivMemoryChangeRef): string {
return `${memoryPath(reference.memory_id)}?change_id=${encodeURIComponent(reference.change_id)}`;
}
function bodyContinuationHref(): string | null {
if (!memory?.body_truncated || memory.body_next_offset == null || memory.body_next_byte_offset == null) return null;
const query = new URLSearchParams({
revision: String(memory.revision),
change_id: String(memory.change_id),
offset: String(memory.body_next_offset),
byte_offset: String(memory.body_next_byte_offset),
});
@@ -53,7 +53,7 @@
function evidenceContinuationHref(): string | null {
if (!memory?.evidence_has_more || !memory.evidence_next_cursor) return null;
const query = new URLSearchParams({
revision: String(memory.revision),
change_id: String(memory.change_id),
evidence_cursor: memory.evidence_next_cursor,
});
return `${memoryPath()}?${query}`;
@@ -81,7 +81,6 @@
origin.worker_id && `worker ${origin.worker_id}`,
origin.flow_selector && `flow ${origin.flow_selector}`,
origin.flow_definition_id && `definition ${origin.flow_definition_id}`,
origin.flow_definition_revision != null && `definition revision ${origin.flow_definition_revision}`,
].filter(Boolean);
return `${label(origin.kind)}${identity.length ? ` · ${identity.join(' · ')}` : ''}`;
}
@@ -93,7 +92,7 @@
<svelte:head>
<title>{memory?.claim ?? data.memoryId} · Memory · Yoi Workspace</title>
<meta name="description" content="Committed Memory detail, immutable revisions, and provenance" />
<meta name="description" content="Committed Memory detail, immutable changes, and provenance" />
</svelte:head>
<section class="memory-page memory-detail-page" aria-labelledby="memory-detail-heading" data-memory-view="detail" data-memory-state={data.memory.error ? 'error' : memory ? memory.state : 'unavailable'}>
@@ -105,7 +104,7 @@
<div class="pill-row">
<span class="memory-pill is-{memory.state}">{titleLabel(memory.state)}</span>
<span class="memory-kind">{titleLabel(memory.kind)}</span>
{#if memory.revision !== memory.current_revision}<span class="historical-pill">Historical revision</span>{/if}
{#if memory.change_id !== memory.current_change_id}<span class="historical-pill">Historical change</span>{/if}
</div>
<h1 id="memory-detail-heading">{memory.claim}</h1>
<code class="memory-id" title={memory.memory_id}>{memory.memory_id}</code>
@@ -114,7 +113,7 @@
</header>
<dl class="memory-facts" aria-label="Memory details">
<div><dt>Memory revision</dt><dd>{memory.revision} of {memory.current_revision}</dd></div>
<div><dt>Memory change</dt><dd><code>{memory.change_id}</code><small>{memory.change_id === memory.current_change_id ? 'Current change' : `Current change: ${memory.current_change_id}`}</small></dd></div>
<div><dt>Status</dt><dd>{titleLabel(memory.state)}</dd></div>
<div><dt>Type</dt><dd>{titleLabel(memory.kind)}</dd></div>
<div><dt>Created</dt><dd><time datetime={memory.created_at}>{formatDate(memory.created_at)}</time></dd></div>
@@ -223,48 +222,48 @@
<p class="empty-copy">No derivation references.</p>
{:else}
<ul class="derived-list">
{#each memory.derived_from as reference (`${reference.memory_id}:${reference.revision}`)}
<li><a href={derivedHref(reference)}><code>{reference.memory_id}</code><span>Revision {reference.revision}</span></a></li>
{#each memory.derived_from as reference (`${reference.memory_id}:${reference.change_id}`)}
<li><a href={derivedHref(reference)}><code>{reference.memory_id}</code><span>Change {reference.change_id}</span></a></li>
{/each}
</ul>
{/if}
</section>
<section class="detail-section revision-section" aria-labelledby="revision-heading">
<section class="detail-section change-section" aria-labelledby="change-heading">
<header class="section-heading">
<div><p class="memory-eyebrow">Immutable record</p><h2 id="revision-heading">Revision history</h2></div>
{#if data.revisions.data}<span>{revisions.length} shown{data.revisions.data.has_more ? ' · more available' : ''}</span>{/if}
<div><p class="memory-eyebrow">Immutable record</p><h2 id="change-heading">Change history</h2></div>
{#if data.changes.data}<span>{changes.length} shown{data.changes.data.has_more ? ' · more available' : ''}</span>{/if}
</header>
<p class="section-intro">These are versions of this Memory record, separate from the Subject’s internal update number.</p>
{#if data.revisions.data}
{#if revisions.length === 0}
<p class="section-intro">These are immutable changes to this Memory record. Change IDs identify content; they do not imply a numeric order.</p>
{#if data.changes.data}
{#if changes.length === 0}
<div>
<p class="empty-copy">No revision history is available.</p>
{#if data.revisionCursor}<p><a href={revisionPageHref()}>Return to the first page</a></p>{/if}
<p class="empty-copy">No change history is available.</p>
{#if data.changeCursor}<p><a href={changePageHref()}>Return to the first page</a></p>{/if}
</div>
{:else}
<ol class="revision-list">
{#each revisions as revision (revision.revision)}
<li class:current={revision.revision === memory.revision}>
<a href={revisionHref(revision.revision)} aria-current={revision.revision === memory.revision ? 'page' : undefined}>
<div><strong>Memory revision {revision.revision}</strong><span class="memory-pill is-{revision.state}">{titleLabel(revision.state)}</span><span class="memory-kind">{titleLabel(revision.kind)}</span></div>
<p>{revision.claim}</p>
<small>{revision.change_reason} · <time datetime={revision.updated_at}>{formatDate(revision.updated_at)}</time></small>
<ol class="change-list">
{#each changes as change (change.change_id)}
<li class:current={change.change_id === memory.change_id}>
<a href={changeHref(change.change_id)} aria-current={change.change_id === memory.change_id ? 'page' : undefined}>
<div><strong>Memory change {change.change_id}</strong>{#if change.change_id === memory.current_change_id}<span>Current</span>{/if}<span class="memory-pill is-{change.state}">{titleLabel(change.state)}</span><span class="memory-kind">{titleLabel(change.kind)}</span></div>
<p>{change.claim}</p>
<small>{change.change_reason} · <time datetime={change.updated_at}>{formatDate(change.updated_at)}</time></small>
</a>
</li>
{/each}
</ol>
<nav class="memory-pagination" aria-label="Revision history pages">
{#if data.revisionCursor}<a href={revisionPageHref()}>First page</a>{/if}
{#if data.revisions.data.has_more && data.revisions.data.next_cursor}
<a href={revisionPageHref(data.revisions.data.next_cursor)}>Next page →</a>
<nav class="memory-pagination" aria-label="Change history pages">
{#if data.changeCursor}<a href={changePageHref()}>First page</a>{/if}
{#if data.changes.data.has_more && data.changes.data.next_cursor}
<a href={changePageHref(data.changes.data.next_cursor)}>Next page →</a>
{/if}
</nav>
{/if}
{:else if data.revisions.error}
<div class="memory-state is-error" role="alert"><strong>Revision history unavailable.</strong><p>{data.revisions.error}</p></div>
{:else if data.changes.error}
<div class="memory-state is-error" role="alert"><strong>Change history unavailable.</strong><p>{data.changes.error}</p></div>
{:else}
<div class="memory-state" role="status"><p>Revision history data is unavailable.</p></div>
<div class="memory-state" role="status"><p>Change history data is unavailable.</p></div>
{/if}
</section>
{:else if data.memory.error}
@@ -346,7 +345,7 @@
}
.pill-row,
.revision-list a > div,
.change-list a > div,
.evidence-heading {
display: flex;
flex-wrap: wrap;
@@ -408,6 +407,8 @@
white-space: normal;
}
.memory-facts small { display: block; margin-top: var(--space-1); color: var(--text-muted); }
.memory-facts dd {
margin-top: var(--space-1);
overflow-wrap: anywhere;
@@ -503,7 +504,7 @@
.evidence-list,
.source-list,
.derived-list,
.revision-list {
.change-list {
display: grid;
gap: var(--space-2);
margin: 0;
@@ -564,8 +565,8 @@
.derived-list a:hover,
.derived-list a:focus-visible,
.revision-list a:hover,
.revision-list a:focus-visible {
.change-list a:hover,
.change-list a:focus-visible {
background: var(--interactive-hover);
}
@@ -578,19 +579,19 @@
font-size: var(--font-size-compact);
}
.revision-list {
.change-list {
border-top: 1px solid var(--line);
}
.revision-list li {
.change-list li {
border-bottom: 1px solid var(--line);
}
.revision-list li.current {
.change-list li.current {
border-left: 3px solid var(--accent);
}
.revision-list a {
.change-list a {
display: grid;
gap: var(--space-1);
min-width: 0;
@@ -599,13 +600,14 @@
text-decoration: none;
}
.revision-list p,
.revision-list small {
.change-list strong,
.change-list p,
.change-list small {
margin: 0;
overflow-wrap: anywhere;
}
.revision-list small {
.change-list small {
color: var(--text-muted);
}
@@ -1,7 +1,7 @@
import { loadJson, workspaceApiPath } from "#lib/workspace/api/http.ts";
import {
MEMORY_API_LOAD_POLICY,
parseSubjektivMemoryListRevisionsResponse,
parseSubjektivMemoryListChangesResponse,
parseSubjektivMemoryReadResponse,
parseSubjektivSubjectResponse,
} from "#lib/workspace/memory/api.ts";
@@ -40,7 +40,6 @@ export const load: PageLoad = async ({ fetch, params, url }) => {
const detailQuery = new URLSearchParams({ limit: "1000" });
for (
const [key, positive] of [
["revision", true],
["offset", false],
["byte_offset", false],
] as const
@@ -48,16 +47,18 @@ export const load: PageLoad = async ({ fetch, params, url }) => {
const value = safeIntegerQuery(url.searchParams, key, positive);
if (value !== null) detailQuery.set(key, value);
}
const changeId = boundedCursor(url.searchParams.get("change_id"), 512);
if (changeId) detailQuery.set("change_id", changeId);
const evidenceCursor = boundedCursor(
url.searchParams.get("evidence_cursor"),
512,
);
if (evidenceCursor) detailQuery.set("evidence_cursor", evidenceCursor);
const revisionCursor = boundedCursor(url.searchParams.get("revision_cursor"));
const revisionQuery = new URLSearchParams({ limit: "100" });
if (revisionCursor) revisionQuery.set("cursor", revisionCursor);
const changeCursor = boundedCursor(url.searchParams.get("change_cursor"));
const changeQuery = new URLSearchParams({ limit: "100" });
if (changeCursor) changeQuery.set("cursor", changeCursor);
const [subject, memory, revisions] = await Promise.all([
const [subject, memory, changes] = await Promise.all([
loadJson(
fetch,
workspaceApiPath(params.workspaceId, subjectPath),
@@ -75,10 +76,10 @@ export const load: PageLoad = async ({ fetch, params, url }) => {
loadJson(
fetch,
`${
workspaceApiPath(params.workspaceId, `${memoryPath}/revisions`)
}?${revisionQuery}`,
workspaceApiPath(params.workspaceId, `${memoryPath}/changes`)
}?${changeQuery}`,
undefined,
(value) => parseSubjektivMemoryListRevisionsResponse(value, memoryId),
(value) => parseSubjektivMemoryListChangesResponse(value, memoryId),
MEMORY_API_LOAD_POLICY,
),
]);
@@ -87,9 +88,9 @@ export const load: PageLoad = async ({ fetch, params, url }) => {
workspaceId: params.workspaceId,
subjectId,
memoryId,
revisionCursor,
changeCursor,
subject,
memory,
revisions,
changes,
};
};
@@ -35,9 +35,8 @@ function subject() {
id: "subject-1",
role: "Release coordinator",
behavior_md: "Prefer explicit evidence.",
behavior_revision: 2,
state: "active" as const,
store_revision: 9,
memory_fingerprint: "fingerprint-9",
created_at: "2026-01-01T00:00:00Z",
updated_at: "2026-01-02T03:04:05Z",
};
@@ -122,9 +121,9 @@ test("creates one Subject with the exact role and navigates to its generated det
id: "generated-subject-1",
role: " Review lead ",
behavior_md: "Challenge unsupported assumptions.",
behavior_revision: 0,
state: "active",
store_revision: 0,
memory_fingerprint: "fingerprint-0",
created_at: "2026-01-03T00:00:00Z",
updated_at: "2026-01-03T00:00:00Z",
}, { status: 201 }));
@@ -254,8 +253,8 @@ test("renders a ready resident surface and committed Memory lifecycle states", a
snapshot_id: "snapshot-1",
body_md:
"# Resident context\n\nUse the **current** committed record.",
memory_refs: [{ memory_id: "memory-1", revision: 3 }],
built_from_store_revision: 9,
memory_refs: [{ memory_id: "memory-1", change_id: "change-current" }],
built_from_memory_fingerprint: "fingerprint-9",
created_at: "2026-01-02T03:04:05Z",
},
}),
@@ -263,7 +262,7 @@ test("renders a ready resident surface and committed Memory lifecycle states", a
items: [
{
id: "memory-1",
revision: 3,
change_id: "change-current",
kind: "decision" as const,
state: "active" as const,
claim: "Current decision",
@@ -272,7 +271,7 @@ test("renders a ready resident surface and committed Memory lifecycle states", a
},
{
id: "memory-2",
revision: 2,
change_id: "change-earlier",
kind: "lesson" as const,
state: "resolved" as const,
claim: "Resolved lesson",
@@ -281,7 +280,7 @@ test("renders a ready resident surface and committed Memory lifecycle states", a
},
{
id: "memory-3",
revision: 1,
change_id: "change-original",
kind: "constraint" as const,
state: "retracted" as const,
claim: "Retracted constraint",
@@ -309,7 +308,7 @@ test("renders a ready resident surface and committed Memory lifecycle states", a
expect(screen.getByText("Connected", { exact: true })).not.toBeNull();
expect(screen.getByText("Release Worker", { exact: true })).not.toBeNull();
expect(screen.getByText("3 on this page", { exact: true })).not.toBeNull();
expect(screen.getByText("Subject store revision", { exact: true })).not
expect(screen.getByText("Memory fingerprint", { exact: true })).not
.toBeNull();
expect(screen.getByText(/not a Memory count/)).not.toBeNull();
expect(screen.getAllByText("Active", { exact: true }).length).toBeGreaterThan(
@@ -329,7 +328,6 @@ test("edits and clears user-managed behavior with CAS while distinguishing save
return Response.json({
...subject(),
behavior_md: request.behavior_md,
behavior_revision: request.expected_behavior_revision + 1,
});
});
vi.stubGlobal("fetch", fetchMock);
@@ -360,11 +358,11 @@ test("edits and clears user-managed behavior with CAS while distinguishing save
expect(path).toBe("/api/w/workspace-1/subjektiv/subjects/subject-1/behavior");
expect(init.method).toBe("PATCH");
expect(init.body).toBe(
'{"expected_behavior_revision":2,"behavior_md":"Ask before irreversible actions."}',
'{"expected_behavior_md":"Prefer explicit evidence.","behavior_md":"Ask before irreversible actions."}',
);
await waitFor(() => {
expect(
screen.getByText(/Behavior storage confirmed at revision 3/).textContent,
screen.getByText(/Behavior storage confirmed/).textContent,
)
.toContain("does not confirm application");
});
@@ -380,7 +378,7 @@ test("edits and clears user-managed behavior with CAS while distinguishing save
await waitFor(() => expect(fetchMock).toHaveBeenCalledTimes(2));
const secondBody =
(fetchMock.mock.calls[1] as unknown as [string, RequestInit])[1].body;
expect(secondBody).toBe('{"expected_behavior_revision":3,"behavior_md":""}');
expect(secondBody).toBe('{"expected_behavior_md":"Ask before irreversible actions.","behavior_md":""}');
await waitFor(() =>
expect(screen.getByText("No behavior is set.")).not.toBeNull()
);
@@ -411,7 +409,6 @@ test.each([
return Response.json({
...nextSubject,
behavior_md: request.behavior_md,
behavior_revision: request.expected_behavior_revision + 1,
});
});
vi.stubGlobal("fetch", fetchMock);
@@ -453,11 +450,11 @@ test.each([
`/api/w/${workspaceId}/subjektiv/subjects/${id}/behavior`,
);
expect(JSON.parse(String(init.body))).toEqual({
expected_behavior_revision: 2,
expected_behavior_md: "Only B behavior.",
behavior_md: "Only B behavior.",
});
await waitFor(() =>
expect(screen.getByText(/storage confirmed at revision 3/)).not.toBeNull()
expect(screen.getByText(/storage confirmed/)).not.toBeNull()
);
},
);
@@ -498,8 +495,7 @@ test.each(["success", "failure"])(
outcome === "success"
? Response.json({
...subject(),
behavior_md: "Late A response",
behavior_revision: 3,
behavior_md: "Prefer explicit evidence.",
})
: Response.json({
error: "Forbidden",
@@ -524,11 +520,10 @@ test.each(["success", "failure"])(
Response.json({
...nextSubject,
behavior_md: "Updated B behavior.",
behavior_revision: 3,
}),
);
await waitFor(() =>
expect(screen.getByText(/storage confirmed at revision 3/)).not.toBeNull()
expect(screen.getByText(/storage confirmed/)).not.toBeNull()
);
expect(
screen.getByRole("article", { name: "User-managed Subject behavior" })
@@ -557,14 +552,12 @@ test("a refreshed loader snapshot fences old saves even after returning to the s
const fresh = {
...subject(),
behavior_md: "Fresh A loader",
behavior_revision: 4,
};
await view.rerender({ data: subjectPageData(fresh) } as never);
resolveRequest(
Response.json({
...subject(),
behavior_md: "Stale A response",
behavior_revision: 3,
behavior_md: "Prefer explicit evidence.",
}),
);
await new Promise((resolve) => setTimeout(resolve, 0));
@@ -589,6 +582,28 @@ test("a refreshed loader snapshot fences old saves even after returning to the s
).toBeNull();
});
test.each([409, 503])("behavior save rejection %s preserves the draft and observed CAS text", async (status) => {
const fetchMock = vi.fn(async () => Response.json({ message: "save rejected" }, { status }));
vi.stubGlobal("fetch", fetchMock);
render(SubjectPage, { data: subjectPageData() } as never);
await fireEvent.click(screen.getByRole("button", { name: "Edit" }));
const textarea = screen.getByRole("textbox", { name: "Subject behavior" });
await fireEvent.input(textarea, { target: { value: " Unsaved\nbehavior.\n" } });
await fireEvent.click(screen.getByRole("button", { name: "Save behavior" }));
await waitFor(() => expect(screen.getByRole("alert")).not.toBeNull());
expect(textarea).toHaveProperty("value", " Unsaved\nbehavior.\n");
expect(screen.queryByText(/storage confirmed/)).toBeNull();
expect(fetchMock).toHaveBeenCalledTimes(1);
expect(fetchMock.mock.calls[0]).toEqual([
"/api/w/workspace-1/subjektiv/subjects/subject-1/behavior",
{
method: "PATCH",
headers: { "content-type": "application/json" },
body: JSON.stringify({ expected_behavior_md: "Prefer explicit evidence.", behavior_md: " Unsaved\nbehavior.\n" }),
},
]);
});
test("candidate-only Subjects show no committed Memories, not an inferred candidate count", () => {
// The store regression candidate_decisions_reject_conflicts_and_roll_back_partial_writes
// stages a candidate without committing a Memory. Its public read projection is
@@ -598,7 +613,7 @@ test("candidate-only Subjects show no committed Memories, not an inferred candid
workspaceId: "workspace-1",
subjectId: "subject-1",
cursor: null,
subject: result({ ...subject(), store_revision: 0 }),
subject: result({ ...subject(), memory_fingerprint: "fingerprint-0" }),
surface: result({ subject_id: "subject-1", availability: "ungenerated" }),
memories: result({ limit: 100, items: [], has_more: false }),
},
@@ -620,7 +635,7 @@ test("candidate-only Subjects show no committed Memories, not an inferred candid
test.each(["ready", "ungenerated", "stale", "failed"] as const)(
"user behavior and diagnostic disclosures stay independent of %s generated Memory",
async (availability) => {
const record = { ...subject(), store_revision: 0 };
const record = { ...subject(), memory_fingerprint: "fingerprint-0" };
const view = render(SubjectPage, {
data: {
...subjectPageData(record),
@@ -633,7 +648,7 @@ test.each(["ready", "ungenerated", "stale", "failed"] as const)(
snapshot_id: "empty-snapshot",
body_md: "",
memory_refs: [],
built_from_store_revision: 0,
built_from_memory_fingerprint: "fingerprint-0",
created_at: "2026-01-02T03:04:05Z",
},
}
@@ -654,7 +669,7 @@ test.each(["ready", "ungenerated", "stale", "failed"] as const)(
"details.subject-technical-details",
) as HTMLDetailsElement;
expect(technical.open).toBe(false);
expect(technical.textContent).toContain("Subject store revision");
expect(technical.textContent).toContain("Memory fingerprint");
if (availability === "ready") {
expect(screen.getByText("Resident context is current but empty.")).not
.toBeNull();
@@ -663,7 +678,7 @@ test.each(["ready", "ungenerated", "stale", "failed"] as const)(
expect(
view.container.querySelector("details.surface-details")?.textContent,
)
.toContain("Generated from Subject revision");
.toContain("Generated from Memory fingerprint");
}
await fireEvent.click(screen.getByRole("button", { name: "Edit" }));
await fireEvent.click(screen.getByRole("button", { name: "Cancel" }));
@@ -693,7 +708,7 @@ test("distinguishes ready-empty, stale, failed, unavailable, and request error s
snapshot_id: "snapshot-empty",
body_md: "",
memory_refs: [],
built_from_store_revision: 9,
built_from_memory_fingerprint: "fingerprint-9",
created_at: "2026-01-02T03:04:05Z",
},
}),
@@ -760,18 +775,18 @@ test("distinguishes ready-empty, stale, failed, unavailable, and request error s
);
});
test("renders committed Memory detail, provenance, derivation, and immutable revisions", async () => {
test("renders committed Memory detail, provenance, derivation, and immutable changes", async () => {
render(MemoryDetailPage, {
data: {
workspaceId: "workspace-1",
subjectId: "subject-1",
memoryId: "memory-1",
revisionCursor: "revision-current",
changeCursor: "a/current",
subject: result(subject()),
memory: result({
memory_id: "memory-1",
revision: 2,
current_revision: 3,
change_id: "z/earlier & context",
current_change_id: "a/current",
kind: "decision" as const,
state: "resolved" as const,
claim: "Keep provenance typed",
@@ -782,7 +797,9 @@ test("renders committed Memory detail, provenance, derivation, and immutable rev
updated_at: "2026-01-02T03:04:05Z",
body_offset: 0,
body_byte_offset: 0,
body_truncated: false,
body_truncated: true,
body_next_offset: 2,
body_next_byte_offset: 0,
source_candidate_ids: ["candidate-1"],
source_candidates: [{
candidate_id: "candidate-1",
@@ -802,28 +819,29 @@ test("renders committed Memory detail, provenance, derivation, and immutable rev
source_refs_total: 1,
source_refs_truncated: false,
}],
derived_from: [{ memory_id: "memory-parent", revision: 4 }],
evidence_has_more: false,
derived_from: [{ memory_id: "memory-parent", change_id: "parent/source & context" }],
evidence_has_more: true,
evidence_next_cursor: "evidence/next",
}),
revisions: result({
changes: result({
memory_id: "memory-1",
current_revision: 3,
current_change_id: "a/current",
items: [{
revision: 3,
change_id: "a/current",
kind: "decision" as const,
state: "active" as const,
claim: "Current claim",
change_reason: "Clarified",
updated_at: "2026-01-03T00:00:00Z",
}, {
revision: 2,
change_id: "z/earlier & context",
kind: "decision" as const,
state: "resolved" as const,
claim: "Earlier claim",
change_reason: "Resolved",
updated_at: "2026-01-02T00:00:00Z",
}],
next_cursor: "revision-next",
next_cursor: "change-next",
has_more: true,
}),
},
@@ -836,11 +854,29 @@ test("renders committed Memory detail, provenance, derivation, and immutable rev
expect(screen.getByText("candidate-1")).not.toBeNull();
expect(screen.getByText("Decision discussion")).not.toBeNull();
expect(screen.getByText("memory-parent")).not.toBeNull();
expect(screen.getByRole("heading", { name: "Revision history" })).not
expect(screen.getByRole("heading", { name: "Change history" })).not
.toBeNull();
expect(screen.getByText("Historical revision")).not.toBeNull();
expect(screen.getByRole("navigation", { name: "Revision history pages" }))
expect(screen.getByText("Historical change")).not.toBeNull();
expect(screen.getByRole("navigation", { name: "Change history pages" }))
.not.toBeNull();
expect(screen.getByRole("link", { name: /Next page/ }).getAttribute("href"))
.toContain("revision_cursor=revision-next");
const historicalLink = screen.getByRole("link", { name: /Memory change z\/earlier/ });
expect(historicalLink.getAttribute("aria-current")).toBe("page");
expect(historicalLink.getAttribute("href")).toBe("/w/workspace-1/memory/subject-1/memory-1?change_id=z%2Fearlier%20%26%20context");
const currentLink = screen.getByRole("link", { name: /Memory change a\/current/ });
expect(currentLink.getAttribute("aria-current")).toBeNull();
expect(currentLink.textContent).toContain("Current");
expect(screen.getByRole("link", { name: /memory-parent/ }).getAttribute("href")).toBe("/w/workspace-1/memory/subject-1/memory-parent?change_id=parent%2Fsource%20%26%20context");
const bodyHref = screen.getByRole("link", { name: "View next body segment" }).getAttribute("href");
const bodyQuery = new URL(bodyHref!, "https://example.com").searchParams;
expect(bodyQuery.get("change_id")).toBe("z/earlier & context");
expect(bodyQuery.get("offset")).toBe("2");
expect(bodyQuery.get("byte_offset")).toBe("0");
const evidenceHref = screen.getByRole("link", { name: "View next evidence page" }).getAttribute("href");
const evidenceQuery = new URL(evidenceHref!, "https://example.com").searchParams;
expect(evidenceQuery.get("change_id")).toBe("z/earlier & context");
expect(evidenceQuery.get("evidence_cursor")).toBe("evidence/next");
const nextHref = screen.getByRole("link", { name: /Next page/ }).getAttribute("href");
const nextQuery = new URL(nextHref!, "https://example.com").searchParams;
expect(nextQuery.get("change_cursor")).toBe("change-next");
expect(nextQuery.get("change_id")).toBe("z/earlier & context");
});
@@ -59,12 +59,12 @@
<div><dt>Repository</dt><dd>{mergeRequest.repository_key}</dd></div>
<div><dt>State</dt><dd>{mergeRequest.state}</dd></div>
<div><dt>Source selector</dt><dd><code>{mergeRequest.selector_from ?? "requires repair"}</code></dd></div>
<div><dt>Live source revision</dt><dd>{mergeRequest.source.status}{mergeRequest.source.ref ? ` · ${mergeRequest.source.ref}` : ""}</dd></div>
<div><dt>Live source ref</dt><dd>{mergeRequest.source.status}{mergeRequest.source.ref ? ` · ${mergeRequest.source.ref}` : ""}</dd></div>
{#if mergeRequest.source.diagnostic}
<div><dt>Source observation error</dt><dd>{mergeRequest.source.diagnostic.code}: {mergeRequest.source.diagnostic.message}</dd></div>
{/if}
<div><dt>Target selector</dt><dd><code>{mergeRequest.selector_to}</code></dd></div>
<div><dt>Target revision</dt><dd>{mergeRequest.target.status}{mergeRequest.target.ref ? ` · ${mergeRequest.target.ref}` : ""}</dd></div>
<div><dt>Target ref</dt><dd>{mergeRequest.target.status}{mergeRequest.target.ref ? ` · ${mergeRequest.target.ref}` : ""}</dd></div>
<div><dt>Updated</dt><dd>{prettyDate(mergeRequest.updated_at)}</dd></div>
</dl>
</section>
@@ -101,7 +101,7 @@
operation_id: `repository-ssh-confirm-${crypto.randomUUID()}`,
runtime_id: operation.runtimeId,
host_key: selectedHostKey,
expected_host_trust_revision: probe.expected_host_trust_revision
expected_host_key_fingerprint: probe.expected_host_key_fingerprint
};
parseRepositorySshHostTrust(await requestConnectionTest('PUT', body));
if (!probeFence.isCurrent(operation, selectedRuntimeId)) return;
@@ -147,8 +147,8 @@
<dd><a href={`/w/${encodeURIComponent(data.workspace.workspace_id)}/settings/repository-access`}>Manage SSH credentials and pinned host keys</a></dd>
</div>
<div>
<dt>Source revision</dt>
<dd>{data.repository.item.source_revision} · {data.repository.item.source_fingerprint}</dd>
<dt>Source fingerprint</dt>
<dd>{data.repository.item.source_fingerprint}</dd>
</div>
<div>
<dt>Observed</dt>
@@ -50,7 +50,7 @@
</a>
</header>
<p>
These launch profiles are derived from the active Workspace configuration revision. Edit Profile declarations and Decodal sources in the shared configuration editor.
These launch profiles are derived from the active Workspace configuration. Edit Profile declarations and Decodal sources in the shared configuration editor.
</p>
{#if loading}
@@ -59,7 +59,7 @@
<p class="status-message error">{message}</p>
{:else if profileSettings}
<p class="settings-note">
Revision {profileSettings.config_revision ?? "unknown"} · tree {profileSettings.tree_digest ?? "unknown"} · projection {profileSettings.projection_digest ?? "unknown"}
Tree {profileSettings.tree_digest ?? "unknown"} · projection {profileSettings.projection_digest ?? "unknown"}
</p>
<ul class="settings-profile-list">
{#each profileSettings.profiles as profile (profile.profile_id)}
@@ -95,14 +95,14 @@
let hostname = $state('');
let port = $state(22);
let hostKey = $state('');
let hostExpectedRevision = $state<number | null>(null);
let hostExpectedFingerprint = $state<string | null>(null);
let hostErrors = $state<RepositoryAccessFieldErrors>({});
let hostTrustIdInput = $state<HTMLInputElement>();
let hostKeyInput = $state<HTMLTextAreaElement>();
let hostFormElement = $state<HTMLFormElement>();
let hostReturnFocus: HTMLElement | null = null;
const editingHostTrust = $derived(
hostExpectedRevision === null ? null : hostTrusts.find((entry) => entry.host_trust_id === hostTrustId) ?? null
hostExpectedFingerprint === null ? null : hostTrusts.find((entry) => entry.host_trust_id === hostTrustId) ?? null
);
function resetPageState(next: PageProps['data']): void {
@@ -139,7 +139,7 @@
hostname = '';
port = 22;
hostKey = '';
hostExpectedRevision = null;
hostExpectedFingerprint = null;
hostErrors = {};
hostReturnFocus = null;
}
@@ -460,7 +460,7 @@
try {
const body: RotateRepositorySshCredentialRequest = {
operation_id: operationId('credential-rotate'),
expected_revision: credential.current_revision,
expected_public_key_fingerprint: credential.public_key_fingerprint,
private_key: rotatePrivateKey,
passphrase: rotatePassphrase || null
};
@@ -504,7 +504,7 @@
try {
const body: DeleteRepositorySshCredentialRequest = {
operation_id: operationId('credential-delete'),
expected_revision: credential.current_revision
expected_public_key_fingerprint: credential.public_key_fingerprint
};
await request(`/credentials/${encodeURIComponent(credential.credential_id)}`, 'DELETE', body, null, `delete ${credential.name}`);
requireCurrentOperation(operation);
@@ -528,7 +528,7 @@
hostname = '';
port = 22;
hostKey = '';
hostExpectedRevision = null;
hostExpectedFingerprint = null;
hostErrors = {};
hostFormNotice = null;
await tick();
@@ -542,7 +542,7 @@
hostname = hostTrust.hostname;
port = hostTrust.port;
hostKey = hostTrust.host_key;
hostExpectedRevision = hostTrust.current_revision;
hostExpectedFingerprint = hostTrust.fingerprint;
hostErrors = {};
hostFormNotice = null;
await tick();
@@ -552,13 +552,13 @@
async function closeHostEditor(restore = true) {
const returnFocus = hostReturnFocus;
const shouldRestoreFocus = restore && focusIsInside(hostFormElement);
const fallbackId = hostExpectedRevision !== null ? `rotate-host-${hostTrustId}` : undefined;
const fallbackId = hostExpectedFingerprint !== null ? `rotate-host-${hostTrustId}` : undefined;
hostEditorOpen = false;
hostTrustId = '';
hostname = '';
port = 22;
hostKey = '';
hostExpectedRevision = null;
hostExpectedFingerprint = null;
hostErrors = {};
hostFormNotice = null;
hostReturnFocus = null;
@@ -571,7 +571,7 @@
await focusFirstInvalid(hostFormElement);
return;
}
const rotating = hostExpectedRevision !== null;
const rotating = hostExpectedFingerprint !== null;
const rotationTarget = editingHostTrust;
if (rotating && !rotationTarget) {
hostFormNotice = { tone: 'error', text: 'This pinned host key changed or was removed. Close the editor and try again.' };
@@ -587,7 +587,7 @@
hostname: rotationTarget?.hostname ?? hostname.trim(),
port: rotationTarget?.port ?? port,
host_key: hostKey.trim(),
expected_revision: hostExpectedRevision
expected_fingerprint: hostExpectedFingerprint
};
const saved = await request('/host-trusts', 'POST', body, parseRepositorySshHostTrust, rotating ? 'rotate this pinned SSH host key' : 'pin this SSH host key');
requireCurrentOperation(operation);
@@ -619,7 +619,7 @@
try {
const body: DeleteRepositorySshHostTrustRequest = {
operation_id: operationId('host-trust-delete'),
expected_revision: hostTrust.current_revision
expected_fingerprint: hostTrust.fingerprint
};
await request(`/host-trusts/${encodeURIComponent(hostTrust.host_trust_id)}`, 'DELETE', body, null, `delete the pin for ${hostTrust.hostname}:${hostTrust.port}`);
requireCurrentOperation(operation);
@@ -677,7 +677,6 @@
<details class="repository-access-technical">
<summary>Technical details</summary>
<dl>
<div><dt>Config revision</dt><dd>{accessProjection.config_revision}</dd></div>
<div><dt>Projection digest</dt><dd><code>{accessProjection.projection_digest}</code></dd></div>
</dl>
</details>
@@ -802,12 +801,6 @@
</form>
{/if}
{#if credentials.length > 0 && !data.credentialsError}
<details class="repository-access-technical">
<summary>Credential revisions</summary>
<dl>{#each credentials as credential (credential.credential_id)}<div><dt>{credential.credential_id}</dt><dd>Revision {credential.current_revision}</dd></div>{/each}</dl>
</details>
{/if}
</section>
<section class="repository-access-section" aria-labelledby="host-trust-heading">
@@ -862,30 +855,24 @@
<a class="inline-link" href={connectionTestHref}>{sshRepository ? `Check ${sshRepository.repository_key} SSH connection` : 'Open Repository settings'}</a>
{#if hostEditorOpen}
<form bind:this={hostFormElement} class="repository-access-form" aria-busy={hostExpectedRevision === null ? isPending('add-host-trust') : isPending(`rotate-host-${hostTrustId}`)} novalidate onsubmit={(event) => { event.preventDefault(); void saveHostTrust(); }}>
<form bind:this={hostFormElement} class="repository-access-form" aria-busy={hostExpectedFingerprint === null ? isPending('add-host-trust') : isPending(`rotate-host-${hostTrustId}`)} novalidate onsubmit={(event) => { event.preventDefault(); void saveHostTrust(); }}>
<div class="repository-access-form-heading">
<div>
<h3>{hostExpectedRevision === null ? 'Add pinned host key' : `Rotate key for ${hostname}:${port}`}</h3>
<p>{hostExpectedRevision === null ? 'Confirm the host, port, and fingerprint against a trusted source before saving.' : `This keeps the endpoint fixed and replaces its current trusted identity. ${editingHostTrust?.referenced_repositories.length ? `Explicitly referenced by ${editingHostTrust.referenced_repositories.join(', ')}. ` : 'No Repository explicitly references this pin. '}Repositories without an explicit host-key binding may also select it automatically when it is the unique pin matching this host and port.`}</p>
<h3>{hostExpectedFingerprint === null ? 'Add pinned host key' : `Rotate key for ${hostname}:${port}`}</h3>
<p>{hostExpectedFingerprint === null ? 'Confirm the host, port, and fingerprint against a trusted source before saving.' : `This keeps the endpoint fixed and replaces its current trusted identity. ${editingHostTrust?.referenced_repositories.length ? `Explicitly referenced by ${editingHostTrust.referenced_repositories.join(', ')}. ` : 'No Repository explicitly references this pin. '}Repositories without an explicit host-key binding may also select it automatically when it is the unique pin matching this host and port.`}</p>
</div>
{#if hostExpectedRevision !== null}<div><small>Current fingerprint</small><code>{editingHostTrust?.fingerprint}</code></div>{/if}
{#if hostExpectedFingerprint !== null}<div><small>Current fingerprint</small><code>{editingHostTrust?.fingerprint}</code></div>{/if}
</div>
{#if hostFormNotice}<p class="repository-access-notice {hostFormNotice.tone}" role={hostFormNotice.tone === 'error' ? 'alert' : 'status'}>{hostFormNotice.text}</p>{/if}
<div class="repository-access-form-grid">
<label><span>Host trust id</span><input bind:this={hostTrustIdInput} bind:value={hostTrustId} disabled={hostExpectedRevision !== null} aria-invalid={Boolean(hostErrors.hostTrustId)} aria-describedby={hostErrors.hostTrustId ? 'host-id-error' : undefined} />{#if hostErrors.hostTrustId}<small id="host-id-error" class="field-error">{hostErrors.hostTrustId}</small>{/if}</label>
<label><span>Hostname</span><input bind:value={hostname} disabled={hostExpectedRevision !== null} aria-invalid={Boolean(hostErrors.hostname)} aria-describedby={hostErrors.hostname ? 'hostname-error' : undefined} />{#if hostErrors.hostname}<small id="hostname-error" class="field-error">{hostErrors.hostname}</small>{/if}</label>
<label><span>Port</span><input type="number" bind:value={port} disabled={hostExpectedRevision !== null} aria-invalid={Boolean(hostErrors.port)} aria-describedby={hostErrors.port ? 'host-port-error' : undefined} />{#if hostErrors.port}<small id="host-port-error" class="field-error">{hostErrors.port}</small>{/if}</label>
<label><span>Host trust id</span><input bind:this={hostTrustIdInput} bind:value={hostTrustId} disabled={hostExpectedFingerprint !== null} aria-invalid={Boolean(hostErrors.hostTrustId)} aria-describedby={hostErrors.hostTrustId ? 'host-id-error' : undefined} />{#if hostErrors.hostTrustId}<small id="host-id-error" class="field-error">{hostErrors.hostTrustId}</small>{/if}</label>
<label><span>Hostname</span><input bind:value={hostname} disabled={hostExpectedFingerprint !== null} aria-invalid={Boolean(hostErrors.hostname)} aria-describedby={hostErrors.hostname ? 'hostname-error' : undefined} />{#if hostErrors.hostname}<small id="hostname-error" class="field-error">{hostErrors.hostname}</small>{/if}</label>
<label><span>Port</span><input type="number" bind:value={port} disabled={hostExpectedFingerprint !== null} aria-invalid={Boolean(hostErrors.port)} aria-describedby={hostErrors.port ? 'host-port-error' : undefined} />{#if hostErrors.port}<small id="host-port-error" class="field-error">{hostErrors.port}</small>{/if}</label>
<label class="wide"><span>OpenSSH public host key (Ed25519)</span><textarea bind:this={hostKeyInput} bind:value={hostKey} rows="4" aria-invalid={Boolean(hostErrors.hostKey)} aria-describedby={hostErrors.hostKey ? 'host-key-error host-key-help' : 'host-key-help'}></textarea><small id="host-key-help">The new fingerprint is derived from this key after save; compare the key with a trusted source first.</small>{#if hostErrors.hostKey}<small id="host-key-error" class="field-error">{hostErrors.hostKey}</small>{/if}</label>
</div>
<div class="repository-access-form-actions"><button type="submit" class="primary" disabled={hostExpectedRevision === null ? isPending('add-host-trust') : isPending(`rotate-host-${hostTrustId}`)}>{hostExpectedRevision === null ? (isPending('add-host-trust') ? 'Saving…' : 'Pin host key') : (isPending(`rotate-host-${hostTrustId}`) ? 'Saving…' : 'Save new key')}</button><button type="button" class="secondary" disabled={hostExpectedRevision === null ? isPending('add-host-trust') : isPending(`rotate-host-${hostTrustId}`)} onclick={() => void closeHostEditor()}>Cancel</button></div>
<div class="repository-access-form-actions"><button type="submit" class="primary" disabled={hostExpectedFingerprint === null ? isPending('add-host-trust') : isPending(`rotate-host-${hostTrustId}`)}>{hostExpectedFingerprint === null ? (isPending('add-host-trust') ? 'Saving…' : 'Pin host key') : (isPending(`rotate-host-${hostTrustId}`) ? 'Saving…' : 'Save new key')}</button><button type="button" class="secondary" disabled={hostExpectedFingerprint === null ? isPending('add-host-trust') : isPending(`rotate-host-${hostTrustId}`)} onclick={() => void closeHostEditor()}>Cancel</button></div>
</form>
{/if}
{#if hostTrusts.length > 0 && !data.hostTrustsError}
<details class="repository-access-technical">
<summary>Host key revisions</summary>
<dl>{#each hostTrusts as hostTrust (hostTrust.host_trust_id)}<div><dt>{hostTrust.host_trust_id}</dt><dd>Revision {hostTrust.current_revision}</dd></div>{/each}</dl>
</details>
{/if}
</section>
</div>
@@ -18,7 +18,6 @@ const defaultCredential = {
name: "Workspace default",
public_key_algorithm: "ssh-ed25519",
public_key_fingerprint: "SHA256:default",
current_revision: 3,
status: "active",
created_at: "2026-01-01T00:00:00Z",
rotated_at: null,
@@ -31,7 +30,6 @@ const deployCredential = {
name: "Deploy key",
public_key_algorithm: "ssh-ed25519",
public_key_fingerprint: "SHA256:deploy",
current_revision: 2,
status: "active",
created_at: "2026-01-01T00:00:00Z",
rotated_at: null,
@@ -46,7 +44,6 @@ const hostTrust = {
key_algorithm: "ssh-ed25519",
host_key: "ssh-ed25519 AAAA-current",
fingerprint: "SHA256:host-current",
current_revision: 4,
created_at: "2026-01-01T00:00:00Z",
updated_at: "2026-01-01T00:00:00Z",
referenced_repositories: [],
@@ -62,7 +59,6 @@ function pageData(workspaceId = "workspace-1") {
credentialsError: null,
publicKeys: [{
credential_id: "workspace-default",
current_revision: 3,
public_key_algorithm: "ssh-ed25519",
public_key_fingerprint: "SHA256:default",
public_key: "ssh-ed25519 AAAA-default",
@@ -72,7 +68,6 @@ function pageData(workspaceId = "workspace-1") {
hostTrustsError: null,
accessProjection: {
workspace_id: workspaceId,
config_revision: 1,
projection_digest: "sha256:projection",
bindings: [],
},
@@ -144,7 +139,6 @@ test("fences a deferred editor while an unrelated host draft remains active afte
if (url.endsWith("/credentials/generated/public-key")) {
return Promise.resolve(Response.json({
credential_id: "generated",
current_revision: 1,
public_key_algorithm: "ssh-ed25519",
public_key_fingerprint: "SHA256:generated",
public_key: "ssh-ed25519 AAAA-generated",
@@ -202,7 +196,6 @@ test("fences a deferred editor while an unrelated host draft remains active afte
credential_id: "generated",
name: "Generated key",
public_key_fingerprint: "SHA256:generated",
current_revision: 1,
}));
expect(await screen.findByText("Generated key")).not.toBeNull();
expect(screen.getByRole("heading", { name: "Add pinned host key" })).not
@@ -321,7 +314,6 @@ test("rerendering for another Workspace resets local state and fences stale resp
}],
accessProjection: {
workspace_id: "workspace-2",
config_revision: 7,
projection_digest: "sha256:workspace-b",
bindings: [],
},
@@ -335,7 +327,6 @@ test("rerendering for another Workspace resets local state and fences stale resp
stalePublicKey.resolve(Response.json({
credential_id: "deploy",
current_revision: 2,
public_key_algorithm: "ssh-ed25519",
public_key_fingerprint: "SHA256:deploy",
public_key: "ssh-ed25519 AAAA-stale",
@@ -363,7 +354,6 @@ test("retries public-key loading after a post-rotation refresh failure", async (
return Promise.resolve(Response.json({
...deployCredential,
public_key_fingerprint: "SHA256:deploy-new",
current_revision: 3,
rotated_at: "2026-02-01T00:00:00Z",
}));
}
@@ -374,7 +364,6 @@ test("retries public-key loading after a post-rotation refresh failure", async (
}
return Promise.resolve(Response.json({
credential_id: "deploy",
current_revision: 3,
public_key_algorithm: "ssh-ed25519",
public_key_fingerprint: "SHA256:deploy-new",
public_key: newPublicKey,
@@ -388,7 +377,6 @@ test("retries public-key loading after a post-rotation refresh failure", async (
const data = pageData();
data.publicKeys.push({
credential_id: "deploy",
current_revision: 2,
public_key_algorithm: "ssh-ed25519",
public_key_fingerprint: "SHA256:deploy",
public_key: "ssh-ed25519 AAAA-old-deploy",
@@ -454,7 +442,7 @@ test("locks a rotation to its host and port and discloses explicit and automatic
const body = JSON.parse(String(fetchMock.mock.calls[0]?.[1]?.body));
expect(body.hostname).toBe("github.com");
expect(body.port).toBe(22);
expect(body.expected_revision).toBe(4);
expect(body.expected_fingerprint).toBe("SHA256:host-current");
expect(
(screen.getByRole("button", { name: "Saving…" }) as HTMLButtonElement)
.disabled,
@@ -474,7 +462,6 @@ test("locks a rotation to its host and port and discloses explicit and automatic
...hostTrust,
host_key: "ssh-ed25519 AAAA-new",
fingerprint: "SHA256:host-new",
current_revision: 5,
}));
expect(await within(row).findByText(/Rotated the key for github.com:22/)).not
.toBeNull();
@@ -19,6 +19,7 @@
let { data }: PageProps = $props();
let runtimePublicBundle = $state('');
let workspaceTrustId = $state('');
let displayName = $state('');
let endpoint = $state('');
let runtimeFingerprint = $state<string | null>(null);
@@ -144,11 +145,13 @@
}
await createRemoteRuntime(data.workspaceId, {
public_bundle: publicBundle,
workspace_trust_id: workspaceTrustId,
display_name: displayName || null,
endpoint,
expected_revision: null,
expected_binding_id: null,
});
runtimePublicBundle = '';
workspaceTrustId = '';
runtimeFingerprint = null;
displayName = '';
endpoint = '';
@@ -168,14 +171,14 @@
runtime: WorkspaceRuntimeResource,
): RuntimeConnectionTestResponse | undefined {
const result = testResults[runtime.runtime_id];
return result?.binding_revision === runtime.management?.binding?.revision
return result?.binding_id === runtime.management?.binding?.binding_id
? result
: undefined;
}
async function testRuntime(runtime: WorkspaceRuntimeResource): Promise<void> {
const bindingRevision = runtime.management?.binding?.revision;
if (typeof bindingRevision !== 'number') return;
const bindingId = runtime.management?.binding?.binding_id;
if (typeof bindingId !== 'string') return;
const generation = ++connectionTestGeneration;
requestError = null;
busyRuntimeId = runtime.runtime_id;
@@ -183,7 +186,7 @@
const result = await testRuntimeConnection(data.workspaceId, runtime.runtime_id);
if (
generation !== connectionTestGeneration ||
result.binding_revision !== bindingRevision
result.binding_id !== bindingId
) {
await invalidateAll();
return;
@@ -258,9 +261,15 @@
</button>
<pre>yoi-runtime trust-workspace add --bundle {workspaceBundleFilename()}</pre>
<small>
Copy the Runtime-issued <code>workspace_trust_id</code> from <code>trust-workspace add/show</code>.
It identifies this enrollment, not the Workspace key fingerprint.
Pass the same <code>--fs-root</code> and <code>--fs-runtime-dir</code> options used by the Runtime
service. Existing Workspace trust entries are preserved.
</small>
<label>
Workspace trust ID
<input bind:value={workspaceTrustId} required autocomplete="off" spellcheck="false" />
</label>
{:else}
<p class="section-state">Loading Workspace public identity…</p>
{/if}
@@ -285,10 +294,10 @@
spellcheck="false"
placeholder={runtimeBundlePlaceholder}
></textarea>
<button type="button" disabled={busyRuntimeId !== null} onclick={previewRuntimeFingerprint}>
Preview fingerprint
</button>
</label>
<button class="settings-form-wide" type="button" disabled={busyRuntimeId !== null} onclick={previewRuntimeFingerprint}>
Preview fingerprint
</button>
{#if runtimeFingerprint}
<dl class="runtime-facts">
<div>
@@ -321,7 +330,7 @@
<div class="settings-action-row">
<button
type="submit"
disabled={busyRuntimeId !== null || !data.signingIdentity?.public_bundle || !runtimeFingerprint}
disabled={busyRuntimeId !== null || !data.signingIdentity?.public_bundle || !runtimeFingerprint || !workspaceTrustId}
>Register Runtime</button>
<button type="button" disabled={busyRuntimeId !== null} onclick={() => showAddRuntime = false}>
Cancel
@@ -25,6 +25,7 @@
let showPublicKey = $state(false);
let revealedPublicKey = $state<string | null>(null);
let publicKey = $state('');
let workspaceTrustId = $state('');
let displayName = $state('');
let endpoint = $state('');
let editingMetadata = $state(false);
@@ -49,6 +50,7 @@
showPublicKey = false;
revealedPublicKey = null;
publicKey = '';
workspaceTrustId = '';
displayName = data.runtimeDetail?.runtime.label ?? '';
endpoint = data.runtimeDetail?.endpoint ?? '';
editingMetadata = false;
@@ -201,12 +203,14 @@
identity_id: operation.runtimeId,
public_key: key,
},
workspace_trust_id: workspaceTrustId,
display_name: data.runtimeDetail.runtime.label,
endpoint: data.runtimeDetail.endpoint,
expected_revision: binding.revision,
expected_binding_id: binding.binding_id,
});
if (!isCurrentRoute(operation)) return;
publicKey = '';
workspaceTrustId = '';
showPublicKey = false;
revealedPublicKey = null;
successMessage = action === 'create'
@@ -233,7 +237,7 @@
async function revokeTrust(): Promise<void> {
if (busyAction !== null || !data.runtimeDetail) return;
const trust = data.runtimeDetail.trust_key;
if (trust.revision == null || trust.status !== 'active') {
if (trust.binding_id == null || trust.status !== 'active') {
requestError = 'Only active Workspace trust can be revoked.';
return;
}
@@ -248,7 +252,7 @@
const operation = routeFence.capture(data.runtimeId);
busyAction = 'revoke';
const request: RevokeRuntimeTrustKeyRequest = {
expected_revision: trust.revision,
expected_binding_id: trust.binding_id,
};
try {
@@ -289,8 +293,8 @@
const routeOperation = routeFence.capture(data.runtimeId);
const trust = data.runtimeDetail.trust_key;
if (trust.revision == null) {
deleteRuntimeError = 'The authoritative Runtime binding revision is unavailable. Reload before removal.';
if (trust.binding_id == null) {
deleteRuntimeError = 'The authoritative Runtime binding identity is unavailable. Reload before removal.';
return;
}
const operationId = runtimeRemovalAttempt.operationId();
@@ -302,7 +306,7 @@
routeOperation.runtimeId,
{
operation_id: operationId,
expected_binding_revision: trust.revision,
expected_binding_id: trust.binding_id,
},
);
if (!isCurrentRoute(routeOperation)) return;
@@ -405,11 +409,9 @@
<div><dt>Connection state</dt><dd>{runtime.management.binding?.connection_state ?? 'Not configured'}</dd></div>
<div><dt>Workspace signing key</dt><dd><code>{runtime.management.binding?.workspace_key_id ?? '—'}</code></dd></div>
<div><dt>Verified</dt><dd>{formatTimestamp(runtime.management.binding?.verification?.verified_at)}</dd></div>
<div><dt>Verified binding revision</dt><dd>{runtime.management.binding?.verification?.binding_revision?.toString() ?? '—'}</dd></div>
<div><dt>Last verification check</dt><dd>{runtime.management.binding?.verification?.last_outcome ?? '—'} · {formatTimestamp(runtime.management.binding?.verification?.last_checked_at)}</dd></div>
<div><dt>Runtime key status</dt><dd>{trust.status}</dd></div>
<div><dt>Fingerprint</dt><dd><code>{trust.fingerprint ?? '—'}</code></dd></div>
<div><dt>Revision</dt><dd>{trust.revision?.toString() ?? '—'}</dd></div>
<div><dt>Created</dt><dd>{formatTimestamp(trust.created_at)}</dd></div>
<div><dt>Updated</dt><dd>{formatTimestamp(trust.updated_at)}</dd></div>
<div><dt>Revoked</dt><dd>{formatTimestamp(trust.revoked_at)}</dd></div>
@@ -503,6 +505,15 @@
</div>
{:else}
<form class="runtime-trust-form" onsubmit={saveTrustKey}>
<label for="runtime-workspace-trust-id-input">Workspace trust ID</label>
<input
id="runtime-workspace-trust-id-input"
bind:value={workspaceTrustId}
required
autocomplete="off"
spellcheck="false"
/>
<p>Copy the Runtime-issued <code>workspace_trust_id</code> from <code>trust-workspace add/show</code> for the current enrollment. Do not use a key fingerprint or a revoked enrollment ID.</p>
<label for="runtime-public-key-input">Runtime public key</label>
<textarea
id="runtime-public-key-input"
@@ -570,13 +581,12 @@
<div class="runtime-audit-table-wrap">
<table class="runtime-audit-table">
<thead>
<tr><th>Action</th><th>Revision</th><th>Fingerprint</th><th>Actor</th><th>Time</th></tr>
<tr><th>Action</th><th>Fingerprint</th><th>Actor</th><th>Time</th></tr>
</thead>
<tbody>
{#each detail.recent_audit as entry}
<tr>
<td>{entry.action}</td>
<td>{entry.revision.toString()}</td>
<td><code>{entry.new_fingerprint ?? entry.old_fingerprint ?? '—'}</code></td>
<td><code>{entry.actor_account_id}</code></td>
<td>{formatTimestamp(entry.at)}</td>
@@ -3,7 +3,7 @@
import { parseRuntimeCleanupPlan } from '#lib/workspace/api/runtime-workers.ts';
import { parseWorkingDirectoryListResponse } from '#lib/workspace/api/workdirs.ts';
import { canRemoveWorkdir, removalCause, removalGuard, removeWorkdir, WorkdirRemovalError } from '#lib/workspace/settings/workdir-removal.ts';
import { formatCurrentWorkdirRevision } from '#lib/workspace/settings/workdir-revision.ts';
import { formatCurrentWorkdirCheckout } from '#lib/workspace/settings/workdir-checkout.ts';
import type { CleanupWorkdirCandidate, RuntimeCleanupPlanResponse, WorkingDirectorySummary } from '#lib/workspace/sidebar/types.ts';
import type { PageProps } from './$types';
@@ -30,10 +30,10 @@
function repositoryKey(workdir: WorkingDirectorySummary): string | null {
return workdir.source.kind === 'repository' ? workdir.source.repository_key : null;
}
function currentRevision(workdir: WorkingDirectorySummary): string {
function currentCheckout(workdir: WorkingDirectorySummary): string {
const key = repositoryKey(workdir);
const provider = key ? data.repositories?.items.find((repository) => repository.repository_key === key)?.provider ?? null : null;
return formatCurrentWorkdirRevision(workdir, provider);
return formatCurrentWorkdirCheckout(workdir, provider);
}
function cleanupCandidate(workdir: WorkingDirectorySummary): CleanupWorkdirCandidate | undefined {
return cleanupCandidates.find((candidate) => candidate.workdir_id === workdir.working_directory_id);
@@ -131,7 +131,7 @@
<tr>
<th>Workdir</th>
<th>Repository</th>
<th>Revision</th>
<th>Checkout</th>
<th>Status</th>
<th>Cleanliness</th>
<th>Occupied by</th>
@@ -147,7 +147,7 @@
<small><code>{workdir.working_directory_id}</code></small>
</td>
<td>{repositoryKey(workdir) ?? 'External'}</td>
<td><code>{currentRevision(workdir)}</code></td>
<td><code>{currentCheckout(workdir)}</code></td>
<td>{workdir.status}</td>
<td>{workdir.cleanliness ?? 'unknown'}</td>
<td>
@@ -0,0 +1,153 @@
// @vitest-environment happy-dom
import {
cleanup,
fireEvent,
render,
screen,
waitFor,
} from "@testing-library/svelte";
import { afterEach, expect, test, vi } from "vitest";
import type { ComponentProps } from "svelte";
import RuntimeSettings from "./+page.svelte";
import RuntimeDetail from "./[runtimeId]/+page.svelte";
import { identityFixture } from "#lib/workspace/settings/identity.test-fixtures.ts";
const api = vi.hoisted(() => ({ create: vi.fn(), fingerprint: vi.fn() }));
vi.mock("$app/navigation", () => ({ invalidateAll: vi.fn(async () => {}) }));
vi.mock("#lib/workspace/api/runtime-management.ts", async (importOriginal) => ({
...await importOriginal<
typeof import("#lib/workspace/api/runtime-management.ts")
>(),
createRemoteRuntime: api.create,
previewRuntimePublicKeyFingerprint: api.fingerprint,
}));
afterEach(() => {
cleanup();
vi.resetAllMocks();
});
test("Runtime registration requires and preserves the Runtime-issued enrollment ID independently of the fingerprint", async () => {
api.fingerprint.mockResolvedValue("sha256:runtime-public-key");
api.create.mockResolvedValue({});
const data = {
workspaceId: "workspace-a",
workspace: { permissions: { manage_runtimes: true } },
runtimes: { items: [], diagnostics: [] },
signingIdentity: identityFixture("workspace-a"),
} as unknown as ComponentProps<typeof RuntimeSettings>["data"];
render(RuntimeSettings, {
props: { data, params: { workspaceId: "workspace-a" } },
});
await fireEvent.click(screen.getByRole("button", { name: "Add Runtime" }));
await fireEvent.input(
screen.getByLabelText("Runtime public bundle", { selector: "textarea" }),
{
target: {
value: JSON.stringify({
identity_id: "runtime-a",
public_key: "runtime-public-key",
}),
},
},
);
await fireEvent.input(screen.getByLabelText("Endpoint"), {
target: { value: "https://runtime.example" },
});
await fireEvent.click(
screen.getByRole("button", { name: "Preview fingerprint" }),
);
const submit = screen.getByRole("button", { name: "Register Runtime" });
await waitFor(() =>
expect(screen.getByText("sha256:runtime-public-key")).toBeTruthy()
);
expect(submit).toHaveProperty("disabled", true);
expect(api.create).not.toHaveBeenCalled();
await fireEvent.input(screen.getByLabelText("Workspace trust ID"), {
target: { value: "Runtime-issued-enrollment-A" },
});
expect(submit).toHaveProperty("disabled", false);
await fireEvent.submit(submit.closest("form")!);
await waitFor(() => expect(api.create).toHaveBeenCalledOnce());
expect(api.create).toHaveBeenCalledWith("workspace-a", {
public_bundle: {
identity_id: "runtime-a",
public_key: "runtime-public-key",
},
workspace_trust_id: "Runtime-issued-enrollment-A",
display_name: null,
endpoint: "https://runtime.example",
expected_binding_id: null,
});
});
test("Runtime reactivation submits a new enrollment ID with the observed binding instead of reusing revoked trust", async () => {
api.fingerprint.mockResolvedValue("sha256:replacement-key");
api.create.mockResolvedValue({});
const data = {
workspaceId: "workspace-a",
runtimeId: "runtime-a",
workspace: { permissions: { manage_runtimes: true } },
runtimeDetail: {
workspace_id: "workspace-a",
endpoint: "https://runtime.example",
runtime: {
runtime_id: "runtime-a",
label: "Runtime A",
diagnostics: [],
management: {
built_in: false,
config_managed: true,
removable: true,
binding: {
state: "revoked",
connection_state: "revoked",
binding_id: "observed-binding",
workspace_trust_id: "revoked-enrollment",
},
},
},
trust_key: {
status: "revoked",
fingerprint: "sha256:old-key",
binding_id: "observed-binding",
},
recent_audit: [],
},
} as unknown as ComponentProps<typeof RuntimeDetail>["data"];
render(RuntimeDetail, {
props: {
data,
params: { workspaceId: "workspace-a", runtimeId: "runtime-a" },
},
});
expect(screen.getByLabelText("Workspace trust ID")).toHaveProperty(
"value",
"",
);
await fireEvent.input(screen.getByLabelText("Workspace trust ID"), {
target: { value: "new-enrollment" },
});
await fireEvent.input(screen.getByLabelText("Runtime public key"), {
target: { value: "yoi-ed25519-pub:v1:new-key" },
});
await waitFor(() =>
expect(screen.getByText("sha256:replacement-key")).toBeTruthy()
);
await fireEvent.submit(
screen.getByRole("button", { name: "Reactivate with this key" }).closest(
"form",
)!,
);
await waitFor(() => expect(api.create).toHaveBeenCalledOnce());
expect(api.create).toHaveBeenCalledWith("workspace-a", {
public_bundle: {
identity_id: "runtime-a",
public_key: "yoi-ed25519-pub:v1:new-key",
},
workspace_trust_id: "new-enrollment",
display_name: "Runtime A",
endpoint: "https://runtime.example",
expected_binding_id: "observed-binding",
});
});
@@ -114,6 +114,10 @@ test("saved settings have no editable form, badges or raw data expanded; each te
);
expect(document.querySelectorAll("details[open]")).toHaveLength(0);
expect(document.querySelector(".badge")).toBeNull();
expect(screen.queryByText("Revision", { exact: true })).toBeNull();
expect(
screen.getByText(identityFixture().public_bundle!.public_key_fingerprint),
).toBeTruthy();
expect(api.mock.calls).toHaveLength(2);
expect(api.mock.calls.some(([path]) => String(path).includes("deletion")))
.toBe(false);
@@ -133,7 +137,7 @@ test("Edit focuses the field; Cancel discards the draft and returns focus withou
expect(api.mock.calls.every(([, init]) => !init?.method)).toBe(true);
});
test("save uses the current revision, publishes confirmed name and invalidates the Workspace header", async () => {
test("save uses the observed update timestamp, publishes confirmed name and invalidates the Workspace header", async () => {
const api = mockApi();
mount();
await fireEvent.input(await openName(), {
@@ -152,15 +156,15 @@ test("save uses the current revision, publishes confirmed name and invalidates t
)),
).toEqual({
display_name: "Renamed Workspace",
revision: metadataFixture().revision,
expected_updated_at: metadataFixture().updated_at,
});
expect(invalidate).toHaveBeenCalledWith("/api/w/home-owner/workspace");
});
test("failed save preserves the draft and saved name; Reload saved name recovers from revision conflicts", async () => {
test("failed save preserves the draft and saved name; Reload saved name recovers from metadata conflicts", async () => {
mockApi((request) =>
request.method === "PUT"
? new Response("Revision conflict", { status: 409 })
? new Response("Metadata conflict", { status: 409 })
: null
);
mount();
@@ -168,7 +172,7 @@ test("failed save preserves the draft and saved name; Reload saved name recovers
await fireEvent.input(input, { target: { value: "Keep draft" } });
await fireEvent.click(screen.getByRole("button", { name: "Save name" }));
expect((await screen.findByRole("alert")).textContent).toBe(
"Revision conflict",
"Metadata conflict",
);
expect(input.value).toBe("Keep draft");
expect(screen.getByText(`Current: ${metadataFixture().display_name}`))
@@ -435,7 +439,7 @@ test("reload resumes persisted deletion without sending another POST", async ()
"yoi:workspace-deletion:home-owner",
JSON.stringify({
operation_id: "op-test",
expected_revision: metadataFixture().revision,
expected_workspace_updated_at: metadataFixture().updated_at,
confirmation: metadataFixture().display_name,
}),
);
@@ -501,7 +505,7 @@ test("slow identity keeps the name usable and old identity responses cannot leak
test("a completed deletion request from an unmounted Workspace cannot navigate away from the new scope", async () => {
const request = {
operation_id: "op-test",
expected_revision: metadataFixture().revision,
expected_workspace_updated_at: metadataFixture().updated_at,
confirmation: metadataFixture().display_name,
};
sessionStorage.setItem(
@@ -270,7 +270,7 @@
{/each}
</select>
</label>
<label>Ref selector<input bind:value={target.ref_selector} placeholder={selectedRepository?.default_selector ?? "branch, tag, or revision"} /></label>
<label>Ref selector<input bind:value={target.ref_selector} placeholder={selectedRepository?.default_selector ?? "branch, tag, or commit"} /></label>
<label>Access
<select bind:value={target.access}>
<option value="read_write">Read and write</option>
@@ -92,7 +92,7 @@
let manualWorkerId = $state("");
let manualBindings = $state<{ alias: string; working_directory_id: string; connection_id: string }[]>([]);
let cancellationReason = $state("");
let routeTicketSnapshot = `${initialData.ticketId}:${loadedTicket.item_revision}`;
let routeTicketSnapshot = JSON.stringify([initialData.workspaceId, initialData.ticketId, loadedTicket]);
let routeGeneration = 0;
const workerAssignment = $derived(
ticket.assignments.find((assignment) => assignment.role === "worker") ?? null,
@@ -164,7 +164,7 @@
const incomingTicketId = data.ticketId;
const incomingTicket = data.ticket.data;
if (!incomingTicket) return;
const incomingSnapshot = `${incomingTicketId}:${incomingTicket.item_revision}`;
const incomingSnapshot = JSON.stringify([data.workspaceId, incomingTicketId, incomingTicket]);
untrack(() => {
if (incomingSnapshot === routeTicketSnapshot) return;
@@ -176,7 +176,7 @@
function ticketMutationError(error: unknown): string {
const message = error instanceof Error ? error.message : String(error);
if (/\(409\)/.test(message)) return `Update conflict. Refresh the Ticket and resolve the current revision or resource binding before retrying. ${message}`;
if (/\(409\)/.test(message)) return `Update conflict. Refresh the Ticket and resolve the current content or resource binding before retrying. ${message}`;
if (/\(401\)|\(403\)/.test(message)) return `Permission denied. This action did not grant access. ${message}`;
if (/\(404\)/.test(message)) return `Resource unavailable or not connected. Check the Ticket and authorized resource connection. ${message}`;
return `Outcome unknown or request rejected. Refresh before retrying; no success is inferred. ${message}`;
@@ -382,7 +382,7 @@
await mutate("state", "/state", {
state: nextState,
operation_key: crypto.randomUUID(),
expected_item_revision: ticket.item_revision,
expected_content_digest: ticket.content_digest,
expected_state: ticket.state,
reason: transitionReason.trim(),
body: progressBody.trim() || null,
@@ -408,7 +408,7 @@
await mutate("close", "/close", {
resolution: resolution.trim(),
operation_key: crypto.randomUUID(),
expected_item_revision: ticket.item_revision,
expected_content_digest: ticket.content_digest,
expected_state: ticket.state,
})
) resolution = "";
@@ -674,7 +674,7 @@
{/each}
</select>
</label>
<label>Ref selector<input bind:value={target.ref_selector} placeholder={selectedRepository?.default_selector ?? "branch, tag, or revision"} disabled={busy !== null} /></label>
<label>Ref selector<input bind:value={target.ref_selector} placeholder={selectedRepository?.default_selector ?? "branch, tag, or commit"} disabled={busy !== null} /></label>
<label>Access
<select bind:value={target.access} disabled={busy !== null}>
<option value="read_write">Read and write</option>
@@ -12,7 +12,7 @@
} from '#lib/workspace/api/runtime-workers.ts';
import { workerHref } from '#lib/workspace/resource-links.ts';
import { formatWorkdirPermissions } from '#lib/workspace/settings/workdir-permissions.ts';
import { formatCurrentWorkdirRevision } from '#lib/workspace/settings/workdir-revision.ts';
import { formatCurrentWorkdirCheckout } from '#lib/workspace/settings/workdir-checkout.ts';
import { canOpenWorkerConsole } from '#lib/workspace/sidebar/workers.ts';
import { liveWorkerState } from '#lib/workspace/sidebar/worker-state.ts';
import type { CleanupWorkerCandidate, RuntimeCleanupPlanResponse, Worker } from '#lib/workspace/sidebar/types.ts';
@@ -48,17 +48,17 @@
workers = initial.workers?.items ?? [];
cleanupPlans = initial.cleanupPlans;
let previousCatalog: string | undefined;
let previousObservation: number | undefined;
let previousRequest: AbortSignal | null | undefined;
let previousRefreshing: boolean | undefined;
const scope = actionScope();
const unsubscribe = workspaceWorkersStore(id).subscribe((state) => {
if (!scope.isCurrent()) return;
const observationChanged = previousObservation !== state.observationVersion;
const observationChanged = previousRequest !== state.catalogRequest;
const refreshStarted = state.catalogRefreshing && previousRefreshing !== true;
const refreshCompleted = !state.catalogRefreshing && previousRefreshing === true;
const catalog = state.catalogWorkers === null ? undefined : JSON.stringify(state.catalogWorkers);
const catalogChanged = catalog !== previousCatalog;
previousObservation = state.observationVersion;
previousRequest = state.catalogRequest;
previousRefreshing = state.catalogRefreshing;
previousCatalog = catalog;
catalogRefreshing = state.catalogRefreshing;
@@ -210,7 +210,7 @@
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
expected_plan_revision: plan.revision,
expected_plan_digest: plan.digest,
worker_target_ids: [candidate.target_id],
workdir_target_ids: [],
@@ -261,7 +261,7 @@
if (directory.source.kind === 'external_grant') {
return `${alias}: ${label} · ${formatWorkdirPermissions(effective_permissions)}`;
}
return `${alias}: ${label} · ${formatCurrentWorkdirRevision(directory, provider)}`;
return `${alias}: ${label} · ${formatCurrentWorkdirCheckout(directory, provider)}`;
}).join(', ');
}
</script>
@@ -186,7 +186,7 @@
});
let protocolSubscription: WorkspaceMultiplexerSubscription | null = null;
let pendingSubmissions = $state<PendingSubmissionsSnapshot>({
revision: 0,
notification_count: 0,
head_id: null,
submissions: [],
@@ -2095,7 +2095,6 @@
method: "cancel_pending_submission",
params: {
submission_id: submission.submission_id,
expected_revision: pendingSubmissions.revision,
},
}, "Pending submission cancellation")}
>
@@ -94,7 +94,7 @@ function pageData(workerId = "worker-a", runtimeId = "runtime-a") {
function emptySession(): SessionSnapshot {
return {
pending_submissions: {
revision: 0,
notification_count: 0,
head_id: null,
submissions: [],
@@ -591,13 +591,13 @@ function subworkerFrame() {
session.entries.push(reply, { ...reply, entry_id: "child-reply-2" });
const snapshot = snapshotEvent(sessionWithUserMessage("parent question")) as Extract<ProtocolEvent, { event: "snapshot" }>;
snapshot.data.internal_workers = [{
worker: child, revision: 5, session, status: "idle",
worker: child, session, status: "idle",
in_flight: { blocks: [] }, internal_workers: [],
}];
const frame = subscribedFrame();
frame.message.payload.snapshot.data.events = [snapshot, ...[6, 7].map((revision): ProtocolEvent => ({
frame.message.payload.snapshot.data.events = [snapshot, ...[reply, reply].map((entry): ProtocolEvent => ({
event: "internal_worker", data: {
worker: child, revision, event: { event: "session_entry_committed", data: { entry: reply } },
worker: child, event: { event: "session_entry_committed", data: { entry } },
},
}))];
return frame;
@@ -782,13 +782,13 @@ test.each([
expect(form.children).toHaveLength(1);
});
test("pending inputs show literal previews above Composer with revision-fenced icon actions", async () => {
test("pending inputs show literal previews above Composer with submission-identity icon actions", async () => {
vi.stubGlobal("fetch", vi.fn(async () => Response.json({ availability: "live_protocol" })));
const view = render(ConsolePage, { data: pageData() });
await waitFor(() => expect(multiplexer.subscribe).toHaveBeenCalledOnce());
const session = emptySession();
session.pending_submissions = {
revision: 7, head_id: "private-submission-id", notification_count: 1,
head_id: "private-submission-id", notification_count: 1,
notification_previews: ["<b>通知内容</b>"],
submissions: [
{ submission_id: "private-submission-id", preview: "<b>実際の入力</b> " + "長文".repeat(100), accepted_at_ms: 1, segment_count: 1, byte_len: 500 },
@@ -820,14 +820,14 @@ test("pending inputs show literal previews above Composer with revision-fenced i
await fireEvent.click(cancel);
expect(multiplexer.sendWorkerMethod).toHaveBeenLastCalledWith({
method: "cancel_pending_submission",
params: { submission_id: "private-submission-id", expected_revision: 7 },
params: { submission_id: "private-submission-id" },
});
// Do not remove a row optimistically before the authoritative snapshot arrives.
expect(queue.querySelectorAll("li")).toHaveLength(2);
latestListener().onFrame({ frame: "event", message: { event: "event", data: {
payload: { event: "worker_protocol", data: { worker_id: "worker-a", event: {
event: "pending_submissions_changed", data: { pending: {
...session.pending_submissions, revision: 8, head_id: "legacy-submission-id",
...session.pending_submissions, head_id: "legacy-submission-id",
submissions: session.pending_submissions.submissions.slice(1),
} },
} } },
@@ -836,7 +836,7 @@ test("pending inputs show literal previews above Composer with revision-fenced i
expect(notifications.textContent).toContain("<b>通知内容</b>");
await fireEvent.click(screen.getByRole("button", { name: "Cancel queued input 1" }));
expect(multiplexer.sendWorkerMethod).toHaveBeenLastCalledWith({
method: "cancel_pending_submission", params: { submission_id: "legacy-submission-id", expected_revision: 8 },
method: "cancel_pending_submission", params: { submission_id: "legacy-submission-id" },
});
latestListener().onFrame(subscribedFrame(emptySession()));
await waitFor(() => expect(screen.queryByRole("region", { name: "Pending activations" })).toBeNull());
@@ -12,7 +12,7 @@
parseWorkerLaunchOptionsResponse,
} from '#lib/workspace/api/workers.ts';
import { parseSubjektivSubjectListResponse } from '#lib/workspace/memory/api.ts';
import { formatCurrentWorkdirRevision } from '#lib/workspace/settings/workdir-revision.ts';
import { formatCurrentWorkdirCheckout } from '#lib/workspace/settings/workdir-checkout.ts';
import { formatExternalGrantPermissionLevel } from '#lib/workspace/settings/workdir-permissions.ts';
import {
buildCreateWorkspaceWorkerRequest,
@@ -46,7 +46,7 @@
if (directory.source.kind === 'external_grant') {
return `${label} · External · ${formatExternalGrantPermissionLevel(directory.source.grant_permissions)}`;
}
return `${label} · ${formatCurrentWorkdirRevision(directory, provider)}`;
return `${label} · ${formatCurrentWorkdirCheckout(directory, provider)}`;
}
let { data }: PageProps = $props();
@@ -52,17 +52,16 @@ function worker(overrides: Partial<Worker> = {}): Worker {
...overrides,
};
}
function plan(revision = "plan-1"): RuntimeCleanupPlanResponse {
function plan(scenario = "plan-1"): RuntimeCleanupPlanResponse {
return {
workspace_id: "page-test",
generated_at: "2026-10-06T00:00:00Z",
runtime_id: "runtime-a",
revision,
digest: `digest-${revision}`,
digest: `digest-${scenario}`,
diagnostics: [],
workdirs: [],
workers: [{
target_id: `target-${revision}`,
target_id: `target-${scenario}`,
action: "worker_delete",
worker_id: "W-1",
runtime_worker_id: "worker-a",
@@ -80,7 +79,7 @@ function catalog(id = "page-test") {
loading: true,
workers: [],
catalogWorkers: null,
observationVersion: 0,
catalogRequest: null,
catalogRefreshing: true,
});
shared.stores.set(id, state);
@@ -107,7 +106,7 @@ function publish(store: ReturnType<typeof catalog>, items: Worker[]) {
loading: false,
workers: [],
catalogWorkers: items,
observationVersion: previous.observationVersion + 1,
catalogRequest: new AbortController().signal,
catalogRefreshing: false,
}));
}
@@ -199,7 +198,7 @@ test("refresh start invalidates an installed Delete candidate without requesting
.toBeTruthy();
expect(request).toHaveBeenCalledTimes(1);
// The flag itself invalidates plans, independently of the observation version.
// The flag itself invalidates plans, independently of the catalog request identity.
store.update((previous) => ({ ...previous, catalogRefreshing: true }));
await waitFor(() =>
expect(screen.queryByRole("button", { name: "Delete Catalog worker" }))
@@ -211,7 +210,7 @@ test("refresh start invalidates an installed Delete candidate without requesting
expect(screen.queryByRole("button", { name: "Delete Catalog worker" }))
.toBeNull();
// No catalog or version change on completion: the transition still reloads.
// No catalog or request change on completion: the transition still reloads.
store.update((previous) => ({ ...previous, catalogRefreshing: false }));
expect(await screen.findByRole("button", { name: "Delete Catalog worker" }))
.toBeTruthy();
@@ -234,7 +233,7 @@ test.each(["during refresh", "after completion"] as const)(
store.update((previous) => ({
...previous,
catalogRefreshing: true,
observationVersion: previous.observationVersion + 1,
catalogRequest: new AbortController().signal,
catalogWorkers: [
worker({ pinned: false, display_name: "Overlay worker" }),
],
@@ -250,7 +249,7 @@ test.each(["during refresh", "after completion"] as const)(
.toBeNull();
}
// Successful catalog GET retains exactly the same catalog and observationVersion.
// Successful catalog GET retains exactly the same catalog and catalog request identity.
store.update((previous) => ({ ...previous, catalogRefreshing: false }));
await waitFor(() => expect(request).toHaveBeenCalledTimes(2));
if (settlement === "after completion") {
@@ -288,7 +287,7 @@ test("restore follow-up cannot request cleanup while the authoritative catalog r
store.update((previous) => ({
...previous,
catalogRefreshing: true,
observationVersion: previous.observationVersion + 1,
catalogRequest: new AbortController().signal,
}));
});
await fireEvent.click(
@@ -88,7 +88,6 @@ function plan(state: string) {
workspace_id: workspaceId,
runtime_id: runtimeId,
generated_at: "2026-10-06T00:00:00Z",
revision: state,
digest: state,
diagnostics: [],
workdirs: [],
+9 -12
View File
@@ -10,7 +10,7 @@ import {
commitConfigTree,
ConfigSourceApiError,
fetchConfigEntry,
fetchConfigRevision,
fetchConfigHistory,
fetchConfigTree,
parseWorkspaceConfigTreeResponse,
} from "../../src/lib/workspace/config-source/api.ts";
@@ -29,7 +29,6 @@ const entry = {
content_digest: "sha256:entry",
};
const snapshot = {
revision: 7,
digest: "sha256:tree",
entries: { "profiles/main.dcdl": entry },
};
@@ -57,15 +56,14 @@ Deno.test("config source API commits directly through the workspace scope", asyn
const url = String(input);
calls.push({ url, init });
if (url.includes("/entries/")) return Promise.resolve(response(entry));
if (url.includes("/revisions/")) return Promise.resolve(response(snapshot));
if (url.includes("/history/")) return Promise.resolve(response(snapshot));
return Promise.resolve(response(tree));
}) as typeof fetch;
await fetchConfigTree("w/one", fetcher);
await fetchConfigRevision("w/one", 7, fetcher);
await fetchConfigHistory("w/one", "sha256:tree", fetcher);
await fetchConfigEntry("w/one", "profiles/main.dcdl", fetcher);
await commitConfigTree("w/one", {
base_revision: 4,
base_digest: "sha256:base",
changes: [],
entrypoints: [],
@@ -73,7 +71,7 @@ Deno.test("config source API commits directly through the workspace scope", asyn
assertEquals<unknown>(calls.map((call) => call.url), [
"/api/w/w%2Fone/config/source-tree",
"/api/w/w%2Fone/config/source-tree/revisions/7",
"/api/w/w%2Fone/config/source-tree/history/sha256%3Atree",
"/api/w/w%2Fone/config/source-tree/entries/profiles%2Fmain.dcdl",
"/api/w/w%2Fone/config/source-tree/commit",
]);
@@ -81,6 +79,9 @@ Deno.test("config source API commits directly through the workspace scope", asyn
assert(
String(calls[3].init?.body).includes('"base_digest":"sha256:base"'),
);
assertEquals(JSON.parse(String(calls[3].init?.body)), {
base_digest: "sha256:base", changes: [], entrypoints: [],
});
});
function withSchema(source: unknown, contributionSource: unknown = "builtin") {
@@ -113,7 +114,6 @@ for (const field of ["bundle", "contribution"] as const) {
assertEquals<unknown>(await fetchConfigTree("w", fetcher), body);
assertEquals<unknown>(
await commitConfigTree("w", {
base_revision: 7,
base_digest: snapshot.digest,
changes: [],
entrypoints: ["main.dcdl"],
@@ -197,12 +197,12 @@ Deno.test("config source API rejects mismatched entry map paths", async () => {
);
});
Deno.test("config source API rejects unsafe revisions and oversized entry content", () => {
Deno.test("config source API rejects removed snapshot revision fields and oversized entry content", () => {
assertThrows(
() =>
parseWorkspaceConfigTreeResponse({
...tree,
snapshot: { ...snapshot, revision: Number.MAX_SAFE_INTEGER + 1 },
snapshot: { ...snapshot, revision: 7 },
}),
ConfigSourceApiError,
"invalid response",
@@ -251,7 +251,6 @@ Deno.test("config source API does not materialize imported builtins in the works
Deno.test("config source API preserves unknown read-only builtin diagnostics without fallback requests", async () => {
const diagnostic = {
path: "main.dcdl",
revision: 7,
tree_digest: "sha256:tree",
kind: "import",
span: { start_byte: 8, end_byte: 40 },
@@ -270,7 +269,6 @@ Deno.test("config source API preserves unknown read-only builtin diagnostics wit
const error = await assertRejects(
() =>
commitConfigTree("w", {
base_revision: 7,
base_digest: "sha256:tree",
changes: [],
entrypoints: ["main.dcdl"],
@@ -290,7 +288,6 @@ Deno.test("config source API surfaces bounded failed evaluation details", async
let message = "";
try {
await commitConfigTree("w", {
base_revision: 1,
base_digest: "sha256:base",
changes: [],
entrypoints: [],
@@ -156,7 +156,7 @@ Deno.test("commit formats every working Decodal source without a preview roundtr
assert(
source.includes("await formatWorkingSources();") &&
source.includes("entrypoints: entrypoints()") &&
source.includes("Committed formatted revision"),
source.includes("Committed formatted source tree"),
"commit should format first and send the working changes directly to Backend authority",
);
assert(
@@ -167,7 +167,7 @@ Deno.test("commit formats every working Decodal source without a preview roundtr
);
});
Deno.test("config diagnostics analyze continuously with debounce and generation fencing", async () => {
Deno.test("config diagnostics analyze continuously with debounce and request cancellation", async () => {
const source = await Deno.readTextFile(
new URL(
"../../src/lib/workspace/config-source/ConfigSourceEditor.svelte",
@@ -179,7 +179,7 @@ Deno.test("config diagnostics analyze continuously with debounce and generation
source.includes("setTimeout(() =>") &&
source.includes("}, 250)") &&
source.includes("analyzer.analyze(path, value)") &&
source.includes("generation === analysisGeneration") &&
source.includes("!request.signal.aborted") &&
source.includes("analysisReady"),
"source changes should trigger only the latest debounced analysis after snapshot initialization",
);
@@ -16,7 +16,6 @@ function assert(condition: unknown, message: string): asserts condition {
Deno.test("toolchain converts reactive-like proxies to plain Worker messages", async () => {
const snapshot = new Proxy(
{
revision: 1,
digest: "sha256:test",
entries: {},
},
@@ -1,8 +1,11 @@
/// <reference lib="deno.ns" />
import { assertEquals } from "jsr:@std/assert";
import { createHash } from "node:crypto";
import { assertEquals, assertThrows } from "jsr:@std/assert";
import init, {
analyze_snapshot,
apply_changes,
changes_between,
complete_current,
compose_schema_bundle,
evaluate_snapshot,
@@ -32,6 +35,23 @@ async function digestText(text: string): Promise<string> {
}`;
}
function fixtureSnapshot(tree: Pick<ConfigTreeSnapshot, "entries">): ConfigTreeSnapshot {
const hash = createHash("sha256").update("yoi-config-tree-v1\0");
const entries: ConfigTreeSnapshot["entries"] = {};
for (const path of Object.keys(tree.entries).toSorted()) {
const entry = tree.entries[path];
entries[path] = {
...entry,
content_digest: "sha256:" + createHash("sha256").update(entry.content).digest("hex"),
};
hash.update(path).update("\0")
.update(entry.content_type === "decodal" ? "text/x-decodal" : "text/plain")
.update("\0").update(entry.content).update("\0");
}
return { digest: "sha256:" + hash.digest("hex"), entries };
}
async function toolchainFingerprint(
entrypoints: string[],
schemaBundle: WorkspaceConfigSchemaBundle,
@@ -47,7 +67,6 @@ async function toolchainFingerprint(
}
const snapshot: ConfigTreeSnapshot = {
revision: 4,
digest: "sha256:test-tree",
entries: {
"workspace.dcdl": {
@@ -88,15 +107,40 @@ const mainEntrypointContract: ToolchainContract = {
};
Deno.test("generated WASM evaluates the same virtual import contract", () => {
const result = evaluate_snapshot(snapshot, contract) as {
const result = evaluate_snapshot(fixtureSnapshot(snapshot), contract) as {
projections: Array<{ data_json: { answer: number } }>;
};
assertEquals(result.projections[0].data_json, { answer: 42 });
});
Deno.test("generated WASM keeps literal paths matching old cache identities as relative import bases", async () => {
const source = "{}";
const literalPath = `a.dcdl@text/x-decodal@${await digestText(source)}`;
// Match the native fixture: even an unreferenced source must resolve its
// relative import from the literal path rather than another entry's cache ID.
const entries: ConfigTreeSnapshot["entries"] = {};
for (const [path, content] of [
["main.dcdl", "{}"],
["a.dcdl", source],
[literalPath, 'import "./shared.dcdl"'],
["a.dcdl@text/shared.dcdl", "{ answer = 42; }"],
]) {
entries[path] = {
path,
content_type: "decodal",
content,
content_digest: await digestText(content),
};
}
const result = evaluate_snapshot(
fixtureSnapshot({ entries }),
mainEntrypointContract,
) as { projections: Array<{ data_json: unknown }> };
assertEquals(result.projections[0].data_json, {});
});
Deno.test("generated WASM accepts the mandatory main schema assertion", () => {
const assertedSnapshot: ConfigTreeSnapshot = {
revision: 1,
digest: "sha256:asserted-main",
entries: {
"main.dcdl": {
@@ -107,8 +151,7 @@ Deno.test("generated WASM accepts the mandatory main schema assertion", () => {
},
},
};
const result = evaluate_snapshot(
assertedSnapshot,
const result = evaluate_snapshot(fixtureSnapshot(assertedSnapshot),
mainEntrypointContract,
) as { projections: Array<{ data_json: { answer: number } }> };
@@ -116,19 +159,17 @@ Deno.test("generated WASM accepts the mandatory main schema assertion", () => {
});
Deno.test("generated WASM diagnostics carry snapshot provenance", () => {
const diagnostics = analyze_snapshot(
snapshot,
const diagnostics = analyze_snapshot(fixtureSnapshot(snapshot),
"workspace.dcdl",
"{ broken = ; }",
) as Array<{
path: string;
revision: number;
tree_digest: string;
kind: string;
}>;
assertEquals(diagnostics[0].path, "workspace.dcdl");
assertEquals(diagnostics[0].revision, 4);
assertEquals(diagnostics[0].tree_digest, "sha256:test-tree");
assertEquals(diagnostics[0].tree_digest, fixtureSnapshot(snapshot).digest);
assertEquals("revision" in diagnostics[0], false);
assertEquals(diagnostics[0].kind, "syntax");
});
@@ -153,7 +194,6 @@ const schemaBundle = compose_schema_bundle([
function schemaSnapshot(source: string): ConfigTreeSnapshot {
return {
revision: 7,
digest: "sha256:schema-tree",
entries: {
"main.dcdl": {
@@ -177,7 +217,6 @@ const schemaContract: ToolchainContract = {
};
const markdownSnapshot: ConfigTreeSnapshot = {
revision: 8,
digest: "sha256:markdown-tree",
entries: {
"main.dcdl": {
@@ -203,7 +242,7 @@ const markdownContract: ToolchainContract = {
};
Deno.test("generated WASM evaluates Markdown with the shared Skill projection", () => {
const result = evaluate_snapshot(markdownSnapshot, markdownContract) as {
const result = evaluate_snapshot(fixtureSnapshot(markdownSnapshot), markdownContract) as {
projections: Array<{ data_json: Record<string, unknown> }>;
};
assertEquals(result.projections[0].data_json, {
@@ -219,10 +258,9 @@ Deno.test("generated WASM evaluates Markdown with the shared Skill projection",
});
Deno.test("generated WASM applies Decodal 0.4 typed maps and explicit object rest", () => {
const result = evaluate_snapshot(
schemaSnapshot(
const result = evaluate_snapshot(fixtureSnapshot(schemaSnapshot(
"{ features = { console = { enabled = true; }; }; web = { enabled = true; extension_value = 42; }; }",
),
)),
schemaContract,
) as { projections: Array<{ data_json: Record<string, unknown> }> };
assertEquals(result.projections[0].data_json, {
@@ -240,7 +278,7 @@ type ProjectedDiagnostic = {
function evaluateFailure(source: string): ProjectedDiagnostic {
try {
evaluate_snapshot(schemaSnapshot(source), schemaContract);
evaluate_snapshot(fixtureSnapshot(schemaSnapshot(source)), schemaContract);
} catch (error) {
const diagnostics = error as ProjectedDiagnostic[];
assertEquals(Array.isArray(diagnostics), true);
@@ -274,7 +312,7 @@ Deno.test("generated WASM preserves native Decodal 0.4 diagnostic semantics", ()
Deno.test("generated WASM returns completion items for the editor adapter", () => {
const source = 'import "./"';
set_snapshot({
set_snapshot(fixtureSnapshot({
...snapshot,
entries: {
...snapshot.entries,
@@ -283,7 +321,7 @@ Deno.test("generated WASM returns completion items for the editor adapter", () =
content: source,
},
},
});
}));
const result = complete_current(
"workspace.dcdl",
source,
@@ -301,7 +339,7 @@ Deno.test("generated WASM returns completion items for the editor adapter", () =
Deno.test("generated WASM exposes read-only builtin completions without editable builtin entries", () => {
const tree = schemaSnapshot("{}");
set_snapshot(tree);
set_snapshot(fixtureSnapshot(tree));
set_schema_bundle(emptySchemaBundle);
const source = 'let 名 = 1; import "$builtin/profiles/comp"';
const result = complete_current(
@@ -320,8 +358,7 @@ Deno.test("generated WASM exposes read-only builtin completions without editable
assertEquals(companion?.kind, "file");
assertEquals(companion?.detail, "read-only builtin Decodal source");
assertEquals(Object.keys(tree.entries), ["main.dcdl"]);
const evaluated = evaluate_snapshot(
schemaSnapshot('import "$builtin/profiles/companion.dcdl"'),
const evaluated = evaluate_snapshot(fixtureSnapshot(schemaSnapshot('import "$builtin/profiles/companion.dcdl"')),
mainEntrypointContract,
) as {
projections: Array<{ data_json: { slug: string } }>;
@@ -338,16 +375,14 @@ Deno.test("generated WASM rejects unknown builtin imports without same-suffix wo
content_digest: "sha256:workspace-fallback",
};
set_schema_bundle(emptySchemaBundle);
const diagnostics = analyze_snapshot(tree, "main.dcdl", undefined) as Array<{
const diagnostics = analyze_snapshot(fixtureSnapshot(tree), "main.dcdl", undefined) as Array<{
path: string;
revision: number;
tree_digest: string;
message: string;
}>;
assertEquals(diagnostics.length > 0, true);
assertEquals(diagnostics[0].path, "main.dcdl");
assertEquals(diagnostics[0].revision, tree.revision);
assertEquals(diagnostics[0].tree_digest, tree.digest);
assertEquals(diagnostics[0].tree_digest, fixtureSnapshot(tree).digest);
assertEquals(
diagnostics[0].message.includes(
"unknown or non-public read-only builtin source",
@@ -360,7 +395,7 @@ Deno.test("generated WASM rejects unknown builtin imports without same-suffix wo
);
let failed = false;
try {
evaluate_snapshot(tree, mainEntrypointContract);
evaluate_snapshot(fixtureSnapshot(tree), mainEntrypointContract);
} catch (error) {
failed = true;
assertEquals(
@@ -372,26 +407,24 @@ Deno.test("generated WASM rejects unknown builtin imports without same-suffix wo
}
assertEquals(failed, true);
assertEquals(
analyze_snapshot(tree, "main.dcdl", 'import "./profiles/missing.dcdl"'),
analyze_snapshot(fixtureSnapshot(tree), "main.dcdl", 'import "./profiles/missing.dcdl"'),
[],
);
});
function importFailure(tree: ConfigTreeSnapshot): ProjectedDiagnostic[] {
try {
evaluate_snapshot(tree, mainEntrypointContract);
evaluate_snapshot(fixtureSnapshot(tree), mainEntrypointContract);
} catch (error) {
// A WASM trap/stack overflow must not count as a structured import failure.
assertEquals(Array.isArray(error), true);
const diagnostics = error as Array<
ProjectedDiagnostic & {
revision: number;
tree_digest: string;
}
>;
assertEquals(diagnostics.length > 0, true);
assertEquals(diagnostics[0].revision, tree.revision);
assertEquals(diagnostics[0].tree_digest, tree.digest);
assertEquals(diagnostics[0].tree_digest, fixtureSnapshot(tree).digest);
return diagnostics;
}
throw new Error("expected a structured import diagnostic");
@@ -427,7 +460,7 @@ Deno.test("generated WASM enforces the import depth budget on a real 33-deep cha
return tree;
}
// The synthetic evaluation-wrapper import consumes one of the 32 edges.
const accepted = evaluate_snapshot(chain(31), mainEntrypointContract) as {
const accepted = evaluate_snapshot(fixtureSnapshot(chain(31)), mainEntrypointContract) as {
projections: Array<{ data_json: { answer: number } }>;
};
assertEquals(accepted.projections[0].data_json, { answer: 42 });
@@ -447,7 +480,7 @@ Deno.test("generated WASM completes blank nested schema positions after Unicode"
const source =
'{ description = "日本語"; profile = { }\n} as WorkspaceConfigSchema';
const cursor = source.indexOf("{ }") + 2;
set_snapshot({
set_snapshot(fixtureSnapshot({
...snapshot,
entries: {
...snapshot.entries,
@@ -456,7 +489,7 @@ Deno.test("generated WASM completes blank nested schema positions after Unicode"
content: source,
},
},
});
}));
set_schema_bundle({
contributions: [],
source: "{ profile = { default_profile = String; }; prompts = {}; }",
@@ -486,7 +519,7 @@ Deno.test("generated WASM completes asserted WorkspaceConfigSchema keys", () =>
const bareSource = "{ pro }";
const source = "{ pro } as WorkspaceConfigSchema";
const cursor = source.indexOf("pro") + 3;
set_snapshot({
set_snapshot(fixtureSnapshot({
...snapshot,
entries: {
...snapshot.entries,
@@ -495,7 +528,7 @@ Deno.test("generated WASM completes asserted WorkspaceConfigSchema keys", () =>
content: source,
},
},
});
}));
set_schema_bundle({
contributions: [],
source: "{ profile = { default_profile = String; }; prompts = {}; }",
@@ -566,7 +599,6 @@ Deno.test("generated WASM authors value-based Profiles using the Backend schema
const content =
'{ profile = { entries = [{ selector = "project:alpha"; profile = {}; }]; }; } as WorkspaceConfigSchema';
const tree: ConfigTreeSnapshot = {
revision: 8,
digest: "sha256:value-profile-tree",
entries: {
"main.dcdl": {
@@ -583,9 +615,9 @@ Deno.test("generated WASM authors value-based Profiles using the Backend schema
schema_bundle: schema,
fingerprint: await toolchainFingerprint(["main.dcdl"], schema),
};
set_snapshot(tree);
set_snapshot(fixtureSnapshot(tree));
set_schema_bundle(schema);
const evaluated = evaluate_snapshot(tree, valueContract) as {
const evaluated = evaluate_snapshot(fixtureSnapshot(tree), valueContract) as {
projections: Array<
{ data_json: { profile: { entries: Array<{ profile: unknown }> } } }
>;
@@ -648,13 +680,13 @@ Deno.test("generated WASM authors value-based Profiles using the Backend schema
`profile = ${value};`,
);
assertEquals(
analyze_snapshot(tree, "main.dcdl", partialContent),
analyze_snapshot(fixtureSnapshot(tree), "main.dcdl", partialContent),
[],
value,
);
}
const recipe = "{ worker = { mode = 42; }; }";
const analysisTree = {
const analysisTree: ConfigTreeSnapshot = {
...tree,
entries: {
...tree.entries,
@@ -685,13 +717,12 @@ Deno.test("generated WASM authors value-based Profiles using the Backend schema
"profile = {};",
`profile = ${value};`,
);
const diagnostics = analyze_snapshot(
analysisTree,
const diagnostics = analyze_snapshot(fixtureSnapshot(analysisTree),
"main.dcdl",
invalidContent,
) as Array<{
path: string;
revision: number;
tree_digest: string;
kind: string;
span: { start_byte: number; end_byte: number };
message: string;
@@ -702,7 +733,7 @@ Deno.test("generated WASM authors value-based Profiles using the Backend schema
assertEquals(diagnostics.length > 0, true, value);
const diagnostic = diagnostics[0];
assertEquals(diagnostic.path, expectedPath);
assertEquals(diagnostic.revision, tree.revision);
assertEquals(diagnostic.tree_digest, fixtureSnapshot(analysisTree).digest);
assertEquals(
["constraint_violation", "type_mismatch"].includes(diagnostic.kind),
true,
@@ -783,7 +814,7 @@ Deno.test("generated WASM authors value-based Profiles using the Backend schema
"profile = {};",
`profile = ${form};`,
);
const scopedTree = {
const scopedTree: ConfigTreeSnapshot = {
...tree,
entries: {
"main.dcdl": {
@@ -800,12 +831,11 @@ Deno.test("generated WASM authors value-based Profiles using the Backend schema
},
};
assertEquals(
analyze_snapshot(scopedTree, "main.dcdl", undefined),
analyze_snapshot(fixtureSnapshot(scopedTree), "main.dcdl", undefined),
[],
form,
);
const result = evaluate_snapshot(
scopedTree,
const result = evaluate_snapshot(fixtureSnapshot(scopedTree),
valueContract,
) as typeof evaluated;
assertEquals(
@@ -839,7 +869,7 @@ Deno.test("generated WASM authors value-based Profiles using the Backend schema
"profile = {};",
`profile = ${form};`,
);
const fixtureTree = {
const fixtureTree: ConfigTreeSnapshot = {
...tree,
entries: {
"main.dcdl": {
@@ -856,12 +886,11 @@ Deno.test("generated WASM authors value-based Profiles using the Backend schema
},
};
assertEquals(
analyze_snapshot(fixtureTree, "main.dcdl", undefined),
analyze_snapshot(fixtureSnapshot(fixtureTree), "main.dcdl", undefined),
[],
`${fixture.name}: ${form}`,
);
const result = evaluate_snapshot(
fixtureTree,
const result = evaluate_snapshot(fixtureSnapshot(fixtureTree),
valueContract,
) as typeof evaluated;
assertEquals(
@@ -896,5 +925,65 @@ Deno.test("generated WASM authors value-based Profiles using the Backend schema
);
}
// Neither completion nor diagnostic shape checking changes the saved value.
assertEquals(evaluate_snapshot(tree, valueContract), evaluated);
assertEquals(evaluate_snapshot(fixtureSnapshot(tree), valueContract), evaluated);
});
Deno.test("generated WASM returns to the same content digest and enforces entry digest CAS", () => {
const base = fixtureSnapshot(schemaSnapshot("{ answer = 1; }"));
set_schema_bundle(emptySchemaBundle);
set_snapshot(base);
const changed = apply_changes([{
kind: "update", path: "main.dcdl",
expected_digest: base.entries["main.dcdl"].content_digest,
content: "{ broken = ; }",
}]) as ConfigTreeSnapshot;
assertEquals(changed.digest === base.digest, false);
assertEquals("revision" in changed, false);
const diagnostics = analyze_snapshot(changed, "main.dcdl", undefined) as Array<{ tree_digest: string }>;
assertEquals(diagnostics.length > 0, true);
assertEquals(diagnostics.every((diagnostic) => diagnostic.tree_digest === changed.digest), true);
assertThrows(() => apply_changes([{
kind: "update", path: "main.dcdl",
expected_digest: base.entries["main.dcdl"].content_digest,
content: "{ answer = 2; }",
}]));
const restored = apply_changes(changes_between(changed, base)) as ConfigTreeSnapshot;
assertEquals(restored, base);
assertEquals(analyze_snapshot(restored, "main.dcdl", undefined), []);
});
Deno.test("generated WASM rejects cache URI paths in snapshot JSON despite valid content digests", async () => {
const source = "{}";
const cacheUri = `config-source://a.dcdl@text/x-decodal@${await digestText(source)}`;
// Both entry and tree digests are correct, so rejection must come from the
// untrusted VirtualPath boundary rather than a content-identity mismatch.
const snapshot = fixtureSnapshot({
entries: {
"a.dcdl": {
path: "a.dcdl",
content_type: "decodal",
content: source,
content_digest: await digestText(source),
},
[cacheUri]: {
path: cacheUri,
content_type: "decodal",
content: 'import "./shared.dcdl"',
content_digest: await digestText('import "./shared.dcdl"'),
},
},
});
const json = JSON.stringify(snapshot);
const rejection = assertThrows(() => set_snapshot(JSON.parse(json)));
assertEquals(String(rejection).includes("config-source://"), true);
});
Deno.test("generated WASM rejects forged content identity at the snapshot boundary", () => {
const base = fixtureSnapshot(schemaSnapshot("{}"));
for (const forged of [
{ ...base, digest: "sha256:forged" },
{ ...base, entries: { "main.dcdl": { ...base.entries["main.dcdl"], content: "{ changed = true; }" } } },
]) {
assertThrows(() => set_snapshot(forged));
}
});
+4 -4
View File
@@ -34,19 +34,19 @@ Start `tools/web-ux/drive-fixture/server.ts <port> <build-root>` with `--allow-n
- `*denied*`: typed HTTP 403 root/read; `*offline*`/`home-error`: typed HTTP 503; `*empty*`: empty successful root list; `*paged*`: 240 additional nodes.
- `POST /__fixture/fault` JSON `{workspace, mode}`: one-shot `conflict` or committed `unknown` mutation response; persistent `image_error` binary fetch failure.
- `home-owner`/`home-member`/`home-long`: representative authoritative Worker catalogs; REST and subscription projections come from the same generated-shape records. `home-long` includes long display names and opaque Runtime/Worker IDs. `workers-error` intentionally fails catalog reads with HTTP 503.
- `GET /__fixture/log`: observable API request IDs/methods/paths/node IDs/CAS revisions for identity and no-resend assertions.
- `GET /__fixture/log`: observable API request IDs/methods/paths/node IDs/CAS observed committed requests for identity and no-resend assertions.
- `POST /__fixture/reset`: fresh in-memory state.
Request delay/loss tests intercept the API boundary with Playwright and synchronize on gates, not guessed sleeps. They let the fixture commit before dropping/holding the response, then verify receipt query without blind upload retry.
## Visual evidence
Preserved production baseline: `.svelte-kit/t724/before-build/{site,output,build.log}`. Official bundles live below `.svelte-kit/t724/visual/`. Each contains screenshots, console/request/page diagnostics, accessibility snapshots, screenshot hashes, source revision/dirty context, and `review-context.json`.
Preserved production baseline: `.svelte-kit/t724/before-build/{site,output,build.log}`. Official bundles live below `.svelte-kit/t724/visual/`. Each contains screenshots, console/request/page diagnostics, accessibility snapshots, screenshot hashes, source commit/dirty context, and `review-context.json`.
Home/sidebar and Workers are captured under identical owner/member/error/empty/long conditions, light/dark at 1440/768/390/320. After captures add Drive owner/member/empty/error/denied/paged/Markdown/image/image-error/truncated surfaces, pinned rename/replace revision forms, and owner grant selection/read-only/read-write/revoked states. Expected injected 503/403 diagnostics and superseded breadcrumb metadata aborts remain in official context; `diagnostic-disposition.json` explicitly separates unexpected diagnostics. CLI `completed-with-errors` is not relabeled a visual pass.
Home/sidebar and Workers are captured under identical owner/member/error/empty/long conditions, light/dark at 1440/768/390/320. After captures add Drive owner/member/empty/error/denied/paged/Markdown/image/image-error/truncated surfaces, pinned rename/replace observed committed request forms, and owner grant selection/read-only/read-write/revoked states. Expected injected 503/403 diagnostics and superseded breadcrumb metadata aborts remain in official context; `diagnostic-disposition.json` explicitly separates unexpected diagnostics. CLI `completed-with-errors` is not relabeled a visual pass.
The production shell scrolls `main` internally, so `fullPage` does not expand that scroll panel. Capture points use ordinary keyboard focus to show the actual form or the top denial state; no CSS/DOM screenshot override is applied. The responsive audit separately records document/main/table-region geometry, visible keyboard focus, and page errors across the full theme/viewport matrix. Raster checks wait for actual decoding, assert natural dimensions 1200×800, preserve the 3:2 aspect ratio, and constrain the rendered box by both the available width and 60vh height at all four widths/both themes.
The Drive browser suite also exercises the file-browser redesign at 1440/768/390/320px in light/dark themes: menu placement, native-dialog focus and Escape restoration, folder creation, search, and destination-folder navigation. Screenshots are written to `.svelte-kit/drive-redesign/` for the list, New menu, creation dialog, Markdown preview, and move dialog. Component tests cover failed/paged destination reads, exclusion of files and the moving folder itself, and pinned mutation revisions. These checks use the same fixture boundary, not a live Backend.
The Drive browser suite also exercises the file-browser redesign at 1440/768/390/320px in light/dark themes: menu placement, native-dialog focus and Escape restoration, folder creation, search, and destination-folder navigation. Screenshots are written to `.svelte-kit/drive-redesign/` for the list, New menu, creation dialog, Markdown preview, and move dialog. Component tests cover failed/paged destination reads, exclusion of files and the moving folder itself, and pinned mutation observed committed requests. These checks use the same fixture boundary, not a live Backend.
Parent and reviewer must open images and inspect context before asserting visual acceptance. In particular, a successful capture command is not sufficient and HTTP 200 does not excuse visible UI errors.
@@ -55,31 +55,31 @@ for (const scheme of ["light", "dark"]) {
}
if (stage === "after") {
routes.push({
id: "drive-form-revision",
label: "Pinned rename and replace form revisions",
id: "drive-form-observation",
label: "Pinned rename and replace form observations",
path: "/w/home-owner/drive/3",
goal: "Show the form's locked read revision and explicit destination/file controls.",
goal: "Show the form's locked read observation and explicit destination/file controls.",
dataState:
"Revision 1 read from fixture metadata; no mutation is sent by these capture interactions.",
"Initial committed request read from fixture metadata; no mutation is sent by these capture interactions.",
ready: { kind: "selector", selector: '.drive-page[data-drive-ready="true"]' },
capturePoints: [
{
id: "rename",
label: "Rename / move pinned revision",
label: "Rename / move pinned observation",
fullPage: true,
interaction: [
{ action: "click", selector: '.drive-menu > summary[aria-label="More actions"]' },
{ action: "click", selector: '.drive-toolbar button:has-text("Rename / move")' },
{
action: "wait",
ready: { kind: "selector", selector: '.drive-form:has-text("Expected revision 1.")' },
ready: { kind: "selector", selector: '.drive-form:has-text("Changes by another writer will not be overwritten.")' },
},
{ action: "press", selector: ".drive-form input:not([inputmode])", key: "Shift" },
],
},
{
id: "replace",
label: "Replace file pinned revision",
label: "Replace file pinned observation",
fullPage: true,
interaction: [
{ action: "click", selector: '.drive-form button:has-text("Cancel form")' },
@@ -87,7 +87,7 @@ for (const scheme of ["light", "dark"]) {
{ action: "click", selector: '.drive-toolbar button:has-text("Replace file")' },
{
action: "wait",
ready: { kind: "selector", selector: '.drive-form:has-text("Expected revision 1.")' },
ready: { kind: "selector", selector: '.drive-form:has-text("Changes by another writer will not be overwritten.")' },
},
{ action: "press", selector: ".drive-form input[type=file]", key: "Shift" },
],
@@ -367,7 +367,7 @@ for (const scheme of ["light", "dark"]) {
if (
[
"drive-markdown",
"drive-form-revision",
"drive-form-observation",
"drive-image",
"drive-image-error",
"drive-truncated",
@@ -10,7 +10,7 @@ const ready = (page: Page) =>
const button = (page: Page, name: string) =>
page.getByRole("button", { name, exact: true });
const draft = (page: Page) =>
page.getByRole("textbox", { name: /^Draft \(expected revision/ });
page.getByRole("textbox", { name: /^Draft$/ });
const preview = (page: Page) =>
page.getByRole("article", { name: "Current saved content" });
function gate() {
@@ -135,7 +135,7 @@ async function safeFulfill(
} catch { /* selection intentionally canceled this old request */ }
}
Deno.test("production Drive two tabs retain losing CAS draft and never retry with a newer revision", () =>
Deno.test("production Drive two tabs retain losing CAS draft and never retry with a newer observation", () =>
harness(async (origin, browser) => {
const context = await browser.newContext();
const first = await context.newPage(), second = await context.newPage();
@@ -151,7 +151,7 @@ Deno.test("production Drive two tabs retain losing CAS draft and never retry wit
await first.getByText("Published — DB commit confirmed", { exact: false })
.last().waitFor();
await button(second, "Save").click();
await second.getByText("Revision conflict — your draft is retained.", {
await second.getByText("Content changed — your draft is retained.", {
exact: true,
})
.waitFor();
@@ -167,7 +167,7 @@ Deno.test("production Drive two tabs retain losing CAS draft and never retry wit
"Second actor unsaved draft",
);
assert(
(await second.getByText("Draft (expected revision 1)", { exact: false })
(await second.getByText("Draft", { exact: false })
.count()) > 0,
);
assert(
@@ -187,7 +187,7 @@ Deno.test("production Drive two tabs retain losing CAS draft and never retry wit
const casWrites = (await logs(origin)).filter((x) =>
x.path === "/mutate"
);
assertEquals(casWrites.map((x) => [x.nodeId, x.expectedRevision]), [[
assertEquals(casWrites.map((x) => [x.nodeId, x.expectedMutationId]), [[
"3",
"1",
], ["3", "1"]]);
@@ -229,7 +229,7 @@ Deno.test("production Drive delayed old read cannot replace another file or work
.last().waitFor();
const mutation = (await logs(origin)).filter((x) => x.path === "/mutate");
assertEquals(
mutation.map((x) => [x.workspace, x.nodeId, x.expectedRevision]),
mutation.map((x) => [x.workspace, x.nodeId, x.expectedMutationId]),
[[
"home-owner",
"4",
@@ -330,12 +330,14 @@ Deno.test("production Drive delayed old save and status preserve destination dra
const pinnedWrites = (await logs(origin)).filter((x) =>
x.path === "/mutate"
);
assertEquals(pinnedWrites.map((x) => [x.nodeId, x.expectedRevision]), [
assertEquals(pinnedWrites.map((x) => [x.nodeId, x.expectedMutationId]), [
["3", "1"],
["4", "1"],
["3", "2"],
["4", "2"],
["3", pinnedWrites[0].requestId],
["4", pinnedWrites[1].requestId],
]);
assert(pinnedWrites.every((x) => typeof x.requestId === "string" && x.requestId.length > 0));
assertEquals(new Set(pinnedWrites.map((x) => x.requestId)).size, 4);
} finally {
release.release();
await handled;
@@ -47,13 +47,13 @@ Deno.test("browser fixture preserves decimal identity, bounded reads, binary and
assertEquals(escaped.text, "\u0000".repeat(65536));
assertEquals(escaped.truncated, false);
});
Deno.test("browser fixture two revisions conflict without committing the losing request", async () => {
Deno.test("browser fixture two last_mutation_ids conflict without committing the losing request", async () => {
const { client } = setup();
const entry = await client.metadata("home-owner", ref("home-owner", "3"));
await client.mutate("home-owner", "tab-one", {
operation: "update_text",
id: entry.entry,
expected_revision: entry.revision,
expected_mutation_id: entry.last_mutation_id,
text: "first actor",
content_type: "text/markdown",
});
@@ -62,7 +62,7 @@ Deno.test("browser fixture two revisions conflict without committing the losing
client.mutate("home-owner", "tab-two", {
operation: "update_text",
id: entry.entry,
expected_revision: entry.revision,
expected_mutation_id: entry.last_mutation_id,
text: "second actor draft",
content_type: "text/markdown",
}),
@@ -113,7 +113,7 @@ Deno.test("browser fixture rename and move retain identity while delete and recr
const moved = await client.mutate("home-owner", "move", {
operation: "relocate",
id: entry.entry,
expected_revision: entry.revision,
expected_mutation_id: entry.last_mutation_id,
parent: ref("home-owner", "2"),
name: "renamed.md",
});
@@ -122,7 +122,7 @@ Deno.test("browser fixture rename and move retain identity while delete and recr
await client.mutate("home-owner", "delete", {
operation: "delete",
id: entry.entry,
expected_revision: entry.revision,
expected_mutation_id: entry.last_mutation_id,
});
const replacement = await client.mutate("home-owner", "recreate", {
operation: "create_text",
+1 -1
View File
@@ -6,7 +6,7 @@ The ordinary Rust test exercises loopback HTTP signed Worker ingress: explicit r
The explicit process test starts a loopback Axum Server and launches Deno against it. `roundtrip.ts` imports the production `src/lib/workspace/drive/api.ts` adapter, not a copy or mock. Its typed `fetchFn` resolves relative paths only to the bound loopback origin and adds fixture bearer authorization. Tokens travel over stdin, not command-line arguments or logs; Rust failure diagnostics defensively redact both fixture bearer tokens. Deno has network permission only for the exact loopback port, uses no npm dependencies, and runs with `--cached-only`. Each listener is abort-on-drop, child processes are kill-on-drop, and the child has a 60-second upper timeout, without sleeps or readiness polling.
Coverage: Worker file list/read, Japanese folder/Markdown names, create/read/edit/download, empty files, actual PNG upload/authenticated image/download and binary update, publication receipt lookup, paged list, two authenticated accounts racing a common revision (one commit/one typed conflict, no hidden retry), stable latest URL through rename and move, deleted URL and same-name recreation, and unauthorized latest URLs. The exact Workers-page `drive-grants/api.ts` adapter also lists persisted grants, creates both access levels, revokes them and re-reads durable state, and rejects member administration. Its global browser fetch is temporarily origin/auth-bound in this isolated child; network responses are never mocked. This tests real authenticated HTTP with test bearer credentials, not a Passkey/cookie browser login or a full Runtime process. UI dirty-draft retention, delayed response handling, visual review and browser cancellation belong to the **separate browser fixture**, not this API test.
Coverage: Worker file list/read, Japanese folder/Markdown names, create/read/edit/download, empty files, actual PNG upload/authenticated image/download and binary update, publication receipt lookup, paged list, two authenticated accounts racing a common observed committed request (one commit/one typed conflict, no hidden retry), stable latest URL through rename and move, deleted URL and same-name recreation, and unauthorized latest URLs. The exact Workers-page `drive-grants/api.ts` adapter also lists persisted grants, creates both access levels, revokes them and re-reads durable state, and rejects member administration. Its global browser fetch is temporarily origin/auth-bound in this isolated child; network responses are never mocked. This tests real authenticated HTTP with test bearer credentials, not a Passkey/cookie browser login or a full Runtime process. UI dirty-draft retention, delayed response handling, visual review and browser cancellation belong to the **separate browser fixture**, not this API test.
## Registration
+13 -13
View File
@@ -93,9 +93,9 @@ async function download(
entry: DriveEntry,
): Promise<Uint8Array> {
const response = await fetchFn(
driveDownloadUrl(ws, entry.entry, entry.revision),
driveDownloadUrl(ws, entry.entry, entry.last_mutation_id),
);
assert(response.status === 200, "download must succeed at exact revision");
assert(response.status === 200, "download must succeed at exact last_mutation_id");
equal(
response.headers.get("x-content-type-options"),
"nosniff",
@@ -203,7 +203,7 @@ equal(
const updated = await saved(owner, ws, "web-edit", {
operation: "update_text",
id: markdown.entry,
expected_revision: markdown.revision,
expected_mutation_id: markdown.last_mutation_id,
text: "# 更新\n",
content_type: "text/markdown",
});
@@ -274,7 +274,7 @@ const binaryUpdated = await owner.upload(
{
operation: "update",
id: empty.entry,
expected_revision: empty.revision,
expected_mutation_id: empty.last_mutation_id,
content_type: "image/png",
},
);
@@ -313,12 +313,12 @@ equal(
"paged traversal must contain each file once",
);
// Two authenticated accounts read the same revision, then race real HTTP writes.
// Two authenticated accounts read the same last_mutation_id, then race real HTTP writes.
const secondRead = await second.readText(ws, updated.entry);
equal(
secondRead.entry.revision,
updated.revision,
"second actor must observe same revision",
secondRead.entry.last_mutation_id,
updated.last_mutation_id,
"second actor must observe same last_mutation_id",
);
const drafts = ["# actor A draft\n", "# actor B draft\n"];
const race = await Promise.allSettled(
@@ -326,7 +326,7 @@ const race = await Promise.allSettled(
saved(client, ws, `web-race-${index}`, {
operation: "update_text",
id: updated.entry,
expected_revision: updated.revision,
expected_mutation_id: updated.last_mutation_id,
text: drafts[index],
content_type: "text/markdown",
})
@@ -335,7 +335,7 @@ const race = await Promise.allSettled(
equal(
race.filter((result) => result.status === "fulfilled").length,
1,
"same-revision two-actor CAS must have exactly one winner",
"same-last_mutation_id two-actor CAS must have exactly one winner",
);
const loser = race.find((result) => result.status === "rejected");
assert(
@@ -358,14 +358,14 @@ const stableUrl = markdown.latest_url;
const renamed = await saved(owner, ws, "web-rename", {
operation: "relocate",
id: latest.entry.entry,
expected_revision: latest.entry.revision,
expected_mutation_id: latest.entry.last_mutation_id,
parent: folder.entry,
name: "改名.md",
});
const moved = await saved(owner, ws, "web-move", {
operation: "relocate",
id: renamed.entry,
expected_revision: renamed.revision,
expected_mutation_id: renamed.last_mutation_id,
parent: root.entry,
name: renamed.name,
});
@@ -397,7 +397,7 @@ await rejected(
const deleted = await owner.mutate(ws, "web-delete", {
operation: "delete",
id: moved.entry,
expected_revision: moved.revision,
expected_mutation_id: moved.last_mutation_id,
});
equal(deleted.entry, null, "delete must remove live metadata");
await rejected(() => owner.readText(ws, moved.entry), "not_found");
@@ -40,7 +40,7 @@ function reviewRequested(
event_id: eventId,
sequence: 1,
subject_ref: subjectRef,
ticket_item_revision: "ticket-revision",
ticket_content_digest: "ticket-digest",
ticket_merge_request_subjects: [
{ merge_request_id: "MR-1", subject_ref: subjectRef },
],
@@ -61,7 +61,7 @@ function review(
sequence: 1,
request_event_id: requestEventId,
subject_ref: subjectRef,
ticket_item_revision: "ticket-revision",
ticket_content_digest: "ticket-digest",
ticket_merge_request_subjects: [
{ merge_request_id: "MR-1", subject_ref: subjectRef },
],
@@ -37,7 +37,6 @@ const credential = {
name: "Deploy key",
public_key_algorithm: "ssh-ed25519",
public_key_fingerprint: "SHA256:credential",
current_revision: 2,
status: "active",
created_at: "2026-09-01T00:00:00Z",
rotated_at: null,
@@ -52,7 +51,6 @@ const hostTrust = {
key_algorithm: "ssh-ed25519",
host_key: "ssh-ed25519 AAAA",
fingerprint: "SHA256:host",
current_revision: 3,
created_at: "2026-09-01T00:00:00Z",
updated_at: "2026-09-02T00:00:00Z",
referenced_repositories: ["main"],
@@ -63,14 +61,12 @@ Deno.test("Repository Access parsers accept generated response contracts", () =>
assertEquals(
parseRepositorySshPublicKey({
credential_id: "deploy-key",
current_revision: 2,
public_key_algorithm: "ssh-ed25519",
public_key_fingerprint: "SHA256:credential",
public_key: "ssh-ed25519 AAAA",
}),
{
credential_id: "deploy-key",
current_revision: 2,
public_key_algorithm: "ssh-ed25519",
public_key_fingerprint: "SHA256:credential",
public_key: "ssh-ed25519 AAAA",
@@ -80,7 +76,6 @@ Deno.test("Repository Access parsers accept generated response contracts", () =>
assertEquals(
parseRepositoryAccessProjection({
workspace_id: "workspace-1",
config_revision: 4,
projection_digest: "sha256:projection",
bindings: [{
repository_key: "main",
@@ -91,7 +86,6 @@ Deno.test("Repository Access parsers accept generated response contracts", () =>
}),
{
workspace_id: "workspace-1",
config_revision: 4,
projection_digest: "sha256:projection",
bindings: [{
repository_key: "main",
@@ -115,10 +109,11 @@ Deno.test("Repository Access parsers reject malformed list responses", () => {
});
Deno.test("Repository Access parsers reject missing and wrong-typed fields", () => {
const { current_revision: _revision, ...missingRevision } = credential;
const { public_key_fingerprint: _fingerprint, ...missingFingerprint } =
credential;
assertSchemaError(
() => parseRepositorySshCredentials([missingRevision]),
"credentials[0].current_revision",
() => parseRepositorySshCredentials([missingFingerprint]),
"credentials[0].public_key_fingerprint",
);
assertSchemaError(
() => parseRepositorySshHostTrusts([{ ...hostTrust, port: "22" }]),
@@ -128,7 +123,6 @@ Deno.test("Repository Access parsers reject missing and wrong-typed fields", ()
() =>
parseRepositoryAccessProjection({
workspace_id: "workspace-1",
config_revision: 4,
projection_digest: "sha256:projection",
bindings: [{
repository_key: "main",
@@ -21,7 +21,6 @@ type LoadedPage = {
const projectionFixture = {
workspace_id: "workspace-1",
config_revision: 1,
projection_digest: "sha256:projection",
bindings: [],
};
@@ -31,7 +30,6 @@ const credentialFixture = {
name: "Workspace default",
public_key_algorithm: "ssh-ed25519",
public_key_fingerprint: "SHA256:credential",
current_revision: 1,
status: "active",
created_at: "2026-01-01T00:00:00Z",
rotated_at: null,
@@ -39,7 +37,6 @@ const credentialFixture = {
};
const publicKeyFixture = {
credential_id: "workspace-default",
current_revision: 1,
public_key_algorithm: "ssh-ed25519",
public_key_fingerprint: "SHA256:credential",
public_key: "ssh-ed25519 AAAA-workspace-default",
@@ -52,7 +49,6 @@ const hostTrustFixture = {
key_algorithm: "ssh-ed25519",
host_key: "ssh-ed25519 AAAA-host",
fingerprint: "SHA256:host",
current_revision: 1,
created_at: "2026-01-01T00:00:00Z",
updated_at: "2026-01-01T00:00:00Z",
referenced_repositories: [],
@@ -90,9 +90,6 @@ test("Repository Access presents current state as tables with technical disclosu
"Pinned SSH host keys",
"repository-access-table-wrap",
"Technical details",
"Credential revisions",
"Host key revisions",
"accessProjection.config_revision",
"accessProjection.projection_digest",
"binding.repository_key",
"binding.credential_id",
@@ -30,7 +30,7 @@ Deno.test("Repository SSH probe parser preserves the confirmation contract", ()
port: 22,
trust_state: "untrusted" as const,
host_trust_id: "tofu-example.test-22",
expected_host_trust_revision: null,
expected_host_key_fingerprint: null,
candidates: [
{
algorithm: "ssh-ed25519",
@@ -136,7 +136,7 @@ Deno.test("Repository SSH connection test requires an explicit host-key confirma
"selectedRuntimeId",
"candidate.fingerprint",
"Confirm and trust selected host key",
"expected_host_trust_revision",
"expected_host_key_fingerprint",
"requestConnectionTest('POST'",
"requestConnectionTest('PUT'",
]
@@ -138,7 +138,7 @@ Deno.test("sidebar Delete executes the authoritative runtime cleanup plan", asyn
workspace_id: "team",
runtime_id: worker.runtime_id,
generated_at: "2026-01-01T00:00:00Z",
revision: "7",
digest: "digest-7",
workers: [{
target_id: "worker-target",
@@ -171,7 +171,7 @@ Deno.test("sidebar Delete executes the authoritative runtime cleanup plan", asyn
workspace_id: "team",
runtime_id: worker.runtime_id,
generated_at: "2026-01-01T00:00:01Z",
revision: "8",
digest: "digest-8",
workers: [],
workdirs: [],
@@ -189,7 +189,7 @@ Deno.test("sidebar Delete executes the authoritative runtime cleanup plan", asyn
]);
assertEquals(requests[1]?.init?.method, "POST");
assertEquals(JSON.parse(String(requests[1]?.init?.body)), {
expected_plan_revision: "7",
expected_plan_digest: "digest-7",
worker_target_ids: ["worker-target"],
workdir_target_ids: [],
@@ -204,7 +204,7 @@ Deno.test("sidebar Delete reports cleanup-plan blocking reasons", async () => {
workspace_id: "team",
runtime_id: worker.runtime_id,
generated_at: "2026-01-01T00:00:00Z",
revision: "8",
digest: "digest-8",
workers: [{
target_id: "worker-target",
@@ -16,7 +16,7 @@ Deno.test("ticket detail synchronizes reused route data", () => {
const token of [
"data.ticketId",
"data.ticket.data",
"incomingTicket.item_revision",
"JSON.stringify([data.workspaceId, incomingTicketId, incomingTicket])",
"routeGeneration += 1",
"resetTicketView(incomingTicket)",
]
+79 -35
View File
@@ -5,7 +5,7 @@ declare const Deno: {
import {
createSubjektivSubject,
parseMemoryStagingListResponse,
parseSubjektivMemoryListRevisionsResponse,
parseSubjektivMemoryListChangesResponse,
parseSubjektivMemoryQueryResponse,
parseSubjektivMemoryReadResponse,
parseSubjektivResidentSurfaceResponse,
@@ -75,9 +75,8 @@ function subject(id = "subject-1") {
id,
role: "Release coordinator",
behavior_md: "Prefer explicit evidence.",
behavior_revision: 2,
state: "active",
store_revision: 12,
memory_fingerprint: "fingerprint-12",
created_at: "2026-09-01T00:00:00Z",
updated_at: "2026-09-02T00:00:00Z",
};
@@ -110,7 +109,7 @@ function currentWorker() {
function queryItem(id = "memory-1") {
return {
id,
revision: 3,
change_id: "change-current",
kind: "decision",
state: "active",
claim: "Keep provenance typed.",
@@ -127,12 +126,11 @@ function detailFixture() {
worker_id: "worker-1",
flow_selector: "builtin:coder-review",
flow_definition_id: "flow-1",
flow_definition_revision: 7,
};
return {
memory_id: "memory-1",
revision: 2,
current_revision: 3,
change_id: "change-earlier",
current_change_id: "change-current",
kind: "decision",
state: "resolved",
claim: "Keep provenance typed.",
@@ -173,13 +171,13 @@ function detailFixture() {
source_refs_total: 2,
source_refs_truncated: true,
}],
derived_from: [{ memory_id: "memory-parent", revision: 4 }],
derived_from: [{ memory_id: "memory-parent", change_id: "change-parent" }],
evidence_next_cursor: "evidence-page-2",
evidence_has_more: true,
};
}
Deno.test("Subject parsers enforce identity, exact enums, safe revisions, and bounds", () => {
Deno.test("Subject parsers enforce identity, exact enums, bounded fingerprints, and bounds", () => {
assertEquals(
parseSubjektivSubjectResponse(subject(), "subject-1"),
subject(),
@@ -196,9 +194,9 @@ Deno.test("Subject parsers enforce identity, exact enums, safe revisions, and bo
() =>
parseSubjektivSubjectResponse({
...subject(),
store_revision: Number.MAX_SAFE_INTEGER + 1,
memory_fingerprint: 12,
}),
"safe integer",
"bounded identifier",
);
assertThrows(
() => parseSubjektivSubjectResponse({ ...subject(), id: "x".repeat(513) }),
@@ -313,7 +311,7 @@ Deno.test("Subject behavior update uses CAS and preserves exact text", async ()
return Response.json({
...subject("subject/one"),
behavior_md: "Line one.\nLine two.",
behavior_revision: 3,
});
}) as typeof fetch;
@@ -321,9 +319,8 @@ Deno.test("Subject behavior update uses CAS and preserves exact text", async ()
fetchFn,
"workspace one",
"subject/one",
{ expected_behavior_revision: 2, behavior_md: "Line one.\nLine two." },
{ expected_behavior_md: "Prefer explicit evidence.", behavior_md: "Line one.\nLine two." },
);
assertEquals(updated.behavior_revision, 3);
assertEquals(updated.behavior_md, "Line one.\nLine two.");
assertEquals(
requests[0].path,
@@ -332,20 +329,20 @@ Deno.test("Subject behavior update uses CAS and preserves exact text", async ()
assertEquals(requests[0].init?.method, "PATCH");
assertEquals(
requests[0].init?.body,
'{"expected_behavior_revision":2,"behavior_md":"Line one.\\nLine two."}',
'{"expected_behavior_md":"Prefer explicit evidence.","behavior_md":"Line one.\\nLine two."}',
);
});
Deno.test("Subject behavior update reports conflicts without claiming Worker application", async () => {
const conflictFetch =
(async () =>
Response.json({ message: "revision conflict" }, {
Response.json({ message: "behavior content conflict" }, {
status: 409,
})) as typeof fetch;
await assertSubjectCreateRejects(
() =>
updateSubjektivSubjectBehavior(conflictFetch, "workspace", "subject", {
expected_behavior_revision: 1,
expected_behavior_md: "old",
behavior_md: "new",
}),
"rejected",
@@ -459,7 +456,7 @@ Deno.test("Resident surface parser accepts ready-empty and enforces snapshot inv
snapshot_id: "snapshot-empty",
body_md: "",
memory_refs: [],
built_from_store_revision: 0,
built_from_memory_fingerprint: "fingerprint-0",
created_at: "2026-09-02T00:00:00Z",
},
};
@@ -506,7 +503,7 @@ Deno.test("Resident surface parser accepts ready-empty and enforces snapshot inv
);
});
Deno.test("Current Memory list parser rejects unknown variants, unsafe integers, cursors, and oversized pages", () => {
Deno.test("Current Memory list parser rejects unknown variants, invalid change IDs, cursors, and oversized pages", () => {
const response = {
items: [queryItem()],
has_more: true,
@@ -533,9 +530,9 @@ Deno.test("Current Memory list parser rejects unknown variants, unsafe integers,
() =>
parseSubjektivMemoryQueryResponse({
...response,
items: [{ ...queryItem(), revision: 0 }],
items: [{ ...queryItem(), change_id: "" }],
}),
"positive safe integer",
"bounded identifier",
);
assertThrows(
() =>
@@ -572,7 +569,7 @@ Deno.test("Memory detail parser preserves body/evidence continuation, sources, o
);
assertEquals(parsed.derived_from, [{
memory_id: "memory-parent",
revision: 4,
change_id: "change-parent",
}]);
});
@@ -586,9 +583,9 @@ Deno.test("Memory detail parser fails closed on identity, variants, bounds, and
() =>
parseSubjektivMemoryReadResponse({
...fixture,
current_revision: Number.MAX_SAFE_INTEGER + 1,
current_change_id: 3,
}),
"safe integer",
"bounded identifier",
);
const missingBodyContinuation = structuredClone(fixture) as Record<
string,
@@ -637,26 +634,26 @@ Deno.test("Memory detail parser fails closed on identity, variants, bounds, and
);
});
Deno.test("Memory revision parser preserves immutable states and checks response identity", () => {
Deno.test("Memory change parser preserves immutable states and checks response identity", () => {
const response = {
memory_id: "memory-1",
current_revision: 3,
current_change_id: "change-current",
items: [{
revision: 3,
change_id: "change-current",
kind: "decision",
state: "active",
claim: "Current claim.",
change_reason: "Clarified wording.",
updated_at: "2026-09-03T00:00:00Z",
}, {
revision: 2,
change_id: "change-earlier",
kind: "decision",
state: "resolved",
claim: "Earlier claim.",
change_reason: "Resolved.",
updated_at: "2026-09-02T00:00:00Z",
}, {
revision: 1,
change_id: "change-original",
kind: "decision",
state: "retracted",
claim: "Original claim.",
@@ -666,16 +663,16 @@ Deno.test("Memory revision parser preserves immutable states and checks response
has_more: false,
};
assertEquals(
parseSubjektivMemoryListRevisionsResponse(response, "memory-1"),
parseSubjektivMemoryListChangesResponse(response, "memory-1"),
response,
);
assertThrows(
() => parseSubjektivMemoryListRevisionsResponse(response, "memory-2"),
() => parseSubjektivMemoryListChangesResponse(response, "memory-2"),
"identity does not match",
);
assertThrows(
() =>
parseSubjektivMemoryListRevisionsResponse({
parseSubjektivMemoryListChangesResponse({
...response,
items: [{ ...response.items[0], state: "archived" }],
}),
@@ -683,14 +680,61 @@ Deno.test("Memory revision parser preserves immutable states and checks response
);
assertThrows(
() =>
parseSubjektivMemoryListRevisionsResponse({
parseSubjektivMemoryListChangesResponse({
...response,
items: [{ ...response.items[0], revision: 4 }],
items: [response.items[0], response.items[0]],
}),
"future revision",
"Memory changes must be unique",
);
});
Deno.test("Memory changes preserve server order without interpreting opaque identities", () => {
const item = { kind: "decision", state: "active", claim: "Claim", change_reason: "Reason", updated_at: "2026-09-01T00:00:00Z" };
const response = {
memory_id: "memory-1",
current_change_id: "a/current",
items: [{ ...item, change_id: "z/earlier" }, { ...item, change_id: "a/current" }],
has_more: false,
};
assertEquals(parseSubjektivMemoryListChangesResponse(response), response);
assertEquals(parseSubjektivMemoryReadResponse({ ...detailFixture(), change_id: "z/earlier", current_change_id: "a/current" }).change_id, "z/earlier");
});
Deno.test("Memory identities reject numeric counters, empty and oversized strings, and legacy fields", () => {
for (const value of [3, "", "x".repeat(513)]) {
assertThrows(() => parseSubjektivMemoryReadResponse({ ...detailFixture(), change_id: value }), "bounded identifier");
assertThrows(() => parseSubjektivMemoryReadResponse({ ...detailFixture(), current_change_id: value }), "bounded identifier");
assertThrows(() => parseSubjektivSubjectResponse({ ...subject(), memory_fingerprint: value }), "bounded identifier");
}
assertThrows(() => parseSubjektivMemoryReadResponse({ ...detailFixture(), revision: 2 }), "unknown field");
assertThrows(() => parseSubjektivSubjectResponse({ ...subject(), behavior_revision: 2 }), "unknown field");
assertThrows(() => parseSubjektivSubjectResponse({ ...subject(), store_revision: 12 }), "unknown field");
});
Deno.test("Behavior CAS sends the observed text byte-for-byte including empty content", async () => {
for (const expected of ["", " Previous\nbehavior.\n"]) {
let body: unknown;
const fetchFn = (async (_path: string | URL | Request, init?: RequestInit) => {
body = JSON.parse(String(init?.body));
return Response.json({ ...subject(), behavior_md: "Replacement" });
}) as typeof fetch;
await updateSubjektivSubjectBehavior(fetchFn, "workspace", "subject-1", { expected_behavior_md: expected, behavior_md: "Replacement" });
assertEquals(body, { expected_behavior_md: expected, behavior_md: "Replacement" });
}
});
Deno.test("Behavior saves with mismatched response text have unknown outcome and are not retried", async () => {
let calls = 0;
const fetchFn = (async () => {
calls += 1;
return Response.json(subject());
}) as typeof fetch;
await assertSubjectCreateRejects(() => updateSubjektivSubjectBehavior(fetchFn, "workspace", "subject-1", {
expected_behavior_md: "Prefer explicit evidence.", behavior_md: "Replacement",
}), "unknown_outcome", "could not be confirmed");
assertEquals(calls, 1);
});
Deno.test("Legacy staging parser remains strict for deprecated compatibility", () => {
const origin = { kind: "worker_input", worker_id: "worker-1" };
const response = {
+10 -14
View File
@@ -44,8 +44,6 @@ const diagnostic = {
function profileSettingsFixture(): Record<string, unknown> {
return {
workspace_id: "workspace 1",
registry_revision: "config-source:7:tree:projection",
config_revision: 7,
tree_digest: "tree",
projection_digest: "projection",
default_profile: "workspace:coder",
@@ -68,7 +66,6 @@ function profileSettingsFixture(): Record<string, unknown> {
content_digest: "sha256:source",
provenance: "project_profile_source_tree",
editable: false,
revision: "config-source:7",
size_bytes: 128,
diagnostics: [],
}],
@@ -86,7 +83,7 @@ Deno.test("profile settings requests use scoped API and strictly validate respon
try {
const response = await fetchProfileSettings("workspace 1");
assertEquals(response.config_revision, 7);
assertEquals(response.tree_digest, "tree");
assertEquals(response.sources[0].provenance, "project_profile_source_tree");
assertEquals(requests.length, 1);
assertEquals(requests[0].url, "/api/w/workspace%201/settings/profiles");
@@ -103,7 +100,7 @@ Deno.test("workspace metadata requests use generated DTO shapes", async () => {
workspace_id: "workspace 1",
display_name: "Workspace",
created_at: "2026-01-01T00:00:00Z",
revision: "sha256:metadata",
updated_at: "2026-01-02T00:00:00Z",
source: "workspace-config",
diagnostics: [diagnostic],
};
@@ -118,13 +115,13 @@ Deno.test("workspace metadata requests use generated DTO shapes", async () => {
try {
assertEquals(
(await fetchWorkspaceMetadata("workspace 1")).revision,
"sha256:metadata",
(await fetchWorkspaceMetadata("workspace 1")).updated_at,
"2026-01-02T00:00:00Z",
);
assertEquals(
(await updateWorkspaceMetadata("workspace 1", {
display_name: "Renamed",
revision: "sha256:metadata",
expected_updated_at: "2026-01-02T00:00:00Z",
})).workspace.workspace_id,
"workspace 1",
);
@@ -135,7 +132,10 @@ Deno.test("workspace metadata requests use generated DTO shapes", async () => {
assertEquals(requests[1].init?.method, "PUT");
assertEquals(
requests[1].init?.body,
JSON.stringify({ display_name: "Renamed", revision: "sha256:metadata" }),
JSON.stringify({
display_name: "Renamed",
expected_updated_at: "2026-01-02T00:00:00Z",
}),
);
} finally {
globalThis.fetch = originalFetch;
@@ -152,7 +152,6 @@ Deno.test("Workspace signing identity requests use flat settings routes", async
workspace_id: "workspace 1",
key_id: "workspace-signing-key",
algorithm: "ed25519",
revision: 1,
state: "pending_provisioning",
created_at: "2026-01-01T00:00:00Z",
},
@@ -182,7 +181,7 @@ Deno.test("profile settings parser rejects missing, mistyped, stale, and invalid
);
const mistyped = profileSettingsFixture();
mistyped.config_revision = "7";
mistyped.tree_digest = 7;
assertThrows(
() => parseProfileSettingsResponse(mistyped),
ProfileApiError,
@@ -212,7 +211,6 @@ Deno.test("Workspace signing identity parser validates active and pending public
algorithm: "ed25519",
public_key: "public-key",
public_key_fingerprint: "sha256:fingerprint",
revision: 1,
state: "active",
created_at: "2026-01-01T00:00:00Z",
provisioned_at: "2026-01-01T00:00:00Z",
@@ -224,7 +222,6 @@ Deno.test("Workspace signing identity parser validates active and pending public
algorithm: "ed25519",
public_key: "public-key",
public_key_fingerprint: "sha256:fingerprint",
revision: 1,
},
};
assertEquals(
@@ -237,7 +234,6 @@ Deno.test("Workspace signing identity parser validates active and pending public
workspace_id: "workspace-1",
key_id: "WK-1",
algorithm: "ed25519",
revision: 1,
state: "pending_provisioning",
created_at: "2026-01-01T00:00:00Z",
},
@@ -13,7 +13,6 @@ const repository = {
kind: "git",
provider: "git",
source: { kind: "local_path", uri: "/srv/main" },
source_revision: 1,
source_fingerprint: "sha256:main",
observed_status: "ready",
record_authority: "workspace-control-plane",

Some files were not shown because too many files have changed in this diff Show More